题解:洛谷 P2971 Cow Politics

【题目来源】

洛谷:P2971 [USACO10HOL] Cow Politics G - 洛谷

【题目描述】

农夫约翰的奶牛住在 \(n\) 片不同的草地上,标号为 \(1\sim n\)

恰好有 \(n-1\) 条单位长度的双向道路,用各种各样的方法连接这些草地。而且从每片草地出发都可以抵达其他所有草地。也就是说,这些草地和道路构成了一种叫做树的图。输入包含一个详细的草地的集合,详细说明了每个草地的父节点 \(p_i\)。根节点的 \(p_i=0\),表示它没有父节点。

因为奶牛建立了 \(1\sim k\) 一共 \(k\) 个政党。每只奶牛都要加入某一个政党,其中,第 \(i\) 只奶牛属于第 \(a_i\) 个政党。而且每个政党至少有两只奶牛。 每个政党都想知道自己的“范围”有多大。其中,定义一个政党的范围是这个政党离得最远的两只奶牛(沿着双向道路行走)的距离。

【输入】

第一行两个整数 \(n,k\)

\(2\sim n+1\) 行:第 \(i+1\) 行两个整数 \(a_i,p_i\)

【输出】

一共 \(K\) 行,第 \(i\) 行一个整数表示第 \(i\) 个政党的范围。

【输入样例】

6 2
1 3
2 1
1 0
2 1
2 1
1 5

【输出样例】

3
2

【核心思想】

  1. 问题分析:给定一棵 \(n\) 个节点的树,每个节点有颜色 \(a_i\)(共 \(k\) 种)。每种颜色的"范围"定义为该颜色所有节点两两之间距离的最大值。求每种颜色的范围。这是一个树的直径 + LCA问题,关键在于每种颜色的范围等价于该颜色节点在树中的直径。

  2. 算法选择

    • BFS 预处理:以根节点 BFS 计算每个节点的深度 dep 和倍增祖先 fa,同时找到每种颜色深度最大的节点 id[c]
    • LCA(最近公共祖先):利用倍增法 \(O(\log n)\) 计算两节点距离:dist(u,v) = dep[u] + dep[v] - 2*dep[lca(u,v)]
    • 直径性质:树的直径可通过两次 BFS/DFS 找到,但本题需要对每种颜色独立求直径。利用"直径的一个端点必为距离某个固定点最远的点"的性质,用 id[c](最深节点)作为直径的一个端点
  3. 关键步骤

    • 建树与 BFS:读入 \(n, k\) 和每个节点的颜色、父节点,建立无向树。BFS 计算深度、倍增数组,同时维护 maxx[c]id[c](颜色 \(c\) 的最大深度及对应节点)
    • 计算每种颜色的直径
      • 遍历每个节点 \(i\),设 \(p_1 = i\)\(p_2 = id[a[i]]\)(同颜色最深节点)
      • l = lca(p_1, p_2)
      • t = dep[p_1] + dep[p_2] - 2 * dep[l]
      • ans[a[i]] = max(ans[a[i]], t)
    • 输出 ans[1..k]
  4. 时间/空间复杂度

    • 时间复杂度:\(O(n \log n)\),BFS \(O(n)\),LCA 每次 \(O(\log n)\),共 \(n\)
    • 空间复杂度:\(O(n \log n)\),倍增数组 \(fa[n][20]\)
  5. 树直径 + LCA 的核心思想

    • 直径端点性质:树的直径的一个端点,是从任意节点出发能到达的最远节点之一。因此用每种颜色最深的节点 id[c] 作为直径的一个端点,遍历该颜色所有节点找最远点即可得直径
    • LCA 距离公式:树上两节点距离 = 深度之和 \(- 2 \times\) LCA 深度,将路径长度转化为深度运算
    • 倍增预处理:BFS 时同步计算 \(2^k\) 级祖先,使 LCA 查询从 \(O(n)\) 降为 \(O(\log n)\)
    • 颜色聚合:不单独对每种颜色做两次 BFS,而是利用已预处理的全局深度和 LCA,遍历所有节点统一计算,避免重复遍历
    • 适用于"树上多颜色子集直径查询"问题,核心在于利用直径端点性质和 LCA 高效计算距离

【算法标签】

普及+ #最近公共祖先

【代码详解】

#include <bits/stdc++.h>
using namespace std;
const int N = 200005, M = N * 2;
int n, k, root;  // n: 节点数,k: 颜色种类数,root: 根节点
int h[N], e[M], ne[M], idx;  // 邻接表存储树
int a[N], p;  // a: 节点颜色,p: 父节点
int dep[N], fa[N][20], maxx[100005], id[100005], ans[100005];  // dep: 节点深度,fa: 倍增祖先数组,maxx: 每种颜色的最大深度,id: 每种颜色深度最大的节点,ans: 每种颜色的答案
queue<int> q;  // BFS队列

// 添加无向边
void add(int a, int b)
{
    e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}

// BFS预处理节点深度、倍增数组,并找到每种颜色深度最大的节点
void bfs(int u)
{
    memset(dep, 0x3f, sizeof(dep));  // 初始化深度为无穷大
    dep[0] = 0, dep[u] = 1;  // 虚拟节点0深度为0,根节点深度为1
    q.push(u);

    while (!q.empty())
    {
        int t = q.front();
        q.pop();
        // 更新当前颜色的最大深度和对应的节点
        if (maxx[a[t]] < dep[t])
        {
            maxx[a[t]] = dep[t];  // 更新最大深度
            id[a[t]] = t;  // 记录深度最大的节点
        }

        for (int i = h[t]; i != -1; i = ne[i])  // 遍历子节点
        {
            int j = e[i];
            if (dep[j] > dep[t] + 1)  // 如果j未被访问
            {
                dep[j] = dep[t] + 1;  // 计算深度
                q.push(j);
                fa[j][0] = t;  // j的2^0祖先为t
                for (int k = 1; k < 20; k++)  // 预处理倍增数组
                {
                    fa[j][k] = fa[fa[j][k - 1]][k - 1];
                }
            }
        }
    }
}

// 求节点x和y的最近公共祖先
int lca(int x, int y)
{
    if (dep[x] < dep[y])  // 保证x深度较大
        swap(x, y);

    // 将x向上跳,使x和y深度相同
    for (int k = 19; k >= 0; k--)
    {
        if (dep[fa[x][k]] >= dep[y])
            x = fa[x][k];
    }

    if (x == y)  // 如果x等于y,说明y是x的祖先
        return x;

    // x和y同时向上跳,直到找到最近公共祖先的下一层
    for (int k = 19; k >= 0; k--)
    {
        if (fa[x][k] != fa[y][k])
        {
            x = fa[x][k];
            y = fa[y][k];
        }
    }
    return fa[x][0];  // 返回最近公共祖先
}

int main()
{
    cin >> n >> k;  // 输入节点数和颜色种类数
    memset(h, -1, sizeof(h));  // 初始化邻接表

    for (int i = 1; i <= n; i++)  // 输入每个节点的颜色和父节点
    {
        cin >> a[i] >> p;
        if (p != 0)  // 如果有父节点,添加边
            add(i, p), add(p, i);
        else  // 否则是根节点
            root = i;
    }

    bfs(root);  // BFS预处理

    // 计算每种颜色的答案:同颜色节点间的最大距离
    for (int i = 1; i <= n; i++)
    {
        int p_1 = i;  // 当前节点
        int p_2 = id[a[i]];  // 当前颜色深度最大的节点
        int l = lca(p_1, p_2);  // 求最近公共祖先
        int t = dep[p_1] + dep[p_2] - 2 * dep[l];  // 计算距离
        ans[a[i]] = max(ans[a[i]], t);  // 更新答案
    }

    for (int i = 1; i <= k; i++)  // 输出每种颜色的答案
        cout << ans[i] << endl;

    return 0;
}

【运行结果】

6 2
1 3
2 1
1 0
2 1
2 1
1 5
3
2
posted @ 2026-08-10 12:29  团爸讲算法  阅读(8)  评论(0)    收藏  举报