可持久化入门 ի( ‘▿ ‘ )

这是一篇面向初学者的可持久入门文章
人道:“入门持久化,出门莫队人”
莫队【传送门】 (别被传送走了啊

前置芝士:

线段树 【传送门】

一些人文芝士

名称?

可持久化数据机构也叫主席树(反正我是这么理解的
还有知乎讨论过这件事情 【传送门】

身世?

和归并树有着千丝万缕的联系
对于大部分的可持久化数据结构题目,归并树不好写而且难调,思维难度也比主席树高一些
很久以前(也不是很久吧),有位巨佬hjt在比赛的时候遇到一道归并树的题目,他可能觉得太烦了,于是想出了一种新的可持久化数据结构,后来称作主席树了。

思考一下为啥叫主席树。

作用?

能在线查询历史版本,仅此而已

优缺点?

优点:

查询,插入都是 1 1 1 l o g log log ,是优秀的数据机构
支持在线查询
代码非常简洁,好调,而且基本都是一个板子

缺点:

时间常数大,几乎是卡满的, 有道题我甚至没莫队的一半快
空间常数大,一半来说看到 512 M B 512MB 512MB 的题基本就是这玩意儿了


.

可持久入门

引入

以模板题P3919 可持久化线段树 1为例
当学习可持久化数据结构时,首先要抛开离线的想法

简述

本题要求支持历史版本修改,查询操作

暴力

我们用数组把每一个版本记录下来。
每个新版本就是上一个版本改变了一个数,其他都保持相同。
于是只要开 f [ m ] [ n ] f[m][n] f[m][n] 这样一个数组存一下每个版本的数就行
每次修改是 O ( n ) O(n) O(n) 的,查询也是 O ( n ) O(n) O(n) ,空间复杂度达到 O ( n m ) O(nm) O(nm)

升级一下?

暴力为什么拉胯,就是因为每次操作只改变一个数,而暴力要将其他的数都拷贝过来,才能保证下次询问到此版本时能提供这些数据。
我们发现这道题真正的难点在于每次版本生成都要供后来的操作修改,所以我们必须要记录下这个版本的信息,如何记录这个版本成为做题关键

这时候就有神仙想到了线段树的好性质:对于单点修改,线段树只被修改了一条链
有个图就好理解了


我们发现,对于单点修改,其被改动只有 l o g ( n ) log(n) log(n) 的节点,可持久化就基于这个良好性质

可以想到,除去这些被改动的部分,其他的部分是会原封不动的继承给新版本的,因此版本的继承可以转换为:我是一个版本 X X X ,我的上一个版本是 Y Y Y ,我只要改掉属于我的 l o g ( n ) log(n) log(n) 个节点,其他的我可以和 Y Y Y 姐姐共享就行了
我反正理解能力不太好,这个时候就需要几张图

如此我们可以做到 l o g ( n ) log(n) log(n)的修改和查询,就变得很优了

每个人的 C o d e Code Code都略微有不同,主流有两种,一种是回根式的,一种是下传式的

Modify:
//pr是上一个版本,找到s位置改为k,和普通线段树是一样的
//以下所有I都是int,个人码风比较毒瘤
I Mdfy(I pr,I a,I b,I s,I k){I x=++trcnt;I mid=a+b>>1;//一个新节点x
	if(!(a^b)) return t[x].val=k,x;//!(a^b)就是a==b,函数要反回根x
	if(s<=mid) ls(x)=Mdfy(ls(pr),a,mid,s,k),rs(x)=rs(pr);//右儿子是原封不动的
	else rs(x)=Mdfy(rs(pr),mid+1,b,s,k),ls(x)=ls(pr);//左儿子是原封不动的
	return x;//回根式
}
//如果没返回可能会RE,int函数经典问题

T a l k Talk Talk i s is is n e c c e s s a r y , s h o w neccessary,show neccessary,show m e me me y o u r your your C o d e : Code: Code:

#include<bits/stdc++.h>
using namespace std;

#define I int  
#define F(a,b,c) for(register int a=b;a<=c;++a) 
#define ll long long
//#define I long long
I R(){int x=0,f=1;char c=getchar();
	for(;c<'0'||c>'9';c=getchar()) if(c=='-') f=-1;
	for(;c>='0'&&c<='9';c=getchar()) x=(x<<3)+(x<<1)+(c^48);
	return x*f;
}

const int N = 1e6+10;
const int L = 21;
I n,m,d[N];
I trcnt,T[N];
struct TREE{
	I ls,rs,val;
}t[N*L*2];
#define ls(x) t[x].ls
#define rs(x) t[x].rs
void Bld(I &x,I a,I b){
	x=++trcnt;I mid=a+b>>1;
	if(!(a^b)) return t[x].val=d[a],void();
	Bld(ls(x),a,mid);
	Bld(rs(x),mid+1,b);
}
I Mdfy(I pr,I a,I b,I s,I k){I x=++trcnt;I mid=a+b>>1;
	if(!(a^b)) return t[x].val=k,x;
	if(s<=mid) ls(x)=Mdfy(ls(pr),a,mid,s,k),rs(x)=rs(pr);
	else rs(x)=Mdfy(rs(pr),mid+1,b,s,k),ls(x)=ls(pr);
	return x;
}
I Qry(I x,I a,I b,I s){I mid=a+b>>1;
	if(!(a^b)) return t[x].val;
	if(s<=mid) return Qry(ls(x),a,mid,s);
	else return Qry(rs(x),mid+1,b,s);
}

signed main(){n=R(),m=R();
	F(i,1,n) d[i]=R();
	Bld(T[0],1,n);
	F(i,1,m){I rec=R(),opt=R(),x=R();
		if(opt==1){I y=R();
			T[i]=Mdfy(T[rec],1,n,x,y);
		}
		else T[i]=T[rec],printf("%d\n",Qry(T[rec],1,n,x));
	}
	return 0;
}

什么是历史版本?

上面这道题是一个纯模板,入门题其实是P3834 可持久化线段树 2

简述

给出一个数组,求区间 [ l , r ] [l,r] [l,r] 的第 k k k

思路

这里我就蒙了啊,我当时先做的这道题,看题解直接人傻了,我完全不知道这和权值线段树有什么关系

如何理解“历史版本”是一个关键问题,在这道题,我们可以把区间理解为版本。
然后我们维护一颗权值线段树,就结束了 $ $ $ KaTeX parse error: Expected group after '_' at position 1: _̲by$某dalao。

$ dalao$ 就是 d a l a o dalao dalao ,但这是面向初学者的文章,于是我们要先剖析一下主席树的性质。

主席树基于什么,正是线段树,于是线段树维护的信息,决定了主席树维护什么。所以我们要先想如何用一个线段树完成类似的操作。

对于第一道题,因为只是记录信息,所以我们用线段树的每个位置对应信息记录的位置,对于此题,我们可以把数据插入到对应数据大小的位置,那这就是一颗权值线段树

权值线段树

其实只是把数据插入对应大小的位置
特点是能求出当前插入数据的第 k k k 小值
s i z e size size 代表该区间所含的数据个数



贴一下对应的代码,~~不要问为什么码风不一样,我每个主席树的题码风都不一样。 ~~

//由于是权值线段树,所以走左子表示向左缩小范围,我们要尽可能走左子
int qry(int u,int v,int l,int r,int k){
	if(l>=r) return l;
	int mid=(l+r)>>1;
	int x=sum[L[v]]-sum[L[u]];//求左区间的数据个数
	if(x>=k) return qry(L[u],L[v],l,mid,k);//查询左区间的第k小
	else return qry(R[u],R[v],mid+1,r,k-x);//左区间只有x个数,不够查,查询右区间的k-x小
}
离散化

这往往和权值线段树挂钩,由于数据权值太大了,权值线段树的规模和数据权值大小挂钩,是空间复杂度是 O ( 2 ∗ D a t ) O(2*Dat) O(2Dat),所以我们考虑离散化,每两个数相对的大小不变,但是数值缩小
标准懒人代码,带只 l o g log log

F(i,1,n) X[i]=a[i]=Read();
sort(X+1,X+n+1);Tail=unique(X+1,X+n+1)-X-1;
F(i,1,n) a[i]=lower_bound(X+1,X+Tail+1,a[i])-X;

此时的 a a a 数组大小缩小,相对大小不变, X [ a [ i ] ] X[a[i]] X[a[i]] 为原始数值大小

持久++

了解了的权值线段树,我们发现,每插入一个数后,也只修改一条 l o g ( n ) log(n) log(n) 的链
于是我们考虑第 i i i 个版本表示 [ 1 , i ] [1,i] [1,i] 的数据所构成的权值线段树,那么从第 i i i 个版本到第 i + 1 i+1 i+1 个版本,只是多插入了一个数据

如何查询呢?

我们发现主席树的一个特性:他的每个版本规格都是一定的。
那么查询 [ l , r ] [l,r] [l,r] 的区间相当于把版本 r r r 和版本 l − 1 l-1 l1 拎出来,把 r r r 树的每一个点的 s i z siz siz 减去 l − 1 l-1 l1 树每个点的 s i z e size size,就等于 [ l , r ] [l,r] [l,r] 的数构成的权值线段树
T a l k Talk Talk i s is is n e c c e s s a r y , s h o w neccessary,show neccessary,show m e me me y o u r your your C o d e : Code: Code:

#include<bits/stdc++.h>
using namespace std;
#define int long long
#define oo 0x3f3f3f3f
#define mp make_pair
#define pb push_back
#define ft first
#define sd second
#define ub upper_bound
#define lb lower_bound
#define pii pair<int,int>
void File(){freopen(".in","r",stdin);freopen(".out","w",stdout);}
const int mod=1e9+7;
struct mint{
	int x;mint(int o=0){x=o;}
	mint&operator+=(mint a){x+=a.x;x>=mod&&(x-=mod);return*this;}
	mint&operator-=(mint a){x-=a.x;x<0&&(x+=mod);return*this;}
	mint&operator*=(mint a){x=1ll*x*a.x%mod;return*this;}
	mint&operator^=(int b){mint a=*this;x=1;while(b)(b&1)&&(*this*=a,1),b>>=1,a*=a;return*this;}
	mint&operator/=(mint a){return*this*=(a^=mod-2);}
	friend mint operator+(mint a,mint b){return a+=b;}
	friend mint operator-(mint a,mint b){return a-=b;}
	friend mint operator*(mint a,mint b){return a*=b;}
	friend mint operator/(mint a,mint b){return a/=b;}
};
template<class T>void read(T&x){x=0;char c=getchar();bool f=0;for(;!isdigit(c);c=getchar()){f|=c=='-';}for(;isdigit(c);c=getchar()){x=x*10+c-'0';}if(f)x=-x;}
template<class T,class ...ARK>void read(T&x,ARK&...ark){read(x);read(ark...);}
template<class T>void write(T x){if(x<0) putchar('-'),x=-x;if(x>=10) write(x/10);putchar((x%10)^48);}
template<class T,class ...ARK>void write(T x,ARK...ark){write(x);putchar(' ');write(ark...);puts("");}
const int N=2e5+10;
int n,q,m,cnt;
int a[N],rt[N];
int sum[N<<5],L[N<<5],R[N<<5];
vector<int>uni;
int upd(int pre,int l,int r,int val){
	int t=++cnt,mid=(l+r)>>1;
	L[t]=L[pre],R[t]=R[pre];
	sum[t]=sum[pre]+1;
	if(l<r){
		if(val<=mid) L[t]=upd(L[pre],l,mid,val);
		else R[t]=upd(R[pre],mid+1,r,val);
	}
	return t;
}
int qry(int u,int v,int l,int r,int k){
	if(l>=r) return l;
	int mid=(l+r)>>1;
	int x=sum[L[v]]-sum[L[u]];
	if(x>=k) return qry(L[u],L[v],l,mid,k);
	else return qry(R[u],R[v],mid+1,r,k-x);
}
signed main(){
	read(n,q);
	for(int i=1;i<=n;i++) read(a[i]),uni.pb(a[i]);
	sort(uni.begin(),uni.end());
	m=unique(uni.begin(),uni.end())-uni.begin()+100;
	for(int i=1;i<=n;i++){
		int t=lb(uni.begin(),uni.end(),a[i])-uni.begin()+1;
		rt[i]=upd(rt[i-1],1,m,t);
	}
	while(q--){
		int x,y,z;
		read(x,y,z);
		int t=qry(rt[x-1],rt[y],1,m,z);
		write(uni[t-1]),puts("");
	}
    return 0;
}

简洁一点?

#include<bits/stdc++.h>
using namespace std; 
#define I int  
#define F(a,b,c) for(register int a=b;a<=c;++a) 
int R(){int x=0,f=1;char c=getchar();for(;c<'0'||c>'9';c=getchar()) if(c=='-') f=-1;
	  for(;c>='0'&&c<='9';c=getchar()) x=(x<<3)+(x<<1)+(c^48);return x*f;}
const int N = 2e5+10;
const int L = 19;
struct TREE{int ch[2],num;}t[N*L];		   
int n,m,Tail,trcnt,T[N],X[N],a[N];
I Mdfy(I pr,I a,I b,I S){I x=++trcnt;t[x].num=t[pr].num+1;
	if(a^b){I mid=a+b>>1,d=S>mid;	
		t[x].ch[d]=Mdfy(t[pr].ch[d],d?mid+1:a,d?b:mid,S); 
		t[x].ch[!d]=t[pr].ch[!d];
	}return x;
}
I Qry(I l,I r,I k){I a=1,b=Tail;
	while(a^b){I ls=t[t[r].ch[0]].num-t[t[l].ch[0]].num,d=ls<k,mid=a+b>>1;
		l=t[l].ch[d],r=t[r].ch[d],k-=d*ls,a=d?mid+1:a,b=d?b:mid;
	}return a;
}//splay后遗症
signed main(){n=R(),m=R();
	F(i,1,n) X[i]=a[i]=R();
	sort(X+1,X+n+1);Tail=unique(X+1,X+n+1)-X-1;
	F(i,1,n) T[i]=Mdfy(T[i-1],1,Tail,lower_bound(X+1,X+Tail+1,a[i])-X);
	F(i,1,m){int l=R(),r=R(),k=R();
		printf("%d\n",X[Qry(T[l-1],T[r],k)]);
	}
	return 0;
}

类似的题目有几道:

P1383 高级打字机

模板题,加深印象

P1533 可怜的狗狗

需要注意的是有时候查询 n n n 个数中的第 k k k 大不代表查询这 n n n 个数中的第 n − k + 1 n-k+1 nk+1

P2163 [SHOI2007]园丁的烦恼

园丁的烦恼正解就是离线,内存只给了 128 M B 128MB 128MB,其实主席树能跑。

P3168 [CQOI2015]任务查询系统

这题主席树是正解,因为强制在线

还可以干啥?

又是一道经典题目,哈哈的项链
介绍一下这道题,这道题主席树是最慢的,早期这题好像还卡过主席树,那时候时限是1.5s,主席树常数大,TLE了,连莫队都打不过(可是能在线啊)

简述

维护 [ l , r ] [l,r] [l,r] 区间内不同颜色的个数

难点在于如何去重,颜色可能会重复,但只能算一次贡献
做法是维护 p r e [ x ] pre[x] pre[x] 值,表示颜色 x x x上一次出现在哪个位置,对于 [ l , r ] [l,r] [l,r] 如果 p r e [ x ] < l pre[x]<l pre[x]<l,我们就算一次贡献,这样我们就不会重复计入贡献

这和主席树有啥关系呢?
其中一种简单的做法是对于第 i i i 个数,他的颜色是 x x x 把第 i i i 棵树的 p r e [ x ] pre[x] pre[x]位置+1,若我们要计算 [ l , r ] [l,r] [l,r] 的颜色数量,把第 r r r 棵树拎出来,查询他树上第 [ 1 , l − 1 ] [1,l-1] [1,l1] 的位置的和,就是一半的答案,为什么说一半呢,因为这重复记录了第 l − 1 l-1 l1 棵树上 [ 1 , l − 1 ] [1,l-1] [1,l1]的贡献,这些贡献会被计算到,而第 l − 1 l-1 l1树根本不属于 [ l , r ] [l,r] [l,r] ,减去这部分不合法的贡献即可。

T a l k Talk Talk i s is is n e c c e s s a r y , s h o w neccessary,show neccessary,show m e me me y o u r your your C o d e : Code: Code:

#include<bits/stdc++.h>
#define ls(x) t[x].ls
#define rs(x) t[x].rs
using namespace std;
const int maxn = 1e6+10;

int n,q,pre[maxn];

inline int read(){
	int x=0,f=1;
	char c=getchar();
	while(c<'0'||c>'9'){if(c=='-') f=-1;c=getchar();}
	while(c>='0'&&c<='9'){x=(x<<3)+(x<<1)+(c^48);c=getchar();}
	return x*f;
}

int T[maxn];
int trcnt,rcnt;
struct TREE{
	int ls,rs,val;
}t[maxn*24];
inline void Build(int x,int a,int b){
	if(a==b) return;
	int mid=a+b>>1;
	ls(x)=++trcnt,Build(trcnt,a,mid);
	rs(x)=++trcnt,Build(trcnt,mid+1,b);
}
inline void Updata(int R,int x,int col,int a,int b){
	t[x].val=t[R].val+1;
	if(a==b) return;
	int mid=a+b>>1;
	if(col<=mid){
		ls(x)=++trcnt,rs(x)=t[R].rs;
		Updata(ls(R),ls(x),col,a,mid);	
	}
	if(col>=mid+1){
		rs(x)=++trcnt,ls(x)=t[R].ls;
		Updata(rs(R),rs(x),col,mid+1,b);
	}
}
inline int Query(int x,int D,int a,int b){
	if(b<=D) return t[x].val;
	int mid=a+b>>1;
	if(mid>=D) return Query(ls(x),D,a,mid);
	else return Query(rs(x),D,mid+1,b)+t[ls(x)].val;
}

int x;
int main(){
	scanf("%d",&n);
	T[0]=++trcnt,Build(trcnt,0,n);
	for(register int i=1;i<=n;++i){
		x=read();
		T[i]=++trcnt;
		Updata(T[i-1],trcnt,pre[x],0,n);
		pre[x]=i;
	}
	q=read();
	for(register int i=1;i<=q;++i){
		int l,r;
		l=read(),r=read();
		printf("%d\n",Query(T[r],l-1,0,n)-Query(T[l-1],l-1,0,n));
	}
	return 0;
}

more??

我们发现只要一颗线段树能干,又是单点修改,直接就可以可持久化
看题 P3963 [TJOI2013] 奖学金

简述

n n n 个数有 a a a b b b 两个属性,选出 c c c个数,满足按 a a a 属性排序的中位数最大, b b b 属性之和最小

这道题启发我们要找好可持久化什么。
我们枚举每一个数 x x x 可不可能成为中位数,如果这个数成为 a a a 属性排序的中位数,按 a a a 属性排序后,目标序列 [ 1 , c / 2 − 1 ] [1,c/2-1] [1,c/21] 的部分的 a a a x . a x.a x.a 小, 目标序列 [ c / 2 + 1 , c ] [c/2+1,c] [c/2+1,c] 的部分的 a a a x . a x.a x.a 大,我们只需要找出满足满足这个条件的 b b b 属性最小前后缀就行。

具体做法

先把所有数按照 a a a 属性排序,保证枚举第 i i i 个数为中位数时, [ 1 , i − 1 ] [1,i-1] [1,i1]的数的 a a a 属性都小于第 i i i 个数, [ i + 1 , n ] [i+1,n] [i+1,n] 的数 a a a 属性都大于 i i i a a a 属性
是造一颗权值线段树,每次把 b b b 属性插进去,版本i表示[1,i]的数的b属性构造出的权值线段树,对于第 i i i 个数,从 [ 1 , i − 1 ] [1,i-1] [1,i1] 中选出 c / 2 − 1 c/2-1 c/21 b b b 属性之和,使他最小,表示构造目标序列 [ 1 , c / 2 − 1 ] [1,c/2-1] [1,c/21] 的部分,从 [ i + 1 , n ] [i+1,n] [i+1,n] 中选出 c − c / 2 c-c/2 cc/2 b b b属性之和,使他最小,表示构造目标序列 [ c / 2 + 1 , c ] [c/2+1,c] [c/2+1,c]的部分

#include<bits/stdc++.h>
using namespace std;

#define int long long 

int read(){int x=0,f=1;char c=getchar();
	for(;!isdigit(c);c=getchar()) if(c=='-') f=-1;
	for(;isdigit(c);c=getchar()) x=(x<<3)+(x<<1)+(c^48);
	return x*f;
}

const int N = 3e5+10;
const long long INF = 1e18+10;
int c,n,f,X[N],ans=INF;
struct NODE{
	int score,cost;
}dat[N];
bool cmp(NODE a,NODE b){
	return a.score^b.score?a.score<b.score:a.cost<b.cost;
}


int T[N],trcnt;
struct TREE{
	int val,num,ls,rs;
}t[N*40];
#define ls(x) t[x].ls
#define rs(x) t[x].rs
int Mdfy(int pr,int x,int a,int b,int seat,int add){if(!x) x=++trcnt;
	t[x].val=t[pr].val+add,t[x].num=t[pr].num+1;
	if(!(a^b)) return x;
	int mid=(a+b)>>1;
	if(seat<=mid) ls(x)=Mdfy(ls(pr),ls(x),a,mid,seat,add),rs(x)=rs(pr);
	else rs(x)=Mdfy(rs(pr),rs(x),mid+1,b,seat,add),ls(x)=ls(pr);
	return x;
}

int Qry(int pr,int x,int a,int b,int k){
	if(!(a^b)) return k*X[a];int Lsum=t[ls(pr)].num-t[ls(x)].num;
	int mid=(a+b)>>1;
	if(Lsum>=k) return Qry(ls(pr),ls(x),a,mid,k);
	else return t[ls(pr)].val-t[ls(x)].val+Qry(rs(pr),rs(x),mid+1,b,k-Lsum);
}

signed  main(){
	c=read(),n=read(),f=read();
	for(int i=1;i<=n;++i) dat[i].score=read(),X[i]=dat[i].cost=read();
	sort(X+1,X+n+1);int Tail=unique(X+1,X+n+1)-X-1;
	sort(dat+1,dat+n+1,cmp);
    int hlf=c>>1;
	for(int i=1;i<=n;++i){
		int k=lower_bound(X+1,X+Tail+1,dat[i].cost)-X;
		T[i]=Mdfy(T[i-1],T[i],1,Tail,k,dat[i].cost);
	}
	for(int i=hlf+1;i<=n-hlf;++i){
		int k1=Qry(T[i-1],T[0],1,Tail,hlf);
		int k2=Qry(T[n],T[i],1,Tail,hlf);
		if(k1+k2+dat[i].cost<=f) ans=i;
	}
	if(ans>n) puts("-1");
	else printf("%lld",dat[ans].score);
	return 0;
}

P2839 [国家集训队]middle

简述:

左端点在 [ a , b ] [a,b] [a,b] ,右端点在 [ c , d ] [c,d] [c,d] ,其构成的区间中的数最大中位数

思路:

这是一道类似的题,我们可以想到二分的一个经典操作,当选取到 m i d mid mid,判断他是中位数时,把小于 m i d mid mid 的数都赋值 − 1 -1 1 ,把大于等于 m i d mid mid 的数赋值为 1 1 1 ,那么如果求得的和大于等于 0 0 0,表示 m i d mid mid 还可以再大,不然就小

我们用主席树维护当每一个数成为如果成为 m i d mid mid ,其对应的 + 1 +1 +1 − 1 -1 1 的情况
具体来说,当 m i d mid mid是递增时,我们每次最多只要修改一个点,把他从 + 1 +1 +1,改成 − 1 -1 1,于是我们先把输入进来的数排序,把 − 1 -1 1 插入排序后该数的位置,这里是为了 m i d mid mid 递增,而把位置当做了下标。
查询 [ l , r ] [l,r] [l,r] 关于 m i d mid mid 的值时,只要查询 m i d mid mid等效(这个二分出来的值原来可能没出现过,但是必定有一个输入进来的数所构成的树与其等效)的树中 [ l , r ] [l,r] [l,r] 的和,就是其区间和,判断这大于等于0。

由于 [ b + 1 , c − 1 ] [b+1,c-1] [b+1,c1] 的区间是必选的,我们只要用相同的套路加上前后缀,主席树是给二分提供服务的,我们要二分最大的中位数,要尽可能保证和大于等于 0 0 0,需要支持查询 [ c , d ] [c,d] [c,d]的最大后缀, [ a , b ] [a,b] [a,b]的最大前缀。

可以尝试自己去码, 因为我代码丑,不贴

more??

P3402 可持久化并查集

这就是一道模板了,我们看看哪里可以可持久化

简述

如题

我们发现路径压缩并查集是不可能可持久化的,因为每次把路径压缩,虽然得到了飞快的速度,但破坏了原有形态。

想到启发式合并,对于一个并查集,每次把深度较小并查集 X X X 连向深度较大的并查集 Y Y Y ,这样就可以保证深度是 l o g log log 级别的,而且每次的更改是改变一个 f a ( X ) = f a ( Y ) fa(X)=fa(Y) fa(X)=fa(Y) ,我们可以把 f a fa fa数组进行可持久化,每次使用并查集的时候把某版本的 f a fa fa值调用出来。
至于为什么启发式合并是 l o g log log 的,感性理解一下,只有当前并查集深度为 x x x合并到深度同为 x x x的并查集上,深度才会 + 1 +1 +1

#include<bits/stdc++.h>
using namespace std;
const int N = 1e5+10;
const int M = N*100;

int n,m,cnt;
int trcnt,T[2][N];
struct TREE{
	int val,ls,rs;
}dep[M],fa[M];

void Build(int &x,int a,int b,int data,TREE t[]){
	if(!x) x=++trcnt;
	if(a==b){t[x].val=(data==0)?a:1;return;}
	int mid=a+b>>1;
	Build(t[x].ls,a,mid,data,t);
	Build(t[x].rs,mid+1,b,data,t);
}
void Updata(int P,int &x,int a,int b,int seat,int data,TREE t[]){
	if(!x) x=++trcnt;
	t[x]=t[P];
	if(a==b){t[x].val=data;return;}
	int mid=a+b>>1;
	if(seat<=mid){
		t[x].ls=0;
		Updata(t[P].ls,t[x].ls,a,mid,seat,data,t);
	}
	else{
		t[x].rs=0;
		Updata(t[P].rs,t[x].rs,mid+1,b,seat,data,t);
	}
}
int Query(int x,int a,int b,int data,TREE t[]){
	if(a==b) return t[x].val;
	int mid=a+b>>1;
	if(data<=mid) return Query(t[x].ls,a,mid,data,t);
	else return Query(t[x].rs,mid+1,b,data,t);
}

int find(int x){
	int v=Query(T[0][cnt-1],1,n,x,fa);
	if(x!=v) return find(v);
	return x;
}

void merge(int x,int y){
	x=find(x);
	y=find(y);
	int depx=Query(T[1][cnt-1],1,n,x,dep),depy=Query(T[1][cnt-1],1,n,y,dep);
	if(depx<depy) swap(x,y),swap(depx,depy);
	Updata(T[0][cnt-1],T[0][cnt],1,n,y,x,fa);
	Updata(T[1][cnt-1],T[1][cnt],1,n,x,max(depx,depy+1),dep);
}

int main(){
	scanf("%d%d",&n,&m);
	Build(T[0][0],1,n,0,fa);
	Build(T[1][0],1,n,1,dep);
	for(cnt=1;cnt<=m;++cnt){
		int opt;
		scanf("%d",&opt);
		if(opt==1){
			int x,y;
			scanf("%d%d",&x,&y);
			merge(x,y);
		}
		else if(opt==2){
			int pnt;
			scanf("%d",&pnt);
			T[0][cnt]=T[0][pnt],T[1][cnt]=T[1][pnt];
		}
		else{
			int x,y;
			scanf("%d%d",&x,&y);
			if(find(x)==find(y)) printf("1\n");
			else printf("0\n");
			T[0][cnt]=T[0][cnt-1],T[1][cnt]=T[1][cnt-1];
	}
	return 0;
}

由于这篇文写得有点长了,简述就免了。

more???

CF1000F One Occurrence
简述

按照哈哈的项链,先处理出 p r e pre pre 数组, p r e [ x ] pre[x] pre[x] 表示 x x x 上一次出现的位置
那么问题就转化为求取 x ∈ [ l , r ] x∈[l,r] x[l,r] ,满足 x x x 是最靠右的且 p r e [ x ] < l pre[x]<l pre[x]<l,此时, x x x 必然是 [ l , r ] [l,r] [l,r] 中唯一的一个数
然后建树,考虑给右端点建树,把对应位置存入主席树里,维护 p r e [ x ] pre[x] pre[x] 值,由于我们要求最靠右的,所以每次加新点把上一个位置存储的信息清空就行
查询区间的时候把r的树取出来,把这里面 [ l , r ] [l,r] [l,r] 的子区间全部找出来,最多 2 ∗ l o g ( 5 e 5 ) 2*log(5e5) 2log(5e5) 个,对其中每个子区间直接暴力求取合法的答案,若合法再求个答案,只有单个子区间是 l o g log log ,单次查询复杂度 l o g 2 ( 5 e 5 ) log_2(5e5) log2(5e5)

#include<bits/stdc++.h>
using namespace std;

int read(){int x=0,f=1;char c=getchar();
	for(;c<'0'||c>'9';c=getchar()) if(c=='-') f=-1;
	for(;c>='0'&&c<='9';c=getchar()) x=(x<<3)+(x<<1)+(c^48);
	return x*f;
}
const int N = 5e5+10;
const int L = 20;
const int INF = 1e9+10;
const int qwq = 0;
const int qvq = 1;
const int MaxDat = 5e5;

int n,m,pre[N],dat[N];

#define pii pair<int,int>
#define fi first
#define se second
int trcnt,T[N];
struct TREE{
	int ls,rs;
	pii val;
}t[N*L*2];
#define ls(x) t[x].ls
#define rs(x) t[x].rs
void Push_up(int x){t[x].val=min(t[ls(x)].val,t[rs(x)].val);}
int Mdfy(int pr,int x,int a,int b,int seat,int lim){
	if(!x) x=++trcnt,t[x].val.fi=INF;
	if(!(a^b)) return t[x].val.fi=lim,t[x].val.se=a,x;
	int mid=a+b>>1;
	if(seat<=mid) ls(x)=Mdfy(ls(pr),qwq,a,mid,seat,lim),rs(x)=rs(pr);
	else rs(x)=Mdfy(rs(pr),qwq,mid+1,b,seat,lim),ls(x)=ls(pr);
	Push_up(x);
	return x;
}
int Qry(int x,int a,int b,int L,int R){
	if(L<=a&&b<=R) return t[x].val.fi<L?t[x].val.se:INF;
	int mid=a+b>>1,res=INF;
	if(mid>=L) res=min(res,Qry(ls(x),a,mid,L,R)); 
	if(res!=INF) return res;
	if(mid+1<=R) res=min(res,Qry(rs(x),mid+1,b,L,R));
	return res;
}

signed main(){n=read();
	for(int i=1;i<=n;++i){dat[i]=read();int tmp;
		if(pre[dat[i]]) tmp=Mdfy(T[i-1],qwq,1,MaxDat,pre[dat[i]],INF);
		else tmp=T[i-1];
		T[i]=Mdfy(tmp,qwq,1,MaxDat,i,pre[dat[i]]);
		pre[dat[i]]=i;
	}
	m=read();
	for(int i=1;i<=m;++i){int l=read(),r=read();
		int k=Qry(T[r],1,MaxDat,l,r);
		printf("%d\n",dat[k==INF?0:k]);
	}
	return 0;
}

more????

论思路之拓宽:CF522D Closest Equals

对于区间最近相同数的距离,在预处理的时候只需要考虑相邻两个相同的数的距离,将其预处理出来
预处理 l s t lst lst 数组, l s t [ x ] lst[x] lst[x] 表示 x x x 上一次的位置,上一次和这一次同时出现在一个区间里,那么 L < = l s t [ x ] < = x < = R L<=lst[x]<=x<=R L<=lst[x]<=x<=R
考虑主席树,第 i i i 棵树把所有 l s t [ x ] > = i lst[x]>=i lst[x]>=i 的信息储存在对应位置,权值为最近的距离,那么在查找 [ L , R ] [L,R] [L,R] 时,查询第 L L L 棵树的 [ 1 , R ] [1,R] [1,R] 位置上距离最小的即可
由于主席树节约空间把上一棵树拿来用,这题要满足 l s t [ x ] > = L lst[x]>=L lst[x]>=L 后信息才能被插入到 L L L 中,按照 n − > 1 n->1 n>1的顺序按照倒着建树即可

#include<bits/stdc++.h>
using namespace std;
const int INF = 1e9+10;
int read(){
	int x=0,f=1;char c=getchar();
	for(;!isdigit(c);c=getchar()) if(c=='-') f=-1;
	for(;isdigit(c);c=getchar()) x=(x<<3)+(x<<1)+(c^48);
	return x*f;
}
const int N = 5e5+10;
const int L = 20;
int n,m,dat[N],X[N],pre[N];
int trcnt,T[N];
struct TREE{
	int minn,ls,rs;
}t[N*L];
#define ls(x) t[x].ls
#define rs(x) t[x].rs
vector<int>G[N];
void Push_up(int x){
	int res=INF;
	if(ls(x)) res=min(res,t[ls(x)].minn);
	if(rs(x)) res=min(res,t[rs(x)].minn);
	t[x].minn=res;
}
int Mdfy(int Pre,int x,int a,int b,int seat,int DAT){
	if(!x) x=++trcnt;
	if(!(a^b)) return t[x].minn=DAT,x;
	int mid=a+b>>1;
	if(seat<=mid) ls(x)=Mdfy(ls(Pre),ls(x),a,mid,seat,DAT),rs(x)=rs(Pre);
	else rs(x)=Mdfy(rs(Pre),rs(x),mid+1,b,seat,DAT),ls(x)=ls(Pre);
	Push_up(x);
	return x;
}

int Qry(int x,int a,int b,int L,int R){
	if(!x) return INF;
	if(L<=a&&b<=R) return t[x].minn;
	int mid=a+b>>1,res=INF;
	if(mid>=L) res=min(res,Qry(ls(x),a,mid,L,R));
	if(mid+1<=R) res=min(res,Qry(rs(x),mid+1,b,L,R));
	return res;
}

signed main(){
	n=read(),m=read();
	for(int i=1;i<=n;++i) dat[i]=read(),X[i]=dat[i];
	sort(X+1,X+n+1);
	int Tail=unique(X+1,X+n+1)-X-1;
	for(int i=1,tmp;i<=n;++i){
		tmp=lower_bound(X+1,X+Tail+1,dat[i])-X;
		if(pre[tmp]) G[pre[tmp]].push_back(i),dat[i]=i-pre[tmp];
		pre[tmp]=i;
	}
	for(int i=n;i>=1;--i){
		if(!G[i].size()){T[i]=T[i+1];continue;}
		int lst=T[i+1],prs=0;
		for(vector<int>::iterator it=G[i].begin();it!=G[i].end();++it){
			int gr=(*it);
			prs=Mdfy(lst,prs,1,n,gr,dat[gr]);
			swap(lst,prs);
		}
		T[i]=lst;
	}
	for(int i=1;i<=m;++i){
		int l=read(),r=read();
		int ans=Qry(T[l],1,n,1,r);
		if(ans==INF) puts("-1");
		else printf("%d\n",ans);
	}
	return puts(""),0;
}

还有骚操作

我们逐渐意识到主席树有一种连续性的限制,如果需要第 l l l 棵和第 r r r 棵进行操作,必须满足 l l l~ r r r的中间构成都是合法的
于是来一道经典题:P4197 Peaks

这一看,发现和主席树搭边的就只是第 k k k 小了,这怎么搞呢,就需要用到 k r u s c a l kruscal kruscal 重构树

kruscal重构树

这是一种性质极好的算法,具体了解可以问度娘,这里简述一下。
每次把权值最小的边连向的两个点集拎出来,把这两个点集连到一个新的点上,该点的权值是比边权,用并查集把这些点全部归到新点的点集里。

这样有什么用呢,我们发现对于星(新=星= ∗ * ,谐音梗)点,如果当前查询的值大于星点的权值,那么星点的子树里的点我都可以走到。反过来看,如果从询的叶子节点往上跳,查询值大于等于权值就跳,那么我跳到的点的整颗子树的点我都可以走到。

更棒的什么呢,由于重构树是树形,代表叶子节点(原始节点)是顺序是按dfs序固定的,那么也就代表,我们的询问在重构树求得的真正区间一定是连续一段,连续,也就代表可以构成可持久化。
T a l k Talk Talk i s is is c h e e p : cheep: cheep:

#include<bits/stdc++.h>
using namespace std;

int read(){int x=0,f=1;char c=getchar();
	for(;c<'0'||c>'9';c=getchar()) if(c=='-') f=-1;
	for(;c>='0'&&c<='9';c=getchar()) x=(x<<3)+(x<<1)+(c^48);
	return x*f;
}


const int N = 1e6+10;
int n,m,q;
int h[N<<1],dif[N<<1],L[N<<1],R[N<<1],X[N],f[N<<1][25];
struct EDGE{
	int f,t,val;
	bool operator <(EDGE x)const{
		return val<x.val;
	}
}e[N];
struct Graph{
	int hd[N],nxt[N],to[N],val[N],cnt;
	void add(int f,int t){
		nxt[++cnt]=hd[f],to[cnt]=t,hd[f]=cnt;
	}	
}G;

int trcnt,T[N];
struct TREE{
	int ls,rs,num;
}t[N*23];
#define ls(x) t[x].ls
#define rs(x) t[x].rs
const int qwq = 0;
int Mdfy(int pr,int x,int a,int b,int seat){if(!x) x=++trcnt;
	t[x].num=t[pr].num+1;
	if(a^b){int mid=a+b>>1;
		if(seat<=mid) ls(x)=Mdfy(ls(pr),qwq,a,mid,seat),rs(x)=rs(pr);
		else rs(x)=Mdfy(rs(pr),qwq,mid+1,b,seat),ls(x)=ls(pr);
	}
	return x;
}
int Qry(int lft,int rht,int a,int b,int k){if(k<=0) return 0;
	if(!(a^b)) return a;
	int mid=a+b>>1,rsum=t[rs(rht)].num-t[rs(lft)].num;
	if(k>rsum) return Qry(ls(lft),ls(rht),a,mid,k-rsum);
	else return Qry(rs(lft),rs(rht),mid+1,b,k);
}
int num=0;
void Dfs(int u){
	for(int i=1;i<=20;++i) f[u][i]=f[f[u][i-1]][i-1];
	if(u<=n){int t=lower_bound(X+1,X+n+1,h[u])-X;
		L[u]=R[u]=++num;
		T[num]=Mdfy(T[num-1],qwq,1,n,t);
		return;
	}
	for(int i=G.hd[u],gr;i;i=G.nxt[i]){
		Dfs(gr=G.to[i]);
		L[u]=min(L[u],L[gr]);
		R[u]=max(R[u],R[gr]);
	}
}

int fa[N];
int Getf(int x){
	if(fa[x]^x) fa[x]=Getf(fa[x]);
	return fa[x];
}
const int INF = 1e9+10;
void Rebuild(){int cnt=n;
	sort(e+1,e+m+1);
	for(int i=1;i<=n;++i) fa[i]=i;
	for(int i=1;i<=m;++i){int u=Getf(e[i].f),v=Getf(e[i].t);
		if(u^v){
			dif[++cnt]=e[i].val;//按重构树的叶子结点顺序排序,那么查询得到的区间必然是连续的
			fa[u]=fa[v]=fa[cnt]=cnt;
			G.add(cnt,v),G.add(cnt,u);
			f[u][0]=f[v][0]=cnt;
		}
	}
	for(int i=1;i<=cnt;++i) L[i]=INF,R[i]=-INF;
	sort(X+1,X+n+1);
	Dfs(cnt);
}

int Findroot(int u,int lim){
	for(int i=20;~i;--i)
		if(f[u][i]&&dif[f[u][i]]<=lim) 
			u=f[u][i];
	return u;
}

signed main(){n=read(),m=read(),q=read();
	for(int i=1;i<=n;++i) X[i]=h[i]=read();	
	for(int i=1;i<=m;++i) e[i].f=read(),e[i].t=read(),e[i].val=read();
	Rebuild();
	for(int i=1;i<=q;++i){int v=read(),x=read(),k=read();
		int t=Findroot(v,x);
		int ans=R[t]-L[t]+1>=k?X[Qry(T[L[t]-1],T[R[t]],1,n,k)]:-1;
		printf("%d\n",ans);
	}
	return 0;
}

【双倍经验】

P4899 [IOI2018] werewolf 狼人

道理是一样的,需要建两棵重构树。

骚操作2:

P4137 Rmq Problem / mex

这道题绝对是小清新题,我最开始用莫队过的,我仔细一想,发现好像可以用主席树,一看题解,真可以。
因为要求 m e x mex mex,我们先把所有数据 + 1 +1 +1 0 0 0 一起看做数据(这个地方我卡了 0.5 h 0.5h 0.5h ,我给忘了
然后对于每棵权值线段树,我们维护每个位置(权值)最后出现的位置(原数列位置)最小是多少。对于区间 [ l , r ] [l,r] [l,r],在第 r r r棵树里找值小于 l l l的,如果当前区间里的这个最小值 < l <l <l ,说明该区间(权值)至少有一个数不是属于 [ l , r ] [l,r] [l,r] 区间(原数列位置)的。

T a l k Talk Talk i s is is i m p o r t a n t important important

#include<bits/stdc++.h>
using namespace std;
int read(){int x=0,f=1;char c=getchar();
	for(;c<'0'||c>'9';c=getchar()) if(c=='-') f=-1;
	for(;c>='0'&&c<='9';c=getchar()) x=(x<<3)+(x<<1)+(c^48);
	return x*f;
}

const int N = 2e5+10;
const int MaxDat = 2e5+10;
int n=read(),m=read(),a[N],X[N<<2],cntX;
int T[N],trcnt;
struct NODE{
	int ls,rs,dat;
}t[MaxDat*20];
#define ls(x) t[x].ls
#define rs(x) t[x].rs
int Modify(int Pre,int a,int b,int seat,int dat){
	int x=++trcnt,mid=(a+b)>>1;
	if(a==b) return t[x].dat=dat,x;
	if(seat<=mid) ls(x)=Modify(ls(Pre),a,mid,seat,dat),rs(x)=rs(Pre);
	else rs(x)=Modify(rs(Pre),mid+1,b,seat,dat),ls(x)=ls(Pre);
	t[x].dat=min(t[ls(x)].dat,t[rs(x)].dat);
	return x;
}
int Qry(int x,int a,int b,int cut){
	if(!x||a==b) return a;
	int mid=(a+b)>>1;
	if(t[ls(x)].dat<cut) return Qry(ls(x),a,mid,cut);
	else return Qry(rs(x),mid+1,b,cut);
}
signed main() {
	X[++cntX]=0;
	for(int i=1;i<=n;++i){
		a[i]=read();
		X[++cntX]=a[i],X[++cntX]=a[i]+1;
	}
	sort(X+1,X+cntX+1);
	int Tail=unique(X+1,X+cntX+1)-X-1;
	cntX=Tail;
	for(int i=1;i<=n;++i)
		T[i]=Modify(T[i-1],1,cntX,lower_bound(X+1,X+cntX+1,a[i])-X,i);
	for(int i=1;i<=m;++i){
		int l=read(),r=read();
		printf("%d\n",X[Qry(T[r],1,cntX,l)]);
	}
	return 0;
}


骚操作3

P2633 Count on a tree

这真的骚到我了,但也是常规题目,维护第k小就表明这个可持久化是有可减性的,那么我们只要用差分的套路,
u − > v u->v u>v路径实际 = T r e e [ u ] + T r e e [ v ] − T r e e [ l c a ] − T r e e [ f a ( l c a ) ] =Tree[u]+Tree[v]-Tree[lca]-Tree[fa(lca)] =Tree[u]+Tree[v]Tree[lca]Tree[fa(lca)]
传4个变量确实挺不戳的

#include<bits/stdc++.h>
using namespace std;

#define I int  
#define F(a,b,c) for(register int a=b;a<=c;++a) 
#define ll long long
//#define I long long
I R(){int x=0,f=1;char c=getchar();
	for(;c<'0'||c>'9';c=getchar()) if(c=='-') f=-1;
	for(;c>='0'&&c<='9';c=getchar()) x=(x<<3)+(x<<1)+(c^48);
	return x*f;
}


const int N = 2e5+10;
const int L = 17;
I n,m,a[N],X[N],ans,Tail;
I trcnt,T[N];
struct TREE{
	I ls,rs,num;
}t[N*L];
#define ls(x) t[x].ls
#define rs(x) t[x].rs
I Mdfy(I pr,I a,I b,I s){I x=++trcnt,mid=a+b>>1;
	t[x].num=t[pr].num+1;
	if(!(a^b)) return x;
	if(s<=mid) ls(x)=Mdfy(ls(pr),a,mid,s),rs(x)=rs(pr);
	else rs(x)=Mdfy(rs(pr),mid+1,b,s),ls(x)=ls(pr);
	return x;
}
I Qry(I l,I r,I y,I z,I a,I b,I k){I mid=a+b>>1;
	if(!(a^b)) return a;
	I sum=t[ls(l)].num+t[ls(r)].num-t[ls(y)].num-t[ls(z)].num;
	if(sum>=k) return Qry(ls(l),ls(r),ls(y),ls(z),a,mid,k);
	else return Qry(rs(l),rs(r),rs(y),rs(z),mid+1,b,k-sum);
}

struct Graph{
	I to[N],nxt[N],hd[N],dep[N],fa[N][L],cnt;
	void Adde(I f,I t){nxt[++cnt]=hd[f],to[cnt]=t,hd[f]=cnt;}
	#define Fe(x,y) for(int z=hd[x],y=to[z];z;y=to[z=nxt[z]])
	void Dfs(I u,I f){
		dep[u]=dep[f]+1,fa[u][0]=f;
		T[u]=Mdfy(T[f],1,Tail,a[u]);
		F(i,1,L-1) fa[u][i]=fa[fa[u][i-1]][i-1];
		Fe(u,gr)if(f^gr)Dfs(gr,u);
	}
	I LCA(I x,I y){
		if(dep[x]<dep[y]) swap(x,y);
		for(I i=L-1;~i;--i) 
			if(dep[fa[x][i]]>=dep[y]) 
				x=fa[x][i];
		if(!(x^y)) return x;
		for(I i=L-1;~i;--i) 
			if(fa[x][i]^fa[y][i]) 
				x=fa[x][i],y=fa[y][i];
		return fa[x][0];
	}
}G;

signed main(){n=R(),m=R();
	F(i,1,n) X[i]=a[i]=R();
	sort(X+1,X+n+1);Tail=unique(X+1,X+n+1)-X-1;
	F(i,1,n) a[i]=lower_bound(X+1,X+Tail+1,a[i])-X;
	F(i,1,n-1){I u=R(),v=R();
		G.Adde(u,v),G.Adde(v,u);
	}G.Dfs(1,0);
	F(i,1,m){I u=R()^ans,v=R(),k=R(),lca=G.LCA(u,v);
		printf("%d\n",ans=X[Qry(T[u],T[v],T[lca],T[G.fa[lca][0]],1,Tail,k)]);
	}
	return 0;
}

我到底该维护什么?-CF1422F Boring Queries

b o r i n g boring boring了。

简述

[ l , r ] [l,r] [l,r]内的最小公倍数

我们知道,很多个数的最小公倍数就相当于每个数都质因数分解后,对于每一个质数,取质因数数量的最大作为最小公倍数的那个质因数数量,那么这就是最小公倍数
$lcm=\prod\limits_{i=1}^{num} {prime[i]* \max\limits_{j=1}^{n}{primenum[j][i]}} $

我们可以考虑根号分块,一个数 x x x能表示成很多质数相乘,但大于 x \sqrt x x 的质数最多只能存在一个,所以我们可以考虑枚举 x \sqrt x x 以内的质数,(一种做法是单调栈,我用的是 s t st st 表),这很好计算,比如用 s t [ k ] [ i ] [ j ] st[k][i][j] st[k][i][j]维护第 k k k个质数在 [ i , i + j 2 − 1 ] [i,i+j^2-1] [i,i+j21]中出现的最大次数,这样的质数好像有450个左右,空间贼大, s t st st 表甚至要用 c h a r char char ,对于大于 x \sqrt x x 的质数。

用主席树维护其是否出现过,如果出现过,那么也只能出现一次,可以仿照之前HH的项链那种做法,只对它进行一次贡献的计算,但最麻烦的是 % \% %运算,因为最小公倍数太大,需要 % 1 e 9 + 7 \%1e9+7 %1e9+7,所以我们不能用 r r r 的子树除以 l l l 的子树(ps:其实好像求个逆元就行了,怕常数太大超时)。

其实我们要统计的东西是 [ l , r ] [l,r] [l,r] p r e [ x ] < l − 1 pre[x]<l-1 pre[x]<l1 的数的乘积,只是传统插值我们会遇到重复插的问题,以前是可减,的现在不能直接除,我们反过来考虑,查询第 l − 1 l-1 l1的树,在里面找合法的 [ l , r ] [l,r] [l,r]的数的乘积,那么我这第 l − 1 l-1 l1棵树里需要插入的就是$ lst[x]<l-1$ 的数,把他们插到对应的原序列位置上,我维护乘积就行

T a l k Talk Talk i s is is n e c e s s a r y : necessary: necessary:

using namespace std;
typedef long long ll;
#define pii pair<int,int>
#define mp make_pair
#define fi first
#define se second
const long long mod1 = 1e9+7;
int read(){int x=0,f=1;char c=getchar();
	for(;c<'0'||c>'9';c=getchar()) if(c=='-') f=-1;
	for(;c>='0'&&c<='9';c=getchar()) x=(x<<3)+(x<<1)+(c^48);
	return x*f;
}
const int N = 1e5+2;
const int D = 2e5+2;
const int M = 88;
const int L = 18;
const int Lim = 87;
int n,m;
bool isprime[1000];int dat[N],prime[100],cntP;
void Euler(int x){
	memset(isprime,true,sizeof(isprime));
	isprime[1]=false;
	for(int i=2;i<=x;++i){
		if(isprime[i]) prime[++cntP]=i;
		for(int j=1;j<=cntP&&i*prime[j]<=x;++j){
			isprime[prime[j]*i]=false;
			if(i%prime[j]==0) break;
		}
	}
}

int Quick_Pow(int x,int k){
	int res=1;
	for(;k;k>>=1,x=x*x%mod1)
		if(k&1) 
			res=res*x%mod1;
	return res;
}

int T[N],trcnt;
struct TREE{
	int ls,rs;
	ll mul=1ll;
}t[N*L];
#define ls(x) t[x].ls
#define rs(x) t[x].rs
int Mdfy(int Pre,int x,int a,int b,int seat,int dat){
	if(!x) x=++trcnt;
	t[x].mul=t[Pre].mul;
	if(!(a^b)) return t[x].mul=t[x].mul*1ll*dat%mod1,x;
	int mid=(a+b)>>1;
	if(seat<=mid) ls(x)=Mdfy(ls(Pre),ls(x),a,mid,seat,dat),rs(x)=rs(Pre);
	else rs(x)=Mdfy(rs(Pre),rs(x),mid+1,b,seat,dat),ls(x)=ls(Pre);
	t[x].mul=t[ls(x)].mul*t[rs(x)].mul%mod1;
	return x;
}
ll Qry(int x,int a,int b,int L,int R){
	if(!x) return 1;
	if(L<=a&&b<=R) return t[x].mul;
	int mid=(a+b)>>1;ll res=1;
	if(mid>=L) res=Qry(ls(x),a,mid,L,R)%mod1;
	if(mid+1<=R) res=res*Qry(rs(x),mid+1,b,L,R)%mod1;
	return res;
}

int pre[D];
vector<pii> pos[N];
void Pre_(){
	for(int i=1;i<=n;++i){
		int x=dat[i];
		pos[pre[x]].push_back(mp(i,x));
		pre[x]=i;
	}
	for(int i=0;i<=n;++i){
		if(pos[i].size()){
			int Now=0,Pre=0;
			if(i>=1) Pre=T[i-1];
			for(vector<pii>::iterator it=pos[i].begin();it!=pos[i].end();++it){
				T[i]=Now=Mdfy(Pre,Now,1,n,(*it).fi,(*it).se);
				swap(Now,Pre);
			}
		}
		else T[i]=T[i-1];
	}
}



char f[M][N][L];int Pow[M][20],lg[N];
void Pre(){
	for(int i=1;i<=Lim;++i){
		for(int j=1;j<=20;++j)
			Pow[i][j]=Quick_Pow(prime[i],j);
		Pow[i][0]=1;
	}
	for(int i=2;i<=n;++i) lg[i]=lg[i>>1]+1;
	for(int k=1;k<=Lim;++k){
		for(int j=1;j<=lg[n];++j){
			for(int i=1;i+(1<<j)-1<=n;++i){
				f[k][i][j]=max(f[k][i][j-1],f[k][i+(1<<(j-1))][j-1]);
			}
		}
	}	
}
int Query(int k,int l,int r){
	int p=lg[r-l+1];
	return max(f[k][l][p],f[k][r-(1<<p)+1][p]);
}

signed main(){
	n=read();
	Euler(450);
	for(int i=1;i<=n;++i){
		int x=read();
		for(int j=1;j<=Lim;++j)
			for(;!(x%prime[j]);x/=prime[j]) 
				++f[j][i][0];
		dat[i]=x;
	}
	Pre_();
	Pre();
	m=read();ll ans=0;
	for(register int i=1;i<=m;++i){
		int l=(ans+read())%n+1,r=(ans+read())%n+1;
		if(l>r) swap(l,r);
		ans=1;
		for(register int j=1;j<=Lim;++j) 
			ans=ans*1ll*(Pow[j][Query(j,l,r)])%mod1;
		ans=(ans*1ll*Qry(T[l-1],1,n,l,r))%mod1;
		printf("%lld\n",ans);		
	}
	return 0;
}
这和主席树有什么关系?-P3293 [SCOI2016]美味

这道题极好,建议别看题解,自己做做

讲讲思路,这是一个经典的贪心,如果我们希望亦或出来的值最大,我们尽可能的要从高位到低位将他按位取反,然后我们发现这样从高位到低位的二分,不正是一半一半的缩短范围。

我们的答案是一位一位推出来的,我们记录 a n s ans ans 为已经推得的部分,如果当前在处理第 d d d 位,如果 d d d 1 1 1 ,那么ans最终的范围在 [ a n s + ( 1 < < d ) , a n s + ( 1 < < ( d + 1 ) ) − 1 ] [ans+(1<<d),ans+(1<<(d+1))-1] [ans+(1<<d),ans+(1<<(d+1))1],前者为 a n s + ( 10000.. ) 2 ans+(10000..)_2 ans+(10000..)2后者为 a n s + ( 11111.. ) 2 ans+(11111..)_2 ans+(11111..)2,同理,如果 d d d取0,那么 a n s ans ans 范围在 [ a n s , a n s + ( 1 < < d ) − 1 ] [ans,ans+(1<<d)-1] [ans,ans+(1<<d)1],我们只需要一个能帮助我们查询数列 [ a , b ] [a,b] [a,b]所构成的权值线段树在 [ l , r ] [l,r] [l,r]的权值范围内是否有数存在就行,如果存在,那么 $ans+ (1<<d) $ 表示这一位取到,不然就不变,表示这一位没取到

T a l k Talk Talk i s is is n e c e s s a r y necessary necessary

#include<bits/stdc++.h>
#define ls(x) t[x].ls
#define rs(x) t[x].rs
using namespace std;
const int maxn = 2e5+10;
const int maxq = 1e5+10;
const int maxd = 1e5+10;

inline int read(){
	int x=0,f=1;char c=getchar();
	while(c<'0'||c>'9'){if(c=='-') f=-1;c=getchar();}
	while(c>='0'&&c<='9'){x=(x<<3)+(x<<1)+(c^48);c=getchar();}
	return x*f;
}

int n,m;
int T[maxn],trcnt;
struct TREE{
	int num,ls,rs;
}t[maxn*50];
void Updata(int &P,int &x,int a,int b,int data){
	if(!P) P=++trcnt;
	if(!x) x=++trcnt;
	t[x]=t[P],++t[x].num;
	if(a==b) return;
	int mid=a+b>>1;
	if(data<=mid){
		ls(x)=0;
		Updata(ls(P),ls(x),a,mid,data);
	}
	else{
		rs(x)=0;
		Updata(rs(P),rs(x),mid+1,b,data);
	}
}

int Query(int &P,int &x,int a,int b,int L,int R){
	if(L>R||a>b) return 0;
	if(!P) P=++trcnt; if(!x) x=++trcnt;
	if(L<=a&&b<=R) return t[x].num-t[P].num;
	int mid=a+b>>1,res=0;
	if(L<=mid) res+=Query(ls(P),ls(x),a,mid,L,R);
	if(R>=mid+1) res+=Query(rs(P),rs(x),mid+1,b,L,R);
	return res;
}

int main(){
	n=read(),m=read();
	for(int i=1;i<=n;++i){
		int x=read();
		Updata(T[i-1],T[i],0,maxd,x);
	}
	for(int i=1;i<=m;++i){
		int b=read(),x=read(),l=read(),r=read(),ans=0;
		for(int j=18;j>=0;--j){
			int d=0;
			if(((1<<j)&b)&&!Query(T[l-1],T[r],0,maxd,ans-x,ans-x+(1<<j)-1)) 
				ans+=(1<<j);
			if(!((1<<j)&b)&&Query(T[l-1],T[r],0,maxd,ans-x+(1<<j),ans-x+(1<<(j+1))-1))
				ans+=(1<<j);
		}
		printf("%d\n",ans^b);
	}
	return 0;
} 

误区

这里有个大误区,就是很多人说有一种叫做“树状数组套主席树”的东西
我在这里辟个谣,其实那玩意儿就是普通的树状数组套权值线段树,只是因为单点修改,程序长得像的不得了,然后就混为一谈了。

具体理论应该是每个点维护对应管辖区间所组成的权值线段树,因为树状数组的特性,不论是修改还是查询,一个区间都可以被分成 l o g n logn logn个点分别存储,所以不论是查询还是修改,都把那 l o g n logn logn个点弄出来,把他们的权值线段树都做对应的操作就行了

还是找道题贴代码:

P2617 Dynamic Rankings

$Code: $

#include<bits/stdc++.h>
using namespace std;
int read(){int x=0,f=1;char c=getchar();
	for(;c<'0'||c>'9';c=getchar()) if(c=='-') f=-1;
	for(;c>='0'&&c<='9';c=getchar()) x=(x<<3)+(x<<1)+(c^48);
	return x*f;
}

const int N = 2e5+10;
int n,m,len,dat[N],X[N];
struct NODE{
	bool opt;
	int l,r,k,pos,y;
}Ques[N];

int trcnt,T[N];
struct TREE{
	int num,ls,rs;
}t[N*400];
#define ls(x) t[x].ls
#define rs(x) t[x].rs
int lowbit(int x){return x&-x;}
int Mdfy(int x,int a,int b,int seat,int add){
	if(!x) x=++trcnt;
	t[x].num+=add;
	if(!(a^b)) return x;
	int mid=a+b>>1;
	if(seat<=mid) ls(x)=Mdfy(ls(x),a,mid,seat,add);
	else rs(x)=Mdfy(rs(x),mid+1,b,seat,add);
	return x;
}
void PreMdfy(int x,int add){
	int k=lower_bound(X+1,X+len+1,dat[x])-X;
	for(int i=x;i<=n;i+=lowbit(i)) 
		T[i]=Mdfy(T[i],1,len,k,add);
}
int tmp[2][N],cnt[2];
int Qry(int a,int b,int k){
	if(!(a^b)) return a;
	int mid=a+b>>1,sum=0;
	for(int i=1;i<=cnt[1];++i) sum+=t[ls(tmp[1][i])].num;
	for(int i=1;i<=cnt[0];++i) sum-=t[ls(tmp[0][i])].num;
	if(k<=sum){
		for(int i=1;i<=cnt[1];++i) tmp[1][i]=ls(tmp[1][i]);
		for(int i=1;i<=cnt[0];++i) tmp[0][i]=ls(tmp[0][i]);
		return Qry(a,mid,k);
	}
	else{
		for(int i=1;i<=cnt[1];++i) tmp[1][i]=rs(tmp[1][i]);
		for(int i=1;i<=cnt[0];++i) tmp[0][i]=rs(tmp[0][i]);
		return Qry(mid+1,b,k-sum);
	}
}
int PreQry(int l,int r,int k){
	cnt[0]=cnt[1]=0;
	for(int i=r;i;i-=lowbit(i)) tmp[1][++cnt[1]]=T[i];//取出r的那些权值线段树
	for(int i=l-1;i;i-=lowbit(i)) tmp[0][++cnt[0]]=T[i];//取出l-1的那些权值线段树
	return Qry(1,len,k);
}
signed main(){
	n=read(),m=read();
	for(int i=1;i<=n;++i) dat[i]=read(),X[++len]=dat[i];
	for(int i=1;i<=m;++i){char c;cin>>c;
		if(c=='Q'){
			int l=read(),r=read(),k=read();
			Ques[i].opt=true,Ques[i].l=l,Ques[i].r=r,Ques[i].k=k;
		}
		else{
			int pos=read(),u=read();
			X[++len]=u;
			Ques[i].opt=false,Ques[i].pos=pos,Ques[i].y=u;
		}
	}
	sort(X+1,X+len+1);
	int Tail=unique(X+1,X+len+1)-X-1;len=Tail;
	for(int i=1;i<=n;++i) PreMdfy(i,1);
	for(int i=1;i<=m;++i){
		if(Ques[i].opt){
			printf("%d\n",X[PreQry(Ques[i].l,Ques[i].r,Ques[i].k)]);
		}	
		else{
			PreMdfy(Ques[i].pos,-1);
			dat[Ques[i].pos]=Ques[i].y;
			PreMdfy(Ques[i].pos,1);
		}
	}
	return 0;
}

然后您就入门成功啦~ ( ๑ • ˋ ㅂ • ˊ ) و ✧ (๑•̀ㅂ•́)و✧ (ˋˊ)و

一些练习题:

P7577 「PMOI-3」简单模拟题

毒瘤题,我最开始连二分都没想到(出题人好像还和某篇题解吵了起来)

P4559 [JSOI2018]列队

列队绿色小清新

P4592 [TJOI2018]异或

可持久化trie?

P4735 最大异或和

我仔细一想,这玩意儿真的是可持久化trie

P2048 [NOI2010] 超级钢琴

可以练练手,但我建议用题解的那种方法做

P5283 [十二省联考 2019] 异或粽子

和上面的一题一样

P3567 [POI2014]KUR-Couriers

无脑题

P4094 [HEOI2016/TJOI2016]字符串

二分套SA套主席树,大常数,大容量,我不会

P5385 [Cnoi2019]须臾幻境

经典套lct,码量居然比上面那题小。

P7357 「PMOI-1」中位数

这题我也没做过,看上去不错

P6214 「SWTR-04」Taking a Walk

这是一道经典的不可做题,只是个摆设罢了

夹带私货

绘图工具 draw.io/diagrams

一个极适合 o i oi oi 的绘图工具,比一般的画图软件精美,也没有一些画图那么繁杂
本文所有图片都是用这个画的

发现

洛谷的图片空间不够用怎么办, L o f t e r Lofter Lofter c s d n csdn csdn,知乎,百度贴吧等网站的图好能直接引用,博客园炸了,不知道为什么

一些话

快去学莫队吧, h h hh hh

这篇文大概写了我 6 , 7 h 6,7h 6,7h,整理了我可持久化数组基本所有的题解,挺开心的。

写在最后

谢谢你 5 r 5r 5r

− B u t -But But j u s t just just f o r for for t o d a y today today,$ we$ c a n can can h a r m o n i z e harmonize harmonize

− − e n d d d d --end_{d_{d_{d}}} endddd

转载自未闻何灯盏狂拂浅

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值