专栏文章
线段树合并:从入门到放弃
算法·理论参与者 51已保存评论 57
文章操作
快速查看文章及其快照的属性,并进行相关操作。
- 当前评论
- 57 条
- 当前快照
- 1 份
- 快照标识符
- @mhz5ta6b
- 此快照首次捕获于
- 2025/11/15 01:56 4 个月前
- 此快照最后确认于
- 2025/11/29 05:25 3 个月前
开始前先个小剧场
某次模拟赛前两天
本蒟蒻:“雨天的尾巴怎么写啊?我被日推了。”
大佬:“线段树合并啊,瞎搞搞就行了。”
本蒟蒻:“哦,那想必提高组不会考”
然后就爆零了,于是赶紧抢救一下,写下这篇博客,如果有误,还请大佬指正
零、前置算法:动态开点线段树和权值线段树
感觉动态开点线段树和普通线段树的区别比较明显的就是x的左儿子和右儿子不一定是和而是要另外储存一下。
其实如果你学的是结构体线段树,在写动态开点的时候应该不会有任何违和感……
动态开点线段树有着一些优点,比如说当你让某个节点继承另一个节点的左儿子或者右儿子的时候,你可以不用新建一棵线段树,而是直接将该节点的左右儿子赋成那个节点的左右儿子就行了,总之就是空间上有一定的优越性。
线段树合并如果要每次都完整建一棵线段树的话,怕不是建完树就好凉凉了,因此我们需要选择动态开点。
权值线段树能代替平衡树做一些求大、排名、找前驱后继的操作,了解一下就可以啦
一、线段树合并的思想
线段树合并,顾名思义,就是建立一棵新的线段树保存原有的两颗线段树的信息。

图片来源于主席的线段树合并讲义
这个思想非常简单,假设我们现在合并到了两棵线段树a、b的pos位置,那么
如果a有pos位置,b没有,那么新的线段树pos位置赋成a,返回
如果b有pos位置,a没有,赋成b,返回
如果此时已经合并到两棵线段树的叶子节点了,就把b在pos的值加到a上,把新线段树上的pos位置赋成a,返回
递归处理左子树
递归处理右子树
用左右子树的值更新当前节点
将新线段树上的pos位置赋成a,返回
代码大概长这样:
CPP int merge(int a,int b,int l,int r)
{
if(!a) return b;
if(!b) return a;
if(l==r)
{
//按照所需合并
return a;
}
int mid=(l+r)>>1;
tr[a].l=merge(tr[a].l,tr[b].l,l,mid);
tr[a].r=merge(tr[a].r,tr[b].r,mid+1,r);
push_up(a);
return a;
}
好的,思路是不是很简单?
所以问题来了,这么搞的复杂度是多少?
二、线段树合并的复杂度证明
这道题也就最多合并100000棵满线段树,一遍复杂度也就是O(logn),所以不是显然能过的嘛
--by 某天天算错复杂度的神仙
上面那个神仙是谁,我们姑且不谈。
先来思考一下在动态开点线段树中插入一个点会加入多少个新的节点
线段树从顶端到任意一个叶子结点之间有层,每层最多新增一个节点
所以插入一个新的点复杂度是的
线段树从顶端到任意一个叶子结点之间有层,每层最多新增一个节点
所以插入一个新的点复杂度是的
两棵线段树合并的复杂度显然取决于两棵线段树重合的叶子节点个数,假设有个重合的点,这两棵线段树合并的复杂度就是了,所以说,如果要合并两棵满满的线段树,这个复杂度绝对是远大于级别的。
也就是说,千万不要以为线段树合并对于任何情况都是的!
也就是说,千万不要以为线段树合并对于任何情况都是的!
那么为什么数据范围的题目线段树合并还稳得一批?
这是因为的复杂度仅适用于插入点少的情况。
如果与加入的总点数规模基本相同,我们就可以把它理解成每次操作
这是因为的复杂度仅适用于插入点少的情况。
如果与加入的总点数规模基本相同,我们就可以把它理解成每次操作
来证明一下:
假设我们会加入个点,由上面的结论,我们可以推出最多要新增个点。
而正如我们所知,每次合并两棵线段树同位置的点,就会少掉一个点,复杂度为,总共个点,全部合并的复杂度就是
假设我们会加入个点,由上面的结论,我们可以推出最多要新增个点。
而正如我们所知,每次合并两棵线段树同位置的点,就会少掉一个点,复杂度为,总共个点,全部合并的复杂度就是
可见,上面那个证明是只与插入点个数有关,也就是插入次数在左右、值域左右的题目,线段树合并还是比较稳的。
至于更快的方法?
网上有说可以用可并堆的思路合并,我太菜了,并没有试过,所以就点到为止了~
网上有说可以用可并堆的思路合并,我太菜了,并没有试过,所以就点到为止了~
对了,由上可知,因为插入个节点,所以线段树合并的空间复杂度也是的
三、线段树合并的用处
处理一些用其他数据结构不好处理的子树问题
大概是本来你对每个点开一棵线段树然后能维护的东西,线段树合并都能维护,因为他们的本质是一样的
大概是本来你对每个点开一棵线段树然后能维护的东西,线段树合并都能维护,因为他们的本质是一样的
常见的比如说查询子树内前驱后继第大,或者是第几大之类的权值线段树能干的各种事
以及出现最多的数,总和最大的数,这些权值线段树也能干的事
暂时还没有见过不是权值线段树的线段树合并。猜测一下大概是因为这种线段树合并复杂度会很鬼畜?
如果有的话也请大佬分享一下啦~
不好意思,我实在太菜了,被题意杀了好几发,所以强迫症的断了一下句,希望有助于大家理解。
这题只要dfs一下,将每个子节点的线段树与父亲节点线段树合并,再将父亲节点的颜色插入该节点对应的线段树,然后直接查询答案就可以了。
以及**远程评测BUG**QAQ
代码长这样:
CPP#include<cstdio>
#include<vector>
#include<cstring>
#include<iostream>
#include<algorithm>
#define lson tr[now].l
#define rson tr[now].r
#define int long long
using namespace std;
struct tree
{
int l,r,sum,val,ans;
}tr[5000050];
int rt[100010],cl[100010],cnt,n,anss[100010];
vector<int> g[100010];
int push_up(int now)
{
if(tr[lson].sum>tr[rson].sum)
{
tr[now].sum=tr[lson].sum;
tr[now].val=tr[lson].val;
tr[now].ans=tr[lson].ans;
}
if(tr[rson].sum>tr[lson].sum)
{
tr[now].sum=tr[rson].sum;
tr[now].val=tr[rson].val;
tr[now].ans=tr[rson].ans;
}
if(tr[lson].sum==tr[rson].sum)
{
tr[now].sum=tr[lson].sum;
tr[now].val=tr[lson].val;
tr[now].ans=tr[lson].ans+tr[rson].ans;
}
}
int update(int &now,int l,int r,int pos,int v)
{
if(!now) now=++cnt;
if(l==r)
{
tr[now].val=l;
tr[now].sum+=v;
tr[now].ans=l;
return 0;
}
int mid=(l+r)>>1;
if(pos<=mid) update(lson,l,mid,pos,v);
else update(rson,mid+1,r,pos,v);
push_up(now);
}
int merge(int a,int b,int l,int r)
{
if(!a) return b;
if(!b) return a;
if(l==r)
{
tr[a].val=l;
tr[a].sum+=tr[b].sum;
tr[a].ans=l;
return a;
}
int mid=(l+r)>>1;
tr[a].l=merge(tr[a].l,tr[b].l,l,mid);
tr[a].r=merge(tr[a].r,tr[b].r,mid+1,r);
push_up(a);
return a;
}
int dfs(int now,int f)
{
for(int i=0;i<g[now].size();i++)
{
if(g[now][i]==f) continue;
dfs(g[now][i],now);
merge(rt[now],rt[g[now][i]],1,100000);
}
update(rt[now],1,100000,cl[now],1);
anss[now]=tr[rt[now]].ans;
}
int main()
{
scanf("%lld",&n);
for(int i=1;i<=n;i++)
{
scanf("%lld",&cl[i]);
rt[i]=i;
cnt++;
}
int from,to;
for(int i=1;i<n;i++)
{
scanf("%lld%lld",&from,&to);
g[from].push_back(to);
g[to].push_back(from);
}
dfs(1,0);
for(int i=1;i<=n;i++)
{
printf("%lld ",anss[i]);
}
}
一个小小的trick是在最早一次插入之前将所有点的根先编号,这样子可能即使写翻了也有概率A掉(因为)
四、线段树合并与平衡树启发式合并的比较
splay启发式合并的思路大概是把一棵较小splay的值全部拿出来插到大的splay里,这样子复杂度大概是的,不过如果你按照较小的中序遍历的顺序插入,复杂度大概也是级别的
但是但是,线段树合并代码这么短,能用线段树合并解决的题,为什么要去写平衡树启发式合并呢qwq
upd.大佬告诉蒟蒻splay合并的空间更优,可以达到,涨姿势了~
五、一些例题
牛客网NOIP赛前集训营-提高组(第一场)T3 保护
首先还是要差分一下,这题可以转化为求子树中第浅的链所在的位置,线段树合并完就是权值线段树查询大值得内容了,最后答案是当前点的深度减去找到的点的深度与取个较大值就可以了
首先还是要差分一下,这题可以转化为求子树中第浅的链所在的位置,线段树合并完就是权值线段树查询大值得内容了,最后答案是当前点的深度减去找到的点的深度与取个较大值就可以了
洛谷P3224永无乡
因为只有新增的桥,我们会想到并查集,问题转化成如何求一个并查集里的小值,怎么办呢?当然是线段树合并了!我们在将搞成的父亲是顺便把的线段树合并到上就可以啦,接着就是权值线段树查询小值的内容,显然不用再讲了。
因为只有新增的桥,我们会想到并查集,问题转化成如何求一个并查集里的小值,怎么办呢?当然是线段树合并了!我们在将搞成的父亲是顺便把的线段树合并到上就可以啦,接着就是权值线段树查询小值的内容,显然不用再讲了。
某同学要求加一道线段树合并在提高组的应用,那就这题吧qwq
考虑把一个人的路径拆成到和到两段,差分一下用线段树维护
具体的操作是对起点插入,终点插入起点,相当于把起点沿翻上去。 然后线段树合并一波就搞定了
查询的是距离每个点和的点有几个
考虑把一个人的路径拆成到和到两段,差分一下用线段树维护
具体的操作是对起点插入,终点插入起点,相当于把起点沿翻上去。 然后线段树合并一波就搞定了
查询的是距离每个点和的点有几个
对了,闲(yi)得(jing)无(A)聊(K)的同学们可以玩玩这道题CF666E
标算好像是广义后缀自动机+线段树合并来着
标算好像是广义后缀自动机+线段树合并来着
相关推荐
评论
共 57 条评论,欢迎与作者交流。
正在加载评论...