ImALAS
2022-08-13 15:38:51
长链剖分是树链剖分的一种,平常我们所说的树链剖分一般默认为轻重链剖分。
轻重链剖分有着很优秀的性质:每个结点跳到链顶的次数至多为
但轻重链剖分并不是万能的,遇到某些神奇的问题,它就显得无力了。
给定一棵以
1 为根,n 个节点的树。设d(u,x) 为u 子树中到u 距离为x 的节点数。对于每个点,求一个最小的
k ,使得d(u,k) 最大。
显然可以设计出一个最暴力的 DP:
设
状态转移方程:
这样做显然时空复杂度均为
据说可以 dsu on tree
或者线段树合并,但我不会(
有一个比较经典的结论:状态只跟深度有关的问题一般都可以用长链剖分优化。
让我们看看长链剖分是怎么把这道题优化到时空复杂度
如果熟悉轻重链剖分,那听到长链剖分的名字应该就知道怎么做了。
在轻重链剖分中,我们以子树最大的子节点为重儿子,重儿子与父节点的连边称为重边,若干条重边连成一条重链。
长链剖分和它很详细,只不过把“子树最大”变为“子树深度最大”。
把
代码:
std::vector<int> G[maxn];
void dfs1(int u,int fa) {
for(auto& v : G[u]) {
if(v == fa)continue ;
dfs1(v , u);
if(d[v] > d[son[u]])son[u] = v;
}
d[u] = d[son[u]] + 1;
return ;
}
它有一些性质:
所有长链的长度和为
O(n) 级别。
证明:每条边只会在一条长链里出现,显然成立。
证明:反证法,如果
任意一点向上跳跃长链的次数至多为
O(\sqrt{N}) 。
证明:显然,这个节点跳到的长链的长度大于其原来所在的长链长度。
那么在最劣的情况下,长链长度为
[模板]树上 k 级祖先
面对这个问题,如果我们使用倍增,那么时间复杂度预处理
如果询问次数特别多的话,倍增就显得乏力了。
不过,在长链剖分优秀性质的优化下,可以结合倍增做到
首先,还是要预处理出每个结点的
对于每条长链,假设链长为
这显然是
对于一次询问
根据我们上面提到的性质 2,
那么我们直接根据上面预处理好的信息
代码:太懒了还没写
会发现,上面提到的例题就是一道典型的满足这种特征的题。
我们如何在没有任何其他方法的帮助下紧靠长链剖分解决这道题呢?
首先我们发现,
我们可不可以在统计
答案是可以。我们只对长链的顶点申请内存,其它的节点与它共享内存。
具体地,抛弃静态数组,改用动态数组,即为指针:int *f[maxn];
。
对于节点
对于
这样做,由于我们只在每条长链的链头进行合并,时空复杂度就是长链的长度之和,即
// Problem: CF1009F Dominant Indices
// Contest: Luogu
// URL: https://www.luogu.com.cn/problem/CF1009F
// Memory Limit: 500 MB
// Time Limit: 4500 ms
//
// Powered by CP Editor (https://cpeditor.org)
#include <bits/stdc++.h>
#define pb emplace_back
using namespace std;
const int maxn = 1e6 + 5;
int n,ans[maxn],d[maxn],son[maxn],g[maxn];
int *f[maxn],*now = g;
vector<int> G[maxn];
void dfs1(int u,int fa) {
for(auto& v : G[u]) {
if(v == fa)continue ;
dfs1(v , u);
if(d[v] > d[son[u]])son[u] = v;
}
d[u] = d[son[u]] + 1;
return ;
}
void dfs2(int u,int fa) {
f[u][0] = 1;
if(!son[u])return ;
f[son[u]] = f[u] + 1;
dfs2(son[u] , u);
ans[u] = ans[son[u]] + 1;
for(auto& v : G[u]) {
if(v == fa||v == son[u])continue ;
f[v] = now;
now += d[v];
dfs2(v , u);
for(int i = 1;i <= d[v];++ i) {
f[u][i] += f[v][i - 1];
if(f[u][i] > f[u][ans[u]]||(f[u][i] == f[u][ans[u]]&&i < ans[u]))ans[u] = i;
}
}
if(f[u][ans[u]] == 1)ans[u] = 0;
return ;
}
int main() {
scanf("%d",&n);
for(int i = 1;i < n;++ i) {
int u,v;
scanf("%d %d",&u,&v);
G[u].pb(v);
G[v].pb(u);
}
dfs1(1 , 0);
f[1] = now;
now += d[1];
dfs2(1 , 0);
for(int i = 1;i <= n;++ i)printf("%d\n",ans[i]);
return 0;
}
这篇文章中仅列出了长链剖分最基本最基本的操作,至于其它的神仙应用我还没有进行了解,以后会在做题过程中慢慢学习。