题解:洛谷 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
【核心思想】
-
问题分析:给定一棵 \(n\) 个节点的树,每个节点有颜色 \(a_i\)(共 \(k\) 种)。每种颜色的"范围"定义为该颜色所有节点两两之间距离的最大值。求每种颜色的范围。这是一个树的直径 + LCA问题,关键在于每种颜色的范围等价于该颜色节点在树中的直径。
-
算法选择:
- 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](最深节点)作为直径的一个端点
- BFS 预处理:以根节点 BFS 计算每个节点的深度
-
关键步骤:
- 建树与 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]
- 建树与 BFS:读入 \(n, k\) 和每个节点的颜色、父节点,建立无向树。BFS 计算深度、倍增数组,同时维护
-
时间/空间复杂度:
- 时间复杂度:\(O(n \log n)\),BFS \(O(n)\),LCA 每次 \(O(\log n)\),共 \(n\) 次
- 空间复杂度:\(O(n \log n)\),倍增数组 \(fa[n][20]\)
-
树直径 + 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
浙公网安备 33010602011771号