代码随想录算法训练营第20天|39. 组合总和、40.组合总和II、131.分割回文串
LeetCode39
2025-02-20 15:45:16 星期四
题目描述:力扣39
文档讲解:代码随想录(programmercarl)39. 组合总和
视频讲解:《代码随想录》算法视频公开课:Leetcode:39. 组合总和讲解
代码随想录视频内容简记
要点1
注意,这个39有两个点需要分清楚,就是在确定单层递归的逻辑中,for循环中i = index
和递归中给index
参数赋值i
的作用是不同的,二者是配合起来使用的
for (int i = index; i < candidates.size(); i++) {
...
backtracking(candidates, target, i, sum);
...
}
- 在for循环中
i = index
是为了在每次取元素时,只能取index及以后或index以后的元素。例如,如果给index
参数赋值i
,那么[2, 5, 3]
取完2之后,就还能取[2, 5, 3]
;给index
参数赋值i + 1
,[2, 5, 3]
取完2之后,就只能取[5, 3]
。一种是可以重复取,一种是不可以重复取,但都是为了避免出现一个组合不同排列的情况。
如果写错写成i = 0
,那么index参数失效,不再控制取数的位置。那么此时递归函数的i也就会失效,没有意义,所以这么写是很离谱的。比如取3之后,又可以从[2, 5, 3]
里面取,就会出现下面的情况
for (int i = 0; i < candidates.size(); i++) {
sum += candidates[i];
path.push_back(candidates[i]);
backtracking(candidates, target, i, sum);
sum -= candidates[i];
path.pop_back();
}

- 给index参数赋值
i
的作用,在之前的77组合中,是i + 1
的,他的作用就是控制一个相同的元素能不能取多次,也只有给在本题中可以,所以不用+1
如果写成i + 1
就是不能重复取,就是下面的这种结果,如果写成i
,就是可以重复取,他的目的就是给下一次递归的for循环的i = index创造条件
for (int i = index; i < candidates.size(); i++) {
sum += candidates[i];
path.push_back(candidates[i]);
backtracking(candidates, target, i + 1, sum);
sum -= candidates[i];
path.pop_back();
}

所以,for循环中的i = index
一定是和递归中的参数index同时存在的,如果没有了i = index
,那么参数index也就失去了其意义。只有当i = index
,才能去给index赋值i
或者i + 1
来决定是否可以取index所指的重复元素
要点2
在本题中,因为是可以一直取重复的元素的,那么在一条path上只要不加限制,就会无限添加,一开始的时候总会报stackoverflow。

那么,就是终止条件有问题,这道题需要加上的终止条件有这个if (sum > target) return;
其实看着很像之前的剪枝操作,但确实是一个终止条件,因为不加上就终止不了了。
要点3
这道题和之前的很像,一开始写过了的,又看k哥视频的时候,就着重看了一下剪枝的部分,感觉这个剪枝法确实有点出乎意料。

虽然有sum > target
的条件卡着,但是这样的递归本身是无效的,也一定是无效的,所以本题的剪枝就索性跳过递归的过程。对candidates先进行排序,之后如上图,后面的部分进行剪枝,其实我感觉也没有剪多少嘿嘿😅。之后直接在for循环中控制即可。
因为是sum + candidates[i] <= target
,那么终止条件应该加上candidates[i] <= target - sum;
,变成i < candidatse.size() && candidates[i] <= target - sum;
这个是一个数值类的条件限制,不是77组合优化那个数量类的条件限制,所以不用考虑什么i加不加1的问题。说白了就是少进入一次递归,一进去也就返回了。
LeetCode测试
力扣的执行用时和内存分布每次都不太一样,其实想看一下剪不剪差了多少的。又崩掉一回😀

未剪枝
点击查看代码
class Solution {
private:
vector<int> path;
vector<vector<int>> result;
void backtracking (vector<int>& candidates, int target, int index, int sum) {
if (sum > target) return;
if (sum == target) {
result.push_back(path);
return;
}
for (int i = index; i < candidates.size(); i++) {
sum += candidates[i];
path.push_back(candidates[i]);
backtracking(candidates, target, i, sum);
sum -= candidates[i];
path.pop_back();
}
}
public:
vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
backtracking(candidates, target, 0, 0);
return result;
}
};
剪枝
点击查看代码
class Solution {
private:
vector<int> path;
vector<vector<int>> result;
void backtracking (vector<int>& candidates, int target, int index, int sum) {
if (sum > target) return;
if (sum == target) {
result.push_back(path);
return;
}
for (int i = index; i < candidates.size() && candidates[i] <= target - sum; i++) {
sum += candidates[i];
path.push_back(candidates[i]);
backtracking(candidates, target, i, sum);
sum -= candidates[i];
path.pop_back();
}
}
public:
vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
sort(candidates.begin(), candidates.end());
backtracking(candidates, target, 0, 0);
return result;
}
};
LeetCode40
题目描述:力扣40
文档讲解:代码随想录(programmercarl)40.组合总和II
视频讲解:《代码随想录》算法视频公开课:回溯算法中的去重,树层去重树枝去重,你弄清楚了没?| LeetCode:40.组合总和II
代码随想录视频内容简记
这个题其实是很巧妙的,和216,77还有上面的39不同的地方在于,40是有重复元素的。这个写着写着就发现不对劲了,一开始写发现总是这样的结果

画出树形图看了一下,发现确实是k哥说的“树层”有重复的元素。不太会操作,认真听k哥讲了一下,用到了核心的used数组,这个数组就是用来进行树层去重。
什么是树层去重?什么又是树枝去重?

注:图中蓝色的是used数组
可以看到,红色的三个结果,因为2的数量的关系,其在递归中位于同一层,就是所谓的同一“树层”。
下面所说的“重复的元素”都指:是不同的元素,但是数值相同,所以看着是重复的
那么树枝去重,可以理解为深度优先搜索,就是向深处递归的过程中,出现了重复的元素,举个例子,比如在39题中,可以重复取元素,像[2,2,3]就是同一树枝,不需要去重。但在本题中,题目中明确要求不能重复选一个元素,所以需要进行去重,这个其实就是for循环递归中的i + 1就可以去
其实也就是,需要对[1,2,2,2,5]中的树层进行去重,当然这里的数组是排过序的,至于为什么需要排序,因为used数组使用的特殊性,就先当成特殊用法记住好了
梳理
-
确定函数的参数和返回值,其他不变,参数会多一个used数组
-
确定递归的终止条件,这个和之前几个题都是一样的
-
确定单层递归的逻辑。要想对树层去重,最直观的就是数组元素相等,就是重复的了呗。但是还有一点,就是这里要用到核心的used数组,used数组会将每一个取过的元素的位置置为1,只有i的当前一位的used值为0才能去重,前一位为0,表示这是刚刚回溯过的。因为在本题中,递归中出现了一次,那么回溯时出现的重复元素,就一定包含在前面的递归中,可能会出现相同的结果集path,所以只有回溯的(used[i - 1]为0)且元素相等,才能去去重。

LeetCode测试
在书写的时候注意一个小细节
if (i > 0 && candidates[i] == candidates[i - 1] && used[i - 1] == false) {
continue;
}
这里正常来说是要书写i - 1
的,我刚开始写的i--
,感觉应该是一样的。
# include<iostream>
using namespace std;
int main() {
int nums[5] = {0, 1, 2, 3, 4};
int i = 4;
if (nums[i--]) cout << i << endl;
}
这段代码输出结果i = 3
# include<iostream>
using namespace std;
int main() {
int nums[5] = {0, 1, 2, 3, 4};
int i = 4;
if (nums[i - 1]) cout << i << endl;
}
这段代码输出结果i = 4
,可见,当用i--的时候,它实际多了一步给i赋值的操作,忽略了这点,所以i的值被改变了,就肯定不对了。
剪枝
这道题不剪枝的话是会在提交的时候报“超出时间限制”的,卡在第125个测试用例。还是两个剪枝的地方
第一个是if (sum > target) return;
,加上这一个就可以通过了
还有一个是在for循环中,candidates[i] + sum <= target;
,其实就是少进一层递归
感觉这个题比之前的77,216,39琢磨头都多,有点绞尽脑汁的感觉,确实感谢k哥的刷题顺序,要不然按照里扣的题号做估计得头秃了。哈哈😂完整代码如下:
点击查看代码
class Solution {
private:
vector<int> path;
vector<vector<int>> result;
void backtracking (vector<int>& candidates, int target, int index, int sum, vector<bool> used) {
if (sum > target) return;
if (sum == target) {
result.push_back(path);
return;
}
for (int i = index; i < candidates.size() && candidates[i] + sum <= target; i++) {
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, i + 1, sum, used);
sum -= candidates[i];
path.pop_back();
used[i] = false;
}
}
public:
vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
vector<bool> used(candidates.size(), false);
sort(candidates.begin(), candidates.end());
backtracking(candidates, target, 0, 0, used);
return result;
}
};
LeetCode131
题目描述:力扣131
文档讲解:代码随想录(programmgercarl)131.分割回文串
视频讲解:《代码随想录》算法视频公开课:131.分割回文串
代码随想录视频内容简记
-
这是一个分割问题,其实和前面的组合是类似的,在字符串的分割中,其“划分”就是通过之前的index来进行标注
-
另外,关于回文串的判断
回文 串是向前和向后读都相同的字符串。
直接用双指针一个从前向后,一个从后向前遍历判断相等即可
- 关于
substr(pos, len)
函数,其pos表示起始位置,len表示从pos开始的长度为len的字符。举个例子,"abcd",我要取前面字符abc,应该用s.substr(0, 3)
梳理
-
确定函数的参数和返回值
-
确定递归的终止条件
-
确定单层递归的逻辑
大致代码内容
-
void backtracking (string s, int index)
-
确定递归的终止条件,
if (index == s.size()) result.push_back(path); return;
-
确定单层递归的逻辑,
for (int i = index; i <= s.size(); i++)
,之后要紧跟一个判断,就是if (isPalindrome(s, index, i)) string str = s.substr(index, i - index + 1)
之后添加path.push_back(str)
,否则直接return
LeetCode测试
点击查看代码
class Solution {
private:
bool isPalindrome(string s, int start, int end) {
int left;
int right;
for (left = start, right = end; left <= right; left++) {
if (s[left] != s[right]) return false;
right--;
}
return true;
}
vector<vector<string>> result;
vector<string> path;
void backtracking (string s, int index) {
if (index == s.size()) {
result.push_back(path);
return;
}
for (int i = index; i < s.size(); i++) {
if (isPalindrome(s, index, i)) {
string str = s.substr(index, i - index + 1);
path.push_back(str);
} else continue;
backtracking(s, i + 1);
path.pop_back();
}
}
public:
vector<vector<string>> partition(string s) {
backtracking(s, 0);
return result;
}
};
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 25岁的心里话
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
· 零经验选手,Compose 一天开发一款小游戏!
· 因为Apifox不支持离线,我果断选择了Apipost!
· 通过 API 将Deepseek响应流式内容输出到前端