外观
meet-in-the-middle.md---
title: Meet in the Middle
createTime: 2025/6/21
categories:
- IT
tags:
- OI
- OI-note
---
可以用来处理数据范围是爆搜范围的两倍以下的可拆分问题。
## 例 1:[P4799 [CEOI 2015] 世界冰球锦标赛 (Day2)](https://www.luogu.com.cn/problem/P4799)
乍一看像背包,但是值域 $2^N$ 太大了,背包还不如 $O(2^N)$ 的爆搜。
如果 $N$ 再小一点,就可以爆搜了。
于是引出了主题:把原序列拆成两半 $S, T$,爆搜 $O(2^{N/2})$,由于折半,变得可以接受了。
分别暴力出不同价格对应的方案数集合。然后,我们发现可以用这两个集合通过双指针(或者偷懒用 STL 二分查找)得出答案。
经过折半 + 合并的过程,时间复杂度就完成了 $O(2^N) \longrightarrow O(N \cdot 2^{N/2})$ 的蜕变。
```cpp
#include <bits/stdc++.h>
using namespace std;
#define int long long
const int maxn=50;
int n, m;
int a[maxn];
vector<int> sl, sr;
void dfs(int l, int r, int id, int sum, vector<int> &out){
if (sum > m){
return; // 其实影响不大。
}
if (id > r){
out.push_back(sum);
return;
}
dfs(l, r, id+1, sum, out);
dfs(l, r, id+1, sum + a[id], out);
}
signed main(){
ios::sync_with_stdio(0);
cin.tie(0); cout.tie(0);
cin >> n >> m;
for (int i=1; i<=n; i++){
cin >> a[i];
}
int mid = n/2;
dfs(1, mid, 1, 0, sl);
dfs(mid+1, n, mid+1, 0, sr);
sort(sl.begin(), sl.end());
sort(sr.begin(), sr.end());
int ans = 0;
for (int suml: sl){
int maxr = m - suml;
ans += upper_bound(sr.begin(), sr.end(), maxr) - sr.begin();
}
cout << ans;
return 0;
}
```
这里直接朴素地存下搜索结果即可,如无必要,勿增实体。
如果对 `upper_bound` 一句有疑问,见:
```text
i 0 1 2 3 4 5
sr 1 2 2 2 3 4
[ 4 ] ^upper_bound
```
:::tip 如果你担心找到 end 的情况
直接查找等价于插入一个 $+\infty$ 再查找,而后者的 $+\infty$ 显然不会影响连续性.
也就是说,`end` 和正常情况没什么两样. 如果不放心可以自己画一下图.
:::
## 例 2(特殊去重):[P3067 [USACO12OPEN] Balanced Cow Subsets G](https://www.luogu.com.cn/problem/P3067?contestId=252170)
要求 $(\sum A) = (\sum B)$,处理这种等式的常用手法是作差,$s = (\sum A) - (\sum B) = 0$.
一个元素对 $s$ 的贡献一定属于 $\{0, 1, -1\}$,于是可以分两段 $O(3^{n/2})$ 爆搜这个和。
不过这种方法存在重复。因此,我们需要对一种集合的不同分割方式进行去重。将集合状压后装入 `unordered_map` 即可。
由于去重的关系,最后合并答案只能使用暴力。但是由于操作的是集合,时间复杂度是 $O(2^n)$,刚好足够通过此题。
```cpp
#include <bits/stdc++.h>
using namespace std;
#define int long long
const int maxn=30;
int a[maxn];
typedef unordered_map<int, unordered_set<int>> mis;
mis statel, stater;
unordered_set<int> ans;
void dfs(int l, int r, int id, int sum, int state, mis &out){
if (id > r){
out[sum].insert(state);
return;
}
dfs(l, r, id+1, sum, state, out);
dfs(l, r, id+1, sum+a[id], state|(1<<id), out);
dfs(l, r, id+1, sum-a[id], state|(1<<id), out);
}
signed main(){
ios::sync_with_stdio(0);
cin.tie(0); cout.tie(0);
int n;
cin >> n;
for (int i=1; i<=n; i++){
cin >> a[i];
}
int mid = n/2;
dfs(1, mid, 1, 0, 0, statel);
dfs(mid+1, n, mid+1, 0, 0, stater);
for (auto [suml, vl]: statel){
for (int l: vl){
for (int r: stater[-suml]){
if (l | r){ // 去掉空集
ans.insert(l | r);
}
}
}
}
cout << ans.size();
return 0;
}
```
## 例 3(特殊合并):[P10884 [COCI 2017/2018 #2] San](https://www.luogu.com.cn/problem/P10884)
之前的写法都是用一样的写法找两边,之后再合并. 现在介绍一种正统、适应性更强的做法. 就是左右分开,右边找到之后立刻更新答案.
因为要求子序列非严格单调递增,需要记录下截断处 $\mathrm{end}_i$ 和收益 $s_i$.
假设右边找到了路径 $(S, H)$,若可以和左半边的第 $i$ 条记录合并,则要求
$$
\begin{cases}
s_i \ge K - S \\
h_\mathrm{end_i} \le H \\
\end{cases}
$$
由于 $\mathrm{end}$ 这一维值域极小 $(\le 20)$,我们可以直接穷举.
于是,把所有 $s_i$ 存到以 $\mathrm{end}_i$ 为键的表中,直接二分即可.
> 碎碎念:
>
> 原来想的是离线二维数点,然后发现还有更简单的方法.
> 之前认为这种方法开销大,但是最后发现它不仅开销甚至还小,而且更简单.
> 这就是返璞归真的力量 // ?
此算法的时间复杂度为 $O(2^{N/2} N)$,各项复杂度如下:
| 项目 | 时间复杂度 |
| --- | --- |
| 搜索 | $O(2^{N/2})$ |
| 排序 | $O(2^{N/2} N)$ |
| 合并 | $O(N^3)$ |
```cpp
#include <bits/stdc++.h>
using namespace std;
#define int long long
const int maxn=40;
int n, k, mid;
int h[maxn+10], g[maxn+10];
int ans;
vector<int> lans[maxn];
void dfsl(int id, int pre, int sum){
if (id == mid + 1){
// printf("left s=%d, end=%d\n", sum, pre);
lans[pre].push_back(sum);
return;
}
dfsl(id+1, pre, sum);
if (h[id] >= h[pre]){
dfsl(id+1, id, sum + g[id]);
}
}
void proc(int H, int S);
void dfsr(int id, int pre, int sum) {
// 反着找
if (id == mid){
proc(h[pre], sum);
return;
}
dfsr(id-1, pre, sum);
if (h[id] <= h[pre]){
dfsr(id-1, id, sum + g[id]);
}
}
void proc(int H, int S){
// printf("right s=%d, start.H=%d\n", S, H);
for (int ed=0; ed<=mid; ed++){
if (h[ed] <= H){
int lb = lower_bound(lans[ed].begin(), lans[ed].end(), k-S) - lans[ed].begin();
// e.g. K-S = 5, size=7
// i 0 1 2 3 4 5 6
// s 1 3 5 5 6 7 9
// lb^
// [ 7-2=5 ]
ans += lans[ed].size() - lb;
}
}
}
signed main(){
ios::sync_with_stdio(0);
cin.tie(0); cout.tie(0);
cin >> n >> k;
for (int i=1; i<=n; i++){
cin >> h[i] >> g[i];
}
mid = n/2;
dfsl(1, 0, 0);
for (int i=0; i<=mid; i++){
sort(lans[i].begin(), lans[i].end());
}
h[n+1] = 0x3f3f3f3f3f3f3f3f; // 填充 pre,且可处理 empty_path
dfsr(n, n+1, 0);
cout << ans;
return 0;
}
```