目录
[题目一:39. 组合总和](#题目一:39. 组合总和)
一、做题心得
今天是代码随想录打卡的第23天,来到了回溯章节的part2。今天共完成了三道打卡题,分别是组合问题的延续以及分割回文串的经典问题。整体来说,今天的题还是有些比较新颖的地方,尤其是一些需要注意的点,等下会慢慢提到。
好了,话不多说,直接开始今天的内容。
二、题目与题解
题目一: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 输出: []
提示:
1 <= candidates.length <= 30
2 <= candidates[i] <= 40
candidates
的所有元素 互不相同1 <= target <= 40
题解:回溯
这个题和昨天打卡做的216. 组合总和 III - 力扣(LeetCode)基本一致,这里唯一的区别就是这道题允许同一个数字多次使用。怎样考虑到多次使用同一个数的情况呢 ?其实我们只需要改变递归状态就可以了--我们知道,递归在这里等价于纵向遍历,只要我们每次递归的时候都再次从当前开始的位置进行,那么就考虑到了多次使用同一个数的情况:
即**backtrack(candidates,target,i);**每次递归都从i再次开始。(注意与216题区分)
代码如下(注意我个人习惯用ans表示结果数组,vec表示临时(当前)数组):
cpp
class Solution {
public:
vector<vector<int>> ans;
vector<int> vec;
void backtrack(vector<int>& candidates,int target,int start) { //回溯函数:注意start参数的定义--实现递归必不可少的部分
int sum = 0; //用来统计当前选取元素之和
for (int i = 0; i < vec.size(); i++) {
sum += vec[i];
}
if (sum == target) {
ans.push_back(vec);
return;
}
if (sum > target) { //(注意)剪枝:当前选取元素求和大于目标值,则无需继续添加元素,返回递归
return;
}
for (int i = start; i < candidates.size(); i++) {
vec.push_back(candidates[i]);
backtrack(candidates,target,i); //不用i+1了,表示可以重复读取当前的数(与不能重复的题相区分)
vec.pop_back();
}
}
vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
backtrack(candidates,target,0);
return ans;
}
};
题目二: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] ]
提示:
1 <= candidates.length <= 100
1 <= candidates[i] <= 50
1 <= target <= 30
题解:回溯
这个题的话,看上去和之前两个题差不多,也是组合求和问题,所以我们依然可以套用之前的模板。不过需要注意的是,题目中 candidates 数组中可能存在重复的数字,而每个数字不能重复使用,最重要的是,要满足解集不能包含重复的组合-- 这个就将是这个题的关键(即如何实现去重)。
我们首先要搞清楚一个概念:回溯过程就像是树的遍历搜索--for循环对应树的横向操作(即针对某一层),递归则对应着树的纵向遍历(即针对一条的分支,一个路线)。
我们去重,需要的是解集不能包含重复的组合(即多个组合之间的问题 ),而不是某个组合不能出现同样的元素--这就需要我们考虑横向操作,即不能在同一层中出现相同的元素。(这可能不是很好理解,就是纵向操作(递归)是解决的一个组合里边的问题,横向操作(for循环)是解决多个组合之间的问题)
其他部分和之前的题差不多,应该问题不大,这里我们直接看代码:
cpp
class Solution {
public:
vector<vector<int>> ans;
vector<int> vec;
void backtrack(vector<int>& candidates, int target, int start) {
int sum = 0;
for (int i = 0; i < vec.size(); i++) {
sum += vec[i];
}
if (sum == target) {
ans.push_back(vec);
return;
}
if (sum > target) { //剪枝操作
return;
}
for (int i = start; i < candidates.size(); i++) {
if (i > start && candidates[i] == candidates[i - 1]) { //横向操作:跳过重复元素--注意理解:这里跳过的是同一个树层重复的元素(横向看),而不是某一个组合中重复的元素(纵向看--每一个组合都是由不同树层元素构成的)
continue; //跳过同一树层使用过的元素
}
vec.push_back(candidates[i]);
backtrack(candidates, target, i + 1); //纵向操作(不同树层):递归遍历得到各种组合
vec.pop_back();
}
}
vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
sort(candidates.begin(), candidates.end()); //先对 candidates 进行排序:排序后则重复元素相邻
backtrack(candidates, target, 0);
return ans;
}
};
题目三:131.分割回文串
题目链接
给你一个字符串
s
,请你将s
分割成一些子串,使每个子串都是回文串
。返回s
所有可能的分割方案。示例 1:
输入:s = "aab" 输出:[["a","a","b"],["aa","b"]]
示例 2:
输入:s = "a" 输出:[["a"]]
提示:
1 <= s.length <= 16
s
仅由小写英文字母组成
题解:回溯
这个题相对前两道难度就要大一点了。
我们先回顾下回文串的判断:双指针。这个没记错的话,应该是刚开始那几天打卡的内容吧,现在看来已经很简单了。通过定义左右指针一个从头遍历,一个从尾遍历,比较对应的字符,若一致,则左指针右移,右指针左移,继续遍历,直到结束;若不一致,则不是回文串。
这道题的话,个人认为主要的难点在于终止条件的书写以及如何将子串分割出来并做判断--判断是否为回文串。
这里我们先引入一个函数:substr
s.substr(pos, len):表示截取字符串 s 从 start 开始往后长度为 len 的子串
这样我们就能够通过递归得出字符串的所有子串,并挨个判断是否是回文串了。
这样终止条件也就好确定了:即到达递归调用的终点,此时vec存放满足的字符串组合
代码如下:
cpp
class Solution {
public:
vector<vector<string>> ans;
vector<string> vec;
bool isHuiWen(string str) { //判断字符串是否为回文子串:简单双指针
int left = 0;
int right = str.size() - 1;
while (left < right) {
if (str[left] != str[right]) {
return false;
}
else {
left++;
right--;
}
}
return true;
}
void backtrack(string s, int start) {
if (start == s.size()) { //终止条件:到达递归调用的终点
ans.push_back(vec);
return;
}
for (int i = start; i < s.size(); i++) {
string substring = s.substr(start, i - start + 1); //截取从start到i的子串
if (isHuiWen(substring)) { //如果当前子串是回文,则加入当前划分,并继续向后搜索
vec.push_back(substring);
backtrack(s, i + 1); //注意start在递归调用过程中不断变化的,它代表了当前正在考虑的子串的起始位置
vec.pop_back();
}
}
}
vector<vector<string>> partition(string s) {
backtrack(s, 0);
return ans;
}
};
三、小结
今天的打卡到此也就结束了,重点学习了回溯的实现过程。最后,我是算法小白,但也希望终有所获。