最小生成树
参考资料
水平有限,欢迎交流
【图-最小生成树-Prim (普里姆) 算法和 Kruskal (克鲁斯卡尔) 算法】
【Kruskal一往无前,并查集鼎力相助(算法童话第一回)】
【Prim稳扎稳打,最小堆暗中相助(算法童话第二回)】
练习题
P3366 【模板】最小生成树 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
leetcode1584
Kruskal(克鲁斯卡尔)算法和 Prim(普里姆)算法都是用于寻找图中最小生成树(Minimum Spanning Tree, MST)的经典算法。这两种算法虽然目的相同,但在实现细节、效率和适用场景上有所不同。下面简要概述两种算法的主要优点和缺点:
Kruskal 算法(找最小的边)
优点:
- 简单易懂:Kruskal 算法的逻辑相对直接,易于理解和实现。
- 适用于稀疏图:当图中的边数远小于节点数的平方时,Kruskal 算法通常比 Prim 算法更高效,因为它的时间复杂度主要取决于边的数量。
- 并行处理友好:由于 Kruskal 算法在每一步中可以独立地选择最短的边,因此它非常适合并行处理。
缺点: - 需要额外的数据结构:为了检测环路,Kruskal 算法通常需要使用并查集(Union-Find)等数据结构,这增加了算法的空间复杂度。
- 对于稠密图效率较低:当图非常密集时,即边的数量接近于节点数量的平方时,Kruskal 算法的效率会低于 Prim 算法。
//路径压缩并查集
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Arrays;
class Edge implements Comparable<Edge>{
int src;
int dest;
int w;
Edge(int src,int dest,int w){
this.src = src;
this.dest = dest;
this.w = w;
}
@Override
public int compareTo(Edge o){
return this.w-o.w;
}
}
class UnionFind {
private int []parent;
UnionFind(int v){
parent = new int[v];
for (int i = 0; i < v; i++) {
parent[i] = i;
}
}
int find(int n){
if(parent[n]!=n){
parent[n] = find(parent[n]);
}
return parent[n];
}
void union(int x,int y){
parent[find(x)] = find(y);
}
}
class KruskalMST{
private int v;
private Edge[]edges;
private UnionFind uf;
KruskalMST(int v,Edge[]edges){
this.v = v;
this.edges = edges;
uf = new UnionFind(v);
}
void kruskalMST(){
int e = 0;
int res = 0;
Arrays.sort(edges);
while(e<edges.length){
Edge nextEdge = edges[e++];
int src = nextEdge.src;
int dest = nextEdge.dest;
int w = nextEdge.w;
int x = uf.find(src);
int y = uf.find(dest);
if(x!=y){
uf.union(x, y);
res+=w;
}
}
for(int v = 1;v<this.v;v++){
if(uf.find(v)!=uf.find(0)){
System.out.println("orz");
return;
}
}
System.out.println(res);
}
}
public class Main {
static int n,m;
public static void main(String[] args) throws IOException{
BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
String []line = in.readLine().split(" ");
n = Integer.parseInt(line[0]);
m = Integer.parseInt(line[1]);
Edge []edges = new Edge[m];
for (int i = 0; i < m; i++) {
line = in.readLine().split(" ");
int x = Integer.parseInt(line[0])-1;
int y = Integer.parseInt(line[1])-1;
int z = Integer.parseInt(line[2]);
edges[i] = new Edge(x, y, z);
}
KruskalMST k = new KruskalMST(n,edges);
k.kruskalMST();
in.close();
}
}
Prim 算法(找最小的顶点)
邻接矩阵写法
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Arrays;
class Graph {
private int v;
private int[][] g;
private boolean[] flag;
private int[] key;
Graph(int v) {
this.v = v;
g = new int[v][v];
}
void add(int v1, int v2, int w) {
if (v1 == v2)
return; // 自循环检查
if (g[v1][v2] == 0 || w < g[v1][v2]) {//重复边处理
g[v1][v2] = g[v2][v1] = w; // 更新为最小权重
}
}
int getMinV() {
int min = -1;
int minVal = Integer.MAX_VALUE;
for (int i = 0; i < this.v; i++) {
if (!flag[i] && key[i] < minVal) {
minVal = key[i];
min = i;
}
}
return min;
}
void Prim() {
flag = new boolean[this.v];
key = new int[this.v];
Arrays.fill(key, Integer.MAX_VALUE);
key[0] = 0;
for (int i = 0; i < this.v; i++) {
int u = getMinV();
if (u == -1) {
break;
}
flag[u] = true; // 更新标志位
for (int v = 0; v < this.v; v++) {
if (g[u][v] != 0 && !flag[v] && g[u][v] < key[v]) {
key[v] = g[u][v];
}
}
}
long res = 0;
for (int i = 1; i < this.v; i++) {
if (!flag[i]) {
System.out.println("orz");
return;
}
res += key[i];
}
System.out.println(res);
}
}
public class Main {
static int n, m;
public static void main(String[] args) throws IOException {
BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
String[] line = in.readLine().split(" ");
n = Integer.parseInt(line[0]);
m = Integer.parseInt(line[1]);
Graph g = new Graph(n);
for (int i = 0; i < m; i++) {
line = in.readLine().split(" ");
int v1 = Integer.parseInt(line[0]) - 1;
int v2 = Integer.parseInt(line[1]) - 1;
int w = Integer.parseInt(line[2]);
g.add(v1, v2, w);
}
g.Prim();
in.close();
}
}
邻接矩阵(堆优化写法)
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Arrays;
import java.util.PriorityQueue;
class Edge implements Comparable<Edge> {
int vertex;
int weight;
public Edge(int v, int w) {
this.vertex = v;
this.weight = w;
}
@Override
public int compareTo(Edge other) {
return Integer.compare(this.weight, other.weight);
}
}
class Graph {
private int v;
private int[][] g;
private boolean[] flag;
private int[] key;
private PriorityQueue<Edge> pq;
Graph(int v) {
this.v = v;
g = new int[v][v];
pq = new PriorityQueue<>();
}
void add(int v1, int v2, int w) {
if (v1 == v2)
return; // 自循环检查
if (g[v1][v2] == 0 || w < g[v1][v2]) {
g[v1][v2] = g[v2][v1] = w; // 更新为最小权重
}
}
void Prim() {
flag = new boolean[this.v];
key = new int[this.v];
Arrays.fill(key, Integer.MAX_VALUE);
key[0] = 0;
// 将起点加入优先队列
pq.offer(new Edge(0, 0));
while (!pq.isEmpty()) {
Edge current = pq.poll();
int u = current.vertex;
if (flag[u]) continue; // 如果已经处理过这个节点,则跳过
flag[u] = true; // 标记为已访问
for (int v = 0; v < this.v; v++) {
if (g[u][v] != 0 && !flag[v] && g[u][v] < key[v]) {
key[v] = g[u][v];
pq.offer(new Edge(v, key[v])); // 将新的候选边加入优先队列
}
}
}
long res = 0;
for (int i = 1; i < this.v; i++) {
if (!flag[i]) {
System.out.println("orz");
return;
}
res += key[i];
}
System.out.println(res);
}
}
public class Main {
static int n, m;
public static void main(String[] args) throws IOException {
BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
String[] line = in.readLine().split(" ");
n = Integer.parseInt(line[0]);
m = Integer.parseInt(line[1]);
Graph g = new Graph(n);
for (int i = 0; i < m; i++) {
line = in.readLine().split(" ");
int v1 = Integer.parseInt(line[0]) - 1;
int v2 = Integer.parseInt(line[1]) - 1;
int w = Integer.parseInt(line[2]);
g.add(v1, v2, w);
}
g.Prim();
in.close();
}
}
邻接表写法 (适合稀疏图)
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Arrays;
class Node {
int nei;
int w;
Node(int nei,int w){
this.nei = nei;
this.w = w;
}
}
class Graph {
private int v;
private ArrayList<ArrayList<Node>> list;
private boolean []flag;
private int[]key;
Graph(int v){
this.v = v;
list = new ArrayList<>();
for(int i = 0;i<=v;i++){
list.add(new ArrayList<>());
}
}
int getMinV(){
int min = -1;
int minVal = Integer.MAX_VALUE;
for(int i = 1;i<=this.v;i++){
if(!flag[i] && key[i]<minVal){
minVal = key[i];
min = i;
}
}
return min;
}
void add(int v1, int v2, int w) {
// 避免自环边
if (v1 == v2) {
return;
}
list.get(v1).add(new Node(v2, w));
list.get(v2).add(new Node(v1, w));
}
void Prim(){
flag = new boolean[this.v + 1];
key = new int[this.v+1];
Arrays.fill(key, Integer.MAX_VALUE);
key[1] = 0;
for(int i = 0;i<this.v;i++){
int u = getMinV();
if(u == -1){
break;
}
flag[u] = true;
for (Node n : list.get(u)) {
int v = n.nei;
if(!flag[v] && n.w<key[v]){
key[v] = n.w;
}
}
}
long res = 0;
for(int i = 2;i<=this.v;i++){
if(!flag[i]){
System.out.println("orz");
return;
}
res+=key[i];
}
System.out.println(res);
}
}
public class Main {
static int n,m;
public static void main(String[] args) throws IOException{
BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
String []line = in.readLine().split(" ");
n = Integer.parseInt(line[0]);
m = Integer.parseInt(line[1]);
Graph g = new Graph(n);
for(int i = 0;i<m;i++){
line = in.readLine().split(" ");
int v1 = Integer.parseInt(line[0]);
int v2 = Integer.parseInt(line[1]);
int w = Integer.parseInt(line[2]);
g.add(v1, v2, w);
}
g.Prim();
in.close();
}
}
邻接表堆优化写法
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.List;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.PriorityQueue;
// 定义图的节点,用于存储相邻节点信息和边的权重
class Node implements Comparable<Node> {
int nei; // 相邻节点
int w; // 边的权重
// 构造函数
Node(int nei, int w) {
this.nei = nei;
this.w = w;
}
// 实现Comparable接口的compareTo方法,按照边的权重升序排序
@Override
public int compareTo(Node other) {
return this.w - other.w;
}
}
// 表示图的类
class Graph {
private int v; // 图中节点的数量
List<List<Node>> adjList; // 邻接表,存储图的结构
PriorityQueue<Node> pq; // 优先队列,用于Prim算法中选择最小权重的边
// 构造函数,初始化图
Graph(int v) {
this.v = v;
adjList = new ArrayList<>();
for (int i = 0; i < v; i++) {
adjList.add(new ArrayList<>());
}
pq = new PriorityQueue<>();
}
// 添加边到图中
void add(int v1, int v2, int w) {
if (v1 == v2)// 自环边
return;
adjList.get(v1).add(new Node(v2, w));
adjList.get(v2).add(new Node(v1, w));
}
// Prim算法,用于寻找最小生成树的权值和
void prim() {
boolean[] flag = new boolean[this.v]; // 标记节点是否已访问
int[] key = new int[this.v]; // 存储节点到最小生成树的最小权值
Arrays.fill(key, Integer.MAX_VALUE); // 初始化key数组
pq.offer(new Node(0, 0)); // 将起始节点加入优先队列
key[0] = 0; // 起始节点到自身的权值为0
// Prim算法主循环
while (!pq.isEmpty()) {
Node n = pq.poll(); // 从优先队列中取出权值最小的节点
int u = n.nei;
if (flag[u])
continue;
flag[u] = true; // 标记节点u为已访问
for (Node node : adjList.get(u)) {
int v = node.nei;
int w = node.w;
if (!flag[v] && w < key[v]) {
key[v] = w; // 更新节点v到最小生成树的最小权值
pq.offer(node); // 将节点v加入优先队列
}
}
}
long res = 0; // 最小生成树的权值和
for (int i = 1; i < this.v; i++) {
if (!flag[i]) {
System.out.println("orz"); // 如果有节点未被访问,表示图不连通
return;
}
res += key[i]; // 累加权值
}
System.out.println(res); // 输出最小生成树的权值和
}
}
public class Main {
static int n, m;
public static void main(String[] args) throws IOException {
BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
String[] line = in.readLine().split(" ");
n = Integer.parseInt(line[0]); // 读取节点数量
m = Integer.parseInt(line[1]); // 读取边的数量
Graph g = new Graph(n); // 创建图对象
// 读取并添加图的边
for (int i = 0; i < m; i++) {
line = in.readLine().split(" ");
int v1 = Integer.parseInt(line[0]) - 1;
int v2 = Integer.parseInt(line[1]) - 1;
int w = Integer.parseInt(line[2]);
g.add(v1, v2, w);
}
g.prim(); // 执行Prim算法
in.close(); // 关闭输入流
}
}
优点:
- 更适合稠密图:对于边数较多的图,Prim 算法通常表现得更好,因为它的性能主要依赖于节点数而不是边数。
- 不需要额外的数据结构来检测环路:Prim 算法通过维护一个包含已加入 MST 的节点集合来避免形成环路,因此不需要像 Kruskal 算法那样使用额外的数据结构。
- 构建过程连续:从任一顶点开始,逐步添加邻接顶点到当前的 MST 中,构建过程比较直观。
缺点: - 实现相对复杂:相比于 Kruskal 算法,Prim 算法的实现稍微复杂一些,特别是在处理图的动态更新时。
- 难以并行化:由于 Prim 算法在每一步的选择都依赖于之前的选择结果,因此不太适合并行计算环境。
总结
- 如果图是稀疏的,或者你希望算法能够更容易地并行化,那么 Kruskal 算法可能是一个更好的选择。
- 对于稠密图或需要快速构建最小生成树的情况,Prim 算法通常更有效率。