线段树合而并之
线段树合并:
线段树合并者,算法之妙用也。其法若二木相合,取其枝节而并之。初立两树,各司其职,一主左,一主右。遍历其节点,若一方虚则取其实,若两方皆实则融其值。递归而下,至于叶处,乃成新树。此术可解诸区间之问,譬如极值、求和,效甚速焉。智者用之,可化繁为简,收事半功倍之效。盖分治之道,存乎一心而已。
线段树的合并是线段树的常用技巧,常见于权值线段树维护可重集的场景。
例如,树上某些结点处有若干操作,如果需要自下而上地将子节点信息传递给亲节点,而单个结点处的信息又方便用线段树维护时,就可以应用线段树合并的技巧控制整体的复杂度。
线段树合并就是把两个线段树合并,左右两个儿子分别合并,但是如果把整颗线段树建全的话它的空间复杂度是爆炸的,所以就要用到动态开点,这样才可以保证空间上的正确性。
经实践可知线段树合并通常和权值线段树混合使用
具体的,先看例题:
[HNOI2012] 永无乡
简要题意:
初始给一个图,不保证连通,每个点有一个重要值排名。
操作1: \(B\) \(x\) \(y\) 表示在岛 \(x\) 与岛 \(y\) 之间修建一座新桥。
操作2: \(Q\) \(x\) \(k\) 表示询问当前与岛 \(x\) 连通的所有岛中第 \(k\) 重要的是哪座岛,即所有与岛 \(x\) 连通的岛中重要度排名第 \(k\) 小的岛是哪座,请你输出那个岛的编号
思路:
点击查看
可以对于每一个点动态开点线段树,维护重要度排名,并记录线段树左右儿子的大小。然后用并查集维护联通性,每次连边把两棵线段树合并就可以了。
\(Code :\)
#include<bits/stdc++.h>
const int N = 2e7+10;
using namespace std;
int tot,a[N],rt[N],fa[N],n,m;
struct node {
int ls,rs,sum;
//sum维护线段树的大小
}tr[N];
int find(int x) {return fa[x]==x?x:fa[x]=find(fa[x]);}
int insert(int x,int l,int r) {
int id=++tot;//动态开点
tr[id].sum=1;
if(l==r) return id;
int mid=l+r>>1;
if(x<=mid) tr[id].ls=insert(x,l,mid);
else tr[id].rs=insert(x,mid+1,r);
return id;
}
int merge(int x,int y,int l,int r) {
if(!x||!y) return x+y;
tr[x].sum+=tr[y].sum;
if(l==r) return x;
int mid=l+r>>1;
tr[x].ls=merge(tr[x].ls,tr[y].ls,l,mid);
tr[x].rs=merge(tr[x].rs,tr[y].rs,mid+1,r);
return x;
}
int query(int x,int y,int l,int r) {
if(tr[x].sum<y) return -1;
if(l==r) return a[l];
int mid=l+r>>1;
if(tr[tr[x].ls].sum>=y) return query(tr[x].ls,y,l,mid);
else return query(tr[x].rs,y-tr[tr[x].ls].sum,mid+1,r);
}
signed main() {
scanf("%d%d",&n,&m);
for(int i=1,x;i<=n;i++) {
scanf("%d",&x);
a[x]=i;//维护有关排名的线段树,这样便于输出答案
fa[i]=i;//用路径压缩并查集维护每个岛的集合,集合内互相连通
rt[i]=insert(x,1,n);
}
for(int i=1,x,y;i<=m;i++) {
scanf("%d%d",&x,&y);
int fx=find(x),fy=find(y);
if(fx!=fy) {
fa[fy]=fx;//并入一个集合
rt[fy]=merge(rt[fx],rt[fy],1,n);
}
}
int q;scanf("%d",&q);
while(q--) {
char opt;int x,y;
cin>>opt>>x>>y;
if(opt=='Q') {
cout<<query(rt[find(x)],y,1,n)<<endl;
}
else {
int fx=find(x),fy=find(y);
if(fx!=fy) {
fa[fy]=fx;
rt[fy]=merge(rt[fx],rt[fy],1,n);
}
}
}
return 0;
}
[Vani有约会] 雨天的尾巴 /【模板】线段树合并
不简要的题意:
村落里一共有 \(n\) 座房屋,并形成一个树状结构。然后救济粮分 \(m\) 次发放,每次选择两个房屋 \((x,y)\),然后对于 \(x\) 到 \(y\) 的路径上(含 \(x\) 和 \(y\))每座房子里发放一袋 \(z\) 类型的救济粮。
求:当所有的救济粮发放完毕后,每座房子里存放的最多的是哪种救济粮。
思路:
点击查看
做法:权值线段树+线段树合并+LCA+树上差分
树上差分
由于需要在树上进行路径更新,最有效的方法是使用树上差分。具体来说,对于每次操作 \((x,y,z)\),可以拆分为四个操作:
- \(x\) 到根节点的路径上 \(+z\)
- \(y\) 到根节点的路径上 \(+z\)
- lca(x,y)到根节点的路径上 \(-z\)
- \(fa[lca(x,y)]\) 到根节点的路径上 \(-z\)
线段树合并
为了高效统计每个节点的救济粮种类数量,我们为每个节点建立一棵权值线段树,维护每种救济粮的数量。然后通过线段树合并的方式,自底向上将子节点的线段树合并到父节点中。合并过程中统计最大值和对应的救济粮种类.
\(Code\):
#include<bits/stdc++.h>
const int N = 2e5+10, M = 2002000<<2;
using namespace std;
inline int read() {
int x=0,f=1;char s;s=getchar();
while(s<48||s>57) f=(s=='-')?-1:1,s=getchar();
while(s>=48&&s<=57) x=x*10+s-48,s=getchar();
return x*f;
}
int n,m,x,y,z,T[N];
int head[N],tot;
struct node { int to,nxt; }e[N];
void add(int x,int y) { e[++tot]={y,head[x]};head[x]=tot; }
struct tree{
int ls,rs,ma,id;
//ma:个数 id:种类
}t[M];
int siz[N],fa[N],dep[N],top[N],son[N],dp[N];
void push_up(int p) {
int ls=t[p].ls,rs=t[p].rs;
if(t[ls].ma>=t[rs].ma) t[p].ma=t[ls].ma,t[p].id=t[ls].id;
else t[p].ma=t[rs].ma,t[p].id=t[rs].id;
//更新父亲节点记录的最大值
}
void dfs1(int p,int f) {
dep[p]=dep[f]+1,fa[p]=f,siz[p]++;
for(int i=head[p];i;i=e[i].nxt) {
int v=e[i].to;
if(v!=f) {
dfs1(v,p);siz[p]+=siz[v];
if(siz[v]>siz[son[p]]) son[p]=v;
}
}
}
void dfs2(int p,int t) {
top[p]=t;
if(!son[p]) return ;
dfs2(son[p],t);
for(int i=head[p];i;i=e[i].nxt) {
int v=e[i].to;
if(v==fa[p]||v==son[p]) continue;
dfs2(v,v);
}
}
int update(int p,int l,int r,int x,int k) {
if(!p) p=++tot;
if(l==r) {
t[p].id=x,t[p].ma+=k;return p;
}
int mid=(l+r)>>1;
if(x<=mid) t[p].ls=update(t[p].ls,l,mid,x,k);
else t[p].rs=update(t[p].rs,mid+1,r,x,k);
push_up(p);
return p;
}
void LCA(int x,int y,int z) {
while(top[x]!=top[y]) {
if(dep[top[x]]<dep[top[y]]) swap(x,y);
x=fa[top[x]];
}
if(dep[x]>dep[y]) swap(x,y);
T[x]=update(T[x],1,1e5,z,-1);
//LCA到1减去z种类的一袋
if(fa[x]) T[fa[x]]=update(T[fa[x]],1,1e5,z,-1);
//LCA的父亲到1减去z种类的一袋
}
int merge(int x,int y,int l,int r) {
if(!x||!y) return x+y;
if(l==r) {
t[x].id=l;t[x].ma+=t[y].ma; return x;
}
int mid=(l+r)>>1;
t[x].ls=merge(t[x].ls,t[y].ls,l,mid);
t[x].rs=merge(t[x].rs,t[y].rs,mid+1,r);
push_up(x);
return x;
}
void DP(int p) {
for(int i=head[p],v;i;i=e[i].nxt) {
v=e[i].to;
if(v==fa[p]) continue;
DP(v);
T[p]=merge(T[p],T[v],1,1e5);
}
if(t[T[p]].ma) dp[p]=t[T[p]].id;
//合并,fa节点记录的就是最大值
}
int main() {
n=read();m=read();
for(int i=1;i<n;i++) {
x=read();y=read();
add(x,y),add(y,x);
}
tot=0;
dfs1(1,0);dfs2(1,1);
tot=0;
for(int i=1;i<=m;i++) {
x=read();y=read();z=read();
T[x]=update(T[x],1,1e5,z,1),T[y]=update(T[y],1,1e5,z,1);
LCA(x,y,z);
}
DP(1);
for(int i=1;i<=n;i++) cout<<dp[i]<<endl;
return 0;
}
[USACO17JAN] Promotion Counting P
简化题意:
一棵树,每个点有一个权值,\(1\) 号为根节点,需要求出每个点的子树中有多少个节点的权值是大于它的。
思路:
点击查看
做法:权值线段树+线段树合并+离散化
先对于权值离散化,然后对于每一个叶子节点维护一个权值线段树,然后从叶子节点开始向上合并。
\(Code\):
#include<bits/stdc++.h>
using namespace std;
const int N=2000009;
int cnt,n,val[N],fa[N],c[N],tot,rt[N],ans[N];
//权值线段树
struct node {
int ls,rs,sum;
}tr[N];
int d[N<<2];
vector<int> q[N];
void pushup(int x) {tr[x].sum=tr[tr[x].ls].sum+tr[tr[x].rs].sum;}
int build(int x,int l,int r,int k) {
if(!x) x=++tot;
if(l==r) {
tr[x].sum++;
return x;
}
int mid=(l+r)>>1;
if(k<=mid) tr[x].ls=build(tr[x].ls,l,mid,k);
else tr[x].rs=build(tr[x].rs,mid+1,r,k);
pushup(x);
return x;
}
int merge(int x,int y,int l,int r) {
if(!x||!y) return x+y;
if(l==r) {
tr[x].sum+=tr[y].sum;
}
int mid=(l+r)>>1;
tr[x].ls=merge(tr[x].ls,tr[y].ls,l,mid);
tr[x].rs=merge(tr[x].rs,tr[y].rs,mid+1,r);
pushup(x);
return x;
}
int query(int p,int l,int r,int x,int y) {
if(x<=l&&r<=y) return tr[p].sum;
int mid=(l+r)>>1,res=0;
if(x<=mid) res+=query(tr[p].ls,l,mid,x,y);
if(y>mid) res+=query(tr[p].rs,mid+1,r,x,y);
return res;
}
void dfs(int x) {
for(auto v:q[x]) {
if(v==fa[x]) continue;
dfs(v);
rt[x]=merge(rt[x],rt[v],1,cnt);
}
ans[x]=query(rt[x],1,cnt,val[x]+1,cnt);
}
int main() {
scanf("%d",&n);
for(int i=1; i<=n; i++)
scanf("%d",&val[i]),c[i]=val[i];
//离散化
sort(c+1,c+n+1);
cnt=unique(c+1,c+1+n)-c-1;
for(int i=1; i<=n; i++) {
val[i]=lower_bound(c+1,c+1+n,val[i])-c;
rt[i]=build(rt[i],1,cnt,val[i]);
}
for(int i=2; i<=n; i++) {
int x;
scanf("%d",&x);
fa[i]=x;
q[x].push_back(i);
q[i].push_back(x);
}
dfs(1);
for(int i=1; i<=n; i++)printf("%d\n",ans[i]);
return 0;
}
[湖南集训] 更为厉害
简要题意:
给一颗有根树,从 \(1\) 到 \(n\) 编号,有若干询问给定 \(a,k\) 找出 \(b,c\) 满足 \(a,b\) 都是 \(c\) 的祖先,且 \(a,b\) 之间的距离不超过 \(k\).
思路:
点击查看
首先观察题面,发现 \(a,b\) 都为 \(c\) 的祖先,这就说明 \(a,b,c\) 就应该是属于一条链上的,我们记录每个节点的深度,这样就可以 \(O(1)\) 计算 \(a,b\) 之间的距离。
然后分类讨论,\(a,b\)在一条链上就只有两种情况:
设 \(siz[a]\) 表示 \(a\) 的子树大小(包括 \(a\))
\(b\) 为 \(a\) 的祖先,那么答案的贡献就为\(min(dep[a]−1,k)*(siz[a]-1)\)
\(a\) 为 \(b\) 的祖先,那么答案的贡献就为$$\sum_{dep_b\in[dep_a+1,dep_a+k]} (siz_b-1)$$
\(Code\):
#include<bits/stdc++.h>
#define int long long
const int N=3e6+10,M=2e7+10,inf=0x3f3f3f3f;
using namespace std;
inline int read() {int s=0,w=1;char ch=getchar();while(ch<'0'||ch>'9'){if(ch=='-')w=-1;ch=getchar();}while(ch>='0'&&ch<='9')s=(s<<3)+(s<<1)+(ch^48),ch=getchar();return s*w;}
int n,m,cnt;
int dep[N],siz[N];
int nxt[N<<1],to[N<<1];
int sum[M],ls[M],rs[M],rt[N];
int head[N],tot=1;
struct node {int to,nxt;}e[N];
inline void add(int u,int v){e[++tot]={v,head[u]};head[u]=tot;}
int update(int x,int l,int r,int pos,int k) {
if(!x) x=++cnt;
if(l==r) {
sum[x]+=k;
return x;
}
int mid=l+r>>1;
if(pos<=mid) ls[x]=update(ls[x],l,mid,pos,k);
else rs[x]=update(rs[x],mid+1,r,pos,k);
sum[x]=sum[ls[x]]+sum[rs[x]];
return x;
}
int merge(int x,int y) {
if(!x||!y)return x+y;
int id=++cnt;
sum[id]=sum[x]+sum[y];
ls[id]=merge(ls[x],ls[y]);
rs[id]=merge(rs[x],rs[y]);
return id;
}
int query(int k,int l,int r,int x,int y) {
if(x<=l&&r<=y)return sum[k];
int mid=(l+r)>>1,res=0;
if(x<=mid)res+=query(ls[k],l,mid,x,y);
if(mid<y)res+=query(rs[k],mid+1,r,x,y);
return res;
}
void dfs(int u,int fa) {
dep[u]=dep[fa]+1,siz[u]=1;
for(int i=head[u];i;i=e[i].nxt){
int v=e[i].to;
if(v==fa) continue;
dfs(v,u);siz[u]+=siz[v];
rt[u]=merge(rt[u],rt[v]);
}
rt[u]=update(rt[u],1,n,dep[u],siz[u]-1);
}
signed main() {
n=read();m=read();
for(int i=2,u,v;i<=n;i++) {
u=read(); v=read();
add(u,v); add(v,u);
}
dfs(1,0);
while(m--) {
int a=read(),k=read();
int ans=min(k,dep[a]-1)*(siz[a]-1);
ans+=query(rt[a],1,n,dep[a]+1,min(n,dep[a]+k));
printf("%lld\n",ans);
}
return 0;
}
[BJOI2014] 大融合
简要题意:
给定若个点,有若干个连边操作,还有若干个询问,每次询问给出一条边,询问经过这条边的简单路径有多少条
思路:
点击查看
做法:动态开点权值线段树+dfn序+并查集+线段树合并
首先是简单的题目转化,求经过一条边的简单路径就是求这条边两个端点所在子树的长度的乘积。
先把最终的树建出来,跑一遍 \(dfs\) 求出 \(dfn\) 序,然后按 \(dfn\) 序把树形结构放在区间上
对于每一个点的 \(dfn\) 序开一棵权值线段树,连边操作就是把两棵子树合并.
\(Code\):
#include<bits/stdc++.h>
const int N=1e6+10;;
using namespace std;
inline int read() {char ch=getchar();int res=0,f=1;while(!isdigit(ch)){if(ch=='-')f=-f;ch=getchar();}while(isdigit(ch))res=(res<<3)+(res<<1)+(ch^48),ch=getchar();return res*f;}
int st[N],en[N],rt[N],siz[N<<5],n,q,head[N],tot,dfn,cnt,lc[N<<5],rc[N<<5],dep[N],fa[N];
char c;
struct node {
int to,nxt;
}e[N];
struct ask {
int u,v,op;
}pos[N];
inline void addedge(int u,int v) {e[++cnt]={v,head[u]};head[u]=cnt;}
int find(int x) {return fa[x]==x?fa[x]:fa[x]=find(fa[x]);}
int update(int u,int l,int r,int k) {
if(!u) u=++tot;
siz[u]=1;
if(l==r) return u;
int mid=(l+r)>>1;
if(k<=mid) lc[u]=update(lc[u],l,mid,k);
else rc[u]=update(rc[u],mid+1,r,k);
return u;
}
int merge(int u,int v,int l,int r) {
if(!u||!v) {return u+v;}
siz[u]+=siz[v];
if(l==r) return u;
int mid=(l+r)>>1;
lc[u]=merge(lc[u],lc[v],l,mid);
rc[u]=merge(rc[u],rc[v],mid+1,r);
return u;
}
int query(int u,int l,int r,int x,int y) {
if(x<=l&&r<=y) return siz[u];
int res=0,mid=(l+r)>>1;
if(x<=mid) res+=query(lc[u],l,mid,x,y);
if(mid<y) res+=query(rc[u],mid+1,r,x,y);
return res;
}
void dfs(int u,int fa) {
st[u]=++dfn;
for(int i=head[u];i;i=e[i].nxt) {
int v=e[i].to;
if(v==fa) continue;
dep[v]=dep[u]+1;
dfs(v,u);
}
en[u]=dfn;
//把一棵子树放在区间上
}
int main() {
n=read(),q=read();
for(int i=1;i<=n;i++)fa[i]=i;
for(int i=1;i<=q;i++) {
scanf("%c",&c);
int u=read(),v=read(),k=(c=='A');
pos[i]=(ask){u,v,k};
if(c=='A')addedge(v,u),addedge(u,v);
}
for(int i=1;i<=n;i++) {
if(!st[i]) dfs(i,0);
rt[i]=update(0,1,n,st[i]);
//对于每个点开一个权值线段树
}
for(int i=1;i<=q;i++) {
int u=pos[i].u,v=pos[i].v,k=pos[i].op;
if(dep[u]>dep[v]) swap(u,v);
if(k) {
int x=find(u),y=find(v);
fa[y]=x,merge(rt[x],rt[y],1,n);
}
else {
int x=find(u),y=query(rt[x],1,n,st[v],en[v]);
//query()返回的是子树大小
cout<<(y*(siz[rt[x]]-y))<<"\n";
}
}
}
[Cnoi2019] 雪松果树
不简要的题面:
一棵以 \(1\) 为根有 \(N\) 个节点的树。
有 \(Q\) 个询问,每个询问是一个二元组 \((u,k)\),表示询问 \(u\) 节点的 \(k\)-\(cousin\) 有多少个。
定义:
节点 \(u\) 的 \(1 - father\) 为 路径 \((1, u)\) (不含 \(u\))上距 \(u\) 最近的节点
节点 \(u\) 的 \(k-father\) 为 节点 「\(u\) 的 \((k-1)-father\) 」 的 \(1-father\)
节点 \(u\) 的 \(k-son\) 为所有 \(k-father\) 为 \(u\) 的节点
节点 \(u\) 的 \(k-cousin\) 为 节点「 \(u\) 的 \(k-father\)」的 \(k-son\) (不包含 \(u\) 本身)
思路:
点击查看
首先对于给定的点 \(u\),我们要确定它有没有 \(k\) 级祖先,只要看它的深度是不是 \(\le k\) 就可以了。
然而后面的求解还需要用到这个祖先的子树,所以不能只判断,还要求这个祖先。
考虑树上倍增,用类似倍增求 \(LCA\) 的写法,单次时间 \(O(log n)\)。
找到祖先以后,题意显然是要求在这个子树中,深度为 \(dep_v\) 的点的个数减 \(1\)。
但是这样就需要维护子树的信息,所以我们使用线段树合并把儿子的子树信息合并在一起,拼成这个点(父亲)的子树信息。
还有一个细节:对于每次询问,不能直接去求答案。
因为如果直接去求,实际上这样一个祖先可能会被询问多次,这样时间复杂度就不对了。
将所有询问先离线下来,存到对应祖先上去。然后整体 \(dfs\),到哪个点就处理哪个点上的询问,这样就能保证复杂度正确了.
然后是权值线段树,我们开一个有关每个节点的 \(k\) 儿子,权值线段树的权值是这个 \(k\) ,然后合并的时候合并个数.
原本这个题的空间是 \(125MB\) 普通的线段树合并是不可过的,需要将空间回收再利用,但是管理员偷偷把空间改成 \(512MB\) 了,就不需要空间回收了。 现在是 \(256MB\) 需要空间回收
\(Code\):
#include<bits/stdc++.h>
const int N = 1e6+10, M = 4e6+10;
using namespace std;
inline int read(){
int s=0,f=1;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){s=(s<<1)+(s<<3)+(ch^48);ch=getchar();}
return s*f;
}
int n,cnt,tot,rt[N],ans[N],dep[N],f[N][20],Q,st[M],top;
struct node{int ls,rs,sum;}t[M];
struct edge{int k,id;};
vector<int>e[N];
vector<edge>q[N];
void init(int x) {t[x].ls=t[x].rs=t[x].sum=0;}
int update(int k,int l,int r,int x){
if(!k)k=top?st[top--]:++tot;
if(l==r) {t[k].sum++;return k;}
int mid=l+r>>1;
if(x<=mid) t[k].ls=update(t[k].ls,l,mid,x);
else t[k].rs=update(t[k].rs,mid+1,r,x);
return k;
}
int Merge(int x,int y,int l,int r){
if(!x||!y) return x+y;
if(l==r) {
t[x].sum+=t[y].sum;
init(y);
st[++top]=y;
return x;
}
int mid=l+r>>1;
t[x].ls=Merge(t[x].ls,t[y].ls,l,mid);
t[x].rs=Merge(t[x].rs,t[y].rs,mid+1,r);
init(y);
st[++top]=y;
return x;
}
int Query(int k,int l,int r,int x) {
if(l==r) return t[k].sum;
int mid=l+r>>1;
if(x<=mid) return Query(t[k].ls,l,mid,x);
else return Query(t[k].rs,mid+1,r,x);
}
void Solve(int u,int fa) {
rt[u]=++tot;
for(auto y:e[u]) {
if(y==fa) continue;
Solve(y,u);
rt[u]=Merge(rt[u],rt[y],1,n);
}
rt[u]=update(rt[u],1,n,dep[u]);
for(edge y:q[u]) ans[y.id]=Query(rt[u],1,n,y.k);
}
int BZ(int x,int k) {for(int i=19;i>=0;i--) if(k&(1<<i)) x=f[x][i];return x;}
void dfs(int u,int fl) {
dep[u]=fl;
for(int i=1;i<=19;i++) f[u][i]=f[f[u][i-1]][i-1];
for(auto y:e[u]) {
f[y][0]=u; dfs(y,fl+1);
}
}
int main() {
n=read();Q=read();
for(int i=2,x;i<=n;i++) {x=read();e[x].push_back(i);}
dfs(1,1);
for(int i=1,x,k;i<=Q;i++) {
x=read(); k=read();
int v=BZ(x,k);
if(v) q[v].push_back({dep[v]+k,i});
}
Solve(1,0);
for(int i=1;i<=Q;i++) printf("%d ",max(ans[i]-1,0));
return 0;
}
[POI 2011] ROT-Tree Rotations
简要题意:
给一棵二叉树,每个叶子节点都有一个权值,权值构成一个排列, 可以有若干次操作,可以选择一些节点的左右子树并交换
对于这棵二叉树的任何一个结点,保证其要么是叶节点,要么左右两个孩子都存在。
求按先序遍历利的最小逆序对个数。
\(ps\):感觉读入格式不像人类。
思路:
点击查看
做法:权值线段树+线段树合并
对题目细细端详,发现它的逆序对情况只有三种:
1.左子树
2.右子树
3.横跨两个子树
对于 \(1,2\) 在之前的子树递归时就已经处理了。
对于 \(3\) 只需要在合并的时候判断哪种更优。
至于逆序对的维护就用权值线段树。
\(Code\):
#include<bits/stdc++.h>
using namespace std;
const int N=2e6+10;
int read() {int x=0;short f=1;char s=getchar();while(s<48||s>57){f=s=='-'?-1:1;s=getchar();}while(s>=48&&s<=57){x=x*10+s-48;s=getchar();}return x*f;}
int n,tot,rt[N],cnt;
long long fur,ans,nw;
struct Tree {
int ls,rs;int s;
}t[N<<4];
int insert(int x,int l,int r,int k) {
if(!x) x=++tot;
t[x].s++;
if(l==r) return x;
int mid=l+r>>1;
if(k<=mid) t[x].ls=insert(t[x].ls,l,mid,k);
else t[x].rs=insert(t[x].rs,mid+1,r,k);
return x;
}
int merge(int x,int y) {
if(!x||!y) { x=x+y; return x; }
t[x].s+=t[y].s;
nw+=1ll*t[t[x].ls].s*t[t[y].rs].s;
fur+=1ll*t[t[x].rs].s*t[t[y].ls].s;
t[x].ls=merge(t[x].ls,t[y].ls);
t[x].rs=merge(t[x].rs,t[y].rs);
return x;
}
int Scanf() {
int val=read(),ls,rs;
if(!val) {
ls=Scanf();rs=Scanf();
nw=0; fur=0;
rt[++cnt]=ls,rt[cnt]=merge(rt[cnt],rs);
ans+=min(nw,fur);
}
else rt[cnt]=insert(rt[++cnt],1,n,val);
return rt[cnt];
}
signed main() {
n=read();
Scanf();
cout<<ans;
return 0;
}
P8496 [NOI2022] 众数
不简要的题意:
对于一个序列,定义其众数为序列中出现次数严格大于一半的数字。注意该定义与一般的定义有出入,在本题中请以题面中给出的定义为准。
一开始给定 \(n\) 个长度不一的正整数序列,编号为 \(1 \sim n\),初始序列可以为空。这 \(n\) 个序列被视为存在,其他编号对应的序列视为不存在。
有 \(q\) 次操作,操作有以下类型:
\(1. \ x \ y\):在 \(x\) 号序列末尾插入数字 \(y\)。保证 \(x\) 号序列存在,且 \(1 \le x, y \le n + q\)。
\(2. \ x\):删除 \(x\) 号序列末尾的数字,保证 \(x\) 号序列存在、非空,且 \(1 \le x \le n + q\)。
\(3. \ m \ x_1 \ x_2 ...\ x_m\):将 \(x_1, x_2, \ldots, x_m\) 号序列顺次拼接,得到一个新序列,并询问其众数。如果不存在满足上述条件的数,则返回 \(-1\)。数据保证对于任意 \(1 \le i \le m\),\(x_i\) 是一个仍然存在的序列,\(1 \le x_i \le n + q\),且拼接得到的序列非空。注意:不保证 \(\boldsymbol{x_1, \ldots, x_m}\) 互不相同,询问中的合并操作不会对后续操作产生影响。
\(4. \ x_1 \ x_2 \ x_3\):新建一个编号为 \(x_3\) 的序列,其为 \(x_1\) 号序列后顺次添加 \(x_2\) 号序列中数字得到的结果,然后删除 \(x_1, x_2\) 对应的序列。此时序列 \(x_3\) 视为存在,而序列 \(x_1, x_2\) 被视为不存在,在后续操作中也不会被再次使用。保证 \(1 \le x_1, x_2, x_3 \le n + q\)、\(x_1 \ne x_2\)、序列 \(x_1, x_2\) 在操作前存在、且在操作前没有序列使用过编号 \(x_3\)。
\(1 \le n, q \le 5 \times {10}^5\)。
思路:
点击查看
做法:二分 + 线段树合并 + 手搓链表
\(Code\):
#include <bits/stdc++.h>
#define int long long
const int N = 5e6+10, inf = 1e6;
using namespace std;
int read() {
int s=0,x=1;char ch=getchar();
while(!isdigit(ch)){if(ch=='-')x=-1;ch=getchar();}
while(isdigit(ch))s=(s<<3)+(s<<1)+(ch^48),ch=getchar();
return s*x;
}
int n,Q;
int hed[N],tal[N],las[N],val[N],id;
// 头 尾 上一个 值 编号
int lc[N],rc[N],tot,rt[N],cnt[N];
vector<int>m;
void pushup(int i) {cnt[i]=cnt[lc[i]]+cnt[rc[i]];}
int merge(int x,int y,int l,int r) {
if(!x||!y) return x+y;
if(l==r) return cnt[x]+=cnt[y],x;
int mid=l+r>>1;
lc[x]=merge(lc[x],lc[y],l,mid),
rc[x]=merge(rc[x],rc[y],mid+1,r);
pushup(x);
return x;
}
int update(int x,int k,int l,int r,int i) {
if(!i) i=++tot;
if(l==r) return cnt[i]+=k,i;
int mid=l+r>>1;
if(x<=mid) lc[i]=update(x,k,l,mid,lc[i]);
else rc[i]=update(x,k,mid+1,r,rc[i]);
pushup(i);
return i;
}
int query(int siz) {
int l=1,r=inf;siz>>=1;
while(1) {
int res=0,len=m.size();
int mid=l+r>>1;
if(l==r) {
for(auto v:m) res+=cnt[v];
if(res>siz) return l;
return -1;
}
for(int v:m) res+=cnt[lc[v]];
if(res>siz) {
for(int i=0;i<len;i++) m[i]=lc[m[i]];
r=mid;
}
else {
for(int i=0;i<len;i++) m[i]=rc[m[i]];
l=mid+1;
}
}
}
signed main() {
n=read();Q=read();
for(int i=1,len;i<=n;i++) {
len=read();
hed[i]=id+1;
for(int j=1,x;j<=len;j++) {
x=read();
val[++id]=x;
rt[i]=update(x,1,1,inf,rt[i]);
if(j>1) las[id]=id-1;
}
tal[i]=id;
}
int opt,x,y,z;
while(Q--) {
opt=read();
if(opt==1) {
x=read();y=read();
val[++id]=y,las[id]=tal[x],tal[x]=id;
rt[x]=update(y,1,1,inf,rt[x]);
if(!hed[x]) hed[x]=id;
}
else if(opt==2) {
x=read();
rt[x]=update(val[tal[x]],-1,1,inf,rt[x]);
tal[x]=las[tal[x]];
if(tal[x]==0) hed[x]=0;
}
else if(opt==3) {
x=read();m.clear();int siz=0;
for (int i=1;i<=x;i++) y=read(),m.push_back(rt[y]),siz+=cnt[rt[y]];
printf("%d\n",query(siz));
}
else {
x=read();y=read();z=read();
hed[z]=hed[x]?hed[x]:hed[y];
tal[z]=tal[y]?tal[y]:tal[x];
if(hed[y]) las[hed[y]]=tal[x];
rt[z]=merge(rt[x],rt[y],1,inf);
}
}
return 0;
}
[FJOI2018] 领导集团问题
简要题意:
给出一棵树,有点权,求出一个最大点集,要求祖先点权小于等于子孙点权。
\(1\le n\le 2\times 10 ^ 5\),\(0 < w_i \le 10 ^ 9\)
思路:
点击查看
显然这是一道 \(dp\) 题,因为我们现在在讲线段树合并,所以这题是线段树合并优化 \(dp\)
我们设 \(f_{i,j}\) 表示节点 \(i\) 的子树中点权都 \(\ge j\) 的能选出来的点个数的最大值。
然后就可以列出 \(dp\) 式子:
但是显然这个式子难以维护,直接暴力的话是 \(O(n^2)\) 级别的,我们把它拆成两个式子:
\[f_{i,j}=\sum_{z\in son(i)}f_{z,j} \]
\[f_{i,j}=max(f_{i,j} ,\sum_{z\in son(i)}f_{z,w[i]}+1) (j\in [1,w[i]]) \]
然后是线段树合并优化,这个点权只与大小关系相关,直接就是一个离散化,然后对于每一个点开一个权值线段树,维护 \(w[i]\),第一个式子线段树合并,第二个式子是取 \(max\) 然后就是对于一些细节的处理。打了就知道了。
#include<bits/stdc++.h>
const int N = 2e7+10;
using namespace std;
inline int read() {
int s=0,x=1;
char ch=getchar();
while(!isdigit(ch))x=ch=='-'?-1:1,ch=getchar();
while(isdigit(ch))s=(s<<3)+(s<<1)+(ch^48),ch=getchar();
return s*x;
}
struct tree {
int ls,rs,sum,num,add;
} tr[N];
int cnt,rt[N];
struct node {int to,nxt;} e[N*2];
int head[N],n,tot,w[N],W[N];
void add(int u,int v) {e[++tot]= {v,head[u]},head[u]=tot;}
void pushdown(int k) {
if(!tr[k].ls) tr[k].ls=++cnt;
if(!tr[k].rs) tr[k].rs=++cnt;
if (tr[k].add) {
tr[tr[k].ls].sum+=tr[k].add,tr[tr[k].rs].sum+=tr[k].add;
tr[tr[k].ls].add+=tr[k].add,tr[tr[k].rs].add+=tr[k].add;
tr[tr[k].ls].num+=tr[k].add,tr[tr[k].rs].num+=tr[k].add;
tr[k].add=0;
}
if (tr[k].num != -1) {
tr[tr[k].ls].sum=max(tr[k].num,tr[tr[k].ls].sum), tr[tr[k].rs].sum=max(tr[k].num,tr[tr[k].rs].sum);
tr[tr[k].ls].num=max(tr[k].num,tr[tr[k].ls].num), tr[tr[k].rs].num=max(tr[k].num,tr[tr[k].rs].num);
tr[k].num=-1;
}
}
int merge(int x,int y,int l,int r) {
if(!x||!y) return x+y;
if (!tr[y].rs && !tr[y].ls) swap(x,y);
if (!tr[x].ls && !tr[x].rs) {
tr[y].sum+=tr[x].sum;
if(tr[y].num!=-1) tr[y].num+=tr[x].sum;
tr[y].add+=tr[x].sum;
return y;
}
if(l==r) return tr[x].sum+=tr[y].sum;
int mid=l+r>>1;
pushdown(x),pushdown(y);
tr[x].ls=merge(tr[x].ls,tr[y].ls,l,mid);
tr[x].rs=merge(tr[x].rs,tr[y].rs,mid+1,r);
return x;
}
int update(int k,int l,int r,int L,int R,int x) {
if(!k) k=++cnt;
if(L<=l&&r<=R) {
tr[k].sum=max(tr[k].sum,x);
tr[k].num=max(tr[k].num,x);
return k;
}
pushdown(k);
int mid=l+r>>1;
if(L<=mid) tr[k].ls=update(tr[k].ls,l,mid,L,R,x);
if(mid<R) tr[k].rs=update(tr[k].rs,mid+1,r,L,R,x);
return k;
}
int query(int k,int l,int r,int x) {
if(l==r) return tr[k].sum;
int ans=0;
int mid=l+r>>1;
pushdown(k);
if(x<=mid) ans=max(ans,query(tr[k].ls,l,mid,x));
else ans=max(ans,query(tr[k].rs,mid+1,r,x));
return ans;
}
void dfs(int u) {
int sum=0;
for(int i=head[u]; i; i=e[i].nxt) {
int v=e[i].to;
dfs(v);
sum+=query(rt[v],1,n,w[u]);
rt[u]=merge(rt[u],rt[v],1,n);
}
rt[u]=update(rt[u],1,n,1,w[u],sum+1);
}
void init() {
sort(W+1,W+n+1);
int num=unique(W+1,W+n+1)-W-1;
for(int i=1; i<=n; i++) w[i]=lower_bound(W+1,W+num+1,w[i])-W;
}
int main() {
n=read();
for(int i=1; i<=n; i++) W[i]=w[i]=read();
init();
for(int i=2; i<=n; i++) add(read(),i);
dfs(1);
cout<<query(rt[1],1,n,1);
return 0;
}

线段树合并
浙公网安备 33010602011771号