给定一个 n 个点的无向完全图,从中删去 m 条边,问图中有多少个连通块。
输出连通块的数量,并输出每个连通块的具体情况。
输入格式
输入的第一行包含两个正整数 n,m,用一个空格分隔,分别表示点数和边数。
接下来 m 行,每行包含两个正整数 ui,vi,用一个空格分隔,表示删除 ui 和 vi 之间的边。保证同一条边不会被删除两次。
输出格式
输出的第一行包含一个整数 x,表示连通块的数量。
接下来 x 行,每行包含一个连通块的描述。其中第一个数 y 表示该连通块的点数,接下来 y 个数,依次表示其中每个点的编号,相邻整数之间使用一个空格分隔。连通块按其中最小结点的升序输出;每个连通块内的点按升序排列。
输入输出样例
输入 #1复制
5 6
1 2
2 4
2 5
1 3
3 4
3 5
输出 #1复制
2
3 1 4 5
2 2 3
说明/提示
评测用例规模与约定
对于 40% 的评测用例,n≤1000;
对于所有评测用例,1≤n,m≤2×1e5,m≤n(n−1)/2,ui,vi,1≤ui,vi≤n。
这道题的"正解"巧妙在:它利用链表(List)或并查集/集合(Set)动态维护"还未被访问的节点",从而将原本 O(N^2) 的暴力扫描,优化到了 O(N+M)。
1. 核心矛盾:为什么暴力会慢?
在普通 BFS 中,当我们访问节点 u 时,我们需要找到它的所有邻居。
-
普通图:邻居很少,直接遍历 G[u] 即可。
-
补图:邻居极多(接近 N 个)。如果你遍历每一个点 v 来检查它是不是 u 的邻居,你就会陷入 O(N^2) 的泥潭。
2. 破局思路:反向思维
我们不问"谁是 u的邻居",而问:"在还没被访问的点里,谁【不是】 u 的邻居?"
-
在补图中,v 不是 u的邻居 在原图中,v 是 u 的邻居。
-
原图里的邻居很少(总共才 M 条边)。
3. 正解算法步骤
我们要维护一个未访问列表(Unvisited List),里面初始包含 1 到 n。
-
开启新块:从列表中取出一个点(比如第一个点),作为 BFS 的起点,并从列表中删掉它。
-
寻找邻居:对于当前点 u:
-
在原图中,把 u 的所有邻居(那些被删掉边的另一头)打上标记(比如
tag[v] = true)。 -
遍历当前的未访问列表。
-
对于列表中的每个点 v:
-
如果
tag[v]是false:说明 u, v在补图中有边!把 v 加入 BFS 队列,并立即从列表中彻底删除 v。 -
如果
tag[v]是true:说明 u, v在原图中被删了边,补图中不连通。跳过 v,让它留在列表里等别人来救它。
-
-
清空标记 :把 u 在原图邻居的
tag改回false,准备处理下一个点。
-
4. 为什么正解这么快?
这个算法的时间复杂度是 O(N+M),原因如下:
-
每个节点只会被放进队列一次。
-
每个节点一旦被加入连通块,就会从"未访问列表"中被永久删除。
-
对于每个节点 u,它只会让原本在列表中可能成为邻居的点"失败" M 次(即对应的删除边数)。
主要还是看代码
#include <iostream>
#include <vector>
#include <list>
#include <queue>
#include <algorithm>
using namespace std;
const int MAXN = 200005;
vector<int> G[MAXN]; // 存储原图被删掉的边
bool tag[MAXN]; // 标记原图中的邻居
list<int> unvisited; // 核心:未访问节点列表
vector<vector<int>> comps;
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
int n, m;
cin >> n >> m;
for (int i = 0; i < m; i++) {
int u, v;
cin >> u >> v;
G[u].push_back(v);
G[v].push_back(u);
}
// 初始化:所有点都在未访问列表中
for (int i = 1; i <= n; i++) unvisited.push_back(i);
while (!unvisited.empty()) {
vector<int> cur_comp;
queue<int> q;
// 取出一个点作为新连通块的起点
int start = unvisited.front();
unvisited.pop_front();
q.push(start);
//bfs搜索连通块
while (!q.empty()) {
int u = q.front();
q.pop();
cur_comp.push_back(u);
// 1. 标记 u 在原图(被删边)里的所有邻居
for (int v : G[u]) tag[v] = true;
// 2. 扫描未访问列表,寻找补图邻居
for (auto it = unvisited.begin(); it != unvisited.end(); ) {
if (!tag[*it]) {
// 如果没有标记,说明补图连通
q.push(*it);
it = unvisited.erase(it); // 关键:从列表中彻底移除,下次不再扫描
} else {
it++; // 被标记了,留着等下一个人
}
}
// 3. 还原标记,供 BFS 下一个节点使用
for (int v : G[u]) tag[v] = false;
}
sort(cur_comp.begin(), cur_comp.end());
comps.push_back(cur_comp);
}
// 题目要求的排序输出
sort(comps.begin(), comps.end(), [](const vector<int>& a, const vector<int>& b) {
return a[0] < b[0];
});
cout << comps.size() << "\n";
for (auto& c : comps) {
cout << c.size();
for (int x : c) cout << " " << x;
cout << "\n";
}
return 0;
}
//解释下为啥要使用链表,一个visited数组貌似也可以完成功能,的确,但是效率会差很多,链表这种删除方式使得遍历所有未访问过的点的数量会越来越少,而visited数组即便可以跳过已经访问的点,但是你这个跳过仍需要消耗额外的时间