Day23 回溯算法:39.组合总和 40.组合总和Ⅱ 131.分割回文串

1,293 阅读6分钟

39.组合总和

题目链接:39.组合总和

本题是 集合里元素可以用无数次,那么和组合问题的差别 其实仅在于 startIndex 上的控制

难度指数:😀😐😕

Q:如果集合里面有0,你还重复选取,那不就死循环了吗?

A:不用担心,组合里面的元素都是正整数

树形结构

23.01.png

本题用 target 来限制树的高度

代码思路:

定义全局变量

  • 二维数组 vector<vector<int>> result;

  • 一维数组vector<int> path;

sum 用来统计 path 里面的和;也可以不用 sum,可以用 target 做相应的减操作,最后如果 target = 0,正好说明找到了一个组合,和等于 target

(这里也可以直接 sum(path) 放进判断语句,本质一样的,也就是少了两行代码而已哈哈哈哈哈)

 vector<vector<int>> result;
 vector<int> path;
 void backtracking(candidates, target, sum, startIndex) {
     if (sum > target) {
         return;
     }
     if (sum == target) {
         result.push_back(path);
         return;
     }
     for (int i = startIndex; i < candidates.size(); i++) {
         sum += candidates[i];
         path.push_back(candidates[i]);
         backtracking(candidates, target, sum, i);
         sum -= candidates[i];
         path.pop_back();
     }
 }

AC代码: (核心代码模式)

 class Solution {
 private:
     vector<vector<int>> result;
     vector<int> path;
     void backtracking(vector<int>& candidates, int target, int sum, int startIndex) {
         if (sum > target) {
             return;
         }
         if (sum == target) {
             result.push_back(path);
             return;
         }
         for (int i = startIndex; i < candidates.size(); 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();
         backtracking(candidates, target, 0, 0);
         return result;
     }
 };

剪枝(优化):

在用回溯法解决类似问题的时候,剪枝通常要在这个 for循环 做文章

对总集合排序之后,如果下一层的sum(就是本层的 sum + candidates[i])已经大于target,就可以结束本轮for循环的遍历

树形结构

23.02.png

AC代码: (核心代码模式)

 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;
     }
 };

40.组合总和Ⅱ

好题

题目链接:40.组合总和Ⅱ

本题开始涉及到一个问题了:去重

注意题目中给我们的集合是有重复元素的,那么求出来的组合有可能重复,但题目要求不能有重复组合。 (需要去重)

难度指数:😀😐😕

你可能会这么想:我就直接用之前的方式进行搜索,搜索出来若干个组合,这些组合中可能有重复的,然后用 mapset 做去重,将去重后的组合输出。

思路没啥问题,但实现起来麻烦,且容易超时。

代码思路:

我们可以在搜索的过程中直接进行去重

(说人话:使用过的元素不再使用,避免重复)要理解这道题的精髓。

  • 树层去重
  • 树枝去重

可以用回溯算法解决的任何问题都可以抽象成一个树形结构

为了理解去重我们来举一个例子,candidates = [1, 1, 2], target = 3 ,(方便起见 candidates已经排序了)

想要去重需要先进行排序

对应的这个下标,用过就赋成1,没用过赋成0,可以把它定义成 bool 类型。

树形结构

23.03.png

🦄🐧🐣去重的关键在 树层去重

一维数组 path :存放符合条件的单个组合(路径)

二维数组 result :存放各个组合的结果集

这里递归函数无须返回值,因此函数返回类型是 void ;存放组合的结果集也都放在全局变量

Sum 记录单条路径收集的结果和,startIndex 保证组合中不能重复使用相同的元素,

used[] 数组标记元素是否使用过(使用过标记为true)

思考:为什么是 used[i - 1] == 0 而不是 used[i - 1] == 1

 vector<int> path;  //存放符合条件的单个组合
 vector<vector<int>> result;  //存放组合集合
 void backtracking(nums, targetSum, Sum, startIndex, used) {
     //终止条件
     if (Sum > targetSum) {
         return;
     }
     if (Sum == targetSum) {
         result.push_back(path);  //放进结果集
         return;
     }
     //单层搜索的逻辑
     for (int i = startIndex; i < nums.size(); i++) {  //取1      取1      取2
         //树层上要进行去重
         if (i > 0 && nums[i] == nums[i - 1] && used[i - 1] == 0) {  //如果当前遍历元素和前一个元素相同    (i>0是为了防止后面的i-1数组越界)
             continue;  //就是说这里不取1了,继续往后取   (这就是去重)
         }
         path.push_back(nums[i]);  //收集单个组合
         Sum += nums[i];
         used[i] = true;
         backtracking(nums, targetSum, Sum, i + 1, used);  //递归
         path.pop_back();  //回溯
         Sum -= nums[i];   //回溯
         used[i] = false;  //回溯
     }
 }

注:力扣上主函数在调用这个函数之前要先对nums[] 这个集合进行排序。

⚠️去重操作是在排完序之后进行去重的!

23.04.png

AC代码: (核心代码模式)

 class Solution {
 private:
     vector<int> path;  //存放符合条件的单个组合
     vector<vector<int>> result;  //存放组合集合
     void backtracking(vector<int>& nums, int targetSum, int Sum, int startIndex, vector<bool>& used) {
         //终止条件
         if (Sum > targetSum) {
             return;
         }
         if (Sum == targetSum) {
             result.push_back(path);  //放进结果集
             return;
         }
         //单层搜索的逻辑
         for (int i = startIndex; i < nums.size(); i++) {  //取1 取1 取2
             //树层上要进行去重
             if (i > 0 && nums[i] == nums[i - 1] && used[i - 1] == 0) {
                 continue;
             }
             path.push_back(nums[i]);  //收集单个组合
             Sum += nums[i];
             used[i] = true;
             backtracking(nums, targetSum, Sum, i + 1, used);  //递归
             path.pop_back();
             Sum -= nums[i];
             used[i] = false;
         }
     }
 public:
     vector<vector<int>> combinationSum2(vector<int>& nums, int targetSum) {
         int Sum, startIndex;
         vector<bool> used(nums.size(), false);
         path.clear();
         result.clear();
         //先对nums进行排序,让其相同的元素都挨在一起
         sort(nums.begin(), nums.end());
         backtracking(nums, targetSum, 0, 0, used);
         return result;
     }
 };

131.分割回文串

切割问题其实是一种组合问题!

题目链接:131.分割回文串

难度指数:😀😐😕😫

23.05.jpg

代码思路:

一维数组 path :存放符合条件的单个组合(路径)

二维数组 result :存放组合集合

这两个全局变量,我们也是可以考虑放到递归的参数里面。

startIndex 保证切割过的不会重复切割; startIndex 就是切割线(的位置)。

⚠️C++er,注意传入递归函数的参数是const,还是引用之类的。

if (startIndex == s.size()) { //说明已经到了叶子节点 
    result.push_back(path); 
    return;
}

↑↑↑ 同学疑问:收集的 path 应该得是符合回文子串规则,才能加入到 result[] 数组,怎么能直接就把 path 放进 result

其实我们是将判断回文的逻辑放到单层搜索的逻辑里面

在递归函数里面的for循环中,如何表示切割的子串? 这是一个难点

答:startIndex 是切割线,那么切割的字串就是 [startIndex, i] 这个左闭右闭的区间。

i每向后移动一位,就代表一个子串,再向后移动,就代表一个子串,这样startIndex到i之间就是递归搜索过程中不断搜索的范围。

知道了子串的范围之后,就要判断它是不是回文。

 vector<int> path;  //存放符合条件的单个组合(路径)
 vector<vector<int>> result;  //存放组合集合
 void backtracking(const string& s, int startIndex) {
     //终止条件
     if (startIndex >= s.size()) {  //说明已经到了叶子节点   (其实 == 就已经算切割到末尾了)
         result.push_back(path);
         return;
     }
     //单层搜索的逻辑
     for (int i = startIndex; i < s.size(); i++) {
         //(判断切割的子串是不是回文串,若是,放到path数组里)
         if (isPalindrome(s, startIndex, i)) {  //如果是回文
             path.push_back(子串);
         }
         else {  //不是回文,for循环进入下一个i
             continue;
         }
         backtracking(s, i + 1);  //递归
         path.pop_back();  //回溯
         return;
     }
 }

AC代码: (核心代码模式)

 class Solution {
 private:
     vector<string> path;  //存放符合条件的单个组合(路径)
     vector<vector<string>> result;  //存放组合集合
     void backtracking(const string& s, int startIndex) {
         //终止条件
         if (startIndex >= s.size()) {  //说明到叶子节点
             result.push_back(path);
             return;
         }
         //单层搜索的逻辑
         for (int i = startIndex; i < s.size(); i++) {
             if (isPalindrome(s, startIndex, i)) {  //若是回文
                 //获取[startIndex, i]在s中的子串
                 string str = s.substr(startIndex, i - startIndex + 1);
                 path.push_back(str);
             }
             else {  //若不是回文  (for循环进入下一个i)
                 continue;
             }
             backtracking(s, i + 1);  //递归
             path.pop_back();  //回溯
             //return;  (不要return)
         }
     }
     bool isPalindrome(const string& s, int start, int end) {
         for (int i = start, j = end; i < j; i++, j--) {
             if (s[i] != s[j]) {
                 return false;
             }
         }
         return true;
     }
 public:
     vector<vector<string>> partition(string s) {
         result.clear();
         path.clear();
         backtracking(s, 0);
         return result;
     }
 };