题解:洛谷 CF1009F Dominant Indices
【题目来源】
【题目描述】
给定一棵有 \(n\) 个顶点的有根树,以顶点 \(1\) 作为根。
我们定义顶点 \(x\) 的深度数组为一个无限序列 \([d_{x, 0}, d_{x, 1}, d_{x, 2}, \dots]\),其中 \(d_{x, i}\) 表示满足以下两个条件的顶点 \(y\) 的数量:
- \(x\) 是 \(y\) 的祖先;
- 从 \(x\) 到 \(y\) 的简单路径恰好经过 \(i\) 条边。
顶点 \(x\) 的深度数组的主导下标(dominant index)(简称顶点 \(x\) 的主导下标)定义为一个下标 \(j\),满足:
- 对于所有 \(k < j\),都有 \(d_{x, k} < d_{x, j}\);
- 对于所有 \(k > j\),都有 \(d_{x, k} \le d_{x, j}\)。
请你计算树中每个顶点的主导下标。
【输入】
第一行包含一个整数 \(n\)(\(1 \le n \le 10^6\)),表示树的顶点数。
接下来 \(n-1\) 行,每行包含两个整数 \(x\) 和 \(y\)(\(1 \le x, y \le n\),\(x \ne y\)),表示树中的一条边。
保证这些边构成一棵树。
【输出】
输出 \(n\) 个数字,第 \(i\) 个数字表示顶点 \(i\) 的主导下标。
【输入样例】
4
1 2
2 3
3 4
【输出样例】
0
0
0
0
【核心思想】
-
问题分析:给定一棵 \(n\) 个节点的有根树(根为 1),对每个节点 \(u\),定义 \(d_{u,k}\) 为 \(u\) 的子树中到 \(u\) 距离恰好为 \(k\) 的节点数量。需要为每个节点 \(u\) 找到一个下标 \(j\),使得 \(d_{u,j}\) 是最大值中的最小下标(即众数距离中最小的那个距离值)。这是一个树上距离分布众数问题,本质上是求每个节点子树中所有深度(相对该节点)出现次数最多的最小深度值。
-
算法选择:
- 长链剖分 + 指针数组 DP:利用长链剖分(heavy-light decomposition by depth)的特性,将每个节点的“距离分布数组”沿重儿子(子树深度最大的儿子)进行内存复用,避免 \(O(n^2)\) 的空间和时间开销。每个节点 \(u\) 维护一个数组 \(f[u]\),其中 \(f[u][k]\) 表示 \(u\) 的子树中到 \(u\) 距离为 \(k\) 的节点数。重儿子的数组直接复用 \(u\) 的数组偏移一位,轻儿子则分配独立内存并暴力合并到 \(u\) 的数组中。总时间复杂度 \(O(n)\),空间复杂度 \(O(n)\)。
- 状态维护:在合并过程中,动态更新 \(ans[u]\)(主导下标),比较 \(f[u][j]\) 与当前最大值,优先选择数量更大的下标;若数量相同则选择更小的下标。
-
关键步骤:
- 第一次 DFS:计算每个节点的重儿子(子树深度最大的儿子)和子树高度 \(len[u]\)(从 \(u\) 到子树中最远叶子的边数)。对于叶子节点,\(len[u]=1\)。
- 内存池分配:使用一个全局数组
buf作为内存池,f[u]指向该池中的一段连续空间。根节点 \(f[1]\) 分配 \(len[1]\) 大小的空间,重儿子 \(f[son[u]] = f[u] + 1\) 复用父节点空间,轻儿子则单独分配 \(len[v]\) 大小的空间。 - 第二次 DFS(DP):
- 初始化 \(f[u][0] = 1\)(自身)。
- 若存在重儿子,递归处理重儿子,并初步设置 \(ans[u] = ans[son[u]] + 1\)(因为重儿子子树中距离重儿子 \(ans[son]\) 的节点,到 \(u\) 的距离为 \(ans[son]+1\))。
- 对于每个轻儿子 \(v\),递归处理,然后将 \(v\) 的分布数组合并到 \(u\):\(f[u][j] += f[v][j-1]\)(\(1 \le j \le len[v]\))。合并时,若 \(f[u][j]\) 大于当前记录的最大值,或相等但 \(j\) 更小,则更新 \(ans[u]\)。
- 若 \(f[u][ans[u]] == 1\)(即只有自身),则 \(ans[u] = 0\)(因为下标 0 为最大值且最小)。
- 输出:按顺序输出每个节点的 \(ans[i]\)。
-
时间/空间复杂度:
- 时间复杂度:\(O(n)\),每个节点及其轻儿子的合并操作总复杂度为 \(O(n)\)(每条轻边最多被合并一次)。
- 空间复杂度:\(O(n)\),内存池大小等于所有长链长度之和,即 \(O(n)\)。
-
长链剖分 + 指针数组 DP 的核心思想:
- 长链剖分的优势:将树剖分为若干条长链,每条长链上的节点共享同一段 DP 数组,重儿子直接复用父节点内存,避免了为每个节点分配完整数组的开销。
- 轻儿子合并的复杂度保证:每个节点只会在其所属长链的顶部作为轻儿子被合并一次,因此总合并次数为 \(O(n)\)。
- 距离分布的计算:\(f[u][k]\) 表示 \(u\) 的子树中到 \(u\) 距离为 \(k\) 的节点数,转移时重儿子整体偏移一位,轻儿子逐点累加。
- 主导下标的维护:在合并过程中实时比较并更新最大值及最小下标,无需额外遍历数组查找众数。
- 适用于树上距离统计、众数距离查询等需要高效维护深度分布的问题。
【算法标签】
提高+ #长链剖分
【代码详解】
#include <bits/stdc++.h>
using namespace std;
const int N = 1000005;
int n; // 树的顶点数
int son[N], len[N]; // son[u]: u 的重儿子(子树深度最大的儿子),len[u]: 从 u 到其子树中最远叶子的边数
vector<int> g[N]; // 邻接表存储树
int buf[N]; // 内存池,用于分配每个长链的 DP 数组
int *f[N]; // f[u] 指向一个数组,f[u][j] 表示在 u 的子树中,距离 u 为 j 的顶点个数(j >= 0)
int *p = buf; // 当前内存池分配指针
int ans[N]; // ans[u] 存储顶点 u 的主导下标
// 第一次 DFS:计算每个节点的重儿子及子树高度 len
void dfs(int u, int fa)
{
for (int v : g[u])
{
if (v == fa)
{
continue;
}
dfs(v, u);
// 选择子树深度最大的儿子作为重儿子
if (len[son[u]] < len[v])
{
son[u] = v;
}
}
// 当前节点的高度 = 重儿子高度 + 1
len[u] = len[son[u]] + 1;
}
// 第二次 DFS:进行长链剖分上的动态规划,计算每个节点的主导下标
void DP(int u, int fa)
{
f[u][0] = 1; // 距离自己为 0 的节点只有自身,数量为 1
// 优先处理重儿子,共享内存:重儿子的 DP 数组紧跟在 u 的数组后面
if (son[u])
{
f[son[u]] = f[u] + 1; // 重儿子复用当前节点的数组偏移 1
DP(son[u], u);
// 在重儿子子树中,距离重儿子为 ans[son[u]] 的节点,
// 到 u 的距离为 ans[son[u]] + 1,因此初步设置主导下标
ans[u] = ans[son[u]] + 1;
}
// 处理所有轻儿子
for (int v : g[u])
{
if (v == fa || v == son[u])
{
continue;
}
// 为轻儿子分配独立内存
f[v] = p;
p += len[v];
DP(v, u);
// 合并轻儿子的 DP 信息到 u
// 距离 u 为 j 的节点数 += 距离 v 为 j-1 的节点数
for (int j = 1; j <= len[v]; j++)
{
f[u][j] += f[v][j - 1];
// 更新主导下标:优先选择数量更大的;若数量相等,选择下标更小的
if (f[u][j] > f[u][ans[u]] || (f[u][j] == f[u][ans[u]] && j < ans[u]))
{
ans[u] = j;
}
}
}
// 如果最大值仅为 1(即只有自身),则主导下标为 0
if (f[u][ans[u]] == 1)
{
ans[u] = 0;
}
}
int main()
{
scanf("%d", &n);
// 读入树的 n-1 条边
for (int i = 1; i < n; i++)
{
int u, v;
scanf("%d%d", &u, &v);
g[u].push_back(v);
g[v].push_back(u);
}
// 从根节点 1 开始进行第一次 DFS,计算重儿子和高度
dfs(1, 0);
// 为根节点分配内存,内存池大小足够容纳所有长链的总长度
f[1] = p;
p += len[1];
// 第二次 DFS,进行长链剖分 DP
DP(1, 0);
// 输出每个顶点的主导下标
for (int i = 1; i <= n; i++)
{
printf("%d\n", ans[i]);
}
return 0;
}
【运行结果】
4
1 2
2 3
3 4
0
0
0
0
浙公网安备 33010602011771号