并查集判断设计图是否为一棵树:任意两点有且仅有一条路径,即无环且连通。
OJ: luogu
题目 ID: P2307
难度:普及+/提高
标签:并查集图论树
日期: 2026-08-05 11:35
题意
多组数据。每组给若干条边(房间号对),以 0 0 结束;整个文件以 -1 -1 结束。
判断设计图是否符合要求:任意两个房间有且仅有一条路径可以相通。符合输出 1,否则输出 0。
思路
最直接的想法是建图后暴力检查:
cpp
/**
* Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2026-08-05 11:30
* update_at: 2026-08-05 11:30
*/
// brute.cpp:小数据暴力解,对每组数据建邻接表,
// 用 BFS 判连通 + DFS 判环,与并查集解法独立实现。
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 100005;
vector<int> g[MAXN]; // 邻接表
bool appear[MAXN];
bool vis[MAXN];
vector<int> nodes; // 本组出现的所有节点
// BFS:从 start 出发,统计能到达的节点数
int bfs_count(int start) {
memset(vis, 0, sizeof(vis));
queue<int> q;
q.push(start);
vis[start] = true;
int cnt = 1;
while (!q.empty()) {
int u = q.front();
q.pop();
for (int v : g[u]) {
if (!vis[v]) {
vis[v] = true;
cnt++;
q.push(v);
}
}
}
return cnt;
}
// DFS 判环:从 u 出发,若访问到已在当前栈中的点则有环
bool dfs_cycle(int u, int parent) {
vis[u] = true;
for (int v : g[u]) {
if (v == parent) continue; // 忽略回父亲的边(无向图)
if (vis[v]) return true; // 访问到已访问过的点:有环
if (dfs_cycle(v, u)) return true;
}
return false;
}
// 清空本组数据的邻接表和标记
void clear_all() {
for (int x : nodes) {
g[x].clear();
appear[x] = false;
}
nodes.clear();
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int u, v;
while (cin >> u >> v) {
if (u == -1 && v == -1) break;
if (u == 0 && v == 0) {
bool ok = true;
if (!nodes.empty()) {
// 判环
memset(vis, 0, sizeof(vis));
if (dfs_cycle(nodes[0], -1)) ok = false;
// 判连通:BFS 能到达的节点数 == 出现节点总数
int reach = bfs_count(nodes[0]);
if (reach != (int)nodes.size()) ok = false;
}
cout << (ok ? 1 : 0) << '\n';
clear_all();
continue;
}
if (!appear[u]) { appear[u] = true; nodes.push_back(u); }
if (!appear[v]) { appear[v] = true; nodes.push_back(v); }
g[u].push_back(v);
g[v].push_back(u);
}
return 0;
}暴力做法对每组数据建邻接表,用 DFS 判环、BFS 判连通。正确但每次都要遍历整张图。
关键观察:“任意两点有且仅有一条路径” ⇔ 这张无向图是一棵树 ⇔ 同时满足两个条件:
- 无环:加边时若两端点已经连通,说明这条边形成了环;
- 连通:所有出现过的房间在同一个集合里。
树的性质正好可以用并查集在线判断:
text
加边 (u, v):
├─ find(u) == find(v) → 已经连通 → 有环 → 不合法
└─ 否则合并两个集合
一组数据结束:
所有出现过的节点必须属于同一个根(连通)以样例第三组为例,边依次为 0。
注意边界:一组数据没有边(直接 0 0)是合法的空树,输出 1。
代码
cpp
/**
* Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2026-08-05 11:30
* update_at: 2026-08-05 11:30
*/
// 并查集:迷宫合法当且仅当 无环 且 所有出现过的房间连通(即是一棵树)。
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 100005;
int fa[MAXN]; // 并查集父节点
bool appear[MAXN]; // 该房间编号是否在本组数据中出现过
void init() {
for (int i = 1; i <= 100000; i++) fa[i] = i;
memset(appear, 0, sizeof(appear));
}
int find(int x) {
if (fa[x] == x) return x;
return fa[x] = find(fa[x]); // 路径压缩
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
init();
bool has_edge = false; // 本组数据是否至少有一条边
bool ok = true; // 是否满足无环且连通
int u, v;
while (cin >> u >> v) {
if (u == -1 && v == -1) break; // 整个文件结束
if (u == 0 && v == 0) { // 一组数据结束
// 有边时检查连通:找一个出现过的节点做基准根
int root = -1;
for (int i = 1; i <= 100000; i++) {
if (appear[i]) {
root = find(i);
break;
}
}
// 所有出现过的节点必须在同一个集合里
if (root != -1) {
for (int i = 1; i <= 100000; i++) {
if (appear[i] && find(i) != root) {
ok = false;
break;
}
}
}
cout << (ok ? 1 : 0) << '\n';
init();
has_edge = false;
ok = true;
continue;
}
// 本组数据内的一条边
has_edge = true;
appear[u] = true;
appear[v] = true;
int ru = find(u), rv = find(v);
if (ru == rv) ok = false; // 两点已经连通:出现环
else fa[ru] = rv;
}
return 0;
}复杂度
并查集带路径压缩,每组数据每条边
总结
“任意两点唯一路径” = 树的特征。用并查集判断"树"的通用套路:
- 加边时检查两端是否已连通(判环);
- 结束后检查所有出现节点是否同根(判连通)。
图示解析
text
合法(树) 非法(有环)
1──2──3 5──3──8
│ │ ╱
4 6──4
任意两点只有一条路 5 到 8 有两条路读图方法:左边没有环且连通,任意两点唯一路径;右边节点