代码随想录第二十三天 | Leecode 39. 组合总和、40.组合总和II、131. 分割回文串
Leecode 39. 组合总和
题目描述
给你一个 无重复元素 的整数数组 candidates
和一个目标整数 target
,找出 candidates
中可以使数字和为目标数 target
的 所有 不同组合 ,并以列表形式返回。你可以按 任意顺序 返回这些组合。
candidates
中的 同一个 数字可以 无限制重复被选取 。如果至少一个数字的被选数量不同,则两种组合是不同的。
对于给定的输入,保证和为 target
的不同组合数少于 150
个。
- 示例 1:
输入:candidates =
[2,3,6,7]
, target =7
输出:[[2,2,3],[7]]
解释:
2
和3
可以形成一组候选,2 + 2 + 3 = 7
。注意2
可以使用多次。
7
也是一个候选,7 = 7
。
仅有这两种组合。
- 示例 2:
输入: candidates =
[2,3,5]
, target =8
输出:[[2,2,2,2],[2,3,3],[3,5]]
- 示例 3:
输入: candidates =
[2]
, target =1
输出:[]
第一次错误尝试的思路
本题的一上来的思路并不算难,同时记录当前数组与当前数组的和作为当前状态,在递归函数中使用for循环来遍历所有下一个状态,并在递归后进行恢复回溯。主要难点就在于要确保不会重复。首先对于去重我想到的是使用set
集合容器,将结果数组存放入集合中,就可以删去重复项。但此时又会遇到,当存入的vector中数相同但顺序不同时也算集合中的不同元素。因此为了解决这个问题我又在满足条件时的判断中加了一条对每一次符合条件的vector进行一次快排,得到代码如下:
class Solution {
public:
void combHelper(vector<int>& candidates, set<vector<int>>& vecSet, vector<int>& curVec, int& curSum, int target){
if(curSum == target){
sort(curVec.begin(), curVec.end()); // 使用sort对vector排序,确保存入的vector不会有重复
vecSet.insert(curVec); // 用set来存放结果,避免重复
return;
}
if(curSum > target) return; // 如果当前和大于目标值,则直接返回
for(int i = 0; i < candidates.size(); i++){
curVec.push_back(candidates[i]); // 存入下一状态的值
curSum += candidates[i]; // 更新下一状态的和
combHelper(candidates, vecSet, curVec, curSum, target); // 递归下一状态
curSum -= candidates[i]; // 回溯,恢复数组和
curVec.pop_back(); // 回溯,恢复数组
}
}
vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
set<vector<int>> vecSet; // 使用set来存放结果,避免重复
vector<int> curVec; // 初始化初始数组,初始数组和
int curSum = 0;
combHelper(candidates, vecSet, curVec, curSum, target); // 递归调用查找所有符合条件的值
vector<vector<int>> result(vecSet.begin(), vecSet.end()); // 将set转换为vec结果
return result; // 返回
}
};
上面这段代码乍一看感觉没什么问题,但是实际上运行时会有一些测试用例无法通过,例如:
输入:candidates =
[2,3,5]
,target =8
输出:[[2,2,2,2],[2,2,3],[2,3,3],[2,5],[3,5]]
预期结果:[[2,2,2,2],[2,3,3],[3,5]]
观察到其中会输出一个[2,2,3]
和[2,5]
的结果,这让我非常困惑,因为我的代码中明确了只有当curSum == target
时才能进入存放结果的分支条件中,而此时这两个数组的求和显然应该是7
而不是目标给出的8
。这个问题让我百思不得其解,检查了很久都没想通。(如果有读者看到这里,也可以尝试一下阅读上面代码找一下问题所在)
为了找出这个问题,我甚至在Visual Studio中逐行deBug,最终终于发现了问题所在。
原来问题在于其中的sort
。当我在if分支中使用sort
对当前数组进行排序的时候,即:
if(curSum == target){
sort(curVec.begin(), curVec.end()); // 此时对curVec进行排序,会导致curVec的顺序改变,不再按照我存入时的顺序排列
vecSet.insert(curVec);
return;
}
而在使用了sort
将顺序打乱变为和我存入时的顺序不同后,再我的for循环部分代码中:
for(int i = 0; i < candidates.size(); i++){
curVec.push_back(candidates[i]); // 存入下一状态的值
curSum += candidates[i]; // 更新下一状态的和
combHelper(candidates, vecSet, curVec, curSum, target); // 这一步的递归中,可能会因为sort而改变数组的顺序
curSum -= candidates[i]; // 此时回溯减去的值和上一次加入的值相等
curVec.pop_back(); // 但此时pop出vec的值,和上面存入的值可能因为sort而导致不是同一个数
}
终于找出了问题所在,为此我们总结经验:
- 在使用回溯递归的过程中,不要对传入的包含了上一步状态的变量进行修改。(例如此时传入的curVec其实包含了回溯退回的路径这个信息,如果对其进行sort,会导致回退到错误的状态)。
同时总结本题的思路,上面我们使用sort
其实是为了去重而引入的,但是引入之后反而导致产生了错误。为此我们需要寻找其他的去重方法。思考昨天所做的回溯题目中,我们是通过设定startPoint来限定每一次for循环开始的值,从而规避了重复的情况,接下来我们尝试使用同样的方法来解决本题。
正确的回溯解法
上一节中展示了一种错误的去重方式,这里考虑给每次传入递归函数中的参数新增一个int类型的start
变量,用于表示每次搜索下一状态时的起始搜索状态,从而避免重复。这样我们可以写出代码如下:
class Solution {
public:
void combHelper(vector<int>& candidates, int target, int start){
if(curSum == target){ // 终止条件
result.push_back(curVec);
return;
}
if(curSum > target) return;
for(int i = start; i < candidates.size(); i++){
curVec.push_back(candidates[i]); // 存储当前状态
curSum += candidates[i];
combHelper(candidates, target, i); // 根据for循环中i的取值从不同的点开始递归
curSum -= candidates[i]; // 回溯
curVec.pop_back();
}
}
vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
combHelper(candidates, target,0);
return result;
}
private:
vector<vector<int>> result; // 在类中用全局变量来存储结果数组,以及表示当前状态的curVec和curSum
vector<int> curVec;
int curSum = 0;
};
上面的回溯可以看做是对一个多叉树的深度优先搜索,先搜索了全部i=0
时的结果,随后随着i
增加,再去搜索其他分支(这里可以保证搜索分支序号i具有一个单调不减的性质)。同时每一次的start都是从i开始,表示了可以和上一个节点相同,而如果要与上一个节点不同,即每个节点不能重复的话,每次调用传入的start就该变为i+1。
Leecode 40.组合总和II
题目描述
给定一个候选人编号的集合 candidates
和一个目标数 target
,找出 candidates
中所有可以使数字和为 target
的组合。
candidates
中的每个数字在每个组合中只能使用 一次 。
注意:解集不能包含重复的组合。
- 示例 1:
输入: candidates =
[10,1,2,7,6,1,5]
, target =8
,
输出:[
[1,1,6],
[1,2,5],
[1,7],
[2,6]
]
- 示例 2:
输入: candidates =
[2,5,2,1,2]
, target =5
,
输出:[
[1,2,2],
[5]
]
解题思路
首先需要充分理解本题,特别是本题和上一题以及昨天所做的216. 组合总和 III的区别。区别主要就在于,本题给定的向量中的数是有重复的,同时其中每一个数只能使用一次。同时还需要注意,本题输出的结果需要和此前做过的组合一样,最终结果都是不重复的。在这里我们可以举一个例子:
当输入 candidates =
[1, 1, 2]
, target =3
时
由于输入的candidates中有重复元素,而在检查到由第一个1
组成的[1,2]
后发现和为3进行存入,随后又遇到第二个1
组成[1,2]
会再次进行存入,此时就相当于在结果中存放了两个一模一样的[1,2]
。这会导致最终结果会产生重复值。
为了解决上面提到的重复问题,即为了避免在输出的vec中同一个位置取到candidates中不同的数但却是相等的值,我们可以将原本的vector先进行一次排序。这样就可以使得所有相等的值都相邻,随后在此基础上再继续考虑去重。
在原本的candidates有序之后,如果要去重,当然还是可以考虑使用set容器(尽管在上一题中尝试使用set得到了错误的结果)。为此我们可以写出如下代码:
class Solution {
public:
set<vector<int>> result; // 使用set来存放进行去重
vector<int> curVec;
int curSum = 0;
void combHelper(vector<int>& candidates, int target, int start){
if(curSum == target){
result.insert(curVec);
return;
}
if(curSum > target) return;
for(int i = start; i < candidates.size() && curSum + candidates[i] <= target; i++){
curVec.push_back(candidates[i]);
curSum += candidates[i];
combHelper(candidates, target, i+1);
curSum -= candidates[i];
curVec.pop_back();
}
}
vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
sort(candidates.begin(), candidates.end());
combHelper(candidates, target, 0);
vector<vector<int>> result1(result.begin(), result.end());
return result1;
}
};
上面代码确实没啥问题,测试算例也基本上都能跑过,但是在遇到测试算例:
输入:
candidates = [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1]
target = 30
会发生超时的情况,原因是当输入全为 1 的数组且目标值较大(如 30)时,你的代码会遍历所有可能的组合(如 C(100,30)
种),并通过 set 去重。尽管最终结果唯一,但遍历所有可能路径的时间复杂度达到指数级,导致超时。
因此为了避免不必要重复带来的时间复杂度,我们应该尝试使用其他的方法来去重。特别是需要避免同一层循环中出现相同值的情况。为此我们可以写出下面代码:
class Solution {
public:
vector<vector<int>> result;
vector<int> curVec;
int curSum = 0;
void combHelper(vector<int>& candidates, int target, int start) {
if (curSum == target) {
result.push_back(curVec);
return;
}
if (curSum > target) return;
for (int i = start; i < candidates.size(); i++) {
if (i > start && candidates[i] == candidates[i - 1]) continue; // 跳过同层级重复元素,从而进行去重操作,使用continue跳过
if (curSum + candidates[i] > target) break; // 剪枝,如果当前值求和已经大于目标和,则没必要再试探后续的结果,直接break
curVec.push_back(candidates[i]);
curSum += candidates[i];
combHelper(candidates, target, i+1);
curSum -= candidates[i];
curVec.pop_back();
}
}
vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
sort(candidates.begin(), candidates.end());
combHelper(candidates, target, 0);
return result;
}
};
此时的回溯代码就可以正常通过测试了。
Leecode 131.分割回文串
题目描述
给你一个字符串 s
,请你将 s
分割成一些 子串,使每个子串都是 回文串 。返回 s
所有可能的分割方案。
- 示例 1:
输入:
s = "aab"
输出:[["a","a","b"],["aa","b"]]
- 示例 2:
输入:
s = "a"
输出:[["a"]]
解题思路
这道题做起来感觉有点难度,首先一上手就不知道如何分割字符串,专门去查了一下字符串自带的函数,本题中主要用到:
substr(i,j)
,表示将字符串中从序号i
到j
的左闭右闭区间组成一个新的字符串
再按照回溯的方法写出下面代码:
class Solution {
public:
vector<vector<string>> result;
vector<string> curVec;
bool isPalindrome(string curStr){
int n = curStr.size();
for(int i = 0; i < n/2 ; i++){
if(curStr[i] != curStr[n-1-i]) return false;
}
return true;
}
void partHelper(const string& s, int start){
if(s.size() <= start){ // 如果start已经超出s中所有的数
result.push_back(curVec); // 将当前向量存放
return;
}
for(int i = start; i < s.size(); i++){ // 用[start,i]左闭右闭区间来表示当前字符串,遍历i取到之后所有长度
string newStr = s.substr(start, i - start + 1); // 建立当前字符串
if(isPalindrome(newStr)){ // 如果当前字符串是回文串
curVec.push_back(newStr); // 存入curVec中
partHelper(s, i+1); // 递归
curVec.pop_back(); // 回溯
}
}
}
vector<vector<string>> partition(string s) {
partHelper(s, 0);
return result;
}
};
上面代码中难点在于,使用start
变量来表示字符串中的子串的起始位置,另外是每次递归回溯要先进行一个if判断,判断其是否为回文串。而对于之前几题组合的回溯代码中,并没有其他的判断而直接进行递归回溯。
今日总结
进一步学习了回溯算法,回溯中递归函数传入的start
变量非常常见,通常在组合中可以用于控制去重、字符串中用于表示子字符串的起始值。
此外在回溯的for循环中,可以通过加上if判断语句来去重、剪枝(相应地在if内加上break或者continue),要根据具体的问题来讨论分析,灵活应对。
今天刷到85题了,再接再厉!
代码随想录第二十三天 | Leecode 39. 组合总和、40.组合总和II、131. 分割回文串的更多相关文章
- LeetCode 39. 组合总和 40.组合总和II 131.分割回文串
欢迎关注个人公众号:爱喝可可牛奶 LeetCode 39. 组合总和 40.组合总和II 131.分割回文串 LeetCode 39. 组合总和 分析 回溯可看成对二叉树节点进行组合枚举,分为横向和纵 ...
- HDU 5371(2015多校7)-Hotaru's problem(Manacher算法求回文串)
题目地址:HDU 5371 题意:给你一个具有n个元素的整数序列,问你是否存在这样一个子序列.该子序列分为三部分,第一部分与第三部分同样,第一部分与第二部分对称.假设存在求最长的符合这样的条件的序列. ...
- 代码随想录第十三天 | 150. 逆波兰表达式求值、239. 滑动窗口最大值、347.前 K 个高频元素
第一题150. 逆波兰表达式求值 根据 逆波兰表示法,求表达式的值. 有效的算符包括 +.-.*./ .每个运算对象可以是整数,也可以是另一个逆波兰表达式. 注意 两个整数之间的除法只保留整数部分. ...
- 代码随想录第二天| 977.有序数组的平方 ,209.长度最小的子数组 ,59.螺旋矩阵II
2022/09/22 第二天 第一题 这题我就直接平方后排序了,很无脑但很快乐啊(官方题解是双指针 第二题 滑动窗口的问题,本来我也是直接暴力求解发现在leetCode上超时,看了官方题解,也是第一次 ...
- 代码随想录算法训练营day08 | leetcode 344.反转字符串/541. 反转字符串II / 剑指Offer05.替换空格/151.翻转字符串里的单词/剑指Offer58-II.左旋转字符串
基础知识 // String -> char[] char[] string=s.toCharArray(); // char[] -> String String.valueOf(str ...
- 39. Combination Sum + 40. Combination Sum II + 216. Combination Sum III + 377. Combination Sum IV
▶ 给定一个数组 和一个目标值.从该数组中选出若干项(项数不定),使他们的和等于目标值. ▶ 36. 数组元素无重复 ● 代码,初版,19 ms .从底向上的动态规划,但是转移方程比较智障(将待求数分 ...
- 【leetcode 简单】第三十三题 验证回文串
给定一个字符串,验证它是否是回文串,只考虑字母和数字字符,可以忽略字母的大小写. 说明:本题中,我们将空字符串定义为有效的回文串. 示例 1: 输入: "A man, a plan, a c ...
- leecode刷题(15)-- 验证回文字符串
leecode刷题(15)-- 验证回文字符串 验证回文字符串 给定一个字符串,验证它是否是回文串,只考虑字母和数字字符,可以忽略字母的大小写. 说明:本题中,我们将空字符串定义为有效的回文串. 示例 ...
- 10.1 csp-s模拟测试(b) X国的军队+排列组合+回文
T1 X国的军队 贪心,按$b-a$的大小降序排序,然后就贪心吧 #include<iostream> #include<cstdio> #include<algorit ...
- [程序员代码面试指南]字符串问题-回文最少分割数(DP)
问题描述 给定一个字符串,输出把它全部切成回文子串的最小分割数. 例:str="ACDCDCDAD",输出2. 解题思路 DP 存储结构 dp数组dp[len+1],dp[i]表示 ...
随机推荐
- 推荐一个DeepSeek 大模型的免费 API 项目!兼容OpenAI接口!
在AI技术飞速发展的今天,大语言模型(LLM)的应用越来越广泛,但高昂的使用成本常常让个人开发者和小型团队望而却步.今天,我要为大家介绍一个非常实用的开源项目--DeepSeek-Free-API,它 ...
- DeepSeek R1本地与线上满血版部署:超详细手把手指南
一.DeepSeek R1本地部署 1.下载ollama下载地址 本人是Mac电脑,所以选第一项,下面都是以Mac环境介绍部署,下载好把ollama运行起来即可启动Ollama服务. Ollama默认 ...
- IDEA - 文件上方的文档注释如何自定义
1.在设置中打开文件和代码模板,根据描述中的参考信息进行自定义配置 File > Settings > Editor > File and Code Templates 2.配置完成 ...
- C#(面向对象的托管语言)类库(区别于应用程序)的异常处理思路
1.不要做出任何应用程序才需要考虑抉择策略,不能想当然的决定一些错误情形.具体的一个体现形式是什么异常都捕获.这不是类库的职责,因为无法掌握所有的调用者的使用情形,这些不确定性是委托.虚方法.接口等特 ...
- 基于Microsoft.Extensions.VectorData实现语义搜索
大家好,我是Edison. 上周水了一篇 Microsoft.Extensions.AI 的介绍文章,很多读者反馈想要了解更多.很多时候,除了集成LLM实现聊天对话,还会有很多语义搜索和RAG的使用场 ...
- FastAPI路由与请求处理进阶指南:解锁企业级API开发黑科技 🔥
title: FastAPI路由与请求处理进阶指南:解锁企业级API开发黑科技 date: 2025/3/3 updated: 2025/3/3 author: cmdragon excerpt: 5 ...
- python py文件名称不能和库名称一样,否则报错module 'requests' has no attribute 'post'
这个问题自己犯过几次,加深一下记忆
- [Qt基础-07 QSignalMapper]
QSignalMapper 本文主要根据QT官方帮助文档以及日常使用,简单的介绍一下QSignalMapper的功能以及使用 文章目录 QSignalMapper 简介 使用方法 主要的函数 信号和槽 ...
- 关于valueOf的一点思考
官方描述:返回值为该对象的原始值. 来源:Object.prototype,所以所有js对象都继承了此方法,根据犀牛书第六版的描述,对象转换为数字和字符串的时候的过程是不一样的. 对象 -> 字 ...
- offsetTop && offsetParent
在迄今为止的一年里,做滚动动画的时候其实对一个概念比较模糊,就是一个元素在此文档中距离文档顶部的距离,一开始的想法是一个元素距离顶部的距离就是此元素同级的previous兄弟节点的高度和加上此元素的父 ...