长链剖分

本来和树(重)链剖分总结放在一起的,后来由于种种原因还是决定分开放。

UPD:长链剖分配合倍增实现\(O(n \log n) - O(1)\)的在线树上\(k\)级祖先查询。

很久以前的坑,现在回头来填。

还是要先预处理倍增数组。将原树长链剖分之后,对于每一条重链的链顶,记录深度差小于或等于所在重链的长度的父亲和重儿子。倍增数组是\(O(n \log n)\)的,长链剖分记录的总信息是\(O(n)\)的。考虑对于每次询问\(k\),跳到询问点的\(t = 2 ^ {\log k}\)级祖先处,还需要往上跳\(d = k - t\)级祖先,显然有\(t > \frac {k} {2} > d\)。注意到长链剖分具有的特殊性质:任意一个点的\(k\)级祖先所在长链的链长一定\(\ge k\)。所以询问点的\(t\)级祖先所在的长链长度一定\(> \frac {k} {2} > d\)。根据我们长链剖分预处理的信息,一定可以通过从链顶向上或向下跳到目标点。

有一道模板题xhgww的奇思妙想,可以上vijos交。

//written by newbiechd
#include <cstdio>
#include <cctype>
#include <vector>
#define R register
#define I inline
#define B 1000000
using namespace std;
const int N = 300007, M = 23;
char buf[B], *p1, *p2;
I char gc() { return p1 == p2 && (p2 = (p1 = buf) + fread(buf, 1, B, stdin), p1 == p2) ? EOF : *p1++; }
I int rd() {
    R int f = 0;
    R char c = gc();
    while (c < 48 || c > 57)
        c = gc();
    while (c > 47 && c < 58)
        f = f * 10 + (c ^ 48), c = gc();
    return f;
}
int lg[N], s[N], fa[N][M], dep[N], pri[N], son[N], top[N];
vector <int> g[N], a[N], b[N];
void dfs1(int x, int f) {
    fa[x][0] = f, pri[x] = dep[x] = dep[f] + 1;
    R int i, y;
    for (i = 1; i < M; ++i)
        fa[x][i] = fa[fa[x][i - 1]][i - 1];
    for (i = 0; i < s[x]; ++i)
        if ((y = g[x][i]) ^ f) {
            dfs1(y, x);
            if (pri[y] > pri[x])
                pri[x] = pri[y], son[x] = y;
        }
}
void dfs2(int x, int t) {
    top[x] = t;
    if (son[x])
        dfs2(son[x], t);
    for (R int i = 0, y; i < s[x]; ++i)
        if ((y = g[x][i]) ^ fa[x][0] && y ^ son[x])
            dfs2(y, y);
}
I int query(int x, int k) {
    if (!k)
        return x;
    if (k >= dep[x])
        return 0;
    x = fa[x][lg[k]], k ^= 1 << lg[k];
    R int t = dep[x] - dep[top[x]];
    if (t < k)
        return a[top[x]][k - t];
    else
        return b[top[x]][t - k];
}
int main() {
    R int n = rd(), m, i, x, y, last = 0;
    for (i = 1; i < n; ++i)
        x = rd(), y = rd(), g[x].push_back(y), g[y].push_back(x);
    lg[0] = -1;
    for (i = 1; i <= n; ++i)
        lg[i] = lg[i >> 1] + 1, s[i] = g[i].size();
    dfs1(1, 0), dfs2(1, 1);
    for (x = 1; x <= n; ++x)
        if (top[x] == x) {
            m = pri[x] - dep[x];
            for (i = 0, y = x; i <= m; ++i)
                a[x].push_back(y), y = fa[y][0];
            for (i = 0, y = x; i <= m; ++i)
                b[x].push_back(y), y = son[y];
        }
    m = rd();
    for (i = 1; i <= m; ++i)
        x = rd() ^ last, y = rd() ^ last, printf("%d\n", last = query(x, y));
    return 0;
}

上面介绍的只是长链剖分的一个特殊应用,其实下面的应用在题目中出现得更多。

长链剖分,dfs1的操作与重链剖分非常类似,但由于它本身的性质使得它一般只用于优化一类特殊的树型DP,dfs2的操作也就与重链剖分大相径庭。这类树型DP就是以深度为下标的一类DP。
先上道裸题:CF1009F
题意就是求每棵子树中\(k\)级子孙最多的最小的\(k(k\ge0)\)
先考虑\(O(n^2)\)DP,设\(f[i][j]\)表示以\(i\)为根的子树中\(i\)\(j\)级子孙有多少个。暴力的转移显然就是\(f[i][j+1]+=f[t][j]\)\(t\)\(i\)的子节点),然后在\(f[i]\)里找符合要求的答案。一看数据范围\(1e6\)无论时间还是空间都肯定过不了,考虑优化。
如果把树剖成若干条不相交的链,再用指针处理转移,发现\(f[i]\)可以直接\(O(1)\)地继承一个儿子的答案(同时还节约了空间,具体实现见下面的代码)。那么怎样剖是最优的呢?可以证明,如果取\(i\)的儿子中子树深度最大(最“长”)的那一个为重儿子,时空复杂度达到最优的\(O(n)\)。长链剖分的思想还是很简单的,只要记住重儿子直接继承,轻儿子暴力转移就好了。
如果上面的思想没有理解得很清楚,可以结合下面的代码理解。
还是先解释一下变量含义,只讲和重链剖分不同的地方了。

d[](depth)//深度
t[](/*没别的好取了*/)//子数内最深的点的深度
o[](/*记答案的数组喜欢用o*/)//记答案
*f[](/*你应该也喜欢这样用吧*/)//DP数组,这里是指针,转移时可以看做二维数组
u[](/*随便乱取的*/)//内存池
*e(/*实在编不下去了,反正变量名只用一个字符*/)//指向新的尚未分配的内存
#include<cstdio>
#include<cctype>
#define R register
#define I inline
using namespace std;
const int S=1000003,N=2000003;
char buf[S],*p1,*p2;
I char gc(){return p1==p2&&(p2=(p1=buf)+fread(buf,1,S,stdin),p1==p2)?EOF:*p1++;}
I int rd(){
	R int f=0; R char c=gc();
	while(c<48||c>57) c=gc();
	while(c>47&&c<58) f=f*10+(c^48),c=gc();
	return f;
}
int h[S],s[N],g[N],d[S],t[S],p[S],q[S],o[S],*f[S],u[S],c,*e=u+1;
I void add(int x,int y){s[++c]=h[x],g[c]=y,h[x]=c;}
void dfs1(int x,int f){p[x]=f,d[x]=t[x]=d[f]+1;
	for(R int i=h[x],y;i;i=s[i])
		if((y=g[i])^f){dfs1(y,x);
			if(t[y]>t[x]) t[x]=t[y],q[x]=y; 
		}
}
void dfs2(int x){f[x][0]=1;
	if(q[x]) f[q[x]]=f[x]+1,dfs2(q[x]),o[x]=o[q[x]]+1;
	for(R int i=h[x],j,y,m;i;i=s[i])
		if((y=g[i])^p[x]&&y^q[x])
			for(f[y]=e,m=t[y]-d[y]+1,e+=m,dfs2(y),j=1;j<=m;++j)
				if((f[x][j]+=f[y][j-1])>f[x][o[x]]||f[x][j]==f[x][o[x]]&&j<o[x])
					o[x]=j;
	if(f[x][o[x]]==1) o[x]=0;
}
int main(){
	R int n=rd(),i,x,y;
	for(i=1;i<n;++i) x=rd(),y=rd(),add(x,y),add(y,x);
	dfs1(1,0),f[1]=e,e+=t[1]-d[1]+1,dfs2(1);
	for(i=1;i<=n;++i) printf("%d\n",o[i]);
	return 0;
}

再看一道[POI2014]HOT-Hotels
这题是放了\(n^2\)做法过了的,B站上也有没放的
如果你把上面的题搞懂了,你应该会觉得写长链剖分比推转移方程还简单。
所以还是先上\(n^2\)DP吧。设\(f[i][j]\)表示\(i\)子树中距离\(i\)\(j\)的点的个数,\(w[i][j]\)\(i\)子树中有两个点距离它们的LCA为\(d\)\(d\)不是指定的),它们的LCA距离\(i\)\(d-j\)的方案数。处理转移(设答案为\(o\)\(t\)\(i\)的儿子):

\(o+=f[i][j]*w[t][j+1]+f[t][j-1]*w[i][j]\)

\(w[i][j]+=f[i][j]*f[t][j-1]+w[t][j+1]\)

\(f[i][j]+=f[t][j-1]\)

具体实现时要注意转移的顺序和一些边界条件,再套个长链剖分的板子。

#include<cstdio>
#include<cctype>
#define R register
#define I inline
#define L long long
using namespace std;
const int S=100003,N=200003,M=400003;
char buf[S],*p1,*p2;
I char gc(){return p1==p2&&(p2=(p1=buf)+fread(buf,1,S,stdin),p1==p2)?EOF:*p1++;}
I int rd(){
	R int f=0; R char c=gc();
	while(c<48||c>57) c=gc();
	while(c>47&&c<58) f=f*10+(c^48),c=gc();
	return f;
}
int h[S],s[N],g[N],p[S],q[S],d[S],t[S],c; L *f[S],*w[S],u[M],*e=u+1,o;
I void add(int x,int y){s[++c]=h[x],g[c]=y,h[x]=c;}
I void ini(int x){R int m=(t[x]-d[x]+1)<<1; f[x]=e,e+=m,w[x]=e,e+=m;}
void dfs1(int x,int f){p[x]=f,d[x]=t[x]=d[f]+1;
	for(R int i=h[x],y;i;i=s[i])
		if((y=g[i])^f){dfs1(y,x);
			if(t[y]>t[x]) t[x]=t[y],q[x]=y;
		}
}
void dfs2(int x){
	R int i=h[x],y,j,m,k;
	if(q[x]) f[q[x]]=f[x]+1,w[q[x]]=w[x]-1,dfs2(q[x]);
	for(f[x][0]=1,i=h[x];i;i=s[i])
		if((y=g[i])^p[x]&&y^q[x]){ini(y),dfs2(y),w[x][0]+=w[y][1];
			for(j=1,m=t[y]-d[y]+1,k=m-2;j<=m;++j){
				o+=w[x][j]*f[y][j-1]+w[y][j+1]*f[x][j],w[x][j]+=f[x][j]*f[y][j-1];
				if(j<=k) w[x][j]+=w[y][j+1];
			}
			for(j=1;j<=m;++j) f[x][j]+=f[y][j-1];
		}
	o+=w[x][0];
}
int main(){
	freopen("in","r",stdin);
	for(R int n=rd(),i=1,x,y;i<n;++i) x=rd(),y=rd(),add(x,y),add(y,x);
	dfs1(1,0),ini(1),dfs2(1),printf("%lld",o);
	return 0;
}

推荐题目:
CF161D Distance in Tree 题解
P3899 [湖南集训]谈笑风生 题解
P2993 [FJOI2014]最短路径树问题 题解
P4292 [WC2010]重建计划 题解

附:长链剖分复杂度的感性理解简单证明——为什么是最长的儿子?

考虑我们的转移为什么是\(O(n)\)的,就是因为每个节点所携带的信息都只会被转移一次,那么怎样保证每个节点的信息都只会被转移一次呢?我们反过来思考,考虑不选长儿子的情形,如果我们在转移时选了一个短一点的儿子,那么一定会有至少一个比它长的儿子,这些比它长的儿子的、比它最深的后代还要深的后代所携带的信息,就不只被转移了一次。相比之下,不难看出如果选择了拥有最深后代的儿子,就不会有节点携带的信息被转移了多次。

posted @ 2018-12-06 13:20  newbiechd  阅读(955)  评论(0编辑  收藏  举报