题解:洛谷 CF1009F Dominant Indices

【题目来源】

洛谷: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

【核心思想】

  1. 问题分析:给定一棵 \(n\) 个节点的有根树(根为 1),对每个节点 \(u\),定义 \(d_{u,k}\)\(u\) 的子树中到 \(u\) 距离恰好为 \(k\) 的节点数量。需要为每个节点 \(u\) 找到一个下标 \(j\),使得 \(d_{u,j}\) 是最大值中的最小下标(即众数距离中最小的那个距离值)。这是一个树上距离分布众数问题,本质上是求每个节点子树中所有深度(相对该节点)出现次数最多的最小深度值。

  2. 算法选择

    • 长链剖分 + 指针数组 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]\) 与当前最大值,优先选择数量更大的下标;若数量相同则选择更小的下标。
  3. 关键步骤

    • 第一次 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]\)
  4. 时间/空间复杂度

    • 时间复杂度:\(O(n)\),每个节点及其轻儿子的合并操作总复杂度为 \(O(n)\)(每条轻边最多被合并一次)。
    • 空间复杂度:\(O(n)\),内存池大小等于所有长链长度之和,即 \(O(n)\)
  5. 长链剖分 + 指针数组 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
posted @ 2026-09-08 20:30  团爸讲算法  阅读(2)  评论(0)    收藏  举报