学习材料声明
所有知识点都来自互联网,进行总结和梳理,侵权必删。
引用来源:基本参考代码随想录的刷题顺序和讲解。
带你学透回溯算法(理论篇)| 回溯法精讲!
1|组合
理论基础
回溯纯暴力的方法(靠剪枝进行一些优化)。是一个递归的过程。
回溯的是当前的解空间。
1.回溯与模板
可以抽象为树形结构!(数的宽度 解空间for循环;树的深度 递归的深度;叶子结点收集结果)
void backtracking(参数) {
if (终止条件) {
存放结果;
return;
}
for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)(剪枝操作!)) {
处理节点;
backtracking(路径,选择列表); // 递归
回溯,撤销处理结果
}
}
2.解决什么问题?
组合问题:N个数里面按一定规则找出k个数的集合
切割问题:一个字符串按一定规则有几种切割方式
子集问题:一个N个数的集合里有多少符合条件的子集
排列问题:N个数按一定规则全排列,有几种排列方式
棋盘问题:N皇后,解数独等等
2.1组合问题
分为个数限制,和限制,个数和加和双限制。candidates有重复元素(used限制层级不重复),无重复元素。
2.2切割问题|重点要关注字符串的相关方法
回文串和复原ip地址,怎么说呢,要写一个相关问题,startIdx开始,获取下一个分割点,分别判断分割点是否合法,不合法continue,合法回溯寻找下一个分割点。
2.3子集问题
与组合问题一致,只是需要再每个节点都收集path。
2.4排列问题
不使用startIndex,但是要使用used限制解空间大小。其他的重复与否之类的和以上问题一直。同层重复(序列中有重复元素)用set。
习题
1|77. 组合|要求组合元素为k个。|不可出现重复元素。
class Solution {
List<Integer> path = new ArrayList<Integer>();
List<List<Integer>> ans = new ArrayList<List<Integer>>();
public void backtracking(int n, int k, int startidx) {
if (path.size()== k) {
ans.add(new ArrayList<Integer>(path));
System.out.print(path);
return;
}
for (int j=startidx; j<=n-k+path.size()+1; j++) {//剪枝n-(k-path.size())+1
path.add(j);
backtracking(n, k, j+1);//这里是j+1,否则出现重复组合。
path.remove(path.size() - 1);
}
}
public List<List<Integer>> combine(int n, int k) {
backtracking(n, k, 1);
return ans;
}
}
剪枝操作!
----------------------------------------------------------------------------2023年10月23日----------------------------------------------------------
2|216. 组合总和 III|找出所有相加之和为 n 的 k 个数的组合|每个数字 最多使用一次
与组合那题没什么区别,只要搞清楚n和k的含义就好。
class Solution {
List<List<Integer>> ans = new ArrayList<List<Integer>>();
List<Integer> path = new ArrayList<Integer>();
int sum = 0;
public void backtracking(int n, int k, int startidx) {
if(sum > n){
return;
}
if (path.size()== k){
if(sum == n){
ans.add(new ArrayList<Integer>(path));
System.out.print(path);
return;
}
}
for (int j=startidx; j<=9-(k-path.size())+1; j++) {//剪枝n-(k-path.size())+1
sum += j;
if(sum > n){
sum -= j;
return;
}
path.add(j);
backtracking(n, k, j+1);//这里是j+1,否则出现重复组合。
path.remove(path.size() - 1);
sum -= j;
}
}
public List<List<Integer>> combinationSum3(int k, int n) {
backtracking(n, k, 1);
return ans;
}
}
3|17. 电话号码的字母组合
这题曾经看过灵茶山艾府的教学视频,试一试可不可以一下就写出来。主要还是在对String的处理上!
class Solution {
public:
vector<string> ans;
string path = "";
const string nums[12] = {" ", " ", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"};
void backtracing(string digits, int n, int k){
if(path.size() == n){
ans.push_back(path);
return;
}
int num = int(digits[k]-'0');
for(int i=0; i<nums[num].size(); i++){
path.push_back(nums[num][i]);
backtracing(digits, n, k+1);
path.pop_back();
}
}
vector<string> letterCombinations(string digits) {
int n = digits.length();
if(n == 0)
return ans;
backtracing(digits, n, 0);
return ans;
}
};
----------------------------------------------------------------------------2023年10月24日----------------------------------------------------------
4|39. 组合总和|可重复利用元素
class Solution {
public:
vector<vector<int>> ans;
vector<int> path;
void backtracing(vector<int>& candidates, int target, int sum, int startidx){
if(sum > target){
return;
}
if(sum == target){
ans.push_back(path);
return;
}
for(int i=startidx; i<candidates.size(); i++){
path.push_back(candidates[i]);
sum += candidates[i];
backtracing(candidates, target, sum, i);
path.pop_back();
sum -= candidates[i];
}
}
vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
backtracing(candidates, target, 0, 0);
return ans;
}
};
优化思路是排序candidates+剪枝,以下代码来自代码随想录
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;
}
// 如果 sum + candidates[i] > target 就终止遍历
for (int i = startIndex; i < candidates.size() && sum + candidates[i] <= target; i++) {
sum += candidates[i];
path.push_back(candidates[i]);
backtracking(candidates, target, sum, i);
sum -= candidates[i];
path.pop_back();
}
}
public:
vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
result.clear();
path.clear();
sort(candidates.begin(), candidates.end()); // 需要排序
backtracking(candidates, target, 0, 0);
return result;
}
};
5|40. 组合总和 II|每个字母至多使用一次但candidates里面有重复数字。(这也是处理难点)|设计一个used,同一树层和同一树枝的概念。即对同一树层,只使用相同值的一个。
class Solution {
public:
vector<vector<int>> ans;
vector<int> path;
void backtracing(vector<int>& candidates, int target, int sum, int startidx, vector<bool> used){
if(sum > target){
return;
}
if(sum == target){
ans.push_back(path);
return;
}
for(int i=startidx; i<candidates.size() && sum + candidates[i] <= target; i++){
if (i > 0 && candidates[i] == candidates[i - 1] && used[i - 1] == false) {
continue;
}
path.push_back(candidates[i]);
sum += candidates[i];
used[i] = true;
backtracing(candidates, target, sum, i+1, used);
path.pop_back();
sum -= candidates[i];
used[i] = false;
}
}
vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
vector<bool> used(candidates.size(), false);
path.clear();
ans.clear();
sort(candidates.begin(), candidates.end());
backtracing(candidates, target, 0, 0, used);
return ans;
}
};
6|131. 分割回文串
主要问题,一用到String就各种方法水土不服。双指针判断回文串不要忘记。
class Solution {
List<List<String>> ans = new ArrayList<List<String>>();
List<String> path = new ArrayList<String>();
public boolean isPalindrome(String s, int start, int end){
for (int i = start, j = end; i < j; i++, j--) {
if (s.charAt(i) != s.charAt(j)) {
return false;
}
}
return true;
}
public void backTracing(String s, int startidx){
if(startidx == s.length()){
ans.add(new ArrayList<String>(path));
return;
}
for(int i = startidx; i<s.length(); i++){
if(isPalindrome(s, startidx, i)){
String str = s.substring(startidx, i + 1);
path.add(str);
}else{
continue;
}
backTracing(s, i+1);
path.remove(path.size() - 1);
}
}
public List<List<String>> partition(String s) {
backTracing(s, 0);
return ans;
}
}
----------------------------------------------------------------------------2023年10月25日----------------------------------------------------------
7|93. 复原 IP 地址
写不出来,主要在回溯出问题了。—>原因在于for外面的一个判断,path,没有复原,导致出现无法回溯的问题。
class Solution {
List<String> ans = new ArrayList<String>();
List<String> path = new ArrayList<String>();
int[] nums = {1, 10, 100};
public boolean isCorrect(String s, int start, int end){
if(end - start + 1 > 3){
return false;
}
int intNum = 0;
String temp = s.substring(start, end+1);
for(int i=end, j=0; i>=start; i--,j++){
intNum += (s.charAt(i) - '0')*nums[j];
}
if(intNum<10 && start == end){
return true;
}
if(intNum>9 && intNum<100 && end-start == 1){
return true;
}
if(intNum > 99 && intNum<=255 && end-start == 2 ){
return true;
}
return false;
}
public void backTracing(String s, int startIdx){
if(path.size() == 3){
if(isCorrect(s, startIdx, s.length()-1)){
path.add(s.substring(startIdx, s.length()));
ans.add(path.get(0)+"."+path.get(1)+"."+path.get(2)+"."+path.get(3));
path.remove(path.size() - 1);//这里!!!!!记住要复原!!!
}
return;
}
for(int i=startIdx; i<=startIdx+2 && i<s.length(); i++){
if(isCorrect(s, startIdx, i)){
String str = s.substring(startIdx, i+1);
path.add(str);
System.out.print(path+"\n");
backTracing(s, i+1);
path.remove(path.size() - 1);
}else{
break;
}
}
}
public List<String> restoreIpAddresses(String s) {
backTracing(s, 0);
return ans;
}
}
8|78. 子集
class Solution {
public:
vector<vector<int>> ans;
vector<int> path;
void backTracing(vector<int>& nums, int startIndex){
ans.push_back(path);//每个节点都收集
if(startIndex == nums.size()){
return;
}
for(int i=startIndex; i<nums.size(); i++){
path.push_back(nums[i]);
backTracing(nums, i+1);
path.pop_back();
}
}
vector<vector<int>> subsets(vector<int>& nums) {
backTracing(nums, 0);
return ans;
}
};
9|90. 子集 II |与组合问题中candidates里面有重复元素一样,要排序后跳跃,或者用used。
class Solution {
public:
vector<vector<int>> ans;
vector<int> path;
void backTracing(vector<int>& nums, int startIndex){
ans.push_back(path);//每个节点都收集
if(startIndex == nums.size()){
return;
}
for(int i=startIndex; i<nums.size(); i++){
if(i > startIndex && nums[i - 1] == nums[i]){
continue;
}
path.push_back(nums[i]);
backTracing(nums, i+1);
path.pop_back();
}
}
vector<vector<int>> subsetsWithDup(vector<int>& nums) {
sort(nums.begin(), nums.end());
backTracing(nums, 0);
return ans;
}
};
----------------------------------------------------------------------------2023年10月27日----------------------------------------------------------
10|491.递增子序列
这一题感觉非常简单呀,就是排序,然后过滤掉一些节点就好了。(错误思路,不能排序)
啊啊啊,好想用动态规划来做。同层不能用同样的,但是怎么确定捏?
还是画树比较容易理解。
很明显一个元素不能重复使用,所以需要startIndex。
回溯三步(参数设计,终止条件,单层搜索逻辑)。
又学到一个知识点,用set标记。
class Solution {
public:
vector<vector<int>> ans;
vector<int> path;
void backTracing(vector<int>& nums, int startIndex){
if(path.size()>=2){
ans.push_back(path);
}//每个节点都收集
if(startIndex == nums.size()){
return;
}
unordered_set<int> uset; // 使用set对本层元素进行去重
for(int i=startIndex; i<nums.size(); i++){
if((path.size()!=0 && nums[i] < path.back()) || uset.find(nums[i]) != uset.end()){
continue;
}
uset.insert(nums[i]);
path.push_back(nums[i]);
backTracing(nums, i+1);
path.pop_back();
}
}
vector<vector<int>> findSubsequences(vector<int>& nums) {
path.clear();
ans.clear();
backTracing(nums, 0);
return ans;
}
};
11|46. 全排列
排列问题不使用startIndex。
每一个节点的拓展宽度受父节点影响,所以记录一条树枝上的used情况。
class Solution {
public:
vector<vector<int>> ans;
vector<int> path;
void backTracing(vector<bool> used, vector<int> nums, int n){
if(n==nums.size()){
ans.push_back(path);
return;
}
for(int i=0; i<nums.size(); i++){
if(used[i]==false){
path.push_back(nums[i]);
used[i]=true;
backTracing(used, nums, n+1);
path.pop_back();
used[i]=false;
}
}
}
vector<vector<int>> permute(vector<int>& nums) {
vector<bool> used(nums.size(), false);
ans.clear();
path.clear();
backTracing(used, nums, 0);
return ans;
}
};
12|47.全排列 II
全排列1+同层去重。
class Solution {
public:
vector<vector<int>> ans;
vector<int> path;
void backTracing(vector<bool> used, vector<int> nums){
if(path.size()==nums.size()){
ans.push_back(path);
return;
}
unordered_set<int> uset; // 使用set对本层元素进行去重
for(int i=0; i<nums.size(); i++){
if(used[i]==false&&uset.find(nums[i]) == uset.end()){//uset中没有这个元素。
uset.insert(nums[i]);
path.push_back(nums[i]);
used[i]=true;
backTracing(used, nums);
path.pop_back();
used[i]=false;
}
}
}
vector<vector<int>> permuteUnique(vector<int>& nums) {
vector<bool> used(nums.size(), false);
ans.clear();
path.clear();
backTracing(used, nums);
return ans;
}
};
13|494. 目标和
class Solution {
public:
int count = 0;
int findTargetSumWays(vector<int>& nums, int target) {
backtrack(nums, target, 0, 0);
return count;
}
void backtrack(vector<int>& nums, int target, int index, int sum) {
if (index == nums.size()) {
if (sum == target) {
count++;
}
} else {
backtrack(nums, target, index + 1, sum + nums[index]);
backtrack(nums, target, index + 1, sum - nums[index]);
}
}
};