题解:洛谷 P2978 Tea Time
【题目来源】
洛谷:P2978 [USACO10JAN] Tea Time S - 洛谷
【题目描述】
\(N (1 \leq N \leq 1000)\) 头奶牛,编号为 \(1 \dots N\),在参加一个喝茶时间活动。在喝茶时间活动开始之前,已经有 \(M (1 \leq M \leq 2,000)\) 对奶牛彼此认识(是朋友)。第i对彼此认识的奶牛通过两个不相同的整数 \(A_i\) 和 \(B_i\) 给定(\(1 \leq A_i \leq N, 1 \leq B_i \leq N\))。输入数据保证一对奶牛不会出现多次。在喝茶时间活动中,如果奶牛 \(i\) 和奶牛 \(j\) 有一个相同的朋友奶牛 \(k\),那么他们会在某次的喝茶活动中去认识对方(成为朋友),从而扩大他们的社交圈。请判断,在喝茶活动举办很久以后(直到没有新的奶牛彼此认识),\(Q (1 \leq Q \leq 100)\) 对奶牛是否已经彼此认识。询问j包含一对不同的奶牛编号 \(X_j\) 和 \(Y_j\)(\(1 \leq X_j \leq N, 1 \leq Y_j \leq N\))。
例如,假设共有 \(5\) 头奶牛,我们知道 \(2\) 号认识 \(5\) 号,\(2\) 号认识 \(3\) 号,而且 \(4\) 号认识 \(5\) 号;如下图(a)。
2---3 2---3 2---3
\ |\ | |\ /|
1 \ --> 1 | \ | --> 1 | X |
\ | \| |/ \|
4---5 4---5 4---5
(a) (b) (c)
在第一次喝茶活动中,\(2\) 号认识 \(4\) 号,\(3\) 号认识 \(5\) 号,如上图(b)所示。接下来的喝茶活动中,\(3\) 号认识了 \(4\) 号,如上图(c)所示。
【输入】
第 \(1\) 行:三个空格隔开的整数:\(N\),\(M\) 和 \(Q\)。
第 \(2 \dots M+1\) 行:第 \(i+1\) 行包含两个空格隔开的整数 \(A_i\) 和 \(B_i\)。
第 \(M+2 \dots M+Q+1\) 行:第 \(j+M+1\) 行包含两个空格隔开的整数 \(X_j\) 和 \(Y_j\),表示询问 \(j\)。
【输出】
第 \(1 \dots Q\) 行:如果第 \(j\) 个询问的两头奶牛认识,第 \(j\) 行输出 Y。如果不认识,第 \(j\) 行输出 N。
【输入样例】
5 3 3
2 5
2 3
4 5
2 3
3 5
1 5
【输出样例】
Y
Y
N
【核心思想】
-
问题分析:给定 \(N\) 头奶牛和 \(M\) 对初始朋友关系,朋友关系具有传递性(若 \(i\) 和 \(k\) 是朋友,\(k\) 和 \(j\) 是朋友,则 \(i\) 和 \(j\) 也会成为朋友)。求 \(Q\) 对奶牛最终是否认识。这是一个并查集/传递闭包问题,关键在于用并查集维护朋友关系的连通分量,同一连通分量内的奶牛互相认识。
-
算法选择:
- 并查集(Union-Find):用路径压缩和按秩合并维护连通分量,支持 \(O(\alpha(N))\) 的查询和合并
- 传递性抽象:将"通过共同朋友介绍而认识"的传递关系抽象为图的连通性,同一连通分量即互相认识
-
关键步骤:
- 初始化:\(fa[i] = i\)(\(i\) 从 \(1\) 到 \(N\))
- 合并朋友关系(\(M\) 对 \((A_i, B_i)\)):
- \(unionn(A_i, B_i)\):将 \(A_i\) 和 \(B_i\) 所在集合合并
- 查询处理(\(Q\) 对 \((X_j, Y_j)\)):
- 若 \(find(X_j) = find(Y_j)\):输出
Y(同一连通分量,认识) - 否则:输出
N(不同连通分量,不认识)
- 若 \(find(X_j) = find(Y_j)\):输出
-
时间/空间复杂度:
- 时间复杂度:\(O((M + Q) \cdot \alpha(N))\),并查集操作近似常数时间
- 空间复杂度:\(O(N)\),父节点数组
-
并查集的核心思想:
- 集合代表元:每个连通分量有一个代表元(祖先节点),\(find(x)\) 返回 \(x\) 所在集合的代表元
- 路径压缩:查询时将访问过的节点直接指向祖先,加速后续查询
- 传递闭包的动态维护:无需显式计算所有传递关系,通过合并操作隐式维护连通性
- 适用于"动态连通性 + 等价类查询"类问题,核心是维护集合的合并与查询
【解题思路】

【算法标签】
普及- #并查集
【代码详解】
#include <bits/stdc++.h>
using namespace std;
int n, m, q, fa[1005];
void init(int n) // 并查集初始化
{
for (int i=1; i<=n; i++) { // 每个节点的父节点都是自己
fa[i] = i;
}
}
int find(int x) // 查找某个节点的祖先节点
{
if (fa[x]==x) { // 如果节点的祖先节点就是自己
return x; // 那就到达递归出口,返回
} else { // 否则
fa[x] = find(fa[x]); // 路径压缩,查过一次后,fa[x]就直接指向了祖先节点
return fa[x]; // 返回这个节点的父节点
}
}
void unionn(int i, int j) // 合并操作
{
int i_fa = find(i); // 找到i节点的祖先节点
int j_fa = find(j); // 找到j节点的祖先节点
fa[i_fa] = j_fa; // 将i节点的祖先指向j的祖先
}
int main()
{
cin >> n >> m >> q; // 输入n、m和q
init(n); // 初始化所有节点的父节点
for (int i=1; i<=m; i++) { // 输入m对节点对,将其进行合并
int x, y;
cin >> x >> y;
unionn(x, y);
}
for (int i=1; i<=q; i++) { // 依次遍历q次询问
int x, y;
cin >> x >> y;
if (find(x)==find(y)) cout << "Y" << endl; // 如果x和y的祖先节点相同,则说明他们认识
else cout << "N" << endl;
}
return 0;
}
【运行结果】
5 3 3
2 5
2 3
4 5
2 3
Y
3 5
Y
1 5
N
浙公网安备 33010602011771号