回溯
77.组合
可以将回溯问题,想象成树结构进行求解,其中for循环代表横向遍历,递归的过程代表纵向遍历。每层 节点的个数(图中矩形的个数) 代表每层循环的次数。
代码如下:
class Solution {
private:
vector<vector<int>> res;
vector<int> row;
void backtracking(int n, int k, int startindex){
//回溯函数终止条件
if(row.size()==k){
res.push_back(row);
return;
}
//单层搜索过程
for(int i=startindex; i<=n; i++){
row.push_back(i);
backtracking(n, k, i+1);
row.pop_back();
}
return;
/*剪枝优化
for(int i = startIndex; i <= n - (k - path.size()) + 1; i++){
row.push_back(i);
backtracking(n, k, i+1);
row.pop_back();
}
return;
*/
}
public:
vector<vector<int>> combine(int n, int k) {
res.clear();
row.clear();//可以不写
backtracking(n,k,1);
return res;
}
};
组合优化:
可以通过剪枝操作,进行优化,回溯算法其实就是一个穷举的过程,并没有节省多少算法的运行时间,但可以通过剪枝操作,对回溯进行优化。剪枝操作实际上就是找到递归中每层循环的起始位置,若起始位置之后的元素已经不满足我们需要的元素个数,就没有搜索的必要了。
216.组合总和3
剪枝代码如下:
//剪枝
class Solution {
private:
vector<vector<int>> res;
vector<int> row;
void backtracking(int k, int targetsum, int sum, int start){
//剪枝
if(sum > targetsum){
return;
}
if(row.size()==k){
if(sum == targetsum){
res.push_back(row);
}
return;
}
for(int i=start; i<=9-(k-row.size())+1; i++)//剪枝
{
row.push_back(i);
sum+=i;
backtracking(k,targetsum,sum,i+1);
sum-=i;
row.pop_back();
}
return;
}
public:
//找出所有相加之和为 n 的 k 个数的组合,只使用数字1-9,每个数字用一次
vector<vector<int>> combinationSum3(int k, int n) {
res.clear();
row.clear();
backtracking(k, n, 0, 1);
return res;
}
};
自解代码:
class Solution {
private:
vector<vector<int>> res;
vector<int> row;
void backtracking(int k, int n, int start){
if(row.size()==k){
int sum=0;
for(int i=0; i<k; i++){
sum+=row[i];
}
if(sum == n){
res.push_back(row);
}
return;
}
for(int i=start; i<=9; i++){
row.push_back(i);
backtracking(k,n,i+1);
row.pop_back();
}
return;
}
public:
//找出所有相加之和为 n 的 k 个数的组合,只使用数字1-9,每个数字用一次
vector<vector<int>> combinationSum3(int k, int n) {
res.clear();
row.clear();
backtracking(k, n, 1);
return res;
}
};
17.电话号码的字母组合
代码如下:
class Solution {
private:
string map[10]={
"",
"",
"abc",
"def",
"ghi",
"jkl",
"mno",
"pqrs",
"tuv",
"wxyz"
};
public:
vector<string> res;
string row;
void traversal(string& digits, int index){
if(digits.size()==index){
res.push_back(row);
return;
}
int in=digits[index]-'0';
for(int i=0; i<map[in].size();i++){
row.push_back(map[in][i]);
traversal(digits,index+1);
row.pop_back();
}
return;
}
vector<string> letterCombinations(string digits) {
res.clear();
row.clear();
if(digits=="") return {};
traversal(digits,0);
return res;
}
};
39.组合总和
与之前组合问题不同,本题中candidates中的元素可以重复使用,(应注意for循环时,i是从index开始,避免结果vector中出现重复元素) 代码如下:
class Solution {
private:
void backtracking(vector<int>& vec, int sum, int target, int index){
if(sum>target){
return;
}
if(sum == target){
res.push_back(path);
return;
}
for(int i=index; i<vec.size(); i++){
path.push_back(vec[i]);
sum+=vec[i];
backtracking(vec, sum, target,i);
sum-=vec[i];
path.pop_back();
}
return;
}
public:
vector<vector<int>> res;
vector<int> path;
vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
res.clear();
path.clear();
//sort(candidates.begin(), candidates.end());
backtracking(candidates,0,target,0);
return res;
}
};
剪枝优化:**一开始对candidates数组进行排序,当下一层的总和sum(即本层的sum+candidates[i])已经大于target,就可以结束本轮for循环。
for循环剪枝代码如下:
for (int i = index; i < candidates.size() && sum + candidates[i] <= target; i++)
40.组合总和2(有陷阱)
此题是candidates数组中有重复元素,并且要求我们不能重复使用数组中的元素,所以我们就要对同一树层中的元素进行去重,即横向方向去重。第一种代码如下:
class Solution {
private:
void backtracking(vector<int>& vec, int target, int sum, int index){
if(sum>target) return;
if(target == sum){
res.push_back(path);
return;
}
for(int i=index; i<vec.size()&&sum+vec[i]<=target; i++){
//对同一树层使用过的元素跳过
if(i>index && vec[i]==vec[i-1]) continue;
path.push_back(vec[i]);
sum+=vec[i];
backtracking(vec, target, sum, i+1);
sum-=vec[i];
path.pop_back();
}
return;
}
public:
vector<vector<int>> res;
vector<int> path;
vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
res.clear();
path.clear();
sort(candidates.begin(), candidates.end());
backtracking(candidates, target, 0, 0);
return res;
}
};
以上代码是未使用used标记数组,使用used标记数组代码如下:
class Solution {
private:
void backtracking(vector<int>& vec, int target, int sum, int index, vector<bool>& used){
if(sum>target) return;
if(target == sum){
res.push_back(path);
return;
}
for(int i=index; i<vec.size()&&sum+vec[i]<=target; i++){
//对同一树层使用过的元素跳过
//used[i-1]=false 说明同一数层candidates[i-1]使用过
//used[i-1]=true 说明同一树枝candidates[i-1]使用过
if(i>0 && vec[i]==vec[i-1] && used[i-1]==false) continue;
path.push_back(vec[i]);
used[i]=true;
sum+=vec[i];
backtracking(vec, target, sum, i+1, used);
sum-=vec[i];
used[i]=false;
path.pop_back();
}
return;
}
public:
vector<vector<int>> res;
vector<int> path;
vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
res.clear();
path.clear();
vector<bool> used(candidates.size(),false);
sort(candidates.begin(), candidates.end());
backtracking(candidates, target, 0, 0, used);
return res;
}
};
131.分割回文串**(hard)
代码如下:
class Solution {
private:
void backtracking(string s, int index){
if(index >= s.size()){
res.push_back(path);
return;
}
for(int i=index; i<s.size(); i++){
if(palindromic(s,index,i)){
string str=s.substr(index, i-index+1);
path.push_back(str);
}
//这个过程不太理解 不满足回文串直接跳过
//满足回文串则进行递归
else{
continue;
}
backtracking(s,i+1);
path.pop_back();
}
return;
}
//判断是否是回文串
bool palindromic(string s, int start, int end){
for(int i=start,j=end; i<j; i++,j--){
if(s[i]!=s[j]){
return false;
}
}
return true;
}
public:
vector<vector<string>> res;
vector<string> path;
vector<vector<string>> partition(string s) {
res.clear();
path.clear();
backtracking(s,0);
return res;
}
};
本题是切割问题,切割过程与之前组合问题类似,切割问题也可以抽象为一棵树形结构:
切割线(图中的红线)切割到字符串的结尾位置,说明了找到一种切割方法。
93.复原ip地址*
代码如下:
class Solution {
private:
vector<string> res;
void backtracking(string& s, int index, int pointnum){
if(pointnum==3){
if(isvalid(s,index,s.size()-1)){
res.push_back(s);
}
return;
}
//单层搜索逻辑
for(int i=index; i<s.size(); i++){
if(isvalid(s,index,i)){ //判断[index,i]区间的子串是否合法
s.insert(s.begin()+i+1, '.'); //在i的后面加一个.
pointnum++;
backtracking(s,i+2,pointnum); 插入逗点之后下一个子串的起始位置为i+2
//回溯
pointnum--;
s.erase(s.begin()+i+1);
}
}
return;
}
bool isvalid(string s, int start, int end){
if(start>end)
return false;
//开头字符为0 非法
if(s[start]=='0' && start!=end){
return false;
}
int num=0;
for(int i=start; i<end; i++){
num=num*10+s[i]-'0';
}
if(num>255){
return false;
}
return true;
}
public:
vector<string> restoreIpAddresses(string s) {
res.clear();
if(s.size()<4 || s.size()>12) return res;//算剪枝了
backtracking(s,0,0);
return res;
}
};
注:backtracking参数中不能定义const string& s,因为在函数体内会对字符串进行insert等操作。
78.子集(标准回溯模板题型)
代码如下:
class Solution {
private:
vector<vector<int>> res;
vector<int> path;
void backtracking(vector<int>& nums, int index){
res.push_back(path);//收集子集,要放在最上面,否则会漏掉自己
//可不写
if(index >= nums.size()){
return;
}
//单层搜索过程:
for(int i=index; i<nums.size(); i++){
path.push_back(nums[i]);
backtracking(nums,i+1);
path.pop_back();
}
return;
}
public:
vector<vector<int>> subsets(vector<int>& nums) {
res.clear();
path.clear();
backtracking(nums,0);
return res;
}
};
要清楚子集问题和组合问题、分割问题的的区别,子集是收集树形结构中树的所有节点的结果。而组合问题、分割问题是收集树形结构中叶子节点的结果。
上图中,树的第一个分支的所有得子集都是从最上层的for循环中i=0迭代回溯而来,即通过不断的递归回溯,入栈出栈的过程。许多人可能会深入研究整个递归过程的逻辑,我也一样,总是想要对整个过程一探究竟,我们可以拿出笔试试,不要只通过大脑进行模拟,这样很容易忘记递归回溯究竟走到了哪一层。
小结
对于组合问题,什么时候需要startIndex呢?
如果是一个集合来求组合的话,就需要startIndex,例如:回溯算法:求组合问题!回溯算法:求组合总和!
如果是多个集合取组合,各个集合之间相互不影响,那么就不用startIndex,例如:回溯算法:电话号码的字母组合!
90.子集2
本题和40.组合总和2相似,就是在回溯的基础上进行去重,本题依然可以通过设置used数组进行求解,并且通过树枝去重和树层去重的逻辑进行求解。需要注意的是去重的前提是,需要对vector进行排序。代码如下:
class Solution {
private:
vector<vector<int>> res;
vector<int> path;
void backtracking(vector<int>& nums, int index){
res.push_back(path);
if(index >= nums.size()){
return;
}
for(int i=index; i<nums.size(); i++){
if(i>index && nums[i]==nums[i-1]) continue;
path.push_back(nums[i]);
backtracking(nums, i+1);
path.pop_back();
}
return;
}
public:
vector<vector<int>> subsetsWithDup(vector<int>& nums) {
res.clear();
path.clear();
//去重需要排序
sort(nums.begin(),nums.end());
backtracking(nums,0);
return res;
}
};
49.递增子序列
class Solution {
private:
vector<vector<int>> res;
vector<int> path;
void backtracking(vector<int>& nums, int index){
if(path.size()>=2){
res.push_back(path);
}
//也可以用unordered_set<int> uset,去重,但是使用数组
//来当做哈希,可以减少时间消耗,属于是一种优化的方法。
//每层都重新定义一个used数组,来记录当前层是否有重复元素
int used[201]={0};
for(int i=index; i<nums.size(); i++){
if((!path.empty() && nums[i]<path.back()) || used[nums[i]+100]==1){
continue;
}
path.push_back(nums[i]);
used[nums[i]+100]=1;//去重,本层重复的元素不再使用,直接跳过
backtracking(nums,i+1);
path.pop_back();
}
return;
}
public:
vector<vector<int>> findSubsequences(vector<int>& nums) {
res.clear();
path.clear();
backtracking(nums,0);
return res;
}
};
题目给的示例为 [4,6,7,7] ,这个示例容易给我们造成误导,认为例子数组一定是递增或者递减,但实际上完全可以是 [4,7,6,7] 这种,示例如下:
46.全排列
代码如下:
class Solution {
private:
//不是单纯的组合问题,排列问题也应该用到used
vector<vector<int>> res;
vector<int> path;
void backtracking(vector<int>& nums, vector<bool>& used){
if(path.size() == nums.size()){
res.push_back(path);
return;
}
for(int i=0; i<nums.size(); i++){
if(used[i] == false){
path.push_back(nums[i]);
used[i]=true;
backtracking(nums, used);
used[i]=false;
path.pop_back();
}
else{
//path里已经收录的元素,直接跳过
continue;
}
}
return;
}
public:
vector<vector<int>> permute(vector<int>& nums) {
res.clear();
path.clear();
vector<bool> used(nums.size(), false);
backtracking(nums, used);
return res;
}
};
排列问题的不同:
- 每层都是从0开始搜索,而不是从index;
- 需要used数组来记录path中放入了哪些元素。
47.全排列2(两种去重逻辑,used[i-1]=fasle或used[i-1]=true)
代码如下:
注意去重先排序,本题中 同一树层(横向for循环) 中相同元素不可以重复使用,同一树枝(纵向递归) 中相同元素可以重复使用,使用used数组记录当前元素是否已经被使用。
class Solution {
private:
vector<vector<int>> res;
vector<int> path;
void backtracking(vector<int>& nums, vector<bool> used){
if(path.size() == nums.size()){
res.push_back(path);
return;
}
for(int i=0; i<nums.size(); i++){
//used[i-1]=false也能通过,false是树层去重,true是树枝去重
//对于排列问题,树层去重效率更高
if((i-1>=0 && nums[i]==nums[i-1]) && used[i-1]==true){
continue;
}
if(used[i] == false){
path.push_back(nums[i]);
used[i]=true;
backtracking(nums, used);
used[i]=false;
path.pop_back();
}
}
return;
}
public:
//去重先排序 本题同一树层不可以重复,同一树枝可以重复,并需要used数组
vector<vector<int>> permuteUnique(vector<int>& nums) {
res.clear();
path.clear();
sort(nums.begin(), nums.end());
vector<bool> used(nums.size(),false);
backtracking(nums, used);
return res;
}
};
两种去重逻辑,used[i-1]=fasle或used[i-1]=true,树层上去重(used[i - 1] == false),的树形结构如下:
树枝上去重(used[i - 1] == true)的树型结构如下:
332.重新安排行程(hard,也可用图论深度优先搜索)
本题目锻炼了我们对容器的使用,同时回溯的返回值是bool类型,这说明我们只要找到唯一一个满足条件的行程,即唯一一个到叶子节点的路线, 即可返回。同时应对题目说的字典序要求,使用了map中对key值自动排序的特性进行了解决。代码如下:
class Solution {
private:
//unordered<出发地点,map<到达地点,票的张数>> targets
unordered_map<string,map<string,int>> targets;
//有一条路径即可返回,即地方全到了,res中的数=票的数+1;
bool backtracking(int ticketnum, vector<string>& res){
if(res.size() == ticketnum+1){
return true;
}
for(pair<const string, int>& target: targets[res[res.size()-1]]){
if(target.second > 0){
res.push_back(target.first);
target.second--;
if(backtracking(ticketnum, res)){
return true;
}
target.second++;
res.pop_back();
}
}
return false;
}
public:
vector<string> findItinerary(vector<vector<string>>& tickets) {
targets.clear();
vector<string> res;
for(const vector<string>& vec: tickets){
//targets[vec[0]]代表哈希表的key targets[vec[0]][vec[1]]++
//代表map中的key对应的value++;
targets[vec[0]][vec[1]]++; //记录映射关系
}
res.push_back("JFK");
backtracking(tickets.size(), res);
return res;
}
};
51.n-皇后(hard)
代码如下:
class Solution {
private:
vector<vector<string>> res;
void backtracking(vector<string>& chess, int n, int row){
//终止条件
if(row == n){
res.push_back(chess);
return;
}
//单层搜索逻辑
for(int col=0; col<n; col++){
if(isvalid(row, col, chess, n)){
chess[row][col]='Q';
backtracking(chess, n, row+1);//回溯 求下一列
chess[row][col]='.';//回溯撤销
}
}
return;
}
bool isvalid(int row, int col, vector<string> chess, int n){
//检查列
for(int i=0; i<row; i++){
if(chess[i][col]=='Q'){
return false;
}
}
//检查45度 左侧斜上
for(int i=row-1,j=col-1; i>=0 && j>=0; i--,j--){
if(chess[i][j] == 'Q'){
return false;
}
}
//检查135度 右侧斜上
for(int i=row-1,j=col+1; i>=0 && j<n; i--,j++){
if(chess[i][j] == 'Q'){
return false;
}
}
return true;
}
public:
vector<vector<string>> solveNQueens(int n) {
res.clear();
vector<string> chess(n,string(n,'.'));
backtracking(chess, n, 0);
return res;
}
};
本题为经典回溯问题,n皇后数据形式为
n
∗
n
n*n
n∗n形式,可以将其想象成一个树形结构,并由此进行回溯求解。共为n行, 所以递归深入n层,定义一个row变量记录当前递归行数,当row==n,回溯终止;共有n列, 每层回溯循环n次,判断当前列是否可以放置皇后Q。
37.解数独(hard)
本题与以往不同,借用Carl哥的话,本题的解题形式是二维递归,采用两个for循环,以行列的方式递归。代码如下:
class Solution {
private:
//二维回溯
bool backtracking(vector<vector<char>>& board){
for(int row=0; row<board.size(); row++){
for(int col=0; col<board[0].size(); col++){
if(board[row][col]=='.'){
for(char k='1'; k<='9'; k++){
if(isvalid(row, col, k, board)){
board[row][col]=k;
//如果找到合适的一组,立刻返回
//进入递归,本层递归中board[row][col]已经由.变成了1-9中的数字,
//所以继续递归下去,继续把.变成数字,直到所有的.都变成了数字,即找到了
//一组合法的结果,直接返回。
if(backtracking(board)){
return true;
}
board[row][col]='.';
}
}
return false;
}
}
}
return true; //遍历完没有返回false;说明找到了合适棋盘位置了
}
bool isvalid(int row, int col, char k,vector<vector<char>>& board){
//数字 1-9 在每一行只能出现一次。
//注意:是i<9,不是i<col
for(int i=0; i<9; i++){
if(board[row][i] == k){
return false;
}
}
//数字1-9 在每一列只能出现一次
for(int i=0; i<9; i++){
if(board[i][col] == k){
return false;
}
}
//数字 1-9 在每一个以粗实线分隔的 3x3 宫内只能出现一次。
int startrow=(row/3)*3;
int startcol=(col/3)*3;
//将整个区域划分成九个3*3区域,startrow,startcol为其中某个区域的开始位置
for(int i=startrow; i<startrow+3; i++){
for(int j=startcol; j<startcol+3; j++){
if(board[i][j]==k){
return false;
}
}
}
return true;
}
public:
void solveSudoku(vector<vector<char>>& board) {
backtracking(board);
}
};