最小生成树算法
最小生成树(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);