重复的元素等效于一个,比如 {4,5} 和 {5,4} 是同一个子集。
需要对每次进行元素选取的集合进行修改的话就需要使用index进行起始位置修改
dfs的每一层都是对当前集合的遍历
可能还需要使用数组辅助进行已使用的元素标记
n个集合求组合
n个集合求组合
给你一个例子,假设有3个集合:
set1 = {1, 2}
set2 = {a, b}
set3 = {x, y}
使用回溯法求组合的代码如下:
class Solution {
private:
vector<vector<string>> res; // 存储最终结果
vector<string> path; // 当前组合路径
void backtrack(vector<vector<string>>& sets, int depth) {
// 终止条件:已经选择了k个元素(每个集合选一个)
if (depth == sets.size()) {
res.push_back(path);
return;
}
// 遍历当前集合的所有元素
for (int i = 0; i < sets[depth].size(); i++) {
// 选择当前元素
path.push_back(sets[depth][i]);
// 递归到下一个集合
backtrack(sets, depth + 1);
// 回溯,撤销选择
path.pop_back();
}
}
public:
vector<vector<string>> combine(vector<vector<string>>& sets) {
backtrack(sets, 0);
return res;
}
};
解释一下搜索过程:
- depth = 0(第一层):
- 选择set1中的1或2
- depth = 1(第二层):
- 选择set2中的a或b
- depth = 2(第三层):
- 选择set3中的x或y
对于上面的例子,会生成如下组合:
[1,a,x], [1,a,y], [1,b,x], [1,b,y]
[2,a,x], [2,a,y], [2,b,x], [2,b,y]
这里depth(深度)代表:
- 当前正在处理第几个集合
- 当depth等于集合数量时,表示找到一个完整组合
总结:
- 每层代表一个集合的选择
- 每层的选择是独立的,不需要去重
- 路径长度等于集合数量时得到一个完整组合
77 组合(组合型枚举)
链接
给定两个整数 n 和 k,返回 1 … n 中所有可能的 k 个数的组合。 示例: 输入: n = 4, k = 2 输出: [ [2,4], [3,4], [2,3], [1,2], [1,3], [1,4], ]
思路
- 需要循环(每一个集合的大小)
- 回溯函数的参数确定
- 需要索引
- 当路径长度等于要求的k时,说明找到一个合法组合
- n是可选的
- 深度指的当前path的大小
class Solution {
public:
vector<vector<int>> res;
void dfs(vector<int> &path, int start, int n, int k) {
if(path.size() == k) {
res.push_back(path);
return;
}
for(int i = start; i <= n; i++) {
path.push_back(i); // 应该是将 i 加入 path,而不是加入 res
dfs(path, i + 1, n, k);
path.pop_back();
}
}
vector<vector<int>> combine(int n, int k) {
vector<int> path;
dfs(path, 1, n, k); // start 应该从 1 开始,因为题目要求从 1 开始
return res;
}
};
class Solution {
private:
vector<vector<int>> result;
vector<int> path;
void backtracking(int n, int k, int startIndex) {
if (path.size() == k) {
result.push_back(path);
return;
}
for (int i = startIndex; i <= n - (k - path.size()) + 1; i++) { // 优化的地方
path.push_back(i); // 处理节点
backtracking(n, k, i + 1);
path.pop_back(); // 回溯,撤销处理的节点
}
}
public:
vector<vector<int>> combine(int n, int k) {
backtracking(n, k, 1);
return result;
}
};
39 组合总和
链接
给定一个无重复元素的数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。
candidates 中的数字可以无限制重复被选取。
说明:
- 所有数字(包括 target)都是正整数。
- 解集不能包含重复的组合。
示例 1: - 输入:candidates = [2,3,6,7], target = 7,
- 所求解集为: [ [7], [2,2,3] ]
示例 2: - 输入:candidates = [2,3,5], target = 8,
- 所求解集为: [ [2,2,2,2], [2,3,3], [3,5] ]
思路
- 怎么达到可以重复选,已知dfs的index参数即对每一个集合的大小进行约束,那么使得index=i即可
- 每一层的sum可以使用局部变量进行传递,即sum+candidate[i]
class Solution {
public:
vector<vector<int>> res;
void backtracing(vector<int>& candidates,vector<int>& path,int index,int sum,int target){
if(sum == target){
res.push_back(path);
return;
}
if(sum>target) return;
for(int i = index;i<candidates.size();i++){
path.push_back(candidates[i]);
backtracing(candidates,path,i,sum+candidates[i],target);
path.pop_back();
}
}
vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
vector<int> path;
int sum = 0;
int index = 0;
backtracing(candidates,path,index,sum,target);
return res;
}
};
40 组合总和2
链接
给定一个数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。
candidates 中的每个数字在每个组合中只能使用一次。
说明: 所有数字(包括目标数)都是正整数。解集不能包含重复的组合。
- 示例 1:
- 输入: candidates = [10,1,2,7,6,1,5], target = 8,
- 所求解集为:
[
[1, 7],
[1, 2, 5],
[2, 6],
[1, 1, 6]
]
- 示例 2:
- 输入: candidates = [2,5,2,1,2], target = 5,
- 所求解集为:
[
[1,2,2],
[5]
]
思路
对同一层的树进行去重
对纵向的要使用index控制不使用同一个元素
if (i > startIndex && candidates[i] == candidates[i - 1]) {continue;
}
这一步比较难想,但是如果要跳过相同的就需要一开始就对数组进行排序
不用担心深度搜索的时候又遇到1,因为是去重后才进行dfs,比如{1,1,1,…}不需要担心在跳过2个1后dfs还会遇到1
- 第一层(startIndex=0):
- 选第一个1:继续搜索 [1,…]
- 跳过第二个1(因为重复)
- 选2:继续搜索 [2,…]
- 选5:继续搜索 [5,…]
- 当选了第一个1后,进入第二层(startIndex=1):
- 此时可以选第二个1(因为i=startIndex)
- 可以选2
- 可以选5
- 当选了第二个1后,进入第三层(startIndex=2):
- 可以选2
- 可以选5
同一层不会选择重复的数字 不同层次可以选择相同的数字 避免产生重复的组合
class Solution {
private:
vector<vector<int>> result;
vector<int> path;
void backtracking(vector<int>& candidates, int target, int sum, int startIndex) {
if (sum == target) {
result.push_back(path);
return;
}
for (int i = startIndex; i < candidates.size() && sum + candidates[i] <= target; i++) {
// 要对同一树层使用过的元素进行跳过
if (i > startIndex && candidates[i] == candidates[i - 1]) {
continue;
}
sum += candidates[i];
path.push_back(candidates[i]);
backtracking(candidates, target, sum, i + 1); // 和39.组合总和的区别1,这里是i+1,每个数字在每个组合中只能使用一次
sum -= candidates[i];
path.pop_back();
}
}
public:
vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
path.clear();
result.clear();
// 首先把给candidates排序,让其相同的元素都挨在一起。
sort(candidates.begin(), candidates.end());
backtracking(candidates, target, 0, 0);
return result;
}
};
216 组合总和3
简化版的组合总和1
找出所有相加之和为 n 的 k 个数的组合。组合中只允许含有 1 - 9 的正整数,并且每种组合中不存在重复的数字。
说明:
- 所有数字都是正整数。
- 解集不能包含重复的组合。
示例 1: 输入: k = 3, n = 7 输出: [[1,2,4]]
示例 2: 输入: k = 3, n = 9 输出: [[1,2,6], [1,3,5], [2,3,4]]
思路
class Solution {
private:
vector<vector<int>> result; // 存放结果集
vector<int> path; // 符合条件的结果
// targetSum:目标和,也就是题目中的n。
// k:题目中要求k个数的集合。
// sum:已经收集的元素的总和,也就是path里元素的总和。
// startIndex:下一层for循环搜索的起始位置。
void backtracking(int targetSum, int k, int sum, int startIndex) {
if (path.size() == k) {
if (sum == targetSum) result.push_back(path);
return; // 如果path.size() == k 但sum != targetSum 直接返回
}
for (int i = startIndex; i <= 9; i++) {
sum += i; // 处理
path.push_back(i); // 处理
backtracking(targetSum, k, sum, i + 1); // 注意i+1调整startIndex
sum -= i; // 回溯
path.pop_back(); // 回溯
}
}
public:
vector<vector<int>> combinationSum3(int k, int n) {
result.clear(); // 可以不加
path.clear(); // 可以不加
backtracking(n, k, 0, 1);
return result;
}
};
17 电话号码的字母组合
给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。
给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。
示例:
- 输入:“23”
输出:["ad", "ae", "af", "bd", "be", "bf", "cd", "ce", "cf"].
说明:尽管上面的答案是按字典序排列的,但是你可以任意选择答案输出的顺序。
思路
其实就是组合不过组合的集合需要另外求取不是一开始就给定
多个集合求组合
for (int i = startIndex; i <= 9; i++) {
sum += i; // 处理
path.push_back(i); // 处理
backtracking(targetSum, k, sum, i + 1); // 注意i+1调整startIndex
sum -= i; // 回溯
path.pop_back(); // 回溯
}
再进行不同集合求组合之前需要先取出集合
const string& letters = letterMap[digits[index] - '0'];
const string& letters = letterMap[digits[index] - '0'];
// 遍历当前数字对应的所有字母
for (char letter : letters) {
path.push_back(letter);
backtracking(digits, index + 1, path, res);
path.pop_back();
}
class Solution {
private:
const vector<string> letterMap = {
"", "", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"
};
public:
void backtracking(const string& digits, int index, string& path, vector<string>& res) {
if (index == digits.size()) {
res.push_back(path);
return;
}
// 获取当前数字对应的字母集合
const string& letters = letterMap[digits[index] - '0'];
// 遍历当前数字对应的所有字母
for (char letter : letters) {
path.push_back(letter);
backtracking(digits, index + 1, path, res);
path.pop_back();
}
}
vector<string> letterCombinations(string digits) {
if (digits.empty()) return {};
// 预计算结果大小
vector<string> res;
res.reserve(pow(4, digits.size())); // 最大可能有4个字母的情况
string path;
path.reserve(digits.size()); // 预分配路径大小
backtracking(digits, 0, path, res);
return res;
}
};
复原IP地址
链接
有效 IP 地址 正好由四个整数(每个整数位于 0
到 255
之间组成,且不能含有前导 0
),整数之间用 '.'
分隔。
- 例如:
"0.1.2.201"
和"192.168.1.1"
是 有效 IP 地址,但是"0.011.255.245"
、"192.168.1.312"
和"[email protected]"
是 无效 IP 地址。
给定一个只包含数字的字符串s
,用以表示一个 IP 地址,返回所有可能的有效 IP 地址,这些地址可以通过在s
中插入'.'
来形成。你 不能 重新排序或删除s
中的任何数字。你可以按 任何 顺序返回答案。
示例 1:
输入:s = “25525511135”
输出:[“255.255.11.135”,“255.255.111.35”]
示例 2:
输入:s = “0000”
输出:[“0.0.0.0”]
示例 3:
输入:s = “101023”
输出:[“1.0.10.23”,“1.0.102.3”,“10.1.0.23”,“10.10.2.3”,“101.0.2.3”]
提示:
1 <= s.length <= 20
s
仅由数字组成
思路
随想录
在for (int i = startIndex; i < s.size(); i++)
循环中 [startIndex, i] 这个区间就是截取的子串,需要判断这个子串是否合法。
如果合法就在字符串后面加上符号.
表示已经分割。
如果不合法就结束本层循环,如图中剪掉的分支:
然后就是递归和回溯的过程:
递归调用时,下一层递归的startIndex要从i+2开始(因为需要在字符串中加入了分隔符.
),同时记录分割符的数量pointNum 要 +1。
回溯的时候,就将刚刚加入的分隔符.
删掉就可以了,pointNum也要-1。
最后就是在写一个判断段位是否是有效段位了。
主要考虑到如下三点:
- 段位以0为开头的数字不合法
- 段位里有非正整数字符不合法
- 段位如果大于255了不合法
class Solution {
private:
vector<string> result;// 记录结果
// startIndex: 搜索的起始位置,pointNum:添加逗点的数量
void backtracking(string& s, int startIndex, int pointNum) {
if (pointNum == 3) { // 逗点数量为3时,分隔结束
// 判断第四段子字符串是否合法,如果合法就放进result中
if (isValid(s, startIndex, s.size() - 1)) {
result.push_back(s);
}
return;
}
for (int i = startIndex; i < s.size(); i++) {
if (isValid(s, startIndex, i)) { // 判断 [startIndex,i] 这个区间的子串是否合法
s.insert(s.begin() + i + 1 , '.'); // 在i的后面插入一个逗点
pointNum++;
backtracking(s, i + 2, pointNum); // 插入逗点之后下一个子串的起始位置为i+2
pointNum--; // 回溯
s.erase(s.begin() + i + 1); // 回溯删掉逗点
} else break; // 不合法,直接结束本层循环
}
}
// 判断字符串s在左闭右闭区间[start, end]所组成的数字是否合法
bool isValid(const string& s, int start, int end) {
if (start > end) {
return false;
}
if (s[start] == '0' && start != end) { // 0开头的数字不合法
return false;
}
int num = 0;
for (int i = start; i <= end; i++) {
if (s[i] > '9' || s[i] < '0') { // 遇到非数字字符不合法
return false;
}
num = num * 10 + (s[i] - '0');
if (num > 255) { // 如果大于255了不合法
return false;
}
}
return true;
}
public:
vector<string> restoreIpAddresses(string s) {
result.clear();
if (s.size() < 4 || s.size() > 12) return result; // 算是剪枝了
backtracking(s, 0, 0);
return result;
}
};