递归算法与DFS类似,也与二叉树的先序遍历类似
以下摘自 leetcode回溯算法入门级详解
回溯法 采用试错的思想,它尝试分步的去解决一个问题。在分步解决问题的过程中,当它通过尝试发现现有的分步答案不能得到有效的正确的解答的时候,它将取消上一步甚至是上几步的计算,再通过其它的可能的分步解答再次尝试寻找问题的答案。
-
搜索与遍历:
我们每天使用的搜索引擎帮助我们在庞大的互联网上搜索信息。搜索引擎的「搜索」和「回溯搜索」算法里「搜索」的意思是一样的。
搜索问题的解,可以通过 遍历 实现。所以很多教程把回溯算法称为爆搜(暴力解法)。因此回溯算法用于 搜索一个问题的所有的解 ,通过深度优先遍历的思想实现。 -
与动态规划的区别:
共同点:用于求解多阶段决策问题。多阶段决策问题即:求解一个问题分为很多步骤(阶段);每一个步骤(阶段)可以有多种选择。
不同点:动态规划只需要求我们评估最优解是多少,最优解对应的具体解是什么并不要求。因此很适合应用于评估一个方案的效果;
回溯算法可以搜索得到所有的方案(当然包括最优解),但是本质上它是一种遍历算法,时间复杂度很高。
总结:
做题的时候,建议 先画树形图 ,画图能帮助我们想清楚递归结构,想清楚如何剪枝。拿题目中的示例,想一想人是怎么做的,一般这样下来,这棵递归树都不难画出。
说明:
每一个结点表示了求解全排列问题的不同的阶段,这些阶段通过变量的「不同的值」体现,这些变量的不同的值,称之为**「状态」;
使用深度优先遍历有「回头」的过程,在「回头」以后, 状态变量需要设置成为和先前一样 ,因此在回到上一层结点的过程中,需要撤销上一次的选择,这个操作称之为「状态重置」;
深度优先遍历,借助系统栈空间,保存所需要的状态变量,在编码中只需要注意遍历到相应的结点的时候,状态变量的值是正确的,具体的做法是:往下走一层的时候,path 变量在尾部追加,而往回走的时候,需要撤销上一次的选择,也是在尾部操作,因此 path 变量是一个栈;
深度优先遍历通过「回溯」操作,实现了全局使用一份状态变量**的效果。
使用编程的方法得到全排列,就是在这样的一个树形结构中完成 遍历,从树的根结点到叶子结点形成的路径就是其中一个全排列。
总结一下回溯dfs的一般写法:
void dfs(int index,path..){
if(index==n){
res.add(new String(path));//终止条件
return;
}
//接下来就是列出当前这一步里可走的选择范围
for(int i=0;i<n;i++){有的是整个数组范围,有的是特定范围,有的是选与不选的
//剪枝,增加一些条件限制比如
//有顺序要求
if(vis[i]||((i-1)>=0&&nums[i]==nums[i-1]&&!vis[i-1])) continue;//剪枝
path.add//添加到路径中
dfs//递归
path.remove//移除添加,回归原本状态
}
}
46. 全排列
给定一个不含重复数字的数组 nums ,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。
示例 1:
输入:nums = [1,2,3]
输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]
示例 2:
输入:nums = [0,1]
输出:[[0,1],[1,0]]
示例 3:
输入:nums = [1]
输出:[[1]]
以前我做这题时用的方法:
class Solution {
List<List<Integer>> list = new ArrayList<List<Integer>>();
void qp(List<Integer> a,int k,int m){ //k:当前位置 m:最后一个位置
if(k>m){
list.add(new ArrayList<Integer>(a));//新创建个list,拷贝a里的值,然后在add到list里
}else{
for(int i=k;i<=m;i++){
//t=a[k];a[k]=a[i];a[i]=t;
Collections.swap(a,k,i);
qp(a,k+1,m);
//t=a[k];a[k]=a[i];a[i]=t;
Collections.swap(a,k,i);
}
}
}
public List<List<Integer>> permute(int[] nums) {
int n=nums.length;
List<Integer> a = new ArrayList<Integer>();
for(int i=0;i<n;i++){
a.add(nums[i]);
}
qp(a,0,n-1);
return list;
}
}
按照递归的思想,做这题,代码如下:
class Solution {
List<List<Integer>> res = new ArrayList<List<Integer>>();
//与图的dfs遍历很像
int n;
void dfs(int depth,boolean[] vis,List<Integer> path,int[] nums){
if(depth==n){
res.add(new ArrayList<Integer>(path));
return;
}
for(int i=0;i<n;i++){
if(!vis[i]){//nums数组里的数还没用过
vis[i]=true;
path.add(nums[i]);//记录经过的点
dfs(depth+1,vis,path,nums);
//状态回溯
vis[i]=false;
path.remove(depth);
}
}
}
public List<List<Integer>> permute(int[] nums) {
n=nums.length;
if(n==0) return res;
List<Integer> path = new ArrayList<Integer>();
boolean[] vis=new boolean[n];
dfs(0,vis,path,nums);
return res;
}
}
47. 全排列 II
给定一个可包含重复数字的序列 nums ,按任意顺序 返回所有不重复的全排列。
示例 1:
输入:nums = [1,1,2]
输出:
[[1,1,2],
[1,2,1],
[2,1,1]]
示例 2:
输入:nums = [1,2,3]
输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]
添加剪枝条件:为了控制不重复,我们强制规定相同数字的顺序,即当nums[i]=nums[i-1]时,只有前一个被访问后一个相同点才能被访问【这么个顺序】(即:nums[i-1]被访问过才允许走nums[i])
class Solution {
int n;
List<List<Integer>> res = new ArrayList<List<Integer>>();
public List<List<Integer>> permuteUnique(int[] nums) {
//先排个序,让重复数字相邻
//为了控制不重复,我们强制规定相同数字的顺序,即当nums[i]=nums[i-1]只有nums[i-1]被访问过才允许走nums[i]
Arrays.sort(nums);
n=nums.length;
List<Integer> path = new ArrayList<Integer>();
boolean[] vis = new boolean[n];
dfs(0,vis,path,nums);
return res;
}
void dfs(int depth,boolean[] vis,List<Integer> path,int[] nums){
if(depth==n){
res.add(new ArrayList<Integer>(path));
return;
}
for(int i=0;i<n;i++){
if(vis[i]||((i-1)>=0&&nums[i]==nums[i-1]&&!vis[i-1])) continue;//剪枝
else{
vis[i]=true;
path.add(nums[i]);
dfs(depth+1,vis,path,nums);
//状态回溯
vis[i]=false;
path.remove(depth);
}
}
}
}
78. 子集
给你一个整数数组 nums ,数组中的元素 互不相同 。返回该数组所有可能的
子集(幂集)。解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。
示例 1:
输入:nums = [1,2,3]
输出:[[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]
示例 2:
输入:nums = [0]
输出:[[],[0]]
方法一:递归
以第三步,i=3时为例,将nums[i]与result数组前面的子集组合
class Solution {
int n;
public List<List<Integer>> subsets(int[] nums) {
List<List<Integer>> res = new ArrayList<List<Integer>>();
res.add(new ArrayList<Integer>());//先添加一个空的进去
n=nums.length;
for(int i=0;i<n;i++){
int m=res.size();
for(int j=0;j<m;j++){
ArrayList<Integer> temp = new ArrayList<Integer>(res.get(j));
temp.add(nums[i]);
res.add(temp);
}
}
return res;
}
}
方法二:回溯
关键在于:每个点是选或不选两个状态
全排列那个题,每个点事可以在n个点中选,所以有个for循环
class Solution {
int n;
List<List<Integer>> res = new ArrayList<List<Integer>>();
public List<List<Integer>> subsets(int[] nums) {
//1.回溯方法:重点在于每个点都有选与不选两个状态
//之前的depth代表小list中的位置(这个位置上的元素可以从nums数组中全取一遍),而这里代表走nums的哪了(只能往后取)
//2.迭代递归
n=nums.length;
ArrayList<Integer> path = new ArrayList<Integer>();
dfs(0,path,nums);
return res;
}
void dfs(int index,ArrayList<Integer> path,int[] nums){
if(index==n){
res.add(new ArrayList<Integer>(path));
return;
}
path.add(nums[index]);
dfs(index+1,path,nums);
path.remove(path.size()-1);//移除最后一个
dfs(index+1,path,nums);
}
}
90. 子集 II
给你一个整数数组 nums ,其中可能包含重复元素,请你返回该数组所有可能的
子集(幂集)。解集 不能 包含重复的子集。返回的解集中,子集可以按 任意顺序 排列。
示例 1:
输入:nums = [1,2,2]
输出:[[[1,2,2],[1,2],[1],[2,2],[2],[]]
示例 2:
输入:nums = [0]
输出:[[0],[]]
示例 3:
输入:nums = [1,1,1,1]
输出:[[1,1,1,1],[1,1,1],[1,1],[1],[]]
和上面全排列2题一样,控制一下重复字符的顺序。
关键在于:在顺序上,相同元素除了必须一起出现外,还可以一起不出现。
class Solution {
int n;
List<List<Integer>> res = new ArrayList<List<Integer>>();
public List<List<Integer>> subsetsWithDup(int[] nums) {
//固定重复字符的前后顺序
n=nums.length;
Arrays.sort(nums);//排序,让相同元素挨着
ArrayList<Integer> path = new ArrayList<Integer>();
boolean[] vis = new boolean[n];
dfs(0,path,vis,nums);
return res;
}
void dfs(int index,ArrayList<Integer> path,boolean[] vis,int[] nums){
if(index==n){
res.add(new ArrayList<Integer>(path));
return ;
}
if(index-1>=0&&nums[index]==nums[index-1]&&!vis[index-1]){//两者同时存在或同时不存在
vis[index]=false;
dfs(index+1,path,vis,nums);
return;
}
path.add(nums[index]);
vis[index]=true;
dfs(index+1,path,vis,nums);
path.remove(path.size()-1);
vis[index]=false;
dfs(index+1,path,vis,nums);
}
}
39. 组合总和
给你一个 无重复元素 的整数数组 candidates 和一个目标整数 target ,找出 candidates 中可以使数字和为目标数 target 的 所有 不同组合 ,并以列表形式返回。你可以按 任意顺序 返回这些组合。
candidates 中的 同一个 数字可以 无限制重复被选取 。如果至少一个数字的被选数量不同,则两种组合是不同的。
对于给定的输入,保证和为 target 的不同组合数少于 150 个。
示例 1:
输入:candidates = [2,3,6,7], target = 7
输出:[[2,2,3],[7]]
解释:
2 和 3 可以形成一组候选,2 + 2 + 3 = 7 。注意 2 可以使用多次。
7 也是一个候选, 7 = 7 。
仅有这两种组合。
示例 2:
输入: candidates = [2,3,5], target = 8
输出: [[2,2,2,2],[2,3,3],[3,5]]
示例 3:
输入: candidates = [2], target = 1
输出: []
一开始直接用上述类型题的思想去做,运行后发现没去重,用index标记到数组那个位置了,再从哪个位置往后找。
class Solution {
List<List<Integer>> res = new ArrayList<List<Integer>>();
int n;
public List<List<Integer>> combinationSum(int[] candidates, int target) {
n=candidates.length;
ArrayList<Integer> path = new ArrayList<Integer>();
dfs(0,0,path,candidates,target);
return res;
}
void dfs(int sum,int index,ArrayList<Integer> path,int[] candidates,int target){
if(sum==target){
res.add(new ArrayList<Integer>(path));
return ;
}
if(sum>target) return ;
for(int i=index;i<n;i++){
path.add(candidates[i]);
int m=sum+candidates[i];
dfs(m,i,path,candidates,target);
path.remove(path.size()-1);
}
}
}
17. 电话号码的字母组合
给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。
给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。
示例 1:
输入:digits = “23”
输出:[“ad”,“ae”,“af”,“bd”,“be”,“bf”,“cd”,“ce”,“cf”]
示例 2:
输入:digits = “”
输出:[]
示例 3:
输入:digits = “2”
输出:[“a”,“b”,“c”]
class Solution {
List<String> res = new ArrayList<String>();
public List<String> letterCombinations(String digits) {
HashMap<Integer,String> map = new HashMap<Integer,String>();
map.put(2,"abc");map.put(3,"def");map.put(4,"ghi");
map.put(5,"jkl");map.put(6,"mno");map.put(7,"pqrs");
map.put(8,"tuv");map.put(9,"wxyz");
StringBuilder path = new StringBuilder();
if(digits.length()==0) return res;
dfs(0,path,digits.length(),map,digits);
return res;
}
void dfs(int index,StringBuilder path,int n,HashMap<Integer,String> map,String digits){
if(index==n){//终止条件
res.add(new String(path));
return;
}
//接下来就是列出当前这一步里可走的范围
String str=map.get(digits.charAt(index)-'0');//把拿到的字符变成数字
for(int i=0;i<str.length();i++){//数字对应的一串字母
path.append(str.charAt(i));
dfs(index+1,path,n,map,digits);
path.deleteCharAt(path.length()-1);
}
}
}
131. 分割回文串
给你一个字符串 s,请你将 s 分割成一些子串,使每个子串都是 回文串 。返回 s 所有可能的分割方案。
示例 1:
输入:s = “aab”
输出:[[“a”,“a”,“b”],[“aa”,“b”]]
示例 2:
输入:s = “a”
输出:[[“a”]]
class Solution {
List<List<String>> res = new ArrayList<List<String>>();
public List<List<String>> partition(String s) {
ArrayList<String> path = new ArrayList<String>();
dfs(0,path,s);
return res;
}
boolean huiwen(String str){//判断回文
StringBuilder sb = new StringBuilder(str);
if(str.equals(sb.reverse().toString())) return true;
else return false;
}
void dfs(int index,ArrayList<String> path,String s){
if(index==s.length()){
res.add(new ArrayList<String>(path));
return;
}
for(int i=index;i<s.length();i++){
String str1 = s.substring(index,i+1);
if(!huiwen(str1)) continue;
path.add(str1);
String str2 = s.substring(i+1,s.length());//左闭右开
dfs(index+str1.length(),path,s);//加上前半段的长度
path.remove(path.size()-1);
}
}
}
22. 括号生成
数字 n 代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且 有效的 括号组合。
示例 1:
输入:n = 3
输出:[“((()))”,“(()())”,“(())()”,“()(())”,“()()()”]
示例 2:
输入:n = 1
输出:[“()”]
class Solution {
String str="()";
List<String> res = new ArrayList<String>();
public List<String> generateParenthesis(int n) {
//()(()) )())不行也就是说有括号必须在左括号后出现
StringBuilder path = new StringBuilder();
dfs(path,0,0,n);
return res;
}
void dfs(StringBuilder path,int left,int right,int n){//left用来计左括号的数量
if(left==n&&right==n){
res.add(new String(path));
return ;
}
//两种选择,这个和 78 子集那题类似
for(int i=0;i<2;i++){
if(right>left) continue;//右括号数量比左括号多
if(left>n||right>n) continue;
if(i==1){
path.append(")");
dfs(path,left,right+1,n);
path.deleteCharAt(path.length()-1);
}
if(i==0){
path.append("(");
dfs(path,left+1,right,n);
path.deleteCharAt(path.length()-1);
}
}
}
}
51. N 皇后
按照国际象棋的规则,皇后可以攻击与之处在同一行或同一列或同一斜线上的棋子。
n 皇后问题 研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。
给你一个整数 n ,返回所有不同的 n 皇后问题 的解决方案。
每一种解法包含一个不同的 n 皇后问题 的棋子放置方案,该方案中 ‘Q’ 和 ‘.’ 分别代表了皇后和空位。
示例 1:
输入:n = 4
输出:[[“.Q…”,“…Q”,“Q…”,“…Q.”],[“…Q.”,“Q…”,“…Q”,“.Q…”]]
解释:如上图所示,4 皇后问题存在两个不同的解法。
示例 2:
输入:n = 1
输出:[[“Q”]]
class Solution {
List<List<String>> res = new ArrayList<List<String>>();
public List<List<String>> solveNQueens(int n) {
int[] rowy= new int[n];//记录每个行中Q的所在列
dfs(0,n,rowy);
return res;
}
boolean fun(int[] rowy,int x,int y){//判断是否符合皇后限制
for(int i=0;i<x;i++){
if(rowy[i]==y||Math.abs(x-i)==Math.abs(y-rowy[i]))
return false;
}
return true;
}
void dfs(int index,int n,int[] rowy){
if(index==n){
//已知rowy里存着每行中Q点位置,把图画出来
List<String> path = new ArrayList<String>();
for(int i=0;i<n;i++){
char[] str = new char[n];
Arrays.fill(str,'.');
str[rowy[i]]='Q';
path.add(new String(str));
}
res.add(new ArrayList<String>(path));
return;
}
for(int i=0;i<n;i++){
if(!fun(rowy,index,i)) continue;//剪枝
rowy[index]=i;
dfs(index+1,n,rowy);
rowy[index]=0;
}
}
}