概述
-
回溯
- 采用试错的思想,尝试分步去解决问题,
- 分步过程中,若现有分步答案不能得到正确的答案,则将取消上一步或几步的计算,再通过其他的可能去分步寻求正确的答案
- 找到一个可能存在的正确答案
- 尝试过所有的分步后都没有找到答案,宣布此题无解
- 递归实现
- 又称暴力解法,搜索一个问题的所有解,通过深度优先搜索实现
-
深度优先搜索 dfs
- 用于遍历树和图
- 这个算法会尽可能深的搜索树的分支。当结点 v 的所在边都己被探寻过,搜索将 回溯 到发现结点 v 的那条边的起始结点。这一过程一直进行到已发现从源结点可达的所有结点为止。如果还存在未被发现的结点,则选择其中一个作为源结点并重复以上过程,整个进程反复进行直到所有结点都被访问为止。
-
理解
- 回溯算法和深度优先搜索算法都有不撞南墙不回头的意思,回溯强调深度搜索的用途,在深度搜索过程中不断尝试,寻求想要的结果。回溯强调搜索的合理性,深度搜索强调遍历的思想。
-
回溯与动态规划的区别
- 相同
- 分步骤求解
- 每一步有多个选择
- 不同
- 回溯强调所有解是什么,本质上就是一个遍历算法,时间复杂度较高
- 动态规划只需要求我们评估最优解是多少,最优解的对应的具体不做要求,因此很适合评估一个方案的效果
- 相同
-
49全排列之回溯经典例题
- 总结搜索的方法:按顺序枚举每一位可能出现的情况,已经选择的数字在 当前 要选择的数字中不能出现。按照这种策略搜索就能够做到 不重不漏。这样的思路,可以用一个树形结构表示。
- 每一个结点表示了求解全排列问题的不同的阶段,这些阶段通过变量的「不同的值」体现,这些变量的不同的值,称之为「状态」;
- 使用深度优先遍历有「回头」的过程,在「回头」以后, 状态变量需要设置成为和先前一样 ,因此在回到上一层结点的过程中,需要撤销上一次的选择,这个操作称之为「状态重置」;
- 深度优先遍历,借助系统栈空间,保存所需要的状态变量,在编码中只需要注意遍历到相应的结点的时候,状态变量的值是正确的,具体的做法是:往下走一层的时候,path 变量在尾部追加,而往回走的时候,需要撤销上一次的选择,也是在尾部操作,因此 path 变量是一个栈;
- 深度优先遍历通过「回溯」操作,实现了全局使用一份状态变量的效果
- 总结搜索的方法:按顺序枚举每一位可能出现的情况,已经选择的数字在 当前 要选择的数字中不能出现。按照这种策略搜索就能够做到 不重不漏。这样的思路,可以用一个树形结构表示。
//大佬
public class Solution {
public List<List<Integer>> permute(int[] nums) {
int len = nums.length;
// 使用一个动态数组保存所有可能的全排列
List<List<Integer>> res = new ArrayList<>();
if (len == 0) {
return res;
}
boolean[] used = new boolean[len];
List<Integer> path = new ArrayList<>();
dfs(nums, len, 0, path, used, res);
return res;
}
private void dfs(int[] nums, int len, int depth,
List<Integer> path, boolean[] used,
List<List<Integer>> res) {
if (depth == len) {
//res.add(path);出现全为空的列表
//变量 path 所指向的列表 在深度优先遍历的过程中只有一份 ,深度优先遍历完成以后,回到了根结点,成为空列表。在 Java 中,参数传递是 值传递,对象类型变量在传参的过程中,复制的是变量的地址。这些地址被添加到 res 变量,但实际上指向的是同一块内存地址,因此我们会看到 6 个空的列表对象。解决的方法很简单,在 res.add(path); 这里做一次拷贝即可。
res.add(new ArrayList<>(path);
return;
}
// 在非叶子结点处,产生不同的分支,这一操作的语义是:在还未选择的数中依次选择一个元素作为下一个位置的元素,这显然得通过一个循环实现。
for (int i = 0; i < len; i++) {
if (!used[i]) {
path.add(nums[i]);
used[i] = true;
dfs(nums, len, depth + 1, path, used, res);
// 注意:下面这两行代码发生 「回溯」,回溯发生在从 深层结点 回到 浅层结点 的过程,代码在形式上和递归之前是对称的
used[i] = false;
path.remove(path.size() - 1);
}
}
}
public static void main(String[] args) {
int[] nums = {1, 2, 3};
Solution solution = new Solution();
List<List<Integer>> lists = solution.permute(nums);
System.out.println(lists);
}
}
注意:深度优先搜索的for循环中,如果答案的列表是无序之分,若元素不得重复则下一次搜索从i+1开始,若元素可以重复则从i开始。若答案的列表有序之分则从0开始,但是要去除重复元素(方法一是用数组标记,回溯时记得重置。方法二是判断列表中是否已经有当前值,有则跳过本次搜索)
46全排列
//自己
class Solution {
List<List<Integer>> ans = new ArrayList<>();
int capacity;
public List<List<Integer>> permute(int[] nums) {
capacity=nums.length;
List<Integer> temp=new ArrayList<>();
if(capacity==0){
return ans;
}
dfs(nums,0,temp);
return ans;
}
void dfs(int [] nums,int begin,List<Integer> temp){
if(temp.size()==capacity){
ans.add(new ArrayList<>(temp));
return;
}
for(int i=begin;i<capacity;i++){
//去除重复的元素
if(temp.contains(nums[i])){
continue;
}
temp.add(nums[i]);
dfs(nums,begin,temp);
temp.remove(temp.size()-1);
}
}
}
39组合总和
- 思路分析
- 根据示例 1:输入:
candidates = [2, 3, 6, 7],target = 7。 - 候选数组里有
2,如果找到了组合总和为7 - 2 = 5的所有组合,再在之前加上2,就是7的所有组合; - 同理考虑
3,如果找到了组合总和为7 - 3 = 4的所有组合,再在之前加上3,就是7的所有组合,依次这样找下去。 - 这一类问题都需要先画出树形图,然后编码实现。编码通过 深度优先遍历 实现
- 这样得到的答案带有重复的组合,因此去重,观察该树可知道,第二层开始遍历的节点从当前节点开始。
- 根据示例 1:输入:
class Solution {
List<List<Integer>> ans=new ArrayList<>();
public List<List<Integer>> combinationSum(int[] candidates, int target) {
int len=candidates.length;
if(len==0){
return ans;
}
dfs(candidates,0,len,target,new ArrayList<>());
return ans;
}
void dfs(int[] candidates,int begin,int len,int target,List<Integer> route){
if(target<0){
return;
}
if(target==0){
ans.add(new ArrayList<>(route));
return;
}
for(int i=begin;i<len;i++){
route.add(candidates[i]);
//当前下标开始
dfs(candidates,i,len,target-candidates[i],route);
route.remove(route.size()-1);
}
}
}
78子集
class Solution {
List<List<Integer>> ans=new ArrayList<>();
int len=0;
public List<List<Integer>> subsets(int[] nums) {
len=nums.length;
List<Integer> temp=new ArrayList<>();
dfs(nums,0,temp);
return ans;
}
private void dfs(int[] nums,int begin,List<Integer> temp){
ans.add(new ArrayList<>(temp));
for(int i=begin;i<len;i++){
temp.add(nums[i]);
dfs(nums,i+1,temp);
temp.remove(temp.size()-1);
}
}
}
子集2(数层去重)
class Solution {
List<List<Integer>> ans = new ArrayList<>();
List<Integer> temp = new ArrayList<>();
int len;
//用以树层去重的boolean数组
Boolean[] used;
public List<List<Integer>> subsetsWithDup(int[] nums) {
len=nums.length;
Arrays.sort(nums);
used=new Boolean[len];
dfs(len,0,nums);
return ans;
}
void dfs(int len,int begin,int[] nums){
ans.add(new ArrayList<>(temp));
for(int i=begin;i<len;i++){
if(i>0 && nums[i]==nums[i-1] && !used[i-1]){
continue;
}
temp.add(nums[i]);
used[i]=true;
dfs(len,i+1,nums);
temp.remove(temp.size()-1);
used[i]=false;
}
}
}
全排列2(树层去重+树枝去重)
class Solution {
int len;
List<List<Integer>> ans=new ArrayList<>();
List<Integer> path=new ArrayList<>();
boolean[] used;
public List<List<Integer>> permuteUnique(int[] nums) {
Arrays.sort(nums);
len=nums.length;
used=new boolean[len];
dfs(nums);
return ans;
}
void dfs(int[] nums){
if(path.size()==len){
ans.add(new ArrayList<>(path));
return;
}
for(int i=0;i<len;i++){
// 树层 树枝
if((i>0 && nums[i]==nums[i-1] && !used[i-1]) || used[i]){
continue;
}
path.add(nums[i]);
used[i]=true;
dfs(nums);
used[i]=false;
path.remove(path.size()-1) ;
}
}
}
22括号生成(全排列)
回溯要明确DFS的三个要素,以及剪枝这两个部分。
DFS部分:
- 状态:括号不断叠加后的字符串,可能需要参量辅助记录有几个左括号,几个右括号
- 子状态:每一层的可选状态,两个,左括号,右括号
- 结束态:左括号用完,右括号用完
剪枝部分:
- 添加右括号不得超过左括号的数目
- 左括号最多只有n个
class Solution {
List<String> ans=new ArrayList<>();
int N;
public List<String> generateParenthesis(int n) {
StringBuilder sb = new StringBuilder();
N=n;
dfs(sb,0,0);
return ans;
}
void dfs(StringBuilder sb,int open,int close){
if(sb.length()==2*N){
ans.add(sb.toString());
return;
}
if(open<N){
sb.append("(");
dfs(sb,open+1,close);
sb.deleteCharAt(sb.length()-1);
}
if(open>close){
sb.append(")");
dfs(sb,open,close+1);
sb.deleteCharAt(sb.length()-1);
}
}
}
131分割回文串(动态规划(用以判断是否为回文字符串)+回溯)
class Solution {
List<List<String>> ans=new ArrayList<>();
int len;
public List<List<String>> partition(String s) {
len=s.length()-1;
char[] charArray=s.toCharArray();
List<String> path = new ArrayList<>();
// 预处理
// 状态:dp[i][j] 表示 s[i][j] 是否是回文
boolean[][] dp=new boolean[len+1][len+1];
// 状态转移方程:在 s[i] == s[j] 的时候,dp[i][j] 参考 dp[i + 1][j - 1]
for(int right=0;right<=len;right++){
for(int left=0;left<=right;left++){
if(charArray[left]==charArray[right] && (right-left<=2 || dp[left+1][right-1])){
dp[left][right]=true;
}
}
}
dfs(s,path,0,dp);
return ans;
}
//回溯
void dfs(String s,List<String> path,int start,boolean[][] dp){
if(start>len){
ans.add(new ArrayList<>(path));
return;
}
for(int i=start;i<=len;i++){
if(dp[start][i]){
path.add(s.substring(start,i+1));
dfs(s,path,i+1,dp);
path.remove(path.size()-1);
}
}
}
}
282. 给表达式添加运算符
class Solution {
List<String> ans;
int len;
int t;
String model;
public List<String> addOperators(String num, int target) {
t=target;
ans=new ArrayList<>();
len=num.length();
model=num;
dfs(0,0,0,"");
return ans;
}
void dfs(int n,long prev,long cur,String path){
if(n==len){
if(cur==t)
ans.add(path);
return;
}
//回溯
for(int i=n;i<len;i++){
//该题重点,前导0处理
if(i!=n && model.charAt(n)=='0')
break;
long next=Long.parseLong(model.substring(n,i+1));
if(n==0){
dfs(i+1,next,next,""+next);
}else{
dfs(i+1,next,cur+next,path+"+"+next);
dfs(i+1,-next,cur-next,path+"-"+next);
dfs(i+1,prev*next,cur-prev+prev*next,path+"*"+next);
}
}
}
}