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 算法的练习题:
-
P2962 [USACO09NOV]Lights G。
-
P4799 [CEOI2015 Day2] 世界冰球锦标赛。
-
P3067 [USACO12OPEN]Balanced Cow Subsets G。

浙公网安备 33010602011771号