连通块的遍历

给定一个 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。

  1. 开启新块:从列表中取出一个点(比如第一个点),作为 BFS 的起点,并从列表中删掉它。

  2. 寻找邻居:对于当前点 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数组即便可以跳过已经访问的点,但是你这个跳过仍需要消耗额外的时间

相关推荐
alxraves2 小时前
超声诊断图像的关键算法概述
算法·安全·健康医疗·制造·信号处理
mask哥2 小时前
15种算法模式java实现详解
java·算法·力扣
若尘7972 小时前
数学idea的重构
算法·职场和发展·机器人
思茂信息2 小时前
CST可重构雷达吸波器设计与仿真
网络·算法·平面·智能手机·重构·cst·电磁仿
游乐码2 小时前
c#插入排序
数据结构·算法·排序算法
史迪仔01122 小时前
[QML] Qt6/Qt5四大渐变效果实战指南
开发语言·前端·c++·qt
乐迪信息2 小时前
乐迪信息:AI防爆摄像机,船舶偏航逆行算法实时告警零漏检
大数据·人工智能·物联网·算法·机器学习·计算机视觉·目标跟踪
昵称小白2 小时前
图论专题(上)
算法·深度优先·图论
大大杰哥2 小时前
leetcode hot100(2)双指针,滑动窗口
数据结构·算法·leetcode