Dominant Indices【长链剖分+DP指针优化】
给定一棵以 1 为根,共有 n n n 个节点的树。设 d ( u , x ) d(u,x) d(u,x) 为 u u u 子树中到 u u u 距离为 x x x 的节点数。
对于每个点,求一个最小的 k k k,使得 d ( u , x ) d(u,x) d(u,x) 最大
题解
令
f
u
,
d
e
p
f_{u,dep}
fu,dep 表示
u
u
u 的子树中与
u
u
u 的距离为
d
e
p
dep
dep 的点的个数,则有转移方程
f
u
,
d
e
p
=
∑
v
∈
s
o
n
u
f
v
,
d
e
p
−
1
f_{u,dep} = \sum_{v\in son_u} f_{v,dep-1}
fu,dep=v∈sonu∑fv,dep−1
此时暴力求解,复杂度为
O
(
n
2
)
O(n^2)
O(n2),考虑用长链剖分优化。
对于一个节点 u u u,先遍历其重儿子 s o n u son_u sonu,将重儿子的结果合并到当前点 u u u 上(通过指针 O ( 1 ) O(1) O(1) 实现),然后再遍历这个点 u u u 的所有轻儿子 v v v,并将轻儿子的结果合并到当前节点 u u u 上,此时复杂度就优化为 O ( n ) O(n) O(n)。
对于答案的统计,可以先令 u u u 节点的答案为其重儿子的答案加一,在暴力合并的过程中检查是否有更优的答案。如果最后发现 f u , a n s u = 1 f_{u,ans_u}=1 fu,ansu=1,即 u u u 的子树为一条链,此时答案 a n s u ans_u ansu 应设为0。
复杂度证明
设 u u u 为根节点,则其子树 v v v 的信息会被暴力合并,当且仅当该子树 v v v 是轻儿子,合并的代价就是这棵子树 v v v 中最长链的长度(即子树 v v v 的深度)。根据长链剖分结果,子树 v v v 的根节点必然就是这个最长链的链头,因而就转化成了——只有每条长链的链头会被暴力合并,并且合并的时间复杂度就等于链长。由于所有链长和为 n n n,因此总复杂度就是 O ( n ) O(n) O(n)。
指针 O ( 1 ) O(1) O(1) 优化
继承重儿子结果的过程实际上就是把重儿子的数组复制一遍,因此放弃传统的为每个节点都申请一个空间的DP写法,而是在DP的过程中动态为节点申请内存,进而实现部分节点“共用内存”。
具体来说,只对每一个长链的链头节点申请内存,其大小就等于链长。对于长链上的所有节点,都可以共用这一片空间。例如对节点 u u u 申请了内存后,若 v v v 是 u u u 的重儿子,那么就把 f u f_u fu 数组的起点加 1 当做 f v f_v fv 数组的起点,那么当节点 v v v 完成统计后,实际上就已经更新到了 f u f_u fu 里。
过程模拟
假设有如下一棵树,其中长链为 1 − 2 − 4 1-2-4 1−2−4, t m p tmp tmp 数组用来实现“动态申请内存”,初始情况下指针 i t it it 指向 t m p 0 tmp_0 tmp0,表示当前还未分配空间。
最开始为根节点 1 1 1 分配空间,因为链长为 3 3 3,所以分配了 3 3 3 个空间,即从 t m p 0 tmp_0 tmp0 到 t m p 2 tmp_2 tmp2,并让指针 i t + 3 it+3 it+3,表示前 3 3 3 个位置已经被分配出去了。
从根节点开始 d f s dfs dfs,沿着重儿子走,首先到节点 2 2 2。由于链头 1 1 1 号节点已经申请了足够大的内存,因此链上的节点直接使用即可。
让 d p 2 dp_2 dp2 指向 d p 1 + 1 dp_1+1 dp1+1,即 d p 2 dp_2 dp2 实际拥有的空间就是 t m p 1 tmp_1 tmp1 和 t m p 2 tmp_2 tmp2。
继续走重儿子,到达节点4。同样让 d p 4 dp_4 dp4 指向 d p 2 + 1 dp_2+1 dp2+1,即 d p 4 dp_4 dp4 实际拥有的空间就是 t m p 2 tmp_2 tmp2。
重儿子走完后回溯到节点 2 2 2,继续走轻儿子。因为轻儿子实际上可以看做是另一条长链的开端,因此需要为这条链开辟新的空间。
让 d p 3 dp_3 dp3 指向 i t it it,因为此时深度只有1,因此只开辟了 1 1 1 个空间,即 d p 3 dp_3 dp3 实际拥有的空间就只有 t m p 3 tmp_3 tmp3,并且让 i t + 1 it+1 it+1。
代码
#include <bits/stdc++.h>
using namespace std;
const int maxn = 1e6 + 10;
int n;
struct Edge {
int to, w, nxt;
}edge[maxn << 1];
int tot, head[maxn];
void addEdge(int u, int v, int w) {
edge[tot].to = v;
edge[tot].w = w;
edge[tot].nxt = head[u];
head[u] = tot++;
}
int dep[maxn], son[maxn];
int tmp[maxn], *it = tmp; // 动态开辟空间
int *dp[maxn], ans[maxn];
/* 长链剖分 */
void dfs1(int u, int fa) {
for (int i = head[u]; ~i; i = edge[i].nxt) {
int v = edge[i].to;
if (v == fa) continue;
dfs1(v, u);
if (dep[v] > dep[son[u]]) son[u] = v;
}
dep[u] = dep[son[u]] + 1;
}
/* DP过程 */
void dfs2(int u, int fno) {
dp[u][0] = 1;
if (son[u]) {
dp[son[u]] = dp[u] + 1; // 共享内存
dfs2(son[u], u);
ans[u] = ans[son[u]] + 1; // 从重儿子处继承答案
}
for (int i = head[u]; ~i; i = edge[i].nxt) {
int v = edge[i].to;
if (v == son[u] || v == fno) continue;
dp[v] = it; it += dep[v]; // 分配新内存
dfs2(v, u);
for (int j = 1; j <= dep[v]; j++) {
dp[u][j] += dp[v][j - 1]; // 暴力合并
if (dp[u][j] > dp[u][ans[u]] || (dp[u][j] == dp[u][ans[u]] && j < ans[u])) {
ans[u] = j; // 更新答案
}
}
}
if (dp[u][ans[u]] == 1) ans[u] = 0;
}
int main()
{
ios::sync_with_stdio(0), cin.tie(0);
mem(head, -1);
cin >> n;
for (int i = 1; i < n; i++) {
int u, v; cin >> u >> v;
addEdge(u, v, 1);
addEdge(v, u, 1);
}
dfs1(1, 0); // 长链剖分
dp[1] = it; it += dep[1]; // 为根节点的答案分配内存
dfs2(1, 0); // DP
for (int i = 1; i <= n; i++) {
cout << ans[i] << endl;
}
}