最小生成树

本文介绍了图论中的最小生成树问题,重点讲解了Prim算法和Kruskal算法的实现原理及步骤,并通过实例演示了这两种算法如何找到带权连通无向图的最小生成树。同时,提供了两种算法的C++代码实现,包括使用优先队列优化Prim算法。

最小生成树

一、定义

图的所有生成树中具有边上的权值之和最小的树称为图的最小生成树 (Minimum Spanning Tree,MST) ;

一个连通图的生成树是一个极小连通子图,它含有图中全部顶点,但只有构成一棵树的 n−1n-1n1 条边;

对于一个带权 (假定每条边上的权均为大于零的数) 连通无向图 GGG 中的不同生成树,其每棵树的所有边上的权值之和也可能不同;

简单来说,对于一个有 nnn 个点的图,边一定是大于等于 n−1n-1n1 条的,最小生成树就是在这些边中选择 n−1n-1n1 条出来连接所有的 nnn 个点,且这n−1n-1n1 条边的边权之和是所有方案中最小的;

二、Prim 算法

1. 思路

Prim 算法是一种构造性算法;

假设 G=(V,E)G = (V, E)G=(V,E) 是一个具有 nnn 个顶点的带权连通无向图, T=(U,TE)T = (U, TE)T=(U,TE)GGG 的最小生成树,其中 UUUTTT 的顶点集, TETETETTT 的边集,则由 GGG 构造从起始顶点 vvv 出发的最小生成树 TTT 的步骤如下:

  1. 初始化 U={v}U = \{v\}U={v} ;以 vvv 到其他顶点的所有边为候选边;

  2. 重复以下步骤 n−1n-1n1 次,使得其他 n−1n-1n1 个顶点被加入到 UUU 中:

  3. 以顶点集 UUU 和顶点集 V−UV - UVU 之间的所有边(称为割集 (U,V−U)(U, V - U)(U,VU) )作为候选边,从中挑选权值最小的边(称为轻边)加入 TETETE ,设该边在 V−UV - UVU 中的顶点是 kkk ,将 kkk 加入 UUU 中;

  4. 考察当前 V−UV - UVU 中的所有顶点 jjj ,修改候选边,若 (k,j)(k,j)(kj) 的权值小于原来和顶点 jjj 关联的候选边,则用 (k,j)(k, j)(k,j) 取代后者作为候选边;

2. 例子

示例

使用 Prim 算法生成图的最小生成树,以结点 6 作为起点;

将结点划分成两个集合 UUUV−UV-UVU

  1. U={6},V−U={1,2,3,4,5,7}U=\{6\}, V-U=\{1, 2, 3, 4, 5, 7\}U={6},VU={1,2,3,4,5,7}

    候选边有 (6, 1, 1) 和 (6, 5, 8) ;

    选择 (6, 1, 1) 为最小生成树的一条边,并且将结点1加入到 UUU 集合中;

Prim-eg-1

此时最小生成树为,

Prim-ru-1

  1. U={6,1},V−U={2,3,4,5,7}U = \{ 6, 1 \}, V - U = \{ 2, 3, 4, 5, 7 \}U={6,1},VU={2,3,4,5,7}

    侯选边有 (1, 2, 6) 和 (6, 5 ,8) ;

    选择 (1, 2, 6) 为最小生成树的一条边,并且将结点2加入 UUU 集合中;

Prim-eg-2

此时最小生成树为,

Prim-ru-2

  1. U={6,1,2,7},V−U={3,4,5}U = \{ 6, 1, 2, 7 \}, V - U = \{ 3, 4, 5 \}U={6,1,2,7},VU={3,4,5}

侯选边有 (6, 5, 8), (7, 5, 7), (7, 4, 5), (2, 3, 4);

选择 (2, 3, 4) 为最小生成树的一条边,并且将结点3加入 UUU 集合中;

Prim-eg-3

此时最小生成树为,

Prim-ru-3

  1. U={6,1,2,7,3},V−U={4,5}U = \{ 6, 1, 2, 7, 3 \}, V - U = \{ 4, 5 \}U={6,1,2,7,3},VU={4,5}

侯选边有 (6, 5, 8), (7, 5, 7), (4, 5, 6) ;

选择 (4, 5, 6) 为最小生成树的一条边,并且将结点5加入 UUU 集合中;

Prim-eg-4

n−1n - 1n1 轮执行完毕,得到最小生成树;

结果

3. 实现

以1为起点生成最小生成树,dis[v]dis[v]dis[v] 表示与 vvv 相连的最小边权, MSTMSTMST 表示最小生成树的权值之和;

  1. 初始化,disv=∞dis_v = \inftydisv=dis[1] = 0, MST = 0 ;

  2. 遍历 1∼n1 \sim n1n 号节点,

    寻找 dis[x]dis[x]dis[x] 最小的未进入最小生成树的节点 xxx

    xxx 标记为已进入最小生成树;

    MSTMSTMST 加上当前边;

    枚举与 xxx 相邻的未进入最小生成树的节点 vvv

    (x,v)(x, v)(x,v) 的权值小于原来和顶点 vvv 关联的候选边,则用 (x,v)(x, v)(x,v) 取代后者作为候选边;

  3. 结束遍历,MSTMSTMST 即为最小生成树的权值之和;

4. 代码

int n, m, dis[MAXN], MST;
bool vis[MAXN];
struct edge {
    int to, tot;
};
vector <edge> g[MAXN];
void Prim() {
    memset(vis, false, sizeof(vis));
    memset(dis, 0x3f, sizeof(dis));
    dis[1] = 0;
    for (int i = 1; i <= n; i++) {
        int x, minn = 2147483647;
        for (int j = 1; j <= n; j++) {
            if (!vis[j] && dis[j] < minn) { // 在所有未加入最小生成树的结点中找到距离更近的结点
                minn = dis[j];
                x = j;
            }
        }
        vis[x] = true;
        for (int j = 0; j < g[x].size(); j++) {
        	int v = g[x][j].to, tot = g[x][j].tot;
            if (!vis[v] && dis[v] > tot) { // 利用新加入的结点k对还未加入到的最小生成树的结点距离进行松弛操作
                dis[v] = tot;
            }
        }
        MST += dis[x];
    }
    return;
}
int main() {
    scanf("%d %d", &n, &m);
    for (int i = 1; i <= m; i++) {
        int x, y, z;
        scanf("%d %d %d", &x, &y, &z);
        g[x].push_back(edge({y, z}));
        g[y].push_back(edge({x, z}));
    }
    Prim();
    printf("%d", MST);
    return 0;
}

5. 堆优化

寻找所有未加入最小生成树的结点中距离最近的结点时,可使用优先队列优化;

int n, m, dis[MAXN], MST; // dis[i]表示与i相连的最短路径 
bool vis[MAXN]; // vis[i] 为i是否加入最小生成树 
struct edge {
    int to, tot; // to为终点,tot为边权
    bool operator < (const edge &a) const {
	    return tot > a.tot; // 按照边权从小到大排序
	}
};
vector <edge> g[MAXN];
void Prim() {
    memset(dis, 0x3f, sizeof(dis)); 
    memset(vis, false, sizeof(vis));
    dis[1] = 0; // 初始化 
    priority_queue <edge> q;
    q.push(edge({1, 0}));
    while (!q.empty()) {
        int x = q.top().to;
        q.pop();
        if (vis[x]) continue;
        vis[x] = true;
        for (int i = 0; i < g[x].size(); i++) {
        	int v = g[x][i].to, tot = g[x][i].tot;
            if (!vis[v] && dis[v] > tot) {
                dis[v] = tot; // 将距离最近的结点加入到最小生成树中
                q.push(edge({v, dis[v]}));
            }
        }
    	MST += dis[x];
    }
    return;
}
int main() {
    scanf("%d %d", &n, &m);
    for (int i = 1; i <= m; i++) {
        int x, y, z;
        scanf("%d %d %d", &x, &y, &z);
        g[x].push_back(edge({y, z}));
        g[y].push_back(edge({x, z}));
    }
    Prim();
    printf("%d", MST);
    return 0;
}

三、Kruskal 算法

1. 思路

Kruskal 算法是一种按权值的递增次序选择合适的边来构造最小生成树的方法;

假设 G=(V,E)G=(V, E)G=(V,E) 是一个具有 nnn 个顶点 eee 条边的带权连通无向图,T=(U,TE)T=(U,TE)T=(U,TE)GGG 的最小生成树,则构造最小生成树的步骤如下;

  1. UUU 的初值等于 vvv (即包含有 GGG 中的全部顶点),TETETE 的初值为空集(即图 TTT 中每一个顶点都构成一个分量);
  2. 将图 GGG 中的边按权值从小到大的顺序依次选取:若选取的边未使生成树 TTT 形成回路,则加入 TETETE ;否则舍弃,直到 TETETE 中包含 n−1n-1n1 条边为止;

实现 Kruskal 算法的关键是如何判断选取的边是否与生成树中己有的边形成回路,这可以通过并查集来解决;

2. 例子

示例

首先对图中的边按照边权从小到大排序;

Kru-val

按照边权从小到大的顺序依次将每条边加入到最小生成树中,但不能产生回路;

依次加入 (1, 6, 1), (3, 4, 2), (2, 7, 3), (2, 3, 4) ;

当加入边 (4, 7, 5) 时,发现会产生回路,所以跳过此边;

Kru-eg-1

再按照相同的方法依次加入 (1, 2, 6), (4, 5, 6) ;

当加入边 (5, 7, 7) 时,发现会产生回路,所以跳过此边;

当加入边 (5, 6, 8) 时,发现会产生回路,所以跳过此边;

至此所有结点己经加入到了最小生成树中,算法结束;

结果

3. 实现

  1. 初始化

    每个点的父节点初始化为自己,MST = 0

  2. 遍历每一条边

    若边的两端点不在同一集合,合并两点,MSTMSTMST 加上当前边;

  3. 结束遍历,MSTMSTMST 即为最小生成树的权值之和。

4. 代码

int n, m, father[MAXN], MST;
struct edge {
    int x, y, z; // (x, y) 边,边权为 z
    bool operator < (const edge a) { // 边权从小到大排序 
	    return z < a.z;
	}
} g[EDGE_MAXN];
void firstset(int n) { // 初始化并查集 
    for (int i = 1; i <= n; i++) {
        father[i] = i;
    }
    return;
}
int findset(int x) { // 查找并查集初始点 
    if (father[x] == x) return x;
    else return father[x] = findset(father[x]);
}
void Kruskal(int n, int m) {
	firstset(n);
    sort(1 + g, 1 + g + m); // 边按边权排序 
    for (int i = 1; i <= m; i++) { // 从小到大便利每一条边 
        int x = findset(g[i].x), y = findset(g[i].y);
        if (x != y) { // 边的两端点不在同一集合 
            father[x] = y; // 合并 
            MST += g[i].z; // 最小生成树加上当前边 
        }
    }
    return;
}
int main() {
	scanf("%d %d", &n, &m);
	for (int i = 1; i <= m * 2; i += 2) {
		int x, y, z;
		scanf("%d %d %d", &x, &y, &z);
		g[i].x = g[i + 1].x = x;
		g[i].y = g[i + 1].y = y;
		g[i].z = g[i + 1].z = z;
	}
	Kruskal(n, m * 2);
	printf("%d\n", MST);
	return 0;
}
<think>好的,用户想了解Kruskal算法的实现和原理,特别是关于最小生成树的部分。我需要先回忆一下Kruskal算法的基本概念和步骤。首先,Kruskal算法是一种贪心算法,用于在连通图中找到最小生成树。它的核心思想是按边的权重从小到大排序,然后逐步选择不形成环的边,直到所有顶点都连通。 接下来,用户可能需要知道具体的实现步骤。我应该分点说明:第一步是排序所有边,第二步是使用并查集来检测环。需要解释并查集的作用,即在合并两个集合时判断是否形成环。这部分需要详细一点,因为并查集对于不熟悉的用户来说可能不太容易理解。 然后,关于时间复杂度,Kruskal算法的时间复杂度主要是由排序和并查集操作决定的。排序通常是$O(E \log E)$,而并查集操作近似线性,所以总的时间复杂度是$O(E \log E + E \alpha(V))$,其中$\alpha$是阿克曼函数的反函数,增长缓慢。这部分可能需要简化说明,让用户更容易理解。 用户提供的引用资料中提到了Kruskal算法的实现细节,比如边排序和环检测,我需要确保我的回答涵盖这些点,并适当引用。例如,引用[1]提到使用并查集判断环,引用[2]讨论了贪心策略和边排序,这些都需要整合到回答中。 另外,用户可能需要代码示例来更好地理解算法实现。根据引用[3]和[4],C++的实现代码可能有用,但用户的问题中没有指定语言,所以可以考虑提供一个通用的伪代码或Python示例。但根据用户提供的示例中的Python代码,可能更适合用Python示例,但引用中的代码是C++,需要确认。不过用户的问题可能更关注原理,代码示例可以辅助说明。 可能还需要对比Prim算法,但用户没有提到,所以暂时不需要展开。但相关问题可以提到,作为扩展问题。 最后,生成的相关问题需要围绕Kruskal算法的优化、应用场景、与其他算法的比较等。比如,如何优化排序过程,或者在实际应用中的案例。 现在需要确保回答结构清晰,分步骤解释原理,代码示例正确,引用适当。同时,检查LaTeX格式是否正确,所有数学表达式用$...$,独立公式用$$...$$,比如时间复杂度部分可能需要独立公式。 总结,回答的结构应该是: 1. 算法原理(贪心、步骤) 2. 实现细节(排序、并查集) 3. 时间复杂度分析 4. 代码示例(Python或C++) 5. 引用整合 6. 相关问题生成 需要确保每个部分自然衔接,引用标识放在相关解释的末尾。例如,在讲解并查集时引用[1]和[4]。 可能还需要注意术语的一致性,比如“连通块”或“集合”在解释过程中的使用。此外,避免过于技术性的术语,用通俗的中文解释,同时保持准确性。</think>### Kruskal算法原理与实现 #### 一、算法原理 Kruskal算法是一种基于**贪心策略**的最小生成树算法,其核心思想是: 1. **边排序**:将图中所有边按权重从小到大排序[^2] 2. **逐条选择**:从最小权重边开始,依次选择不构成环的边加入生成树 3. **终止条件**:当选择的边数达到$n-1$($n$为顶点数)时停止 算法通过**并查集(Union-Find)**数据结构判断是否形成环。当两个顶点属于同一连通分量时,连接它们的边会形成环,此时应舍弃该边[^1][^4]。 #### 二、算法步骤 1. 初始化每个顶点为一个独立连通块 2. 按边权重升序排序 3. 遍历排序后的边: - 若边的两个顶点属于不同连通块: - 合并两个连通块 - 将该边加入最小生成树 4. 重复步骤3直到生成树包含$n-1$条边 时间复杂度为$O(E \log E)$,主要来自排序操作,其中$E$为边数。 #### 三、Python代码实现 ```python class UnionFind: def __init__(self, size): self.parent = list(range(size)) def find(self, x): if self.parent[x] != x: self.parent[x] = self.find(self.parent[x]) return self.parent[x] def union(self, x, y): root_x = self.find(x) root_y = self.find(y) if root_x != root_y: self.parent[root_y] = root_x def kruskal(vertices, edges): edges.sort(key=lambda x: x[2]) uf = UnionFind(vertices) mst = [] for u, v, weight in edges: if uf.find(u) != uf.find(v): uf.union(u, v) mst.append((u, v, weight)) if len(mst) == vertices - 1: break return mst ``` #### 四、关键优化 1. **路径压缩**:在并查集的`find`操作中压缩路径,降低时间复杂度 2. **按秩合并**:在`union`操作中根据树深度决定合并方向[^3] 3. **提前终止**:当已选边数达到$n-1$时立即停止遍历[^4]
评论 12
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值