LeetCode 39. 组合总和
思路:
还是组合总和,这次不同的是这次给了一个数组candidates,需要在这个数组内寻找符合条件的子数组。值得注意的是,这次选取的数是可以重复的,也就是说,每层子树的搜索范围和之前不一样了。假设我们给了一个candidates数组[1,2,3,4],在第一层我们选取了数字1,那么在下一层,子树的搜索范围还是[1,2,3,4],也就是说1可以重复选取。
为了使1重复能够取到,回想先前我们怎么避免重复选取的。就是在循环每层子树的时候,给定了一个idx,它告诉我们这一层的子树搜索范围是多少的,为了不和上一层重复,每往前走一层我们都会把这个idx加上1,但是这里不需要避免重复了,所以可以直接把当前的i的值传到下一层,而不是i+1。
除此之外还有一个剪枝的操作。我们可以先给candidates排序,然后在每次相加的过程中,如果sum+candidates[i]的值大于target,那么i之后的值一定会大于target,这样就避免了i之后的子树的搜索过程,大大降低了搜索空间。
代码:
class Solution {
public:
vector<int> path;
vector<vector<int>> ans;
int idx = 0;
vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
sort(candidates.begin(), candidates.end());
backtrackting(candidates, target, 0, 0);
return ans;
}
void backtrackting(vector<int>& candidates, int target, int sum, int idx)
{
if (ans.size() == 150)
return;
if (sum == target)
{
ans.push_back(path);
return;
}
for (int i = idx; i < candidates.size(); i++)
{
// 剪枝
if (sum + candidates[i] > target)
break;
path.push_back(candidates[i]);
sum += candidates[i];
backtrackting(candidates, target, sum, i);
path.pop_back();
sum -= candidates[i];
}
}
};
LeetCode 40.组合总和II
思路:
又是一道组合总和题,和39. 组合总和唯一的区别是出现了重复的数字,所以去重是必须要考虑的问题。注意重复的数字和取重复的索引是不一样的概念,以数组nums = [1,1,2]为例。在树的第一层搜索空间索引为[0,1,2],如果可以取重复的索引,那么在下一层的搜索空间索引还是[0,1,2],因为我们并没有把idx+1传入进去。在不可以取重复索引的场合,也就是idx+1传入到下一树层,搜索空间就变成了[1,2]。但是需要注意的是,虽然我们并没有取重复的索引,在第一层取的是nums[0],在第二层取的是nums[1],但因为数组里有重复的数字nums[0] == nums[1],这个时候还是会把同一个数字取两遍,所以需要对重复的数字去重。
很容易想到的一个去重代码为:
if (i > 0 && nums[i] == nums[i-1])
continue;
当nums[i] == nums[i-1]的时候,跳过这一次循环。实际上我们不能这样剪枝,因为数字1可以取两遍,题目要求的是每个数字在每个组合中只能使用一次,我们有两个1,两个1都需要分别使用1次。但是如果不跳过的话,会出现两组答案同为[1,2],因为我们对两个1都进行了搜索,都找到了2,满足target=3这个条件,所以我们只需要令第二个1只使用1次就好。
我们可以用一个数组来记录这个数是否用过,如果第i个数用过的话令used[i] = true,回溯的时候也需要令used[i] = false。当出现nums[i] == nums[i-1]的时候,查找nums[i-1]是否被用过,如果used[i-1] == false,说明i-1在前一颗子树被使用过,需要跳过:
if (i > 0 && candidates[i] == candidates[i - 1] && used[i-1] == false)
continue;
如下图代码随想录中的图所示:
- used[i - 1] == true,说明同一树枝candidates[i - 1]使用过
- used[i - 1] == false,说明同一树层candidates[i - 1]使用过
只有used[i - 1] == false表示现在在同一层数,从i-1回溯过来的。而used[i - 1] == true说明现在在子树上,还没有回溯。所以used[i-1] == false相当于在树层上剪枝,而used[i-1] == true相当于在树枝上剪枝。
另外注意一点,使用的数组是需要排序的,否则的话当数组里有重复的数并且两个数没有挨在一起时nums[i-1]和nums[i]的判断会失效,没办法通过i-1来查找到是否与i重复。还有一点是,为了节省空间可以让i>idx代替used数组,因为i>idx的时候i必然是在数层中的第二个往后的节点查找,也相当于在树层上剪枝。
代码:
class Solution {
public:
vector<int> path;
vector<vector<int>> ans;
int idx = 0;
vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
sort(candidates.begin(), candidates.end());
backtrackting(candidates, target, 0, 0);
return ans;
}
void backtrackting(vector<int>& candidates, int target, int sum, int idx)
{
if (sum == target)
{
ans.push_back(path);
return;
}
for (int i = idx; i < candidates.size(); i++)
{
// 剪枝
if (sum + candidates[i] > target)
break;
// 最关键的去重步骤
// 遇到和前一个元素相同的情况(注意不是和后一个,所以是和i-1比较)
// 直接continue跳过当前层的循环,因为同一层数不能重复
// 注意去重的时机,可以在子树重复,所以i==idx的时候是不需要跳过的
if (i > idx && candidates[i] == candidates[i - 1])
continue;
path.push_back(candidates[i]);
sum += candidates[i];
backtrackting(candidates, target, sum, i + 1);
// 回溯
path.pop_back();
sum -= candidates[i];
}
}
};
LeetCode 131.分割回文串
思路:
题目要求在给定的字符串中找到所有为回文的子字符串。解这道题目的逻辑很简单,一共只有两个问题需要考虑:
- 如何分割子字符串
- 如何判断一个子字符串是回文
关于第一个问题,其实就是经典的回溯。用左指针和右指针表示子字符串的头和尾,用枚举的办法把所有可能的子字符串枚举出来。枚举出每一个子字符串后,就要考虑这个子字符串是不是回文,可以用双指针的方法分别指向头和尾,然后左右指针向中间靠拢,确保左右指针所指的字符是一致的,当左指针大于右指针即可。
递归停止的条件为left==s.size(),这说明指针遍历到了字符串的终点,把当前记录下的一组答案push到最终结果里,然后返回树的上一层。在每个树层循环遍历搜索空间的时候,右指针都往后移动,而左指针不动,然后判断右指针和左指针之间的子字符串是否为回文,是回文的话则进入树的下一层,同时更新左右指针,否则的话右指针继续向后移动一位。
代码:
class Solution {
public:
int left;
int right;
vector<string> path;
vector<vector<string>> ans;
vector<vector<string>> partition(string s) {
backtracking(s, 0, 0);
return ans;
}
void backtracking(const string& s, int left, int right)
{
if (right == s.size())
{
ans.push_back(path);
return;
}
for (int i = left; i < s.size(); i++)
{
// 检测子字符串是否为回文
if (check(s, left, i))
{
// 如果是,将当前切割的子字符串放入path
string temp;
for (int j = left; j <= i; j++)
temp.push_back(s[j]);
path.push_back(temp);
backtracking(s, i + 1, i + 1);
// 回溯
path.pop_back();
}
}
}
bool check(const string& s, int left, int right)
{
while (left <= right)
{
if (s[left] != s[right])
return false;
left++;
right--;
}
return true;
}
};