- 重刷回溯
- 46. 全排列">46. 全排列
- 47. 全排列 II">47. 全排列 II
- 39. 组合总和">39. 组合总和
- 40. 组合总和 II">40. 组合总和 II
- 77. 组合">77. 组合
- 78. 子集">78. 子集
- 90. 子集 II">90. 子集 II
- 60. 排列序列">60. 排列序列
- 93. 复原 IP 地址">93. 复原 IP 地址
- 733. 图像渲染">733. 图像渲染
- 200. 岛屿数量">200. 岛屿数量
- 130. 被围绕的区域">130. 被围绕的区域
- 79. 单词搜索">79. 单词搜索
- 17. 电话号码的字母组合">17. 电话号码的字母组合
- 22. 括号生成">22. 括号生成
- 22. 括号生成">22. 括号生成
- 51. N 皇后">51. N 皇后
重刷回溯
题型一:排列、组合、子集相关问题
提示:这部分练习可以帮助我们熟悉「回溯算法」的一些概念和通用的解题思路。解题的步骤是:先画图,再编码。去思考可以剪枝的条件, 为什么有的时候用 used 数组,有的时候设置搜索起点 begin 变量,理解状态变量设计的想法。
- 全排列(中等)
- 全排列 II(中等):思考为什么造成了重复,如何在搜索之前就判断这一支会产生重复;
- 组合总和(中等)
- 组合总和 II(中等)
- 组合(中等)
- 子集(中等)
- 子集 II(中等):剪枝技巧同 47 题、39 题、40 题;
- 第 k 个排列(中等):利用了剪枝的思想,减去了大量枝叶,直接来到需要的叶子结点;
- 复原 IP 地址(中等)
题型二:Flood Fill
提示:Flood 是「洪水」的意思,Flood Fill 直译是「泛洪填充」的意思,体现了洪水能够从一点开始,迅速填满当前位置附近的地势低的区域。类似的应用还有:PS 软件中的「点一下把这一片区域的颜色都替换掉」,扫雷游戏「点一下打开一大片没有雷的区域」。
下面这几个问题,思想不难,但是初学的时候代码很不容易写对,并且也很难调试。我们的建议是多写几遍,忘记了就再写一次,参考规范的编写实现(设置 visited 数组,设置方向数组,抽取私有方法),把代码写对。
- 图像渲染(Flood Fill,中等)
- 岛屿数量(中等)
- 被围绕的区域(中等)
- 单词搜索(中等)
说明:以上问题都不建议修改输入数据,设置 visited 数组是标准的做法。可能会遇到参数很多,是不是都可以写成成员变量的问题,面试中拿不准的记得问一下面试官
题型三:字符串中的回溯问题
提示:字符串的问题的特殊之处在于,字符串的拼接生成新对象,因此在这一类问题上没有显示「回溯」的过程,但是如果使用 StringBuilder 拼接字符串就另当别论。
在这里把它们单独作为一个题型,是希望朋友们能够注意到这个非常细节的地方。
- 电话号码的字母组合(中等),题解;
- 字母大小写全排列(中等);
- 括号生成(中等) :这道题广度优先遍历也很好写,可以通过这个问题理解一下为什么回溯算法都是深度优先遍历,并且都用递归来写。
题型四:游戏问题
回溯算法是早期简单的人工智能,有些教程把回溯叫做暴力搜索,但回溯没有那么暴力,回溯是有方向地搜索。「力扣」上有一些简单的游戏类问题,解决它们有一定的难度,大家可以尝试一下。 - N 皇后(困难):其实就是全排列问题,注意设计清楚状态变量,在遍历的时候需要记住一些信息,空间换时间;
- 解数独(困难):思路同「N 皇后问题」;
- 祖玛游戏(困难)
- 扫雷游戏(困难)
46. 全排列
数不重复
解体思路
- 由于要遍历所有的元素,所以需要记录哪些元素已经被使用
- 全排列需要所有的元素,所以需要记录元素个数
class Solution {private:int len_;vector<vector<int>> res;vector<int> path;vector<int> nums;public:vector<vector<int>> permute(vector<int>& nums) {len_ = nums.size();this->nums = nums;vector<bool> vis(len_, false);dfs(vis, 0);return res;}void dfs(vector<bool>& vis, int count){if (count == len_){res.push_back(path);return;}for (int i=0; i<len_; i++){if (vis[i]) continue;path.push_back(nums[i]);vis[i] = true;dfs(vis, count+1);path.pop_back();vis[i] = false;}}};
47. 全排列 II
对比上一题
- 如何去重(剪枝)
- 记得排序
class Solution {private:int len_;vector<vector<int>> res;vector<int> path;vector<int> nums;public:vector<vector<int>> permuteUnique(vector<int>& nums) {len_ = nums.size();sort(nums.begin(), nums.end());this->nums = nums;vector<bool> vis(len_, false);dfs(vis, 0);return res;}void dfs(vector<bool>& vis, int count){if (count == len_){res.push_back(path);return;}for (int i=0; i<len_; i++){if (vis[i] || (i>0 && nums[i] == nums[i-1] && !vis[i-1]))continue;path.push_back(nums[i]);vis[i] = true;dfs(vis, count+1);path.pop_back();vis[i] = false;}}};
39. 组合总和
本题通过画图, 剪枝条件:是大于target 每次都是从当前元素向后选(因此要排序)
class Solution {private:vector<vector<int>> res_;vector<int> candidates_;vector<int> combi_;int len_;public:vector<vector<int>> combinationSum(vector<int>& candidates, int target) {sort(candidates.begin(), candidates.end());candidates_ = candidates;len_ = candidates.size();dfs(0, target);return res_;}void dfs(int idx, int target){if (target == 0){res_.push_back(combi_);return ;}for (int i=idx; i<len_&&target-candidates_[i]>=0; i++){combi_.push_back(candidates_[i]);dfs(i, target-candidates_[i]);combi_.pop_back();}}};
class Solution {private:vector<vector<int>> res_;vector<int> candidates_;vector<int> combi_;int len_;public:vector<vector<int>> combinationSum(vector<int>& candidates, int target) {sort(candidates.begin(), candidates.end());candidates_ = candidates;len_ = candidates.size();dfs(0, target);return res_;}void dfs(int idx, int target){if (target == 0){res_.push_back(combi_);return ;}if (idx>=len_ || target-candidates_[idx]<0)return ;// choose idxcombi_.push_back(candidates_[idx]);dfs(idx, target-candidates_[idx]);combi_.pop_back();// don't choose idxdfs(idx+1, target);}};
40. 组合总和 II

黑色的圈中是重复的情况,需要剪枝 条件是: 当前元素和前一个相同,且前一个没有使用到
class Solution {private:vector<vector<int>> res_;vector<int> candidates_;vector<int> combi_;vector<bool> vis_;int len_;public:vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {sort(candidates.begin(), candidates.end());candidates_ = candidates;len_ = candidates.size();vis_ = vector<bool>(len_, false);dfs(0, target);return res_;}void dfs(int idx, int target){if (target == 0){res_.push_back(combi_);return ;}for (int i=idx; i<len_&&target-candidates_[i]>=0; i++){if (i>0 && candidates_[i] == candidates_[i-1] && !vis_[i-1])continue;combi_.push_back(candidates_[i]);vis_[i] = true;dfs(i+1, target-candidates_[i]);vis_[i] = false;combi_.pop_back();}}};
下面到写法存在问题
class Solution {private:vector<vector<int>> res_;vector<int> candidates_;vector<int> combi_;vector<bool> vis_;int len_;public:vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {sort(candidates.begin(), candidates.end());candidates_ = candidates;len_ = candidates.size();vis_ = vector<bool>(len_, false);dfs(0, target);return res_;}void dfs(int idx, int target){if (target == 0){res_.push_back(combi_);return ;}if ((idx>0 && candidates_[idx] == candidates_[idx-1] && !vis_[idx-1])|| (idx >= len_)|| (target - candidates_[idx] < 0))return ;combi_.push_back(candidates_[idx]);vis_[idx] = true;dfs(idx+1, target-candidates_[idx]);vis_[idx] = false;combi_.pop_back();dfs(idx+1, target);}};
[[1,1,6],[1,2,5],[1,7]]
可以从上面的代码分析出,此写法,由于idx的状态无法从第2(3……n)开始,导致回溯到第一个元素就输出了。
77. 组合
本题思路简单
class Solution {private:vector<vector<int>> res_;vector<int> combi_;int n_;public:vector<vector<int>> combine(int n, int k) {n_ = n;dfs(1, k);return res_;}void dfs(int idx, int count){if (count == 0){res_.push_back(combi_);return ;}for (int i=idx; i<=n_; i++){combi_.push_back(i);dfs(i+1, count-1);combi_.pop_back();}}};
这里是可以执行剪枝的
if (idx+count-1 > n_) return ; // 剪枝
可以发现,如果没有涉及到重复元素,都可以选择不使用循环,回到某个状态
class Solution {private:vector<vector<int>> res_;vector<int> combi_;int n_;public:vector<vector<int>> combine(int n, int k) {n_ = n;dfs(1, k);return res_;}void dfs(int idx, int count){if (idx+count-1 > n_) return ;if (count == 0){res_.push_back(combi_);return ;}combi_.push_back(idx);dfs(idx+1, count-1);combi_.pop_back();dfs(idx+1, count);}};
78. 子集
class Solution {private:vector<vector<int>> res_;vector<int> combi_;vector<int> nums_;int n_;public:vector<vector<int>> subsets(vector<int>& nums) {nums_ = nums;n_ = nums.size();dfs(0, 0);return res_;}void dfs(int idx, int count){if (count == n_){res_.push_back(combi_);return ;}for (int i=idx; i<n_; i++){// don't choosedfs(i+1, count+1);// chooosecombi_.push_back(nums_[i]);dfs(i+1, count+1);combi_.pop_back();}}};
受到下一题的启发,哈哈
class Solution {private:vector<vector<int>> res_;vector<int> combi_;vector<int> nums_;int n_;public:vector<vector<int>> subsets(vector<int>& nums) {nums_ = nums;n_ = nums.size();dfs(0);return res_;}void dfs(int idx){res_.push_back(combi_);for (int i=idx; i<n_; i++){combi_.push_back(nums_[i]);dfs(i+1);combi_.pop_back();}}};
90. 子集 II
class Solution {private:vector<vector<int>> res_;vector<int> nums_;vector<int> combi_;vector<bool> used_;int n_;public:vector<vector<int>> subsetsWithDup(vector<int>& nums) {sort(nums.begin(), nums.end());n_ = nums.size();nums_ = nums;used_ = vector<bool>(n_, false);dfs(0, 0);return res_;}void dfs(int idx, int count){if (count == n_){res_.push_back(combi_);return;}for (int i=idx; i<n_; i++){dfs(i+1, count+1); // this is differentif (i>0 && nums_[i]==nums_[i-1] && !used_[i-1])continue;combi_.push_back(nums_[i]);used_[i] = true;dfs(i+1, count+1);used_[i] = false;combi_.pop_back();}}};
这里有个不一样到地方,就是将不选在循环的每一步都执行

通过绘图可以发现,每次dfs时,插入一个元素,得到都是一个子集,只需要注意剪枝即可
class Solution {private:vector<vector<int>> res_;vector<int> nums_;vector<int> combi_;vector<bool> used_;int n_;public:vector<vector<int>> subsetsWithDup(vector<int>& nums) {sort(nums.begin(), nums.end());n_ = nums.size();nums_ = nums;used_ = vector<bool>(n_, false);dfs(0);return res_;}void dfs(int idx){res_.push_back(combi_);for (int i=idx; i<n_; i++){if (i>0 && nums_[i]==nums_[i-1] && !used_[i-1])continue;combi_.push_back(nums_[i]);used_[i] = true;dfs(i+1);used_[i] = false;combi_.pop_back();}}};
60. 排列序列
看到题目直接就想起了直接递归到第k个然后给出答案,哈哈哈
class Solution {public:string getPermutation(int n, int k) {stringstream ss;for (int i=1; i<=n; i++)ss << i;if (k == 1) return ss.str();int i = 1;string s1 = ss.str(), res;while (next_permutation(s1.begin(), s1.end())){if (++i == k) {res = s1;break;}}return res;}};

注意两点
- 第k个数实际上是第k-1个(这里应该是从0开始的原因吧,猜测)
- 可以看到每次取start的值时,需要在前一次将数组从使用过的地方前移。
class Solution {public:string getPermutation(int n, int k) {vector<int> nums(n);iota(nums.begin(), nums.end(), 1);k--;string res;while (n--){int cur_layer_num = factorial(n);int start = k / cur_layer_num;k %= cur_layer_num;res.push_back(nums[start] + '0');// 将数组从使用到到数开始向前移动for (int j=start; j<n; j++)nums[j] = nums[j+1];}return res;}int factorial(int n){if (n == 0) return 1;return n * factorial(n-1);}};
这个思路借鉴的是比较符合我的思路的
93. 复原 IP 地址
class Solution {private:string s;vector<string> combi;vector<string> res;public:vector<string> restoreIpAddresses(string s) {this->s = s;dfs(0, s);return res;}void dfs(int count, string remainStr){if (count == 3 && isValid(remainStr)){res.push_back(vec2str(combi)+remainStr);return ;}for (int i=1; i<=3&&i<=remainStr.size(); i++){string cur = remainStr.substr(0, i);if (isValid(cur)){combi.push_back(cur + ".");dfs(count+1, remainStr.substr(i));combi.pop_back();}}}bool isValid(string& s){if (s=="" || s.size()>1 && s[0]=='0') return false;long num = stol(s);return (num>=0 && num<=255);}string vec2str(const vector<string>& v){string res;for (auto &i : v)res += i;return res;}};
这里的写法是借鉴剑指offer中的写法
解题的时候需要注意的点
- 0开头的需要排除
- 判断是否合理时:
- “”空字符串的情况
- 0*这种情况(如03.1.1.1)
class Solution {public:vector<string> restoreIpAddresses(string s) {// write code herevector<string> result;string curAns;dfs(s, result, curAns, 0); //需不需要循环要根据具体的情况而定。return result;}void dfs(string remainString, vector<string>& ans, string curAns, int count){/*回溯出口*/if (count==3 && valid(remainString))/*IP address only contain 4 parts*/{ans.emplace_back(curAns + remainString);return;}for (int i = 1; i < remainString.size()&&i<4; ++i){string sub = remainString.substr(0,i);if (valid(sub))dfs(remainString.substr(i), ans, curAns+sub+".", count + 1);}}bool valid(string& s){/*0-255合法01,这种前面的0是不合法的*/if (s.size() > 1 && s[0] == '0')return false;// stoi 会爆intreturn stol(s) >= 0 && stol(s) <= 255;}};
733. 图像渲染
这里我纠结了很久x和y 的表示 /doge
注意一开始
class Solution {vector<pair<int, int>> moves{{-1,0}, {1, 0}, {0, -1}, {0, 1}};int m, n;int newColor, oldColor;public:vector<vector<int>> floodFill(vector<vector<int>>& image, int sr, int sc, int newColor) {n = image[0].size(), m = image.size(); // n:y m:xthis->newColor = newColor;oldColor = image[sr][sc];if (newColor != oldColor)dfs(image, sr, sc);return image;}void dfs(vector<vector<int>>& image, int sr, int sc){image[sr][sc] = newColor;for (int i=0; i<4; i++){int dx = sr + moves[i].first, dy = sc + moves[i].second;if (dx>=0 && dx<m && dy>=0 && dy<n && oldColor==image[dx][dy])dfs(image, dx, dy);}}};
200. 岛屿数量
思路: 刚开始在如何记录岛屿的数量上犯难, 后面想到,用dfs将走到过的位置标记,每一次dfs,会将岛屿包含的所以的点都标记 然后遍历每一个点,如果遇到没有标记的陆地,则是新的岛屿,岛屿数量+1;
class Solution {private:int dx[4] = {-1, 1, 0, 0};int dy[4] = { 0, 0, -1, 1};int m, n;int res{0};vector<vector<bool>> vis;public:int numIslands(vector<vector<char>>& grid) {printVec(grid);m = grid.size(), n = grid[0].size();vis = vector<vector<bool>>(m, vector<bool>(n, false));for (int i=0; i<m; i++){for (int j=0; j<n; j++){if (!vis[i][j] && grid[i][j]=='1'){res++;dfs(grid, i, j);}}}return res;}void dfs(vector<vector<char>>& grid,int x, int y){vis[x][y] = true;for (int k=0; k<4; k++){int dxx, dyy;dxx = x + dx[k], dyy = y + dy[k];if (dxx>=0 && dxx<m && dyy>=0 && dyy<n && !vis[dxx][dyy] && grid[dxx][dyy]=='1')dfs(grid, dxx, dyy);}}};
130. 被围绕的区域
思路
从四周向中间遍历,最后没有被访问到地方就是被围绕到区域
本题有两个需要注意的情况:
- 最后被围绕到有可能是非陆地
- 周围陆地相邻的陆地也是岛屿
class Solution {vector<vector<bool>> vis;int m, n;int dx[4] = {-1, 1, 0, 0};int dy[4] = {0, 0, -1, 1};public:void solve(vector<vector<char>>& board) {m = board.size(), n = board[0].size();vis = vector<vector<bool>>(m, vector<bool>(n, false));for (int i=0; i<m; i++)dfs(board, i, 0, board[i][0]), dfs(board, i, n-1, board[i][n-1]);for (int j=0; j<n; j++)dfs(board, 0, j, board[0][j]), dfs(board, m-1, j, board[m-1][j]);for (int i=1; i<m-1; i++)for (int j=1; j<n-1; j++)if(!vis[i][j] && board[i][j]=='O') board[i][j] = 'X';}void dfs(vector<vector<char>>& board, int x, int y, char c){vis[x][y] = true;for (int i=0; i<4; i++){int dxx, dyy;dxx = x + dx[i], dyy = y + dy[i];if (dxx>=0 && dxx<m && dyy>=0 && dyy<n && !vis[dxx][dyy] && board[dxx][dyy]==c)dfs(board, dxx,dyy, c);}}};
79. 单词搜索
在写的时候多次忘记了回溯访问状态,哈哈
前面的题目都不需要回溯,本题需要
class Solution {
int m, n;
int dx[4] = {-1, 1, 0, 0};
int dy[4] = {0, 0, -1, 1};
vector<vector<char>> board;
string word;
bool res{false};
public:
bool exist(vector<vector<char>>& board, string word) {
m = board.size(), n = board[0].size();
this->board = board;
this->word = word;
for (int i=0; i<m; i++)
{
for (int j=0; j<n; j++)
{
if (board[i][j] != word[0]) continue;
vector<vector<bool>> vis(m, vector<bool>(n, false));
dfs(vis, i, j, 0);
if (res) return true;
}
}
return false;
}
void dfs(vector<vector<bool>>& vis, int x, int y, int idx)
{
if (idx == word.size()-1) res = true;
vis[x][y] = true;
for (int i=0; i<4; i++)
{
int dxx, dyy;
dxx = dx[i] + x, dyy = dy[i] + y;
if (dxx>=0 && dxx<m && dyy>=0 && dyy<n && !vis[dxx][dyy] && board[dxx][dyy]==word[idx+1])
dfs(vis, dxx, dyy, idx+1);
}
vis[x][y] = false; // backtrace is important
}
};
17. 电话号码的字母组合
这题和全排列很像
class Solution {
private:
int n;
vector<string> res;
string combi;
unordered_map<char, string> mp{{'2', "abc"}, {'3', "def"}, {'4', "ghi"}, {'5',"jkl"}, {'6', "mno"}, {'7',"pqrs"}, {'8', "tuv"}, {'9',"wxyz"}};
public:
vector<string> letterCombinations(string digits) {
if (digits == "") return {};
n = digits.size();
dfs(digits, 0);
return res;
}
void dfs(string& digits, int idx)
{
if (idx == n)
{
res.push_back(combi);
return;
}
string curStr = mp[digits[idx]];
for (int i=0; i<curStr.size();i++)
{
combi.push_back(curStr[i]);
dfs(digits, idx+1);
combi.pop_back();
}
}
};
22. 括号生成
虽然写出来了,但是代码有点丑 \dog
class Solution {
private:
string s;
vector<string> res;
string combi;
int n;
public:
vector<string> letterCasePermutation(string s) {
n = s.size();
this->s = s;
dfs(0);
return res;
}
void dfs(int idx)
{
if (idx == n)
{
res.push_back(combi);
return ;
}
char ch = s[idx];
combi.push_back(ch);
dfs(idx+1);
combi.pop_back();
if (islower(ch))
{
ch = toupper(ch);
combi.push_back(ch);
dfs(idx+1);
combi.pop_back();
}
else if (isupper(ch))
{
ch = tolower(ch);
combi.push_back(ch);
dfs(idx+1);
combi.pop_back();
}
}
};
22. 括号生成
这里有一个需要注意的点是:先“(” 后 “)” ==》 当前的结果中,左括号一定是大于等于右括号的
class Solution {
private:
vector<string> res;
string combi;
int n;
public:
vector<string> generateParenthesis(int n) {
this->n = n;
dfs(0, 0);
return res;
}
void dfs(int cnt1, int cnt2)
{
if (cnt1==n && cnt2==n)
{
res.push_back(combi);
return;
}
if (cnt1 < n)
{
combi.push_back('(');
dfs(cnt1+1, cnt2);
combi.pop_back();
}
if (cnt2<n && cnt2<cnt1)
{
combi.push_back(')');
dfs(cnt1, cnt2+1);
combi.pop_back();
}
}
};
51. N 皇后
yxc yyds
class Solution {
int N;
vector<bool> dg, udg, rows;
vector<vector<string>> res;
vector<string> combi;
public:
vector<vector<string>> solveNQueens(int n) {
N = n;
dg = vector<bool>(2*n, false);
udg = vector<bool>(2*n, false);
rows = vector<bool>(n, false);
combi = vector<string>(n, string(n, '.'));
dfs(0);
return res;
}
void dfs(int row)
{
if (row == N)
{
res.push_back(combi);
return;
}
for (int col=0; col<N; col++)
{
if (!rows[col] && !dg[col+row] && !udg[row+N-col])
{
combi[row][col] = 'Q';
rows[col] = dg[col+row] = udg[row+N-col] = true;
dfs(row+1);
rows[col] = dg[col+row] = udg[row+N-col] = false;
combi[row][col] = '.';
}
}
}
};
