BFS 算法解题套路框架 :: labuladong的算法小抄 #990
Replies: 64 comments 13 replies
-
我觉得用先进先出的队列存储要遍历的节点,就是BFS;用先进后出的栈来存储即将遍历的节点就是DFS; BFS可以用集合,需要在遍历当前层级的节点时,把下层节点存储到临时集合变量中,保证先处理当前层节点,队列本身就能做到,所以不用临时变量。 回溯法是DFS,是因为它用函数递归的方法,相当于是用函数栈存储的节点信息,转换成迭代法就是自己用个栈结构存储节点就行了; 双向BFS不只要知道目标在哪里,是不是还必须有逆向路径,比如用链表实现二叉树,知道目标是那个节点,想知道从根到它的路径,如果是双向链表,就可以从目标往根找,否则只能从根往目标找。 不知理解的对不对。 非常感谢作者,你把特殊的算法个例进行了一般化,变成了各种框架,和编程中把特例需求一般化,解决了一般化的问题就解决了所有的特列是一个道理。我能有兴趣研究算法真是多亏了你。 |
Beta Was this translation helpful? Give feedback.
-
@tinyhare 很高兴能帮到你,你理解的很透彻,BFS 和 DFS(回溯)本质上都是暴力穷举算法,就是实现形式上的不同导致特点不同。很多算法本质都类似,所以只要能够掌握框架,算法并没有多难的,加油~ |
Beta Was this translation helpful? Give feedback.
-
感谢作者大大! |
Beta Was this translation helpful? Give feedback.
-
我有一个问题,既然已经记录在visited中的字符串不会再次被记录在 q1 or q2 中,那么q1和q2怎么会有交集呢? |
Beta Was this translation helpful? Give feedback.
-
Python版本的双向bfs,ac100% class Solution(object):
def openLock(self, deadends, target):
"""
:type deadends: List[str]
:type target: str
:rtype: int
"""
visited = set(deadends)
if '0000' in visited:
return -1
q1 = set(['0000'])
q2 = set([target])
step = 0
while q1 and q2:
tmp = set()
for curr in q1:
if curr in q2:
return step
if curr in visited:
continue
visited.add(curr)
for i in range(4):
for d in (-1, 1):
next = curr[:i] + str((int(curr[i]) + d) % 10) + curr[i + 1:]
tmp.add(next)
step += 1
q1 = q2
q2 = tmp
return -1 |
Beta Was this translation helpful? Give feedback.
-
@yzw-Joey 代码中temp保存的是下一轮的所有节点,visited只保存了当前轮的所有节点,所以temp转为q2,q2里面的节点并没有存入visited中,判断q1和q2是否有交集是可行的。 |
Beta Was this translation helpful? Give feedback.
-
@yzw-Joey |
Beta Was this translation helpful? Give feedback.
-
合并后的代码如下: class Solution {
public int openLock(String[] deadends, String target) {
Set<String> visited = new HashSet<>();
for(String s:deadends) visited.add(s);
Queue<String> q = new LinkedList<>();
int res = 0;
q.offer("0000");
while(!q.isEmpty()){
int sz = q.size();
for(int i=0; i<sz; i++){
String cur = q.poll();
if(visited.contains(cur)){
continue;
}else{
visited.add(cur);
}
if(target.equals(cur)) return res;
for(int j=0; j<4; j++){
String up = plusOne(cur,j);
if(!visited.contains(up)) q.offer(up);
String down = minusOne(cur,j);
if(!visited.contains(down)) q.offer(down);
}
}
res++;
}
return -1;
}
private String plusOne(String s, int i){
char[] c = s.toCharArray();
if(c[i]=='9') c[i] = '0';
else c[i]++;
return new String(c);
}
private String minusOne(String s, int i){
char[] c = s.toCharArray();
if(c[i]=='0') c[i] = '9';
else c[i]--;
return new String(c);
}
} |
Beta Was this translation helpful? Give feedback.
-
请问对于从queue里移除元素的q.poll(), q.remove()和在queue里加元素的q.offer(), q.add()的选择对于BFS来说有什么讲究吗。我看模版里用的是q.poll()和q.offer(),是另一个组用法有什么地方不好吗,还是两种用法在BFS的题里没有区别。谢谢了 :D |
Beta Was this translation helpful? Give feedback.
-
@YANLING-H 没啥区别,Java 的队列 API 罢了,你查下语言相关的知识就知道了。 |
Beta Was this translation helpful? Give feedback.
-
@YANLING-H 对于面试没区别,工业上会有应用。
|
Beta Was this translation helpful? Give feedback.
This comment was marked as spam.
This comment was marked as spam.
-
js 版二叉树最小高度 function minDepth(root) {
if (root == null) return 0;
const q = [];
q.push(root);
// root 本身就是一层,depth 初始化为 1
let depth = 1;
while (q.length) {
const len = q.length;
/* 将当前队列中的所有节点向四周扩散 */
for (let i = 0; i < len; i++) {
const cur = q.shift();
/* 判断是否到达终点 */
if (cur.left == null && cur.right == null) return depth;
/* 将 cur 的相邻节点加入队列 */
if (cur.left !== null) q.push(cur.left);
if (cur.right !== null) q.push(cur.right);
}
/* 这里增加步数 */
depth++;
}
return depth;
} |
Beta Was this translation helpful? Give feedback.
-
js 版开锁次数 var openLock = function(deadends, target) {
// 将 s[j] 向上拨动一次
function plusOne(s, j) {
let replacement = s[j] === "9" ? 0 : parseInt(s[j]) + 1;
return s.substr(0, j) + replacement + s.substr(j + 1);
}
// 将 s[j] 向下拨动一次
function minusOne(s, j) {
let replacement = s[j] === "0" ? 9 : parseInt(s[j]) - 1;
return s.substr(0, j) + replacement + s.substr(j + 1);
}
// BFS 框架,打印出所有可能的密码
function BFS() {
// 记录已经穷举过的密码,防止走回头路
const visited = {
"0000": true,
};
// 从起点开始启动广度优先搜索
const q = ["0000"];
let step = 0;
while (q.length) {
const len = q.length;
/* 将当前队列中的所有节点向周围扩散 */
for (i = 0; i < len; i++) {
const cur = q.shift();
/* 判断死亡数字 */
if (deadends.includes(cur)) continue;
/* 判断是否到达终点 */
if (cur === target) return step;
/* 将一个节点的相邻节点加入队列 */
for (j = 0; j < 4; j++) {
up = plusOne(cur, j);
if (visited[up] === undefined) {
q.push(up);
visited[up] = true;
}
down = minusOne(cur, j);
if (visited[down] === undefined) {
q.push(down);
visited[down] = true;
}
}
}
/* 在这里增加步数 */
step++;
}
return -1;
}
return BFS();
}; |
Beta Was this translation helpful? Give feedback.
-
使用PHP解题时遇到的坑:判断当前节点是否存在于visited和deadends时一开始使用的是in_array函数,这样会拖慢速度,后来看到大佬的题解,将visited和deadends存成map,然后判断当前节点作为map的键是否存在相应的值 |
Beta Was this translation helpful? Give feedback.
-
东哥,开锁的那个case请教一个问题: BFS只能获取到要走多少步步,有没有办法获取到具体是走了哪几步到目的地的? 多谢 |
Beta Was this translation helpful? Give feedback.
-
js 版本的开锁次数 var openLock = function (deadends, target) {
const visited = new Set(deadends) // 合并 deadends 至 visited
let step = 0
const q = []
const changePwd = (pwd, pos, offset) => {
const arr = pwd.split('').map((val) => parseInt(val))
arr[pos] = (arr[pos] + offset + 10) % 10 // +1 和 -1 的拨动方向
return arr.join('')
}
const pushQueue = (pwd) => {
if (!visited.has(pwd)) {
q.push(pwd)
visited.add(pwd)
}
}
pushQueue('0000')
while (q.length !== 0) {
const len = q.length
for (let i = 0; i < len; i++) {
const pwd = q.shift()
if (pwd === target) return step
for (let j = 0; j < 4; j++) {
for (let offset of [-1, 1]) {
const newPwd = changePwd(pwd, j, offset)
pushQueue(newPwd)
}
}
}
step++
}
return -1
} |
Beta Was this translation helpful? Give feedback.
-
我的理解dfs和bfs还有个很大的区别就是dfs有能力从下层往上层带回来信息,而bfs就不可能。所以遇到需要叶节点信息的遍历的题就不能用bfs,只能用dfs。 |
Beta Was this translation helpful? Give feedback.
-
2022.10.29 打卡 |
Beta Was this translation helpful? Give feedback.
-
第一个二叉树的最小深度,可以使用前面讲二叉树章节的技巧,在root前面加一个节点,以省去边界条件的判断 |
Beta Was this translation helpful? Give feedback.
-
#2022-11-13 done |
Beta Was this translation helpful? Give feedback.
-
minDepth 这道题错了,还要return 还要再+1 return level+1; |
Beta Was this translation helpful? Give feedback.
-
第二例题解开密码锁的最少次数,作者最后一段结论有问题,或者说这段结论容易让人产生误解。
- if (deads.has(cur)) continue;
+ if (visited.has(cur)) continue; 这样做会导致程序 |
Beta Was this translation helpful? Give feedback.
-
回头路没理解,为啥会回去啊,先进先出,不是一层一层的嘛 |
Beta Was this translation helpful? Give feedback.
-
Leetcode 111 二叉树最小深度 最后一句 return depth 应该可以省略了 def minDepth(self, root: Optional[TreeNode]) -> int:
if root is None:
return 0
queue = deque([root])
depth = 1
while queue:
# for loop to pop all nodes at the same level
for i in range(len(queue)):
node = queue.popleft()
if node.left is None and node.right is None:
return depth
if node.left:
queue.append(node.left)
if node.right:
queue.append(node.right)
depth += 1 |
Beta Was this translation helpful? Give feedback.
-
752 题 密码锁 char_plus_one = str((int(password) + 1) % 10)
char_minus_one = str((int(password) - 1) % 10) |
Beta Was this translation helpful? Give feedback.
-
C++代码 int openLock(vector<string>& deads, string target) {
return BFS(deads, target);
}
int BFS(vector<string>& deads, string target) {
unordered_set<string> visited(deads.begin(), deads.end());
if (visited.count("0000")) return -1;
queue<string> que;
que.push("0000");
visited.insert("0000");
int step = 0;
while (!que.empty()) {
int size = que.size();
for (int i = 0; i < size; ++i) {
string cur = que.front(); que.pop();
if (cur == target) return step;
for (int j = 0; j < 4; ++j) {
string up = upOne(cur, j);
if (!visited.count(up)) {
que.push(up);
visited.insert(up);
}
string down = downOne(cur, j);
if (!visited.count(down)) {
que.push(down);
visited.insert(down);
}
}
}
++step;
}
return -1;
}
string upOne(string s, int n) {
s[n] == '9' ? s[n] = '0' : ++s[n];
return s;
}
string downOne(string s, int n) {
s[n] == '0' ? s[n] = '9' : --s[n];
return s;
}
|
Beta Was this translation helpful? Give feedback.
-
猜密码那题用JavaScript写,如果visited和dead不用set,只用array,会超时。不知道为什么,理论上set和array查找都是logn |
Beta Was this translation helpful? Give feedback.
-
这一篇文章也写得特别好,逻辑清晰,读完之后,醍醐灌顶 |
Beta Was this translation helpful? Give feedback.
-
// openLock 尝试解开一个由四个滚动数字组成的锁。deadends 是无法访问的密码组合,target 是目标密码。
// 函数返回到达目标密码所需的最少步数,如果无法到达,则返回 -1。
func openLock(deadends []string, target string) int {
// visit 存储已访问过的密码以及死亡密码
visit := make(map[string]bool)
// 初始化 deadends 到 visit 集合中
for _, str := range deadends {
visit[str] = true
}
// 如果起始密码 "0000" 已在 deadends 中,无法开始,直接返回 -1
if visit["0000"] {
return -1
}
// 初始化队列,并将起始密码 "0000" 加入队列和 visit 集合
queue := []string{"0000"}
visit["0000"] = true
count := 0
// 使用广度优先搜索解锁过程
for len(queue) != 0 {
length := len(queue)
// 遍历当前队列中的所有密码
for i := 0; i < length; i++ {
cur := queue[0]
queue = queue[1:]
// 检查当前密码是否为目标密码
if cur == target {
return count
}
// 尝试每个轮盘的每个方向
for j := 0; j < 4; j++ {
up := plusOne(cur, j)
// 如果新密码未被访问过,则加入队列
if _, ok := visit[up]; !ok {
queue = append(queue, up)
visit[up] = true
}
down := minusOne(cur, j)
// 同样检查下拨情况
if _, ok := visit[down]; !ok {
queue = append(queue, down)
visit[down] = true
}
}
}
// 每完成一层,步数增加
count++
}
// 如果无法到达目标,返回 -1
return -1
}
// plusOne 模拟在指定位置将数字加一,如果是9则回到0
func plusOne(str string, i int) string {
arr := []byte(str)
if arr[i] == '9' {
arr[i] = '0'
} else {
arr[i]++
}
return string(arr)
}
// minusOne 模拟在指定位置将数字减一,如果是0则跳到9
func minusOne(str string, i int) string {
arr := []byte(str)
if arr[i] == '0' {
arr[i] = '9'
} else {
arr[i]--
}
return string(arr)
} |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
文章链接点这里:BFS 算法解题套路框架
评论礼仪 见这里,违者直接拉黑。
Beta Was this translation helpful? Give feedback.
All reactions