线段树,合并!

线段树,合并!

是啥#

合并两颗动态开点线段树。

比方说我要合并两颗维护区间和的线段树,那么我就这样写:

int merge(int x,int y,int s,int t){
	if(!x||!y)return x|y;
	if(s==t){
		tr[x].val+=tr[y].val;
		return x;
	}
	int mid=(s+t)>>1;
	tr[x].l=merge(tr[x].l,tr[y].l,s,mid);
	tr[x].r=merge(tr[x].r,tr[y].r,mid+1,t);
	pushup(x);
	return x;
}

这样单次合并的复杂度是 O() 的,就是两棵线段树重叠部分的大小。

就是这样。应用即可。

P4556 [Vani有约会] 雨天的尾巴 /【模板】线段树合并#

每个节点开一个线段树,以类型为下标,进行树上差分即可。

对于一个询问,线段树二分即可。

在一棵树上做这样的线段树合并的总复杂度是 O(nlogn) 的,所以这样的总复杂度是 O(nlogn) 的。

void pushup(int p){
	tr[p].val=max(tr[tr[p].l].val,tr[tr[p].r].val);
}
void update(int pos,int s,int t,int &p,int val){
	//简单的单点加
	if(!p)p=++cnt;
	if(pos<=s&&t<=pos){
		tr[p].val+=val;
		return;
	}
	int mid=(s+t)>>1;
	if(pos<=mid)update(pos,s,mid,tr[p].l,val);
	else update(pos,mid+1,t,tr[p].r,val);
	pushup(p);
}
int query(int s,int t,int p){
	//简单的线段树二分
	if(s==t)return s;
	int mid=(s+t)>>1;
	if(tr[tr[p].l].val==tr[p].val)return query(s,mid,tr[p].l);
	else return query(mid+1,t,tr[p].r);
}
int merge(int x,int y,int s,int t){
	//线段树合并
	if(!x||!y)return x|y;
	if(s==t){
		tr[x].val+=tr[y].val;
		return x;
	}
	int mid=(s+t)>>1;
	tr[x].l=merge(tr[x].l,tr[y].l,s,mid);
	tr[x].r=merge(tr[x].r,tr[y].r,mid+1,t);
	pushup(x);
	return x;
}
void init(int now){
	//初始化,预处理倍增数组
	for(int i=1;i<20;i++)fa[now][i]=fa[fa[now][i-1]][i-1];
	dep[now]=dep[fa[now][0]]+1;
	for(int nxt:g[now]){
		if(nxt==fa[now][0])continue;
		fa[nxt][0]=now;
		init(nxt);
	}
}
int lca(int x,int y){
	//简单的lca
}
void dfs(int now){
	//遍历这棵树,做线段树合并
	for(int nxt:g[now]){
		if(nxt==fa[now][0])continue;
		dfs(nxt);
		rt[now]=merge(rt[now],rt[nxt],0,maxw);
	}
	ans[now]=query(0,maxw,rt[now]);
}
signed main(){
	n=read(),m=read();
	for(int i=1;i<n;i++){
		int u=read(),v=read();
		g[u].push_back(v);
		g[v].push_back(u);
	}
	init(1);
	for(int i=1;i<=m;i++){
		int u=read(),v=read(),w=read();
		//树上差分
		update(w,0,maxw,rt[u],1);
		update(w,0,maxw,rt[v],1);
		int l=lca(u,v);
		update(w,0,maxw,rt[l],-1);
		if(fa[l][0])update(w,0,maxw,rt[fa[l][0]],-1);
	}
	dfs(1);
	for(int i=1;i<=n;i++)cout<<ans[i]<<'\n';
	return 0;
}

P3521 [POI2011] ROT-Tree Rotations#

显然,子树的答案不影响,所以每个点分别贪心即可。

我们考虑两个子树哪个放在左边。

我们在每个节点开一个值域线段树,维护子树中每个值的出现次数,支持查询区间和。

显然,我们可以分别遍历两个子树,找出两种方案的新增逆序对数。当然,遍历一棵子树也可以。只要查询另一棵子树中比我的值大的和小的即可。

对于每一个节点,我们选择遍历它较小的儿子。这样的查询的总复杂度是和启发式合并一样的。

所以这样的总复杂度是 O(nlog2n) 的。

#include<bits/stdc++.h>
#define ll long long
using namespace std;
inline int read(){
	int ans=0;bool op=0;char ch=getchar();
	while(ch<'0'||'9'<ch){if(ch=='-')op=1;ch=getchar();}
	while('0'<=ch&&ch<='9'){ans=(ans<<1)+(ans<<3)+(ch^48);ch=getchar();}
	if(op)return -ans;
	return ans;
}
const int maxn=5e5+10;
int n;
int v[maxn],l[maxn],r[maxn],nd=0;
int rt[maxn],nt=0;
int siz[maxn];
ll ans=0;
struct node{
	int val,l,r;
}tr[maxn*20];
void input(int now){
	//递归读入/tuu
	v[now]=read();
	if(!v[now]){
		l[now]=++nd;
		input(l[now]);
		r[now]=++nd;
		input(r[now]);
	}
}
void add(int pos,int s,int t,int &p){
	//单点加
}
int query(int l,int r,int s,int t,int p){
	//区间查询
}
int merge(int x,int y,int s,int t){
	//线段树合并
}
pair<ll,ll> get(int now,int c){
	//遍历子树,进行查询
	if(v[now])return make_pair(query(1,v[now]-1,1,n,c),query(v[now]+1,n,1,n,c));
	pair<ll,ll> la=get(l[now],c);
	pair<ll,ll> ra=get(r[now],c);
	return make_pair(la.first+ra.first,la.second+ra.second);
}
void dfs(int now){
	//遍历这棵树,做线段树合并和启发式遍历
	if(v[now]){
		siz[now]=1;
		add(v[now],1,n,rt[now]);
		return;
	}
	dfs(l[now]);
	dfs(r[now]);
	siz[now]=siz[l[now]]+siz[r[now]]+1;
	if(siz[l[now]]>siz[r[now]])swap(l[now],r[now]);
	pair<ll,ll>tmp=get(l[now],rt[r[now]]);
	ans+=min(tmp.first,tmp.second);
	rt[now]=merge(rt[now],rt[l[now]],1,n);
	rt[now]=merge(rt[now],rt[r[now]],1,n);
}
signed main(){
	//简简单单主函数
	n=read();
	input(++nd);
	dfs(1);
	cout<<ans;
	return 0;
}

CF932F Escape Through Leaf 题解#

一眼斜率优化。

但是单调队列不能合并,平衡树过于狗屎。

于是我们用李超树

然后就做完了。

struct seg{
	//一个线段
	int k,b;
	seg(int kk=0,int bb=inf){
		k=kk,b=bb;
	}
};
struct node{
	//一个李超树节点
	int l,r;
	seg v;
}tr[maxn*20];
int rt[maxn],cn;
int f(int x,seg l){
	//计算线段的值
	return l.k*x+l.b;
}
void upd(int s,int t,int &p,seg u){
	//全局加入线段
	if(!p)p=++cn;
	seg &v=tr[p].v;
	int mid=(s+t)>>1;
	if(f(mid,v)>f(mid,u))swap(u,v);
	if(s==t)return;
	if(f(s,u)<f(s,v))upd(s,mid,tr[p].l,u);
	if(f(t,u)<f(t,v))upd(mid+1,t,tr[p].r,u);
}
int qry(int pos,int s,int t,int p){
	//查询最小值
	if(!p)return inf;
	if(s==t)return f(pos,tr[p].v);
	int mid=(s+t)>>1;
	int ans=f(pos,tr[p].v);
	if(pos<=mid)ans=min(ans,qry(pos,s,mid,tr[p].l));
	else ans=min(ans,qry(pos,mid+1,t,tr[p].r));
	return ans;
}
void update(int &p,seg u){
	//减少函数调用的代码复杂度
	u.b=u.b-lim*u.k;
	upd(0,lim<<1,p,u);
}
int query(int p,int x){
	//减少函数调用的代码复杂度
	return qry(x+lim,0,lim<<1,p);
}
int merge(int x,int y,int s,int t){
	//李超树合并
	if(!x||!y)return x|y;
	if(s==t){
		if(f(s,tr[x].v)>f(s,tr[y].v))tr[x].v=tr[y].v;
		return x;
	}
	int mid=(s+t)>>1;
	tr[x].l=merge(tr[x].l,tr[y].l,s,mid);
	tr[x].r=merge(tr[x].r,tr[y].r,mid+1,t);
	upd(s,t,x,tr[y].v);
	return x;
}
void dfs(int now,int fa){
	//遍历这棵树
	for(int nxt:g[now]){
		if(nxt==fa)continue;
		dfs(nxt,now);
		rt[now]=merge(rt[now],rt[nxt],0,lim<<1);
	}
	if(g[now].size()>1||!fa)dp[now]=query(rt[now],a[now]);
	update(rt[now],(seg){b[now],dp[now]});
}
signed main(){
	//简简单单主函数
	n=read();
	for(int i=1;i<=n;i++)a[i]=read();
	for(int i=1;i<=n;i++)b[i]=read();
	for(int i=1;i<n;i++){
		int u=read(),v=read();
		g[u].push_back(v);
		g[v].push_back(u);
	}
	dfs(1,0);
	for(int i=1;i<=n;i++)cout<<dp[i]<<' ';
	return 0;
}

P5298 [PKUWC2018] Minimax#

fi,j 为节点 i 出现 j 的概率。

考虑三种情况:

  1. 当前点是叶子
    显然,fi,w=1
  2. 当前点只有一个儿子
    显然,直接继承儿子的 f 即可。
  3. 当前点有两个儿子
    这种情况似乎有些麻烦。

我们来推推柿子。

如果一个值要作为最大值出现,那么要让这个儿子中出现,且另一个子树中没有比它大的。其他几种情况也是同理。

所以柿子就是:

fi,j=fl,j(pik=1j1fr,k+(1pi)k=j+1mfr,k)+fr,j(pik=1j1fl,k+(1pi)k=j+1mfl,k)

非常的优美,不是吗?

我们可以给每个节点 i 开一个线段树,以 j 为下标,维护 fi,j

容易发现这个东西是不可以直接合并的,因为这个东西会和前缀和和后缀和有关。

所以我们在合并的时候维护前缀和和后缀和就可以了。

struct node{
	int l,r;
	int val,mul;
	node(){
		l=r=0;
		val=0;
		mul=1;
	}
}tr[maxn*20];
struct genshin{
	//不要在意变量名
	//这个结构体用于维护前缀和、后缀和什么的
	int p,xp,xs,yp,ys;
	genshin add(int axp,int axs,int ayp,int ays){
		return (genshin){p,(xp+axp)%mod,(xs+axs)%mod,(yp+ayp)%mod,(ys+ays)%mod};
	}
};
int rt[maxn],cn;
void lsh(){
	//简简单单离散化
	mp.push_back(0);
	for(int i=1;i<=n;i++)if(!ls[i])mp.push_back(p[i]);
	sort(mp.begin(),mp.end());
	mp.erase(unique(mp.begin(),mp.end()),mp.end());
	for(int i=1;i<=n;i++)if(!ls[i])p[i]=lower_bound(mp.begin(),mp.end(),p[i])-mp.begin();
}
void pushup(int p){
	tr[p].val=(tr[tr[p].l].val+tr[tr[p].r].val)%mod;
}
void pushdown(int p){
	//乘法标记下放
	if(tr[p].l)tr[tr[p].l].val=tr[tr[p].l].val*tr[p].mul%mod,tr[tr[p].l].mul=tr[tr[p].l].mul*tr[p].mul%mod;
	if(tr[p].r)tr[tr[p].r].val=tr[tr[p].r].val*tr[p].mul%mod,tr[tr[p].r].mul=tr[tr[p].r].mul*tr[p].mul%mod;
	tr[p].mul=1;
}
void ins(int pos,int s,int t,int &p){
	//单点插入元素
	if(!p)p=++cn;
	if(s==t){
		tr[p].val=1;
		return;
	}
	pushdown(p);
	int mid=(s+t)>>1;
	if(pos<=mid)ins(pos,s,mid,tr[p].l);
	else ins(pos,mid+1,t,tr[p].r);
	pushup(p);
}
int merge(int x,int y,int s,int t,genshin bas){
	//线段树合并
	if(!x){
		tr[y].val=tr[y].val*((bas.p*bas.xp)%mod+((1-bas.p+mod)*bas.xs)%mod)%mod;
		tr[y].mul=tr[y].mul*((bas.p*bas.xp)%mod+((1-bas.p+mod)*bas.xs)%mod)%mod;
		return y;
	}
	if(!y){
		tr[x].val=tr[x].val*((bas.p*bas.yp)%mod+((1-bas.p+mod)*bas.ys)%mod)%mod;
		tr[x].mul=tr[x].mul*((bas.p*bas.yp)%mod+((1-bas.p+mod)*bas.ys)%mod)%mod;
		return x;
	}
	//标记下放
	pushdown(x),pushdown(y);
	int mid=(s+t)>>1;
	int xl=tr[tr[x].l].val,yl=tr[tr[y].l].val;
	tr[x].l=merge(tr[x].l,tr[y].l,s,mid,bas.add(0,tr[tr[x].r].val,0,tr[tr[y].r].val));
	tr[x].r=merge(tr[x].r,tr[y].r,mid+1,t,bas.add(xl,0,yl,0));
	pushup(x);
	return x;
}
int inv(int x){
	//逆元,只能用到一次
	int ans=1;
	for(int j=mod-2;j;j>>=1){
		if(j&1)ans=(ans*x)%mod;
		x=(x*x)%mod;
	}
	return ans;
}
int getans(int s,int t,int p){
	//计算答案
	if(!p)return 0;
	if(s==t)return s*tr[p].val%mod*tr[p].val%mod*(mp[s]%mod)%mod;
	int mid=(s+t)>>1;
	pushdown(p);
	int ans=0;
	ans+=getans(s,mid,tr[p].l);
	ans+=getans(mid+1,t,tr[p].r);
	return ans%mod;
}
void dfs(int now){
	//遍历这棵树,做线段树合并
	if(!ls[now])ins(p[now],1,lim,rt[now]);
	else if(!rs[now]){
		dfs(ls[now]);
		rt[now]=rt[ls[now]];
	}
	else{
		dfs(ls[now]);
		dfs(rs[now]);
		rt[now]=merge(rt[ls[now]],rt[rs[now]],1,lim,(genshin){p[now]*inv(10000)%mod,0,0,0,0});
	}
}
signed main(){
	//简简单单主函数
	n=read();
	for(int i=1;i<=n;i++)fa[i]=read();
	for(int i=1;i<=n;i++)p[i]=read();
	for(int i=2;i<=n;i++){
		if(ls[fa[i]])rs[fa[i]]=i;
		else ls[fa[i]]=i;
	}
	lsh();
	dfs(1);
	cout<<getans(1,lim,rt[1]);
	return 0;
}

CF911G Mass Change Queries#

考虑合并操作怎么做。

我们对于每个颜色开一个线段树。

容易发现,只要定位到修改区间对应的节点,然后直接合并即可。

复杂度玄学,应该是均摊的,能过。

void ins(int pos,int s,int t,int &p){
	//插入元素
	if(!p)p=++cn;
	if(s==t){
		++tr[p].val;
		return;
	}
	int mid=(s+t)>>1;
	if(pos<=mid)ins(pos,s,mid,tr[p].l);
	else ins(pos,mid+1,t,tr[p].r);
}
int merge(int x,int y,int s,int t){
	//线段树合并
	if(!x||!y)return x|y;
	int mid=(s+t)>>1;
	tr[x].l=merge(tr[x].l,tr[y].l,s,mid);
	tr[x].r=merge(tr[x].r,tr[y].r,mid+1,t);
	return x;
}
void change(int l,int r,int s,int t,int &x,int &y){
	//把y变成x
	if(!y)return;
	if(!x)x=++cn;
	if(l<=s&&t<=r){
		x=merge(x,y,s,t),y=0;
		return;
	}
	int mid=(s+t)>>1;
	if(l<=mid)change(l,r,s,mid,tr[x].l,tr[y].l);
	if(mid<r)change(l,r,mid+1,t,tr[x].r,tr[y].r);
}
void getans(int s,int t,int p,int val){
	//简简单单求答案
	if(!p)return;
	if(s==t){
		a[s]=val;
		return;
	}
	int mid=(s+t)>>1;
	getans(s,mid,tr[p].l,val);
	getans(mid+1,t,tr[p].r,val);
}
signed main(){
	//简简单单主函数
	n=read();
	for(int i=1;i<=n;i++)a[i]=read();
	for(int i=1;i<=n;i++)ins(i,1,n,rt[a[i]]);
	q=read();
	for(int i=1;i<=q;i++){
		int l=read(),r=read(),x=read(),y=read();
		if(x==y)continue;
		change(l,r,1,n,rt[y],rt[x]);
	}
	for(int i=1;i<=100;i++)getans(1,n,rt[i],i);
	for(int i=1;i<=n;i++)cout<<a[i]<<' ';
	return 0;
}

P6773 [NOI2020] 命运#

fi,j 表示,vi 子树中的、未被满足的、u 最深的限制的深度为 j 的方案数。

显然,只有最深的 u 会产生贡献。

转移考虑枚举当前边是 1 还是 0

式子就是:

fu,i=j=0depufu,ifv,j+j=0ifu,ifv,j+j=0i1fu,jfv,i

这个时候设 gu,i 表示 j=0ifu,i,则转移可以写成这样:

fu,i=fu,i(gv,depu+gv,i)+gu,i1fv,i

来自Karry5307 的博客

然后我们线段树合并优化转移即可,类似于P5298 [PKUWC2018] Minimax

最后#

posted @   洛谷Augury  阅读(37)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!
more_horiz
keyboard_arrow_up light_mode palette
选择主题
menu
点击右上角即可分享
微信分享提示