LeetCode题练习与总结:组合总和Ⅱ

91 篇文章 0 订阅
55 篇文章 0 订阅

一、题目描述

给定一个候选人编号的集合 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。

candidates 中的每个数字在每个组合中只能使用 一次 。

注意:解集不能包含重复的组合。 

示例 1:

输入: candidates = [10,1,2,7,6,1,5], target = 8,
输出:
[
[1,1,6],
[1,2,5],
[1,7],
[2,6]
]

示例 2:

输入: candidates = [2,5,2,1,2], target = 5,
输出:
[
[1,2,2],
[5]
]

提示:

  • 1 <= candidates.length <= 100
  • 1 <= candidates[i] <= 50
  • 1 <= target <= 30

二、解题思路

  1. 排序:首先对 candidates 数组进行排序,这样可以避免在搜索过程中重复使用相同的元素,并且可以利用已经排序的特性来剪枝。

  2. 回溯搜索:使用递归函数进行回溯搜索,每次选择一个候选数字,并从剩余的候选数字中继续寻找可能的组合。

  3. 剪枝:在搜索过程中,如果当前的数字和已经达到或超过目标数 target,则直接返回,不再继续搜索。同时,如果当前选择的数字与上一次选择的数字相同,为了防止重复,也需要剪枝。

  4. 结果存储:当找到一个有效的组合时,将其添加到结果列表中。

三、具体代码

import java.util.ArrayList;
import java.util.List;

public class Solution {
    public List<List<Integer>> combinationSum2(int[] candidates, int target) {
        List<List<Integer>> result = new ArrayList<>();
        List<Integer> combination = new ArrayList<>();
        Arrays.sort(candidates); // 对候选数组进行排序
        dfs(candidates, target, 0, combination, result);
        return result;
    }

    private void dfs(int[] candidates, int remain, int start, List<Integer> combination, List<List<Integer>> result) {
        if (remain == 0) { // 如果当前剩余值为0,说明找到了一个有效的组合
            result.add(new ArrayList<>(combination)); // 将当前组合添加到结果中
            return;
        }

        for (int i = start; i < candidates.length; i++) {
            if (remain - candidates[i] < 0) break; // 剪枝,如果当前数字使剩余值小于0,则不再继续
            if (i > start && candidates[i] == candidates[i - 1]) continue; // 跳过重复的数字

            combination.add(candidates[i]); // 选择当前数字
            dfs(candidates, remain - candidates[i], i + 1, combination, result); // 递归搜索剩余的组合
            combination.remove(combination.size() - 1); // 回溯,移除当前数字
        }
    }
}

四、时间复杂度和空间复杂度

1. 时间复杂度
  • 排序步骤Arrays.sort(candidates) 是对输入数组进行排序的步骤,其时间复杂度通常是 O(n log n),其中 n 是数组 candidates 的长度。

  • DFS搜索:深度优先搜索(DFS)是算法的核心部分。在每次递归中,我们遍历数组 candidates 并尝试每个可能的数字。由于数组已经排序,我们可以通过剪枝来避免不必要的搜索,例如跳过重复的数字和超出目标值的组合。理想情况下,每次递归都会减少目标值 remain,直到 remain 为 0,此时我们找到了一个有效的组合。在最坏的情况下,如果目标值 target 可以通过数组中的多个不同组合来达到,那么我们需要遍历所有可能的组合。因此,DFS搜索的时间复杂度可以表示为 O(2^n),其中 n 是数组 candidates 的长度。然而,由于剪枝的存在,实际的搜索空间会小得多。

  • 总结来说,排序步骤的时间复杂度是 O(n log n),而DFS搜索的时间复杂度在最坏情况下是 O(2^n),但实际执行时由于剪枝操作,时间复杂度会显著降低。

2. 空间复杂度
  • 递归栈:由于使用了递归,我们需要考虑递归栈的深度。在最坏的情况下,递归栈的深度可能达到 n,即数组 candidates 的长度,因为每次递归调用都可能创建一个新的递归层级。

  • 结果存储:结果列表 result 存储所有有效的组合。在最坏的情况下,如果每个候选数字都可以独立地构成目标值 target,那么结果列表的大小将接近 O(n),其中 n 是数组 candidates 的长度。

  • 综合考虑,总的空间复杂度可以表示为 O(n + result.size),其中 n 是数组 candidates 的长度,result.size 是结果列表的大小。在最坏的情况下,结果列表的大小可能会非常大,但是由于剪枝的存在,实际的 result.size 通常会小于 2^n

五、总结知识点

1. 数组排序:使用 Arrays.sort(candidates) 对候选人数组 candidates 进行排序。这是为了确保后续搜索过程中可以利用有序性来剪枝,避免重复使用相同的数字,并减少搜索空间。

2. 深度优先搜索(DFS):通过递归的方式遍历所有可能的组合。DFS 是一种用于遍历或搜索树或图的算法,它沿着一个分支深入到不能再深入为止,然后回溯到上一个分支点继续搜索,直到找到所需的解或遍历完所有的分支。

3. 回溯法:这是一种通过递归来实现的算法策略,它尝试分步解决问题。如果当前步骤不能得到有效的解,它将取消上一步或几步的计算,再通过其他可能的分步解决方案继续尝试。

4. 剪枝:在搜索过程中,通过一些条件判断来减少不必要的搜索,提高算法效率。在这段代码中,剪枝体现在两个地方:

  • 如果当前剩余值 remain 小于当前候选数字 candidates[i],则没有必要继续搜索,因为剩余值不可能通过更大的数字来满足。
  • 如果当前候选数字与前一个候选数字相同(即 candidates[i] == candidates[i - 1]),则跳过当前数字以避免重复的组合。

5. 动态规划思想:虽然代码中没有直接使用动态规划,但是其解决问题的思想与动态规划有相似之处。动态规划通常用于解决具有重叠子问题和最优子结构特性的问题,而这段代码通过递归和回溯来寻找问题的最优解。

以上就是解决这个问题的详细步骤,希望能够为各位提供启发和帮助。

  • 40
    点赞
  • 35
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

一直学习永不止步

谢谢您的鼓励,我会再接再厉的!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值