笔记
二进制最右边的1 :error&(~error+1)
有符号数右移是用符号位填补
把一个整数减去1,再和原来的整数做与运算,会整数最右边的1变成0
二分:
left=0,right=nums.length的时候 while(left<right)退出的时候left=right,[left,right)
left=0,right=nums.length-1的时候 while(left<=right)退出的时候 left=right+1 [right+1,right]合理
其实回溯算法和我们常说的 DFS 算法非常类似,本质上就是一种暴力穷举算法。回溯算法和 DFS 算法的细微差别是:回溯算法是在遍历「树枝」,DFS 算法是在遍历「节点」,本文就是简单提一下,等你看到后文 图论算法基础 时就能深刻理解这句话的含义了。
废话不多说,直接上回溯算法框架,解决一个回溯问题,实际上就是一个决策树的遍历过程,站在回溯树的一个节点上,你只需要思考 3 个问题:
1、路径:也就是已经做出的选择。
2、选择列表:也就是你当前可以做的选择。
3、结束条件:也就是到达决策树底层,无法再做选择的条件。
result = [] def backtrack(路径, 选择列表): if 满足结束条件: result.add(路径) return for 选择 in 选择列表: #做选择
将该选择从选择列表移除
路径.add(选择) backtrack(路径, 选择列表) #撤销选择
路径.remove(选择)
将该选择再加入选择列表
回溯算法的决策树
void traverse(TreeNode root) { for (TreeNode child : root.childern) { // 前序位置需要的操作 traverse(child); // 后序位置需要的操作 } }
PS:细心的读者肯定会疑问:多叉树 DFS 遍历框架的前序位置和后序位置应该在 for 循环外面,并不应该是在 for 循环里面呀?为什么在回溯算法中跑到 for 循环里面了?
是的,DFS 算法的前序和后序位置应该在 for 循环外面,不过回溯算法和 DFS 算法略有不同
前序遍历的代码在进入某一个节点之前的那个时间点执行,后序遍历代码在离开某个节点之后的那个时间点执行。
我们只要在递归之前做出选择,在递归之后撤销刚才的选择,就能正确得到每个节点的选择列表和路径。
图的遍历
多叉树:
/* 多叉树遍历框架 */ void traverse(TreeNode root) { if (root == null) return; // 前序位置 for (TreeNode child : root.children) { traverse(child); } // 后序位置 }
图和多叉树最大的区别是,图是可能包含环的,你从图的某一个节点开始遍历,有可能走了一圈又回到这个节点,而树不会出现这种情况,从某个节点出发必然走到叶子节点,绝不可能回到它自身。
所以,如果图包含环,遍历框架就要一个 visited
数组进行辅助:
// 记录被遍历过的节点 boolean[] visited; // 记录从起点到当前节点的路径 boolean[] onPath; /* 图遍历框架 */ void traverse(Graph graph, int s) { if (visited[s]) return; // 经过节点 s,标记为已遍历 visited[s] = true; // 做选择:标记节点 s 在路径上 onPath[s] = true; for (int neighbor : graph.neighbors(s)) { traverse(graph, neighbor); } // 撤销选择:节点 s 离开路径 onPath[s] = false; }
类比贪吃蛇游戏,visited
记录蛇经过过的格子,而 onPath
仅仅记录蛇身。在图的遍历过程中,onPath
用于判断是否成环,类比当贪吃蛇自己咬到自己(成环)的场景,这下你可以理解它们二者的区别了吧。
如果让你处理路径相关的问题,这个 onPath
变量是肯定会被用到的,比如 拓扑排序 中就有运用。
另外,你应该注意到了,这个 onPath
数组的操作很像前文 回溯算法核心套路 中做「做选择」和「撤销选择」,区别在于位置:回溯算法的「做选择」和「撤销选择」在 for 循环里面,而对 onPath
数组的操作在 for 循环外面。
// DFS 算法,关注点在节点 void traverse(TreeNode root) { if (root == null) return; printf("进入节点 %s", root); for (TreeNode child : root.children) { traverse(child); } printf("离开节点 %s", root); } // 回溯算法,关注点在树枝 void backtrack(TreeNode root) { if (root == null) return; for (TreeNode child : root.children) { // 做选择 printf("从 %s 到 %s", root, child); backtrack(child); // 撤销选择 printf("从 %s 到 %s", child, root); } }
如果执行这段代码,你会发现根节点被漏掉了:
void traverse(TreeNode root) { if (root == null) return; for (TreeNode child : root.children) { printf("进入节点 %s", child); traverse(child); printf("离开节点 %s", child); } }
所以对于这里「图」的遍历,我们应该用 DFS 算法,即把 onPath
的操作放到 for 循环外面,否则会漏掉记录起始点的遍历。
说了这么多 onPath
数组,再说下 visited
数组,其目的很明显了,由于图可能含有环,visited
数组就是防止递归重复遍历同一个节点进入死循环的。
当然,如果题目告诉你图中不含环,可以把 visited
数组都省掉,基本就是多叉树的遍历。
Java 的语言特性,因为 Java 函数参数传的是对象引用,所以向 res
中添加 path
时需要拷贝一个新的列表,否则最终 res
中的列表都是空的。
leetcode797
// 记录所有路径 List<List<Integer>> res = new LinkedList<>(); public List<List<Integer>> allPathsSourceTarget(int[][] graph) { // 维护递归过程中经过的路径 LinkedList<Integer> path = new LinkedList<>(); traverse(graph, 0, path); return res; } /* 图的遍历框架 */ void traverse(int[][] graph, int s, LinkedList<Integer> path) { // 添加节点 s 到路径 path.addLast(s); int n = graph.length; if (s == n - 1) { // 到达终点 res.add(new LinkedList<>(path));
//不return是因为后面会遍历邻接点(没有)并且remove选择递归结束 // 可以在这直接 return,但要 removeLast 正确维护 path // path.removeLast();//提前return要撤销选择 // return; // 不 return 也可以,因为图中不包含环,不会出现无限递归 } // 递归每个相邻节点 for (int v : graph[s]) { traverse(graph, v, path); } // 从路径移出节点 s path.removeLast(); }