C语言经典算法之普里姆(prim)算法

JJJ69 2024-07-01 17:05:05 阅读 95

目录

前言

A.建议

B.简介

一 代码实现

A.伪代码:

B.代码

二 时空复杂度

A.时间复杂度:

B.空间复杂度:

三 优缺点

A.优点:

B.缺点:

四 现实中的应用


前言

A.建议

1.学习算法最重要的是理解算法的每一步,而不是记住算法。

2.建议读者学习算法的时候,自己手动一步一步地运行算法。

B.简介

在C语言中实现普利姆(Prim)算法通常涉及数据结构的选择(如邻接矩阵或邻接表)、优先队列来存储待处理边以及变量来追踪已加入最小生成树的顶点。

一 代码实现

A.伪代码:

1. 初始化:

- 设定一个n×n的邻接矩阵表示图G。

- 创建一个布尔数组visited[n],初始化为false,表示所有顶点未被访问。

- 创建一个二维数组dist[n][n],用于存放从已选择节点到其他节点的最短距离。

- 初始化一个最小生成树的边集为空。

2. 选择任意一个顶点u作为起始顶点,标记visited[u]为true。

3. 使用一个优先队列(例如:最小堆)存储未访问节点及其对应的最小权值边。

4. 主循环直到优先队列为空:

a. 从优先队列中取出当前具有最小权值的边 (u, v),其中u已在生成树中,v不在生成树中。

b. 如果visited[v]为false,则执行以下操作:

i. 将边(u, v)添加到最小生成树中。

ii. 更新dist数组中与顶点v相连的所有未访问节点的距离,如果新发现的路径更短,则更新它们在优先队列中的位置。

iii. 标记顶点v为已访问visited[v] = true。

5. 最终,最小生成树的边将构成在dist数组中记录的所有已访问节点之间的最短路径。

6. 返回最小生成树的边集。

B.代码

//C语言简化的实际实现示例(不完全模拟优先队列,仅用数组代替)

#include <stdio.h>

#define MAXN 100 // 假设最大顶点数为100

#define INF INT_MAX // 一个足够大的数代表无穷大

// 邻接矩阵

int graph[MAXN][MAXN];

// 是否已被加入最小生成树

bool visited[MAXN];

// 存储从已选节点到其他节点的最短距离

int dist[MAXN];

void prim(int n, int start) {

// 初始化visited数组和dist数组

for (int i = 0; i < n; ++i) {

visited[i] = false;

dist[i] = INF;

}

dist[start] = 0;

// 进行n-1次迭代(因为要选出n-1条边)

for (int i = 0; i < n - 1; ++i) {

int min_dist = INF, u = -1, v = -1;

// 寻找当前未访问且与已选集合连接的最小权重边

for (int j = 0; j < n; ++j) {

if (!visited[j] && dist[j] < min_dist) {

min_dist = dist[j];

u = j; // 已在生成树中的顶点

}

}

// 更新visited并找到与u相连的、未加入最小生成树且权值最小的顶点v

for (int k = 0; k < n; ++k) {

if (!visited[k] && graph[u][k] != 0 && graph[u][k] < dist[k]) {

dist[k] = graph[u][k];

v = k; // 新加入生成树的顶点

}

}

// 确保找到了新的顶点

if (v != -1) {

visited[v] = true;

// 输出或累计最小生成树的边 (u, v)

printf("Edge (%d, %d) added to MST with weight %d\n", u + 1, v + 1, graph[u][v]);

}

}

}

int main() {

// 假设已填充了邻接矩阵graph,这里省略读取或构建的过程

int n = /* 图的顶点数 */;

prim(n, 0); // 从顶点0开始构造最小生成树

return 0;

}

注意:上述C语言实现为了简化,并没有采用高效的优先队列数据结构(比如斐波那契堆)。在实际应用中,尤其是对于大规模图来说,应当使用优先队列以确保算法的时间复杂度接近于

O(E log_2E)

或者

O(E log_2 V)

。此外,在大型项目中,还需要考虑加锁等并发控制机制,以便多线程环境下的安全运行。

二 时空复杂度

A.时间复杂度

朴素实现:如果使用邻接矩阵来存储图,每次循环都要检查所有未加入树中的节点的边,因此在最坏情况下,每个节点都要被检查

n-1

次,总共需要进行大约

n(n-1)/2

次操作,所以时间复杂度是

O(n^2)

当使用邻接表(优先队列优化),Prim算法可以得到显著优化。通过维护一个优先队列(例如使用二叉堆或斐波那契堆)来高效地找到当前距离最小的节点,这样每一步的时间复杂度可以降到对数级别。对于包含

n

个节点和

m

条边的图,Prim算法的最优时间复杂度可以达到

O(m log_2 n)

,因为在整个过程中会处理所有的边,并且每次都从优先队列中提取最小元素。

最优情况下的时间复杂度分析:尽管理论上存在最优情况下算法更快的情况,但实际上我们通常关注的是平均或最坏情况下的时间复杂度。实际上,Prim算法在实际应用中经常基于优先队列优化来实现以达到较好的性能。

B.空间复杂度

朴素实现:Prim算法的空间复杂度主要消耗在于存储图、记录已选择节点的信息以及记录每个节点到当前生成树的距离等辅助数据结构。如果用邻接矩阵表示图,则空间复杂度为

O(n^2)

;若使用邻接表,空间复杂度则为

O(n + m)

优化实现:当使用优先队列优化时,除了存储图所需的空间外,还需要额外的空间来维持优先队列和其他辅助变量,但总体上空间复杂度仍然与图的大小成正比,即

O(n)

或者在某些实现中可能是

O(n + m)

三 优缺点

A.优点:

效率高:当图中的边数量与顶点数量接近时,Prim算法通过使用优先队列优化后的时间复杂度可以达到 O(m log n),其中m为图的边数,n为顶点数。这意味着在稠密图上表现优秀。

适用范围广:Prim算法适用于任何连通加权无向图,并且能够确保找到所有顶点都包含在内的最小生成树。

易于理解:Prim算法的思想直观,从一个初始节点开始逐步扩充树,每次加入一条连接已选顶点集和未选顶点集中距离最小的边,算法流程相对简单易懂。

并行化潜力:虽然Prim原算法是序列化的,但其思想可以通过一定的策略进行并行化实现,特别是在现代计算环境中具有潜在的优势。

B.缺点:

空间需求:即使经过优化,Prim算法通常需要额外的数据结构来存储每条边的权重以及每个顶点到当前生成树的距离信息,这会占用一定量的空间,尤其是对于大图而言。

局部最优解:虽然最终结果一定是全局最优解(最小生成树),但在算法过程中可能会陷入局部最优选择,例如在没有有效优先队列优化的情况下,每次选择邻接节点而非全局最优节点会导致更多的比较操作。

不适合稀疏图:相比于Kruskal算法,当图非常稀疏,即边的数量远小于顶点数量的平方时,Kruskal算法由于其按边权排序后依次添加的特性,可能比Prim算法更高效。

并行实现复杂性:尽管有并行化潜力,但实际上要设计一个高效的并行Prim算法并不容易,因为它涉及到对数据的共享访问和同步问题,这些都需要精心设计才能避免冲突和保持正确性。

四 现实中的应用

网络设计

在构建或优化电信网络、计算机网络时,Prim算法可以帮助找到连接所有节点所需的最低成本路径集合。例如,在铺设电缆或建立无线通信塔时,最小生成树能确保所有站点相互连接且总成本最低。

城市基础设施规划

城市规划中,如构建水管、天然气管道、电力线路等基础设施网络时,可以利用Prim算法找出连通所有住宅区或建筑物的最经济路线布局。

旅行推销员问题(TSP)简化版

虽然Prim算法不直接解决旅行推销员问题,但在某些简化场景下,比如确定一个区域内需要访问的所有地点之间的最小距离框架网络,它可以作为基础步骤来指导后续的路线规划。

地图应用

地图软件可能使用类似Prim的方法来寻找两点间的最短多边形边界,以界定某个地理区域或计算两个城市间经过一系列城市的最短路径。

物流与供应链管理

在配送中心选址或者物流网络规划时, Prim算法能够帮助决定如何在保持所有仓库和市场之间相互可达的前提下,以最少的成本建设或升级交通路线。

数据结构可视化

在数据可视化领域,尤其是在图形用户界面展示加权关系图时,Prim算法可用于生成简洁而连贯的子图,仅包含最重要或权重最低的边。

金融投资组合优化

在金融市场中构建资产组合时,投资者可能需要选择一组证券,使得这些证券的投资组合整体风险较低,同时实现最大程度的分散化。尽管这不是经典的最小生成树问题,但相似的优化思想可被借鉴用于选择“最佳”关联资产。



声明

本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。