B. 异或(xor)
题意
给你 \(n\) 个数,你可以执行若干次操作,每次选择一个区间异或上一个数,问最少操作次数使得所有数变成 \(0\),输出最小的操作次数。
\(n \le 17,a_i \le 10^8\)
solution
居然还有异或差分这种思想,是我孤陋寡闻了
因为是区间操作,所以我们考虑在异或差分序列上操作。显然异或是满足差分性质的。
每次操作相当于对两个数异或上 \(x\),如果操作区间是一段后缀就相当于对一个数异或上 \(x\)。
所有数变成 \(0\) 就相当于差分数组变成 \(0\)。
因此题目变成对于一个序列,可以单点或双点异或上一个数,问最小操作次数使所有数变成 \(0\)。
题解转成图论思考,感觉想法过于聪明,或许直接从序列入手本质上也是一样的。
首先如果有两个一样的数,肯定对这两个数进行一次操作变成 \(0\) 是最优的。直接忽略它们。
对于其他数,进行单点异或一定可以把它变成 \(0\)。
如果只进行单点异或操作,可以得到答案上界,是 \(n\)。为了得到更小的答案,我们不得不做一些双点操作。
考虑进行双点操作发生了什么,发现两个数的异或和操作完不变。
然后可能需要敏锐的观察力。对于一堆异或和不为 \(0\) 的数,你无法只使用双点操作把它们都变成 \(0\),最后剩下一个数的时候你必须使用一次单点操作把它变成 \(0\),因此我们干脆把这个数从这一堆里踢出去,所有操作都不带上这个数,答案不会更劣。
因此我们只研究一堆异或和为 \(0\) 的数。假设这一堆有 \(x\) 个数,那么我们可以只进行双点操作把它们全部变成 \(0\)。发现对于极小的一堆异或和为 \(0\) 的数,最优的操作是每次操作 \((a,b)\) 的时候使它们都异或上\(a\),答案下界是 \(x-1\)。
因此我们要划分出尽可能多的极小的异或和为 \(0\) 的子序列,看到 \(n\) 很小,可以状压 DP。
题解给出的子集枚举转移的做法总复杂度是 \(O(3^n)\) 的。参考黄队的实现,可以做到 \(O(2^n n)\)。
对每个子序列枚举加入哪一个数转移。如果当前状态的异或和为 \(0\),又是因为异或的可差分性质,说明当前状态可以分出一个新的异或和为 \(0\) 的子序列。
跑得飞快,比黄队还快。数据可以加强了
code
#include<bits/stdc++.h>
// #define LOCAL
#define sf scanf
#define pf printf
#define rep(x,y,z) for(int x=y;x<=z;x++)
#define per(x,y,z) for(int x=y;x>=z;x--)
using namespace std;
typedef long long ll;
constexpr int N=18;
int n,a[N+2];
int s[1<<N],f[1<<N];
void _max(int &a,int b) { a=max(a,b); }
int main() {
#ifdef LOCAL
#else
freopen("xor.in","r",stdin);
freopen("xor.out","w",stdout);
#endif
sf("%d",&n);
rep(i,1,n) sf("%d",&a[i]);
rep(i,1,n-1) a[i]^=a[i+1];
rep(i,1,(1<<n)-1) rep(j,0,n-1) if((i>>j)&1) s[i]^=a[j+1];
rep(i,1,(1<<n)-1) {
if(!s[i]) f[i]++;
rep(j,0,n-1) if(!((i>>j)&1)) _max(f[i|(1<<j)],f[i]);
}
pf("%d\n",n-f[(1<<n)-1]);
}
标签:xor,差分,异或,序列,操作,变成,双点
From: https://www.cnblogs.com/liyixin0514/p/18472145