文章目录
https://mp.weixin.qq.com/s?__biz=MzAxODQxMDM0Mw==&mid=2247484523&idx=1&sn=8c403eeb9bcc01db1b1207fa74dadbd1&source=41#wechat_redirect
// 二叉树遍历框架
def traverse(root):
if root is None: return
# 前序遍历代码写在这
traverse(root.left)
# 中序遍历代码写在这
traverse(root.right)
# 后序遍历代码写在这
// N 叉树遍历框架
def traverse(root):
if root is None: return
for child in root.children:
# 前序遍历代码写在这
traverse(child)
# 后序遍历代码写在这
回溯算法框架
"""
choiceList:当前可以进行的选择列表
track:可以理解为决策路径,即已经做出一系列选择
answer:用来储存我们的符合条件决策路径
"""
def backtrack(choiceList, track, answer):
if track is OK:
answer.add(track)
else:
for choice in choiceList:
# choose:选择一个 choice 加入 track
backtrack(choices, track, answer)
# unchoose:从 track 中撤销上面的选择
可以理解为,回溯算法相当于一个决策过程,递归地遍历一棵决策树,穷举所有的决策,同时把符合条件的决策挑出来。
unchoose过程是为了能遍历完choiceList中的所有选择
全排序
https://leetcode-cn.com/problems/permutations/
public List<List<Integer>> permute(int[] nums) {
List<List<Integer>> ans = new ArrayList<>();
List<Integer> track = new ArrayList<>();
backtrack(nums,track,ans);
return ans;
}
private void backtrack(int[] nums, List<Integer> track, List<List<Integer>> ans){
// 把符合条件的决策路径挑出来
if(track.size()==nums.length){
ans.add(new ArrayList<>(track));
} else{
for(int i=0; i<nums.length; i++){
// choose过程
// 决策路径中已经存在的元素不能再选择
if(track.contains(nums[i])) continue;
track.add(nums[i]); // 加入决策路径
// 进入下一步决策
backtrack(nums, track, ans);
// unchoose过程
track.remove(track.size()-1);
}
}
}
要是不进行撤回操作,输出结果是 [[1,2,3],[1,3,2],[3,1,2],[3,2,1],[1,2,3],[1,3,2]]
复杂度分析
递归树的复杂度都是这样分析:总时间 = 递归树的节点总数 × 每个递归节点需要的时间
。
全排列问题,节点总数等于 n + n*(n-1) + n*(n-1) (n-2) … * 1!,总之不超过 O(n*n!)。
对于 Java 代码的那个解法,处理每个节点需要 O(n) 的时间,因为 track.contains(nums[i]) 这个操作要扫描数组。
所以全排列问题总时间不超过 O(n^2 * n!)。
可见,回溯算法的复杂度是极其高的,甚至比指数级还高,因为树形结构注定了复杂度爆炸的结局。
N皇后问题
https://leetcode-cn.com/problems/n-queens/
class Solution {
public List<List<String>> solveNQueens(int n) {
char[][] board = new char[n][n];
//初始化数组
for (int i = 0; i < n; i++)
for (int j = 0; j < n; j++)
board[i][j] = '.';
List<List<String>> ans = new ArrayList<>();
backtrack(0,board,ans,n);
return ans;
}
private void backtrack(
int row, char[][] board, List<List<String>> ans, int n
){
if(row == n){
ans.add(construct(board));
} else{
// 每一行可以放置一个皇后
for(int col=0; col<n; col++){
// 如果该位置的皇后会被攻击,则跳过
if(!isValid(board,row,col,n)) continue;
// choose
board[row][col]='Q';
// 进入下一步决策,即下一行的选择
backtrack(row+1,board,ans,n);
// unchoose
board[row][col]='.';
}
}
}
// 判断 board[row][col] 是否可以放置Q
private boolean isValid(char[][] board, int row,int col,int n){
// 检查正上方
for(int i=0;i<n;i++){
if (board[i][col]=='Q') return false;
}
// 检查右斜上方
for(int i=row-1,j=col+1; i>=0 && j<n; i--,j++){
if (board[i][j]=='Q') return false;
}
// 检查左斜上方
for(int i=row-1,j=col-1; i>=0 && j>=0; i--,j--){
if (board[i][j]=='Q') return false;
}
// 下方没有放置皇后,不用检查
return true;
}
//把数组转为list
private List<String> construct(char[][] chess) {
List<String> path = new ArrayList<>();
for (int i = 0; i < chess.length; i++) {
path.add(new String(chess[i]));
}
return path;
}
}
注意: 棋盘board的构造方式,并转换成List <String>
复杂度分析
N 皇后问题,节点总数为 n + n^2 + n^3 + … + n^n,不超过 O(n^(n+1))。
处理每个节点需要向上扫描棋盘以免皇后互相攻击,需要 O(n) 时间。
所以 N 皇后问题总时间不超过 O(n^(n+2))。
剑指 Offer 12. 矩阵中的路径
https://leetcode-cn.com/problems/ju-zhen-zhong-de-lu-jing-lcof/
注意:路径可以从任意一格开始。可以上 下 左 右 移动,但是不能进入重复的格子。
class Solution {
public boolean exist(char[][] board, String word) {
// string转数组
char[] words = word.toCharArray();
// 回溯法
// 从board的[0][0]开始
for(int i=0;i<board.length;i++){
for(int j=0;j<board[0].length;j++){
if(backtrack(board,words,i,j,0)) return true;
}
}
return false;
}
// word用来判断决策路径是否满足条件
// board,i,j 共同决定当前选择决策路径 (= choiceList)
boolean backtrack(char[][] board, char[] word, int i, int j, int k) {
if(i >= board.length || i < 0 || j >= board[0].length || j < 0 || board[i][j] != word[k]) return false;
if(k == word.length - 1) return true;
board[i][j] = '\0'; //choose:选择一个 choice 加入 track
boolean res = backtrack(board, word, i + 1, j, k + 1) ||
backtrack(board, word, i - 1, j, k + 1) ||
backtrack(board, word, i, j + 1, k + 1) ||
backtrack(board, word, i , j - 1, k + 1);
board[i][j] = word[k]; //unchoose:撤销上面选择
return res;
}
}
剑指 Offer 13. 机器人的运动范围
https://leetcode-cn.com/problems/ji-qi-ren-de-yun-dong-fan-wei-lcof/
思路:使用前序遍历。
由于能够到达的格子,只由当前位置决定,与之前到达的格子无关。所以算不上回溯,即不用“撤销”,当前位置能够到达继续递归即可
该题与 剑指offer12 类似,但注意不同的是,只能从 [0,0] 开始移动,而且询问能够到达多少个格子。说明:1、之前已经到达过的格子没必要重复判断。2、机器人只需要在每步向右,向下移动即可
剑指 Offer 13. 机器人的运动范围( 回溯算法,DFS / BFS ,清晰图解)
方法一:DFS(前序)
1、使用类遍历 outcome 记录返回值
2、前序遍历:
1)当前位值 (i,j) 不能到达,直接 return:
超出边界,或已经遍历过,或当前位置不满足条件
if( i>m-1 || j>n-1 || visited[i][j] ||(bit(i)+bit(j))>k) return; // 这里又叫可行性剪枝
2)若当前位置能够访问,outcome+1,标记当前位置
outcome++;
visited[i][j]=true;
3)向右、或向下遍历
backtrack(visited,i+1,j,m,n,k);
backtrack(visited,i,j+1,m,n,k);
这里有个隐藏的优化:只进行向右和向下,没有对向上和向左进行搜索。原因是,可以推出:机器人可 仅通过向右和向下移动,访问所有可达解。
class Solution {
int outcome=0;
public int movingCount(int m, int n, int k) {
//临时变量visited记录格子是否被访问过
boolean[][] visited = new boolean[m][n];
backtrack(visited,0,0,m,n,k);
return outcome;
}
public void backtrack(boolean[][] visited, int i, int j, int m,int n, int k){
if( i>m-1 || j>n-1 || visited[i][j] ||(bit(i)+bit(j))>k) return;
outcome++;
visited[i][j]=true;
backtrack(visited,i+1,j,m,n,k);
backtrack(visited,i,j+1,m,n,k);
}
public int bit(int x) {
int y=0;
while(x>0){
y += x%10;
x=x/10;
}
return y;
}
}
方法二:使用层级遍历
二叉树中的回溯
模板
def traverse(root, track, answer):
if root is None: return
#1、先进行选择,将当前节点加入track,并进行相关操作
if track is OK: #2、判断做出选择后,当前路径是否是解,是的话加入 ans
answer.add(track)
traverse(root.left, track, answer) # 对 左右子节点进行处理,对 track 没有影响(最后一定会撤销),但是 ans 里保留了正确的路径
traverse(root.right, track, answer)
# 3. 进行到此,不论 当前选择不是正确的选择,从 track 中撤销 root
剑指34. 二叉树中和为某一值的路径
思路:先序遍历+回溯
先序遍历: 按照 “根、左、右” 的顺序,遍历树的所有节点。
路径记录: 在先序遍历中,记录从根节点到当前节点的路径。当路径为 ① 根节点到叶节点形成的路径 且 ② 各节点值的和等于目标值 sum 时,将此路径加入结果列表。
中止条件:root==null,返回
先序遍历+回溯
1、使用 LinkedList
class Solution {
LinkedList<List<Integer>> res = new LinkedList<>();
LinkedList<Integer> path = new LinkedList<>();
public List<List<Integer>> pathSum(TreeNode root, int sum) {
backtrack(root,path,res,sum);
return res;
}
public void backtrack(TreeNode root, LinkedList<Integer> path,LinkedList<List<Integer>> res, int tar){
if(root==null) return;
path.add(root.val);
tar -= root.val; // 做出选择
// 这里是做出了选择,再判断是否符合
if(tar == 0 && root.left == null && root.right == null)
res.add(new LinkedList(path));
backtrack(root.left,path,res, tar);
backtrack(root.right, path,res,tar);
path.removeLast();
}
}
2、使用 ArrayList
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
List<List<Integer>> res ;
List<Integer> path ;
public List<List<Integer>> pathSum(TreeNode root, int sum) {
res = new ArrayList<>();
path = new ArrayList<>();
backtrack(root,sum);
return res;
}
public void backtrack(TreeNode root, int tar){
if(root==null) return;
path.add(root.val);
tar -= root.val; // 做出选择
// 这里是做出了选择,再判断是否符合
if(tar == 0 && root.left == null && root.right == null)
res.add(new ArrayList(path));
backtrack(root.left, tar);
backtrack(root.right, tar);
path.remove(path.size()-1); //回溯
tar += root.val;
}
}
字符串中的回溯/其实也是排列
剑指38. 字符串的排列
方法一:回溯(递归字符串位置)
思路:递归字符串的位置 x,则可选择的字符串为 str[i],i=x ~ n
做出选择后,把 str[x]与 str[i] 交换,表示做出选择。(这样,x+1 之后的字符又是递归 x+1 位置的可用字符)
剪枝:由于排列顺序不能重复,保证 “每种字符只在某位置 x 固定一次” 即可
返回条件:由于对位置进行递归,所以 x=len时 return
附·:使用 for 循环,所以其实循环的次数是一定的
图解过程:
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class Solution {
char[] charArr;
List<String> res; // 储存结果
public String[] permutation(String s) {
if (s.length() == 0) {
return new String[0];
}
// 转换成字符数组是常见的做法
charArr = s.toCharArray();
//使用动态数组
res = new ArrayList<>();
dfs(0); // 0 表示处理第0位,即递归处理 第0、1、2...len-1位
// 记得转成字符串数组
return res.toArray(new String[0]);
}
//递归处理 第0、1、2...len-1位
private void dfs(int x) {
if (x == charArr.length) { //表示处理到最后一位已经处理完,x=len,加入结果列表res,并返回
// res.add(new String(charArr)); // 生成新的字符串
res.add(String.valueOf(charArr)); // 生成新的字符串
return;
}
// 处理第x位时,使用 set 集合记录该位置选择过的字符
// 每个位置只能选一种字符。剪枝,题目呀求不能重复
HashSet<Character> set=new HashSet<>();
for (int i = x; i < charArr.length; i++) {
if(set.contains(charArr[i])) continue; //charArr[i]字符在该位置已经选过,跳出本次循环
// 否则,进行选择
set.add(charArr[i]);
swap(x,i); // 交换charArr[x]与 charArr[i]
dfs(x+1); // 处理下一位置
swap(i,x); // 撤销
// 撤销选择不用对set进行操作。set本来就是用于剪枝的
}
}
private void swap(int x,int y){
char tmp=charArr[x];
charArr[x]=charArr[y];
charArr[y]=tmp;
}
}
可以重点看看下部分代码
HashSet<Character> set=new HashSet<>();
for (int i = x; i < charArr.length; i++) {
if(set.contains(charArr[i])) continue; //charArr[i]字符在该位置已经选过,跳出本次循环
// 否则,进行选择
set.add(charArr[i]);
swap(x,i); // 交换charArr[x]与 charArr[i]
dfs(x+1); // 处理下一位置
swap(i,x); // 撤销
// 撤销选择不用对set进行操作。set本来就是用于剪枝的
方法二:官方有个不需要回溯的方法
17. 电话号码的字母组合
思路:与剑指offer 38 比较相似,只不过不需要进行剪枝
递归结果字符串的位置 x,则可选择的字符串为 数字映射的字符串
做出选择后:及时把映射中的某个字符加入path
剪枝:无
返回条件:由于对位置进行递归,所以 x=len时 return (一定要return)
附·:使用 for 循环,所以其实循环的次数是一定的
方法一:回溯
首先使用哈希表存储每个数字对应的所有可能的字母,然后进行回溯操作。
class Solution {
Map<Character, String> phoneMap = new HashMap<Character, String>() {{
put('2', "abc"); // 使用双括号法初始化
put('3', "def");
put('4', "ghi");
put('5', "jkl");
put('6', "mno");
put('7', "pqrs");
put('8', "tuv");
put('9', "wxyz");
}};
List<String> ans;
StringBuffer path;
public List<String> letterCombinations(String digits) {
if(digits.length()==0) return new ArrayList<>();
ans=new ArrayList<>();
path=new StringBuffer();
//写一个回溯函数
// 按照结果位置进行递归
traverse(digits,0);
return ans;
}
// 表示处理 第x位
public void traverse(String digits,int x ){
if(x==digits.length()) {
ans.add(path.toString());
return;
}
char ch=digits.charAt(x);
String str =phoneMap.get(ch);
for(int i=0; i<str.length(); i++){
path.append(str.charAt(i)); //加入路径
traverse(digits,x+1); //进行下一步选择
path.deleteCharAt(path.length()-1); //撤回操作
}
}
}
组合中的回溯
39. 组合总和
方法一:回溯
思路:这是个组合问题。balabala 下次再总结
class Solution {
List<List<Integer>> ans;
List<Integer> tmp;
public List<List<Integer>> combinationSum(int[] candidates, int target) {
//回溯算法
//数字可以无限制重复被选取
ans=new ArrayList<>();
tmp=new ArrayList<>();
dfs(candidates,target,0);
return ans;
}
public void dfs(int[] candidates, int target, int start){
if(target==0){
ans.add(new ArrayList(tmp));
}else if(target<0) return; //
for(int i=start;i<candidates.length;i++){ //注意i从start开使,这样才能保证组合不重复,即按顺序 做选择
//选择一个加入路径
tmp.add(candidates[i]);
target -= candidates[i];
//元素可以重复,只需要i不加1就行
dfs(candidates, target, i);
tmp.remove(tmp.size()-1);
target += candidates[i];
}
}
}