cjwen's blog

「笔记」折半搜索(Meet in the Middle)

2022-03-22 · 7 min read
折半搜索 笔记

思想

先搜索前一半的状态,再搜索后一半的状态,再记录两边状态相结合的答案。

暴力搜索的时间复杂度通常是 O(2n)O(2^{n}) 级别的。但折半搜索可以将时间复杂度降到 O(2×2n2)O(2 \times 2^{\frac{n}{2}}),再加上统计答案的时间复杂度,总复杂度几乎缩小了一半。

例题

「CEOI2015 Day2」世界冰球锦标赛

题目链接

Luogu P4799 [CEOI2015 Day2]世界冰球锦标赛

分析

用折半搜索的思想,先搜索 0n20 \sim \lfloor \frac{n}{2} \rfloor 的比赛,再搜索 (n2+1)n(\lfloor \frac{n}{2} \rfloor + 1) \sim n 的比赛。每个比赛有看与不看两种状态,时间复杂度 O(2×2n2)O(2 \times 2^{\frac{n}{2}})。在搜索后半部分的时候,假设该状态的花费是 ss,则去前半部分的答案中找到所有花费小于等于 msm - s 的结果,统计答案。

前半部分搜索的时候记录所有的答案,然后排序,这样后半部分统计答案的时候可以二分。

总的时间复杂度为 O(2n2+2n2log(2n2))O(2^{\frac{n}{2}} + 2^{\frac{n}{2}} \cdot \log(2^{\frac{n}{2}})),可通过本题。

注意 vector 的常数问题:本题如果采用两个 vector 数组,分别记录两边的答案,最后再统计,则会在 #45 测试点 Time Limit Exceeded(开 O2\text{O2} 可过)。

参考代码

#include<iostream>
#include<cstdio>
#include<vector>
#include<algorithm>

using namespace std;

const int N = 50;

int n;
long long m, w[N];
vector <long long> v1; // 存储所有前部部分可以得到的状态的花费(可重)
long long ans;

void dfs1(int p, long long s){ // p -> 当前位置,s -> 当前花费,下同
	if(p >= (n/2)){
		v1.push_back(s); // 记录前半部分状态
		return ;
	}
	dfs1(p+1, s);
	if(s + w[p] <= m){
		dfs1(p+1, s+w[p]);
	}
}

void dfs2(int p, long long s){
	if(p >= n){
		ans += upper_bound(v1.begin(), v1.end(), m - s) - v1.begin(); // 统计前半部分花费小于 (m-s) 的状态数量
		return ;
	}
	dfs2(p+1, s);
	if(s + w[p] <= m){
		dfs2(p+1, s+w[p]);
	}
}

int main(){
	
	scanf("%d%lld", &n, &m);
	
	for(int i = 0; i < n; i++){
		scanf("%lld", &w[i]);
	}
	
	dfs1(0, 0);
	
	sort(v1.begin(), v1.end());
	
	dfs2((n/2), 0);
	
	printf("%lld\n", ans);
	
	return 0;
}

「USACO 12 OPEN」Balanced Cow Subsets G

题目链接

Luogu P3067 [USACO12OPEN]Balanced Cow Subsets G

分析

同样折半搜索,先搜索 0n20 \sim \lfloor \frac{n}{2} \rfloor 的数,再搜索 (n2+1)n(\lfloor \frac{n}{2} \rfloor + 1) \sim n 的数。

每个数有「放第一组」「放第二组」「不选」共三种状态,可以在搜索的时候把「放第一组」记为 ++,把「放第二组」记为 -,「不选」就不加也不减,这样两组相等就是和为 00

在搜索后半部分的时候,记录答案,假设该状态的和是 ss,则去前半部分的答案中找到所有等于 s-s 的结果。

直接这样交会 Wrong Answer 3838。仔细看题,要求的是找出一些数,使得它们能被分为两组。比如有四个数 a,b,c,da, b, c, d,满足 a+b=c+da + b = c + dc+d=a+bc + d = a + ba+c=b+da + c = b + db+d=a+cb + d = a + c 之类,就会被重复记录。还有诸如此类的多个数的重复情况。所以要记录选数的情况(有些类似 hash 的思想),比如有 a,b,c,da, b, c, d 四个数,选了 a,ca, c 两个,就用二进制数 10101010 记录(11 表示选,00 表示不选)。再左移 1010 位(n20n \leq 20,前半部分最多 1010 个数),并连接上后半部分的选数情况,就得到了形如 1010000000xxxx1010000000xxxx 的二进制数,开 bool 数组去重即可。

这样时间复杂度为 O(3n2+3n2log(3n2))O(3^{\frac{n}{2}} + 3^{\frac{n}{2}} \cdot \log(3^{\frac{n}{2}})),实际远远跑不满,在开 O2\text{O2} 的情况下最慢的测试数据也才 131ms131ms

代码中 v1[x] 是 vector 类型的,该数组表示所有前半部分答案为 xx 的选数情况记录。

还是注意考虑 vector 的常数问题,必要时善用 O2\text{O2}

我写的这份常数比较大,不开 O2 的话用 vector 过不去,但用 set 能过去。

参考代码

#include<iostream>
#include<cstdio>
#include<vector>
#include<unordered_map>
#include<algorithm>

using namespace std;

const int N = 30, F = 1 << 21;

int n;
int a[N];
unordered_map <long long, vector <int> > v1;
long long ans;
bool vis[F];

void dfs1(int p, int s, int tp){ // p -> 当前位置,s -> 当前和,tp -> 选数记录(用于去重),下同
	if(p >= (n/2)){
		v1[s].push_back(tp);
		return ;
	}
	dfs1(p+1, s+a[p], (tp<<1)|1);
	dfs1(p+1, s-a[p], (tp<<1)|1);
	dfs1(p+1, s, (tp<<1));
}

void dfs2(int p, int s, int tp){
	if(p >= n){
		for(int i : v1[-s]){ // 枚举前半部分所有结果为 -s 的
			if(!vis[(i<<10)|tp]){
				vis[(i<<10)|tp] = 1;
				ans++;
			}
		}
		return ;
	}
	dfs2(p+1, s+a[p], (tp<<1)|1);
	dfs2(p+1, s-a[p], (tp<<1)|1);
	dfs2(p+1, s, (tp<<1));
}

int main(){
	
	scanf("%d", &n);
	
	for(int i = 0; i < n; i++){
		scanf("%d", &a[i]);
	}
	
	dfs1(0, 0, 0);
	
	dfs2((n/2), 0, 0);
	
	printf("%lld\n", ans-1); // 减去都不选的情况
	
	return 0;
}
Copyright © 2021~2024 cjwen