首页 > 其他分享 >长链剖分

长链剖分

时间:2023-02-08 14:45:03浏览次数:34  
标签:长链 head 剖分 int son edge include 节点

现在的情况是正在找一些比较轻量化的题单来写。

长链剖分。我们知道树的链剖分有三个,重剖,长剖,还有一个不是很正统的实链剖分。

重链剖分就是每次找子树最大的一个当做重儿子。长链剖分就是每次找子树内深度最大的一个。

性质

其实没啥性质。一个性质是长链剖分从一个节点到根的路径上轻重边切换次数是 \(O(\sqrt n)\) 的。证明考虑每次往上跳的长链长度都一定是递增的,然后到根节点的所有链长在最坏情况下就是 \(1-x\) 之和,为 \(\dfrac{x(x+1)}2\),所以是根号级别的。

一些应用

  1. 树上 \(k\) 级祖先

长链剖分可以做到 \(O(n\log n)\) 预处理,在线 \(O(1)\) 回答询问。

首先我们预处理每个节点往上的所有第 \(2^k\) 个祖先。然后对于每条重链顶端的节点,设重链长度为 \(d\) ,那么我们在这个节点的位置存一下向上的 \(d\) 个节点和重链里的所有节点。

查询的时候我们找到一个最大的 \(2^i\) 满足 \(2^i\le k\)。那么根据长链剖分的性质,每次往上跳一步长链的长度都要增加,就有 \(2^i\le d\),也就有 \(k-2^i\le d\)。直接在这条长链的顶端节点里找就行了。
2. 其他应用

「JOI 2019 Final」独特的城市

不太好说这是个什么东西。

首先我们发现对于每个节点有贡献的部分一定是离这个节点更远的一个直径端点往前一段。那么我们可以从两个直径端点分别处理所有点的可能答案。

用一个栈保存在这个节点处所有能做出贡献的节点。然后每次到达一个节点,由于与这个节点距离不超过这个节点和第二深的子树的距离的所有点一定没有贡献,直接弹栈。然后考虑最深的子树,直接继续向下 dfs 之后把所有距离不超过最深的子树距离的所有节点弹栈,剩下的点就是能做出贡献的点。然后搜所有的儿子,每次搜之前记得将当前节点入栈。

#include <iostream>
#include <algorithm>
#include <cstdio>
#include <cstring>
#include <vector>
using namespace std;
int n,m;
struct node{
    int v,next;
}edge[400010];
int t,S,T,D,head[200010],col[200010],s[200010],ans[200010];
void add(int u,int v){
    edge[++t].v=v;edge[t].next=head[u];head[u]=t;
}
int son[200010],dep[200010],sec[200010],dis[200010];
void dfs1(int x,int f,int d){
    for(int i=head[x];i;i=edge[i].next){
        if(edge[i].v!=f)dfs1(edge[i].v,x,d+1);
    }
    if(d>D)D=d,S=x;
}
void dfs2(int x,int f){
    dep[x]=dep[f]+1;son[x]=sec[x]=0;
    for(int i=head[x];i;i=edge[i].next){
        if(edge[i].v!=f){
            dfs2(edge[i].v,x);
            if(dis[son[x]]<dis[edge[i].v])sec[x]=son[x],son[x]=edge[i].v;
            else if(dis[sec[x]]<dis[edge[i].v])sec[x]=edge[i].v;
        }
    }
    dis[x]=dis[son[x]]+1;
}
int cnt;
void add(int x){
    if(!s[col[x]])cnt++;
    s[col[x]]++;
}
void del(int x){
    s[col[x]]--;
    if(!s[col[x]])cnt--;
}
int top,stk[200010];
void dfs(int x,int f){
    while(top&&dep[x]-dep[stk[top]]<=dis[sec[x]])del(stk[top]),top--;
    stk[++top]=x;add(x);
    if(son[x])dfs(son[x],x);
    while(top&&dep[x]-dep[stk[top]]<=dis[son[x]])del(stk[top]),top--;
    ans[x]=max(ans[x],cnt);
    for(int i=head[x];i;i=edge[i].next){
        if(edge[i].v!=f&&edge[i].v!=son[x]){
            if(stk[top]!=x)stk[++top]=x,add(x);
            dfs(edge[i].v,x顺手看了看昨夜CF,不会,润了。
    }
    if(stk[top]==x)del(x),top--;
}
signed main(){
    scanf("%d%d",&n,&m);
    for(int i=1;i<n;i++){
        int u,v;scanf("%d%d",&u,&v);
        add(u,v);add(v,u);
    }
    for(int i=1;i<=n;i++)scanf("%d",&col[i]);
    dfs1(1,0,0);D=0;swap(S,T);
    dfs1(T,0,0);
    dfs2(S,0);dfs(S,0);
    dfs2(T,0);dfs(T,0);
    for(int i=1;i<=n;i++)printf("%d\n",ans[i]);
    return 0;
}

长链剖分优化 dp

通常长剖都是拿来干这个的。一类状态里带着深度一维的都可以用这个优化,复杂度砍掉一个 \(n\)。举几个例子。

CF1099F Dominant Indices

题意:设 \(d(u,x)\) 为 \(u\) 子树中到 \(u\) 距离为 \(x\) 的点数,对于每个点求使 \(d(u,k)\) 最大的 \(k\),有多个取最小。

设 \(dp_{x,i}\) 为在 \(x\) 节点子树中距离为 \(i\) 的点数,那么转移有:

\[dp_{x,0}=1 \]

\[dp_{x,i}=\sum_{v\in son} dp_{v,i-1} \]

这个看起来就很 \(O(n^2)\)。然后第二维带个深度,可以用长链剖分优化。

具体来讲,我们对于每个链顶开一块和链长一样大的空间(一般指针实现),然后类似启发式合并,每条链的答案都在自己这条链的空间里,先处理重儿子然后暴力合并轻儿子。具体的可以看代码。

#include <iostream>
#include <algorithm>
#include <cstdio>
#include <cstring>
using namespace std;
int n;
struct node{
    int v,next;
}edge[2000010];
int t,head[1000010];
void add(int u,int v){
    edge[++t].v=v;edge[t].next=head[u];head[u]=t;
}
int dep[1000010],son[1000010];
void dfs1(int x,int f){
    for(int i=head[x];i;i=edge[i].next){
        if(edge[i].v!=f){
            dfs1(edge[i].v,x);
            if(dep[son[x]]<dep[edge[i].v])son[x]=edge[i].v;
        }
    }
    dep[x]=dep[son[x]]+1;
}//长链剖分
int buf[1000010],ans[1000010];
int *dp[1000010],*now=buf;
void dfs2(int x,int f){
    dp[x][0]=1;
    if(son[x]){
        dp[son[x]]=dp[x]+1;//先搜重儿子 这里相当于转移方程里重儿子的贡献部分
        dfs2(son[x],x);
        ans[x]=ans[son[x]]+1;//继承重儿子答案
    }
    for(int i=head[x];i;i=edge[i].next){
        if(edge[i].v!=f&&edge[i].v!=son[x]){
            dp[edge[i].v]=now;now+=dep[edge[i].v];
            dfs2(edge[i].v,x);
            for(int j=1;j<=dep[edge[i].v];j++){//暴力合并轻儿子
                dp[x][j]+=dp[edge[i].v][j-1];
                if(dp[x][j]>dp[x][ans[x]]||(dp[x][j]==dp[x][ans[x]]&&j<ans[x]))ans[x]=j;
            }
        }
    }
    if(dp[x][ans[x]]==1)ans[x]=0;
}
int main(){
    scanf("%d",&n);
    for(int i=1;i<n;i++){
        int u,v;scanf("%d%d",&u,&v);
        add(u,v);add(v,u);
    }
    dfs1(1,0);
    dp[1]=now;now+=dep[1];
    dfs2(1,0);
    for(int i=1;i<=n;i++)printf("%d\n",ans[i]);
    return 0;
}

[湖南集训]谈笑风生

这玩意现在洛谷叫更为厉害,题面也改过了。题意不再叙述。

首先 \(b\) 是 \(a\) 的祖先的情况下答案是显然的。然后考虑 \(b\) 在 \(a\) 子树中的情况。

仍然搞个 dp:\(dp_{x,i}\) 为 \(x\) 节点 \(k\le i\) 时的答案。转移仍然有:

\[dp_{x,i}=\sum_{v\in son_x}dp_{v,i-1}+size_v-1 \]

仍然是长链剖分的套路式子,但是后边带了个常数。显然我们不能扫一遍一个一个加上。

发现后面每个都要加一次,那直接打个标记完事。然而如果这样的话到了上边的地方超出了 \(i\) 的距离仍然会算上标记。

这个的处理方法是把 \(dp_{x,0}\) 减去标记。这样每次向上传递都会在和 \(x\) 的距离位置减一次,相当于抵消了影响。

#include <iostream>
#include <algorithm>
#include <cstdio>
#include <cstring>
#include <vector>
#define int long long
using namespace std;
int n,m;
struct node{
    int v,next;
}edge[600010];
int t,head[300010];
struct ques{
    int p,k;
}q[300010];
vector<int>v[300010];
void add(int u,int v){
    edge[++t].v=v;edge[t].next=head[u];head[u]=t;
}
int dep[300010],son[300010],dis[300010],size[300010];
void dfs1(int x,int f){
    dep[x]=dep[f]+1;size[x]=1;
    for(int i=head[x];i;i=edge[i].next){
        if(edge[i].v!=f){
            dfs1(edge[i].v,x);
            size[x]+=size[edge[i].v];
            if(dis[son[x]]<dis[edge[i].v])son[x]=edge[i].v;
        }
    }
    dis[x]=dis[son[x]]+1;
}
int buf[300010],ans[300010],lz[300010];
int *dp[300010],*now=buf;
void dfs2(int x,int f){
    if(son[x]){
        dp[son[x]]=dp[x]+1;
        dfs2(son[x],x);
        dp[x][0]=0;
        lz[x]=lz[son[x]]+size[son[x]]-1;
    }
    for(int i=head[x];i;i=edge[i].next){
        if(edge[i].v!=son[x]&&edge[i].v!=f){
            dp[edge[i].v]=now;now+=dis[edge[i].v];
            dfs2(edge[i].v,x);
            lz[x]+=lz[edge[i].v]+size[edge[i].v]-1;
            for(int j=1;j<=dis[edge[i].v];j++)dp[x][j]+=dp[edge[i].v][j-1];
        }
    }
    dp[x][0]=-lz[x];
    for(int id:v[x]){
        ans[id]=dp[x][min(q[id].k,dis[x]-1)]+lz[x];
    }
}
signed main(){
    scanf("%lld%lld",&n,&m);
    for(int i=1;i<n;i++){
        int u,v;scanf("%lld%lld",&u,&v);
        add(u,v);add(v,u);
    }
    for(int i=1;i<=m;i++){
        int p,k;scanf("%lld%lld",&q[i].p,&q[i].k);
        v[q[i].p].push_back(i);
    }
    dfs1(1,0);
    dp[1]=now;now+=dis[1];
    dfs2(1,0);
    for(int i=1;i<=m;i++)printf("%lld\n",ans[i]+min(dep[q[i].p]-1,q[i].k)*(size[q[i].p]-1));
    return 0;
}

[POI2014]HOT-Hotels

题意:树上选三个点距离相等方案数。

首先容易发现只有两种情况:三个节点在同一个点子树内,或者两个点在另一个点子树内。

那么可以设 \(f_{x,i}\) 为 \(x\) 子树内到 \(x\) 距离为 \(i\) 的方案数,\(g_{x,i}\) 为 \(x\) 子树内满足 \(a,b\) 在 \(x\) 子树内且 \(d(\text{lca}(a,b),a)=d(\text{lca}(a,b),b)=d(\text{lca}(a,b),i)+j\) 的无序数对 \((a,b)\) 的个数。那么考虑转移:

第一种情况对答案的贡献就是 \(\sum_{a,b\in son_x,a\neq b}f_{a,j-1}g_{b,j+1}\)。第二种情况显然是 \(g_{x,0}\)。然后考虑 \(f,g\) 的转移。\(f\) 的转移上边已经有过。

\[g_{x,i}=\sum_{a,b\in son_x,a\neq b}f_{a,i-1}f_{b,i-1}+\sum_{v\in son_x}g_{v,i+1} \]

那么这几个求和就可以乘法分配律一下 \(O(n)\) 转移了。然后套个长链剖分。

然而有一个注意的点:由于在转移重儿子的时候 \(g\) 是倒着开的(从 \(g_{i+1}\) 转移到 \(g_i\)),所以 \(g\) 需要先开空间再上指针,而且空间要开两倍。

#include <iostream>
#include <algorithm>
#include <cstdio>
#include <cstring>
#include <vector>
#define int long long
using namespace std;
int n,m,ans;
struct node{
    int v,next;
}edge[200010];
int t,head[100010];
void add(int u,int v){
    edge[++t].v=v;edge[t].next=head[u];head[u]=t;
}
int son[100010],dis[100010];
void dfs1(int x,int f){
    for(int i=head[x];i;i=edge[i].next){
        if(edge[i].v!=f){
            dfs1(edge[i].v,x);
            if(dis[son[x]]<dis[edge[i].v])son[x]=edge[i].v;
        }
    }
    dis[x]=dis[son[x]]+1;
}
int buf1[1000010],buf2[2000010];
int *dp[100010],*g[100010],*now1=buf1,*now2=buf2;
void dfs2(int x,int f){
    dp[x][0]=1;
    if(son[x]){
        dp[son[x]]=dp[x]+1;g[son[x]]=g[x]-1;
        dfs2(son[x],x);
    }
    ans+=g[x][0];
    for(int i=head[x];i;i=edge[i].next){
        if(edge[i].v!=son[x]&&edge[i].v!=f){
            dp[edge[i].v]=now1;now1+=dis[edge[i].v];
            now2+=(dis[edge[i].v]<<1)+10;g[edge[i].v]=now2;now2++;
            dfs2(edge[i].v,x);
            for(int j=0;j<dis[edge[i].v];j++){
                if(j)ans+=dp[x][j-1]*g[edge[i].v][j];
                ans+=g[x][j+1]*dp[edge[i].v][j];
            }
            for(int j=0;j<dis[edge[i].v];j++){
                g[x][j+1]+=dp[x][j+1]*dp[edge[i].v][j];
                if(j)g[x][j-1]+=g[edge[i].v][j];
                dp[x][j+1]+=dp[edge[i].v][j];
            }
        }
    }
}
signed main(){
    scanf("%lld",&n);
    for(int i=1;i<n;i++){
        int u,v;scanf("%lld%lld",&u,&v);
        add(u,v);add(v,u);
    }
    dfs1(1,0);
    dp[1]=now1;now1+=dis[1];
    now2+=(dis[1]<<1)+10;g[1]=now2;now2++;
    dfs2(1,0);
    printf("%lld\n",ans);
    return 0;
}

标签:长链,head,剖分,int,son,edge,include,节点
From: https://www.cnblogs.com/gtm1514/p/17101701.html

相关文章

  • 2019年ICPC南昌网络赛 J. Distance on the tree(树链剖分+主席树 查询路径边权第k大)
    DSM(DataStructureMaster)oncelearnedabouttreewhenhewaspreparingforNOIP(NationalOlympiadinInformaticsinProvinces)inSeniorHighSchool.Sowhen......
  • 长链剖分
    概述长链剖分通过把树剖成尽量长的多个链,高效地解决...我也不知道解决啥(长剖优化DP的东西在DP优化那边)。毕竟这个东西,不具备启发式分裂的复杂度。不过其还是有一......
  • 轻重链剖分
    概述轻重链剖分通过将树剖分为若干条重链和它们之间相连的轻边,将树上路径问题转化成序列问题。具体来讲,有很多树上路径问题本质上是把序列上的问题搬到了树上,此时我......
  • 关于长链剖分的数组实现 | CF1009F Dominant Indices
    请容许我不理解一下为什么这题题解几乎全都是指针实现/kk其实长链剖分是可以直接用数组来写的。考虑朴素DP。设\(f_{u,i}\)表示以点\(u\)为根的子树中与点\(u\)距......
  • 树链剖分
    dfs序与树链剖分dfs序比如dfs序为:ABDGHICEJF时间戳时间戳即dfs每次访问到每个节点的时间,时间从1开始累加比如上图时间戳为用处把树强行搞成连续的......
  • Codeforces-343D Water Tree(树链剖分)
    Description:MadscientistMikehasconstructedarootedtree,whichconsistsof n vertices.Eachvertexisareservoirwhichcanbeeitheremptyorfilledw......
  • SPOJ375--Query on a tree(树链剖分)
    Description:Youaregivenatree(anacyclicundirectedconnectedgraph)with N nodes,andedgesnumbered1,2,3...N-1.Wewillaskyoutoperfromsomeins......
  • 树链剖分
    “在一棵树上进行路径的修改、求极值、求和”乍一看只要线段树就能轻松解决,实际上,仅凭线段树是不能搞定它的。我们需要用到一种貌似高级的复杂算法——树链剖分。树链剖分......
  • 树链剖分入门
    目录树链剖分算法思想模版-树链剖分旅行P4374P4211CF1023FP1505P2486P7735P3976Trick总结树链剖分这玩意也是才开始预习,写得不好勿喷。约定记号:\(siz[x]\),\(x\)为根的......
  • 树链剖分学习笔记
    怕到时候忘了,来写一篇笔记前置芝士:树的存储与遍历,\(dfs\)序,线段树。树链剖分的思想及能解决的问题:树链剖分用于将树分割成若干条链的形式,以维护树上路径的信息。具体......