NOIP2023模拟7联测28 异或

题目大意

给定一个长度为 n n n的由非负整数组成的序列 a a a,你们需要进行一系列操作,每次操作选择一个区间 [ l , r ] [l,r] [l,r],对于所有 l ≤ i ≤ r l\leq i\leq r lir,将 a i a_i ai异或上 w w w。你需要将所有 a i a_i ai都变为 0 0 0,求最小的操作次数。

1 ≤ n ≤ 17 , 0 ≤ a i ≤ 1 0 18 1\leq n\leq 17,0\leq a_i\leq 10^{18} 1n17,0ai1018


题解

首先我们设序列 d d d为序列 a a a的差分序列(即 d i = a i ⊕ a i − 1 d_i=a_i\oplus a_{i-1} di=aiai1),于是我们就可以将区间修改变成单点修改(后缀区间修改)或双点修改(非后缀区间修改)。

求出差分序列后,我们在差分序列上操作,题意变为要将所有 d i d_i di变为 0 0 0

我们先不考虑 w w w的取值,而考虑如何取区间 [ l , r ] [l,r] [l,r]。将 n n n个数看成 n n n个点,将修改操作看成连接两个修改点的无向边(或自环)。那么,一组合法的方案由若干个连通块组成。

考虑每个连通块的最小边数,设这个连通块的大小为 x x x,则最小边数的下界为 x − 1 x-1 x1(即一棵树),上界为 x x x(将其中一个点连向其他所有点,再在这个点上连一个自环即合法)。我们考虑如何取到下界。

我们发现,可以取到这个下界当且仅当连通块内的数异或和为 0 0 0

必要性: 每次操作都是双点修改,那么整个连通块内的数的异或和不变,如果最终的异或和为 0 0 0,则开始时异或和必须为 0 0 0

充分性: 考虑用一条链串起这个连通块中的所有数,每次将链头通过异或自己的方式删掉,下一个数受到影响后成为新的链头。那么,经历 x − 1 x-1 x1次操作后,最后一个数肯定为 0 0 0,不需要再对其操作了。

也就是说,我们要将差分序列 d d d分成若干个子序列,对于每个子序列:

  • 当其异或和为 0 0 0时,其权值为 x − 1 x-1 x1
  • 否则,其权值为 x x x

我们要求一种划分方式,使得权值和最小。

可以发现,答案就是 n n n减去划分出的子序列中异或和为 0 0 0的子序列的数量,那我们要使划分出的子序列中异或和为 0 0 0的子序列的数量最大,可以用状压 D P DP DP,设 f s f_s fs表示 d d d的子序列 s s s可以划分成多少个异或和为 0 0 0的子序列,那么枚举子集的子集来转移即可。

可以先预处理出每个子序列的异或和。

时间复杂度为 O ( 3 n ) O(3^n) O(3n)

code

#include
using namespace std;
int n,f[1<<20];
long long a[25],d[25],v[1<<20];
int main()
{
//	freopen("xor.in","r",stdin);
//	freopen("xor.out","w",stdout);
	scanf("%d",&n);
	for(int i=1;i<=n;i++){
		scanf("%lld",&a[i]);d[i]=a[i]^a[i-1];
	}
	for(int s=1;s<1<<n;s++){
		for(int i=1;i<=n;i++){
			if((s>>i-1)&1) v[s]^=d[i];
		}
	}
	for(int s=1;s<1<<n;s++){
		for(int t=(s-1)&s;t;t=(t-1)&s){
			f[s]=max(f[s],f[t]+f[s^t]);
		}
		if(v[s]==0) f[s]=max(f[s],1);
	}
	printf("%d",n-f[(1<<n)-1]);
	return 0;
}

你可能感兴趣的:(题解,好题,题解,c++)