最开始接触回溯算法是八皇后问题,经典的N皇后问题,当时看了代码就觉得很神奇,但是理解不了为什么是这样的,当时在2n皇后python版一文里面写的题解是
2n皇后问题,解决这个先要知道n皇后问题的解法。下面我们先介绍一下n皇后问题。
n皇后问题是有nxn的棋盘,有n个皇后,皇后与皇后之间不能在同一行,同一列,以及对角线上。问皇后能有多少种排列方式(皇后都是一样的)。
我们采用两个函数,一个函数判断皇后在这个位置上是否合法(是否同行,是否同列,是否对角线)。
另一个函数用来深搜,搜到一条符合条件的路线,把路线加到一个列表。(我也说不清楚,可以debug代码,然后看到一步一步是怎么递归的)
当时就是说不清楚(虽然现在也未必能说的很清楚(笑哭)哈哈哈),接下来就从力扣上面找几个回溯的题目来整理一下回溯大法的神奇之处吧。
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”]]
提示:
1 <= n <= 9
皇后彼此不能相互攻击,也就是说:任何两个皇后都不能处于同一条横行、纵行或斜线上。
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/n-queens
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
分析:
相当于是 n ∗ n n*n n∗n的格子里面合理放置一种皇后,皇后之间不同行不同列不同对角线。
回溯算法的框架展示如下:
vector<vector<string>> res;
//回溯算法
void fun(路径,当前行)
{
if(当前行=n)//说明走到尽头了,且走到这了,就是前面n-1行都是符合要求的
{
res.push_back(路径);
return;
}
int n = 路径.size();//所有的列数
for(int col = 0;col<n;col++)//相当于在当前行遍历所有的列,就关注到当前行的每一个点了
{
if(valid(路径,row,col)//如果放在这个位置是合理的
路径[row][col] = 'Q';
fun(路径,row+1);//到下一行
路径[row][col] = '.';//从上一行递归到这里就说明,上一行后续有的点不可以,就还原
}
}
这样写起来是能够看懂的,但是还是建议第一次写的话去debug一下,看一下整个到底是怎么回溯的流程,在遇到valid函数判断无效的时候,去退回的过程,在debug里面就看的很清楚,空想的话,脑子里回溯不了几圈就乱了。
上代码:
class Solution {
private:
bool valid(vector<string>& temp,int row, int col)
{
int n = temp.size();
//检查列
for(int i = 0;i<row;i++)
{
if(temp[i][col] == 'Q')
return false;
}
//检查右上角
for(int i = row-1,j = col+1;i >= 0 && j < n;i--,j++)
{
if(temp[i][j] == 'Q')
return false;
}
//检查左上角
for(int i = row-1,j = col-1;i>=0 && j>=0;i--,j--)
{
if(temp[i][j] == 'Q')
return false;
}
return true;
}
void fun(vector<string>& temp,int row){
if(row == temp.size())
{
res.push_back(temp);
return;
}
//列的长度
int n = temp[row].size();
for(int col = 0;col<n;col++)
{
if(valid(temp,row,col))
{
temp[row][col] = 'Q';
fun(temp,row+1);
//回溯
temp[row][col] = '.';
}
}
}
public:
vector<vector<string>> res;
vector<vector<string>> solveNQueens(int n) {
vector<string> temp(n,string(n,'.'));
fun(temp,0);//从第零行开始回溯
return res;
}
};
39. 组合总和
给定一个无重复元素的数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。
candidates 中的数字可以无限制重复被选取。
说明:
所有数字(包括 target)都是正整数。
解集不能包含重复的组合。
示例 1:
输入:candidates = [2,3,6,7], target = 7,
所求解集为:
[
[7],
[2,2,3]
]
示例 2:
输入:candidates = [2,3,5], target = 8,
所求解集为:
[
[2,2,2,2],
[2,3,3],
[3,5]
]
提示:
1 <= candidates.length <= 30
1 <= candidates[i] <= 200
candidate 中的每个元素都是独一无二的。
1 <= target <= 500
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/combination-sum
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
分析:
按照八皇后总结出来的回溯套路,我们可以将题目意思做如下整理:
①回溯求解,target值每次都变换,表示为剩余target的值,那么回溯出口就有两个,分别是:1.target==0,得到一条合理解。2.target<0,这个数值选的不合适。
②和八皇后不一样的是,这个的数字是可以重复选取的。相当于八皇后里面的路径选择的时候,判断当前点是否有效,只能走一个点,是固定的。但是这里面,在选择到当前点的时候,可以选也可以不选。所以要写两个回溯。
③这个都不必考虑会有重复的解加到res里面,因为这个 candidates是没有重复元素的。
看代码:
class Solution {
public:
vector<vector<int>> res;//存结果
//原数组,剩余目标值,当前遍历到的下标,当前已经选取的数字
void dfs(vector<int>& candidates,int target,int idx,vector<int>& temp)
{
//下标都越界了肯定不对了
if(idx == candidates.size())
return;
//如果到了目标值的位置,就找到一组合理解。
if(target == 0)
{
res.push_back(temp);
return;
}
//①不选
dfs(candidates,target,idx+1,temp);
//②选
if(target-candidates[idx]>=0)
{
temp.push_back(candidates[idx]);
dfs(candidates,target-candidates[idx],idx,temp);
temp.pop_back();
}
}
vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
vector<int>temp;
dfs(candidates,target,0,temp);
return res;
}
};
40. 组合总和
给定一个数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。
candidates 中的每个数字在每个组合中只能使用一次。
说明:
所有数字(包括目标数)都是正整数。
解集不能包含重复的组合。
示例 1:
输入: candidates = [10,1,2,7,6,1,5], target = 8,
所求解集为:
[
[1, 7],
[1, 2, 5],
[2, 6],
[1, 1, 6]
]
示例 2:
输入: candidates = [2,5,2,1,2], target = 5,
所求解集为:
[
[1,2,2],
[5]
]
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/combination-sum-ii
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
分析:
和上面一个不一样的是这个题目给的candidates中可能存在重复数字,所以导致结果天然性存在重复的元素。
我们顺一下思路如下:
①未完成,待补充
字符串排列
剑指 Offer 38. 字符串的排列
输入一个字符串,打印出该字符串中字符的所有排列。
你可以以任意顺序返回这个字符串数组,但里面不能有重复元素。
示例:
输入:s = “abc”
输出:[“abc”,“acb”,“bac”,“bca”,“cab”,“cba”]
限制:
1 <= s 的长度 <= 8
分析:
将问题分解成子问题:
固定一个位置,后面的位置全排列
递归的时候,不停细分为,固定一个位置,后面的位置全排列。
class Solution {
public:
vector<string> permutation(string s) {
dfs(s,0);
return res;
}
private:
vector<string>res;
void dfs(string s,int x) //回溯
{
if(x == s.size()-1)
{
res.push_back(s); //结果加一
return;
}
set<int>st; //保证不重复
for(int i = x;i<s.size();i++)
{
if(st.find(s[i]) == st.end())
{
st.insert(s[i]);
swap(s[i],s[x]);
dfs(s,x+1);
swap(s[i],s[x]);
}
}
}
};