最小生成树算法
最小生成树(Minimum Spanning Tree,MST)是图论中一个重要的概念,表示连接图中所有顶点的树,同时保证总权值最小。
比较:
-
贪心策略的不同:
- Prim 算法是一种顶点驱动的贪心算法,从一个初始顶点开始,每次选择距离当前生成树最近的顶点,并将其加入生成树中。这意味着 Prim 算法每次都是在已经形成的生成树上加入一个新的顶点,直到所有顶点都被加入。
- Kruskal 算法是一种边驱动的贪心算法,它首先将所有边按照权值从小到大排序,然后依次选择权值最小的边,如果选择的边不会形成环路,就将其加入生成树中。这意味着 Kruskal 算法是在所有边的集合上进行操作,直到生成树中包含了所有顶点。
-
数据结构的不同:
- Prim 算法通常使用优先队列或最小堆来实现,以快速找到距离当前生成树最近的顶点。
- Kruskal 算法通常使用并查集来检测是否形成环路,以保证生成树的连通性。
-
时间复杂度:
- 在稠密图(边数量接近顶点数量的平方)上,Prim 算法的时间复杂度为 O(V^2),其中 V 是顶点的数量。
- 在稀疏图(边数量远小于顶点数量的平方)上,Prim 算法的时间复杂度可以优化到 O(E log V),其中 E 是边的数量。
- Kruskal 算法的时间复杂度为 O(E log E),其中 E 是边的数量,因为它需要对所有边进行排序。
-
适用性:
- 当图是稠密图时,Prim 算法通常比较适用,因为它的时间复杂度不受边的数量的影响。
- 当图是稀疏图时,Kruskal 算法通常比较适用,因为它的时间复杂度与边的数量相关,而稀疏图的边数量相对较少。
Prim算法
Prim算法是一种贪心算法,它从一个初始顶点开始,逐步扩展生成树,每次选择与当前树相邻的权值最小的边加入。
算法步骤:
- 选择一个初始顶点作为生成树的根节点。
- 初始化一个空的生成树集合和一个优先队列(或最小堆),将初始顶点及其相邻边加入优先队列。
- 从优先队列中选择权值最小的边,将其加入生成树集合,并将其相邻的顶点及边加入优先队列。
- 重复步骤3,直到生成树包含所有顶点。
代码示例:
class PriorityQueue {/*** 优先队列构造函数*/constructor() {// 初始化队列为空数组this.queue = [];}/*** 将顶点及其权重添加到优先队列* @param {string} vertex - 顶点* @param {number} weight - 权重*/enqueue(vertex, weight) {this.queue.push({ vertex, weight });this.sort();}/*** 从优先队列中删除并返回顶点及其权重* @returns {Object} - 包含顶点及其权重的对象*/dequeue() {return this.queue.shift();}/*** 对队列进行排序*/sort() {this.queue.sort((a, b) => a.weight - b.weight);}/*** 检查队列是否为空* @returns {boolean} - 如果队列为空则返回true,否则返回false*/isEmpty() {return this.queue.length === 0;}
}/*** 使用Prim算法查找最小生成树* @param {Object} graph - 表示图的邻接表* @returns {Array} - 包含最小生成树的边的数组*/
function prim(graph) {const visited = {};const mst = [];const startVertex = Object.keys(graph)[0];const priorityQueue = new PriorityQueue();visited[startVertex] = true;// 将起始顶点的所有相邻顶点及权重添加到优先队列for (const neighbor in graph[startVertex]) {const weight = graph[startVertex][neighbor];priorityQueue.enqueue(neighbor, weight);}// 遍历优先队列,直到队列为空while (!priorityQueue.isEmpty()) {const { vertex, weight } = priorityQueue.dequeue();if (!visited[vertex]) {visited[vertex] = true;// 将顶点、权重和起始顶点添加到最小生成树中mst.push({ from: startVertex, to: vertex, weight });// 将顶点的所有未访问相邻顶点及其权重添加到优先队列for (const neighbor in graph[vertex]) {if (!visited[neighbor]) {const weight = graph[vertex][neighbor];priorityQueue.enqueue(neighbor, weight);}}}}return mst;
}// 示例图
const graph = {A: { B: 2, D: 3 },B: { A: 2, C: 1, D: 1 },C: { B: 1, D: 4, E: 5 },D: { A: 3, B: 1, C: 4, E: 1 },E: { C: 5, D: 1 }
};const minimumSpanningTree = prim(graph);
console.log(minimumSpanningTree);
Kruskal算法
Kruskal算法是一种贪心算法,它首先将所有边按照权值从小到大进行排序,然后依次选择权值最小且不形成环的边加入生成树,直到生成树包含所有顶点。
算法步骤:
- 将图中所有边按照权值从小到大进行排序。
- 初始化一个空的生成树集合。
- 依次选择排序后的边,如果该边的两个顶点不在同一个连通分量中,则将该边加入生成树,并合并两个连通分量。
- 重复步骤3,直到生成树包含所有顶点。
代码示例:
class DisjointSet {/*** 创建一个新的并查集* @param {number} n - 初始大小*/constructor(n) {this.parent = new Array(n).fill(-1);}/*** 查找元素所属的集合* @param {number} x - 要查找的元素* @returns {number} - 元素所属的集合的根节点*/find(x) {if (this.parent[x] < 0) return x;return this.parent[x] = this.find(this.parent[x]);}/*** 合并两个集合* @param {number} x - 第一个元素* @param {number} y - 第二个元素* @returns {boolean} - 如果两个元素属于不同的集合,则返回true;否则返回false*/union(x, y) {// 查找节点x的根节点const rootX = this.find(x);// 查找节点y的根节点const rootY = this.find(y);// 如果两个节点不是同一个根节点if (rootX !== rootY) {// 如果节点x的根节点的父节点小于节点y的根节点的父节点if (this.parent[rootX] < this.parent[rootY]) {// 将节点x的根节点的父节点值加上节点y的根节点的父节点值this.parent[rootX] += this.parent[rootY];// 将节点y的根节点的父节点设置为节点x的根节点this.parent[rootY] = rootX;// 如果节点y的根节点的父节点小于等于节点x的根节点的父节点} else {// 将节点y的根节点的父节点值加上节点x的根节点的父节点值this.parent[rootY] += this.parent[rootX];// 将节点x的根节点的父节点设置为节点y的根节点this.parent[rootX] = rootY;}// 返回true,表示合并成功return true;}// 如果两个节点是同一个根节点,表示已经在同一个集合中,不需要合并return false;}
}/**
* 使用Kruskal算法查找最小生成树
* @param {Object} graph - 表示图的邻接表
* @returns {Array} - 包含最小生成树的边的数组
*/
function kruskal(graph) {// 存储边的数组const edges = [];// 存储顶点的数组const vertices = Object.keys(graph);// 创建一个并查集const disjointSet = new DisjointSet(vertices.length);// 存储最小生成树的边const mst = [];// 遍历图中的所有边,并将它们添加到边数组中// 将所有边添加到边数组中for (const from in graph) {for (const to in graph[from]) {edges.push({ from, to, weight: graph[from][to] });}}// 对边数组进行排序,按照边的权重从小到大排序// 根据权重对边数组进行排序edges.sort((a, b) => a.weight - b.weight);// 遍历排序后的边数组,依次加入最小生成树中for (const edge of edges) {const { from, to, weight } = edge;// 如果两个顶点不在同一个集合中,则加入最小生成树,并合并两个集合if (disjointSet.union(vertices.indexOf(from), vertices.indexOf(to))) {mst.push(edge);}}// 返回最小生成树return mst;
}// 示例图
const graph = {A: { B: 2, D: 3 },B: { A: 2, C: 1, D: 1 },C: { B: 1, D: 4, E: 5 },D: { A: 3, B: 1, C: 4, E: 1 },E: { C: 5, D: 1 }
};const minimumSpanningTree = kruskal(graph);
console.log(minimumSpanningTree);