线段树,合并!
线段树,合并!
是啥#
合并两颗动态开点线段树。
比方说我要合并两颗维护区间和的线段树,那么我就这样写:
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;
}
这样单次合并的复杂度是
就是这样。应用即可。
P4556 [Vani有约会] 雨天的尾巴 /【模板】线段树合并#
每个节点开一个线段树,以类型为下标,进行树上差分即可。
对于一个询问,线段树二分即可。
在一棵树上做这样的线段树合并的总复杂度是
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#
显然,子树的答案不影响,所以每个点分别贪心即可。
我们考虑两个子树哪个放在左边。
我们在每个节点开一个值域线段树,维护子树中每个值的出现次数,支持查询区间和。
显然,我们可以分别遍历两个子树,找出两种方案的新增逆序对数。当然,遍历一棵子树也可以。只要查询另一棵子树中比我的值大的和小的即可。
对于每一个节点,我们选择遍历它较小的儿子。这样的查询的总复杂度是和启发式合并一样的。
所以这样的总复杂度是
#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#
设
考虑三种情况:
- 当前点是叶子
显然, 。 - 当前点只有一个儿子
显然,直接继承儿子的 即可。 - 当前点有两个儿子
这种情况似乎有些麻烦。
我们来推推柿子。
如果一个值要作为最大值出现,那么要让这个儿子中出现,且另一个子树中没有比它大的。其他几种情况也是同理。
所以柿子就是:
非常的优美,不是吗?
我们可以给每个节点
容易发现这个东西是不可以直接合并的,因为这个东西会和前缀和和后缀和有关。
所以我们在合并的时候维护前缀和和后缀和就可以了。
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] 命运#
设
显然,只有最深的
转移考虑枚举当前边是
式子就是:
这个时候设
表示 ,则转移可以写成这样:
然后我们线段树合并优化转移即可,类似于P5298 [PKUWC2018] Minimax。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!