回溯法(dfs)的总结
我不是专业搞算法的,肯定不如各路大佬总结的好,程序效率也没有大佬高。但自己总结下来至少用着比较顺手,也希望能帮到你!
就希望自己别在回溯题上磨半天了;
1. 无重复有顺序的子集
序列里面没有重复的值,要求按照原来的顺序。因为要按照原有顺序,用过的就再不能用了,所以每次idx的位置是遍历位置i+1
见图:
无重复有顺序样例:剑指 Offer II 079. 所有子集
class Solution {
private:
void dfs (int idx, vector<int> tmp,vector<int> &nums, int &size, vector<vector<int>> &res) {
// 终止条件不需要写,会在下面的遍历自动终止
for (int i = idx; i < size; ++i) { // 无重复:即不用在进入递归dfs前判断该数是否已经使用过
tmp.emplace_back(nums[i]);
res.emplace_back(tmp);
dfs(i + 1, tmp, nums, size, res); // 有顺序:即按顺序用过的就不能再用了,每次递归时传入的idx是i + 1
tmp.pop_back();
}
}
public:
vector<vector<int>> subsets(vector<int>& nums) {
vector<vector<int>> res;
res.emplace_back();
int size = nums.size();
dfs(0, {}, nums, size, res);
return res;
}
};
带层数的:如剑指 Offer II 080. 含有 k 个元素的组合
class Solution {
private:
void dfs (int idx, vector<int> tmp, int dep, int &targetDep, vector<int> &nums, int size, vector<vector<int>> &res) {
if (dep == targetDep) { // 该题终止条件为遍历深度(组合元素个数)
res.emplace_back(tmp);
return;
}
for (int i = idx; i < size; ++i) { // 无重复:即不用在递归dfs前判断是否与前一数重复
tmp.emplace_back(nums[i]);
dfs(i + 1, tmp, dep + 1, targetDep, nums, size, res); // 有顺序:即按顺序用过的就不能再用了,每次递归时传入的idx是i + 1
tmp.pop_back();
}
}
public:
vector<vector<int>> combine(int n, int k) {
vector<vector<int>> res;
vector<int> nums(n);
for (int i = 1; i <= n; ++i) nums[i-1] = i; // 方便套模板
dfs(0, {}, 0, k, nums, n, res);
return res;
}
};
2. 无重复无顺序的子集 / 全排列
**无重复:**数组里面没有重复的值,也就是进入dfs之前不需要检查当前值是否已用过;
**无顺序:**就是说每层递归,除了当前正在用的数(拿来充tmp的数)都可以继续使用,无论在该层是否用过;这里有个简单的技巧:每次递归前把不可用数(当前正在用的nums[i])和当层遍历的起始头部(即nums[idx])进行交换,如此一来就保证了每一层idx之后的数都是可用的,只需继续遍历即可。具体的解释和过程见下图:
求所有子列的话需要开tmp记录,某长度的全排列在某一层更新即可。
例程:剑指 Offer II 083. 没有重复元素集合的全排列
class Solution {
private:
void dfs (int idx, vector<int> tmp, vector<int> &nums, vector<vector<int>> &res) {
if (idx == nums.size()) {
res.push_back(tmp);
return;
}
for (int i = idx; i < nums.size(); ++i) { // 无重复:即不用在递归dfs前判断是否与前一数重复
tmp.push_back(nums[i]);
swap(nums[idx], nums[i]);
dfs(idx + 1, tmp, nums, res); // 无顺序:即按顺序用过的还能接着用,每次把不能用的nums[i]换到遍历起始的idx头部之后,递归idx + 1
swap(nums[idx], nums[i]);
tmp.pop_back();
}
}
public:
vector<vector<int>> permute(vector<int>& nums) {
vector<vector<int>> res;
dfs(0, {}, nums, res);
return res;
}
};
3. 有重复有顺序的子集
因为同一层相同数字会引出相同的枝,我们将数组排序后遇到与上一个相同的就跳过不进入dfs即可,当然也可用无序set记录使用过的元素。
注:去重是不进dfs递归,而不是进了递归再去重
样例:剑指 Offer II 082. 含有重复元素集合的组合(与上图无关)
虽然没有说有顺序,但看样例是要去除不同顺序但同元素的组合的(如[1, 1, 2]和[1, 2, 1]就算同一个),所以我们直接按有顺序,只返回固定顺序的组合。
class Solution {
private:
void dfs (int idx, int sum, int &target, vector<int> tmp, vector<int> &nums, int &size, vector<vector<int>> &res) {
if (sum > target) {
return;
} else if (sum == target) {
res.emplace_back(tmp);
return;
}
for (int i = idx; i < size; ++i) {
if (i > idx && nums[i] == nums[i - 1]) { // 有重复,就得在这里把重复的丢掉,别进循环,但最通用的还是在这里用无序set存储遍历过的数字,具体见下一节
continue;
}
sum += nums[i];
tmp.emplace_back(nums[i]);
dfs(i + 1, sum, target, tmp, nums, size, res); // 有顺序:即按顺序用过的就不能再用了,每次递归时传入的idx是i + 1
tmp.pop_back();
sum -= nums[i];
}
}
public:
vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
int candidatesSum = accumulate(candidates.begin(), candidates.end(), 0);
if (candidatesSum < target) return {};
else if (candidatesSum == target) return {candidates};
sort(candidates.begin(), candidates.end()); // 排序方便我们后续去重,当然无序set去重才更通用
int size = candidates.size();
vector<vector<int>> res;
dfs(0, 0, target, {}, candidates, size, res);
return res;
}
};
注:这道题升级一下,不要求顺序了,比如[1, 1, 2, 3],要返回的是[1, 2]、[2, 1] 和 [3]。
我们只需要一点点改动:
dfs(i + 1, sum, target, tmp, nums, size, res); // 将上面那行替换为下面这三行: swap(nums[i], nums[idx]); dfs(idx + 1, sum, target, tmp, nums, size, res); // 无顺序:即按顺序用过的还能接着用,每次把不能用的nums[i]换到遍历起始的idx头部之后,递归idx + 1 swap(nums[i], nums[idx]);
4. 有重复无顺序的子集 / 全排列
跟上一个一致,哈希表去重不进dfs就好。
注:这里注意!!!!!由于无顺序要求,有swap操作,即便排好序也有可能在交换后不满足sorted,导致重复数字无法被排除!!!
样例:剑指 Offer II 084. 含有重复元素集合的全排列
class Solution {
private:
void dfs (int idx, vector<int> tmp, vector<int> &nums, int &size, vector<vector<int>> &res) {
if (idx == size) {
res.emplace_back(tmp);
return;
}
unordered_set<int> st;
for (int i = idx; i < size; ++i) {
if (st.count(nums[i])) { // 有重复,需要记录用过的元素,用过则跳过不继续递归
continue;
}
st.emplace(nums[i]);
tmp.emplace_back(nums[i]);
swap(nums[idx], nums[i]);
dfs(idx + 1, tmp, nums, size, res); // 无顺序:即按顺序用过的还能接着用,每次把不能用的nums[i]换到遍历起始的idx头部之后,递归idx + 1
swap(nums[idx], nums[i]);
tmp.pop_back();
}
}
public:
vector<vector<int>> permuteUnique(vector<int>& nums) {
vector<vector<int>> res;
sort(nums.begin(), nums.end());
int size = nums.size();
dfs(0, {}, nums, size, res);
return res;
}
};
5. 无重复有顺序可无限选
元素能无限选,肯定得有终止条件,idx每次保持在i不变,以让当前分支走完。
图解:
如:剑指 Offer II 081. 允许重复选择元素的组合
这里有点特殊,因为虽然没有顺序要求,但是所选数字数量相同视为一样的组合。相当于有顺序,也就是我们只拿固定顺序的组合就好。
class Solution {
private:
void dfs (int idx, int sum, int &target, vector<int> tmp, vector<int> &nums, int &size, vector<vector<int>> &res) {
if (sum > target) { // 终止条件
return;
} else if (sum == target) {
res.emplace_back(tmp);
return;
}
for (int i = idx; i < size; i++) { // 无重复,在进入dfs前不用去重
int num = nums[i];
sum += num;
tmp.emplace_back(num);
dfs (i, sum, target, tmp, nums, size, res); // 可无限选,标志就是传入idx在单根上保持i不变,可以一直用1、1、1、1、……直到终止条件成立再继续遍历下一个i
tmp.pop_back();
sum -= num;
}
}
public:
vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
vector<vector<int>> res;
int size = candidates.size();
dfs(0, 0, target, {}, candidates, size, res);
return res;
}
};
6. 无重复无顺序可无限选
我们把剑指 Offer II 081. 允许重复选择元素的组合升级一下:不同顺序也视为不同的组合,就是这种情况了。
图解:
class Solution {
private:
void dfs(int idx, int sum, int &target, vector<int> tmp, vector<int> &nums, int &size, vector<vector<int>> &res) {
if (sum > target) {
return;
} else if (sum == target) {
res.emplace_back(tmp);
return;
}
for (int i = idx; i < size; i++) { // 无重复,在进入dfs前不用去重
int num = nums[i];
sum += num;
tmp.emplace_back(num);
swap(nums[i], nums[idx]); // 无顺序要求,标志就是交换,相当于我每次都把idx位的元素置成当前遍历到的i位置元素
dfs(i, sum, target, tmp, nums, size, res); // 可无限选,标志就是传入idx在单根上不变,可以一直用1、1、1、1、……直到终止
swap(nums[i], nums[idx]);
tmp.pop_back();
sum -= num;
}
}
public:
vector<vector<int>> combinationSum(vector<int> &candidates, int target) {
vector<vector<int>> res;
int size = candidates.size();
dfs(0, 0, target, {}, candidates, size, res);
return res;
}
};
注: 这里把传入的数到底是哪一个搞清楚。其实可以把dfs传入的数换成idx再改一下swap等的顺序,固定取每层第一个数,但是为了和有顺序的保持一致,还是用了i