Loading

长链剖分

类似于轻重链剖分,也有一种树链剖分方式叫做长链剖分,每次我们选子树深度最大的节点当做重儿子,注意这里子树深度是指一棵子树中所有结点的深度最大值。

有以下性质:

  • 一个节点的 \(k\) 级祖先所在链的长度一定大于等于 \(k\)

比较显然。因为如果小于 \(k\) 的话就会选从那个祖先到这个节点的路径当做重链。

  • 所有链的总长是 \(O(n)\)

比较显然。因为重链不交,可以近似看做是对整个树的划分。

  • 任意一个点向上跳跃重链的次数不会超过 \(\sqrt n\) 次。

我们一直向上跳,每次跳到的一个点所在重链长度最小是上个点的重链长度加 \(1\),所以我们一直经过的重链长度最多是 \(1,2,...\sqrt n\),由此可知性质得证。

\(k\) 级祖先

模板题 P5903

\(k\) 级祖先,长链剖分可以做到 \(O(n\log n)\) 预处理,\(O(1)\) 查询。

具体做法如下:我们倍增处理所有结点的祖先,然后对于每个链,设链长为 \(len\),处理出该链链顶向上,向下(顺着链向下)长度为 \(len\) 的每一个点。这是预处理。

对一个查询,设为 \(x,k\),表示我们要查询 \(x\)\(k\) 级祖先,我们考虑设 \(r\)\(k\) 的最高二进制位表示的那个二进制数,我们考虑寻找 \(x\)\(2^r\) 祖先,由性质 \(1\),我们可以得到这条重链的长度一定是大于等于 \(2^r\) 的,这样我们就可以保证,我们要查询的点,一定是被我们预处理过了。所以我们可以通过判断被查询点和该链顶点的深度来进行 \(O(1)\) 的查询。

代码:


#include<bits/stdc++.h>
#define dd double
#define ld long double
#define ll long long
#define uint unsigned int
#define ull unsigned long long
#define N 500010
#define M number
using namespace std;

const int INF=0x3f3f3f3f;

template<typename T> inline void read(T &x) {
    x=0; int f=1;
    char c=getchar();
    for(;!isdigit(c);c=getchar()) if(c == '-') f=-f;
    for(;isdigit(c);c=getchar()) x=x*10+c-'0';
    x*=f;
}

struct edge{
    int to,next;
    inline void Init(int to_,int ne_){
        to=to_;next=ne_;
    }
}li[N<<1];
int head[N],tail;

inline void Add(int from,int to){
    li[++tail].Init(to,head[from]);
    head[from]=tail;
}

int fa[N][21],Son[N],Dep[N],Deep[N],Top[N];
vector<int> u[N],d[N];

inline void dfs(int k,int fat){
    fa[k][0]=fat;for(int i=1;i<=20;i++) fa[k][i]=fa[fa[k][i-1]][i-1];
    Dep[k]=Dep[fat]+1;Deep[k]=Dep[k];
    for(int x=head[k];x;x=li[x].next){
        int to=li[x].to;
        if(to==fat) continue;
        dfs(to,k);Deep[k]=max(Deep[k],Deep[to]);
        if(Deep[Son[k]]<Deep[to]) Son[k]=to;
    }
}

inline void dfs2(int k,int t){
    Top[k]=t;
    if(k==t){
        int len=Deep[k]-Dep[t];
        d[k].push_back(k);int now=k;
        for(int i=1;i<=len;i++){
            now=Son[now];if(now==0) break;
            d[k].push_back(now);
        }
        u[k].push_back(k);now=k;
        for(int i=1;i<=len;i++){
            now=fa[now][0];if(now==0) break;
            u[k].push_back(now);
        }
    }
    if(Son[k]) dfs2(Son[k],t);
    for(int x=head[k];x;x=li[x].next){
        int to=li[x].to;
        if(to==fa[k][0]||to==Son[k]) continue;
        dfs2(to,to);
    }
}

uint s;
ll Ans;

inline uint get(uint x) {
	x ^= x << 13;
	x ^= x >> 17;
	x ^= x << 5;
	return s = x; 
}

int n,q,root,ans,lg2[N];

inline void Ask(){
    int x=((get(s)^ans)%n)+1;
    int k=(get(s)^ans)%Dep[x];
    // printf("x=%d k=%d\n",x,k);
    if(k==0){ans=x;return;}
    int nowx=fa[x][lg2[k]];int nowk=k-(1<<lg2[k]);
    // printf("nowx=%d nowk=%d\n",nowx,nowk);
    int t=Top[nowx],dep=Dep[nowx]-nowk;
    // printf("t=%d dep=%d\n",t,dep);
    // printf("Dep[t]=%d\n",Dep[t]);
    if(dep>Dep[t]) ans=d[t][dep-Dep[t]];
    else if(dep<=Dep[t]) ans=u[t][Dep[t]-dep];
}

signed main(){
    // freopen("my.in","r",stdin);
    // freopen("my.out","w",stdout);
    read(n);read(q);read(s);
    lg2[0]=-1;for(int i=1;i<=n;i++) lg2[i]=lg2[i>>1]+1;
    for(int i=1;i<=n;i++){
        int fi;read(fi);
        if(fi==0) root=i;else Add(fi,i);
    }
    dfs(root,0);dfs2(root,root);
    for(int i=1;i<=q;i++){
        Ask();
        Ans^=(1ll*i*ans);
        // printf("ans=%d\n",ans);
    }
    printf("%lld\n",Ans);
    return 0;
}

长链剖分优化 dp

具体方法是,我们在合并信息的时候,直接把重儿子的承接过来,其余儿子的信息暴力统计,这样做的话时间复杂度是 \(O(n)\) 的。

考虑证明。当一个儿子的信息被暴力统计时,这个儿子一定是其所在重链的链顶,那么合并这个儿子的信息的时间复杂度可以看做其所在重链的长度。

由此可以看出,总的暴力合并的复杂度不会超过链的总长,由此可以得到时间复杂度为 \(O(n)\)

P3565

P5904 是这个题目的加强版。

一个很妙的 dp 设计是设 \(f_{i,j}\) 为在 \(i\) 的子树内的与 \(i\) 距离为 \(j\) 的点的个数。设 \(f_{i,j}\) 表示在 \(i\) 的子树内的满足 \(d(a,lca(a,b))=d(b,lca(a,b))=d(i,lca(a,b))+j\) 的无序点对 \((a,b)\) 的个数,其中 \(a,b\) 不相等。

这个状态不是正常人能够想到的。尤其是后面那个 \(g\)。我们考虑转移,容易发现:

\[ f_{i,j}=\sum\limits_v f_{v,j-1}\\ g_{i,j}=\sum\limits_v g_{v,j+1}+f'_{i,j}*f_{v,j-1} \]

上面的式子中,\(v\)\(i\) 的一个子节点。第二个式子中的 \(f'\) 表示还没有与 \(f_v\) 合并的 \(f_i\)

上面那个式子显然是正确的。不难发现答案其实就是:

\[ ans=\sum\limits_{u} g_{u,0}\\ ans=\sum\limits_{u}\sum\limits_{v\in son_u}\sum\limits_{i} f_{u,i}g_{v,i+1}+f_{v,i-1}g_{u,i} \]

需要注意在用指针实现的时候,一定要开一个数组当做内存池。先给每个指针一个内存空间。


#include<bits/stdc++.h>
#define dd double
#define ld long double
#define ll long long
#define int long long
#define uint unsigned int
#define ull unsigned long long
#define N 500010
#define M number
using namespace std;

const int INF=0x3f3f3f3f;

template<typename T> inline void read(T &x) {
    x=0; int f=1;
    char c=getchar();
    for(;!isdigit(c);c=getchar()) if(c == '-') f=-f;
    for(;isdigit(c);c=getchar()) x=x*10+c-'0';
    x*=f;
}

struct edge{
    int to,next;
    inline void Init(int to_,int ne_){
        to=to_;next=ne_;
    }
}li[N<<1];
int head[N],tail;

inline void Add(int from,int to){
    li[++tail].Init(to,head[from]);
    head[from]=tail;
}

int n;
int *f[N],*g[N];
// int f[N][N],g[N][N];
int fa[N],Son[N],Dep[N],Deep[N],Ans,p[N<<2];
int *o=p;

inline void dfs(int k,int fat){
    fa[k]=fat;Dep[k]=Dep[fat]+1;Deep[k]=Dep[k];
    for(int x=head[k];x;x=li[x].next){
        int to=li[x].to;
        if(to==fat) continue;
        dfs(to,k);Deep[k]=max(Deep[to],Deep[k]);
        if(Deep[Son[k]]<Deep[to]) Son[k]=to;
    }
}

inline void Dp(int k){
    // printf("k=%d\n",k);
    if(Son[k]){
        f[Son[k]]=f[k]+1;g[Son[k]]=g[k]-1;Dp(Son[k]);
    }
    Ans+=g[k][0];
    f[k][0]=1;
    for(int x=head[k];x;x=li[x].next){
        int to=li[x].to;
        if(to==fa[k]||to==Son[k]) continue;
        // if(to==fa[k]) continue;
        int dep=Deep[to]-Dep[to];
        f[to]=o;o+=((dep+1)<<1);g[to]=o;o+=((dep+1)<<1);
        Dp(to);
        for(int i=0;i<=dep;i++){
            Ans+=f[to][i]*g[k][i+1];
            if(i) Ans+=f[k][i-1]*g[to][i];
        }
        for(int i=0;i<=dep;i++){
            g[k][i+1]+=f[k][i+1]*f[to][i];
            if(i) g[k][i-1]+=g[to][i];
            f[k][i+1]+=f[to][i];
        }
    }
}

signed main(){
    // freopen("my.in","r",stdin);
    // freopen("my.out","w",stdout);
    read(n);
    for(int i=1;i<=n-1;i++){
        int from,to;read(from);read(to);Add(from,to);Add(to,from);
    }
    dfs(1,1);
    f[1]=o;o+=(Deep[1]<<1);g[1]=o;o+=(Deep[1]<<1);
    Dp(1);
    printf("%lld\n",Ans);
    return 0;
}

P3899

这个题要注意的一点是,在转移的时候因为带有常数,我们用一个 \(tag_k\) 数组表示 \(f_k\) 加上 \(tag_k\) 之后才是我们原先的数组。所以我们需要对一些 \(f_k\) 上的值进行处理。

还需要注意的一点事这个题的转移范围并不是 \(f_{to}\) 的范围,因为 \(k\) 只是一个上界,实际上 \(f_k\) 后面的值还需要赋值。

说的有一点抽象,不妨试一下这个样例:


1 2
2 3
3 4
4 5
1 6
6 7

这个样例在合并信息到 \(1\) 节点的时候,转移的上界不是 \(h_2-1\),所以我们需要对整个数组再次进行修正。


#include<bits/stdc++.h>
#define dd double
#define ld long double
#define ll long long
#define int long long
#define uint unsigned int
#define ull unsigned long long
#define N 300100
#define M number
using namespace std;

const int INF=0x3f3f3f3f;

template<typename T> inline void read(T &x) {
    x=0; int f=1;
    char c=getchar();
    for(;!isdigit(c);c=getchar()) if(c == '-') f=-f;
    for(;isdigit(c);c=getchar()) x=x*10+c-'0';
    x*=f;
}

struct edge{
    int to,next;
    inline void Init(int to_,int ne_){
        to=to_;next=ne_;
    }
}li[N<<1];
int head[N],tail;

inline void Add(int from,int to){
    li[++tail].Init(to,head[from]);
    head[from]=tail;
}

struct Ques{
    int u,k,id;
    inline Ques(){}
    inline Ques(int u,int k,int id) : u(u),k(k),id(id) {}
}ques[N];

int n,q,qt,fa[N],Dep[N],h[N],Son[N],tag[N],Size[N],ans[N];
vector<int> v[N];

int p[N<<1];
int *o=p,*f[N];

inline void dfs(int k,int fat){
    fa[k]=fat;Dep[k]=Dep[fat]+1;Size[k]=1;
    for(int x=head[k];x;x=li[x].next){
        int to=li[x].to;
        if(to==fat) continue;
        dfs(to,k);if(h[Son[k]]<h[to]) Son[k]=to;
        Size[k]+=Size[to];
    }
    h[k]=h[Son[k]]+1;
    for(int it:v[k]){
        ans[ques[it].id]=(Size[k]-1)*min(Dep[k]-1,ques[it].k);
    }
}

inline void Dp(int k){
    if(Son[k]){
        f[Son[k]]=f[k]+1;Dp(Son[k]);
        tag[k]=tag[Son[k]]+Size[Son[k]]-1;
        f[k][0]+=-tag[Son[k]]-Size[Son[k]]+1;
    }
    // printf("k=%d\n",k);
    // printf("tag=%d\n",tag[k]);
    // for(int i=0;i<h[k];i++){
    //     printf("%d ",f[k][i]);
    // }puts("");
    for(int x=head[k];x;x=li[x].next){
        int to=li[x].to;
        if(to==fa[k]||to==Son[k]) continue;
        f[to]=o;o+=(h[to]<<1);
        Dp(to);
        for(int i=0;i<=h[to]-1;i++){
            f[k][i+1]+=f[to][i]+Size[to]-1+tag[to];
        }
        for(int i=0;i<=h[to];i++) f[k][i]-=f[to][h[to]-1]+Size[to]-1+tag[to];
        tag[k]+=f[to][h[to]-1]+Size[to]-1+tag[to];
    }
    for(int it:v[k]){
        ans[ques[it].id]+=f[k][min(ques[it].k,h[k]-1)]+tag[k];
    }
    // printf("k=%d\n",k);
    // printf("tag=%d\n",tag[k]);
    // for(int i=0;i<h[k];i++){
    //     printf("%d ",f[k][i]);
    // }puts("");
}

signed main(){
    // freopen("my.in","r",stdin);
    // freopen("my.out","w",stdout);
    read(n);read(q);
    for(int i=1;i<=n-1;i++){
        int from,to;read(from);read(to);
        Add(from,to);Add(to,from);
    }
    for(int i=1;i<=q;i++){
        int u,k;read(u);read(k);
        ques[++qt]=Ques(u,k,i);
        v[u].push_back(qt);
    }
    dfs(1,0);
    // printf("%lld\n",ans[1]);
    f[1]=o;o+=(h[1]<<1);Dp(1);
    for(int i=1;i<=q;i++){
        printf("%lld\n",ans[i]);
    }
    return 0;
}

CF1009F

这个应该是比较经典的长链剖分优化 dp 了。我们只需要在更新数组的时候注意更新答案即可。


#include<bits/stdc++.h>
#define dd double
#define ld long double
#define ll long long
#define uint unsigned int
#define ull unsigned long long
#define N 1001000
#define M number
using namespace std;

const int INF=0x3f3f3f3f;

template<typename T> inline void read(T &x) {
    x=0; int f=1;
    char c=getchar();
    for(;!isdigit(c);c=getchar()) if(c == '-') f=-f;
    for(;isdigit(c);c=getchar()) x=x*10+c-'0';
    x*=f;
}

struct edge{
    int to,next;
    inline void Init(int to_,int ne_){
        to=to_;next=ne_;
    }
}li[N<<1];
int head[N],tail;

int n;

inline void Add(int from,int to){
    li[++tail].Init(to,head[from]);
    head[from]=tail;
}

int fa[N],Dep[N],h[N],Son[N];;
int p[N<<2],*o=p,*f[N];
typedef pair<int,int> P;
P ans[N];

inline void dfs(int k,int fat){
    Dep[k]=Dep[fat]+1;fa[k]=fat;
    for(int x=head[k];x;x=li[x].next){
        int to=li[x].to;
        if(to==fat) continue;
        dfs(to,k);if(h[Son[k]]<h[to]) Son[k]=to;
    }
    h[k]=h[Son[k]]+1;
}

inline void Dp(int k){
    ans[k]=make_pair(INF,INF);
    if(Son[k]){
        f[Son[k]]=f[k]+1;Dp(Son[k]);ans[k]=ans[Son[k]];ans[k].second++;
    }
    f[k][0]=1;if(ans[k]>make_pair(-1,0)) ans[k]=make_pair(-1,0);
    // printf("k=%d\n",k);
    // printf("ans[k]= %d %d\n",ans[k].first,ans[k].second);
    for(int x=head[k];x;x=li[x].next){
        int to=li[x].to;
        if(to==fa[k]||to==Son[k]) continue;
        f[to]=o;o+=(h[to]+1);
        Dp(to);
        for(int i=0;i<h[to];i++){
            f[k][i+1]+=f[to][i];
            if(ans[k]>make_pair(-f[k][i+1],i+1)) ans[k]=make_pair(-f[k][i+1],i+1);
        }
    }
}

int main(){
    // freopen("my.in","r",stdin);
    // freopen("my.out","w",stdout);
    read(n);
    for(int i=1;i<=n-1;i++){
        int from,to;read(from);read(to);
        Add(from,to);Add(to,from);
    }
    dfs(1,0);f[1]=o;o+=(h[1]+1);Dp(1);
    for(int i=1;i<=n;i++) printf("%d\n",ans[i].second);
    return 0;
}

posted @ 2022-02-02 07:53  hyl天梦  阅读(68)  评论(0编辑  收藏  举报