Meet in the middle
投稿:孙言龙
读了上面的歌词,我们可以看出今天的主题:meet in the middle(折半搜索)。
什么是 meet in the middle?
在深度优先搜索中,我们通过暴力枚举每种可行的情况并进行剪枝可以得到答案,这种做法的时间复杂度是 \(O(2^n\times A)\),\(A\) 是任意关于 \(n\) 的多项式,这个时间复杂度非常高,在 \(n\geq 30\) 时,若多项式 \(A\) 的运算量稍高,即使进行了十分恰当的剪枝,也难以通过,那么,如果现在我们的数据范围来到了 \(40\sim 50\),我们需要使用其他的优化方法,那么,“Baby, what if we could meet in the middle?”,meet in the middle(折半搜索)的思想成为了不错的选择。
meet in the middle 的思想就是在 dfs 或者暴力枚举区间 \([l,r]\) 时,先将答案折半,取其中间值 \(mid={l+r\over 2}\),并且对区间 \([l,mid]\) 和 \([mid+1,r]\) 进行分别枚举,最后前一半和后一半将符合条件的情况进行合并即可。
如何 meet in the middle?
看完上述对 meet int the middle 的介绍,我们来详细讲讲如何前后分别枚举,如何合并两区间中的答案以得到最终答案。
先来看一个例题:
给定 \(n\) 个格子每个格子有高度 \(h_i\) 和权值 \(w_i\)。
你每次可以从任意一个格子开始,向右跳到任意一个高度不低于当前格子的位置,可以在任意位置停止。
给定一个限制 \(m\),求出最后经过权值总和 \(\geq k\) 的方案数。
\(k\leq 4\times 10^{10}\),\(w_i,h_i\leq 10^9\),
对于 \(40\\%\) 的数据,\(n\leq 20\),
对于 \(100\\%\) 的数据,\(n\leq 40\)。
显然,\(40\%\) 的分数是很好拿的,只要写一个简单的 dfs 暴力枚举一下就好了,但是如果我们想拿满分,就不那么容易了。
考虑将整个区间分成两部分,\([1,\lfloor{n+1\over 2}\rfloor]\) 和 \([\lfloor{n+1 \over 2}\rfloor+1,n]\),这样的话,时间复杂度就大大降低了,但是,对于两侧分别 \(dfs\) 以后,就难以继续下去了,所以我们如何将两边的答案合并呢?
首先,计算前一半的答案,记录到 vector 当中,但是要注意的是,如果只搜前一半答案就已经大于等于 \(m\) 了,就直接将答案加一,代码如下(详细注释在代码中):
vector<int> v[43]; //存答案的 vector
long long h[43], w[43]; //高度和权值
long long n, m, res = 0; //记录n,m和答案
void dfs(int id, int k, int lim) {
k += w[id]; //增加现在的权值
v[id].push_back(k); //将搜索到的值加入vector中
if (k >= m) res++; //如果已经大于m,那么直接增加答案
for (int i = id + 1; i <= lim; ++i)
if (h[i] >= h[id]) //高度限制
dfs(i, k, lim); //递归搜索
}
接着,计算后一半的答案,如果搜索到当前位置时已经有的权值为 \(w\),那么我们就需要找在前一半中,权值大于等于 \(m-w\),且结束高度小于等于我们后一半的起始高度的数量,这个操作可以直接在 vector 中用 lower_bound 二分快速查询。还是要注意,如果只搜前一半答案就已经大于等于 \(m\) 了,就直接将答案加一,并且为了便于判断起始高度与结束高度的大小关系,我们从后向前,也就是从 \(n\) 向 \(mid+1\) 进行搜索,代码如下(详细注释在代码中):
void query(int id, int t, int lim) {
int k = t + w[id]; //增加现在的权值
if (k >= m) res++; //如果已经大于m,那么直接增加答案
for (int i = 1; i <= lim; ++i)
if (h[i] <= h[id]) //对于每个起始高度大于等于结束高度的vector
res += v[i].end() - lower_bound(v[i].begin(), v[i].end(), m - k); //二分查找
for (int i = id - 1; i > lim; --i) //注意这里从后往前搜
if (h[i] <= h[id]) //高度限制
query(i, k, lim); //继续递归搜索
}
最后在 main() 函数中输出答案:
int main() {
cin >> n >> m;
for (int i = 1; i <= n; ++i) cin >> h[i] >> w[i];
int l = n >> 1;
for (int i = 1; i <= l; ++i) dfs(i, 0, l);
for (int i = 1; i <= l; ++i) sort(v[i].begin(), v[i].end());
for (int i = l + 1; i <= n; ++i) query(i, 0, l);
cout << res << '\n';
return 0;
}
我们就使用 meet in the middle 解决了这道题目!
总结一下,meet in the middle 的关键就在于首先取 middle,然后分段,最后再 "take a leap to the middle" 即可,用十个字概括其思想为 前一半枚举,后一半查询!
当然,meet in the middle 也不一定必须用在 dfs 或状态压缩 dp 中,如 CSP-S 2022 T1 假期计划 也可以用这个思路解决。

浙公网安备 33010602011771号