Luogu8438 solution

Problem

给定序列 \(\{a_n\}\)\(V(s)\) 表示若 \(s\) 在二进制表示下从右到左地 \(i\) 位为 \(1\),则将 \(V(s)\) 异或上 \(a_i\)

\(\bigoplus\limits_{i=0}^{2^n-1} V(i)\times i\)

link->https://www.luogu.com.cn/problem/P8438

Solution

考虑暴力,先枚举 \(1\sim 2^n-1\) 的每个数,然后依次计算 \(V(i)\) 的值,再异或起来即可。复杂度 \(O(n\cdot2^n)\),可过前 \(20\) 分。

逆向思维,与其我们枚举 \(i\),不如枚举 \(i\) 在二进制下的每一位(共 \(n\) 位)是 \(0\) 还是 \(1\),然后填入 \(i\),这样子可以通过两种方法实现:

  • DFS,这样做时间复杂度 \(O(2^n)\),空间复杂度 \(O(n)\)(搜索树最深深度为 \(n\))。

  • DP,这样做时间复杂度 \(O(2^n)\),空间复杂度 \(O(2^n)\)

看见 \(n\le30\)\(2^n\approx10^9\),然后标签里有 O2 的字样,说不定可过?

但是交上去 DFS 却看见 TLE 的字样,稍加分析可知是常数过大;交上去 DP 去看见 MLE 的字样,显然是数组过大,无法优化。

既然 DP 已经无法优化,那我们再考虑 DFS,经过若干次卡常之后,我们发现普通的深搜完全无法通过此题,接下来我们就介绍一种优化搜索高端技艺:meet in the middle 即中途相遇法。

我们可以考虑将前 \(\lfloor\frac{n}{2}\rfloor\) 的取值先确定下来,存在一个数组 \(\{b_m\}\) 里,然后再枚举后 \(\lceil\frac{n}{2}\rceil\) 位,每一次枚举完成再与前已经枚举完成的 \(m\) 种方案一一配对,显然这样包含所有结果。

再说说时间复杂度:第一次 DFS 时间复杂度 \(O(2^{\frac{n}{2}})\)\(m=2^{\frac{n}{2}}\),而第二次遇到边界时有 \(2^\frac{n}{2}\) 种情况,每种情况又要与前 \(m\)\(b_i\) 一一配对,时间复杂度 \(O(2^n)\)

似乎我们并没有进行什么优化,但事实真的如此吗?前面讨论到 DFS TLE 的真正原因是因为常数过大,而在这里,我们又恰好使用中途相遇法优化掉了这一点,所以以此思路可以取得 AC 的好成绩。

当然,meet in the middle 算法不仅仅使用在优化常数上,多数情况下,他往往能将 \(2^k\) 优化至 \(2^{\frac{k}{2}}\)

Code

#define int __int128
using namespace std;
const int N=35;
int a[N],n;
vector<PII> vec;
void dfs(int x,int s,int val) {
	if(x==n/2)
		return (void)vec.push_back(mk(s,val));
	dfs(x+1,s^a[x],val|(1<<x));
	dfs(x+1,s,val);
}
int dfs2(int x,int s,int val) {
	if(x==n) {
		int ans=0;
		_for(i,0,vec.size()-1)
			ans^=(vec[i].F^s)*(vec[i].S+val);
		return ans;
	}
	return dfs2(x+1,s^a[x],val|(1<<x))^dfs2(x+1,s,val);
}
signed main() {
	cin>>n;
	_for(i,0,n-1)
		cin>>a[i];
	dfs(0,0,0);
	cout<<dfs2(n/2,0,0);
	return 0;
}

最后再留几道 meet in the middle 算法的练习题:

  1. P2962 [USACO09NOV]Lights G。

  2. P4799 [CEOI2015 Day2] 世界冰球锦标赛。

  3. P3067 [USACO12OPEN]Balanced Cow Subsets G。

posted @ 2022-08-01 11:57  lsj2009  阅读(47)  评论(0)    收藏  举报