前置知识:异或定义

这里不再进行严格定义,严格定义需要借助抽象代数中格的定义。会的都会,不会的自己去看就行。

我们直观地可以用以下方法定义。

真值表

0 0 0
0 1 1
1 0 1
1 1 0

当A与B相等时,为0,不等时为1。

代数

定义与、或、非运算 (与应该叫做合取,或应该叫析取,这里为了方便就这么说了)

0 0 0
0 1 0
1 0 0
1 1 1

为了方便,这里将 简记为

0 0 0
0 1 1
1 0 1
1 1 1
0 1
1 0

那么 =

两个定义是完全等价的。

由于布尔代数仅考虑集合 ,并不适用于整数集 ,因此我们对定义进行平凡的推广。

按位异或为每一位分别异或。比如

= ==

按位异或在C++中使用 ^ 表示,比如 c=a^b

异或具有一些性质,下面的性质可以直观理解,这里简单地证明一下。

异或满足结合律

有两种办法可以证明。一种是代数推式子,一种是真值表。 满足结合律也就是证明 =。它的证明是很简单的,不妨自行证明一下。

异或满足交换律

由真值表可以很容易看出。

异或存在幺元

如果存在元素 ,使得==,那么元素 是幺元,也叫单位元。

由定义可知,=,因此 是异或的幺元。

任意元素在异或运算下存在唯一逆元,且逆元为自身

如果存在元素,使得==,那么 的逆元,记为

由定义可知,=,因此任何元素自身是它自己的一个逆元。

假设存在其它逆元,即 =,那么 =====。因此只有一个。

上述证明方法是抽象代数常见的证明方法。

由于逆元为自己,所以如果要“减去”一些异或,就等于异或上它们。

维护前缀异或和

定义数组前缀异或和 =

那么由性质可知,=

对于某个连续区间的异或和,==

好想的 线段树解法

考虑到数据规模只有 ,显然 做法可以通过。

考虑异或的性质,我们可以只维护 为1的元素的异或和。因为所有数的异或和异或上 为1的元素的异或和就是 为0的元素的异或和。

我们可以维护一个线段树,用这个线段树维护区间内 的元素的异或和。这是很容易想到的。也很容易实现。

#include <cstdio>
#include <iostream>
#include <cmath>
#include <algorithm>
#include <cstring>
#include <string>
#include <cstdlib>
#define int long long

using namespace std;

int n,q,a[100005],pre[100005];
char s[100005];

struct SegmentTree
{
	int rt[400005],lazy[400005];
	void build(int x = 1,int l = 1,int r = n)
	{
		if(l==r)
		{
			if(s[l]=='1')rt[x]=a[l];
			else rt[x]=0;
			return;
		}
		int mid = (l+r)/2;
		build(x*2,l,mid);
		build(x*2+1,mid+1,r);
		rt[x]=rt[x*2]^rt[x*2+1];
	}
	void pushdown(int x,int l,int r)
	{
		if(lazy[x])
		{
			int mid = (l+r)/2;
			rt[x*2]=pre[mid]^pre[l-1]^rt[x*2];
			rt[x*2+1]=pre[r]^pre[mid]^rt[x*2+1];
			lazy[x]=0;
			lazy[x*2]^=1;
			lazy[x*2+1]^=1;
		}
	}
	void merge(int ql, int qr, int x = 1,int l = 1,int r = n)
	{
//		cout<<"ql qr x l r:"<<ql<<' '<<qr<<' '<<x<<' '<<l<<' '<<r<<endl;
		if(ql<=l&&r<=qr)
		{
			rt[x]=pre[r]^pre[l-1]^rt[x];
			lazy[x]^=1;
			return;
		}
		pushdown(x,l,r);
		int mid = (l+r)/2;
		if(ql<=mid)merge(ql,qr,x*2,l,mid);
		if(mid<qr)merge(ql,qr,x*2+1,mid+1,r);
		rt[x]=rt[x*2]^rt[x*2+1];
	}
	int query(int ql,int qr,int x = 1,int l = 1,int r = n)//其实不用写,因为它只查所有元素
	{
		if(ql<=l&&r<=qr)return rt[x];
		pushdown(x,l,r);
		int mid = (l+r)/2;
		int ans = 0;
		if(ql<=mid)ans^=query(ql,qr,x*2,l,mid);
		if(mid<qr)ans^=query(ql,qr,x*2+1,mid+1,r);
		return ans;
	}
}t;

void solve()
{
	cin>>n;
	for(int i = 1;i <= n * 4;i++)t.rt[i]=t.lazy[i]=0;
	for(int i = 1;i <= n;i ++)cin>>a[i],pre[i]=pre[i-1]^a[i];
	cin>>s+1;
	t.build();
	cin>>q;
	while(q--)
	{
		int opt;
		cin>>opt;
		if(opt == 1)
		{
			int l,r;
			cin>>l>>r;
			t.merge(l,r);
		}
		else
		{
			int g;
			cin>>g;
			if(g)cout<<t.query(1,n)<<' ';
			else cout<<(pre[n]^t.query(1,n))<<' ';
		}
	}
	cout<<endl;
}

signed main(){
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
	int t;
	cin>>t;
	while(t--)solve();
	return 0;
}

最优的 解法

然而这题实际上可以做到 的时间复杂度。 考虑这样一个情况。

a 2 3 4 5
s 0 1 1 0

此时答案为

如果对这个序列进行操作 ,那么会变成

a 2 3 4 5
s 1 0 0 1

此时答案为为

1变0的要删去,0变1的要加上,根据性质,不管是增还是删,都只需要异或即可。而不是0就是1,所以每一个数都相当于异或了一下。

显然,每次修改相当于答案异或上整个区间的异或和。

由异或性质=可知,对于异或前缀和 ,区间上的异或和为

那么做法就显而易见了,预处理出来异或前缀和 和初始状态下所有 为1的数的异或和 ,当为操作 时,令 ans^=pre[r]^pre[l-1],当为操作 时,输出

#include <cstdio>
#include <iostream>
#include <cmath>
#include <algorithm>
#include <cstring>
#include <string>
#include <cstdlib>

using namespace std;

int n,q,pre[100005],ans;

void solve()
{
	ans=0;
	cin>>n;
	for(int i = 1;i <= n;i ++)cin>>pre[i],pre[i]^=pre[i-1];
	for(int i = 1;i <= n;i ++)
	{
		char s;
		cin>>s;
		if(s=='1')ans^=pre[i]^pre[i-1];
	}
	cin>>q;
	while(q--)
	{
		int opt;
		cin>>opt;
		if(opt==1)
		{
			int l,r;
			cin>>l>>r;
			ans^=pre[r]^pre[l-1];
		}
		else
		{
			int g;
			cin>>g;
			if(g)cout<<ans<<' ';
			else cout<<(pre[n]^ans)<<' ';
		}
	}
	cout<<endl;
}

int main(){
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
	int t;
	cin>>t;
	while(t--)solve();
	return 0;
}