线段树各类应用总结

image

修改操作可以很简单的在线段树上打标记即可。

常规做法直接二分 R 然后区间查询 gcd,复杂度是仨log。

upded:其实也是俩log,线段树查询区间gcd是单 log。

注意到你会将区间拆分成 log 个子区间,直接查询他们的 gcd 即可,直接查询为什么不会多乘个 log 呢。

注意到对两个数 x,y 做 gcd 分为两种情况。

1.一个是另一个的倍数,此时做 gcd 是 O(1)

2.不存在一个是另一个的倍数,此时他们的 gcd 至少为两者较小的那个数的一半,最坏情况也就是两者为斐波那契数列的相邻两项,一次gcd要log,但是以后每一次gcd就会变成 O(1),所以均摊下来查询的时候不会直接多乘个 log

高端的做法是,先转换求最小的 R 满足区间 gcd k

维护一个 cur 表示当前的 gcd,从左往右找到线段树上左端点 L 的区间。

如果区间 gcd 和 cur 的gcd 大于 k,就继续往右找下一个区间。

否则就在当前区间递归,先考虑左儿子,如果gcd 大于 k 就向右儿子递归,否则向左儿子递归。

每次向右找区间最坏情况是找了 log 个区间,然后只会进入一个区间进行递归,递归复杂度也是单 log,再带上 gcd 的复杂度就是俩 log

例题二:

P5537 【XR-3】系统设计

妙妙题。介绍一下单 log 做法。

首先我们观察到树形态不变,那么我们可以快速求出根到每个点的序列值,通过差分也就可以知道任意祖孙点对中间的序列值。

我们需要快速查询一个序列值是否出现过,自然也就想到哈希。

哈希也是支持快速合并的,也就满足序列上的单点修改性质。

那么现在也就变成了查询最大的 r ,满足 LrR 满足根到 x 的哈希值加上 [L,r] 的哈希值出现过。

显然 r 是满足单调性的,就可以线段树二分 (不懂为什么其他题解的线段树二分为啥是俩 log),现在对单 log 做法进行讲解。

首先从左往右找到所有满足 LlrR 的 log 个极大子区间,再记录一个 cur 变量记录当前哈希值。

如果 cur 加上当前区间 [l,r] 的哈希值,如果这个值出现过就加入,然后向后循环。

否则,那么答案 r 一定在 [l,r] 中,直接向下递归,具体就是:

加入左子树的哈希值,如果出现过就加入,递归右子树,否则递归左子树。

那么两部分的复杂度是分开的,都是单 log,那么总复杂度也就是单 log。

注意到这里的哈希值很大,使用 map 会被卡常,建议使用 pb_ds 或者手写哈希表,不会pb_ds的右转 如何优雅地使用 pb_ds

跑得飞快,目前是 rk3,默认 n,m,q 同阶的话,总复杂度为 O(nlogn)

还有不懂的可以看代码,任何问题欢迎与我交流:

#include<bits/stdc++.h>
using namespace std;
#include<ext/pb_ds/assoc_container.hpp>
#include<ext/pb_ds/hash_policy.hpp>
using namespace __gnu_pbds;

typedef long long ll;
typedef unsigned long long ull;
const int N=5e5+3;
int n,m,q,rt,a[N],fa[N];
vector<int>ve[N]; 
ull bas=2e6+3,pri=229,cur=0,pw[N],sx[N],tr[N*4];
cc_hash_table<ull,int>mp;
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<<1)+(x<<3)+(c^48);c=getchar();}
    return x*f;
}
void write(int X)
{
	if(X<0){putchar('-');X=~(X-1);}int s[20],o=0;
	while(X){s[++o]=X%10;X/=10;}
	if(!o)s[++o]=0;while(o)putchar(s[o--]+'0');
	putchar('\n');
}
void Dfs(int x,int fa)
{
	mp[sx[x]]=x;
	for(int i=0,y;i<(ll)ve[x].size();i++)if((y=ve[x][i])!=fa)
	    sx[y]=sx[x]*bas+i+1+pri,Dfs(y,x);
}
#define ls (p<<1)
#define rs (p<<1|1)
#define mi ((l+r)>>1)
void Up(int p,int len){tr[p]=tr[ls]*pw[len]+tr[rs];}
void Build(int p,int l,int r)
{
	if(l==r){tr[p]=a[l]+pri;return;}
	Build(ls,l,mi);Build(rs,mi+1,r);Up(p,r-mi); 
}
void Upd(int k,int p,int l,int r,int d)
{
	if(l==r){tr[p]=d+pri;return;}
	k<=mi?Upd(k,ls,l,mi,d):Upd(k,rs,mi+1,r,d);Up(p,r-mi);
}
int Ans(int p,int l,int r)
{
	if(l==r)return l;
	ull x=cur*pw[mi-l+1]+tr[ls];
	if(mp.find(x)==mp.end())return Ans(ls,l,mi);
	cur=x;return Ans(rs,mi+1,r);
}
int Ask(int L,int R,int p,int l,int r,int &o)
{
	if(L<=l&&r<=R)
	{
		ull x=cur*pw[r-l+1]+tr[p];
		if(mp.find(x)==mp.end()){o=1;return Ans(p,l,r);}
		cur=x;return 0;
	}
	if(L>mi)return Ask(L,R,rs,mi+1,r,o);
	if(R<=mi)return Ask(L,R,ls,l,mi,o);
	int res=Ask(L,R,ls,l,mi,o);
	return o?res:Ask(L,R,rs,mi+1,r,o); 
}
int main()
{
	n=read();m=read();q=read();pw[0]=1;
	for(int i=1;i<N;i++)pw[i]=pw[i-1]*bas;
	for(int i=1;i<=n;i++)
	{
	    fa[i]=read();
		if(!fa[i])rt=i;
		else ve[fa[i]].push_back(i);
	}
	for(int i=1;i<=m;i++)a[i]=read();
	for(int i=1;i<=n;i++)sort(ve[i].begin(),ve[i].end());
	Dfs(rt,0);Build(1,1,m);
	while(q--)
	{
		int op,x,l,r;op=read();
		if(op==2){l=read();x=read();Upd(l,1,1,m,x);continue;}
		x=read();l=read();r=read();cur=sx[x];
		int fl=0;Ask(l,r,1,1,m,fl);write(mp[cur]);
	}
}

线段树维护矩阵

大概只会一个表层。

大多数线段树维护的操作都是进行一个线性变换,常见的有区间加,区间乘之类的。

回归本质,若把一个节点维护的信息看成向量,那么一次操作也就是乘上一个矩阵,进行一次线性变换。

比如说区间加区间求和,叶子节点维护。

[1a]

那么非叶子节点也就是维护

[lenA]

那么一次区间加法也就是找到 [l,r] 所在区间,右乘上:

[10k1]

由于矩阵乘法是有结合律的,可以标记下传以及懒标记维护。

如果我们要再维护一个区间乘法操作呢?右乘上:

[100k]

再强调一边,矩阵乘法具有结合律,直接下放矩阵标记即可。

例题:【HDU多校-2021 第二场 1007】I love data structure

维护两个序列 A,B

支持 A 区间加,B 区间加,将区间的所有 A 变为 3ai+2biB 变为 3ai2bi,区间交换 A,B 数组,区间 aibi 的和。

还是按套路来,叶子向量:

[1ababa2b2]

一般化:

[lenABABA2B2]

A 区间加:

[1k00k2001002k0001k00000100000010000001]

B 区间加:

[10k00k2010k00001002k000100000010000001]

交换 A,B

[100000001000010000000100000001000010]

A 变成 3ai+2biB 变成 3ai2bi

[10000003300002200000001212000999000444]

其他经典例题:大魔法师,比赛……分析类似

大魔法师代码:

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

typedef long long ll;
const ll N=2.5e5,H=998244353;
ll n,m,a[N],b[N],c[N];
struct Mat
{
	ll mat[4][4];
	void Clear(){memset(mat,0,sizeof(mat));}
	friend Mat operator *(const Mat &a,const Mat &b)
	{
		Mat c;c.Clear();
		for(int i=0;i<=3;i++)for(int j=0;j<=3;j++)for(int k=0;k<=3;k++)
		    c.mat[i][j]=(c.mat[i][j]+a.mat[i][k]*b.mat[k][j])%H;
		return c;
	}
	friend Mat operator +(const Mat &a,const Mat &b)
	{
		Mat c;c.Clear();
		for(int i=0;i<=3;i++)for(int j=0;j<=3;j++)c.mat[i][j]=(a.mat[i][j]+b.mat[i][j])%H;
		return c;
	}
	void Init(int x){mat[0][0]=1;mat[0][1]=a[x];mat[0][2]=b[x];mat[0][3]=c[x];}
}tr[N*4],tag[N*4],E={{{1,0,0,0},{0,1,0,0},{0,0,1,0},{0,0,0,1}}};
struct SegmentTree
{
	#define ls (p<<1)
	#define rs (p<<1|1)
	#define mi ((l+r)>>1)
	void Up(int p){tr[p]=tr[ls]+tr[rs];}
	void Build(int p,int l,int r)
	{
		tag[p]=E;
		if(l==r){tr[p].Init(l);return;}
		Build(ls,l,mi);Build(rs,mi+1,r);Up(p); 
	}
	void Add(int p,const Mat &d){tr[p]=tr[p]*d;tag[p]=tag[p]*d;}
	void Down(int p){Add(ls,tag[p]),Add(rs,tag[p]),tag[p]=E;}
	void Upd(int L,int R,int p,int l,int r,const Mat &d)
	{
		if(L<=l&&r<=R){Add(p,d);return;}
		Down(p);
		if(L<=mi)Upd(L,R,ls,l,mi,d);
		if(R>mi)Upd(L,R,rs,mi+1,r,d);
		Up(p);
	}
	Mat Ask(int L,int R,int p,int l,int r)
	{
		if(L<=l&&r<=R)return tr[p];
		Down(p);
		if(L>mi)return Ask(L,R,rs,mi+1,r);
		if(R<=mi)return Ask(L,R,ls,l,mi);
		return Ask(L,R,ls,l,mi)+Ask(L,R,rs,mi+1,r);
	}
}T;
int main()
{
	ios::sync_with_stdio(false),cin.tie(0),cout.tie(0); 
	cin>>n;
	for(int i=1;i<=n;i++)cin>>a[i]>>b[i]>>c[i];
	T.Build(1,1,n);cin>>m;
	while(m--)
	{
		int op,l,r,d;cin>>op>>l>>r;Mat bas=E;
		if(op==1)bas.mat[2][1]=1;
		if(op==2)bas.mat[3][2]=1;
		if(op==3)bas.mat[1][3]=1;
		if(op==4)cin>>d,bas.mat[0][1]=d;
		if(op==5)cin>>d,bas.mat[2][2]=d;
		if(op==6)cin>>d,bas.mat[0][3]=d,bas.mat[3][3]=0;
		if(op<=6){T.Upd(l,r,1,1,n,bas);continue;}
		Mat ans=T.Ask(l,r,1,1,n);
		cout<<ans.mat[0][1]<<" "<<ans.mat[0][2]<<" "<<ans.mat[0][3]<<'\n';
	}
	return 0;
}

NOIP2022 比赛代码:

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

typedef long long ll;
typedef unsigned long long ull;
const int N=2.5e5+3;
int n,m,a[N],b[N],sta[N],stb[N];
ull ans[N];
struct Nod{int x,id;};
vector<Nod>ve[N]; 
//struct Mat
//{
//	ull mat[5][5];
//	void Clear(){memset(mat,0,sizeof(mat));}
//	friend Mat operator *(const Mat &a,const Mat &b)
//	{
//		Mat c;c.Clear();
//		for(int i=0;i<=4;i++)for(int j=0;j<=4;j++)for(int k=0;k<=4;k++)
//		    c.mat[i][j]+=a.mat[i][k]*b.mat[k][j];
//		return c;
//	}
//	friend Mat operator +(const Mat &a,const Mat &b)
//	{
//	    Mat c;c.Clear();
//		for(int i=0;i<=4;i++)for(int j=0;j<=4;j++)c.mat[i][j]=a.mat[i][j]+b.mat[i][j];
//		return c;
//	}
//}E={{{1,0,0,0,0},{0,1,0,0,0},{0,0,1,0,0},{0,0,0,1,0},{0,0,0,0,1}}};
struct Tag
{
	ull s[7];
	void Clear(){memset(s,0,sizeof(s));}
	friend Tag operator *(const Tag &a,const Tag &b)
	{
		Tag c;c.s[0]=a.s[0]+b.s[0];c.s[1]=a.s[1]+b.s[1];c.s[2]=b.s[2]+a.s[0]*b.s[1]+a.s[1]*b.s[0]+a.s[2];
		c.s[3]=b.s[3]+a.s[0]*b.s[4]+a.s[1]*b.s[5]+a.s[2]*b.s[6]+a.s[3];c.s[4]=b.s[4]+a.s[1]*b.s[6]+a.s[4];
		c.s[5]=b.s[5]+a.s[0]*b.s[6]+a.s[5];c.s[6]=a.s[6]+b.s[6];return c;
	}
};
struct Tree
{
	ull s[5];
	void Clear(){memset(s,0,sizeof(s));}
	friend Tree operator +(const Tree &a,const Tree &b)
	{
		Tree c;c.s[0]=a.s[0]+b.s[0];c.s[1]=a.s[1]+b.s[1];c.s[2]=a.s[2]+b.s[2];
		c.s[3]=a.s[3]+b.s[3];c.s[4]=a.s[4]+b.s[4];return c;
	}
	friend Tree operator *(const Tree &a,const Tag &b)
	{
	    Tree c;c.s[0]=a.s[0];c.s[1]=a.s[0]*b.s[0]+a.s[1];c.s[2]=a.s[0]*b.s[1]+a.s[2];
		c.s[3]=a.s[0]*b.s[2]+a.s[1]*b.s[1]+a.s[2]*b.s[0]+a.s[3];
		c.s[4]=a.s[0]*b.s[3]+a.s[1]*b.s[4]+a.s[2]*b.s[5]+a.s[3]*b.s[6]+a.s[4];return c;	
	}
};
struct SegmentTree
{
	Tree tr[N*4];Tag tag[N*4];
	#define ls (p<<1)
	#define rs (p<<1|1)
	#define mi ((l+r)>>1)
	void Up(int p){tr[p]=tr[ls]+tr[rs];}
	void Build(int p,int l,int r)
	{
		tag[p].Clear();tr[p].Clear(); 
		if(l==r){tr[p].s[0]=1;return;}
		Build(ls,l,mi);Build(rs,mi+1,r);Up(p); 
	}
	void Add(int p,const Tag &d){tr[p]=tr[p]*d;tag[p]=tag[p]*d;}
	void Down(int p){Add(ls,tag[p]);Add(rs,tag[p]);tag[p].Clear();}
	void Upd(int L,int R,int p,int l,int r,const Tag &d)
	{
		if(L<=l&&r<=R){Add(p,d);return;}
		Down(p);
		if(L<=mi)Upd(L,R,ls,l,mi,d);
		if(R>mi)Upd(L,R,rs,mi+1,r,d);
		Up(p);
	}
	Tree Ask(int L,int R,int p,int l,int r)
	{
		if(L<=l&&r<=R)return tr[p];
		Down(p);
		if(L>mi)return Ask(L,R,rs,mi+1,r);
		if(R<=mi)return Ask(L,R,ls,l,mi);
		return Ask(L,R,ls,l,mi)+Ask(L,R,rs,mi+1,r); 
	}
}T;
mt19937 rd(time(0));
int main()
{
	ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
	int cas,topa=0,topb=0;cin>>cas>>n;
	for(int i=1;i<=n;i++)cin>>a[i];
	for(int i=1;i<=n;i++)cin>>b[i];
	cin>>m;T.Build(1,1,n);
	for(int i=1,l,r;i<=m;i++)cin>>l>>r,ve[r].push_back({l,i});
	for(int i=1,x;i<=n;i++)
	{
		Tag ka,kb,ks;ka.Clear();kb.Clear();ks.Clear();
		ka.s[0]=a[i];T.Upd(i,i,1,1,n,ka); 
		while(topa&&a[(x=sta[topa])]<a[i])ka.s[0]=a[i]-a[x],T.Upd(sta[topa-1]+1,x,1,1,n,ka),topa--;
		kb.s[1]=b[i];T.Upd(i,i,1,1,n,kb);
		while(topb&&b[(x=stb[topb])]<b[i])kb.s[1]=b[i]-b[x],T.Upd(stb[topb-1]+1,x,1,1,n,kb),topb--;
		ks.s[6]=1;T.Upd(1,i,1,1,n,ks);sta[++topa]=stb[++topb]=i;
		for(Nod t:ve[i])ans[t.id]=T.Ask(t.x,i,1,1,n).s[4];
	}
	for(int i=1;i<=m;i++)cout<<ans[i]<<'\n';
}

常数优化:矩阵乘法的复杂度是大小的三次方,往往是不能忽视掉的。

常见的做法是只维护值会发生改变的位置,如何快速判定哪些位置会有值?直接随机 106 个矩阵乘在一起看哪里有值即可。但是直接维护有值的地方写起来会非常痛苦。

可拓展方向:矩阵乘法不一定是常规矩阵乘法,也可以是什么广义矩阵乘法。

缺陷:矩阵乘法支持的只有两种本质不同的运算而且得二者得满足一些性质,如果有三种运算比如说区间加区间乘区间 max 这种东西就没办法用矩阵快速表示,只有手动拆标记或者维护几种矩阵?

吉老师线段树

维护区间最值以及区间历史最值。

这个就是上述矩阵维护不了但能做的一个例子,常见的有区间对 xmax,区间加,区间求和。

常见方法是维护最小值和次小值,按照情况分类。

假设当前操作对 xmax,最小值为 fi ,次小值 为 se

显然 xfi 时什么也不会发生。

fi<x<se 只有值为最小值的数会发生变化,直接更改即可。

xse 时直接暴力递归下去即可。

吉司机告诉我们,如果没有区间加操作,复杂度是单 log,否则是小常数双 log

其他历史最值操作以及版本和等操作直接上上述矩阵维护即可。

posted @   Hanghang007  阅读(144)  评论(1编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具
点击右上角即可分享
微信分享提示