127. 单词接龙

这一题的基本的单向BFS思路就是,从start单词开始,寻找其所有在wordList中的可行下一步,存入队列之后,按照BFS的思路一直找到最后一层。因此这种问题的主要复杂度开销来自于每一层的数量:image.png
解决办法就是两边同时BFS进行搜索:
image.png
那么为什么可以减少搜索空间呢?其实看图就知道了,乍一看可能需要并行程序才可以完成时间的优化,因为最终还是需要搜索整个空间的。但其实搜索整个空间是错误的,单向BFS是三角形的搜索,但是变为双向的,其实是变成了菱形的,也因此不是严格的节约了1/2.
伪码与思路:

  1. d1d2 为两个方向的队列
  2. m1m2 为两个方向的哈希表,记录每个节点距离起点的
  3. // 只有两个队列都不空,才有必要继续往下搜索
  4. // 如果其中一个队列空了,说明从某个方向搜到底都搜不到该方向的目标节点
  5. while(!d1.isEmpty() && !d2.isEmpty()) {
  6. if (d1.size() < d2.size()) {
  7. update(d1, m1, m2);
  8. } else {
  9. update(d2, m2, m1);
  10. }
  11. }
  12. // update 为将当前队列 d 中包含的元素取出,进行「一次完整扩展」的逻辑(按层拓展)
  13. void update(Deque d, Map cur, Map other) {}

使用Rust写,存在一个lifetime的问题:

impl Solution {
    /// 127. 单词接龙
    pub fn ladder_length(begin_word: String, end_word: String, word_list: Vec<String>) -> i32 {


        let set: HashSet<String> = HashSet::from_iter(word_list.into_iter());
        // println!("{:?}", set);
        if !set.contains(&end_word) { return 0; }
        let res = Solution::bfs(&set, begin_word, end_word);
        return if res == -1 { 0 }else { res + 1 }
    }
    /// 双向bfs的具体实现
    fn bfs(set: &HashSet<String>, start: String, end: String) -> i32 {
        // 对每一个String进行处理的包装,deque为当前方向的队列,cur为当前方向走过的路径,other为另一个方向
        fn update<'a>(deque: &mut VecDeque<&'a String>, cur: &mut HashMap<&'a String, i32>,
                      other: &HashMap<&'a String, i32>, set: &'a HashSet<String>) -> i32
        {
            println!("{:?}", deque);
            let mut m = deque.len();
            // 遍历这一层,取出每一个单词进行替换
            while m > 0 {
                let mut temp = deque.pop_front().unwrap();
                // 考虑该单词的每一个字符可能被替换的情况
                for i in 0..temp.len() {
                    for j in b'a'..=b'z' {
                        // 替换该位置的字母为26个字符中的一个
                        let mut next = (*temp).clone();
                        next.replace_range(i..=i, (j as char).to_string().as_str());
                        // println!("{}", next);
                        if set.contains(&next) {
                            println!("{} -> {};  ", temp, next);
                            // 1. 如果这个字符在当前方向被记录过,跳过,后半段是一个小剪枝
                            if cur.contains_key(&next)
                                && *cur.get(&next).unwrap() <= *cur.get(&temp).unwrap()
                            { continue }

                            // 2. 如果另一个方向的包含了当前的这个元素,那么就不用跳过了,直接返回
                            if other.contains_key(&next) {
                                // 需要多加1,不然少算了一步
                                return *cur.get(&temp).unwrap() + *other.get(&next).unwrap() + 1;
                            }else {
                                // 否则进入下一层的BFS
                                let next = set.get(&next).unwrap();
                                deque.push_back(next);
                                cur.insert(next, *cur.get(&temp).unwrap() + 1);
                            }
                        }
                    }
                }
                m -= 1;
            }
            -1
        }

        let (mut d1, mut d2) = (VecDeque::new(), VecDeque::new());
        let (mut m1, mut m2) = (HashMap::new(), HashMap::new());

        // 放入头结点,开始进行BFS
        d1.push_back(&start);
        m1.insert(&start, 0);
        d2.push_back(&end);
        m2.insert(&end, 0);
        // println!("{:?}", m1);
        // println!("{:?}", m2);

        while !d1.is_empty() && !d2.is_empty() {
            let mut t = -1;
            if d1.len() <= d2.len() {
                println!("[--->正向]");
                t = update(&mut d1, &mut m1, &m2, set);
                println!("[--->此时的t]{}\n", t);
            }else {
                println!("[--->反向]");
                t = update(&mut d2, &mut m2, &m1, set);
                println!("[--->此时的t]{}", t);
            }
            if t != -1 { return t; }
        }
        -1
    }
}

因为存储的只是存储在Set中的String,那么就存在两个问题:

  1. Deque以及HashMap中存储的引用的生命周期应该被约束为和函数生命周期以及set生命周期一样,因为存在一个问题,我们在函数作用域中修改了deque,并向其中添加了set中的String的引用,离开函数之后二者均还存在,但是我们需要保证,deque中的引用是可用的,也即deque中的引用不会在set中实际的String被释放之后还存在,因此deuqe中的String的生命周期和set的生命周期保持一致。map也是一样。
  2. 我们不能直接朝deque中添加next,因为next是一个函数范围内的变量,如果添加的话,就会出现悬垂指针。因此需要从set中找到next并获取他的引用。

    433. 最小基因变化

    先用双向BFS写一下: ```rust /// 433. 最小基因变化 双向BFS算法 pub fn min_mutation(start: String, end: String, bank: Vec) -> i32 { fn update<’a> (set: &’a HashSet>, deque: &mut VecDeque<&’a Vec>, items: &Vec,

                cur: &mut HashMap<&'a Vec<char>, i32>, other: &HashMap<&'a Vec<char>, i32>) -> i32
    

    {

     let mut len = deque.len();
     while len > 0 {
         let temp = deque.pop_front().unwrap();
         let step = *cur.get(temp).unwrap();
         // 每一个元素定长为8位
         for i in 0..8 {
             for c in &*items {
                 let mut next = temp.clone();
                 if next[i] == *c { continue; }
    
                 next[i] = *c;
                 if !set.contains(&next) || cur.contains_key(&next) {
                     continue;
                 }
                 if other.contains_key(&next) {
                     return step + *other.get(&next).unwrap() + 1;
                 }
                 let next = set.get(&next).unwrap();
                 cur.insert(next, step + 1);
                 deque.push_back(next);
             }
         }
    
         len -= 1;
     }
    
     -1
    

    }

let mut set = HashSet::new();
bank.into_iter().for_each(|x| {set.insert(x.chars().collect::<Vec<char>>());});

println!("{:?}", set);

let start = start.chars().collect::<Vec<char>>();
let end = end.chars().collect::<Vec<char>>();
if !set.contains(&end) { return -1; }

let items = vec!['A', 'G', 'C', 'T'];
let (mut d1, mut d2) = (VecDeque::new(), VecDeque::new());
let (mut m1, mut m2) = (HashMap::new(), HashMap::new());
d1.push_back(&start);
m1.insert(&start, 0);
d2.push_back(&end);
m2.insert(&end, 0);

while !d1.is_empty() && !d2.is_empty() {
    let mut t = -1;
    if d1.len() <= d2.len() {
        t = update(&set, &mut d1, &items, &mut m1, &m2);
    }else {
        t = update(&set, &mut d2, &items, &mut m2, &m1);
    }
    if t != -1 { return t; }
}
-1

}

<a name="htnmu"></a>
## 启发式搜索:AStar算法
首先我们定义了一个对象,该对象存储基因序列以及其到终点的**步数**:
```rust
/// 记录节点inner与终点之间的距离
#[derive(Debug, Eq)]
struct Node {
    pub inner: Vec<char>,
    val: i32
}
impl Node {
    fn new(s: &Vec<char>, end: &Vec<char>) -> Self {
        let mut val = 0;
        s.iter().zip(end.iter()).for_each(|x| {
            if *x.0 != *x.1 {
                val += *x.0 as i32 - *x.1 as i32;
            }
        });
        Self {
            inner: s.clone(),
            val
        }
    }
}


impl PartialEq<Self> for Node {
    fn eq(&self, other: &Self) -> bool {
        self.inner == other.inner && self.val == other.val
    }
}

impl PartialOrd<Self> for Node {
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
        Some(self.cmp(other))
    }
}

impl Ord for Node {
    fn cmp(&self, other: &Self) -> Ordering {
        self.val.cmp(&other.val)
    }
}

算法的逻辑在于,我们使用一个大根堆,这个堆存储Node,每一次堆顶都是离终点距离最的一个基因序列(不同的基因编号最多)。
因此算法逻辑为:
拿出一个距离end最远的序列temp,查找temp的所有可行下一步next,然后走过去:

  1. 如果next是终点的话,直接返回map维护的到start的距离
  2. 如果不是终点,并且map中没有记录其与Start的距离,或者距离过大,那么就更新,将next放入堆中,并记录其到start的距离

这个的逻辑也是BFS,也是从Start开始,然后遍历能够走到下一步的所有可能性。相比BFS的优化在于,使用了一个优先队列,将我们定义的基因序列到目的地的距离存储起来,每一次在查找的时候,BFS是不可定顺序的,但是基于堆我们可以每一次挑选距离end最近的节点进行选择,简化了代码。

pub fn min_mutation(start: String, end: String, bank: Vec<String>) -> i32 {
    let mut set = HashSet::new();
    bank.into_iter().for_each(|x| {set.insert(x.chars().collect::<Vec<char>>());});
    let start = start.chars().collect::<Vec<char>>();
    let end = end.chars().collect::<Vec<char>>();
    // 存储每一个节点,大根堆,离end最远的元素将被放入堆顶
    let mut p = BinaryHeap::new();
    p.push(Reverse(Node::new(&start, &end)));
    // map记录了这个元素到start的距离
    let mut map = HashMap::new();
    map.insert(&start, 0);

    let items = vec!['A', 'G', 'C', 'T'];
    while !p.is_empty() {
        // 每一步拿出距离最远的序列
        let node = p.pop().unwrap().0;
        let temp = node.inner;
        let step = *map.get(&temp).unwrap();
        // 查找其可以走到的序列
        for i in 0..8 {
            for c in &items {
                let mut next = temp.clone();
                if next[i] == *c { continue; }
                next[i] = *c;
                if !set.contains(&next) { continue; }

                // 在当前序列合法(可以走到的时候)
                // 1. 如果走到了终点,那么直接返回结果
                if next == end { return step + 1; }
                // 2. 没有走到终点,如果曾经没有走过这个序列,或者说走过但是距离要远一些,那么我就进行更新
                //    后面一部分算一个剪枝
                if !map.contains_key(&next) || *map.get(&next).unwrap() > step + 1 {
                    let next = set.get(&next).unwrap();
                    map.insert(next, step+1);
                    p.push(Reverse(Node::new(next, &end)));
                }
            }
        }
    }

    -1
}

建图+DFS

既然可以BFS,那就可以DFS,这个不细看了,没什么特别的

class Solution {
    int N = 15, M = 15 * 15 * 2 + 50, idx = 0, loc = 1;
    int[] he = new int[N], e = new int[M], ne = new int[M];
    int n, ans;
    void add(int a, int b) {
        e[idx] = b;
        ne[idx] = he[a];
        he[a] = idx++;
    }
    void dfs(int u, int fa, int depth) {
        if (depth >= ans) return ; // 最优解剪枝
        if (u == n) {
            ans = depth;
            return ;
        }
        for (int i = he[u]; i != -1; i = ne[i]) {
            int j = e[i];
            if (j == fa) continue;
            dfs(j, u, depth + 1);
        }
    }
    public int minMutation(String S, String T, String[] bank) {
        List<String> list = new ArrayList<>();
        list.add(S);
        boolean ok = false;
        for (String s : bank) {
            if (s.equals(S)) continue;
            if (s.equals(T)) {
                ok = true;
                continue;
            }
            list.add(s);
        }
        if (!ok) return -1;
        list.add(T);
        n = list.size();
        ans = n;
        Arrays.fill(he, -1);
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < n; j++) {
                if (i == j) continue;
                int cnt = 0;
                for (int k = 0; k < 8 && cnt <= 1; k++) {
                    if (list.get(i).charAt(k) != list.get(j).charAt(k)) cnt++;
                }
                if (cnt == 1) {
                    add(i + 1, j + 1); add(j + 1, i + 1);
                }
            }
        }
        dfs(1, -1, 0);
        return ans == n ? -1 : ans;
    }
}