回溯算法08-组合总数II(Java/组合去重的两种方法)

本文介绍了解决一个组合问题,给定一个候选数组,找到所有数字之和为目标值的不重复组合。通过使用标记数组和回溯算法,避免了重复组合的生成,给出了Java代码实现及其优化过程。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

8.组合总数II

  • 题目描述

给定一个候选人编号的集合 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]
]
  • 题目分析
相较于之前的组合总数问题,本题会出现重复组合的问题,比如说candidates=[1,2,7,6,1,5] target = 8时
以1为第一个元素的时候,组合中会出现满足条件的[125]2为第一个元素的时候,组合中会出现满足条件的[2, 1, 5]
不难发现这两个是重复的,因此需要去重。

为什么会出现这种情况?

这是因为当第一个元素取1的时候就已经包含了[1,2,5]这组集合元素,再2为第一个元素时就会导致重复,而且我们发现当且只有在同一数层之上会出现这一种情况,那么如何避免呢?

这里可以通过标记的方法解决这一问题
创建used元素用于记录元素的使用情况
i > 0 && candidates[i] == candidates[i - 1] && used[i] == 1 && used[i - 1] == 0
i > 0: 确保当前遍历的数字不是数组的第一个元素,因为第一个元素无法和前一个元素比较。
candidates[i] == candidates[i - 1]: 检查当前数字是否和前一个数字相同,如果相同则表示存在重复的元素。
used[i] == 1 && used[i - 1] == 0: 确保当前数字已经被使用过,而前一个相同的数字没有被使用过。这个条件是为了避免重复的组合。
综合起来,这个条件的含义是:如果当前数字和前一个数字相同,并且当前数字已经被使用,而前一个数字没有被使用,则应该跳过当前数字,以避免生成重复的组合。

image-20240309205454619

  • Java代码实现
LinkedList<Integer> path = new LinkedList<>(); // 用于存储当前组合的路径
List<List<Integer>> result = new ArrayList<>(); // 用于存储最终结果的列表

/**
 * 寻找给定候选数组中和为目标值的所有不重复组合
 * @param candidates 候选数组
 * @param target 目标值
 * @return 所有符合条件的组合列表
 */
public List<List<Integer>> combinationSum2(int[] candidates, int target) {
    int[] used = new int[candidates.length]; // 用于标记候选数组中数字是否被使用过
    Arrays.sort(candidates); // 对候选数组进行排序
    backtrack(candidates, used, 0, target, 0); // 调用回溯函数开始搜索
    return result; // 返回最终结果
}

/**
 * 回溯函数,用于搜索符合条件的组合
 * @param candidates 候选数组
 * @param used 标记数组
 * @param startIndex 当前搜索的起始位置
 * @param target 目标值
 * @param sum 当前累计和
 */
private void backtrack(int[] candidates, int[] used, int startIndex, int target, int sum) {
    if (sum > target) return; // 如果当前累计和已经超过目标值,则返回
    if (sum == target) { // 如果当前累计和等于目标值
        result.add(new ArrayList<>(path)); // 将当前路径加入最终结果
        return;
    }

    for (int i = startIndex; i < candidates.length; i++) {
        used[i] = 1; // 标记当前数字为已使用
        if (i > 0 && candidates[i] == candidates[i - 1] && used[i] == 1 && used[i - 1] == 0) {
            used[i] = 0; // 如果出现重复数字且没有按顺序使用,则跳过当前数字
            continue;
        }
        path.add(candidates[i]); // 将当前数字加入路径
        sum += candidates[i]; // 更新累计和
        backtrack(candidates, used, i + 1, target, sum); // 递归调用下一层搜索
        used[i] = 0; // 恢复当前数字为未使用
        path.removeLast(); // 移除当前数字
        sum -= candidates[i]; // 恢复累计和
    }
}

  • 不用used数组标记的做法
1.在 combinationSum2 方法中,去除了不必要的参数 used,因为在优化后的方法中并未使用到该参数。同时,对 startIndex 和 sum 也进行了调整,这两个参数被通过 path 和 result 的状态来推导,避免了传递过多参数。

2.在 backtrack 方法中,剪枝操作的位置进行了调整。将重复元素的剪枝操作放在了循环体内的开头,这样可以在进入递归之前就剔除掉重复的情况,减少了不必要的递归次数,提高了效率。

3.循环的起始位置进行了调整,并增加了对重复元素的判断。在每次循环时,如果当前元素与前一个元素相同,则直接跳过,避免了重复计算,进一步提高了效率。

4.将 sum 的计算放在了循环体内,使得每次递归只需要计算当前元素的值,而不需要重复计算之前元素的值。

核心修改

if (i > startIndex && candidates[i] == candidates[i - 1]) {
            continue; // 如果当前数字和前一个数字相同,则跳过当前数字,避免重复组合
        }

解释:在回溯过程中,候选数组已经经过排序,如果当前数字和前一个数字相同,那么意味着在上一层的递归中已经考虑过这个数字,为了避免生成相同的组合,我们需要跳过当前数字。
i > startIndex:这个条件确保我们只考虑从当前位置开始的数字,而不是之前已经被考虑过的数字。
candidates[i] == candidates[i - 1]:这个条件判断当前数字是否和前一个数字相同。
continue:如果当前数字和前一个数字相同,我们使用continue语句跳过当前数字的处理,直接进入下一次循环。
  • Java代码实现
LinkedList<Integer> path = new LinkedList<>(); // 用于存储当前组合的路径
List<List<Integer>> result = new ArrayList<>(); // 用于存储最终结果的列表

/**
 * 寻找给定候选数组中和为目标值的所有不重复组合
 * @param candidates 候选数组
 * @param target 目标值
 * @return 所有符合条件的组合列表
 */
public List<List<Integer>> combinationSum2(int[] candidates, int target) {
    Arrays.sort(candidates); // 对候选数组进行排序
    backtrack(candidates, 0, target); // 调用回溯函数开始搜索
    return result; // 返回最终结果
}

/**
 * 回溯函数,用于搜索符合条件的组合
 * @param candidates 候选数组
 * @param startIndex 当前搜索的起始位置
 * @param target 目标值
 */
private void backtrack(int[] candidates, int startIndex, int target) {
    if (target == 0) { // 如果目标值为0,表示当前组合符合条件
        result.add(new ArrayList<>(path)); // 将当前路径加入最终结果
        return;
    }

    for (int i = startIndex; i < candidates.length; i++) {
        if (i > startIndex && candidates[i] == candidates[i - 1]) {
            continue; // 如果当前数字和前一个数字相同,则跳过当前数字,避免重复组合
        }

        int num = candidates[i]; // 获取当前数字
        if (num > target) {
            break; // 如果当前数字已经大于目标值,结束循环
        }

        path.add(num); // 将当前数字加入路径
        backtrack(candidates, i + 1, target - num); // 递归调用下一层搜索
        path.removeLast(); // 移除当前数字
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值