GitHub_Trending/leetcode1/leetcode组合总和II:回溯法的排序与去重

GitHub_Trending/leetcode1/leetcode组合总和II:回溯法的排序与去重

【免费下载链接】leetcode Leetcode solutions 【免费下载链接】leetcode 项目地址: https://gitcode.com/GitHub_Trending/leetcode1/leetcode

问题引入与核心挑战

在LeetCode算法题库中,组合总和II(Combination Sum II)是一道经典的回溯算法应用题。该问题要求从包含重复元素的候选数组中找出所有不重复的组合,使得组合中元素的和等于目标值,且每个元素只能使用一次。其核心难点在于如何在回溯过程中有效去重,避免生成重复的组合结果。

问题具体描述

  • 输入:候选数组 candidates(可能包含重复元素)和目标值 target
  • 输出:所有不重复的组合列表,其中每个组合元素之和等于 target
  • 约束:每个元素在组合中只能使用一次

重复组合产生的根源

当候选数组存在重复元素时,即使严格遵循"每个元素仅使用一次"的规则,仍可能通过不同的选择路径生成相同的组合。例如对于 candidates = [1,1,2], target = 3

  • 选择第一个1和2 → [1,2]
  • 选择第二个1和2 → [1,2]

这两种不同的选择路径产生了完全相同的组合结果,需要通过特定的去重机制予以排除。

解决方案:排序+回溯去重

算法整体框架

解决组合总和II问题的高效方法是采用排序预处理+回溯搜索+同层去重的策略,具体步骤如下:

mermaid

关键技术点解析

  1. 排序预处理

    • 对候选数组进行升序排序,使重复元素相邻,为后续去重提供条件
    • 排序后可利用元素大小关系进行剪枝优化
  2. 同层去重机制

    • 核心判断条件:i > start && candidates[i] == candidates[i-1]
    • i > start 时,说明当前元素与同层前一个元素相同,需要跳过以避免重复组合
  3. 剪枝优化

    • 当当前元素大于剩余目标值时,可直接终止循环(因数组已排序,后续元素更大)

核心代码实现(C语言)

void backtrack(int *candidates, int candidatesSize, int target, int start, int *current, int currentSize, int **result, int *resultSize, int *returnColumnSizes) {
    if (target == 0) {
        // 找到符合条件的组合,保存结果
        result[*resultSize] = (int *)malloc(currentSize * sizeof(int));
        returnColumnSizes[*resultSize] = currentSize;
        for (int i = 0; i < currentSize; i++) {
            result[*resultSize][i] = current[i];
        }
        (*resultSize)++;
        return;
    }
    
    for (int i = start; i < candidatesSize; i++) {
        // 去重核心判断:跳过同层重复元素
        if (i > start && candidates[i] == candidates[i - 1]) {
            continue;
        }
        
        // 剪枝优化:当前元素大于剩余目标值,直接终止循环
        if (candidates[i] > target) {
            break;
        }
        
        current[currentSize] = candidates[i];
        // 递归调用:注意参数i+1确保每个元素只使用一次
        backtrack(candidates, candidatesSize, target - candidates[i], i + 1, current, currentSize + 1, result, resultSize, returnColumnSizes);
    }
}

int compare(const void *a, const void *b) {
    return (*(int *)a - *(int *)b);
}

int** combinationSum2(int* candidates, int candidatesSize, int target, int* returnSize, int** returnColumnSizes) {
    // 排序预处理
    qsort(candidates, candidatesSize, sizeof(int), compare);
    
    int **result = (int **)malloc(1000 * sizeof(int *));
    int *current = (int *)malloc(candidatesSize * sizeof(int));
    *returnColumnSizes = (int *)malloc(1000 * sizeof(int));
    
    int resultSize = 0;
    backtrack(candidates, candidatesSize, target, 0, current, 0, result, &resultSize, *returnColumnSizes);
    
    free(current);
    
    *returnSize = resultSize;
    return result;
}

多语言实现对比

Ruby实现

def combination_sum2(candidates, target)
  candidates.sort!
  result = []
  backtrack(candidates, target, 0, [], result)
  result
end

def backtrack(candidates, target, start, path, result)
  if target == 0
    result << path.dup
    return
  end
  
  (start...candidates.size).each do |i|
    # 去重逻辑
    next if i > start && candidates[i] == candidates[i-1]
    break if candidates[i] > target
    
    path << candidates[i]
    backtrack(candidates, target - candidates[i], i + 1, path, result)
    path.pop
  end
end

Go实现

import "sort"

func combinationSum2(candidates []int, target int) [][]int {
    sort.Ints(candidates)
    var result [][]int
    var path []int
    backtrack(candidates, target, 0, path, &result)
    return result
}

func backtrack(candidates []int, target, start int, path []int, result *[][]int) {
    if target == 0 {
        temp := make([]int, len(path))
        copy(temp, path)
        *result = append(*result, temp)
        return
    }
    
    for i := start; i < len(candidates); i++ {
        if i > start && candidates[i] == candidates[i-1] {
            continue
        }
        if candidates[i] > target {
            break
        }
        
        path = append(path, candidates[i])
        backtrack(candidates, target - candidates[i], i + 1, path, result)
        path = path[:len(path)-1]
    }
}

去重逻辑深度解析

为什么i > start是必要条件?

i > start 这个条件确保了只跳过同层的重复元素,而允许不同层使用相同元素。通过以下遍历过程可以清晰理解:

mermaid

如果缺少 i > start 条件,将会错误地过滤掉不同层的必要元素,导致遗漏有效组合。

去重前后对比

未去重时可能产生的重复组合:

[1,2], [1,2], [1,1,1], [1,1,1]

经过去重处理后的结果:

[1,2], [1,1,1]

性能优化分析

不同策略性能对比

优化策略时间复杂度空间复杂度适用场景
未排序+无剪枝O(2ⁿ)O(n)小规模数据
排序+剪枝O(n×2ⁿ)O(n)所有情况
排序+剪枝+去重O(n×2ⁿ)O(n)含重复元素

剪枝效果可视化

对于 candidates = [1,2,3,4,5,6,7], target = 8

  • 未剪枝:需要遍历所有可能的组合
  • 剪枝后:当遇到元素大于剩余目标值时立即停止,减少大量无效递归

mermaid

常见错误案例分析

错误1:去重判断缺少i > start

// 错误代码
if (candidates[i] == candidates[i - 1]) {  // 缺少i > start
    continue;
}

后果:会过滤掉所有重复元素,包括不同层的必要元素,导致遗漏有效组合。

错误2:递归时传递i而非i+1

// 错误代码
backtrack(..., i, ...);  // 应该是i+1

后果:元素可被重复使用,违反"每个元素只能使用一次"的题目限制。

错误3:在递归前未排序

// 错误代码
// qsort(candidates, candidatesSize, sizeof(int), compare);  // 遗漏排序

后果:重复元素可能不相邻,导致去重逻辑失效,生成重复组合。

总结与拓展应用

组合总和II问题的核心在于排序预处理同层去重,掌握这一方法后,可以解决一系列类似的回溯去重问题:

  1. 子集II(LeetCode 90):找出所有不重复的子集
  2. 全排列II(LeetCode 47):找出所有不重复的全排列
  3. 电话号码的字母组合(LeetCode 17):处理重复数字映射

回溯算法的本质是深度优先搜索(DFS)与剪枝的结合,通过合理的去重和剪枝策略,可以大幅提高算法效率。在实际应用中,需特别注意去重逻辑的正确性,避免多去或漏去。

掌握本文介绍的排序+同层去重方法,你将能够轻松应对各类回溯去重问题,显著提升算法解题能力。收藏本文,下次遇到类似问题可直接对照参考!

【免费下载链接】leetcode Leetcode solutions 【免费下载链接】leetcode 项目地址: https://gitcode.com/GitHub_Trending/leetcode1/leetcode

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

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

抵扣说明:

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

余额充值