长链剖分

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

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

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

性质

其实没啥性质。一个性质是长链剖分从一个节点到根的路径上轻重边切换次数是 \(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;
}
posted @ 2023-02-08 14:36  gtm1514  阅读(19)  评论(0编辑  收藏  举报