前言
如果要构造长度为\(2\)的字符串,可以写一个二重循环:
for x in "abc"for y in "def"
外层枚举第\(1\)个字母,内层枚举第\(2\)个字母,这样可以。
但是如果要构造长度为\(3\)或者\(4\)或者不确定呢?
原问题:构造长度为\(n\)的字符串\(\rightarrow\)枚举\(1\)个字母
子问题:构造长度为\(n-1\)的字符串
像这样增量构造答案的过程通常用递归实现。
思考回溯问题:
- 当前操作?枚举\(path[i]\)要填入的字母
- 子问题?dfs(i)表示构造字符串\(\geq i\)的部分
- 下一个子问题?构造字符串\(\geq i+1\)的部分
1 电话号码的字母组合

1.1 代码实现
点击查看代码
class Solution {
public:vector<string> letterCombinations(string digits) {unordered_map<char, string> hash_table {{'2', "abc"},{'3', "def"},{'4', "ghi"},{'5', "jkl"},{'6', "mno"},{'7', "pqrs"},{'8', "tuv"},{'9', "wxyz"},};vector<string> ans;int n = digits.size();// dfs(i)表示确定第i个索引对应的数字string path(n, 0);auto dfs = [&](this auto&& dfs, int i) {if (i == n) {ans.emplace_back(path);return;}for (auto& c: hash_table[digits[i]]) {path[i] = c;dfs(i + 1);}};dfs(0);return ans;}
};
- 时间复杂度:\(n*4^n\)(可以从答案的角度来理解,\(digits\)的长度为\(n\),那么第\(1\)个字符就有\(4\)种可能,不断组合,最多有\(4^n\)种;又因为每次记录答案都需要\(O(n)\)的拷贝。)
- 空间复杂度:\(O(n)\)
子集型回溯(包括\(0-1\)背包问题)
2 子集

2.1 解题思路
2.1.1 法一:
站在输入的角度思考问题。
每个元素都可以选 or 不选。
根节点是空的,对于第\(1\)个元素,选 or 不选,生成二叉树;然后左根节点 和 又根节点 再对第\(2\)个元素进行判断,选 or 不选......叶子节点是答案。
思考回溯问题:
- 当前操作?枚举第\(i\)个数,选 or 不选
- 子问题?dfs(i)表示从下标\(\geq i\)的数字中构造子集
- 下一个子问题?从下标\(\geq i+1\)的数字中构造子集
2.1.2 法二:
站在构造答案的角度思考问题。
根节点为空。
每次必须选一个数,枚举答案的第\(1\)个数选谁,可能是\(1,2,3\);然后下一层,看第\(2\)个数选谁。如果第\(1\)层选择了\(1\),那么第二层可能选择\(2,3\)......每个节点都是答案。
\([1,2]\)和\([2,1]\)本质是同一个子集
思考回溯问题:
- 当前操作?枚举一个下标为\(j \geq i\)的数字
- 子问题?从下标\(\geq i\)的数字中构造子集
- 下一个子问题?从下标\(\geq j+1\)的数字中构造子集
2.2 代码实现
2.2.1 法一:
点击查看代码
class Solution {
public:vector<vector<int>> subsets(vector<int>& nums) {vector<vector<int>> ans;int n = nums.size();if (n == 0) {return {};}vector<int> subset;auto dfs = [&](this auto&& dfs, int i) {if (i == n) {ans.push_back(subset);return;}// 如果选择的话subset.emplace_back(nums[i]);dfs(i + 1);subset.pop_back();// 如果不选择当前元素dfs(i + 1);};dfs(0);return ans;}
};
- 时间复杂度:\(O(n*2^n)\)
- 空间复杂度:\(O(n)\)
2.2.2 法二:
点击查看代码
class Solution {
public:vector<vector<int>> subsets(vector<int>& nums) {vector<vector<int>> ans;int n = nums.size();if (n == 0) {return {};}vector<int> subset;auto dfs = [&](this auto&& dfs, int i) {ans.emplace_back(subset);if (i == n) {return;}for (int j = i; j < n; ++j) {subset.emplace_back(nums[j]);dfs(j + 1);subset.pop_back();}};dfs(0);return ans;}
};
3 分割回文串

根据子集型回溯思考问题,看看自己掌握了没有?
3.1 解题思路
3.1.1 法一:
根据示例\(1\),枚举\([a,a,b]\)的两个逗号,选 or 不选。
思考回溯问题:
- 当前操作?枚举第\(i\)个逗号,选 or 不选
- 子问题?从下标\(\geq i\)的数字中构造子集
- 下一个子问题?从下标\(\geq i+1\)的数字中构造子集
3.1.2 法二:
站在构造答案的角度。
回溯三问:
- 当前操作?选择回文串\(s[i\cdots j]\)
- 子问题?从下标\(\geq i\)的后缀中构造回文串
- 下一个子问题?从下拨\(\geq j + 1\)的后缀中构造回文串
3.2 代码实现
3.2.1 法一
点击查看代码
class Solution {bool isPalindrome(string& s, int left, int right) { // [left, right]while (left < right) {if (s[left] != s[right]) {return false;}left++, right--;}return true; }
public:vector<vector<string>> partition(string s) {// 应用法一:选 or 不选int n = s.size();if (n == 0) {return {};}vector<vector<string>> ans;vector<string> substr;// dfs(i, start) i表示i后面的逗号选 or 不选, start 当前回文串的起始位置auto dfs = [&](this auto&& dfs, int i, int start) {if (i == n - 1) {if (isPalindrome(s, start, i)) {substr.emplace_back(s.substr(start, i - start + 1));ans.emplace_back(substr);substr.pop_back();}return;}// 加逗号if (isPalindrome(s, start, i)) {substr.emplace_back(s.substr(start, i - start + 1));dfs(i + 1, i + 1);substr.pop_back();}// 不加dfs(i + 1, start);};dfs(0, 0);return ans;}
};
3.2.2 法二
点击查看代码
class Solution {bool isPalindrome(string& s, int left, int right) { // [left, right]while (left < right) {if (s[left] != s[right]) {return false;}left++, right--;}return true; }
public:vector<vector<string>> partition(string s) {// 应用法二:从构造答案的角度int n = s.size();if (n == 0) {return {};}vector<vector<string>> ans;vector<string> substr;// dfs(i)表示以第i个字符为起点,枚举字符串结束的位置auto dfs = [&](this auto&& dfs, int i) {if (i == n) {ans.emplace_back(substr);return;}// 加逗号for (int j = i; j < n; ++j) {if (isPalindrome(s, i, j)) {substr.emplace_back(s.substr(i, j - i + 1));dfs(j + 1);substr.pop_back();}}};dfs(0);return ans;}
};
- 时间复杂度:\(O(n2^n)\) 从答案的角度理解,选 or 不选一共有\(2^n-2\)情况,拷贝又至多是\(O(n)\)的,所以是\(O(n2^n)\)的。
- 空间复杂度:\(O(n)\)
组合型回溯
4 组合

4.1 解题思路
4.1.1 法一
在子集[构造答案]的基础上增加逻辑判断减枝即可。
从 \(n\) 个数中选 \(k\) 个数的组合可以看成长度固定的子集。
4.1.2 法二
选 or 不选
4.2 代码实现
4.2.1 法一
点击查看代码
class Solution {
public:vector<vector<int>> combine(int n, int k) {vector<vector<int>> ans;vector<int> subset;/*回溯三问:当前操作:选择第 $j >= i$ 元素原问题:dfs(i)表示选择第 $j >= i$ 元素,构造子集子问题:dfs(i+1)表示选择 $> j$的元素*/// 优化1:我们是从小到大枚举的,枚举到哪1个数一定无法满足k个数了呢/*如果当前选择的元素数量为 size(),那么就还需要 k - size()个数,如果n - i + 1< k - size(第i个数还没选),直接返回*/auto dfs = [&](this auto&& dfs, int i) {if (n - i + 1 < k - subset.size()) {return;}if (subset.size() == k) {ans.emplace_back(subset);return;}for (int j = i; j <= n; ++j) {subset.push_back(j);dfs(j + 1);subset.pop_back();}};dfs(1);return ans;}
};
- 时间复杂度: \(O(kC^n_k)\)
- 空间复杂度:\(O(k)\)
注:如果说倒序枚举,设 \(path\) 长为 \(m\),那么还需要选 \(d=k-m\) 个数。设当前需要从 \([1,i]\) 这 \(i\) 个数中选,那么 \(i < d\) 时,必然无法选出 \(k\) 个数,不需要再递归。
4.2.2 法二
点击查看代码
class Solution {
public:vector<vector<int>> combine(int n, int k) {// 选 or 不选/*回溯三问:当前的操作?第 $i$ 个数 选 or 不选原问题 从 $n$ 个数中选择 $k$ 个数子问题 从 $i + 1 ~n$个数中选择 $k-1$ 个数*/vector<vector<int>> ans;vector<int> subset;auto dfs = [&](this auto&& dfs, int i) {if (subset.size() == k) {ans.emplace_back(subset);return;}if (i == n + 1) {return;}// 选subset.push_back(i);dfs(i + 1);subset.pop_back();// 不选dfs(i + 1);};dfs(1);return ans;}
};
- 时间复杂度:\(O(kC^k_n)\)
- 空间复杂度:\(O(k)\)
5 组合总和 III


5.1 解题思路
5.1.1 选 or 不选
5.1.2 构造答案
设还需要选择 \(d = k - m\) 个数字
设还需要选和为 \(t\) 的数字
(初始为 \(n\),每选一个数字 \(j\),就把 \(t\) 减小 \(j\))
剪枝:
- 剩余数字数目不够 \(i \leq d\)
- \(t \leq 0\)
- 剩余数字即使全部选最大的,和也不够 \(t\),例如 \(i=5\),还需要选 \(d=3\) 个数,那么如果 \(t > 5 + 4 + 3\),可以直接返回。
5.2 代码实现
5.2.1 法一
点击查看代码
class Solution {
public:vector<vector<int>> combinationSum3(int k, int n) {// 选 or 不选/* 剪枝 优化设还需要 d = k - subset.size() 个数字设还需要选择和为 target 的数字1. 剩余数字不够 d 个, i < d2. target <= 03. 选择最大的 d 个数字, target依然 > 0 可以直接返回i + (i - 1) + ... + (i - d + 1) = d(i + i -d + 1)/2如果说d = 2*/vector<vector<int>> ans;vector<int> subset;int target = n;auto dfs = [&](this auto&& dfs, int i) {int d = k - subset.size();if (i < d || target < 0 || target > d*(2*i-d+1)/2) {return;}if (subset.size() == k) {ans.emplace_back(subset);return;}// 选subset.push_back(i);target -= i;dfs(i - 1);subset.pop_back();target += i;// 不选dfs(i - 1);};dfs(9);return ans;}
};
5.2.2 法二
点击查看代码
class Solution {
public:vector<vector<int>> combinationSum3(int k, int n) {// 构造答案的角度vector<vector<int>> ans;vector<int> subset;/*回溯三问:当前操作?原问题: dfs(i) 选第 $j \geq i$ 个数子问题:dfs(i + 1) 选第 $ \geq j + 1$ 个数*//* 剪枝优化1. 剩余数字数目不够2. target < 03. 剩余数字即使全部选择最大的,和也不够 target最大的数字是 i,还需要 d 个例如 i = 5,还需要 3 个target > 5 + 4 + 3条件应该是 target > i + (i - 1) + ... (i - d + 1) = d(i + i - d + 1) / 2*/int target = n;auto dfs = [&](this auto&& dfs, int i) {int d = k - subset.size();if (i < d || target < 0 || target > d*(2*i - d + 1) / 2) {return;}if (subset.size() == k) {ans.emplace_back(subset);return;}for (int j = i; j >= 1; --j) {target -= j;subset.push_back(j);dfs(j - 1);subset.pop_back();target += j;}};dfs(9);return ans;}
};
6 括号生成

6.1 解题思路
6.1.1 选 or 不选
- 对于字符串的前缀,左括号的个数一定要大于等于右括号的个数。
- 左括号的个数是 \(n\)。
这道题可以看成是从 \(2*n\) 个位置中选 \(n\) 个位置放置左括号。
对于一个位置,你选择,可以认为是放置左括号;你不选择,就等价于放置右括号。
上述是 选 or 不选的思路。
6.1.2 枚举下一个左括号的位置
6.2 代码实现
6.2.1 法一
点击查看代码
class Solution {
public:vector<string> generateParenthesis(int n) {// 选 or 不选/*回溯三问: 当前操作?枚举 $path[i]$是左括号还是右括号子问题? 构造 $\geq i$ 的部分下一个子问题? 构造 $\geq i + 1$ 的部分*/vector<string> ans;string str(2 * n, 0);int left_cnt = 0;auto dfs = [&](this auto&& dfs, int i) {if (i == 2 * n) {ans.emplace_back(str);return;}// 选// 需要选 $n$ 个左括号,只要左括号的个数小于 $n$ 就可以选择左括号if (left_cnt < n) {str[i] = '(';left_cnt += 1;dfs(i + 1);left_cnt -= 1;}// 不选// 右括号的个数为 $i - left_cnt$,如果右括号的个数 < 左括号的个数,那么可以选择右括号if (i - left_cnt < left_cnt) {str[i] = ')';dfs(i + 1);}};dfs(0);return ans;}
};
- 时间复杂度:\(O(2nC^n_{2n})\)
- 空间复杂度:\(O(n)\)
6.2.2 枚举下一个左括号的位置
还是太抽象了,搞不明白,真遇上了再说吧。
排列型回溯
7 全排列

7.1 解题思路
回溯三问:
数组 \(path\) 记录路径上的数(已选数字),集合 \(s\) 记录未选数字。
当前操作?从集合 \(s\) 中枚举 \(path[i]\) 要填入的数字 \(x\)
子问题? 构造排列 \(\geq i\) 的部分,剩余未选数字集合为 \(s\)
下一个子问题? 构造排列 \(\geq i + 1\) 的部分,剩余未选数字集合为 \(s-\{x\}\)
7.2 代码实现
7.2.1 \(bool\) 数组
点击查看代码
class Solution {
public:vector<vector<int>> permute(vector<int>& nums) {vector<vector<int>> ans;int n = nums.size();vector<int> temp(n, 0);vector<int> visited(n, false);auto dfs = [&](this auto&& dfs, int i) {if (i == n) {ans.emplace_back(temp);return;}for (int j = 0; j < n; ++j) {if (!visited[j]) {temp[i] = nums[j];visited[j] = true;dfs(i + 1);visited[j] = false;}}};dfs(0);return ans;}
};
7.2.2 哈希表
点击查看代码
class Solution {
public:vector<vector<int>> permute(vector<int>& nums) {vector<vector<int>> ans;int n = nums.size();vector<int> temp(n, 0);unordered_set<int> hash_table;auto dfs = [&](this auto&& dfs, int i) {if (i == n) {ans.emplace_back(temp);return;}for (int j = 0; j < n; ++j) {int x = nums[j];if (!hash_table.contains(x)) {hash_table.insert(x);temp[i] = x;dfs(i + 1);hash_table.erase(x);}}};dfs(0);return ans;}
};
- 时间复杂度: \(O(n*n!)\)
解释:对于长度为 \(n\) 的数组,全排列的总数是 \(n!\)
每生成一个排列,需要执行 \(n\) 次操作
无论是哈希表还是 \(bool\)数组,查询时间都是 \(O(1)\)。 - 空间复杂度: \(O(n)\)
解释:递归栈深度 + temp 数组 + hash_table 的空间,都是 O (n) 级别
8 \(N\) 皇后


8.1 解题思路
不同行,不同列 \(\rightarrow\) 每行每列恰好有一个皇后
证明:反证法
假设有一行,一个皇后都没有。那么剩下 \(n - 1\) 行,需要放 \(n\) 个皇后,那么必然有一行至少要放 \(2\) 个皇后,矛盾,所以每行恰好有一个皇后。
用一个长度为 \(n\) 的数组 \(col\) 记录皇后的位置,即第 \(i\) 行的皇后在第 \(col\) 列,那么 \(col\) 将是 \(0 ~ n - 1\) 的排列。
如图1 $\begin{bmatrix}1 & 3 & 0 & 2\end{bmatrix};
如图2 $\begin{bmatrix}2 & 1 & 3 & 1\end{bmatrix}。
于是,变成枚举 \(col\) 的全排列,每行只选一个,每列只选一个,同时还要判断右上(x+y=c)或者左下(x-y=c)是否有其他皇后。
8.2 代码实现
点击查看代码
class Solution {
public:vector<vector<string>> solveNQueens(int n) {// 全排列vector<vector<string>> ans;vector board(n, string(n, '.')); // 一开始棋盘是空的unordered_set<int> hash_table1; // 记录 r - cunordered_set<int> hash_table2; // 记录 r + cvector<bool> col(n, false);// r 表示当前要枚举的行号auto dfs = [&](this auto&& dfs, int r) {if (r == n) {ans.emplace_back(board);return;}// 在 (r, c) 放皇后for (int c = 0; c < n; ++c) {if (!hash_table1.contains(r + c) && !hash_table2.contains(r - c) && !col[c]) {hash_table1.insert(r + c);hash_table2.insert(r - c);board[r][c] = 'Q';col[c] = true;dfs(r + 1);hash_table1.erase(r + c);hash_table2.erase(r - c);board[r][c] = '.';col[c] = false;}}};dfs(0);return ans;}
};
这里的话判断对角条件的哈希表也可以替换成 \(bool\) 数组,然后为了解决索引是负数,需要添加一个偏移量,负数最大为 \(0 - (n - 1)\)(row-col),所以 \(+(n - 1)\)即可。
- 时间复杂度:\(O(n^2n!)\)
- 空间复杂度:\(O(n)\)
完结撒花!