代码随想录算法训练营第二十六天 | 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;
    }
}
相关文章
|
16天前
|
存储 算法 程序员
C 语言递归算法:以简洁代码驾驭复杂逻辑
C语言递归算法简介:通过简洁的代码实现复杂的逻辑处理,递归函数自我调用解决分层问题,高效而优雅。适用于树形结构遍历、数学计算等领域。
|
17天前
|
并行计算 算法 测试技术
C语言因高效灵活被广泛应用于软件开发。本文探讨了优化C语言程序性能的策略,涵盖算法优化、代码结构优化、内存管理优化、编译器优化、数据结构优化、并行计算优化及性能测试与分析七个方面
C语言因高效灵活被广泛应用于软件开发。本文探讨了优化C语言程序性能的策略,涵盖算法优化、代码结构优化、内存管理优化、编译器优化、数据结构优化、并行计算优化及性能测试与分析七个方面,旨在通过综合策略提升程序性能,满足实际需求。
46 1
|
26天前
|
存储 缓存 算法
通过优化算法和代码结构来提升易语言程序的执行效率
通过优化算法和代码结构来提升易语言程序的执行效率
|
1月前
|
算法
分享一些提高二叉树遍历算法效率的代码示例
这只是简单的示例代码,实际应用中可能还需要根据具体需求进行更多的优化和处理。你可以根据自己的需求对代码进行修改和扩展。
|
1月前
|
存储 算法 Java
leetcode算法题-有效的括号(简单)
【11月更文挑战第5天】本文介绍了 LeetCode 上“有效的括号”这道题的解法。题目要求判断一个只包含括号字符的字符串是否有效。有效字符串需满足左括号必须用相同类型的右括号闭合,并且左括号必须以正确的顺序闭合。解题思路是使用栈数据结构,遍历字符串时将左括号压入栈中,遇到右括号时检查栈顶元素是否匹配。最后根据栈是否为空来判断字符串中的括号是否有效。示例代码包括 Python 和 Java 版本。
|
1月前
|
算法 测试技术 开发者
在Python开发中,性能优化和代码审查至关重要。性能优化通过改进代码结构和算法提高程序运行速度,减少资源消耗
在Python开发中,性能优化和代码审查至关重要。性能优化通过改进代码结构和算法提高程序运行速度,减少资源消耗;代码审查通过检查源代码发现潜在问题,提高代码质量和团队协作效率。本文介绍了一些实用的技巧和工具,帮助开发者提升开发效率。
41 3
|
1月前
|
分布式计算 Java 开发工具
阿里云MaxCompute-XGBoost on Spark 极限梯度提升算法的分布式训练与模型持久化oss的实现与代码浅析
本文介绍了XGBoost在MaxCompute+OSS架构下模型持久化遇到的问题及其解决方案。首先简要介绍了XGBoost的特点和应用场景,随后详细描述了客户在将XGBoost on Spark任务从HDFS迁移到OSS时遇到的异常情况。通过分析异常堆栈和源代码,发现使用的`nativeBooster.saveModel`方法不支持OSS路径,而使用`write.overwrite().save`方法则能成功保存模型。最后提供了完整的Scala代码示例、Maven配置和提交命令,帮助用户顺利迁移模型存储路径。
|
3月前
|
Unix Shell Linux
LeetCode刷题 Shell编程四则 | 194. 转置文件 192. 统计词频 193. 有效电话号码 195. 第十行
本文提供了几个Linux shell脚本编程问题的解决方案,包括转置文件内容、统计词频、验证有效电话号码和提取文件的第十行,每个问题都给出了至少一种实现方法。
LeetCode刷题 Shell编程四则 | 194. 转置文件 192. 统计词频 193. 有效电话号码 195. 第十行
|
4月前
|
Python
【Leetcode刷题Python】剑指 Offer 32 - III. 从上到下打印二叉树 III
本文介绍了两种Python实现方法,用于按照之字形顺序打印二叉树的层次遍历结果,实现了在奇数层正序、偶数层反序打印节点的功能。
61 6
|
4月前
|
搜索推荐 索引 Python
【Leetcode刷题Python】牛客. 数组中未出现的最小正整数
本文介绍了牛客网题目"数组中未出现的最小正整数"的解法,提供了一种满足O(n)时间复杂度和O(1)空间复杂度要求的原地排序算法,并给出了Python实现代码。
123 2