You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

大数据量下目标和组合查找算法的性能优化咨询

大数据量下目标和组合查找算法的性能优化咨询

嘿,这个问题我太熟了——你现在用的暴力枚举所有组合的方法,在数据量小的时候没问题,但一旦元素多到几十上百,组合数直接指数爆炸,肯定慢到离谱。咱们得换更聪明的思路,下面给你几个实用的优化方案,都是实际项目里验证过的:

一、先搞懂原代码的核心瓶颈

你原来的代码是枚举从1个元素到n个元素的所有组合,组合数是2^n - 1(每个元素选或不选,减去全不选的情况)。比如n=20就有100多万种组合,n=30是10亿+,n=100的话,这个数字已经大到没法想象了,根本不可能跑完。所以必须放弃暴力枚举,用时间复杂度更低的算法。

二、优化方案1:回溯+剪枝(适合元素多但target不大、想省空间的场景)

先把数组排序,这样在回溯选元素的时候,如果当前元素加上已选的和已经超过target,后面更大的元素直接跳过,不用再递归了,这能砍掉大量无效的分支。如果数组里有重复元素,还能跳过重复项避免生成重复组合。

给你写个示例代码:

def find_all_sum_combinations(target, numbers):
    numbers.sort()
    results = []
    def backtrack(start, current_sum, path):
        if current_sum == target:
            results.append(path.copy())
            return
        for i in range(start, len(numbers)):
            # 剪枝:当前元素+已选和超target,后面更大的元素直接跳过
            if current_sum + numbers[i] > target:
                break
            # 跳过重复元素,避免生成重复组合(数组有重复时启用)
            if i > start and numbers[i] == numbers[i-1]:
                continue
            path.append(numbers[i])
            backtrack(i+1, current_sum + numbers[i], path)
            path.pop()
    backtrack(0, 0, [])
    return results

# 测试
numbers = [2, 7, 16, 19, 33, 41, 52, 58, 60, 67, 68, 73, 85, 92]
target = 100
results = find_all_sum_combinations(target, numbers)
if results:
    for res in results:
        print("+".join(map(str, res)), "=", target)
else:
    print("NO RESULTS FOUND !")

这个方法的效率完全取决于剪枝的力度,排序后能直接砍掉所有超标的分支,比暴力枚举快N个量级,对付几百个元素的数组也没问题。

三、优化方案2:动态规划(适合target不大的场景,效率最高)

这个思路是用字典记录能组成每个和的所有子集,遍历每个数字时更新这个记录。时间复杂度是O(n*target),空间复杂度也是O(n*target),如果target不是特别大(比如几千几万),哪怕n是1000,也能秒出结果。

示例代码如下:

def find_all_sum_combinations(target, numbers):
    # dp: key是和,value是能组成该和的所有子集列表
    dp = {0: [[]]}
    for num in numbers:
        # 用临时字典存新生成的和,避免修改原字典影响遍历
        temp_dp = {}
        for current_sum, subsets in dp.items():
            new_sum = current_sum + num
            if new_sum > target:
                continue
            # 把当前数字加到每个现有子集里,生成新子集
            new_subsets = [subset + [num] for subset in subsets]
            if new_sum in temp_dp:
                temp_dp[new_sum].extend(new_subsets)
            else:
                temp_dp[new_sum] = new_subsets
        # 合并临时字典到主dp
        for s, subsets in temp_dp.items():
            if s in dp:
                dp[s].extend(subsets)
            else:
                dp[s] = subsets
    # 去重(数组有重复元素时启用)
    unique_results = list({tuple(subset) for subset in dp.get(target, [])})
    return [list(res) for res in unique_results]

# 测试
numbers = [2, 7, 16, 19, 33, 41, 52, 58, 60, 67, 68, 73, 85, 92]
target = 100
results = find_all_sum_combinations(target, numbers)
if results:
    for res in results:
        print("+".join(map(str, res)), "=", target)
else:
    print("NO RESULTS FOUND !")

如果数组里有重复元素,最后用集合转元组的方式去重就行,保证每个组合唯一。这个方法对付大n+小target的场景是最优解,比如n=1000、target=1000,也就1e5次操作,瞬间完成。

四、优化方案3:分治算法(Meet-in-the-Middle,适合target特别大的场景)

如果target特别大(比如1e9),动态规划的空间根本不够用,就可以用分治:把数组分成两半,分别计算每一半的所有子集和及其对应的子集,然后对左半部分的每个子集和s,去右半部分找有没有等于target-s的子集和,把对应的子集合并就是结果。

这个方法的时间复杂度是O(n*2^(n/2)),比如n=100的话,分成两个50,每个50的子集数是250(还是很大,但比2100好太多),适合n在40-60之间且target极大的场景。

示例代码:

from itertools import combinations

def get_subset_sums(numbers):
    # 返回所有子集的和与对应子集的元组列表
    subset_sums = []
    for r in range(len(numbers)+1):
        for combo in combinations(numbers, r):
            subset_sums.append( (sum(combo), combo) )
    return subset_sums

def find_all_sum_combinations(target, numbers):
    # 过滤掉比target大的单个元素,减少计算量
    numbers = [num for num in numbers if num <= target]
    mid = len(numbers) // 2
    left_nums = numbers[:mid]
    right_nums = numbers[mid:]
    
    # 计算左右两半的所有子集和
    left_sums = get_subset_sums(left_nums)
    right_sums = get_subset_sums(right_nums)
    
    # 把右半部分的和存到字典,方便快速查找
    right_dict = {}
    for s, combo in right_sums:
        if s in right_dict:
            right_dict[s].append(combo)
        else:
            right_dict[s] = [combo]
    
    results = []
    # 遍历左半部分,匹配右半部分的互补和
    for s, left_combo in left_sums:
        needed = target - s
        if needed in right_dict:
            for right_combo in right_dict[needed]:
                full_combo = left_combo + right_combo
                if full_combo and full_combo not in results:
                    results.append(full_combo)
    return results

# 测试
numbers = [2, 7, 16, 19, 33, 41, 52, 58, 60, 67, 68, 73, 85, 92]
target = 100
results = find_all_sum_combinations(target, numbers)
if results:
    for res in results:
        print("+".join(map(str, res)), "=", target)
else:
    print("NO RESULTS FOUND !")

注意先过滤掉比target大的元素,因为单个元素超target的话,肯定没法出现在组合里,能减少不少计算量。

五、针对你场景的选择建议

  • 如果target不大(比如几万以内):优先用动态规划,n=1000也能轻松处理;
  • 如果target极大且n在40-60之间:用分治算法;
  • 如果target不大但空间紧张:用回溯+剪枝,空间复杂度低,效率也远超暴力;
  • 如果n=1000且target也极大:先过滤掉超target的元素,再用回溯+剪枝,排序后剪枝能砍掉绝大多数无效分支。

备注:内容来源于stack exchange,提问作者Tugrul

火山引擎 最新活动