把关闭谷仓的过程倒过来看成重新开门,按倒序激活点并用并查集维护当前开着的连通块数量。
OJ: luogu
题目 ID: P6121
难度:普及+/提高
标签:并查集图论模拟
日期: 2026-06-20 00:03
题意
给一张无向图,顶点表示谷仓,边表示道路。
接下来会按给定顺序一个一个关闭谷仓。每次都要回答:
- 在当前这一次关闭之前
- 还开着的所有谷仓是否两两连通
也就是当前农场是否仍然是“全连通”的。
思路
先看一个小数据暴力:
cpp
// brute.cpp:小数据暴力解,用来帮助理解题意并辅助对拍。
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 25;
int n, m;
vector<int> graph[MAXN];
int close_order[MAXN];
bool alive[MAXN];
bool vis[MAXN];
bool check_connected() {
int start = 0;
for (int i = 1; i <= n; i++) {
if (alive[i]) {
start = i;
break;
}
}
if (start == 0) {
return true;
}
for (int i = 1; i <= n; i++) {
vis[i] = false;
}
queue<int> q;
q.push(start);
vis[start] = true;
while (!q.empty()) {
int u = q.front();
q.pop();
for (int v : graph[u]) {
if (!alive[v] || vis[v]) {
continue;
}
vis[v] = true;
q.push(v);
}
}
for (int i = 1; i <= n; i++) {
if (alive[i] && !vis[i]) {
return false;
}
}
return true;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> m;
for (int i = 1; i <= n; i++) {
graph[i].clear();
alive[i] = true;
}
for (int i = 1; i <= m; i++) {
int u, v;
cin >> u >> v;
graph[u].push_back(v);
graph[v].push_back(u);
}
for (int i = 1; i <= n; i++) {
cin >> close_order[i];
}
for (int i = 1; i <= n; i++) {
cout << (check_connected() ? "YES" : "NO") << '\n';
alive[close_order[i]] = false;
}
return 0;
}暴力的想法很直接:
- 维护哪些谷仓还开着
- 每次关闭之前做一次 BFS/DFS
- 检查所有还开着的点是否在同一个连通块里
这个写法可以帮助理解题意,但如果每次都重新搜整张图,复杂度太高。
关键观察是:关闭操作不好维护,但把过程倒过来就很好维护。
原问题是:
- 一开始所有谷仓都开着
- 然后不断关闭
倒过来看就是:
- 一开始所有谷仓都关着
- 按关闭顺序的逆序,一个一个重新打开
这样每次“打开”一个谷仓时,只需要把它和当前已经开着的相邻谷仓并查集合并即可。
于是我们维护:
:谷仓 当前是否已经被重新打开 components:当前开着的谷仓形成了多少个连通块
每次倒序打开一个点
- 先把
,因为新开了一个独立点 - 枚举它所有邻居
- 如果
也开着,并且它们原来不在同一个集合,就合并,并让
这样当这一轮处理结束后:
- 如果
,说明当前所有开着的谷仓全连通
再把这个答案倒回原顺序输出即可。
代码
cpp
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 200005;
int n, m;
vector<int> graph[MAXN];
int close_order[MAXN];
bool open_[MAXN];
bool ans[MAXN];
struct DSU {
int fa[MAXN], sz[MAXN];
void init(int n) {
for (int i = 1; i <= n; i++) {
fa[i] = i;
sz[i] = 1;
}
}
int find(int x) {
if (fa[x] == x) {
return x;
}
fa[x] = find(fa[x]);
return fa[x];
}
bool unite(int x, int y) {
x = find(x);
y = find(y);
if (x == y) {
return false;
}
if (sz[x] < sz[y]) {
swap(x, y);
}
fa[y] = x;
sz[x] += sz[y];
return true;
}
} dsu;
void read_input() {
cin >> n >> m;
for (int i = 1; i <= n; i++) {
graph[i].clear();
open_[i] = false;
}
for (int i = 1; i <= m; i++) {
int u, v;
cin >> u >> v;
graph[u].push_back(v);
graph[v].push_back(u);
}
for (int i = 1; i <= n; i++) {
cin >> close_order[i];
}
}
void solve() {
dsu.init(n);
int components = 0; // 当前开着的谷仓形成了多少个连通块
for (int i = n; i >= 1; i--) {
int u = close_order[i];
open_[u] = true;
components++;
for (int v : graph[u]) {
if (!open_[v]) {
continue;
}
if (dsu.unite(u, v)) {
components--;
}
}
ans[i] = (components == 1);
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
read_input();
solve();
for (int i = 1; i <= n; i++) {
cout << (ans[i] ? "YES" : "NO") << '\n';
}
return 0;
}复杂度
设点数为
- 每条边最多在倒序加入时检查两次
- 并查集合并与查询均摊近似常数
总时间复杂度
总结
这题的典型点在于“删点难,倒序加点容易”。一旦把关闭过程翻过来看,问题就变成了并查集维护动态图连通块数量的标准模型。
一图流解析
这张图把本题的建模、关键转移、实现检查和训练方法压缩到一页,适合读完正文后复盘。
