【不知道哪里的某个集训】【DAY 2 T3】Nand与非——线段树卡常

本文介绍了一种使用ZKW线段树优化的算法,针对大量单点插入和区间与非查询的问题,通过避免结合律运算的复杂性,实现了高效的O(n)平均时间复杂度。博客详细探讨了如何利用线段树结构存储与非运算结果,并提供了代码实现,帮助读者理解如何在实际场景中提升查询效率。
摘要由CSDN通过智能技术生成

题目描述

作为一名新世纪共产主义的接班人,你认识到了资本主义的软弱性与妥协性,决定全面根除资本主义,跑步迈入共产主义。但是当你即将跨入共产主义大门的时候,遇到了万恶的资本家留下的与非电路封印,经过千辛万苦的研究,你终于把复杂的破解转变成了以下问题:

初始时你有一个空序列,之后有𝑁个操作。
操作分为以下两种:

  • 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+1nandnandaR(从左往右计算);⊕表示逻辑异或。

数据规模和约定

数据点 N N N 的规模操作1的规模操作2的规模
11000500500
21000500500
3200000100000100000
4200000100000100000
51000000900000100000
640000003900000100000
740000003900000100000
840000003900000100000
940000003900000100000
1040000003900000100000

对于所有数据,满足 x = 0 ∣ ∣ x = 1 , L ≤ R x=0||x=1,L\le R x=0x=1,LR

前言

这题本来是要求插入均摊 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;
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值