lotus

贵有恒何必三更眠五更起 最无益只怕一日曝十日寒

  博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

1. 题目

 

 

读题

https://leetcode.cn/problems/combination-sum/

 

你好,这是Bing。根据搜索结果,leetcode 39是一个编程题目,要求你找出一个整数数组中所有可以使数字和为目标数的不同组合¹²。

例如,如果数组是[2,3,6,7],目标数是7,

那么输出应该是[[2,2,3],[7]]。

 

考查点

 

根据搜索结果,这道题的考查点是**回溯算法**²。回溯算法是一种递归搜索的方法,可以用来找出所有可能的解。这道题要求你使用回溯算法来遍历整数数组,找出所有可以使数字和为目标数的组合。你需要注意以下几点:
- 你可以重复使用数组中的同一个数字,所以在递归时不需要改变起始位置²。
- 你需要剪枝优化,即在搜索过程中排除一些不可能的情况,比如当数字和已经超过目标数时,就不需要继续搜索了²。
- 你需要排序数组,以便在剪枝时提前判断数字和是否超过目标数²。


(1) 「leetcode」39. 组合总和【回溯算法】详解! - 知乎. https://zhuanlan.zhihu.com/p/308364408.
(2) 39. 组合总和 - 力扣(Leetcode). https://leetcode.cn/problems/combination-sum/.
(3) Combination Sum - LeetCode. https://leetcode.com/problems/combination-sum/.

2. 解法

思路

当然可以。思路是这样的:

我们要找出所有可能的组合,所以我们需要遍历数组中的每个元素,看看它是否可以加入到当前的组合中。如果可以,我们就把它加入到当前组合中,然后递归地寻找剩余的目标和。如果不可以,我们就跳过它,继续考虑下一个元素。

为了避免重复的组合,我们需要保证每次递归时,只考虑当前元素及其之后的元素,而不是从头开始。这样,我们就可以保证每个元素只出现在它之前的元素后面,而不会出现在它之前的元素前面。

为了方便回溯,我们使用一个链表来存储当前的组合,每次加入一个元素时,就把它放到链表的末尾,每次回溯时,就把它从链表的末尾移除。

当目标和为零时,说明我们找到了一个有效的组合,我们就把当前的链表转换成一个数组,并加入到结果列表中。当目标和小于零时,或者数组索引越界时,说明我们已经无法找到有效的组合了,我们就直接返回。

这样,通过不断地递归和回溯,我们就可以找出所有可能的组合。

 

代码的思路可以分为以下几个步骤:

  1. 定义两个全局变量,一个是 res,用来存储最终的结果列表,一个是 tmp,用来存储当前的组合链表。
  2. 定义一个公共方法 combinationSum,接收两个参数,一个是 candidates 数组,一个是 target 目标和。在这个方法中,我们初始化 res 和 tmp 为空列表,然后调用一个私有方法 dfs,传入 candidates 数组,target 目标和,和 0 作为初始的数组索引。最后返回 res 作为结果。
  3. 定义一个私有方法 dfs,接收三个参数,一个是 candidates 数组,一个是 target 目标和,一个是 idx 数组索引。在这个方法中,我们进行以下判断和操作:
    • 如果 target 等于 0,说明我们找到了一个有效的组合,我们就把 tmp 链表转换成数组,并加入到 res 列表中,然后返回。
    • 如果 idx 大于等于 candidates 数组的长度,或者 target 小于 0,说明我们已经无法找到有效的组合了,我们就直接返回。
    • 否则,我们先调用 dfs 方法,传入 candidates 数组,target 目标和,和 idx + 1 作为下一个数组索引。这样相当于跳过当前的元素,不加入到当前的组合中。
    • 然后,我们把当前的元素 candidates [idx] 加入到 tmp 链表的末尾,再调用 dfs 方法,传入 candidates 数组,target - candidates [idx] 作为新的目标和,和 idx 作为不变的数组索引。这样相当于选取当前的元素,并继续寻找剩余的目标和。
    • 最后,我们把 tmp 链表的最后一个元素移除,相当于回溯到上一层状态。

这样,通过不断地递归和回溯,我们就可以找出所有可能的组合。

 

 

解释一下第三条。😊

第三条是定义一个私有方法 dfs,接收三个参数,一个是 candidates 数组,一个是 target 目标和,一个是 idx 数组索引。在这个方法中,我们进行以下判断和操作:

这个方法的作用是使用回溯算法,从数组的第 idx 个元素开始,寻找所有可能的组合,使得它们的和等于 target。

我们需要考虑两种情况,一种是选取当前的元素,一种是跳过当前的元素。

为了避免重复的组合,我们需要保证每次递归时,只考虑当前元素及其之后的元素,而不是从头开始。

具体的操作如下:

  • 如果 target 等于 0,说明我们找到了一个有效的组合,我们就把 tmp 链表转换成数组,并加入到 res 列表中,然后返回。这是递归的终止条件。
  • 如果 idx 大于等于 candidates 数组的长度,或者 target 小于 0,说明我们已经无法找到有效的组合了,我们就直接返回。这也是递归的终止条件。
  • 否则,我们先调用 dfs 方法,传入 candidates 数组,target 目标和,和 idx + 1 作为下一个数组索引。这样相当于跳过当前的元素,不加入到当前的组合中。这是递归的一种分支。
  • 然后,我们把当前的元素 candidates [idx] 加入到 tmp 链表的末尾,再调用 dfs 方法,传入 candidates 数组,target - candidates [idx] 作为新的目标和,和 idx 作为不变的数组索引。这样相当于选取当前的元素,并继续寻找剩余的目标和。这是递归的另一种分支。
  • 最后,我们把 tmp 链表的最后一个元素移除,相当于回溯到上一层状态。这是为了恢复之前的选择,以便进行下一次选择。

这样,通过不断地递归和回溯,我们就可以找出所有可能的组合。

具体实现

class Solution {
    List<List<Integer>> res; // 存储结果
    LinkedList<Integer> tmp; // 存储当前组合
    public List<List<Integer>> combinationSum(int[] candidates, int target) {
        res = new ArrayList<>(); // 初始化结果列表
        tmp = new LinkedList<>(); // 初始化当前组合
        dfs(candidates, target, 0); // 从数组第一个元素开始回溯
        return res; // 返回结果
    }
    private void dfs(int[] candidates, int target, int idx) {
        if (target == 0) { // 如果目标和为零,说明找到了一个有效的组合
            res.add(new ArrayList<>(tmp)); // 将当前组合加入到结果列表中
            return; // 返回
        }
        if (idx >= candidates.length || target < 0) { // 如果数组索引越界,或者目标和小于零,说明当前组合无效
            return; // 返回
        }
        // 跳过当前元素
        dfs(candidates, target, idx + 1); 
        // 选取当前元素
        tmp.add(candidates[idx]); 
        dfs(candidates, target - candidates[idx], idx); // 继续寻找剩余的目标和
        tmp.removeLast(); // 回溯,移除当前元素
    }
}

  

3. 总结

posted on 2023-04-29 23:17  白露~  阅读(5)  评论(0编辑  收藏  举报