第一次听说折半搜索(Meet in the Middle)时,我正在刷洛谷的"世界冰球锦标赛"题目。题目看似简单:给定n场比赛的门票价格,问用m元钱有多少种观赛方案。但当n=40时,传统的暴力搜索需要计算2^40种可能性——这个数字比宇宙中的原子数量还多!这时候折半搜索就像一把瑞士军刀,优雅地解决了这个看似无解的问题。
折半搜索的核心思想很像我们处理大型项目的思路:把难题拆成两半分别解决。想象你要在图书馆找一本特定的书,与其从A到Z逐个书架搜索,不如先确定书的前半部分字母所在区域,再在后半部分精确定位。算法层面也是如此,将O(2^n)的复杂度降为O(2^(n/2)),这意味着当n=40时,计算量从1万亿次骤降到100万次——相当于从步行绕地球赤道变成了在小区里散步。
让我们用"世界冰球锦标赛"这个经典案例来拆解算法步骤。假设有4场比赛,门票价格分别为[100,200,300,400],预算m=500元。
传统暴力搜索需要枚举所有16种组合:
而折半搜索这样做:
这个算法的精妙之处在于复杂度控制。设n=40:
具体来说:
最终复杂度稳定在O(n*2^(n/2)),比指数级优化了多个数量级。
在代码实现时,子集生成有几种常见方式:
python复制# 位运算版(适合n较小)
def generate_subsets(arr):
n = len(arr)
subsets = []
for mask in range(1 << n):
total = 0
for i in range(n):
if mask & (1 << i):
total += arr[i]
subsets.append(total)
return subsets
# DFS版(更灵活)
def dfs_subsets(arr, index=0, current_sum=0, result=None):
if result is None:
result = []
if index == len(arr):
result.append(current_sum)
return
dfs_subsets(arr, index+1, current_sum, result) # 不选当前元素
dfs_subsets(arr, index+1, current_sum+arr[index], result) # 选当前元素
return result
实际项目中我发现,当n>25时,位运算版本会因为缓存命中率下降而变慢。这时改用DFS并提前终止无效分支(如当前和已超预算)能获得更好性能。
合并两个子集时的二分查找很容易出错。常见陷阱包括:
正确的C++实现应该这样:
cpp复制sort(w.begin(), w.end());
for(auto t : second_half) {
long long remaining = m - t;
if(remaining < 0) continue;
ans += upper_bound(w.begin(), w.end(), remaining) - w.begin();
}
Python中对应的bisect模块用法:
python复制import bisect
w.sort()
ans = 0
for t in second_half:
remaining = m - t
if remaining < 0: continue
ans += bisect.bisect_right(w, remaining)
折半搜索在以下场景特别有效:
我曾在电商促销系统中用这个算法计算满减组合方案。当有40种商品时,需要快速找出所有总价接近满减门槛的组合,折半搜索比动态规划更节省内存。
折半搜索并非万能钥匙,它的限制包括:
当n>50时,可能需要考虑:
在最近的一个物流优化项目中,当配送点达到60个时,我们最终采用了折半搜索结合分支限界的混合策略,既控制了复杂度,又保证了解的质量。