代码随想录Day25-Day29:回溯算法
77. 组合
问题:两个整数 n 和 k,返回 1 … n 中所有可能的 k 个数的组合
- 变量startIndex,这个参数用来记录本层递归的中,集合从哪里开始遍历,startIndex 就是防止出现重复的组合
- 无论是往广度还是深度,集合都逐渐减少,只留startindex到nums.size()后面的部分
- 剪枝处理:i <= n - (k - path.size()) + 1
- 都是求同一个集合中的组合
216.组合总和III
问题:找出所有相加之和为 n 的 k 个数的组合。组合中只允许含有 1 - 9 的正整数,并且每种组合中不存在重复的数字
- 仍然固定k个数,每种组合不许重复
- k相当于树的深度,9(因为整个集合就是9个数)就是树的宽度
- 剪枝:i <= 9 - (k - path.size()) + 1
- 都是求同一个集合中的组合
17.电话号码的字母组合
问题:给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合
- 遍历的深度,就是输入"23"的长度,而叶子节点就是我们要收集的结果
- 每一个数字代表的是不同集合,也就是求不同集合之间的组合
for (int i = 0; i < letters.size(); i++) {
s.push_back(letters[i]); // 处理
backtracking(digits, index + 1); // 递归,注意index+1,一下层要处理下一个数字了
s.pop_back(); // 回溯
}
39. 组合总和
问题:给定一个无重复元素的数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。candidates 中的数字可以无限制重复被选取。
所有数字(包括 target)都是正整数。
解集不能包含重复的组合。
- 本题元素为可重复选取的
for (int i = startIndex; i < candidates.size(); i++) {
sum += candidates[i];
path.push_back(candidates[i]);
backtracking(candidates, target, sum, i); // 关键点:不用i+1了,表示可以重复读取当前的数
sum -= candidates[i]; // 回溯
path.pop_back(); // 回溯
}
40.组合总和II
问题:给定一个数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。
candidates 中的每个数字在每个组合中只能使用一次。
说明: 所有数字(包括目标数)都是正整数。解集不能包含重复的组合。
- 本题candidates 中的每个数字在每个组合中只能使用一次。
本题数组candidates的元素是有重复的,而39.组合总和是无重复元素的数组candidates
最后本题和39.组合总和 (opens new window)要求一样,解集不能包含重复的组合。 - 树层去重的话,需要对数组排序
- 要去重的是“同一树层上的使用过”,如何判断同一树层上元素(相同的元素)是否使用过了呢。
如果candidates[i] == candidates[i - 1] 并且 used[i - 1] == false,就说明:前一个树枝,使用了candidates[i - 1],也就是说同一树层使用过candidates[i - 1]。 - used[i - 1] == true,说明同一树枝candidates[i - 1]使用过
used[i - 1] == false,说明同一树层candidates[i - 1]使用过
for (int i = startIndex; i < candidates.size() && sum + candidates[i] <= target; i++) {
// used[i - 1] == true,说明同一树枝candidates[i - 1]使用过
// used[i - 1] == false,说明同一树层candidates[i - 1]使用过
// 要对同一树层使用过的元素进行跳过
if (i > 0 && candidates[i] == candidates[i - 1] && used[i - 1] == false) {
continue;
}
sum += candidates[i];
path.push_back(candidates[i]);
used[i] = true;
backtracking(candidates, target, sum, i + 1, used); // 和39.组合总和的区别1:这里是i+1,每个数字在每个组合中只能使用一次
used[i] = false;
sum -= candidates[i];
path.pop_back();
}
131.分割回文串
问题:给定一个字符串 s,将 s 分割成一些子串,使每个子串都是回文串。
返回 s 所有可能的分割方案。
- 递归用来纵向遍历,for循环用来横向遍历,切割线切割到字符串的结尾位置,说明找到了一个切割方法。切割问题的回溯搜索的过程和组合问题的回溯搜索的过程是差不多的
for (int i = startIndex; i < s.size(); i++) {
if (isPalindrome(s, startIndex, i)) { // 是回文子串
// 获取[startIndex,i]在s中的子串
string str = s.substr(startIndex, i - startIndex + 1);
path.push_back(str);
} else { // 如果不是则直接跳过
continue;
}
backtracking(s, i + 1); // 寻找i+1为起始位置的子串
path.pop_back(); // 回溯过程,弹出本次已经添加的子串
}
- 双指针法,一个指针从前向后,一个指针从后向前,如果前后指针所指向的元素是相等的,就是回文字符串了
93.复原IP地址
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; // 不合法,直接结束本层循环
}
78.子集
问题:给定一组不含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。说明:解集不能包含重复的子集。
- 那么组合问题和分割问题都是收集树的叶子节点,而子集问题是找树的所有节点
90.子集II
问题:给定一个可能包含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。说明:解集不能包含重复的子集。
- 理解“树层去重”和“树枝去重”非常重要
491.递增子序列
- 而本题求自增子序列,是不能对原数组进行排序的,排完序的数组都是自增子序列了。所以不能使用之前的去重逻辑!
- 同一父节点下的同层上使用过的元素就不能再使用了
unordered_set<int> uset; // 使用set来对本层元素进行去重
for (int i = startIndex; i < nums.size(); i++) {
if ((!path.empty() && nums[i] < path.back())
|| uset.find(nums[i]) != uset.end()) {
continue;
}
uset.insert(nums[i]); // 记录这个元素在本层用过了,本层后面不能再用了
path.push_back(nums[i]);
backtracking(nums, i + 1);
path.pop_back();
}
- 用数组来做哈希,效率就高了很多
46.全排列
问题:给定一个 没有重复 数字的序列,返回其所有可能的全排列。
- used数组,其实就是记录此时path里都有哪些元素使用了,一个排列里一个元素只能使用一次
for (int i = 0; i < nums.size(); i++) {
if (used[i] == true) continue; // path里已经收录的元素,直接跳过
used[i] = true;
path.push_back(nums[i]);
backtracking(nums, used);
path.pop_back();
used[i] = false;
}
47.全排列 II
问题:给定一个可包含重复数字的序列 nums ,按任意顺序 返回所有不重复的全排列。
- 给定一个可包含重复数字的序列,要返回所有不重复的全排列。
- 要强调的是去重一定要对元素进行排序,这样我们才方便通过相邻的节点来判断是否重复使用了
- 对于排列问题,树层上去重和树枝上去重,都是可以的,但是树层上去重效率更高!