有趣的线段树模板合集(线段树,最短/长路,单调栈,线段树合并,线段树分裂,树上差分,Tarjan-LCA,势能线段树,李超线段树)

线段树分裂

以某个键值为中点将线段树分裂成左右两部分,应该类似Treap的分裂吧(我菜不会Treap)。一般应用于区间排序。

方法很简单,就是把分裂之后的两棵树的重复的\(\log\)个节点新建出来,单次时间复杂度严格\(O(\log n)\)

至于又有合并又有分裂的复杂度,蒟蒻一直不会比较有说服力的证明,直到看见SovietPower巨佬的题解

对于只有合并:合并两棵线段树的过程,是找到它们\(x\)个重合的节点的位置,并将它们合并,而对于不重合的节点会跳过。

注意到合并与分裂类似互逆过程,也就是说可以看做是删掉了这\(x\)个节点。

所以可以得出,时间复杂度上界,等于被删去的节点数的上界,不大于若干线段树最开始的节点数。

那么,对于一些既有合并又有分裂的题目,复杂度也是可以分析滴!

\(n\)棵线段树初始有\(O(n\log n)\)的节点,每一次分裂只会新增\(O(\log n)\)的节点

于是总点数就是\(O((n+m)\log n)\)级别的,线段树合并的总代价就不会超过\(O((n+m)\log n)\)了。

洛谷P2824 [HEOI2016/TJOI2016]排序

如果一个区间有序,那么顺序是唯一的,我们就可以把它们插到一个权值线段树里,记录一下是升序还是降序。区间排序就变成了线段树合并。

但是我们的排序端点可能会落在一个有序区间内,这时候就要拆开。额外用一个set标记已经有序的区间(像珂朵莉树一样),需要拆开时线段树分裂。

突然暂时变成了洛谷rk1

#include<bits/stdc++.h>
#define R register int
#define G if(++ip==ie)if(fread(ip=buf,1,SZ,stdin))
using namespace std;
typedef set<int>::iterator IT;
const int SZ=1<<19,N=1e5+9,M=6e6;
char buf[SZ],*ie=buf+SZ,*ip=ie-1;
inline int in(){
	G;while(*ip<'-')G;
	R x=*ip&15;G;
	while(*ip>'-'){x*=10;x+=*ip&15;G;}
	return x;
}
int p,rt[N],lc[M],rc[M],s[M],o[N];
set<int>t;
void ins(R&x,R l,R r,R k){
	s[x=++p]=1;
	if(l==r)return;
	R m=(l+r)>>1;
	k<=m?ins(lc[x],l,m,k):ins(rc[x],m+1,r,k);
}
int qry(R x,R l,R r){
	if(l==r)return l;
	R m=(l+r)>>1;
	return lc[x]?qry(lc[x],l,m):qry(rc[x],m+1,r);
}
void mer(R&x,R y){//合并
	if(!(x&&y)){x|=y;return;}
	s[x]+=s[y];
	mer(lc[x],lc[y]);
	mer(rc[x],rc[y]);
}
void spl(R&x,R y,R k,R o){//分裂
	if(s[y]==k)return;
	s[x=++p]=s[y]-k;s[y]=k;
	if(o){
		if(k<=s[rc[y]])spl(rc[x],rc[y],k,o),lc[x]=lc[y],lc[y]=0;
		else  spl(lc[x],lc[y],k-s[rc[y]],o);
	}
	else{
		if(k<=s[lc[y]])spl(lc[x],lc[y],k,o),rc[x]=rc[y],rc[y]=0;
		else  spl(rc[x],rc[y],k-s[lc[y]],o);
	}
}
IT Split(R p){//拆区间
	IT i=t.lower_bound(p);
	if(*i==p)return i;
	--i;spl(rt[p],rt[*i],p-*i,o[p]=o[*i]);
	return t.insert(p).first;
}
int main(){
	R n=in(),m=in();
	t.insert(n+1);
	for(R i=1;i<=n;++i)
		ins(rt[i],0,n,in()),t.insert(i);
	while(m--){
		R op=in(),l=in(),r=in();
		IT il=Split(l),ir=Split(r+1);
		for(IT i=++il;i!=ir;++i)mer(rt[l],rt[*i]);
		o[l]=op;t.erase(il,ir);
	}
	R q=in();
	Split(q);Split(q+1);
	printf("%d\n",qry(rt[q],0,n));
	return 0;
}

李超线段树

LiChaoTree,简称LCT,用于维护若干个一次函数的最值

核心思想是标记永久化。线段树每个节点维护在该区间中点取值最大的线段,查询时求一条链上\(log\)个线段的最值。

洛谷P4254 [JSOI2008]Blue Mary开公司

#include<bits/stdc++.h>
#define DB double
#define R register int
#define lc u<<1
#define rc u<<1|1
using namespace std;
const int N=50009;
struct Line{
    DB k,b;
    inline DB operator()(const DB&x){return k*x+b;}
}t[1<<17];
void upd(R u,R l,R r,Line&a){
    R m=(l+r)>>1;
    if(t[u](m)<a(m))swap(t[u],a);
	if(l==r)return;
	a.k<t[u].k?upd(lc,l,m,a):upd(rc,m+1,r,a);
}
DB qry(R u,R l,R r,R x){
    if(l==r)return t[u](x);
    R m=(l+r)>>1;
    return max(x<=m?qry(lc,l,m,x):qry(rc,m+1,r,x),t[u](x));
}
int main(){
    R n;scanf("%d",&n);
    for(R i=1;i<=n;++i){
        char s[9];scanf("%s",s);
        if(s[0]=='P'){
            Line a;
			scanf("%lf%lf",&a.b,&a.k);a.b-=a.k;
            upd(1,1,N,a);
        }
        else{
            R x;scanf("%d",&x);
            printf("%.0lf\n",floor(qry(1,1,N,x))/100);
        }
    }
    return 0;
}

线段树优化连边

某些问题会要你由一个区间的所有点像另一个区间的所有点连一条边。

把线段树的节点看成其对应区间的点的集合。一个给定的区间最多用\(\log\)个线段树节点就可以表示出来,那么我们找到这\(\log\)个节点把边连上就好啦!

注意建两棵线段树,分别对应入边和出边。根据问题类型(最短路/网络流/...)来确定合理的连边方式。

例题:BZOJ 3073【Pa2011】Journeys,权限题,可以到这里交。

小技巧:\(01\)最短路:对于边权只有\(0\)\(1\)的最短路问题,可直接使用双端队列,增广时若边权为\(0\)则丢到队首,否则丢到队尾。

#include<bits/stdc++.h>
#define RG register
#define R RG int
#define G if(++ip==ie)fread(ip=buf,1,N,stdin)
using namespace std;
const int N=5e5+9,M=1e7,S=3e6;
int p,tot,at[N],he[S],ne[M],to[M],d[S];
bool w[M],vis[M];
deque<int>q;
char buf[N],*ie=buf+N,*ip=ie-1;
inline int in(){
	G;while(*ip<'-')G;
	R x=*ip&15;G;
	while(*ip>'-'){x*=10;x+=*ip&15;G;}
	return x;
}
inline void add(R x,R y,R z){
	ne[++p]=he[x];to[he[x]=p]=y;w[p]=z;
}
void build(R x,R l,R r){
	add(2*x,2*(x>>1),0);add(2*(x>>1)|1,2*x|1,0);
	if(l==r){
		add(2*x|1,2*x,0);//出入衔接的边
		tot=max(tot,at[l]=2*x);return;
	}
	R m=(l+r)>>1;
	build(2*x,l,m);build(2*x+1,m+1,r);
}
void upd(R x,R l,R r,R b,R e,R op){
	if(b<=l&&r<=e)return op?add(tot,2*x|1,1):add(2*x,tot,0);
	R m=(l+r)>>1;
	if(b<=m)upd(2*x,l,m,b,e,op);
	if(e>m)upd(2*x|1,m+1,r,b,e,op);
}
int main(){
	R n=in(),m=in(),p=in();
	build(1,1,n);++tot;
	while(m--){
		R a=in(),b=in(),c=in(),d=in();//双向连边
		++tot;upd(1,1,n,a,b,0);upd(1,1,n,c,d,1);
		++tot;upd(1,1,n,c,d,0);upd(1,1,n,a,b,1);
	}
	memset(d+1,127,4*tot);
	d[1]=d[at[p]]=0;
	q.push_back(at[p]);
	while(!q.empty()){//01最短路
		R x=q.front(),y;q.pop_front();
		if(vis[x])continue;vis[x]=1;
		for(R i=he[x];i;i=ne[i])
			if(d[y=to[i]]>d[x]+w[i]){
				d[y]=d[x]+w[i];
				w[i]?q.push_back(y):q.push_front(y);
			}
	}
	for(R i=1;i<=n;++i)
		printf("%d\n",d[at[i]]);
	return 0;
}

线段树维护单调栈

合并两个区间的单调栈时,会有一端被丢弃掉,额外二分出被丢弃那一段的位置。复杂度\(O(n\log^2n)\)

例题:洛谷P4198 楼房重建

#include<bits/stdc++.h>
#define LL long long
#define RG register
#define R RG int
#define G if(++ip==ie)fread(ip=buf,1,N,stdin)
#define lc x<<1
#define rc lc|1
using namespace std;
const int N=1<<18;
char buf[N],*ie=buf+N,*ip=ie-1;
int X,Y,s[N];
double k,mx[N];
inline int in(){
    G;while(*ip<'-')G;
    R x=*ip&15;G;
    while(*ip>'-'){x*=10;x+=*ip&15;G;}
    return x;
}
int qry(R x,R l,R r){
    if(mx[x]<=k)return 0;
    if(l==r)return 1;
    R m=(l+r)>>1;
    if(mx[lc]<k)return qry(rc,m+1,r);
    return qry(lc,l,m)+s[x]-s[lc];
}
void upd(R x,R l,R r){
    if(l==r){
        mx[x]=(double)Y/X;s[x]=1;
        return;
    }
    R m=(l+r)>>1;
    X<=m?upd(lc,l,m):upd(rc,m+1,r);
    mx[x]=max(k=mx[lc],mx[rc]);
    s[x]=s[lc]+qry(rc,m+1,r);//额外二分
}
int main(){
    for(R n=in(),m=in();m;--m){
        X=in();Y=in();
        upd(1,1,n);
        printf("%d\n",s[1]);
    }
    return 0;
}

线段树合并

就是把两个动态开点线段树的信息整体合并。

蒟蒻懒得总结了,放RabbitHu大佬的总结嗯哈一个小兔一个树懒

例题:洛谷P4556 [Vani有约会]雨天的尾巴

主流做法是树剖,据说两个\(\log\),蒟蒻作为G2准AFO选手还不太会就不多提了。

然而这题可以入门线段树合并,复杂度一个\(\log\),于是蒟蒻就写了一下。

对蒟蒻来说少有的1A(有一次傻逼忘加pushup不算吧)

树上差分思想,每个点开一个权值线段树,修改等于在链两端下标\(z\)\(+1\),在\(LCA\)\(LCA\)的父亲处\(-1\),然后自底向上合并。LCA可以直接Tarjan求。

然而要算算空间。动态开点总数\(n\log n\)(认为\(n,m,\)值域同阶),每个点维护左右儿子和区间\(max\),int占4字节,总共快80MB了,再加上邻接表,栈空间什么乱七八糟的,虽说应该勉强不会炸,但也够卡了。

有一个小优化:我们在\(LCA\)\(LCA\)父亲处做减法的时候,对应下标的节点肯定已经存在了。所以做减法的部分直接用vector(蒟蒻手写了链表)存下需要\(-1\)的下标即可。线段树部分的空间马上少了一半,一点也不卡了。

代码短(1.8k),常数有点大,比树剖慢,跟线段树合并的空间复杂度大于树剖不无关系。

#include<bits/stdc++.h>
#define LL long long
#define RG register
#define R RG int
#define G if(++ip==ie)fread(ip=buf,1,SZ,stdin)
#define Pushup mx[x]=max(mx[lc[x]],mx[rc[x]])
using namespace std;
const int SZ=1<<19,N=1e5+1,M=6e5+9,L=3.5e6;
char buf[SZ],*ie=buf+SZ,*ip=ie-1;
int p=1,ehe[N],qhe[N],dhe[N],ne[M],to[M],id[M];bool fl[M];//注意这里的三个表头
int q,rt[N],lc[L],rc[L],mx[L];
int tmp,updv=1,f[N],fa[N],ans[N];
inline int in(){
    G;while(*ip<'-')G;
    R x=*ip&15;G;
    while(*ip>'-'){x*=10;x+=*ip&15;G;}
    return x;
}
inline int gf(R x){//并查集
    return x==f[x]?x:f[x]=gf(f[x]);
}
inline int qry(R x){//找出最大值下标
    R l=0,r=N,m;
    while(l<r){
        m=(l+r)>>1;
        if(mx[lc[x]]>=mx[rc[x]])r=m,x=lc[x];
        else l=m+1,x=rc[x];
    }
    return l;
}
void upd(R&x,R l,R r){//单点更新
    if(!x)x=++q;
    if(l==r){mx[x]+=updv;return;}
    R m=(l+r)>>1;
    tmp<=m?upd(lc[x],l,m):upd(rc[x],m+1,r);
    Pushup;
}
void mer(R x,R y,R l,R r){//线段树合并
    if(l==r){mx[x]+=mx[y];return;}
    R m=(l+r)>>1;
    if(lc[x]&&lc[y])mer(lc[x],lc[y],l,m);
    else lc[x]+=lc[y];
    if(rc[x]&&rc[y])mer(rc[x],rc[y],m+1,r);
    else rc[x]+=rc[y];
    Pushup;
}
void dfs(R x){//一遍dfs一气呵成
    for(R i=ehe[x];i;i=ne[i])
        if(to[i]!=fa[x]){
            fa[to[i]]=x;dfs(to[i]);
            mer(rt[x],rt[to[i]],1,N);
        }
    for(R i=qhe[x];i;i=ne[i])//Tarjan-LCA
        if(fl[i>>1]){//减法直接链表存下标
            ne[++p]=dhe[tmp=gf(to[i])];id[dhe[tmp]=p]=id[i];
            ne[++p]=dhe[tmp=fa[tmp]]  ;id[dhe[tmp]=p]=id[i];
        }
        else fl[i>>1]=1;
    for(R i=dhe[x];i;i=ne[i])//减法开始
        tmp=id[i],upd(rt[x],0,N);
    ans[x]=qry(rt[x]);f[x]=fa[x];
}
int main(){
    R n=in(),m=in(),i,x,y;
    for(i=1;i<n;++i){
        x=in();y=in();
        ne[++p]=ehe[x];to[ehe[x]=p]=y;
        ne[++p]=ehe[y];to[ehe[y]=p]=x;
    }
    for(i=1;i<=n;++i)
        f[i]=i,rt[i]=++q;//蒟蒻的这种merge写法需要根节点非空
    for(i=1;i<=m;++i){
        x=in();y=in();tmp=in();
        upd(rt[x],0,N);upd(rt[y],0,N);
        ne[++p]=qhe[x];to[qhe[x]=p]=y;id[p]=tmp;
        ne[++p]=qhe[y];to[qhe[y]=p]=x;id[p]=tmp;
    }
    updv=-1;dfs(1);
    for(i=1;i<=n;++i)printf("%d\n",ans[i]);
    return 0;
}

区间修改+维护历史最值

update:蒟蒻现在才知道这个和下面一个都是吉老师论文里的东西。

被神仙ZSY在NOIP模(bao)拟(ling)赛里出了出来

洛谷P4314 CPU监控

是个论文题

#include<cstdio>
#define RG register
#define R RG int
#define G c=getchar()
#define lc x<<1
#define rc lc|1
#define Pushup nm[x]=max(nm[lc],nm[rc]),hm[x]=max(hm[lc],hm[rc])
#define Pushdn merge(lc,nl[x],hl[x]),merge(rc,nl[x],hl[x]),nl[x]=hl[x]=NUL;
const int N=4e5,INF=-1e9;
inline int in(){
	RG char G;RG bool f=1;
	while(c<'-')G;
	if(c>'9')return c;
	if(c=='-')f=0,G;
	R x=c&15;G;
	while(c>'-')x=x*10+(c&15),G;
	return f?x:-x;
}
inline int max(R x,R y){
	return x>y?x:y;
}
struct Dat{
	int a,s;
	Dat operator+(RG Dat&x){
		return(Dat){max(INF,a+x.a),max(s+x.a,x.s)};
	}
	Dat operator*(RG Dat&x){
		return(Dat){max(a,x.a),max(s,x.s)};
	}
}t,nl[N],hl[N];
const Dat NUL=(Dat){0,INF};
int op,nm[N],hm[N];
void merge(R x,RG Dat&n,RG Dat&h){
	RG Dat tmp=nl[x]+h;
	hl[x]=hl[x]*tmp;
	nl[x]=nl[x]+n;
	hm[x]=max(hm[x],max(nm[x]+h.a,h.s));
	nm[x]=max(nm[x]+n.a,n.s);
}
void build(R x,R l,R r){
	if(l==r){
		nl[x]=hl[x]=(Dat){nm[x]=hm[x]=in(),INF};
		return;
	}
	nl[x]=hl[x]=NUL;
	R m=(l+r)>>1;
	build(lc,l,m);build(rc,m+1,r);
	Pushup;
}
void upd(R x,R l,R r,R s,R e){
	if(l==s&&r==e){
		merge(x,t,t);
		return;
	}
	Pushdn;
	R m=(l+r)>>1;
	if(e<=m)upd(lc,l,m,s,e);
	else if(s>m)upd(rc,m+1,r,s,e);
	else upd(lc,l,m,s,m),upd(rc,m+1,r,m+1,e);
	Pushup;
}
int qry(R x,R l,R r,R s,R e){
	if(l==s&&r==e)
		return op=='Q'?nm[x]:hm[x];
	Pushdn;
	R m=(l+r)>>1;
	if(e<=m)return qry(lc,l,m,s,e);
	if(s>m) return qry(rc,m+1,r,s,e);
	return max(qry(lc,l,m,s,m),qry(rc,m+1,r,m+1,e));
}
int main(){
	R T=in(),E,x,y;
	build(1,1,T);
	E=in();
	while(E--){
		op=in();x=in();y=in();
		if(op=='Q'||op=='A')
			printf("%d\n",qry(1,1,T,x,y));
		else{
			t=op=='P'?(Dat){in(),INF}:(Dat){INF,in()};
			upd(1,1,T,x,y);
		}
	}
	return 0;
}

势能线段树

网上看到势能线段树和吉利线段树这两个名词,不知如何区分,求大佬指教qwq

感觉就是数列分块5一样(跳转数列分块总结),需要满足所有的修改都会使势能单调下降。

另有题解

BZOJ5312 冒险
BZOJ4695 最假女选手
洛谷P4891 序列

比如说区间开方,每个数最多被开方\(\log\log a\)次。

那么如果区间都是\(1\),就给区间打一个标记,以后不用跳到这里来了。

LOJ6281 数列分块入门 5
洛谷P4145 上帝造题的七分钟2 / 花神游历各国
洛谷SP2713 GSS4 - Can you answer these queries IV

三个题是一样的,代码是GSS4的。

#include<bits/stdc++.h>
#define RG register
#define R RG int
#define G if(++ip==ie)if((ie=buf+fread(ip=buf,1,N,stdin))<buf+N)*ie++=EOF;
using namespace std;
typedef long long LL;
const int N=1<<18;
char buf[N],*ie=buf+N,*ip=ie-1;
LL s[N];
bool f[N];
inline LL in(){
    G;while(*ip<'-'&&*ip!=EOF)G;
    if(*ip==EOF)return -1;
    RG LL x=*ip&15;G;
    while(*ip>'-'){x*=10;x+=*ip&15;G;}
    return x;
}
#define lc x<<1
#define rc lc|1
#define Pushup f[x]=f[lc]&f[rc],s[x]=s[lc]+s[rc]
void build(R x,R l,R r){
    if(l==r){
        f[x]=(s[x]=in())<2;
        return;
    }
    R m=(l+r)>>1;
    build(lc,l,m);build(rc,m+1,r);
    Pushup;
}
void upd(R x,R l,R r,R b,R e){
    if(f[x])return;
    if(l==r){
        f[x]=(s[x]=sqrt(s[x]))<2;
        return;
    }
    R m=(l+r)>>1;
    if(e<=m)upd(lc,l,m,b,e);
    else if(b>m)upd(rc,m+1,r,b,e);
    else upd(lc,l,m,b,m),upd(rc,m+1,r,m+1,e);
    Pushup;
}
LL qry(R x,R l,R r,R b,R e){
    if(l==b&&r==e)return s[x];
    R m=(l+r)>>1;
    if(e<=m)return qry(lc,l,m,b,e);
    if(b>m)return qry(rc,m+1,r,b,e);
    return qry(lc,l,m,b,m)+qry(rc,m+1,r,m+1,e);
}
int main(){
    R n,op,l,r;
    for(R t=1;~(n=in());++t){
        printf("Case #%d:\n",t);
        build(1,1,n);
        for(R m=in();m;--m){
            op=in();l=in();r=in();
            if(l>r)swap(l,r);
            if(op)printf("%lld\n",qry(1,1,n,l,r));
            else upd(1,1,n,l,r);
        }
        puts("");
    }
    return 0;
}
posted @ 2018-09-15 15:54  Flash_Hu  阅读(4523)  评论(17编辑  收藏  举报