视频地址 :
回溯算法套路①子集型回溯【基础算法精讲 14】_哔哩哔哩_bilibili
基本概念
1 . 例子
例如从abc和def(n = 2)中各选出一个组成新的字符串?
如果n很大 , 这个时候for循环的表达能力有限 ;
2 . 原问题 和 子问题
3 . 增量构造答案
这个增量构造答案的过程就是回溯的特点 ;
回溯三问
关于恢复现场的理解
在递归到某一“叶子节点”(非最后一个叶子)时,答案需要向上返回,而此时还有其他的子树(与前述节点不在同一子树)未被递归到,又由于path为全局变量。若直接返回,会将本不属于该子树的答案带上去,故需要恢复现场。
恢复现场的方式为:在递归完成后 dfs(i+1); 后,进行与“当前操作”相反的操作,“反当前操作”。
17 . 电话号码的字母组合
这也就是n(n=8)个字符串中选两个的组合问题 ;
思路 :
1 . 先创建一个下标 与 对应字符串映射的数组,这里使用hash表进行映射也是可以的 ;
2 . 对于回溯 , 如果到了叶子节点 , 那么就直接添加即可 , 对于每一个path[i],都是存放digits[i]中数字字符对应字符串中的一个字符 , 遍历该字符串 , 对每一个字符进行递归操作 ;
3 . 对于不用恢复现场 : 因为每次递归到 i,一定会修改 path【i】,那么递归到终点时,每个 path【i】 都被覆盖成要枚举的字母,所以不需要恢复现场。
代码实现 :
class Solution {
public:
const string mp[10] = {
"", //0
"", //1
"abc", //2
"def", //3
"ghi", //4
"jkl", //5
"mno", //6
"pqrs", //7
"tuv", //8
"wxyz", //9
};
vector<char> path ;
vector<string> ans ;
int n ;
string get(){
string tmp = "" ;
for(char c : path){
tmp += c ;
}
return tmp ;
}
void dfs(string digits , int i){
// 先写递归终止条件
if(i == n){
ans.push_back(get()) ;
return ; // 回溯
}
for(char c : mp[digits[i]-'0']){
path[i] = c ;
dfs(digits , i + 1) ;
// 因为每一次path[i]都会被覆盖 , 那么就不用进行回溯
}
}
vector<string> letterCombinations(string digits) {
n = digits.size() ;
if(n == 0) return ans ;
path.resize(n) ;
dfs(digits , 0) ;
return ans ;
}
};
更加优雅的写法 :
class Solution {
string MAPPING[10] = {"", "", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"};
public:
vector<string> letterCombinations(string digits) {
int n = digits.length();
if (n == 0) return {};
vector<string> ans;
string path(n, 0); // 本题 path 长度固定为 n
function<void(int)> dfs = [&](int i) {
if (i == n) {
ans.emplace_back(path);
return;
}
for (char c : MAPPING[digits[i] - '0']) {
path[i] = c; // 直接覆盖
dfs(i + 1);
}
};
dfs(0);
return ans;
}
};
78 . 子集
链接 :
. - 力扣(LeetCode)
法一(对于每一个数选或不选) :
class Solution {
public:
vector<vector<int>> ans ;
vector<int> path ;
void dfs(vector<int>& nums , int i){
if(i==nums.size()){
ans.push_back(path) ;
return ;// 回溯
}
// 不选
dfs(nums,i+1) ;// 不选i
// 选
path.push_back(nums[i]) ;
dfs(nums,i+1) ;
path.pop_back() ;
}
vector<vector<int>> subsets(vector<int>& nums) {
if(nums.size()==0) return ans ;
ans.clear() ;
path.clear() ;
dfs(nums,0) ;
return ans ;
}
};
法二(枚举第i个数选谁):
这样的话 每一个结点都是答案 ;
class Solution {
public:
vector<vector<int>> ans;
vector<int> path;
void backtrack(vector<int>& nums,int idx){
ans.push_back(path);
if(idx >= nums.size()) return;
for(int i=idx;i<nums.size();i++){
path.push_back(nums[i]);
backtrack(nums,i+1);
path.pop_back();
}
}
vector<vector<int>> subsets(vector<int>& nums) {
path.clear();
ans.clear();
backtrack(nums,0);
return ans;
}
};
131 . 分割回文串
链接 :
. - 力扣(LeetCode)
思路 :
代码 (枚举子串结束位置):
class Solution {
public:
bool pd(string s , int l ,int r){
while(l <= r){
if(s[l++] != s[r--])
return false;
}
return true ;
}
vector<vector<string>> ans ;
vector<string> path ;
int n = 0 ;
// 答案视角 , 判断每一个子串结束的地方
void dfs(string s , int i){
if(i==n){
ans.push_back(path) ;
return ;
}
for(int j=i;j<n;j++){ // 枚举子串结束的位置
if(pd(s,i,j)){
path.push_back(s.substr(i,j-i+1)) ;
dfs(s,j+1) ;
path.pop_back();// 恢复现场
}
}
}
vector<vector<string>> partition(string s) {
n = s.size() ;
dfs(s,0) ;
return ans ;
}
};
代码(每个分割点选不选) :
class Solution {
public:
bool pd(string s , int l ,int r){
while(l <= r){
if(s[l++] != s[r--])
return false;
}
return true ;
}
vector<vector<string>> partition(string s) {
vector<vector<string>> ans ;
vector<string> path ;
// 判断每个逗号选或不选
int n = s.size() ;
// start 表示这一段回文串的开始位置
function<void(int,int)> dfs = [&](int i,int start){
if(i==n){
ans.push_back(path) ;
return ;
}
// 不选i-i+1
if(i < n - 1){
dfs(i+ 1, start) ;
}
// 选
if(pd(s,start,i)){
path.push_back(s.substr(start,i-start+1));
dfs(i + 1 , i + 1 ) ;
path.pop_back() ;
}
};
dfs(0 , 0) ;
return ans ;
}
};