下面是一些常见的回溯算法的题目,
1 二叉树的最大深度
两个思路: 思路 1 进行后序遍历 终止条件: 当 root 为空,说明已越过叶节点,因此返回深度 000 。
递推工作: 本质上是对树做后序遍历。
计算节点 root 的左子树的深度,即调用 maxDepth (root. Left)。
计算节点 root 的右子树的深度,即调用 maxDepth (root. Right)。
返回值: 返回此树的深度,即 max (maxDepth (root. Left), maxDepth (root. Right)) + 1。
思路 2 可以进行层序遍历
特例处理: 当 root 为空,直接返回深度 000 。
初始化: 队列 queue (加入根节点 root ),计数器 res = 0。
循环遍历: 当 queue 为空时跳出。
初始化一个空列表 tmp ,用于临时存储下一层节点。
遍历队列: 遍历 queue 中的各节点 node ,并将其左子节点和右子节点加入 tmp。
更新队列: 执行 queue = tmp ,将下一层节点赋值给 queue。
统计层数: 执行 res += 1 ,代表层数加 1。
返回值: 返回 res 即可。
class Solution {
public int maxDepth(TreeNode root) {
//dfs进行遍历,每到一层的 count+1,
// if(root==null) return 0;
// return Math.max(maxDepth(root.left),maxDepth(root.right))+1;
if (root == null) return 0;
List<TreeNode> queue = new LinkedList<>();
List<TreeNode> tmp;
int res = 0;
queue.add(root);
while (!queue.isEmpty()) {
tmp=new LinkedList<>();//用于临时存储下一层节点。
for(TreeNode node:queue){//将这一层左子节点和右子节点加入 tmp。
if (node.left != null)
tmp.add(node.left);
if (node.right != null)
tmp.add(node.right);
}
queue=tmp;
res++;
}
return res;
}
}
2 路径总和
思路: 先序遍历: 按照 “根、左、右” 的顺序,遍历树的所有节点。
路径记录: 在先序遍历中,记录从根节点到当前节点的路径。当路径满足 (1) 根节点到叶节点形成的路径且 (2) 各节点值的和等于目标值 targetSum 时,将此路径加入结果列表。
- 递推参数:root 和 cur 目标值
- 终止条件:
/**
* Def启后自动记r a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
LinkedList<List<Integer>> res=new LinkedList<>();
LinkedList <Integer>path=new LinkedList<>();
public List<List<Integer>> pathSum(TreeNode root, int targetSum) {
recur(root,targetSum)
return res;
}
void recur(TreeNode root,int tar){
if(root==null) return ;
ptah.add(root.val);
tar=tar-root.val;
if(tar==0&&root.left==null&&root.right==null){
res.add(new LinkedList<Integer>(path));//这里需要使用的path的拷贝,因为path会变化的我们需要不好变动的
}
recur(root.left,tar);
recur(root.right,tar);
path.removeLast();//向上回溯前需要将当前节点从路径 path 中删除,即执行 path.pop(),这个的意思实际上是将数据
//当前指针跳到自己的父节点进行剩下的操作
}
}
3 全排列
思路:
1.终止条件:当 x len(nums)-1时,代表所有位已固定(最后一位只有1种情况),则将当前组合 nums 转化为数组并加入 res,并返回。
2.递推参数:当前固定位x
3.递推工作:将第×位元素与i E[x,len(nums)]元素分别交换,并进入下层递归。
a.固定元素:将元素nums [i]和nums [x]交换,即固定nums[i】为当前位元素。
b.开启下层递归:调用dfs(× 1),即开始固定第× 1个元素。
C.还原交换:将元素nums [i]和
nums [x]交换(还原之前的交换)。
class Solution {
List<Integer>nums;
List<List<Integer>> res;
public List<List<Integer>> permute(int[] nums) {
//对于长度wein的数组,排列方案一共有n的阶乘个
//先固定第一位元素,然后第二位,一种到第n位
//递推参数是固定位x每一次,调整后的列表会一直循环到最后一位。才跳到下一次,从而实现n阶乘次循环。
this.res=new ArrayList<>();
this.nums=new ArrayList<>();
for(int num:nums){
this.nums.add(num);
}
dfs(0);
return res;
}
void swap(int a,int b){
//哈希表元素进行交换
int tmp=nums.get(a);
nums.set(a,nums.get(b));
nums.set(b,tmp);
}
void dfs(int x){
if(x==nums.size()-1){
res.add(new ArrayList<>(nums));// 添加具体的排列方案
return;//退出
}
for(int i=x;i<nums.size();i++){
swap(i,x);//交换将 nums[i]固定在x位置
dfs(x+1);// // 开启固定第 x + 1 位元素
swap(i,x);//回复交换,这个的部分是回溯的一部分
}
}
}
4 全排列 2
这一题是上一题的改进,实际上实现了一个不重复元素的全排列,在上面的代码对于固定位置 x 进行交换的时候,如果相同的话直接跳出这次交换就可以 下面是其代码,其实就是 for 循环当中,如果是已经存在啦就跳过这次操作就可以啦
class Solution {
List<Integer> nums;
List<List<Integer>> res;
void swap(int a,int b){
int tmp= nums.get(a);
nums.set(a,nums.get(b));
nums.set(b,tmp);
}
void dfs(int x){
if(x==nums.size()){
res.add(new ArrayList<>(nums));
return;
}
HashSet<Integer>set=new HashSet<>();
for(int i=x;i<nums.size();i++){
if(set.contains(nums.get(i)))
continue;
set.add(nums.get(i));
swap(i,x);
dfs(x+1);
swap(i,x);
}
}
public List<List<Integer>> permuteUnique(int[] nums) {
this.res=new ArrayList<>();
this.nums=new ArrayList<>();
for(int num:nums){
this.nums.add(num);
}
dfs(0);
return res;
}
}
5 组合总和
上一题是排列,这一题是组合,数字可以无限选取,其实还是 dfs 的思路
在开启搜索前,先将数组 nums 排序。在遍历所有选择时,当子集和超过 target 时直接结束循环,因为后边的元素更大,其子集和都一定会超过 target 。 省去元素和变量 total,通过在 target 上执行减法来统计元素和,当 target 等于 000 时记录解。
剪枝策略,给定输入数组1,2,…,n,设搜索过程中的选择序列为1,2…,,则该选择序列需要满足 i<<…i,不满足该条件的选择序列都会造成重复,应当剪枝。
思路:
class Solution {
public List<List<Integer>> combinationSum(int[] candidates, int target) {
List<Integer>state=new ArrayList<>();//状态子集
Arrays.sort(candidates);//先对数组进行排序子集和超过了target,直接结束循环
int start=0;//起始点
List<List<Integer>> res=new ArrayList<>();//结果列表
backtrack(state,target,candidates, start,res);
return res;
}
void backtrack(List<Integer> state, int target, int[] choices, int start, List<List<Integer>> res) {
if(target==0){//子集和等于target的时候记录{
res.add(new ArrayList<>(state));
return;
}
// 遍历所有选择
for (int i = start; i < choices.length; i++) {
//for(int i= start;i<choices.length;i++){
if(target-choices[i]<0){
//当前组合不满足直接跳出
break;
}
state.add(choices[i]);
//下一轮旋转
backtrack(state,target-choices[i],choices,i,res);
//回退选择,上面的选择选完a选项了,我要开始选b选项了
state.remove(state.size()-1);
}
}
}
6 总数组合 2
这一题是上一题的改进,还是去重每个数字只能使用一次,因为数组元素与已经排序了所以只要数据在进行处理的时候,如果这个数和下个数相同直接跳过。
class Solution {
void backtrack(List<Integer> state, int target, int[] choices, int start, List<List<Integer>> res) {
// 子集和等于 target 时,记录解
if (target == 0) {
res.add(new ArrayList<>(state));
return;
}
// 遍历所有选择
// 剪枝二:从 start 开始遍历,避免生成重复子集
// 剪枝三:从 start 开始遍历,避免重复选择同一元素
for (int i = start; i < choices.length; i++) {
// 剪枝一:若子集和超过 target ,则直接结束循环
// 这是因为数组已排序,后边元素更大,子集和一定超过 target
if (target - choices[i] < 0) {
break;
}
// 剪枝四:如果该元素与左边元素相等,说明该搜索分支重复,直接跳过
if (i > start && choices[i] == choices[i - 1]) {
continue;
}
// 尝试:做出选择,更新 target, start
state.add(choices[i]);
// 进行下一轮选择
backtrack(state, target - choices[i], choices, i + 1, res);
// 回退:撤销选择,恢复到之前的状态
state.remove(state.size() - 1);
}
}
public List<List<Integer>> combinationSum2(int[] candidates, int target) {
List<Integer> state = new ArrayList<>(); // 状态(子集)
Arrays.sort(candidates); // 对 candidates 进行排序
int start = 0; // 遍历起始点
List<List<Integer>> res = new ArrayList<>(); // 结果列表(子集列表)
backtrack(state, target, candidates, start, res);
return res;
}
}
7 单词搜索
算法解析: ·递归参数:当前元素在矩阵board中的行列索引i和j,当前目标字符在word中的索引k。 ·终止条件: 1.返回fals:(1)行或列索引越界或(2)当前矩阵元素与目标字符不同或(3)当前矩阵元素已访问过((3)可合并至(2))。 2.返回true:k=len(word)-1,即字符串word已全部匹配。 ·递推工作: 1.标示记当前矩阵元素:将board[i][j】修改为空字符·,代表此元素已访问过,防止之后搜索时重复访问。 2.搜索下一单元格:朝当前元素的上、下、左、右四个方向开启下层递归,使用或连接(代表只需找到一条可行路径就直接返回,不再做后续DFS),并记录结果至rs。 3.还原当前矩阵元素:将board[i][j1元素还原至初始值,即word[k]。 ·返回值:返回布尔量 res,代表是否搜索到旧标字符串。
class Solution {
public boolean exist(char[][] board, String word) {
char[] words = word.toCharArray();
for(int i = 0; i < board.length; i++) {
for(int j = 0; j < board[0].length; j++) {
if (dfs(board, words, i, j, 0)) return true;
}
}
return false;
}
boolean dfs(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';//标记当前矩阵元素:将当前元素标记为空
boolean res = dfs(board, word, i + 1, j, k + 1) || dfs(board, word, i - 1, j, k + 1) ||
dfs(board, word, i, j + 1, k + 1) || dfs(board, word, i , j - 1, k + 1);
board[i][j] = word[k];
return res;
}
}