C++刷题周记(五)——并查集/最小生成树(Prim/Kruskal)

目录

最小生成树

基础概念

应用场景

辨析最小生成树与最短路径

Prim算法

概念

代码模板

辨析Prim与Dijkstra

并查集

思路

Kruskal算法

概念

代码模板

两种方法的辨析


最小生成树

基础概念

图论中的最小生成树指的是:包含原图的所有节点(假设图的节点数为n,最小生成树的边数则为n-1),且所用边权值最小的一条路径。

ps:图中可能存在重边和自环,边权可能为负数

应用场景

要在n个城市之间铺设光缆,主要目标是要使这 n 个城市的任意两个之间都可以通信,但铺设光缆的费用很高,且各个城市之间铺设光缆的费用不同,因此另一个目标是要使铺设光缆的总费用最低。这就需要找到带权的最小生成树

其他不同的表达方式:城市群之间修建公路/铁路,局部区域岛屿联通修桥......

常用Prim/Kruskal这两种算法解决最小生成树问题。

辨析最小生成树与最短路径

最小生成树:把连通的图的所有顶点连起来路径之和最小的问题,即生成树路径总权值之和最小。

最短路:把两点之间路径最短。最短路只需要将连接起点到终点,其路径并不一定经过所有点,即图中可以有孤立点。而最小生成树要连接图中的每一个点,不能有孤立点。

Prim算法

概念

Prim算法 : 把所有点到 已连通集合 的距离dis设成∞ ,每次找到距离最小的点,加入到连通集合中,并用该点距离进行松弛操作,更新所有点到集合距离 dis[i]=min(dis[i],g[t][i])
即:从图中任意找一个起点,每次循环均要找到当前距连通集合最近的点,直到所有点都加入连通集合。

代码模板

模板题:Acwing 858      该代码与朴素Dijkstra的板子相似,流程与思想也比较接近

// 稠密图用邻接矩阵
int g[N][N];
// 记录点至连通集合的距离
int dist[N];
// 结点是否在连通集合中
bool state[N];
// 与迪杰算法的区别 D中的松弛操作是更新到起始点的距离
// 而Prim是更新到集合S的距离
int prim(){
    memset(dist,0x3f,sizeof dist);
    int res = 0;
    // 当i=0时 第一次循环 所取的t点一定为1 
    dist[1] = 0;// 起点的dist一定为0 从而无需在循环里特判
    for(int i = 0; i < n; i++){
        int t = -1;
        //*1 寻找已连通集合之外 距连通集合最近的点t
        for(int j = 1; j <= n; j++){
            if(!state[j] && (t == -1 || dist[t] > dist[j])){
                t = j;
            }
        }
        // *2 状态更新,加入结果
        // 当距离为INF,表示图中有不与集合连通的孤立点,此时肯定没有最小生成树
        if(dist[t] == INF) return INF;
        res += dist[t];
        state[t] = true;
        
        // *3 g[t][j]即为j至 刚加入连通集合中的点t 的距离 根据此进行松弛操作
        for(int j = 1; j <= n; j++){
            dist[j] = min(dist[j],g[t][j]);
        }
    }
    return res;
}

辨析Prim与Dijkstra

参考资料:AcWing 858. prim 与dijkstra的区别​​​​​​

相同点:算法时间复杂度均为O(n^2)

与朴素版的三部曲流程与思想基本一致

区别

Prim算法 : 把所有点到 已连通集合 的距离dis设成∞ ,每次找到未加入集合的距离最小的点t,加入到连通集合中,并用该点距离进行松弛操作,更新所有点到集合距离 dis[i]=min(dis[i],g[t][i])
即:从图中任意找一个起点,每次循环均要找到当前距连通集合最近的点,直到所有点都加入连通集合。

dijkstra:把所有点到 起点 距离dis设成∞ ,每次找到未遍历过的距离最小的点t 加入路径中 修改状态,并用该点距离进行松弛操作,更新所有点到起点距离 dis[i]=min(dis[i],w+g[t][i]);
即:基于题意确定起点,每次确定距离最近的点,直到终点

唯一区别就是,dijkstra 更新的是到起点的距离,prim更新的是到连通集合的距离

在代码实现层面,即state状态数组与dist距离数组的意义不同,2*步骤更新状态的具体操作不同

并查集

在了解Kruskal算法之前,我们需要先补充其核心思想所用到的并查集

顾名思义,并查集是一种大大提升两个集合间的合并集合中的元素查询的一种数据结构

模板题:Acwing 836     Acwing 837 连通块中点的数量

练习题:lc 547. 省份数量

思路:

参考资料:AcWing 836. 基础_并查集_合并集合​​​​​​

1. 初始化:

// 每个节点的father数组
int p[N];//p[i]--节点i所在集合的根节点(即祖宗节点)    
for(int i = 1;i <= n; i++){
    p[i] = i;// 初始化 令当前节点的父节点均为自己
}

示意图(来源于上面的参考资料)如下: 

2.查找 + 路径压缩:

// 返回x的祖宗节点 + 路径压缩
// 把整条查找路径上所有节点的父节点都变成了祖宗节点
int find(int x){
    // 递归终止条件:祖先节点的父节点是自己本身
    if(p[x] != x){
        // 将x的父亲置为x父亲的祖先节点,实现路径的压缩
        p[x] = find(p[x]);
    }
    return p[x];
}

因为一次查找可以递归将整条查找路径上所有节点的路径压缩,将压缩路径的消耗平均至每个节点,查询的时间复杂度接近O(1) 

3. 合并操作

        if(op == 'M'){
            // 合并操作-令a的祖宗节点的父节点 为 b的祖宗节点
            p[find(a)] = find(b);
        }

假设有以下两个集合 

合并1,5所在的集合
find(1) = 3 find(5) = 4    p[find(1)] = find(5) –> p[3] = 4
如下图所示

Kruskal算法

概念

Kruskal算法将一个连通块当做一个集合。(参考资料:最小生成树详解(模板 + 例题)_潘小蓝)

①.Kruskal首先将所有的边按从小到大顺序排序(一般使用快排),并认为每一个点都是孤立的,分属于n个独立的集合。然后按从小到大顺序枚举每一条边。时间复杂度:O(mlogm)

②.如果这条边连接着两个不同的集合,那么就把这条边加入最小生成树,这两个不同的集合就合并成了一个集合(使用并查集)。如果这条边连接的两个点属于同一集合,就跳过本次操作。直到选取了n-1条边为止。时间复杂度:O(m)

代码模板

模板题:Acwing 859

#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 10,M = 2 * N;
int n,m;
int res = 0,cnt = 0;
int p[N];
// krus使用并查集 并不需要将图完整存储 
// 只需将每条边孤立地存储即可
struct Edge{
    int a,b,w;
}edges[M];
bool cmp(Edge a,Edge b){
    return a.w < b.w;
}

int find(int x){// 并查集找祖宗
    if(p[x] != x)
        p[x] = find(p[x]);
    return p[x];
}
void kruskal(){
    // 1* 对边进行排序
    sort(edges,edges + m,cmp);

    // 2* 使用并查集 按边权从小至大合并集合
    for(int i = 0; i < m; i++){
        // 从小到大遍历
        int a = edges[i].a, b = edges[i].b, w = edges[i].w;
        a = find(a),b = find(b);
        if(a != b){
            p[a] = b;// 合并集合
            res += w;
            cnt++;// 记录已处理的边数
        }
    }
}
int main(){
    ios::sync_with_stdio(false);
    cin.tie(0);
    cin >> n >> m;
    for(int i = 0; i < m; i++){
        int a,b,w;
        cin >> a >> b >> w;
        edges[i] = {a,b,w};
    }
    // 点的并查集初始化
    for(int i = 1; i <= n; i++) p[i] = i;
    kruskal();
    
    if(cnt == n-1){
        cout << res;
    }else{
        cout <<"impossible";
    }
}

两种方法的辨析

稠密图——优先选择Prim方法,时间复杂度为O(n^2)一般采用 邻接矩阵 进行存储边.
稀疏图——优先选择Kruskal,遍历每条边来决定是否合并集合,使时间复杂度O(mlogm)主要受边数的影响。一般采用邻接表进行存储边之间的关系(更简洁方便的是采用结构体的方式,只需将每条边的起点、终点、权值孤立地存储即可)。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
算法是解决特定问或执行特定任务的一系列步骤或规则的有序集合。在计算机科学中,算法通常用来指导计算机执行特定的任务或解决问。良好设计的算法能够有效地解决问,并且在给定的输入下能够产生正确的输出。 常见的算法包括但不限于以下几种: 排序算法:排序算法是将一组数据按照一定的顺序排列的算法。常见的排序算法包括冒泡排序、插入排序、选择排序、快速排序、归并排序等。 搜索算法:搜索算法用于在数据集中查找特定元素的算法。常见的搜索算法包括线性搜索、二分搜索等。 图算法:图算法用于处理图结构的数据,如最短路径算法(如Dijkstra算法、Floyd-Warshall算法)、最小生成算法(如Prim算法、Kruskal算法)等。 动态规划:动态规划是一种通过将问分解成更小的子问来解决复杂问的算法。常见的动态规划问包括背包问、最长递增子序列、编辑距离等。 贪心算法:贪心算法是一种在每一步选择中都采取当前状态下最优决策的算法。常见的贪心算法包括最小生成算法中的Prim算法、Dijkstra算法等。 字符串匹配算法:字符串匹配算法用于在一个字符串(文本)中查找一个子串(模式)的出现位置。常见的字符串匹配算法包括暴力匹配、KMP算法、Boyer-Moore算法等。 这些是计算机科学中常见的算法类型,每种算法都有不同的应用场景和解决问的方法。在实际编程中,选择合适的算法对于提高程序效率和性能至关重要。
Prim算法和Kruskal算法都是用于求解最小生成的经典算法。 Prim算法的基本思想是从一个点开始,每次选择一个与当前生成距离最近的点加入生成中,直到所有点都被加入生成为止。具体实现时,可以使用一个优先队列来维护当前生成与未加入生成的点之间的距离,每次选择距离最小的点加入生成中。 Kruskal算法的基本思想是从边开始,每次选择一条权值最小且不会形成环的边加入生成中,直到生成中包含所有点为止。具体实现时,可以使用并查集来判断是否形成环。 下面是Prim算法和Kruskal算法的C语言代码实现: Prim算法: ```c #include <stdio.h> #include <stdlib.h> #include <limits.h> #define MAX_VERTICES 1000 int graph[MAX_VERTICES][MAX_VERTICES]; int visited[MAX_VERTICES]; int dist[MAX_VERTICES]; int prim(int n) { int i, j, u, min_dist, min_index, sum = 0; for (i = 0; i < n; i++) { visited[i] = 0; dist[i] = INT_MAX; } dist[0] = 0; for (i = 0; i < n; i++) { min_dist = INT_MAX; for (j = 0; j < n; j++) { if (!visited[j] && dist[j] < min_dist) { min_dist = dist[j]; min_index = j; } } u = min_index; visited[u] = 1; sum += dist[u]; for (j = 0; j < n; j++) { if (!visited[j] && graph[u][j] < dist[j]) { dist[j] = graph[u][j]; } } } return sum; } int main() { int n, m, i, j, u, v, w; scanf("%d%d", &n, &m); for (i = 0; i < n; i++) { for (j = 0; j < n; j++) { graph[i][j] = INT_MAX; } } for (i = 0; i < m; i++) { scanf("%d%d%d", &u, &v, &w); graph[u][v] = graph[v][u] = w; } printf("%d\n", prim(n)); return 0; } ``` Kruskal算法: ```c #include <stdio.h> #include <stdlib.h> #include <limits.h> #define MAX_VERTICES 1000 #define MAX_EDGES 1000000 struct edge { int u, v, w; }; int parent[MAX_VERTICES]; struct edge edges[MAX_EDGES]; int cmp(const void *a, const void *b) { return ((struct edge *)a)->w - ((struct edge *)b)->w; } int find(int x) { if (parent[x] == x) { return x; } return parent[x] = find(parent[x]); } void union_set(int x, int y) { parent[find(x)] = find(y); } int kruskal(int n, int m) { int i, sum = 0; for (i = 0; i < n; i++) { parent[i] = i; } qsort(edges, m, sizeof(struct edge), cmp); for (i = 0; i < m; i++) { if (find(edges[i].u) != find(edges[i].v)) { union_set(edges[i].u, edges[i].v); sum += edges[i].w; } } return sum; } int main() { int n, m, i; scanf("%d%d", &n, &m); for (i = 0; i < m; i++) { scanf("%d%d%d", &edges[i].u, &edges[i].v, &edges[i].w); } printf("%d\n", kruskal(n, m)); return 0; } ```

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值