原题目链接:
P1536 村村通 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
原题目截图:
思路分析:
思路一:并查集法
我们先看一个例子:
可见,我们只要求出题目给的数据,能够组成多少联通分量,就可以求出要加几条路了。
而并查集,就是我们处理这道题的关键。
什么是并查集:
并查集(Union-Find)是一种用于处理一些不交集(Disjoint Sets)的集合操作的数据结构。它支持两种主要的操作:
- Find:确定一个元素属于哪个子集。通常使用“路径压缩”技术来优化查询的效率。
- Union:将两个元素所属的集合合并。
并查集通常用于处理一些不相交集合的合并及查询问题,比如网络连接性、等价类划分、图的连通分量等。
我在csdn上看见一个写的很好的博客,详细的说明了什么是并查集和用法:
解决代码:
#include<iostream>
#include<vector>
using namespace std;
struct DSU {
vector<int> pre;
vector<int> rank;
int num; // 集合的个数
DSU(int size) : pre(size), rank(size, 0), num(size) {
for (int i = 0; i < size; i++)
pre[i] = i;
}
int find(int x) { // 查找x的代表元
if (x == pre[x]) return x;
return pre[x] = find(pre[x]); // 路径压缩算法
}
void union_set(int x, int y) {
// 找到x,y的代表元
int _x = find(x), _y = find(y);
if (_x == _y) return; // 代表元相同说明已经是一个集合,不用处理
if (rank[_x] > rank[_y]) pre[_y] = _x;
else if (rank[_x] < rank[_y]) {
pre[_x] = _y;
}
else {
pre[_x] = _y;
rank[_y]++;
}
num--; // 合并后,总集合数减一
}
bool isconected(int x, int y) { // 判断两个元素是否在同一个集合
return find(x) == find(y);
}
};
int main() {
int n, m;
while (true) {
cin >> n;
if (n == 0) break;
cin >> m;
DSU dsu(n); // 因为从1开始数的
for (int i = 0; i < m; i++) {
int town_x, town_y;
cin >> town_x >> town_y;
dsu.union_set(town_x-1 , town_y-1 ); // 假设输入是从1开始的,需要减1
}
cout << dsu.num-1<< endl; // 输出剩余的集合数量
}
return 0;
}
思路二:BFS广度优先搜索
不过很显然,相比并查集方法,BFS方法有些许的问题:
第一需要储存图,占用空间很大。
第二如果这个图的连通分量很多,那么就需要多次调用BFS,时间开销大。
当然,这道题因为数据量并不大,因此也能通过。
解决代码:
#include <iostream>
#include <vector>
#include <queue>
using namespace std;
// BFS算法寻找连通分量
int bfs(int start, vector<bool>& visited, const vector<vector<int>>& graph) {
queue<int> q;
q.push(start);
visited[start] = true;
int components = 0;
while (!q.empty()) {
int current = q.front();
q.pop();
components++;
for (int neighbor : graph[current]) {
if (!visited[neighbor]) {
visited[neighbor] = true;
q.push(neighbor);
}
}
}
return components > 0 ? 1 : 0; // 如果有访问过的节点,返回1,否则返回0
}
int main() {
int n, m;
while (cin >> n && n != 0) {
cin >> m;
vector<vector<int>> graph(n + 1); // 创建图
for (int i = 0; i < m; i++) {
int town_x, town_y;
cin >> town_x >> town_y;
graph[town_x].push_back(town_y);
graph[town_y].push_back(town_x); // 因为是无向图
}
vector<bool> visited(n + 1, false);
int components = 0;
for (int i = 1; i <= n; i++) {
if (!visited[i]) {
components += bfs(i, visited, graph);
}
}
// 输出结果:最少还需要建设的道路数目
cout << components - 1 << endl;
}
return 0;
}
总结两种方法的利弊:
并查集
优势:
-
高效性:并查集特别适用于处理动态连通性问题,即频繁地执行合并和查找操作的场景。其平均时间复杂度为O(α(n)),其中α是阿克曼函数的反函数,增长非常慢,近似于O(1)。
-
路径压缩:并查集通过路径压缩技术,可以在查找操作中减少查找深度,使得操作更加快速。
-
按秩合并:并查集通过按秩合并策略,可以保持树的平衡,避免形成过深的树结构,从而保持操作的高效性。
劣势:
-
实现复杂:并查集的实现相对复杂,尤其是路径压缩和按秩合并的优化策略。
-
不保留具体路径:并查集不保留元素之间的具体路径,只记录了元素所属的集合,因此如果需要路径信息,则并查集不适合。(这是并查集特点之一,不关注具体怎么连接,只关注是否连接)
-
不能撤销操作:一旦两个集合合并,无法撤销合并操作。
BFS算法
优势:
-
直观简单:BFS算法的实现相对直观和简单,容易理解和实现。
-
保留路径信息:BFS在搜索过程中可以记录路径,如果需要路径信息,BFS是更好的选择。
-
适用性广:BFS不仅可以用来找连通分量,还广泛应用于其他图搜索问题,如最短路径问题。
劣势:
-
效率较低:对于大规模数据,BFS的时间复杂度为O(V+E),其中V是顶点数,E是边数,相比于并查集的近似O(1),效率较低。
-
空间消耗大:BFS需要使用队列来存储待访问的节点,对于大规模数据,可能需要较大的内存空间。
-
重复访问:在没有优化的情况下,BFS可能会重复访问节点,需要额外的逻辑来避免重复访问。
总结
-
并查集更适合处理动态连通性问题,尤其是需要频繁进行合并和查找操作的场景。
-
BFS算法更适合解决需要路径信息的图搜索问题,且实现相对简单直观。
因此,在这一道“村村通工程”问题中,如果只关心连通性而不关心路径,那么并查集是一个更高效的选择。如果需要知道具体的连通路径或者问题规模较小,BFS也是一个不错的选择。
今天的博客就写到这里了,明天就是国庆节了,预祝各位国庆节have a good day!