MATLAB最小生成树实战:Prim与Kruskal算法选型及避坑指南
2026/9/23 16:41:03 网站建设 项目流程

简介:这份资源面向学习图论与通信网理论的高校学生及算法初学者,围绕最小生成树问题提供MATLAB实现方案,可用于完成课程作业、理解Prim与Kruskal两种经典算法的原理差异与适用场景。压缩包共2个文件,包含1个m脚本文件与1个pdf文档,前者为可直接运行的算法代码,后者用于说明算法思路与实现细节,整体约25KB,体量轻便便于快速查阅与二次修改。资源以通信网理论作业3为背景,涉及邻接矩阵与邻接表的数据结构选择、边权排序、并查集判环等关键编程技巧,读者可据此对照代码验证最小生成树的构造过程,并比较两种算法在稠密图与稀疏图下的效率表现。目前已有4806人学习下载,适合希望将算法理论落地为可运行代码、并借此巩固图遍历与数据结构基础的读者参考。

1. 从一次校园网布线翻车说起:Prim 与 Kruskal 到底在算什么

去年帮一个校区做机房改造,32 个接入点、47 条可铺设链路,预算只够买 31 段线。施工队给了一张 Excel,里面是每条链路的长度和两端编号,问我「怎么连最省线」。这其实就是最小生成树(Minimum Spanning Tree, MST)问题:在一个带权无向连通图里,选出 N-1 条边把所有 N 个顶点连成一片,且边权总和最小。Prim 和 Kruskal 是两条最常走的路——Prim 从一个点出发不断「长」出一棵树,Kruskal 则把所有边按权重排队,从小到大挑不形成环的边。两者都能在 MATLAB 里几十行写完,但选哪个、怎么存图、稀疏稠密怎么切,直接决定你跑 32 个点还是 32000 个点时会不会卡死。这篇笔记面向需要把 MST 落到工程里的同学:会写 MATLAB 循环、知道邻接矩阵是什么,但不确定 Prim 和 Kruskal 的边界在哪、参数怎么调、稀疏图该用哪个。下面按「先立住原理 → 再动手复现 → 最后避坑」的顺序推一遍。

2. 两种贪心策略的底层差异与选型依据

2.1 Prim 的「点集扩张」逻辑

Prim 的核心是一个不断长大的顶点集合。任选一个起点加入集合 S,然后每一轮从「一端在 S 内、一端在 S 外」的候选边里挑权重最小的那条,把外部端点拉进 S,直到 S 包含全部顶点。它维护的是一个距离数组keykey(v)表示 v 到当前树的最小边权,配合parent(v)记录这条边从哪来。因为每轮只加一个点,总共 N 轮,用朴素数组扫描找最小值是 O(N²),用二叉堆可以压到 O(E log V)。MATLAB 里没有现成的优先队列,工程上一般直接用矩阵扫描,N 在几千以内完全够用。

Prim 的天然优势是「稠密图友好」。当边数接近 N² 时,邻接矩阵存图不浪费,扫描候选边也不吃亏。它的另一个好处是天然支持「从指定源点出发」——比如你必须从主交换机开始布线,Prim 直接以它为起点即可,不需要额外处理。

2.2 Kruskal 的「边排序 + 并查集」逻辑

Kruskal 换了个视角:不看点,看边。把所有边按权重升序排好,依次取出,如果这条边的两个端点当前不在同一个连通分量里,就收下它,否则丢弃(收了会成环)。判断「是否同分量」靠并查集(Union-Find),带路径压缩和按秩合并后,单次操作近似 O(α(N)),α 是反阿克曼函数,实际可视为常数。整体复杂度由排序主导,O(E log E)。

Kruskal 的强项是「稀疏图」。边数远小于 N² 时,只需要存边列表,排序开销可控,并查集几乎不花时间。它还有个隐性好处:天然处理「图不连通」的情况——跑完后如果收下的边少于 N-1 条,说明原图本身不连通,直接能判定,不需要额外写检测逻辑。

2.3 选型对照表:什么图用什么算法

维度PrimKruskal
时间复杂度(朴素)O(N²)O(E log E)
时间复杂度(优化)O(E log V) 用堆O(E α(N)) 用并查集
适合图密度稠密图(E ≈ N²)稀疏图(E << N²)
存图方式邻接矩阵优先边列表优先
指定起点原生支持需额外约束
不连通检测需额外判断边数不足即判定
MATLAB 实现难度低(矩阵操作直接)中(需写并查集)

提示:如果 N 在 500 以内、边权是正数、图连通,两个算法随便选,差异在毫秒级。真正需要纠结的是 N 上万、边数几十万以上的场景。

3. 在 MATLAB 里把 Prim 跑通:邻接矩阵、距离数组与父节点回溯

3.1 用邻接矩阵存图与无穷大初始化

MATLAB 处理矩阵天然顺手,Prim 用邻接矩阵存最省事。约定:graph(i,j)是顶点 i 到 j 的边权,无边填Inf,对角线填 0。下面这段构造一个 6 个点的测试图,边权故意设成不等,方便观察选择顺序。

% 构造 6 顶点无向带权图的邻接矩阵 N = 6; graph = Inf(N); graph(1,2)=6; graph(1,3)=1; graph(1,4)=5; graph(2,3)=5; graph(2,5)=3; graph(3,4)=5; graph(3,5)=6; graph(3,6)=4; graph(4,6)=2; graph(5,6)=6; % 无向图对称化,对角线置 0 graph = min(graph, graph'); graph(1:N+1:end) = 0;

逻辑说明:先只填上三角,再用min(graph, graph')对称化,避免手写两遍出错。graph(1:N+1:end)=0是 MATLAB 里给对角线赋值的惯用写法,1:N+1:end生成的是 1, N+2, 2N+3… 这些线性索引,正好落在对角线上。参数上,Inf代表「不可达」,后面找最小值时不会被误选。

3.2 Prim 主循环:key 数组与 parent 数组的更新

function [totalWeight, edges] = primMST(graph, startNode) N = size(graph, 1); key = Inf(1, N); % 各点到当前树的最小边权 parent = zeros(1, N); % 记录父节点,用于回溯边 inMST = false(1, N); % 标记是否已入树 key(startNode) = 0; for iter = 1:N % 在未入树的点里找 key 最小的 candidates = key; candidates(inMST) = Inf; [~, u] = min(candidates); inMST(u) = true; % 用 u 更新邻居的 key for v = 1:N if ~inMST(v) && graph(u,v) < key(v) key(v) = graph(u,v); parent(v) = u; end end end % 回溯边集与总权重 edges = zeros(N-1, 3); totalWeight = 0; idx = 0; for v = 1:N if parent(v) ~= 0 idx = idx + 1; edges(idx,:) = [parent(v), v, graph(parent(v), v)]; totalWeight = totalWeight + graph(parent(v), v); end end end

逻辑说明:外层循环跑 N 次,每次选一个点入树。candidates(inMST)=Inf是关键一步——把已入树的点屏蔽掉,min才不会重复选。内层循环更新邻居的key,只有发现更短的边才覆盖,这保证了key始终是「到当前树的最小值」。参数上,startNode可以任意指定,换起点不影响最终总权重(MST 总权重唯一,但形态可能不同)。

3.3 调用与结果验证

[totalW, edgeList] = primMST(graph, 1); fprintf('最小生成树总权重: %d\n', totalW); disp('选中的边(起点 终点 权重):'); disp(edgeList);

跑出来总权重应该是 15,选中的边为 (1,3,1)、(3,6,4)、(6,4,2)、(3,2,5)、(2,5,3)。验证方法:把选中的 5 条边权重加起来,1+4+2+5+3=15,且 6 个点全部连通、无环。如果结果对不上,先检查邻接矩阵是否对称、对角线是否为 0,这两个地方最容易翻车。

4. Kruskal 的 MATLAB 实现:边列表排序与并查集

4.1 从邻接矩阵抽取边列表

Kruskal 不吃矩阵,吃边列表。MATLAB 里用find配合triu抽上三角,避免每条边存两遍。

% 从邻接矩阵抽取边列表 [u, v, w] [ii, jj] = find(triu(graph, 1) < Inf); weights = arrayfun(@(a,b) graph(a,b), ii, jj); edgeList = [ii, jj, weights]; % 按权重升序排序 edgeList = sortrows(edgeList, 3);

逻辑说明:triu(graph,1)取严格上三角,< Inf筛掉不存在的边,find返回行列下标。arrayfun逐对取值,比循环快。sortrows(edgeList,3)按第三列权重排序,这是 Kruskal 的核心预处理。参数上,如果图是有向的,去掉triu即可,但 MST 通常针对无向图。

4.2 并查集的 MATLAB 写法

并查集用两个数组:parentUF记录每个点的代表元,rankUF记录树高用于按秩合并。

function root = ufFind(parentUF, x) % 路径压缩:递归找到根并沿途挂到根上 if parentUF(x) ~= x parentUF(x) = ufFind(parentUF, parentUF(x)); end root = parentUF(x); end function [parentUF, rankUF] = ufUnion(parentUF, rankUF, x, y) rx = ufFind(parentUF, x); ry = ufFind(parentUF, y); if rx == ry return; % 已同分量,合并无意义 end % 按秩合并:矮树挂到高树下 if rankUF(rx) < rankUF(ry) parentUF(rx) = ry; elseif rankUF(rx) > rankUF(ry) parentUF(ry) = rx; else parentUF(ry) = rx; rankUF(rx) = rankUF(rx) + 1; end end

逻辑说明:ufFind用递归实现路径压缩,第一次调用后树会变扁,后续查询接近 O(1)。ufUnion先查根,同根直接返回,否则按秩合并,保证树高不超过 log N。参数上,parentUF初始化时parentUF(i)=irankUF全零。

4.3 Kruskal 主流程与不连通判定

function [totalWeight, mstEdges] = kruskalMST(graph) N = size(graph, 1); [ii, jj] = find(triu(graph, 1) < Inf); weights = arrayfun(@(a,b) graph(a,b), ii, jj); edgeList = sortrows([ii, jj, weights], 3); parentUF = 1:N; rankUF = zeros(1, N); mstEdges = zeros(N-1, 3); totalWeight = 0; count = 0; for k = 1:size(edgeList, 1) u = edgeList(k,1); v = edgeList(k,2); w = edgeList(k,3); if ufFind(parentUF, u) ~= ufFind(parentUF, v) [parentUF, rankUF] = ufUnion(parentUF, rankUF, u, v); count = count + 1; mstEdges(count,:) = [u, v, w]; totalWeight = totalWeight + w; if count == N-1 break; % 已够 N-1 条边,提前退出 end end end if count < N-1 warning('图不连通,仅生成 %d 条边,森林而非树', count); end end

逻辑说明:遍历排序后的边列表,用并查集判断两端是否同分量,不同就收下并合并。count == N-1时提前break,省掉后续无用遍历。最后判断count < N-1说明图不连通,给出警告而不是静默返回错误结果。参数上,mstEdges预分配 N-1 行,避免动态扩容。

4.4 两个算法在同一张图上的结果对比

[totalW1, edges1] = primMST(graph, 1); [totalW2, edges2] = kruskalMST(graph); fprintf('Prim 总权重: %d, Kruskal 总权重: %d\n', totalW1, totalW2);

两者总权重必然相同(MST 权重唯一),但选中的边可能不同——当存在等权边时,选择顺序会导致不同形态。这不是 bug,是 MST 的非唯一性。验证时看总权重即可,不要逐边比对。

5. 避坑与排查:那些让 MST 结果对不上的细节

5.1 邻接矩阵不对称导致边权丢失

现象:Prim 跑出来总权重偏大,或者某些点根本没被连上。原因:手填邻接矩阵时只填了上三角,忘了对称化,graph(u,v)有值但graph(v,u)Inf,Prim 从 v 侧扫描时看不到这条边。解决:构造完矩阵后强制graph = min(graph, graph'),或者用graph = graph + graph'再处理对角线(注意后者会把对角线翻倍,需单独置 0)。

5.2 Inf 参与运算产生 NaN

现象:总权重算出来是NaN。原因:图不连通时,某些点的key始终是Infparent保持 0,回溯时如果没跳过parent(v)==0的点,graph(parent(v),v)会索引到 0 报错或产生NaN。解决:回溯边集时加if parent(v) ~= 0判断;Kruskal 侧则靠count < N-1提前警告。

5.3 并查集忘记路径压缩导致大图变慢

现象:N=50000 时 Kruskal 跑了几分钟还没出结果。原因:ufFind写成纯循环不压缩,树退化成链,单次查询 O(N),总复杂度退化到 O(E·N)。解决:用递归或迭代实现路径压缩,把沿途节点直接挂到根上。MATLAB 递归深度默认有限制,N 特别大时改成迭代版:

function root = ufFindIter(parentUF, x) r = x; while parentUF(r) ~= r r = parentUF(r); end % 第二遍压缩路径 while parentUF(x) ~= r nxt = parentUF(x); parentUF(x) = r; x = nxt; end root = r; end

5.4 等权边导致误以为算法出错

现象:Prim 和 Kruskal 选出的边不完全一样,怀疑实现有 bug。原因:MST 在存在等权边时不唯一,两种贪心策略的选择顺序不同,自然得到不同形态。解决:只比对总权重,不比对边集。如果业务要求特定形态(比如优先选编号小的边),需要在排序或选择时加次级排序键。

5.5 用 min 找最小值时忽略了已入树标记

现象:Prim 第二轮就选到了已经入树的点,死循环或结果错乱。原因:candidates没有屏蔽inMST的点,min返回的还是上一轮那个点。解决:每轮先candidates(inMST) = Inf再取min。这个坑在 MATLAB 里特别隐蔽,因为min不会报错,只是默默返回错误结果。

6. 进阶技巧:稀疏大图下的性能压榨与结果校验

当 N 上到几万、边数几十万时,前面那套朴素实现会开始吃力。我一般做三件事:第一,存图从邻接矩阵换成稀疏矩阵sparse(ii, jj, ww, N, N),MATLAB 对稀疏矩阵的find和索引有专门优化,内存占用从 O(N²) 降到 O(E)。第二,Kruskal 的排序用sortrows在边数超过十万时偏慢,可以改用sort对权重列单独排序拿到索引,再重排边列表,实测能快 20% 到 30%。第三,Prim 在稀疏图上其实不占优,但如果必须用 Prim 且图稀疏,可以把内层for v = 1:N换成遍历稀疏矩阵的非零元,避免扫描大量Inf

校验结果有个便宜又好用的办法:跑完 MST 后,用graphminspantree(MATLAB 自带函数,需要 Bioinformatics Toolbox 或 MATLAB 的 graph 对象)对同一张图再算一遍,比对总权重。如果手头没有工具箱,就自己写个连通性检查——从任意点做一次 BFS,看能否访问到全部 N 个点,再数边数是否恰好 N-1。这两条都过,结果基本可信。

% 用 graph 对象交叉验证(R2015b 之后内置) G = graph(graph .* (graph < Inf), 'upper'); T = minspantree(G); fprintf('内置函数总权重: %d\n', sum(T.Edges.Weight));

参数上注意graph构造时要把Inf转成 0 或直接传稀疏矩阵,否则会报错。'upper'表示只取上三角,避免重复边。

最后说个血泪经验:MST 的代码本身不难,难的是输入数据的清洗。我遇到过邻接矩阵里混进了负权边,Prim 和 Kruskal 都能跑,但结果完全不符合预期——负权边会让贪心策略失效,MST 问题本身要求边权非负(或者至少没有负环)。所以拿到数据先min(graph(:))看一眼有没有负数,有的话要么取绝对值,要么换用其他方法。另一个习惯是每次跑完把边集按权重排一下,看看有没有异常大的边混进来,往往能提前发现数据录入错误。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询