广度优先搜索

1. 简介

BFS 的核心思想应该不难理解的,就是把一些问题抽象成图,从一个点开始,向四周开始扩散。一般来说,我们写 BFS 算法都是用「队列」这种数据结构,每次将一个节点周围的所有节点加入队列。

BFS 相对 DFS 的最主要的区别是:BFS 找到的路径一定是最短的,但代价就是空间复杂度可能比 DFS 大很多。

序号 题目 难度
1 752. 打开转盘锁 中等
2 127. 单词接龙 困难

BFS 框架模板如下:

// 计算从起点 start 到终点 target 的最近距离
int BFS(Node start, Node target) {
    Queue<Node> q; // 核心数据结构
    Set<Node> visited; // 避免走回头路
    
    q.offer(start); // 将起点加入队列
    visited.add(start);

    while (q not empty) {
        int sz = q.size();
        /* 将当前队列中的所有节点向四周扩散 */
        for (int i = 0; i < sz; i++) {
            Node cur = q.poll();
            /* 划重点:这里判断是否到达终点 */
            if (cur is target)
                return step;
            /* 将 cur 的相邻节点加入队列 */
            for (Node x : cur.adj()) {
                if (x not in visited) {
                    q.offer(x);
                    visited.add(x);
                }
            }
        }
    }
    // 如果走到这里,说明在图中没有找到目标节点
}

2. 应用

2.1. Leetcode 752. 打开转盘锁

2.1.1. 题目

752. 打开转盘锁

你有一个带有四个圆形拨轮的转盘锁。每个拨轮都有10个数字: '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' 。每个拨轮可以自由旋转:例如把 '9' 变为 '0','0' 变为 '9' 。每次旋转都只能旋转一个拨轮的一位数字。
锁的初始数字为 '0000' ,一个代表四个拨轮的数字的字符串。
列表 deadends 包含了一组死亡数字,一旦拨轮的数字和列表里的任何一个元素相同,这个锁将会被永久锁定,无法再被旋转。
字符串 target 代表可以解锁的数字,你需要给出解锁需要的最小旋转次数,如果无论如何不能解锁,返回 -1 。

示例 1:

输入:deadends = ["0201","0101","0102","1212","2002"], target = "0202"
输出:6
解释:
可能的移动序列为 "0000" -> "1000" -> "1100" -> "1200" -> "1201" -> "1202" -> "0202"。
注意 "0000" -> "0001" -> "0002" -> "0102" -> "0202" 这样的序列是不能解锁的,
因为当拨动到 "0102" 时这个锁就会被锁定。

2.1.2. 解题思路

使用广度优先搜索的思想,从起点开始枚举邻近的数字组合,枚举过程需要跳过死亡数字和已经访问过的数字,直到找到密码为止。

2.1.3. 代码实现

class Solution {
    private static final String START = "0000";
    private static final int SUCCESS = 0;
    private static final int FAIL = -1;

    public int openLock(String[] deadends, String target) {
        if (target.equals(START)) {
            return SUCCESS;
        }
        Set<String> deadNumbers = new HashSet<>(Arrays.asList(deadends));
        if (deadNumbers.contains(START)) {
            return FAIL;
        }

        Set<String> visit = new HashSet<>();
        Queue<String> queue = new LinkedList<>();
        queue.offer(START);
        visit.add(START);
        int step = 0;
        while (!queue.isEmpty()) {
            int size = queue.size();
            for (int i = 0; i < size; i++) {
                String currentNumber = queue.poll();
                for (String number : nextNumber(currentNumber)) {
                    if (visit.contains(number) || deadNumbers.contains(number)) {
                        continue;
                    }

                    if (target.equals(number)) {
                        return step + 1;
                    }
                    queue.offer(number);
                    visit.add(number);
                }
            }
            step += 1;
        }
        return -1;
    }

    private List<String> nextNumber(String number) {
        List<String> result = new ArrayList<>();
        for (int i = 0; i < number.length(); i++) {
            result.add(numberOperate(number.toCharArray(), i, 1));
            result.add(numberOperate(number.toCharArray(), i, -1));
        }
        return result;
    }

    private String numberOperate(char[] state, int i, int value) {
        int newVal = state[i] - '0' + value;
        if (newVal == 10) {
            state[i] = '0';
        } else if (newVal == -1) {
            state[i] = '9';
        } else {
            state[i] = (char) (newVal + '0');
        }
        return new String(state);
    }
}

2.2. Leetcode 127. 单词接龙

2.2.1. 题目

127. 单词接龙

字典 wordList 中从单词 beginWord 和 endWord 的 转换序列 是一个按下述规格形成的序列 beginWord -> s1 -> s2 -> ... -> sk:
每一对相邻的单词只差一个字母。
对于 1 <= i <= k 时,每个 si 都在 wordList 中。注意, beginWord 不需要在 wordList 中。
sk == endWord
给你两个单词 beginWord 和 endWord 和一个字典 wordList ,返回 从 beginWord 到 endWord 的 最短转换序列 中的 单词数目 。如果不存在这样的转换序列,返回 0 。

示例 1:

输入:beginWord = "hit", endWord = "cog", wordList = ["hot","dot","dog","lot","log","cog"]
输出:5
解释:一个最短转换序列是 "hit" -> "hot" -> "dot" -> "dog" -> "cog", 返回它的长度 5。

2.2.2. 解题思路

这里,我们可以使用广度优先搜索的思路,对于每一个单词,我们都可以通过找到它的邻近单词。

即对于当前单词的每一位,都可以将其替换为 \(a - z\) 范围内的其他字母,这样就找到了当前单词的邻近的单词。

2.2.3. 代码实现

class Solution {
    public int ladderLength(String beginWord, String endWord, List<String> wordList) {
        Set<String> canUseWords = new HashSet<>(wordList);
        Set<String> visit = new HashSet<>();
        Queue<String> queue = new LinkedList<>();

        queue.offer(beginWord);
        visit.add(beginWord);
        int step = 1;
        while (!queue.isEmpty()) {
            int size = queue.size();
            for (int i = 0; i < size; i++) {
                String candidate = queue.poll();
                for (String word : nextWord(candidate)) {
                    if (visit.contains(word) || !canUseWords.contains(word)) {
                        continue;
                    }
                    if (word.equals(endWord)) {
                        return step + 1;
                    }
                    queue.offer(word);
                    visit.add(word);
                }
            }
            step++;
        }
        return 0;
    }

    private List<String> nextWord(String word) {
        List<String> result = new ArrayList<>();
        for (int i = 0; i < word.length(); i++) {
            for (char j = 'a'; j <= 'z'; j++) {
                if (j == word.charAt(i)) {
                    continue;
                }
                char[] status = word.toCharArray();
                status[i] = j;
                result.add(new String(status));
            }
        }
        return result;
    }
}

参考:

posted @ 2024-01-22 21:32  LARRY1024  阅读(11)  评论(0编辑  收藏  举报