长链剖分
本来和树(重)链剖分总结放在一起的,后来由于种种原因还是决定分开放。
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)\)的,就是因为每个节点所携带的信息都只会被转移一次,那么怎样保证每个节点的信息都只会被转移一次呢?我们反过来思考,考虑不选长儿子的情形,如果我们在转移时选了一个短一点的儿子,那么一定会有至少一个比它长的儿子,这些比它长的儿子的、比它最深的后代还要深的后代所携带的信息,就不只被转移了一次。相比之下,不难看出如果选择了拥有最深后代的儿子,就不会有节点携带的信息被转移了多次。