今日收获:最小生成树,prim算法,kruskal算法
1. 最小生成树(以下讲解部分来自代码随想录)
定义:所有节点的最小连通子图。每条边都有权值,用最小的成本(边的权值)将所有点连通起来。如果有n个点,则需要n-1条边
2. prim算法
算法讲解:
(1)prim三部曲:
- 第一步,选距离生成树最近节点
- 第二步,最近节点加入生成树
- 第三步,更新非生成树节点到生成树的距离(即更新minDist数组)
(2)minDist数组用来记录 非生成树节点距离最小生成树的最近距离
(3)过程:
- 刚开始没有节点被选在树中,所以初始化minDist数组为最大值;
- 首先选择任意一个节点加入最小生成树,因为最终所有节点都在最小生成树中,所以选哪个无所谓。为了遍历顺序选第一个节点;
- 此时第一个节点已经在树里面,更新和它相连点的minDist数组值,就是第一个节点和它们相连边的权值;
- 选择minDist数组中距离生成树最近的节点加入生成树,则生成树里现在有两个节点,再更新其他节点距离生成树的minDist数组值
- 重复选择距离生成树距离最小的,选择其加入生成树,再更新剩余节点到选中节点的权值
(4)拓展:如果需要记录最小生成树的边,可以使用parent数组;在更新非树节点到选中节点的距离时,记录非树节点的父节点为当前选中节点
题目链接:53. 寻宝(第七期模拟笔试) (kamacoder.com)
方法:
import java.util.*;
public class Main{
public static void main(String[] args){
Scanner sc=new Scanner(System.in);
int V=sc.nextInt();
int E=sc.nextInt();
// 初始化minDist数组
int[] minDist=new int[V+1];
Arrays.fill(minDist,10001);
// 利用邻接矩阵存储边和权值,初始化权值为最大值+1
int[][] grid=new int[V+1][V+1];
for(int i=1;i<V+1;i++){
Arrays.fill(grid[i],10001);
}
for (int i=0;i<E;i++){
int v1=sc.nextInt();
int v2=sc.nextInt();
int val=sc.nextInt();
grid[v1][v2]=val;
grid[v2][v1]=val;
}
// 开始选择加入生成树的点,遍历V-1次
boolean[] inTree=new boolean[V+1]; // 是否加入生成树
for (int i=1;i<V;i++){
int cur=-1;
int min=Integer.MAX_VALUE;
// 遍历minDist数组,选择距离生成树最小的非生成树节点
for (int j=1;j<V+1;j++){
if (!inTree[j]&&minDist[j]<min){
min=minDist[j];
cur=j;
}
}
// 将选择的节点加入生成树
inTree[cur]=true;
// 更新非生成树节点距离生成树的最小距离
for (int j=1;j<V+1;j++){
if (grid[cur][j]<minDist[j]&&!inTree[j]){
minDist[j]=grid[cur][j];
}
}
}
// 计算结果
int result=0;
for (int i=1;i<V+1;i++){
if (minDist[i]!=10001){
result+=minDist[i];
}
}
System.out.println(result);
}
}
3. kruskal算法
算法讲解:
(1)整体思路:将边的权值进行排序,每次选择最小的边加入生成树,但是要保证不能出现环
(2)不能出现环:就是看当前选中边的两个节点是否在同一个集合中,如果在同一个集合中就不能选择该边;否则可以选择该边,并且将两个节点添加到同一个集合中
(3)需要用到并查集来判断两个节点是否在同一集合中&&添加节点到同一个集合中
拓展:如果需要打印边,则添加结果边的时机是在当前选择最小代价边的两个节点不在同一集合时
题目链接:53. 寻宝(第七期模拟笔试) (kamacoder.com)
方法:
import java.util.*;
public class Main{
public static void main(String[] args){
Scanner sc=new Scanner(System.in);
int V=sc.nextInt();
int E=sc.nextInt();
// 存储边再排序
List<int[]> edges=new ArrayList<>();
for (int i=0;i<E;i++){
int v1=sc.nextInt();
int v2=sc.nextInt();
int val=sc.nextInt();
edges.add(new int[]{v1,v2,val});
}
edges.sort(Comparator.comparingInt(edge->edge[2]));
// 从小到大遍历边,保证不会形成环
int result=0;
DisJoint disJoint=new DisJoint(V);
for (int [] edge:edges){
int v1=edge[0];
int v2=edge[1];
if (disJoint.isSame(v1,v2)){ // 在同一集合中
continue;
}
result+=edge[2];
disJoint.join(v1,v2);
}
System.out.println(result);
}
}
class DisJoint{
private int[] father;
public DisJoint(int V){
father=new int[V+1];
for (int i=1;i<V+1;i++){
father[i]=i;
}
}
public int find(int node){
return father[node]==node?node:(father[node]=find(father[node]));
}
public boolean isSame(int s,int t){
int root1=find(s);
int root2=find(t);
if (root1==root2){
return true;
}
return false;
}
public void join(int s,int t){
if (isSame(s,t)){
return;
}
int root1=find(s);
int root2=find(t);
father[root2]=root1;
}
}
4. 两种算法的对比
(1)prim是选择距离生成树最近的节点,kruskal在保证不出现环的情况下选择最小权值的边。prim作用于点,kruskal算法作用于边
(2)使用场景:
- prim算法适合稠密图
- kruskal算法适合稀疏图,此时边比较少,便于选择权值小的边