
目录
- [C++ 折半搜索(Meet in the Middle):突破指数级复杂度的分治策略](#C++ 折半搜索(Meet in the Middle):突破指数级复杂度的分治策略)
-
- 引言
- 一、折半搜索核心原理
-
- [1. 问题背景:指数级复杂度的瓶颈](#1. 问题背景:指数级复杂度的瓶颈)
- [2. 核心思想:分治 + 合并](#2. 核心思想:分治 + 合并)
- [3. 复杂度对比](#3. 复杂度对比)
- 二、折半搜索适用场景
- 三、基础实现框架(以子集和问题为例)
-
- [1. 问题描述](#1. 问题描述)
- [2. 暴力解法(对比用)](#2. 暴力解法(对比用))
- [3. 折半搜索实现](#3. 折半搜索实现)
- [4. 代码核心解释](#4. 代码核心解释)
- 四、进阶实战案例
- 五、折半搜索优化技巧
-
- [1. 状态去重与优化](#1. 状态去重与优化)
- [2. 数据结构优化](#2. 数据结构优化)
- [3. 内存优化](#3. 内存优化)
- [4. 并行化优化](#4. 并行化优化)
- [六、折半搜索 vs 其他优化算法](#六、折半搜索 vs 其他优化算法)
-
- [1. 折半搜索 vs 动态规划](#1. 折半搜索 vs 动态规划)
- [2. 折半搜索 vs 暴力枚举](#2. 折半搜索 vs 暴力枚举)
- [3. 折半搜索 vs 剪枝搜索](#3. 折半搜索 vs 剪枝搜索)
- 七、常见坑点与避坑指南
-
- [1. 数据溢出](#1. 数据溢出)
- [2. 拆分不均](#2. 拆分不均)
- [3. 重复计算](#3. 重复计算)
- [4. 边界条件遗漏](#4. 边界条件遗漏)
- [5. 二分查找错误](#5. 二分查找错误)
- [八、完整实战:解决 n=40 的子集和问题](#八、完整实战:解决 n=40 的子集和问题)
- 九、总结
C++ 折半搜索(Meet in the Middle):突破指数级复杂度的分治策略
引言
折半搜索(Meet in the Middle,简称 MITM)是一种分治思想驱动的优化搜索算法 ,核心是将原本需要 O ( 2 n ) O(2^n) O(2n) 或 O ( 3 n ) O(3^n) O(3n) 指数级复杂度的问题,拆分为两个规模减半的子问题,分别求解后合并结果,将复杂度降至 O ( 2 n / 2 ) O(2^{n/2}) O(2n/2) 或 O ( 3 n / 2 ) O(3^{n/2}) O(3n/2)。这种策略能将原本无法在合理时间内解决的"中等规模"指数级问题(如 n = 40 n=40 n=40),转化为可解的问题( n / 2 = 20 n/2=20 n/2=20, 2 20 ≈ 1 e 6 2^{20}≈1e6 220≈1e6 可轻松处理)。
本文将从折半搜索的核心原理、适用场景、实现框架、优化技巧到实战案例,全面讲解这一算法,并通过多个典型例题的完整 C++ 实现,帮你掌握其核心思想与工程落地能力。
一、折半搜索核心原理
1. 问题背景:指数级复杂度的瓶颈
许多组合搜索问题的时间复杂度随输入规模 n n n 呈指数增长:
- 子集和问题:枚举所有子集的复杂度为 O ( 2 n ) O(2^n) O(2n);
- 三选一组合问题:复杂度为 O ( 3 n ) O(3^n) O(3n);
- n n n 个数的排列问题:复杂度为 O ( n ! ) O(n!) O(n!)(阶乘级,比指数级更差)。
当 n = 20 n=20 n=20 时, 2 20 ≈ 10 6 2^{20}≈10^6 220≈106,计算机可轻松处理;但当 n = 40 n=40 n=40 时, 2 40 ≈ 10 12 2^{40}≈10^{12} 240≈1012,即使是超级计算机也无法在合理时间内完成。折半搜索的核心就是将指数的"底数不变,指数减半" ,把 2 40 2^{40} 240 拆分为 2 20 + 2 20 2^{20} + 2^{20} 220+220,从"不可解"变为"可解"。
2. 核心思想:分治 + 合并
折半搜索的通用流程可总结为三步:
原问题:规模n
拆分:将问题分为A、B两个子问题,规模各为n/2
求解子问题A:枚举所有可能的解,存储结果到集合S1
求解子问题B:枚举所有可能的解,存储结果到集合S2
合并:根据原问题的要求,在S1和S2中查找满足条件的组合
输出最终结果
关键特性:
- 拆分原则:两个子问题相互独立,且原问题的解可由两个子问题的解组合而成;
- 合并策略:通常对其中一个集合排序,再对另一个集合的每个元素进行二分查找(将合并的时间复杂度从 O ( 2 n ) O(2^n) O(2n) 降至 O ( 2 n / 2 log 2 n / 2 ) = O ( n ⋅ 2 n / 2 ) O(2^{n/2} \log 2^{n/2}) = O(n \cdot 2^{n/2}) O(2n/2log2n/2)=O(n⋅2n/2));
- 空间换时间:需要存储两个子问题的所有解,空间复杂度为 O ( 2 n / 2 ) O(2^{n/2}) O(2n/2)。
3. 复杂度对比
以子集和问题为例,对比暴力枚举与折半搜索的复杂度:
| 算法 | 时间复杂度 | 空间复杂度 | 可处理的最大n(1秒内) |
|---|---|---|---|
| 暴力枚举 | O ( 2 n ) O(2^n) O(2n) | O ( 1 ) O(1) O(1)(不存储) | ≈20 |
| 折半搜索 | O ( n ⋅ 2 n / 2 ) O(n \cdot 2^{n/2}) O(n⋅2n/2) | O ( 2 n / 2 ) O(2^{n/2}) O(2n/2) | ≈40 |
二、折半搜索适用场景
折半搜索并非通用算法,仅适用于满足以下条件的问题:
- 问题可拆分:原问题能被拆分为两个独立的子问题,且原问题的解是两个子问题解的组合;
- 枚举子问题可行 :每个子问题的规模为 n / 2 n/2 n/2,枚举所有解的时间在可接受范围内(如 2 20 ≈ 1 e 6 2^{20}≈1e6 220≈1e6, 2 25 ≈ 3 e 7 2^{25}≈3e7 225≈3e7);
- 合并可高效完成:合并时可通过排序 + 二分查找实现高效匹配。
典型适用问题:
- 子集和问题(最经典);
- 目标和问题(给每个数加减号,使和为目标值);
- 第k大的组合和问题;
- 多维背包问题(小维度,中等规模物品数);
- 数组分割问题(将数组分为两部分,满足某种条件)。
三、基础实现框架(以子集和问题为例)
1. 问题描述
子集和问题 :给定一个长度为 n n n 的数组 n u m s nums nums 和目标值 t a r g e t target target,判断是否存在一个子集,其元素和等于 t a r g e t target target。
2. 暴力解法(对比用)
暴力枚举所有子集,时间复杂度 O ( 2 n ) O(2^n) O(2n),仅适用于 n ≤ 20 n≤20 n≤20:
cpp
#include <iostream>
#include <vector>
using namespace std;
bool brute_force_subset_sum(vector<int>& nums, int target) {
int n = nums.size();
// 枚举所有子集(0 ~ 2^n - 1)
for (int mask = 0; mask < (1 << n); ++mask) {
int sum = 0;
for (int i = 0; i < n; ++i) {
if (mask & (1 << i)) {
sum += nums[i];
}
}
if (sum == target) {
return true;
}
}
return false;
}
int main() {
vector<int> nums = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
int target = 30;
cout << (brute_force_subset_sum(nums, target) ? "存在" : "不存在") << endl;
return 0;
}
3. 折半搜索实现
将数组拆分为前半部分和后半部分,分别枚举子集和,再通过二分查找匹配:
cpp
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
// 枚举指定区间内的所有子集和,存入res
void enumerate_subset_sum(vector<int>& nums, int l, int r, vector<long long>& res) {
int len = r - l + 1;
// 枚举所有子集(0 ~ 2^len - 1)
for (int mask = 0; mask < (1 << len); ++mask) {
long long sum = 0;
for (int i = 0; i < len; ++i) {
if (mask & (1 << i)) {
sum += nums[l + i];
}
}
res.push_back(sum);
}
}
// 折半搜索解决子集和问题
bool meet_in_middle_subset_sum(vector<int>& nums, long long target) {
int n = nums.size();
if (n == 0) return target == 0;
// 步骤1:拆分数组为两部分
int mid = n / 2;
vector<long long> left_sums, right_sums;
// 步骤2:枚举左半部分所有子集和
enumerate_subset_sum(nums, 0, mid - 1, left_sums);
// 步骤3:枚举右半部分所有子集和
enumerate_subset_sum(nums, mid, n - 1, right_sums);
// 步骤4:排序右半部分,用于二分查找
sort(right_sums.begin(), right_sums.end());
// 步骤5:遍历左半部分的每个和,查找是否存在右半部分的和 = target - left_sum
for (long long ls : left_sums) {
long long need = target - ls;
// 二分查找need是否存在于right_sums中
if (binary_search(right_sums.begin(), right_sums.end(), need)) {
return true;
}
}
return false;
}
int main() {
// 测试用例:n=40,暴力无法处理,折半可轻松处理
vector<int> nums;
for (int i = 1; i <= 40; ++i) {
nums.push_back(i);
}
long long target = 820; // 1+2+...+40 = 820,必存在
// 计时(可选)
// clock_t start = clock();
bool exists = meet_in_middle_subset_sum(nums, target);
// clock_t end = clock();
// double time = (double)(end - start) / CLOCKS_PER_SEC;
cout << (exists ? "存在" : "不存在") << endl;
// cout << "耗时:" << time << "秒" << endl; // 通常<0.1秒
return 0;
}
4. 代码核心解释
enumerate_subset_sum函数:枚举指定区间[l, r]内的所有子集和,时间复杂度 O ( 2 k ) O(2^k) O(2k)( k = r − l + 1 k=r-l+1 k=r−l+1);- 拆分策略:将数组分为前
n/2和后n-n/2两部分,保证两个子问题规模尽可能接近; - 合并策略:对右半部分的子集和排序( O ( 2 n / 2 log 2 n / 2 ) O(2^{n/2} \log 2^{n/2}) O(2n/2log2n/2)),再对左半部分每个和执行二分查找( O ( log 2 n / 2 ) O(\log 2^{n/2}) O(log2n/2)),总合并复杂度 O ( 2 n / 2 log 2 n / 2 ) O(2^{n/2} \log 2^{n/2}) O(2n/2log2n/2);
- 数据类型:使用
long long避免子集和溢出(尤其是 n = 40 n=40 n=40 时,和可达 820 820 820,虽未溢出,但养成习惯)。
四、进阶实战案例
案例1:目标和问题(带符号的子集和)
问题描述
给定一个数组 n u m s nums nums 和目标值 t a r g e t target target,给每个数添加 + 或 -,使得最终的和等于 t a r g e t target target,求有多少种不同的符号组合方式。
问题转化
设添加 + 的数的和为 s u m p o s sum_{pos} sumpos,添加 - 的数的和为 s u m n e g sum_{neg} sumneg,则:
s u m p o s − s u m n e g = t a r g e t s u m p o s + s u m n e g = t o t a l _ s u m sum_{pos} - sum_{neg} = target \\ sum_{pos} + sum_{neg} = total\_sum sumpos−sumneg=targetsumpos+sumneg=total_sum
联立得: s u m p o s = ( t a r g e t + t o t a l _ s u m ) / 2 sum_{pos} = (target + total\_sum) / 2 sumpos=(target+total_sum)/2。
因此问题转化为:求子集和为 ( t a r g e t + t o t a l _ s u m ) / 2 (target + total\_sum)/2 (target+total_sum)/2 的子集数量(需满足 t a r g e t + t o t a l _ s u m target + total\_sum target+total_sum 为偶数且非负)。
折半搜索实现
cpp
#include <iostream>
#include <vector>
#include <algorithm>
#include <unordered_map>
using namespace std;
// 枚举子集和,并统计每个和出现的次数
void enumerate_sum_count(vector<int>& nums, int l, int r, unordered_map<long long, int>& sum_count) {
int len = r - l + 1;
for (int mask = 0; mask < (1 << len); ++mask) {
long long sum = 0;
for (int i = 0; i < len; ++i) {
if (mask & (1 << i)) {
sum += nums[l + i];
}
}
sum_count[sum]++;
}
}
// 目标和问题:返回符号组合数
int find_target_sum_ways(vector<int>& nums, int target) {
long long total_sum = 0;
for (int num : nums) {
total_sum += num;
}
// 边界条件:无法满足的情况
if ((target + total_sum) % 2 != 0) return 0;
long long required = (target + total_sum) / 2;
if (required < 0) return 0;
int n = nums.size();
int mid = n / 2;
// 步骤1:枚举左半部分的子集和及次数
unordered_map<long long, int> left_count;
enumerate_sum_count(nums, 0, mid - 1, left_count);
// 步骤2:枚举右半部分的子集和及次数
unordered_map<long long, int> right_count;
enumerate_sum_count(nums, mid, n - 1, right_count);
// 步骤3:合并结果:left_sum + right_sum = required
int result = 0;
for (auto& [left_sum, cnt] : left_count) {
long long need = required - left_sum;
if (right_count.count(need)) {
result += cnt * right_count[need];
}
}
return result;
}
int main() {
vector<int> nums = {1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1}; // n=20
int target = 10;
int ways = find_target_sum_ways(nums, target);
cout << "组合数:" << ways << endl; // 输出:184756(C(20,15)=C(20,5)=15504?实际需计算验证)
return 0;
}
案例2:第k大的组合和
问题描述
给定一个数组 n u m s nums nums,求所有非空子集和中第 k k k 大的数(子集和可能重复,需去重后排序)。
折半搜索实现
cpp
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
void enumerate_subset_sum(vector<int>& nums, int l, int r, vector<long long>& res) {
int len = r - l + 1;
for (int mask = 0; mask < (1 << len); ++mask) {
long long sum = 0;
for (int i = 0; i < len; ++i) {
if (mask & (1 << i)) {
sum += nums[l + i];
}
}
if (sum != 0) { // 排除空集
res.push_back(sum);
}
}
}
// 求第k大的子集和(去重)
long long kth_largest_subset_sum(vector<int>& nums, int k) {
int n = nums.size();
int mid = n / 2;
vector<long long> left_sums, right_sums;
enumerate_subset_sum(nums, 0, mid - 1, left_sums);
enumerate_subset_sum(nums, mid, n - 1, right_sums);
// 去重并排序
sort(left_sums.begin(), left_sums.end());
left_sums.erase(unique(left_sums.begin(), left_sums.end()), left_sums.end());
sort(right_sums.begin(), right_sums.end());
right_sums.erase(unique(right_sums.begin(), right_sums.end()), right_sums.end());
// 合并所有可能的和(left_sum + right_sum)
vector<long long> all_sums;
// 先加入左半部分单独的和
all_sums.insert(all_sums.end(), left_sums.begin(), left_sums.end());
// 加入右半部分单独的和
all_sums.insert(all_sums.end(), right_sums.begin(), right_sums.end());
// 加入左右组合的和
for (long long ls : left_sums) {
for (long long rs : right_sums) {
all_sums.push_back(ls + rs);
}
}
// 去重并降序排序
sort(all_sums.begin(), all_sums.end(), greater<long long>());
all_sums.erase(unique(all_sums.begin(), all_sums.end()), all_sums.end());
// 边界检查
if (k > all_sums.size()) {
return -1; // 不存在第k大的和
}
return all_sums[k - 1];
}
int main() {
vector<int> nums = {3, 2, 1, 5, 4};
int k = 3;
long long result = kth_largest_subset_sum(nums, k);
cout << "第" << k << "大的子集和:" << result << endl; // 预期:14(5+4+3+2=14,5+4+3+1=13,5+4+2+1=12,第3大是14?需验证)
return 0;
}
案例3:多维背包问题(2维)
问题描述
给定 n n n 个物品,每个物品有重量 w i w_i wi、体积 v i v_i vi、价值 v a l i val_i vali,背包的最大重量为 W W W,最大体积为 V V V,求能装入背包的最大价值。
折半搜索实现
cpp
#include <iostream>
#include <vector>
#include <algorithm>
#include <climits>
using namespace std;
// 物品结构体
struct Item {
int w, v, val;
Item(int w_=0, int v_=0, int val_=0) : w(w_), v(v_), val(val_) {}
};
// 状态结构体:重量、体积、价值
struct State {
int w, v, val;
State(int w_=0, int v_=0, int val_=0) : w(w_), v(v_), val(val_) {}
// 用于排序:按重量升序,重量相同按体积升序
bool operator<(const State& other) const {
if (w != other.w) return w < other.w;
return v < other.v;
}
};
// 枚举子集的状态(重量、体积、价值)
void enumerate_states(vector<Item>& items, int l, int r, vector<State>& states) {
int len = r - l + 1;
for (int mask = 0; mask < (1 << len); ++mask) {
int w = 0, v = 0, val = 0;
for (int i = 0; i < len; ++i) {
if (mask & (1 << i)) {
w += items[l + i].w;
v += items[l + i].v;
val += items[l + i].val;
}
}
states.push_back(State(w, v, val));
}
}
// 优化状态:对于相同重量,保留体积最小且价值最大的;或按重量排序后,保留价值递增的
void optimize_states(vector<State>& states) {
// 按重量升序,体积升序排序
sort(states.begin(), states.end());
vector<State> optimized;
int max_val = 0;
for (auto& s : states) {
// 只保留体积更小、价值更大的状态
if (s.val > max_val) {
max_val = s.val;
optimized.push_back(s);
}
}
states = optimized;
}
// 二维背包问题:折半搜索求解最大价值
int two_dimensional_knapsack(vector<Item>& items, int W, int V) {
int n = items.size();
if (n == 0) return 0;
int mid = n / 2;
vector<State> left_states, right_states;
// 枚举左右两部分的状态
enumerate_states(items, 0, mid - 1, left_states);
enumerate_states(items, mid, n - 1, right_states);
// 优化右半部分的状态(减少后续查找的时间)
optimize_states(right_states);
int max_val = 0;
// 遍历左半部分的每个状态,查找右半部分满足 w<=W-lw, v<=V-lv 的最大价值
for (auto& ls : left_states) {
if (ls.w > W || ls.v > V) continue;
int remain_w = W - ls.w;
int remain_v = V - ls.v;
// 在右半部分中查找重量<=remain_w的状态,再找体积<=remain_v的最大价值
// 二分查找最大的重量<=remain_w的位置
State target(remain_w, INT_MAX, 0);
int idx = upper_bound(right_states.begin(), right_states.end(), target) - right_states.begin() - 1;
if (idx < 0) continue;
// 在0~idx范围内,找体积<=remain_v的最大价值
for (int i = 0; i <= idx; ++i) {
if (right_states[i].v <= remain_v) {
max_val = max(max_val, ls.val + right_states[i].val);
}
}
}
return max_val;
}
int main() {
// 测试用例:10个物品(n=10,折半为5+5)
vector<Item> items = {
{2, 3, 5}, {3, 4, 7}, {1, 2, 3}, {4, 5, 9}, {2, 2, 4},
{3, 1, 6}, {1, 3, 2}, {2, 4, 8}, {4, 2, 10}, {1, 1, 1}
};
int W = 10; // 最大重量
int V = 10; // 最大体积
int max_val = two_dimensional_knapsack(items, W, V);
cout << "最大价值:" << max_val << endl; // 预期:需计算验证
return 0;
}
五、折半搜索优化技巧
1. 状态去重与优化
- 子集和去重 :枚举子集和时,使用
unordered_set替代vector,避免存储重复的和; - 多维状态优化:如背包问题中,对状态按重量排序后,只保留"重量更小、价值更大"的状态,减少后续查找的数量;
- 剪枝:枚举子集时,若当前和已超过目标值,可提前终止(如子集和问题中,若 sum > target,无需继续枚举)。
2. 数据结构优化
- 哈希表替代二分查找 :若需要统计次数(如目标和问题),使用
unordered_map存储子问题的解及其次数,合并时直接查表; - 二分查找优化 :对有序数组使用
lower_bound/upper_bound代替binary_search,支持更灵活的匹配(如查找小于等于目标值的最大和); - 位运算优化枚举 :使用快速枚举子集的位运算技巧(如
__builtin_popcount统计子集大小,mask = (mask - 1) & full_mask枚举非空子集)。
3. 内存优化
- 分批处理 :若子问题的解数量过大(如 2 25 ≈ 3 e 7 2^{25}≈3e7 225≈3e7),可将解分批存储到磁盘,避免内存溢出;
- 压缩存储 :对于整数子集和,可使用
bitset存储(如bitset<1000000>表示哪些和存在),大幅节省内存。
4. 并行化优化
- 两个子问题的枚举可并行执行(如使用 C++11 的
std::thread),充分利用多核 CPU; - 合并阶段的二分查找也可并行处理(如将左半部分的解分成多份,多线程查找)。
六、折半搜索 vs 其他优化算法
1. 折半搜索 vs 动态规划
| 特性 | 折半搜索 | 动态规划 |
|---|---|---|
| 适用场景 | 中等规模(n≈40)的指数级问题 | 小规模(n≈1e3~1e4)的多项式问题 |
| 时间复杂度 | O ( n ⋅ 2 n / 2 ) O(n \cdot 2^{n/2}) O(n⋅2n/2) | O ( n ⋅ W ) O(n \cdot W) O(n⋅W)(背包问题) |
| 空间复杂度 | O ( 2 n / 2 ) O(2^{n/2}) O(2n/2) | O ( W ) O(W) O(W)(背包问题) |
| 实现难度 | 低(枚举+二分) | 中(状态转移方程设计) |
| 灵活性 | 高(适用于各类组合问题) | 低(需匹配特定问题模型) |
2. 折半搜索 vs 暴力枚举
| 特性 | 折半搜索 | 暴力枚举 |
|---|---|---|
| 时间复杂度 | O ( n ⋅ 2 n / 2 ) O(n \cdot 2^{n/2}) O(n⋅2n/2) | O ( 2 n ) O(2^n) O(2n) |
| 空间复杂度 | O ( 2 n / 2 ) O(2^{n/2}) O(2n/2) | O ( 1 ) O(1) O(1) |
| 可处理规模 | n≈40 | n≈20 |
| 实现难度 | 稍高(需拆分+合并) | 极低(直接枚举) |
3. 折半搜索 vs 剪枝搜索
| 特性 | 折半搜索 | 剪枝搜索(如DFS+剪枝) |
|---|---|---|
| 时间复杂度 | 稳定(最坏情况) | 不稳定(依赖剪枝效果) |
| 实现难度 | 低(固定流程) | 高(需设计剪枝策略) |
| 最优性 | 保证找到最优解 | 可能因剪枝遗漏最优解 |
七、常见坑点与避坑指南
1. 数据溢出
- 坑 :子集和使用
int存储,当 n = 40 n=40 n=40 时,和可能超过 2 31 − 1 2^{31}-1 231−1; - 避坑 :使用
long long存储子集和,或提前判断数值范围。
2. 拆分不均
- 坑 :将数组拆分为 1 和 n-1 两部分,复杂度变为 O ( 2 n − 1 + 2 1 ) ≈ O ( 2 n ) O(2^{n-1} + 2^1) ≈ O(2^n) O(2n−1+21)≈O(2n),失去优化意义;
- 避坑:严格将数组拆分为两部分,规模尽可能接近(如 n=40 拆为 20+20,n=41 拆为 20+21)。
3. 重复计算
- 坑:枚举子集和时,重复存储相同的和,导致合并阶段效率降低;
- 避坑 :使用
unordered_set去重,或排序后unique去重。
4. 边界条件遗漏
- 坑:忽略空集的情况(如子集和问题中,空集和为0);
- 避坑:枚举时明确是否包含空集,合并时处理边界值。
5. 二分查找错误
- 坑 :使用
binary_search查找不存在的元素,或使用lower_bound/upper_bound时索引计算错误; - 避坑:熟悉 C++ 二分查找函数的返回值规则,必要时手动实现二分查找。
八、完整实战:解决 n=40 的子集和问题
以下是一个完整的、优化后的折半搜索实现,可处理 n=40 的子集和问题,并包含计时、去重、内存优化等特性:
cpp
#include <iostream>
#include <vector>
#include <algorithm>
#include <unordered_set>
#include <chrono>
#include <climits>
using namespace std;
using namespace chrono;
// 快速枚举子集和(位运算优化)
void fast_enumerate_subset_sum(vector<int>& nums, int l, int r, unordered_set<long long>& sum_set) {
int len = r - l + 1;
// 预计算子数组
vector<int> sub_nums;
for (int i = l; i <= r; ++i) {
sub_nums.push_back(nums[i]);
}
// 枚举所有子集(位运算优化)
for (int mask = 0; mask < (1 << len); ++mask) {
long long sum = 0;
// 快速遍历mask中的1
int tmp = mask;
int idx = 0;
while (tmp) {
if (tmp & 1) {
sum += sub_nums[idx];
}
tmp >>= 1;
idx++;
}
sum_set.insert(sum);
}
}
// 折半搜索:判断是否存在子集和为target
bool meet_in_middle_optimized(vector<int>& nums, long long target) {
int n = nums.size();
if (n == 0) return target == 0;
int mid = n / 2;
unordered_set<long long> left_set, right_set;
// 并行枚举(可选,需C++11及以上)
// thread t1(fast_enumerate_subset_sum, ref(nums), 0, mid-1, ref(left_set));
// thread t2(fast_enumerate_subset_sum, ref(nums), mid, n-1, ref(right_set));
// t1.join();
// t2.join();
// 串行枚举
fast_enumerate_subset_sum(nums, 0, mid - 1, left_set);
fast_enumerate_subset_sum(nums, mid, n - 1, right_set);
// 转换为vector并排序
vector<long long> right_vec(right_set.begin(), right_set.end());
sort(right_vec.begin(), right_vec.end());
// 查找匹配
for (long long ls : left_set) {
long long need = target - ls;
if (binary_search(right_vec.begin(), right_vec.end(), need)) {
return true;
}
}
return false;
}
int main() {
// 生成测试数据:n=40,数值1~40
vector<int> nums;
for (int i = 1; i <= 40; ++i) {
nums.push_back(i);
}
long long target = 820; // 1+2+...+40=820,必存在
// 计时
auto start = high_resolution_clock::now();
bool exists = meet_in_middle_optimized(nums, target);
auto end = high_resolution_clock::now();
auto duration = duration_cast<milliseconds>(end - start);
// 输出结果
cout << "数组长度:" << nums.size() << endl;
cout << "目标和:" << target << endl;
cout << "是否存在:" << (exists ? "是" : "否") << endl;
cout << "耗时:" << duration.count() << " 毫秒" << endl;
// 测试不存在的情况
target = 821;
exists = meet_in_middle_optimized(nums, target);
cout << "\n目标和:" << target << endl;
cout << "是否存在:" << (exists ? "是" : "否") << endl;
return 0;
}
九、总结
核心要点回顾
- 折半搜索核心 :将指数级问题拆分为两个规模减半的子问题,枚举子问题解后通过排序+二分合并,将复杂度从 O ( 2 n ) O(2^n) O(2n) 降至 O ( n ⋅ 2 n / 2 ) O(n \cdot 2^{n/2}) O(n⋅2n/2);
- 核心流程:拆分 → 枚举子问题解 → 排序 → 二分合并;
- 关键优化 :
- 状态去重:减少重复解的存储和查找;
- 数据结构:使用哈希表/二分查找提升合并效率;
- 内存优化:使用
unordered_set/bitset减少内存占用;
- 适用场景:中等规模(n≈40)的组合搜索问题(子集和、目标和、多维背包等)。
学习建议
- 先掌握基础子集和问题的折半搜索实现,理解拆分与合并的核心;
- 尝试将折半搜索应用到目标和、背包问题等场景,掌握问题转化技巧;
- 学习状态优化、并行化等高级技巧,提升算法效率;
- 对比折半搜索与动态规划、暴力枚举的适用场景,选择合适的算法。
折半搜索是"用空间换时间"的经典典范,其核心思想不仅适用于组合搜索问题,也可推广到其他指数级问题的优化中。掌握这一算法,能让你在面对中等规模的指数级问题时,突破暴力枚举的瓶颈,找到高效的解决方案。