题目描述
作为一名新世纪共产主义的接班人,你认识到了资本主义的软弱性与妥协性,决定全面根除资本主义,跑步迈入共产主义。但是当你即将跨入共产主义大门的时候,遇到了万恶的资本家留下的与非电路封印,经过千辛万苦的研究,你终于把复杂的破解转变成了以下问题:
初始时你有一个空序列,之后有𝑁个操作。
操作分为以下两种:
- 1 x:在序列末尾插入一个元素𝑥(𝑥 = 0||𝑥 = 1)。
- 2 L R:定义
𝑛
𝑎
𝑛
𝑑
(
𝐿
,
𝑅
)
𝑛𝑎𝑛𝑑(𝐿, 𝑅)
nand(L,R) 为序列第𝐿个元素到第𝑅个元素的与非和。查询
𝑛 𝑎 𝑛 𝑑 ( 𝐿 , 𝐿 ) ⊕ 𝑛 𝑎 𝑛 𝑑 ( 𝐿 , 𝐿 + 1 ) ⊕ 𝑛 𝑎 𝑛 𝑑 ( 𝐿 , 𝐿 + 2 ) ⊕ ⋯ ⊕ 𝑛 𝑎 𝑛 𝑑 ( 𝐿 , 𝑅 ) 𝑛𝑎𝑛𝑑(𝐿, 𝐿) ⊕ 𝑛𝑎𝑛𝑑(𝐿, 𝐿 + 1) ⊕ 𝑛𝑎𝑛𝑑(𝐿, 𝐿 + 2) ⊕ ⋯ ⊕ 𝑛𝑎𝑛𝑑(𝐿, 𝑅) nand(L,L)⊕nand(L,L+1)⊕nand(L,L+2)⊕⋯⊕nand(L,R)。
其中, 𝐴 𝑛 𝑎 𝑛 𝑑 𝐵 = 𝑛 𝑜 𝑡 ( 𝐴 𝑎 𝑛 𝑑 𝐵 ) 𝐴\, 𝑛𝑎𝑛𝑑\, 𝐵 = 𝑛𝑜𝑡 (𝐴\, 𝑎𝑛𝑑\, 𝐵) AnandB=not(AandB),𝑛𝑜𝑡即逻辑非,𝑎𝑛𝑑即逻辑与; 𝑛 𝑎 𝑛 𝑑 ( 𝐿 , 𝑅 ) = 𝑎 L 𝑛 𝑎 𝑛 𝑑 𝑎 L + 1 𝑛 𝑎 𝑛 𝑑 ⋯ 𝑛 𝑎 𝑛 𝑑 𝑎 R 𝑛𝑎𝑛𝑑(𝐿, 𝑅) = 𝑎_L\, 𝑛𝑎𝑛𝑑\, 𝑎_{L+1}\, 𝑛𝑎𝑛𝑑\, ⋯\, 𝑛𝑎𝑛𝑑\, 𝑎_R nand(L,R)=aLnandaL+1nand⋯nandaR(从左往右计算);⊕表示逻辑异或。
数据规模和约定
数据点 | N N N 的规模 | 操作1的规模 | 操作2的规模 |
---|---|---|---|
1 | 1000 | 500 | 500 |
2 | 1000 | 500 | 500 |
3 | 200000 | 100000 | 100000 |
4 | 200000 | 100000 | 100000 |
5 | 1000000 | 900000 | 100000 |
6 | 4000000 | 3900000 | 100000 |
7 | 4000000 | 3900000 | 100000 |
8 | 4000000 | 3900000 | 100000 |
9 | 4000000 | 3900000 | 100000 |
10 | 4000000 | 3900000 | 100000 |
对于所有数据,满足 x = 0 ∣ ∣ x = 1 , L ≤ R x=0||x=1,L\le R x=0∣∣x=1,L≤R。
前言
这题本来是要求插入均摊 O ( 1 ) O(1) O(1) 的,但是标程估计没用zkw,常数大得没优势,所以很多插入 O ( log n ) O(\log n) O(logn) 的没被卡。
zkw线段树可以很方便地优化这种只有末尾插入、区间查询的题,比普通线段树快得多。
题解
题目后面还有提示: n a n d nand nand 运算有交换律,没有结合律。
其实大可不必费劲心思地找这种运算的规律或者与异或之间的性质,只需要知道它没有结合律就行了,因为 (我感觉) 出题人只是单纯为了整出个没有结合律的运算来考验你。
没有结合律,其实也好办,只需要保证从左往右算即可。考虑到元素的值只有0和1,经过反复运算后也是0或1,所以只要分别记录下每个区间的运算接在0以后和接在1以后的结果,就可以用线段树了。
具体地,设 a [ 0 / 1 ] a[0/1] a[0/1] 表示前接0/1时区间的与非和, s [ 0 / 1 ] s[0/1] s[0/1] 表示前接0/1时区间所有前缀与非和的异或和,那么合并部分是这样的:
t[x].a[0] = t[x<<1|1].a[ t[x<<1].a[0] ];
t[x].a[1] = t[x<<1|1].a[ t[x<<1].a[1] ];
t[x].s[0] = t[x<<1].s[0] ^ t[x<<1|1].s[ t[x<<1].a[0] ];
t[x].s[1] = t[x<<1].s[1] ^ t[x<<1|1].s[ t[x<<1].a[1] ];
应该很清楚吧?大概就是把左儿子的运算结果接到右儿子前面进行计算。我们在合并任意两个相邻的区间的时候都可以这样做。
普通线段树相信大家都会写吧。但是这题的修改操作次数非常多,跑大样例时,普通线段树把常数卡满也是接近2秒。
注意到这题只有单点修改区间查询。我在之前的博客里说过zkw线段树有且仅有区间修改时懒标记顺序的限制,也就是说,任何只有单点修改的线段树操作,都可以用快到起飞的zkw来做。
zkw线段树确实快了很多,只有1.3秒(没用fread优化),但毕竟带一个 log n \log n logn,无法突破理论速度上限。
前面一直忽略了很重要的一点:每次修改只在序列末尾插入,而查询只在已插入的部分进行。
也就是说,我们单点修改更新的时候,所有包含了当前序列外的点的区间,都不用更新,因为用不到。这样的区间肯定是单点修改链上的包含根的连续一段,所以我们只需要更新从叶子节点往上的一段祖先节点。由于每个区间只会由其右端点对应的叶子节点更新一次,所以单点修改总复杂度 O ( n ) O(n) O(n),均摊是 O ( 1 ) O(1) O(1) 的。
这个时候重口味最大的优势才体现出来。普通线段树的话,叶子节点位置是不确定的吧?你只有预处理出叶节点位置,然后学zkw那样往上更新吧?预处理一遍的话,常数又大起来了(如果你本来就打了建树,那当我没说,可这题是不需要建树的)。可zkw线段树的叶子节点位置是确定的,只需要在正常的单点修改往上更新的时候加一句特判“如果当前节点为左儿子那么更新完退出”即可。
inline void change(int x){
for(f[p+x].ins(g[x]),x=(p+x)>>1;x;x>>=1){
domerg(f[x],f[x<<1],f[x<<1|1]);
if(~x&1)return; //加一句
}
}
运用这个优化,加上zkw线段树,终于卡进了1秒。
代码
#include<cstdio>//JZM yyds!!!
#include<cstring>
#include<iostream>
#include<algorithm>
#include<cmath>
#include<ctime>
#include<vector>
#include<queue>
#include<stack>
#include<map>
#include<set>
#define ll long long
#define uns unsigned
#define MAXN 4000005
#define INF 1e18
#define lowbit(x) ((x)&(-(x)))
#define MOD 1000000007ll
#define IF it->first
#define IS it->second
using namespace std;
inline ll read(){
ll x=0;bool f=1;char s=getchar();
while((s<'0'||s>'9')&&s>0){if(s=='-')f^=1;s=getchar();}
while(s>='0'&&s<='9')x=(x<<1)+(x<<3)+s-'0',s=getchar();
return f?x:-x;
}
int n,m;
bool g[MAXN],las;
struct itn{
bool a[2],s[2];
void ins(bool x){
a[0]=!(0&x),a[1]=!(1&x),s[0]=a[0],s[1]=a[1];
}
}f[MAXN*3];
int p;
inline void domerg(itn&r,itn&a,itn&b){
r.a[0]=b.a[a.a[0]];
r.a[1]=b.a[a.a[1]];
r.s[0]=a.s[0]^(b.s[a.a[0]]);
r.s[1]=a.s[1]^(b.s[a.a[1]]);
}
inline itn merg(itn&a,itn&b){
itn r;
r.a[0]=b.a[a.a[0]];
r.a[1]=b.a[a.a[1]];
r.s[0]=a.s[0]^(b.s[a.a[0]]);
r.s[1]=a.s[1]^(b.s[a.a[1]]);
return r;
}
inline void change(int x){
for(f[p+x].ins(g[x]),x=(p+x)>>1;x;x>>=1){
domerg(f[x],f[x<<1],f[x<<1|1]);
if(~x&1)return;
}
}
inline itn query(int l,int r){
itn rel,rer;bool kl=0,kr=0;
for(l=p+l-1,r=p+r+1;l^1^r;l>>=1,r>>=1){
if(~l&1)rel=kl?merg(rel,f[l^1]):(kl=1,f[l^1]);
if(r&1)rer=kr?merg(f[r^1],rer):(kr=1,f[r^1]);
}
if(!kl)return rer;
if(kr)rel=merg(rel,rer);
return rel;
}
signed main()
{
freopen("nand.in","r",stdin);
freopen("nand.out","w",stdout);
m=read(),n=0;
for(p=1;p<m+2;p<<=1);
for(int D=1;D<=m;D++){
int op=read();
if(op==1)g[++n]=read()^las,change(n);
else{
int l=read(),r=read();
if(las)l=n-l+1,r=n-r+1,swap(l,r);
if(l==r)las=g[l];
else{
itn as=query(l+1,r);
las=g[l]^as.s[g[l]];
}
printf("%d\n",(int)las);
}
}
return 0;
}