长链剖分学习笔记

长链剖分和重链剖分类差不多,都是解决树上问题的一些trick。

下面定义(dep[u])表示(u)到根节点路径上有多少条边((u)的深度),(mdep[u])表示(u)子树内的所有点的深度的最大值。

对比重链剖分选择(size)最大的儿子作为重儿子,长链剖分选择(mdep)最大的儿子作为“重”儿子(这样方便优化一些树上以深度为下标的DP)。

还是直接来看题吧QAQ

LuoguP5903 【模板】树上 k 级祖先

https://www.luogu.com.cn/problem/P5903

这题搞法挺多的...而且实际跑下来长链剖分也不是很快...但还是讲一下吧

不难想到一些单次询问在(O(log n))时间内回答的做法,如果重链剖分后二分重链的话可以做到(O(log log n)),但这些都不是重点,长链剖分可以做到(O(n log n))预处理,单次(O(1))回答 别问我为什么这样还跑的那么慢...

首先长链有一个性质,对于点(u),它的(k)级祖先所在的长链的长度一定大于或等于(k)

这很显然,因为如果那条长链的长度小于(k)的话直接用(u)(k)级祖先到(u)这一段就可以得到一条更长的链,与长链的定义矛盾。

然后有啥子用呢?我们先预处理出(1...n)(n)个数二进制下的最高位(high-bit),然后第一遍(Dfs)时算一个数组(p[x][i])表示(x)向上跳(2^i)步所到达的节点。

然后对于一个询问((u,k)),令(h=2^{lfloorlog_2k floor}),先把(u)调到(f[u][h])上,此时(u)还要向上跳(k-h)层,此时(u)所在重链的长度一定(ge h>k-h),所以直接将(u)跳至链顶,判断向上或者向下跳即可。

也就是说对于一跳重链的链顶(u),维护两个数组(up[u][0...len],down[u][0...len])(len)表示长链长度)。

void dfs1(int u,int f,int deep) {
    dep[u]=deep,mdep[u]=dep[u];
    p[u][0]=f; 
    rep(i,1,LOGN) p[u][i]=p[p[u][i-1]][i-1];
    for (auto v:e[u]) if (v!=f) {
        dfs1(v,u,deep+1);
        if (mdep[v]>mdep[u]) 
            mdep[u]=mdep[v],son[u]=v;
    }
}

void dfs2(int u,int topf) {
    top[u]=topf;
    if (u==top[u]) {
        for (int x=u,i=0;i<=mdep[u]-dep[u];i++,x=son[x]) dn[u].pb(x);
        for (int x=u,i=0;i<=mdep[u]-dep[u];i++,x=p[x][0]) up[u].pb(x);
    }
    if (!son[u]) return;
    dfs2(son[u],topf);
    for (auto v:e[u]) if (v!=p[u][0]&&v!=son[u]) dfs2(v,v);
}

// lg[n]=floor(log2(n))

int query(int u,int k) {
    if (k==0) return u;
    u=p[u][lg[k]],k-=(1<<lg[k])+dep[u]-dep[top[u]],u=top[u];
    return k>=0?up[u][k]:dn[u][-k];
}

(但实际上这个题并没有什么用,只是熟悉一下长链剖分QAQ)

CF1009F Dominant Indices

Luogu的链接

定义(d(u,k))表示(u)子树中到(u)的距离为(k)的点的个数,对于每个(u),你需要找到最小的(k),使得(d(u,k))最大。

考虑把(d)全算出来,不难发现有如下DP

[d(u,k)=sumlimits_{v in son(u)} d(v,k-1) ]

特别的(d(u,0)=1)

这是一个(n^2)的做法,考虑用长链剖分优化。

(len[u])表示(u)到叶子节点的最大距离。

那么只用考虑(d(u,0)...d(u,len[u]))

对于点(u),我们直接让他的重儿子(v),与(d(u,1...len[u]))共用同一空间,然后对于(u)向下链的一些重链,的儿子为链首的一些重链,直接将信息合并到(d(u,0...len[u]))上即可。

这样对每条重链只会合并一次,单次合并的复杂度是(O(1))的,所以最后复杂度为(O(n)),也只需要开(O(n))的空间(下面代码内存池开的naive了...)

可能有点抽象...看代码吧QAQ

#include <bits/stdc++.h>
using namespace std;
#define rep(i,a,n) for (int i=a;i<n;++i)
#define per(i,a,n) for (int i=n-1;i>=a;--i)
#define mp make_pair
#define pb push_back
#define fi first
#define se second
#define all(x) (x).begin(),(x).end()
#define SZ(x) ((int)(x).size())
typedef double db;
typedef long long ll;
typedef pair<int,int> PII;
typedef vector<int> VI;
 
const int N=1e6+10;
namespace Pool {
    int buf[N*23],*p=buf;
    inline int* New(int len) {int *ret=p; p+=len; return ret;}
}
using Pool::New;
 
int n,fa[N],dep[N],mdep[N],son[N];
VI e[N];
 
void dfs(int u,int f,int deep) {
    dep[u]=mdep[u]=deep;
    fa[u]=f;
    for (auto v:e[u]) if (v!=f) {
        dfs(v,u,deep+1);
        if (mdep[v]>mdep[u])
            mdep[u]=mdep[v],son[u]=v;
    }
}
 
int *f[N],ans[N];
 
void dp(int u) {
    if (son[u]) {
        int v=son[u];
        f[v]=f[u]+1,dp(v);
        ans[u]=ans[v]+1;
    }
    f[u][0]=1;
    if (f[u][0]>=f[u][ans[u]]) ans[u]=0;
    int mx=f[u][ans[u]];
    for (auto v:e[u]) if (v!=fa[u]&&v!=son[u]) {
        f[v]=New(mdep[v]-dep[v]+1);
        dp(v);
        for (int i=1;i<=mdep[v]-dep[v]+1;i++) {
            f[u][i]+=f[v][i-1];
            if (f[u][i]>mx||(f[u][i]==mx&&i<ans[u])) ans[u]=i,mx=f[u][i];
        }
    }
}
 
int main() {
#ifdef LOCAL
    freopen("a.in","r",stdin);
#endif
    scanf("%d",&n);
    rep(i,0,n-1) {
        int u,v; scanf("%d%d",&u,&v);
        e[u].pb(v),e[v].pb(u);
    }
    dfs(1,0,1);
    f[1]=New(mdep[1]-dep[1]+1);
    dp(1);
    rep(i,1,n+1) printf("%d
",ans[i]);
    return 0;
}

LuoguP3899 [湖南集训]谈笑风生

https://www.luogu.com.cn/problem/P3899

这题有多种数据结构做法......但这里讲的是长链剖分...

首先一种情况是(b)(a)的祖先,这个很显然,我们不考虑。

对于(b)(a)的子树内,我们考虑DP

(f[u][i])表示(u)子树内到(u)距离为(i)的所有(v)(siz[v]-1)的和((siz[u])表示(u)的子树大小)。

有转移(f[u][i]=sumlimits_{v in son(u)} f[v][i-1])

那么这一部分的贡献就是(sumlimits_{i le k} f[a][i])

这样是一个n^2的DP。

考虑优化,发现这里优化不太好做,将状态的定义改成前缀和又不太好转移,那没事,改后缀和就好了。

转移的柿子不变,只是对于(f[u][0])要将他加上所有(f[v][0])的和以及(siz[u]-1)

长链剖分优化这个DP即可,还有注意这是一个离线的做法(因为内存会被覆盖),所以要算完就回答和(u)有关的问题。

复杂度(O(n+q))

#include <bits/stdc++.h>
using namespace std;
#define rep(i,a,n) for (int i=a;i<n;++i)
#define per(i,a,n) for (int i=n-1;i>=a;--i)
#define mp make_pair
#define pb push_back
#define fi first
#define se second
#define all(x) (x).begin(),(x).end()
#define SZ(x) ((int)(x).size())
typedef double db;
typedef long long ll;
typedef pair<int,int> PII;
typedef vector<int> VI;

const int N=3e5+10;
struct Pool {
    ll buf[N<<3],*pt;
    Pool() {pt=buf;}
    inline ll *New(int sz) {ll *ret=pt; pt+=sz; return ret;}
}p;

VI e[N];
int n,Q,mdep[N],dep[N],son[N];
int fa[N],len[N],siz[N];
ll *f[N];

void dfs(int u,int ff) {
    fa[u]=ff,siz[u]=1;
    mdep[u]=dep[u]=dep[ff]+1;
    for (auto v:e[u]) if (v!=ff) {
        dfs(v,u),siz[u]+=siz[v];
        if (mdep[v]>mdep[u]) {
            mdep[u]=mdep[v];
            son[u]=v;
        }
    }
    len[u]=mdep[u]-dep[u]+1;
}

vector<PII> qs[N];
ll ans[N];

void dp(int u) {
    if (son[u]) {
        f[son[u]]=f[u]+1;
        dp(son[u]);
        f[u][0]+=f[son[u]][0];
    }
    f[u][0]+=siz[u]-1;
    for (auto v:e[u]) if (v!=fa[u]&&v!=son[u]) {
        f[v]=p.New(len[v]<<1);
        dp(v);
        f[u][0]+=f[v][0];
        rep(i,0,len[v]) f[u][i+1]+=f[v][i];
    }
    for (auto q:qs[u]) {
        int k=q.fi,id=q.se;
        ans[id]=1ll*(siz[u]-1)*min(k,dep[u]-1);
        if (k+1<len[u]) ans[id]+=f[u][0]-f[u][k+1]-siz[u]+1;
        else ans[id]+=f[u][0]-siz[u]+1;
    }
}

int main() {
#ifdef LOCAL
    freopen("a.in","r",stdin);
#endif
    scanf("%d%d",&n,&Q);
    rep(i,0,n-1) {
        int u,v; scanf("%d%d",&u,&v);
        e[u].pb(v),e[v].pb(u);
    }
    dfs(1,0);
    f[1]=p.New(len[1]<<1);
    rep(i,1,Q+1) {
        int u,k; scanf("%d%d",&u,&k);
        qs[u].pb(mp(k,i));
    }
    dp(1);
    rep(i,1,Q+1) printf("%lld
",ans[i]);
    return 0;
}

P5904 [POI2014]HOT-Hotels

https://www.luogu.com.cn/problem/P3565

https://www.luogu.com.cn/problem/P5904(加强版 需要长链剖分优化)

这题更侧重思维...并不是后来的优化......

对于三元组((a,b,c)),我们考虑枚举(LCA(a,b,c)),换句话说在(LCA)处算它的贡献。

也就是说Dfs的时候对于点u,枚举儿子v,然后考虑一或两个点在v子树内,其他点在u别的儿子内时的合法三元组个数(或者别的点就是u)。

首先不难想到维护一个东西(f[u][i])表示(u)子树内到(u)的距离为(i)的点的个数。

然后再维护一个(g[u][i])表示(u)子树内有多少个二元组((v,w)),满足(d(v,lca_{v,w})=d(w,lca_{v,w})=d(lca_{v,w},u)+i)(d(i,j))表示树上点(i)到点(j)的距离,(lca)就字面意思啦)。

那么再枚举u的儿子v时,先不要更新u的f和g,枚举(i),用(f[v][i]*g[u][i+1]+g[v][i]*f[u][i-1]),更新答案(建议画图理解QAQ)。

然后考虑如何更新(f)(g)。还是枚举(i),用(f[v][i])更新(f[u][i+1])(g[v][i])更新(g[u][i-1]),对于(g),还有一种情况是一个点在(v)的子树内,另一个点在前面遍历的子树内或另一点是(u),对于这种情况用(f[v][i]*f[u][i+1])更新(g[u][i+1])

用长链剖分优化上述DP即可,注意对(g)要开两倍的空间(因为要吧g[u]-1给g[v])。

#include <bits/stdc++.h>
using namespace std;
#define rep(i,a,n) for (int i=a;i<n;++i)
#define per(i,a,n) for (int i=n-1;i>=a;--i)
#define mp make_pair
#define pb push_back
#define fi first
#define se second
#define all(x) (x).begin(),(x).end()
#define SZ(x) ((int)(x).size())
typedef double db;
typedef long long ll;
typedef pair<int,int> PII;
typedef vector<int> VI;

const int N=1e5+10;

namespace Pool {
    ll buf[N<<3],*p=buf;
    inline ll *New(int len) {ll *ret=p; p+=len; return ret;}
}
using Pool::New;

VI e[N];
int n,len[N],fa[N],son[N];
ll *f[N],*g[N],ans,fs[N],gs[N];

void dfs(int u,int ff) {
    fa[u]=ff;
    int mx=0;
    for (auto v:e[u]) if (v!=ff) {
        dfs(v,u);
        if (len[v]>mx) mx=len[v],son[u]=v;
    }
    len[u]=mx+1;
}

void dp(int u) {
    if (son[u]) {
        int v=son[u];
        f[v]=f[u]+1,g[v]=g[u]-1;
        dp(v);
    }
    ans+=g[u][0],f[u][0]=1;
    for (auto v:e[u]) if (v!=fa[u]&&v!=son[u]) {
        f[v]=New(len[v]<<1),g[v]=New(len[v]<<1);
        dp(v);
        rep(i,0,len[v]) {
            if (i>0) ans+=g[v][i]*f[u][i-1];
            ans+=f[v][i]*g[u][i+1];
        }
        rep(i,0,len[v]) {
            g[u][i+1]+=f[v][i]*f[u][i+1];
            if (i>0) g[u][i-1]+=g[v][i];
        }
        rep(i,0,len[v]) f[u][i+1]+=f[v][i];
    }
}

int main() {
#ifdef LOCAL
    freopen("a.in","r",stdin);
#endif
    scanf("%d",&n);
    rep(i,0,n-1) {
        int u,v; scanf("%d%d",&u,&v);
        e[u].pb(v),e[v].pb(u);
    }
    dfs(1,0);
    f[1]=New(len[1]<<1),g[1]=New(len[1]<<1);
    dp(1);
    printf("%lld
",ans);
    return 0;
}

LuoguP4292 [WC2010]重建计划

蒟蒻的题解


总的来讲一些逼格很高的题目注重的并不是什么奇怪的优化...珂能还是考察思维能力QAQ

原文地址:https://www.cnblogs.com/wxq1229/p/12540188.html