长链剖分题集及总结

长链剖分题集及总结

树剖中我们常用的是重链剖分,是用于解决树上两点路径上相关问题的利器,当然也可以用于解决一些子树问题。今天和大家聊的是另一种树剖,基于深度的剖分

具体怎么剖就不讲了,下面稍微讲一下他的性质和应用

一.链长和是O(n)
二.任意一个点k次祖先y所在的长链长度 ≥ \ge k
三.从任意一个点向上跳长链 ≤ n \leq \sqrt n n

应用

1. O ( n l o g n ) − O ( 1 ) 求 k 次 祖 先 O(nlogn)-O(1)求k次祖先 O(nlogn)O(1)k

本质上是利用性质二,维护每个长链顶端向下和向上长链长度的点,这样通过倍增跳过去,然后 O ( 1 ) O(1) O(1)查询,比一般倍增的 O ( n l o g n ) — O ( 1 ) O(nlogn)—O(1) O(nlogn)O(1)更优

这里直接挂代码了 洛谷P5903

//树上k级祖先 O(nlogn)~O(1)
#include<bits/stdc++.h>
#define pb push_back
using namespace std;
typedef long long ll;
const int maxn=5e5+5;
#define ui unsigned int
ui S;
inline ui get() {
	S ^= S << 13;
	S ^= S >> 17;
	S ^= S << 5;
	return S; 
}
int f[maxn][20],n,u,v,len[maxn],head[maxn],next1[maxn<<1],ver[maxn<<1],tot,hbit[maxn],m,lg[maxn];
vector<int>U[maxn],D[maxn];
void add(int x,int y){
    ver[++tot]=y,next1[tot]=head[x],head[x]=tot;
}

struct LCD{
    int fa[maxn],dep[maxn],md[maxn],hson[maxn],top[maxn];//md[x]表示该点子树的最深深度
    void dfs1(int x,int fat){
        f[x][0]=fa[x]=fat;
        md[x]=dep[x]=dep[fat]+1;
        for(int i=1;i<=lg[dep[x]];++i)
            f[x][i]=f[f[x][i-1]][i-1];
        for(int i=head[x];i;i=next1[i]){
            int y=ver[i];
            if(y==fat)continue;
            dfs1(y,x);
            if(md[y]>md[hson[x]])hson[x]=y,md[x]=md[y];
        }
    }
    void dfs2(int x,int t){
        top[x]=t;
        len[x]=md[x]-dep[top[x]];//链长
        if(!hson[x])return;
        dfs2(hson[x],t);
        for(int i=head[x];i;i=next1[i]){
            int y=ver[i];
            if(y==fa[x]||y==hson[x])continue;
            dfs2(y,y);
        }
    }
    void init(int x){
        dfs1(x,0);dfs2(x,x);
    }
    int query(int x,int k){//x的k级祖先
        if(k>dep[x])return 0;
        if(!k)return x;
        x=f[x][hbit[k]];k^=(1<<hbit[k]);
        if(dep[x]-dep[top[x]]==k)return top[x];
        if(dep[x]-dep[top[x]]>k)return D[top[x]][dep[x]-dep[top[x]]-k-1];
        return U[top[x]][k-(dep[x]-dep[top[x]])-1];
    }
}lcd;
int main(){
    cin>>n>>m>>S;
    int rt=0;
    for(int i=1;i<=n;++i){
        scanf("%d",&u);
        if(!u)rt=i;
        else{
            add(i,u);
            add(u,i);
        }
    }
    for(int i=1;i<=n;++i)
        lg[i]=lg[i-1]+(1<<lg[i-1]==i);
    lcd.init(rt);
    for(int i=1;i<=n;++i){
        if(i==lcd.top[i]){
            int l=0,x=i;
            while(l<len[i]&&x)x=f[x][0],++l,U[i].pb(x);
            l=0,x=i;
            while(l<len[i])x=lcd.hson[x],++l,D[i].pb(x);
        }
    }
    int mx=1;
    for(int i=1;i<=n;++i){
        if((i>>mx)&1)++mx;
        hbit[i]=mx-1;
    }
    ll ans=0;
    int preans=0;
    for(int i=1;i<=m;++i){
        u=(get()^preans)%n+1,v=(get()^preans)%lcd.dep[u];
        preans=lcd.query(u,v);
        ans^=1ll*i*preans;
    }
    cout<<ans;
    return 0;
}
二、快速维护可合并的与深度有关的子树信息

一般都是长链剖分优化dp

常见的有两种,一种是长链剖分dp后缀和维护,一种是直接优化dp,先讲第一种

长链剖分+后缀和 常用于解决离某子树的距离有限制比如 < = k 或 者 > k <=k 或者 >k <=k>k的点满足xx条件的有多少

1.洛谷P3899

题意:

统计有序三元组(a,b,c)满足a,b都是c的祖先,且a,b在树上的距离<=k

思路:

这题在线有可持久化线段树、线段树合并,离线有长链剖分、树状数组。其他几种写法我或许会在未来鸽掉的可持久化线段树上补充

其实本质上一看就是个二维偏序啦,就可以乱搞了,但是我们想用长链剖分做,首先 b b b a a a上面的很傻逼,就 ( s z [ a ] − 1 ) ∗ m i n ( d e p [ a ] − 1 , k ) (sz[a]-1)*min(dep[a]-1,k) (sz[a]1)min(dep[a]1,k)

关键是下面

对于a下面的每个点其实就是对于所有的<=k统计其 ∑ ( s z [ x ] − 1 ) \sum(sz[x]-1) (sz[x]1)而已,<=k一般直接转为后缀和

直接 d p [ i ] [ j ] dp[i][j] dp[i][j]表示以 i i i为根节点,距离 i i i > = j >=j >=j下, a b ab ab c c c祖先, b c bc bc a a a子树下的数量

d p [ x ] [ 0 ] dp[x][0] dp[x][0]就是整颗子树,注意这里一般都用指针转移,长链直接 O ( 1 ) O(1) O(1),短链就直接枚举,由势能分析容易证得其实是 O ( n − m d [ 1 ] ) O(n-md[1]) Onmd[1]的,空间也是 O ( n ) O(n) O(n),转移 d p [ x ] [ i + 1 ] + = d p [ y ] [ i ] dp[x][i+1]+=dp[y][i] dp[x][i+1]+=dp[y][i],但由于使用指针,所以长儿子不用转移,直接赋值的时候 O ( 1 ) O(1) O(1)转移了,但是需要注意的是,此时 d p [ x ] [ 0 ] dp[x][0] dp[x][0]并没有更新,还是得+=,统计答案的时候减法获取需要答案即可,个人认为这道非常适合当长链剖分优化dp后缀和的模板

#include<bits/stdc++.h>
#define pb push_back
#define fi first 
#define se second
using namespace std;
typedef long long ll;
const int maxn=3e5+5;
ll *dp[maxn],tmp[maxn],*id=tmp,ans[maxn];
int sz[maxn],head[maxn],ver[maxn<<1],next1[maxn<<1],n,q,hson[maxn],md[maxn],dep[maxn],tot;
typedef pair<int,int>P;//dp[i][j]以i为根子树距离i>=j下,ab为c祖先,bc在a子树下的数量
vector<P>Q[maxn];
void dfs1(int x,int f){        
    dep[x]=dep[f]+1;sz[x]=1;
    for(int i=head[x];i;i=next1[i]){
        int y=ver[i];
        if(y==f)continue;
        dfs1(y,x);
        sz[x]+=sz[y];
        if(md[y]>md[hson[x]])hson[x]=y;
    }
    md[x]=md[hson[x]]+1;
}
void add(int x,int y){
    ver[++tot]=y,next1[tot]=head[x],head[x]=tot;
}
void dfs(int x,int f){
    dp[x][0]=sz[x]-1;
    if(hson[x]){//指针,长链直接赋值不用+=
        dp[hson[x]]=dp[x]+1;dfs(hson[x],x);dp[x][0]+=dp[hson[x]][0];//但>=0没统计
    }
    for(int i=head[x];i;i=next1[i]){
        int y=ver[i];
        if(y==f||y==hson[x])continue;
        dp[y]=id;id+=md[y];
        dfs(y,x);
        dp[x][0]+=dp[y][0];
        for(int j=0;j<md[y];++j)//后缀和维护
            dp[x][j+1]+=dp[y][j];
    }
    for(auto&v:Q[x]){
        ans[v.fi]+=1ll*(sz[x]-1)*min(dep[x]-1,v.se);
        if(v.se<md[x]-1)ans[v.fi]+=dp[x][0]-dp[x][v.se+1]-(sz[x]-1);
        else ans[v.fi]+=dp[x][0]-(sz[x]-1);
    }
}
int main(){
    scanf("%d%d",&n,&q);
    int u,v;
    for(int i=1;i<n;++i){
        scanf("%d%d",&u,&v);
        add(u,v);add(v,u);
    }
    dfs1(1,0);
    dp[1]=id;id+=md[1];
    for(int i=1;i<=q;++i){
        scanf("%d%d",&u,&v);
        Q[u].pb({i,v});
    }
    dfs(1,0);
    for(int i=1;i<=q;++i)cout<<ans[i]<<"\n";
    return 0;
}

2.[计蒜客 42385]

icpc徐州防AK题,但感觉学会长链剖分后很套路

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sRu9bZS0-1615813234887)(C:\Users\98753\AppData\Roaming\Typora\typora-user-images\image-20210315170001435.png)]

其实就是计算每个点为根上面那个东西,距离<=k,

枚举已经 O ( l o g 2 n ) O(log^2n) O(log2n)了,所以我们使用长链剖分维护,暴力枚举两位后,表示出某两点对于这两位的4种状态,同样转移即可,这里下面用了另外一种方法,就是用dfs序列表示点的位置,然后全部转移到长儿子上,其实原理一样,更推荐写指针式的

#include<bits/stdc++.h>
using namespace std;
const int maxn=1e5+5;
typedef unsigned long long ull;
int head[maxn],ver[maxn<<1],next1[maxn<<1],tot,n,K,a[maxn],cnt[maxn][4],dfn[maxn],st[maxn],ti,k1,k2;
ull ans[maxn];
void add(int x,int y){
    ver[++tot]=y,next1[tot]=head[x],head[x]=tot;
}
struct LCD{
    int md[maxn],hson[maxn];
    void dfs1(int x,int f){
        for(int i=head[x];i;i=next1[i]){
            int y=ver[i];
            if(y==f)continue;
            dfs1(y,x);
            if(md[y]>md[hson[x]])hson[x]=y;
        }
        md[x]=md[hson[x]]+1;
    }
}lcd;
ull getS(int x,int k,int sta){
    if(k<lcd.md[x]-1)return cnt[dfn[x]][sta]-cnt[dfn[x]+k+1][sta];
    return cnt[dfn[x]][sta];
}
void dfs(int x,int f){//cnt[i]表示以i为根数字某两位状态的数量
    dfn[x]=++ti;
    cnt[dfn[x]][st[x]]++;
    if(lcd.hson[x]){
        dfs(lcd.hson[x],x);
        for(int i=0;i<4;++i)
            cnt[dfn[x]][i]+=cnt[dfn[lcd.hson[x]]][i];
    }
    for(int i=head[x];i;i=next1[i]){
        int y=ver[i];
        if(y==f||y==lcd.hson[x])continue;
        dfs(y,x);
        for(int j=0;j<4;++j)cnt[dfn[x]][j]+=cnt[dfn[y]][j];//短儿子更新dp
        for(int j=0;j<lcd.md[y];++j)
            for(int k=0;k<4;++k)//全部转移给长儿子链上,维护后缀和
                cnt[dfn[x]+j+1][k]+=cnt[dfn[y]+j][k];//长儿子链上cnt[i]维护dep>=i且x子树内的结点的和
    }
    for(int i=0;i<2;++i)ans[x]+=getS(x,K,i)*getS(x,K,3^i)<<(k1+k2);
}
int main(){
    scanf("%d%d",&n,&K);
    for(int i=1;i<=n;++i)scanf("%d",&a[i]);
    int x;
    for(int i=2;i<=n;++i){
        scanf("%d",&x);
        add(i,x);add(x,i);
    }
    lcd.dfs1(1,0);
    for(k1=0;k1<=29;++k1){
        for(k2=k1+1;k2<=29;++k2){
            for(int k=1;k<=n;++k){
                st[k]=0;
                if(a[k]&(1<<k1))st[k]|=1;
                if(a[k]&(1<<k2))st[k]|=2;
                for(int i=0;i<4;++i)cnt[k][i]=0;
            }
        	ti=0;
       		dfs(1,0);
        }
    }
    for(int i=1;i<=n;++i)ans[i]<<=1;
    for(k1=k2=0;k1<=29;k1++,k2++){
        for(int k=1;k<=n;++k){
            if(a[k]&(1<<k1))st[k]=3;
            else st[k]=0;
            for(int i=0;i<4;++i)cnt[k][i]=0;
        }
        ti=0;
        dfs(1,0);
    }
    for(int i=1;i<=n;++i)cout<<ans[i]<<'\n';
    return 0;
}

长链剖分直接优化DP,其实思路一样,都是利用长链的特性减少时间复杂度

先看下这题

1.bzoj4543

这是一道非常状态非常变态的树形DP+长链剖分优化题,所以重写了一篇,详情请看这里

https://blog.youkuaiyun.com/weixin_45539557/article/details/114848353?spm=1001.2014.3001.5501

2.cf1009E

题意:给一颗树, f [ i ] [ j ] f[i][j] f[i][j]表示 i i i的子树中距离 i i i距离为 j j j的点数,对每个 i i i求出最大的 j j j

思路:

首先是裸的长剖没意见吧。转移和上一题完全一样,在转移的时候顺便记录一下答案就好了,需要注意的是,我是每次用当前重儿子+1的答案,所以最后如果方案数最少是一种,也就是本身的话,答案得更新为0这个位置

#include<bits/stdc++.h>

using namespace std;
const int maxn=1e6+5;
typedef long long ll;
ll *f[maxn],tmp[maxn],*id=tmp,g[maxn],head[maxn],ver[maxn<<1],next1[maxn<<1];
int md[maxn],hson[maxn],tot,ans[maxn],a,b,n;
void add(int x,int y){
    ver[++tot]=y,next1[tot]=head[x],head[x]=tot;
}
void dfs1(int x,int f){
    for(int i=head[x];i;i=next1[i]){
        int y=ver[i];
        if(y==f)continue;
        dfs1(y,x);
        if(md[y]>md[hson[x]])hson[x]=y;
    }
    md[x]=md[hson[x]]+1;
}
void dfs(int x,int fa){
    if(hson[x]){
        f[hson[x]]=f[x]+1;dfs(hson[x],x);ans[x]=ans[hson[x]]+1;
    }
    f[x][0]=1;
    for(int i=head[x];i;i=next1[i]){
        int y=ver[i];
        if(y==fa||y==hson[x])continue;
        f[y]=id;id+=md[y];
        dfs(y,x);
        for(int j=1;j<=md[y];++j){
            f[x][j]+=f[y][j-1];
            if(f[x][j]>f[x][ans[x]]||(f[x][j]==f[x][ans[x]]&&ans[x]>j))
                ans[x]=j;
        }
    }
    if(f[x][ans[x]]==1)ans[x]=0;//特判最大值为1的时候 因为前面ans赋值最多为0
}
int main(){
    scanf("%d",&n);
    for(int i=1;i<n;++i){
        scanf("%d%d",&a,&b);
        add(a,b);
        add(b,a);
    }
    dfs1(1,0);
    f[1]=id;id+=md[1];
    dfs(1,0);
    for(int i=1;i<=n;++i)cout<<ans[i]<<"\n";
    return 0;
}

3.计蒜客40257 2019 icpc南昌E题

也是道变态题,另写了一篇

https://blog.youkuaiyun.com/weixin_45539557/article/details/114850692

总结:很明显,长链剖分在对于深度相关的信息合并的时候具有很优的复杂度,常见的前缀转后缀和,dp优化用指针实现,也是比较套路的事情

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值