理论
回溯的本质是穷举,穷举所有可能,然后选出我们想要的答案。一般可以解决如下几种问题:
组合问题:N个数里面按一定规则找出k个数的集合
切割问题:一个字符串按一定规则有几种切割方式
子集问题:一个N个数的集合里有多少符合条件的子集
排列问题:N个数按一定规则全排列,有几种排列方式
棋盘问题:N皇后,解数独等等
组合是不强调元素顺序的,排列是强调元素顺序。因此组合无序,排列有序。
例题
77. 组合
回溯就是递归,所以按照递归三部曲来做
- 参数:n,k固定参数,startIndex,从startindex开始往后取元素,path,存放一条支路的结果,re:存放所有支路的结果
返回值:因为要遍历整棵树,所以不需要返回值 - 终止条件:当到达终止条件时存放结果,也就是path里的元素数等于k
- 本层递归:从startindex开始往后选择,递归调用下一层,回溯。
for循环次数就是树的分支数,递归的深度就是树的深度。
class Solution {
public:
vector<vector<int>>re;
vector<int>path;
void backtracking(int startIndex,int n,int k){
if(path.size()==k){//到达叶子结点
re.push_back(path);
return ;
}
for(int i=startIndex;i<=n;i++){
path.push_back(i);
backtracking(i+1,n,k);//下一层就要从i+1开始选择了
path.pop_back();//回溯
}
}
vector<vector<int>> combine(int n, int k) {
backtracking(1,n,k);
return re;
}
};
优化:
当startIndex开始往后的元素个数小于要找的个数,就不需要再找了。
即 n-i+1(还剩的个数) < k-path.size() (还要找的个数)
因此只需要从startIndex开始查找到n+1-k+path.size()
class Solution {
public:
vector<vector<int>>re;
vector<int>path;
void backtracking(int startIndex,int n,int k){
if(path.size()==k){//到达叶子结点
re.push_back(path);
return ;
}
for(int i=startIndex;i<=n+1-k+path.size();i++){
path.push_back(i);
backtracking(i+1,n,k);
path.pop_back();//回溯
}
}
vector<vector<int>> combine(int n, int k) {
backtracking(1,n,k);
return re;
}
};
- 参数:n,k:找k个数并且和为n,startIndex:从startindex位置开始找
返回值:空 - 终止条件:如果要找和为负数的集合,直接返回
如果要找和0的集合并且当前已经找到了k个,保存结果
如果n为0或者path.size()==k,直接返回 - 本层递归:从startindex开始往后取,递归,回溯
class Solution {
public:
vector<int>path;
vector<vector<int>>re;
void backtracking(int k,int n,int startIndex){//找k个数,并且和为n的集合
if(path.size()==k){//找够了k个数
if(n==0){
re.push_back(path);
}
return ;
}
for(int i=index;i<=9;i++){
if(n-i<0){//当前位置往后的都比n大,自然也就没有必要再找了
break;
}
path.push_back(i);
f(k,n-i,i+1);//找从i+1开始,组成n-i的组合
path.pop_back();
}
}
vector<vector<int>> combinationSum3(int k, int n) {
backtracking(k,n,1);
return re;
}
};
17. 电话号码的字母组合
每一种组合中集合的个数就是digits的长度,从当前位置对于的字符串中找一个字母,然后再去找下一个位置的字母
-
参数:digits 固定参数,index:从index开始找
-
终止条件:当找到digits长度大小的集合时,就保存一下
-
本层递归:映射出当前位置的数字字符对于的字符串,然后穷举每一个可能。
class Solution {
public:
string number [10]={"","","abc","def","ghi","jkl","mno","pqrs","tuv","wxyz"};//数字字符串映射表
vector<string>re;
string path;
void f(string & digits,int index){//从digits中的index位置开始找
if(path.size()==digits.size()){
re.push_back(path);
return ;
}
string cur=number[digits[index]-'0'];//index位置对应字符串,对于digits="23",index==0,cur就是"abc"
for(int i=0;i<cur.size();i++){
path.push_back(cur[i]);
f(digits,index+1);
path.pop_back();
}
}
vector<string> letterCombinations(string digits) {
if(digits==""){
return re;
}
f(digits,0);
return re;
}
};
和选硬币问题完全一样
- 参数:candidates 固定参数,index:从index往后开始找和为rest的组合
- 终止条件:rest小于0,直接返回
rest等于0,保存结果 - 本层递归:从index往后选一个元素,然后去找减去index位置元素的组合
和上几个题的区别是本题元素可以重复选
class Solution {
public:
vector<int>path;
vector<vector<int>>re;
void f(vector<int>& candidates,int index,int rest){
if(rest<0){//非法情况
return ;
}
if(rest==0){
re.push_back(path);
return ;
}
for(int i=index;i<candidates.size();i++){
path.push_back(candidates[i]);
f(candidates,i,rest-candidates[i]);//从i开始,找和
//为rest-candidates[i]的组合
path.pop_back();
}
}
vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
f(candidates,0,target);
return re;
}
};
优化:先对candidates进行排序,如果rest-candidates[i]已经小于0了,后面就没有必要再尝试了
class Solution {
public:
vector<int>path;
vector<vector<int>>re;
void f(vector<int>& candidates,int index,int rest){
if(rest<0){//非法情况
return ;
}
if(rest==0){
re.push_back(path);
return ;
}
for(int i=index;i<candidates.size();i++){
if(rest-candidates[i]<0){//剪枝
break;
}
path.push_back(candidates[i]);
f(candidates,i,rest-candidates[i]);//从i开始,找和
//为rest-candidates[i]的组合
path.pop_back();
}
}
vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
sort(candidates.begin(),candidates.end());
f(candidates,0,target);
return re;
}
};
本题难点在于如何去重,可以先排序,这样相同的元素肯定紧挨着,在每一层递归下设一个哈希表,用来标记有没有使用过这个元素,只有没有使用过的才去跑递归
因为candidates里可能有重复的元素,所以同一个组合里会有重复元素,但是不同的组合元素要不同,也就是同一树层去重
剪枝的操作和上一题一样。
class Solution {
public:
vector<int>path;
vector<vector<int>>re;
void f(vector<int>& candidates,int index,int rest){//从index开始往后找和为rest的组合
if(rest==0){
re.push_back(path);
return ;
}
unordered_map<int,bool>visit;
for(int i=index;i<candidates.size() && rest-candidates[i]>=0 ;i++){
if(!visit[candidates[i]]){//去重,如果已经使用了candidates[i]元素就剪掉
visit[candidates[i]]=true;
path.push_back(candidates[i]);
f(candidates,i+1,rest-candidates[i]);
path.pop_back();
}
}
}
vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
sort(candidates.begin(),candidates.end());
f(candidates,0,target);
return re;
}
};
省空间的写法,因为已经排过序了,所以相同的元素肯定紧靠着,只要判断它和它的前一个元素是否相等(同一树层下)
class Solution {
public:
vector<int>path;
vector<vector<int>>re;
void f(vector<int>& candidates, int index, int rest) {//从index开始往后找和为rest的组合
if (rest == 0) {
re.push_back(path);
return;
}
unordered_map<int, bool>visit;
for (int i = index; i < candidates.size() && rest - candidates[i] >= 0; i++) {
if (i > index && candidates[i] == candidates[i - 1]) {//i>index是保证在同一树层下
continue;
}
path.push_back(candidates[i]);
f(candidates, i + 1, rest - candidates[i]);
path.pop_back();
}
}
vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
sort(candidates.begin(), candidates.end());
f(candidates, 0, target);
return re;
}
};
- 参数:s 固定参数,index:从index位置开始分割
- 终止条件:当index等于s的长度时保存结果
- 本层递归:要找从index开始的回文子串,i初始为index 表示从index位置开始分割,例如s=“aab”,index=0,i
开始等于0,就是a作为一个子串,然后去切割ab;如果i等于1,就是aa作为一个子串,然后去切割ab。注意要判断子串是否为回文串。
class Solution {
public:
vector<string>path;
vector<vector<string>>re;
bool isPalindrome(const string& s){//判断是否为回文串
int l=0;
int r=s.size()-1;
while(l<r){
if(s[l++]!=s[r--]){
return false;
}
}
return true;
}
void f(string& s,int index){//找从index位置开始分割的子串
if(index==s.size()){
re.push_back(path);
return ;
}
for(int i=index;i<s.size();i++){
string substr=s.substr(index,i-index+1);//截取从index到i的子串
if(isPalindrome(substr)){//只有substr是回文串才去跑递归
path.push_back(substr);
f(s,i+1);
path.pop_back();
}
}
}
vector<vector<string>> partition(string s) {
f(s,0);
return re;
}
};
类似于切割字符串,直接在原字符串上修改就好了
- 参数:s,startindex:从startindex位置开始找,pointNum:s中 . 的个数
- 终止位置:pointNum为3,判断startindex到结束的字符串是否合法
- 本层递归:也是先判断从startindex位置到i的字符串是否合法,若合法,在i+1位置插入 . ,然后找i+2开始字符串。
class Solution {
public:
vector<string>re;
bool isValid(string& s,int start,int end){//判断是否合法
if(start>end){
return false;
}
if(s[start]=='0'){
if(start==end){
return true;
}
else{
return false;
}
}
int num=0;
for(int i=start;i<=end;i++){
num=num*10+(s[i]-'0');
if(num>255){//当前num已经超过255了,后续一定也大于255
return false;
}
}
return true;
}
void f(string s,int startIndex,int pointNum){
if(pointNum==3){//已经添加了3个点了,只需要判断startIndex到结束是否是合法的
if(isValid(s,startIndex,s.size()-1)){
re.push_back(s);
}
return ;
}
for(int i=startIndex;i<s.size();i++){
if(isValid(s,startIndex,i)){//相当于在i后面加. 需要提前判断startindex到i是否是合法的
s.insert(s.begin()+i+1,'.');//在i后面插入 .
pointNum++;
f(s,i+2,pointNum);//因为插入了一个. 所以从i+2开始
pointNum--;
s.erase(s.begin()+i+1);
}
else {//不符合的话,后续一定也是不符合的,因为后续都包含这一部分。
break;
}
}
}
vector<string> restoreIpAddresses(string s) {
f(s,0,0);
return re;
}
};
集合是无序的,也就是{1,2}和{2,1}是同一个集合,所以每次for循环都是从index开始,如果是排列的话,就要从0开始。
class Solution {
public:
vector<int>path;
vector<vector<int>>re;
void f(vector<int>& nums,int index){
re.push_back(path);//子集出现在递归过程中,不仅仅只是终止情况
if(index==nums.size()){
return ;
}
for(int i=index;i<nums.size();i++){
path.push_back(nums[i]);
f(nums,i+1);
path.pop_back();
}
}
vector<vector<int>> subsets(vector<int>& nums) {
f(nums,0);
return re;
}
};
和组合中II类似,都是同一层去重
class Solution {
public:
vector<vector<int>>re;
vector<int>path;
void f(vector<int>& nums,int index){
re.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]);
f(nums,i+1);
path.pop_back();
}
}
vector<vector<int>> subsetsWithDup(vector<int>& nums) {
sort(nums.begin(),nums.end());
f(nums,0);
return re;
}
};
需要注意的是递增子序列至少2个元素。
去重逻辑和上一个一样了,因为相同的不一定是紧挨着的,所以采用set去重。
class Solution {
public:
vector<int>path;
vector<vector<int>>re;
void f(vector<int>& nums, int index) {
if (path.size() > 1) {
re.push_back(path);
}
if (index == nums.size()) {
return;
}
unordered_set<int> uset;
for (int i = index; i < nums.size(); i++) {
if (uset.find(nums[i]) == uset.end()) {//本层第一次出现
uset.insert(nums[i]);//标记使用
if (path.size() > 0) {//path不为空要判断是否递增
int lastNum = path.back();//path最后一个元素
if (nums[i] >= lastNum) {
path.push_back(nums[i]);
f(nums, i + 1);
path.pop_back();
}
}
else {//path为空的时直接放
path.push_back(nums[i]);
f(nums, i + 1);
path.pop_back();
}
}
}
}
vector<vector<int>> findSubsequences(vector<int>& nums) {
f(nums, 0);
return re;
}
};
优化:用数组做哈希表
class Solution {
public:
vector<int>path;
vector<vector<int>>re;
void f(vector<int>& nums, int index) {
if (path.size() > 1) {
re.push_back(path);
}
if (index == nums.size()) {
return;
}
//unordered_set<int> uset;
int used[201]={0};//0表示为使用过,1表示使用过
for (int i = index; i < nums.size(); i++) {
if (used[nums[i]+100]==0) {//本层第一次出现
used[nums[i]+100]=1;//标记使用
if (path.size() > 0) {//path不为空要判断是否递增
int lastNum = path.back();//path最后一个元素
if (nums[i] >= lastNum) {
path.push_back(nums[i]);
f(nums, i + 1);
path.pop_back();
}
}
else {//path为空的时直接放
path.push_back(nums[i]);
f(nums, i + 1);
path.pop_back();
}
}
}
}
vector<vector<int>> findSubsequences(vector<int>& nums) {
f(nums, 0);
return re;
}
};
注意排列和组合的区别:排列有顺序,组合无顺序
排列问题可以不需要index,但需要借助used数组
这个去重是同一树枝下去重,也就是在同一条支路下,一个元素只能选择一次。
class Solution {
public:
vector<vector<int>>re;
vector<int>path;
void f(vector<int>& nums,vector<int>& used){
if(path.size()==nums.size()){
re.push_back(path);
return ;
}
for(int i=0;i<nums.size();i++){
if(used[i]==0){//只选择本树枝下没有用过的
used[i]=1;
path.push_back(nums[i]);
f(nums,used);
path.pop_back();
used[i]=0;
}
}
}
vector<vector<int>> permute(vector<int>& nums) {
vector<int>used(nums.size());
f(nums,used);
return re;
}
};
解2 :交换实现
如果一个集合有3个元素,那么它的排列方式有6种,第一个位置3种选择,第二个位置2种选择,第三个位置1种选择,相乘起来就是6种选择。
class Solution {
public:
vector<vector<int>>re;
void f(vector<int>& nums,int index){//从index开始的排列
if(index==nums.size()){
re.push_back(nums);
return ;
}
for(int i=index;i<nums.size();i++){//从index开始往后的元素都可以放在index位置,相当于在index位置选一个元素
swap(nums[index],nums[i])
f(nums,index+1);//找index+1的全排列
swap(nums[index],nums[i]);
}
}
vector<vector<int>> permute(vector<int>& nums) {
f(nums,0);
return re;
}
};
只需要去重就好了
class Solution {
public:
vector<vector<int>>re;
vector<int>path;
void f(vector<int>& nums,int index){
if(index==nums.size()){
re.push_back(nums);
return ;
}
//unordered_set<int>uset;
int visit[21]={0};
for(int i=index;i<nums.size();i++){
if(visit[nums[i]+10]==0){
visit[nums[i]+10]=1;
swap(nums[index],nums[i]);
f(nums,index+1);
swap(nums[index],nums[i]);
}
}
}
vector<vector<int>> permuteUnique(vector<int>& nums) {
f(nums,0);
return re;
}
};
- 参数:vectorre;
nordered_map<string,map<string,int>>targets;//unordered_map<开始地,map<目的地,航班数(0:表示不能再飞往此地了)>>targets targets表示从开始地到目的地所有航班的映射
vector<vector>& tickets
返回值:bool,找到一个解法就返回 - 终止条件:re.size()==tickets.size()+1,n趟航班就n+1个目的地。
- 本层递归:
优先考虑目的地字典序靠前的航班,可以选择的条件是:本航班还有机票——target.second>0
-为什么第一次找到的解法就是字典序靠前的解法?
map默认是按字典序进行排序的,所以如果有多种解法,会先尝试字典序小的。
class Solution {
public:
vector<string>re;
unordered_map<string,map<string,int>>targets;//unordered_map<开始地,map<目的地,航班数(0:表示不能再飞往此地了)>>targets targets表示从开始地到目的地所有航班的映射
bool f(vector<vector<string>>& tickets){
if(re.size()==tickets.size()+1){
return true;
}
string curPlace=re[re.size()-1];//从curPlace出发
for(pair<const string,int>&target:targets[curPlace]){//从curPlace出发的所有目的地
if(target.second>0){//表示从curPlace还可以飞的航班
target.second--;
re.push_back(target.first);
if(f(tickets)){//找到了一个就返回,因为map默认是按字典序排的,所以第一个找到的一定是符合要求的
return true;
}
re.pop_back();
target.second++;
}
}
return false;
}
vector<string> findItinerary(vector<vector<string>>& tickets) {
for(int i=0;i<tickets.size();i++){
vector<string>curFlight=tickets[i];//<开始地,目的地>
targets[curFlight[0]][curFlight[1]]++;//增加映射关系
}
re.push_back("JFK");
f(tickets);
return re;
}
};
思路:一行一行的摆,只需要判断是否共列 共斜线就好了
共斜线的简便判法:abs(row1 - row2) == abs(col1,col2) (row1,col1)和(row2,col2)共斜线
- 参数:vector<vector>re存结果;vectorpath;//每一行摆皇后的情况;ectorrecord;//记录每一行皇后摆的位置 record[i]=j:表示i行j列放皇后;string ss;//固定参数,哪一列上放皇后,就将相应位置修改为Q;
- 终止位置:index==n表示已经摆完了n行皇后(0-n-1)
- 本层递归:从本行的第一列到第n列尝试,如果当前位置合法,就记录下皇后的位置,然后递归跑下一行。
class Solution {
public:
vector<vector<string>>re;
vector<string>path;//每一行摆皇后的情况
vector<int>record;//记录每一行皇后摆的位置 record[i]=j:表示i行j列放皇后
bool isValid(int i, int j, int n) {//判断i行j列放皇后合不合法
for (int row = 0; row < i; row++) {
if (record[row] == j || abs(row - i) == abs(record[row] - j)) {//共列 共斜线
return false;
}
}
return true;
}
void f(int index, int n, string ss) {
if (index == n) {
re.push_back(path);
return;
}
for (int j = 0; j < n; j++) {
if (isValid(index, j, n)) {
record[index] = j;
string cur = ss;
cur[j] = 'Q';
path.push_back(cur);
f(index + 1, n, ss);
path.pop_back();
}
}
}
vector<vector<string>> solveNQueens(int n) {
string ss;//固定参数,哪一列上放皇后,就将相应位置修改为Q
for (int i = 0; i < n; i++) {
ss += '.';
}
record.resize(n);
f(0, n, ss);
return re;
}
};
37. 解数独
和N皇后类似,但比N皇后更复杂,N皇后每行只放一个皇后,而本题是要填满整个方格。
N皇后是一行一行的填,本题要一格一格的填,因此至少需要两层for循环
- 参数:board
返回值:找到一个方案就返回,题目也说了只有一种方案。 - 终止条件:不需要写,表格填满了自然就终止
- 本层递归:从0行0列到8行8列,找到第一个空白的格子,然后尝试1~9的数字,若合法,继续放下一个格子。
class Solution {
public:
bool isValid(int i,int j,char val,vector<vector<char>>& board){//判断(i,j)填k是否合法,此时i行j列位置是空的
for(int col=0;col<9;col++){//判断同行
if(board[i][col]==val){
return false;
}
}
for(int row=0;row<9;row++){//判断同列
if(board[row][j]==val){
return false;
}
}
int startRow=i/3*3;//(i,j)所在的3*3
int startCol=j/3*3;
for(int row=startRow;row<startRow+3;row++){//判断同3*3
for(int col=startCol;col<startCol+3;col++){
if(board[row][col]==val){
return false;
}
}
}
return true;
}
bool f(vector<vector<char>>& board){
for(int i=0;i<9;i++){
for(int j=0;j<9;j++){
if(board[i][j]!='.'){
continue;
}
for(char k='1';k<='9';k++){//从1~9尝试
if(isValid(i,j,k,board)){
board[i][j]=k;
if(f(board)){//找到一个方案就返回
return true;
}
board[i][j]='.';
}
}
return false;//1到9都不行,说明该方案没有解
}
}
return true;//所有的格都填满了
}
void solveSudoku(vector<vector<char>>& board) {
f(board);
}
};
总结
回溯是递归的副产品,只要有递归就会有回溯,所以回溯法也经常和二叉树遍历,深度优先搜索混在一起,因为这两种方式都是用了递归。回溯法就是暴力搜索,并不是什么高效的算法,最多再剪枝一下。
-
组合问题
组合无顺序
for循环代表树的分支,横向遍历,递归深度代表树的高度,纵向遍历,回溯不断调整结果集 -
切割问题
类似与组合问题,参数的index就代表切割线 -
子集问题
子集问题也属于组合,不仅仅在终止收集结果 -
排列问题
排列有顺序
可以用交换的方式,也可以加一个used数组 -
去重问题
同一树层去重,只需要在函数内部创建一个visit数组,只供本层使用
这个visit数组也可以是set,如果题目中没有明确规定结点的范围,就要使用set了 -
安排行程问题
有递归的地方就有回溯,深度优先搜索也是用递归来实现的,所以往往伴随着回溯。
本题难在如何选择正确的容器 -
棋盘问题
一层for循环的递归:N皇后,一行只放一个
二层for循环的递归:解数独,一格放一个
这两道题目的树的分支都是固定的,N皇后的分支固定为n,每行就有n个位置可以选择
解数独的分支固定为9,从1到9选择一个数来填
但是解数独的递归深度明显大于N皇后。