代码随想录算法训练营第二十六天 | LeetCode 39. 组合总和、40. 组合总和 II、131. 分割回文串

简介: 代码随想录算法训练营第二十六天 | LeetCode 39. 组合总和、40. 组合总和 II、131. 分割回文串

1. LeetCode 39. 组合总和

1.1 思路

  1. 这题跟上面那些组合的题目的区别在于可以重复取数,而且这题抽象出来的树形结构的深度是由和来限定的。这里我们举例,数组[2,5,3],和为4,那么我们取了2之后,子集合是[2,5,3],因为可以重复取数,后续接着取。然后如果在第一层取5时(此时取2的路径已经走完了),子集合就是[5,3]了,这时如果再把2带上就会得到重复的组合,这也是startIndex控制的地方。这题和上面做个的类似组合问题的差别就是子集合是把已经选过的元素带上的,这样才可达到可重复选取的目的
  2. 定义两个全局变量,二维数组result存最终结果,path存放当前的单次结果。
  3. 回溯算法的返回值和参数:返回值void,参数candidates,target,sum统计path里的和,startIndex控制子集合的初始位置
  4. 终止条件:如果sum>target就return;如果sum==target就把path加入result中,然后return
  5. 单层搜索的逻辑:for循环(int i=startIndex; i<candidates.length; i++)。
  6. 注意:这里的for循环中startIndex第一次传入时是从0开始的,就是大家最常见的数组遍历方式,为什么与上面做过的那些组合的for循环不一样呢?因为上面的题取的元素都是1到9这样子的,要从1开始取之类的,而本题的数组遍历是通过下标遍历的,因此有点不一样
  7. 接上述的for循环,然后path.add(数组[i]);sum+=数组[i];然后就递归的过程backtracking(candidates, target, sum, i)。注意这里传入的是i不是startIndex,因为传入startIndex就会一直是0,而i会在for循环里递增,那样才是正确的遍历顺序。而这里不用i+1就是可以重复取数的关键,下一层递归依然可以取同一个数
  8. 然后是回溯的过程,path.removeLast()去除最后一个元素也就是刚刚加入的元素,这样才能往树的右边搜索,并且sum-=数组[i]
  9. 剪枝优化:通常是在for循环里做文章的,剪枝就是剪去多余的分支,分支就体现在for循环上,原理就是让其少循环,上述的sum>target就是不进入循环,下面的方式就是提前退出循环。对于本题,还是上面的例子[2 ,5, 3],先取出2和5发现已经大于4了,如果我们先排序数组,变成[2, 3, 5]那么取到2和3发现已经大于4了,那么后续的5就可以不用去搜索了。
  10. 这个操作就是写在for循环里的,如果sum+数组[i]>target了就直接break跳出循环,这就是把此树右边的分支剪去了。

1.2 代码

//
// 剪枝优化
class Solution {
    public List<List<Integer>> combinationSum(int[] candidates, int target) {
        List<List<Integer>> res = new ArrayList<>();
        Arrays.sort(candidates); // 先进行排序
        backtracking(res, new ArrayList<>(), candidates, target, 0, 0);
        return res;
    }
    public void backtracking(List<List<Integer>> res, List<Integer> path, int[] candidates, int target, int sum, int idx) {
        // 找到了数字和为 target 的组合
        if (sum == target) {
            res.add(new ArrayList<>(path));
            return;
        }
        for (int i = idx; i < candidates.length; i++) {
            // 如果 sum + candidates[i] > target 就终止遍历
            if (sum + candidates[i] > target) break;
            path.add(candidates[i]);
            backtracking(res, path, candidates, target, sum + candidates[i], i);
            path.remove(path.size() - 1); // 回溯,移除路径 path 最后一个元素
        }
    }
}

2. LeetCode 40. 组合总和 II

2.1 思路

  1. 这题和上述的组合类的题目的区别在于题目给的数组是有重复元素的,上面做过的是没有的,而且题目要求组合中不可以有重复的组合,即答案里的数组可以出现[1,1,6]但不同出现[1,1,6],[1,1,6],因此要去重
  2. 举个例子,数组[10, 1, 2, 7, 6, 1, 5],和为8,这里就会出现两个1和7组成组合然后和为8,因此要做去重,而且题目要求同一个元素在单个组合中只能使用一次。大逻辑就是取过的元素就不要再取了(好像是废话)。
  3. 本题科普两个概念:树层去重,树枝去重。我们要考虑这两个维度,从这两个上考虑去重
  4. 我们需要定义一个boolean数组used,长度和题目给的数组是一样大的,如果对应下标的数组元素使用过就定义为true否则为false。再举个例子如上图所示,排好序的数组[1, 1, 2],和为3。取1后used[0]=true,子集合为[1, 2],以此类推,同样需要startIndex控制起始位置,for循环往右搜索的过程,起始位置都是不断+1的,只是我们要用used标记取过的数为true
  5. 树枝去重:树枝上我们第一层取了1之后,第二层也是可以取1的,这两个1不会是同一个元素的,因为传入的是i+1
  6. 树层去重:树层上我们左边第一个分支取1了,第二个分支还取1虽然不是同一个元素的,但是我们都是从这个1后面开始取的,也就是比如[1, 1, 2,......]这里的第一个1和第二个1虽然不同,但是后面的是相同的,我们可能取后面的元素,就可能重复了。因此关键在树层去重
  7. 定义两个全局变量result存放全部结果、path存放单个结果
  8. 回溯函数的参数和返回值:返回值void,参数candidates,target,sum计算path里的和,startIndex控制子集合的起始位置,为了防止组合里出现重复元素,boolean数组used标记哪个元素用过。不过我们在进入这个回溯函数之前要在主函数先给数组排序,因为去重是在这个前提下进行的
  9. 终止条件:如果sum>target就return。如果sum==target就把path加入result中,然后return
  10. 单层搜索的逻辑:for(int i=startIndex;i<candidates.length;i++)。首先是树层去重的逻辑,如果(i>0&&candidates[i]==candidates[i-1]&&!used[i-1])避免i-1为负数,首先要求i>0,然后是如果前后两个元素相等了就不能再取了,不然就重复了,然后下一个条件是前一个元素不能用过,这里是为了往下递归时,举例,取了一个1后,还能取多个1,这里的多个1不是同一个1,只是数值相同,这个条件不是为了树层去重的,而是为了取数找到结果。如果符合条件就continue跳过此次循环,继续往后取。
  11. 然后就是继续收集元素的过程path.add(数组[i]);sum+=数组[i],used[i]=true;然后就递归的过程backtracking(candidates, target, sum, i+1, used)。然后就是回溯的过程,path.removeLast()去除最后一个元素也就是刚刚加入的元素,这样才能往树的右边搜索,并且sum-=数组[i],used[i]=false。这里又变为false是为了往右搜索,逻辑和第一层往右搜索的原因是一样的

2.2 代码

//
class Solution {
  LinkedList<Integer> path = new LinkedList<>();
  List<List<Integer>> ans = new ArrayList<>();
  boolean[] used;
  int sum = 0;
  public List<List<Integer>> combinationSum2(int[] candidates, int target) {
    used = new boolean[candidates.length];
    // 加标志数组,用来辅助判断同层节点是否已经遍历
    Arrays.fill(used, false);
    // 为了将重复的数字都放到一起,所以先进行排序
    Arrays.sort(candidates);
    backTracking(candidates, target, 0);
    return ans;
  }
  private void backTracking(int[] candidates, int target, int startIndex) {
    if (sum == target) {
      ans.add(new ArrayList(path));
    }
    for (int i = startIndex; i < candidates.length; i++) {
      if (sum + candidates[i] > target) {
        break;
      }
      // 出现重复节点,同层的第一个节点已经被访问过,所以直接跳过
      if (i > 0 && candidates[i] == candidates[i - 1] && !used[i - 1]) {
        continue;
      }
      used[i] = true;
      sum += candidates[i];
      path.add(candidates[i]);
      // 每个节点仅能选择一次,所以从下一位开始
      backTracking(candidates, target, i + 1);
      used[i] = false;
      sum -= candidates[i];
      path.removeLast();
    }
  }
}

3. LeetCode 131. 分割回文串

3.1 思路

  1. 大概是分两步,先求出所有分割后的结果,在叶子节点的位置,然后在这个位置判断是否都是回文
  2. 回溯函数的参数和返回值:返回值void。要先定义两个全局变量,一维数组path收集路径即单个结果,存放的是字符串,二维数组result收集全部结果。函数参数:字符串s,startIndex控制取了一个字符后,剩下的要在这个字符后面的字符串中取
  3. 终止条件:上面参数startIndex就相当于我们图里画是那条切割的竖线“|”,当这条线“|”到了最后的位置说明到叶子节点了,也就是要处理的结果了。if(startIndex==s.length())就是到了叶子节点,然后result里加入path。我们这里是把判断是否回文的逻辑放在了“单层搜索的逻辑”中了,如果不是回文就不会进行下一层的递归
  4. 单层搜索的逻辑:for(int i=startIndex;i<s.length();i++),这里我们要先判断切割的子串是否为回文串,是才加入到path。上面说过startIndex就是我们切割的线,那么[startIndex,i]这个左闭右闭的区间,就是切割的子串。这里startIndex在for循环里是一个固定的值,i是不断++的,因此他们之间的范围包括两端就是我们要搜索的范围。
  5. 这里定义个函数isPalindrome(s, startIndex, end)分别表示字符串,起始位置,终止位置,来判断这个区间内是否是回文的。如果是回文的,就path.add(子串),否则就continue跳过此次循环。
  6. 然后是递归的逻辑backtracking(s,i+1)为什么i+1是因为切割过的不能重复切割。然后是回溯的过程,path.removeLast(),继续向右搜索。
  7. 判断函数就是用双指针,同时向中间遍历,如果都相等就是回文子串

3.2 代码

//
class Solution {
    List<List<String>> lists = new ArrayList<>();
    Deque<String> deque = new LinkedList<>();
    public List<List<String>> partition(String s) {
        backTracking(s, 0);
        return lists;
    }
    private void backTracking(String s, int startIndex) {
        //如果起始位置大于s的大小,说明找到了一组分割方案
        if (startIndex >= s.length()) {
            lists.add(new ArrayList(deque));
            return;
        }
        for (int i = startIndex; i < s.length(); i++) {
            //如果是回文子串,则记录
            if (isPalindrome(s, startIndex, i)) {
                String str = s.substring(startIndex, i + 1);
                deque.addLast(str);
            } else {
                continue;
            }
            //起始位置后移,保证不重复
            backTracking(s, i + 1);
            deque.removeLast();
        }
    }
    //判断是否是回文串
    private boolean isPalindrome(String s, int startIndex, int end) {
        for (int i = startIndex, j = end; i < j; i++, j--) {
            if (s.charAt(i) != s.charAt(j)) {
                return false;
            }
        }
        return true;
    }
}
相关文章
|
10天前
|
算法
分享一些提高二叉树遍历算法效率的代码示例
这只是简单的示例代码,实际应用中可能还需要根据具体需求进行更多的优化和处理。你可以根据自己的需求对代码进行修改和扩展。
|
13天前
|
存储 算法 Java
leetcode算法题-有效的括号(简单)
【11月更文挑战第5天】本文介绍了 LeetCode 上“有效的括号”这道题的解法。题目要求判断一个只包含括号字符的字符串是否有效。有效字符串需满足左括号必须用相同类型的右括号闭合,并且左括号必须以正确的顺序闭合。解题思路是使用栈数据结构,遍历字符串时将左括号压入栈中,遇到右括号时检查栈顶元素是否匹配。最后根据栈是否为空来判断字符串中的括号是否有效。示例代码包括 Python 和 Java 版本。
|
22天前
|
算法 测试技术 开发者
在Python开发中,性能优化和代码审查至关重要。性能优化通过改进代码结构和算法提高程序运行速度,减少资源消耗
在Python开发中,性能优化和代码审查至关重要。性能优化通过改进代码结构和算法提高程序运行速度,减少资源消耗;代码审查通过检查源代码发现潜在问题,提高代码质量和团队协作效率。本文介绍了一些实用的技巧和工具,帮助开发者提升开发效率。
25 3
|
21天前
|
分布式计算 Java 开发工具
阿里云MaxCompute-XGBoost on Spark 极限梯度提升算法的分布式训练与模型持久化oss的实现与代码浅析
本文介绍了XGBoost在MaxCompute+OSS架构下模型持久化遇到的问题及其解决方案。首先简要介绍了XGBoost的特点和应用场景,随后详细描述了客户在将XGBoost on Spark任务从HDFS迁移到OSS时遇到的异常情况。通过分析异常堆栈和源代码,发现使用的`nativeBooster.saveModel`方法不支持OSS路径,而使用`write.overwrite().save`方法则能成功保存模型。最后提供了完整的Scala代码示例、Maven配置和提交命令,帮助用户顺利迁移模型存储路径。
|
1月前
|
存储 缓存 算法
如何通过优化算法和代码结构来提升易语言程序的执行效率?
如何通过优化算法和代码结构来提升易语言程序的执行效率?
|
27天前
|
缓存 分布式计算 监控
优化算法和代码需要注意什么
【10月更文挑战第20天】优化算法和代码需要注意什么
18 0
|
28天前
|
算法 安全 数据安全/隐私保护
基于game-based算法的动态频谱访问matlab仿真
本算法展示了在认知无线电网络中,通过游戏理论优化动态频谱访问,提高频谱利用率和物理层安全性。程序运行效果包括负载因子、传输功率、信噪比对用户效用和保密率的影响分析。软件版本:Matlab 2022a。完整代码包含详细中文注释和操作视频。
|
5天前
|
算法 数据安全/隐私保护 索引
OFDM系统PAPR算法的MATLAB仿真,对比SLM,PTS以及CAF,对比不同傅里叶变换长度
本项目展示了在MATLAB 2022a环境下,通过选择映射(SLM)与相位截断星座图(PTS)技术有效降低OFDM系统中PAPR的算法实现。包括无水印的算法运行效果预览、核心程序及详尽的中文注释,附带操作步骤视频,适合研究与教学使用。
|
13天前
|
算法 数据挖掘 数据安全/隐私保护
基于FCM模糊聚类算法的图像分割matlab仿真
本项目展示了基于模糊C均值(FCM)算法的图像分割技术。算法运行效果良好,无水印。使用MATLAB 2022a开发,提供完整代码及中文注释,附带操作步骤视频。FCM算法通过隶属度矩阵和聚类中心矩阵实现图像分割,适用于灰度和彩色图像,广泛应用于医学影像、遥感图像等领域。
|
14天前
|
算法 调度
基于遗传模拟退火混合优化算法的车间作业最优调度matlab仿真,输出甘特图
车间作业调度问题(JSSP)通过遗传算法(GA)和模拟退火算法(SA)优化多个作业在并行工作中心上的加工顺序和时间,以最小化总完成时间和机器闲置时间。MATLAB2022a版本运行测试,展示了有效性和可行性。核心程序采用作业列表表示法,结合遗传操作和模拟退火过程,提高算法性能。
下一篇
无影云桌面