P4556 [Vani有约会] 雨天的尾巴 /【模板】线段树合并题解
UPD:对代码中的注释进行了一定的修改。
本文所说线段树均指权值线段树。
前置知识
- 线段树(
废话)。 - 树上差分。
树上差分
已经会的巨佬们可以跳过。
讲差分是一定要先讲前缀和的。树上前缀和有两种,一种是从根节点到当前节点的路径权值和,一种是这个节点的子树和。这里我们只关心后一种前缀和。
容易发现只要从下往上,将每个节点的所有儿子合并再与它自己合并就可以求出树上前缀和。
由此可以启发出树上差分的方法。
假设我们要让点
线段树合并
一般情况
先不考虑树上,先来考虑普通的两棵线段树如何合并。
将两棵线段树合并,那么其实就是建一棵新的线段树,将两棵线段树维护的信息合并在新树上。权值线段树维护的是桶,将两个桶对应下标的值相加即为合并。
那么显然,对于叶子节点,将其维护的值相加即可。对于非叶子节点,直接 push_up 即可。具体实现就是一个 dfs 的过程,非常简单。
实际为了节约空间复杂度,往往选择将一棵线段树合并到另一棵上。
设树的大小为
这时就要用到权值线段树的一个重要技巧:动态开点。这样可以最大化节约内存。如果合并时,遇到其中一个线段树的当前节点为空,那么直接将另一个节点作为新线段树对应位置的儿子即可。即共用这个儿子。
复杂度分析
将两个线段树合并时,遇到空节点直接返回,也就是只有两个线段树的重合部分会被 dfs 到。所以总的合并次数不会超过较小的线段树的节点数。即单次合并复杂度不会超过
如果进行了很多次合并,那么时间复杂度依然是
这一部分参考了这一篇题解。
由上面的分析也可知空间复杂度为
树上合并
和一般情况其实并没有什么区别。用树上前缀和的思想,将一个节点所有儿子对应的线段树合并,然后再和它自身合并即可。
本题解法
对于每一个节点,开一颗权值线段树,维护值域最大值。修改时利用树上差分进行修改。然后进行树上前缀和合并即可。
时空复杂度
代码
代码有较为详细的注释。
#include<bits/stdc++.h>
#define ll int
#define dd double
#define lson c[id].ls
#define rson c[id].rs
#define mid ((l+r)/2)
using namespace std;
inline ll read(){
ll x=0,f=1;
char ch=getchar();
while(ch<'0'||ch>'9'){
if(ch=='-'){
f=-1;
}
ch=getchar();
}
while(ch>='0'&&ch<='9'){
x=x*10+ch-'0';
ch=getchar();
}
return x*f;
}
void write(ll x){
if(x<0){
putchar('-');
x=-x;
}
if(x>9){
write(x/10);
}
putchar(x%10+'0');
}
const ll N=1e5+9,R=1e5;
ll n,m;
ll ccnt;//动态开点计数器
struct NODE{
ll ls,rs,sum,lei;//左右儿子,最大值的大小和种类
}c[N*80];//权值树
//由于对于每次修改,根据树上差分的思想,只需要修改四个点,由于动态开点
//至多只需要新开4*logn个点,合并是可以共用节点
//所以空间复杂度o(n*4*logn)
ll root[N*80];//以i号节点为根的权值树的根节点编号
ll deep[N],fa[N][20];//lca
//链式前向星
ll h[N*2],bcnt;
struct LIAN{
ll to,ne;
}b[N*2];
ll ans[N];
inline void addb(ll u,ll v){
b[++bcnt].ne=h[u];
b[bcnt].to=v;
h[u]=bcnt;
}
//lca
void init(ll now,ll father){
fa[now][0]=father;
deep[now]=deep[father]+1;
for(int i=1;i<=19;i++){
fa[now][i]=fa[fa[now][i-1]][i-1];
}
for(ll i=h[now];i;i=b[i].ne){
if(b[i].to==father){
continue;
}
init(b[i].to,now);
}
return;
}
ll lca(ll u,ll v){
if(deep[u]!=deep[v]){
if(deep[u]<deep[v]){
swap(u,v);
}
for(int i=19;i>=0;i--){
if(deep[fa[u][i]]>=deep[v]){
u=fa[u][i];
}
}
}
if(u==v){
return u;
}
for(int i=19;i>=0;i--){
if(fa[u][i]!=fa[v][i]){
u=fa[u][i];
v=fa[v][i];
}
}
return fa[u][0];
}
//权值线段树
void pu(ll id){//push_up
if(lson==0){
c[id].sum=c[rson].sum;
c[id].lei=c[rson].lei;
return;
}
if(rson==0){
c[id].sum=c[lson].sum;
c[id].lei=c[lson].lei;
return;
}
if(c[lson].sum>=c[rson].sum ){
c[id].sum =c[lson].sum ;
c[id].lei =c[lson].lei ;
}
if(c[lson].sum <c[rson].sum ){
c[id].sum =c[rson].sum ;
c[id].lei =c[rson].lei ;
}
}
void change(ll &id,ll l,ll r,ll v,ll val){
if(!id){
id=++ccnt;
}
if(l==r){
c[id].sum +=val;//l==r时,此区间只有一种救济粮,可以直接相加
c[id].lei =v;
return;
}
if(v<=mid){
change(lson,l,mid,v,val);
}
else{
change(rson,mid+1,r,v,val);
}
pu(id);
}
ll merge(ll u,ll v,ll l,ll r){//将子线段树v合并到子线段树u上
if(u==0||v==0){
return u+v;
}
if(l==r){
c[u].sum +=c[v].sum ;
c[u].lei=l;
return u;
}
c[u].ls =merge(c[u].ls ,c[v].ls ,l,mid);
//关于这里为什么要给c[u].ls赋值:
//当u.ls等0而v.ls不等0时,为减少复杂度,让u.ls共用v.ls这个节点。
c[u].rs =merge(c[u].rs ,c[v].rs ,mid+1,r);//同上
pu(u);
return u;
}
void cacl(ll id,ll fa){//统计答案
for(int i=h[id];i;i=b[i].ne ){
ll u=b[i].to ;
if(u==fa)continue;
cacl(u,id);
root[id]=merge(root[id],root[u],1,R);
//这里与上文同理
}
if(c[root[id]].sum ==0){
ans[id]=0;
}
else{
ans[id]=c[root[id]].lei ;
}
}
int main(){
// freopen("P4556_15.in","r",stdin);
n=read();
m=read();
for(int i=1;i<n;i++){
ll a=read(),b=read();
addb(a,b);
addb(b,a);
}
init(1,0);
for(int i=1;i<=m;i++){
ll x=read(),y=read(),z=read();
change(root[x],1,R,z,1);
change(root[y],1,R,z,1);
change(root[lca(x,y)],1,R,z,-1);
change(root[fa[lca(x,y)][0]],1,R,z,-1);//树上差分
}
cacl(1,0);
for(int i=1;i<=n;i++){
write(ans[i]);
putchar('\n');
}
return 0;
}