最小生成树 java实现
最小生成树
最小生成树的定义:最小生成树是在一个给定的无向图G(V,E)中求一颗树T,使得这棵树拥有图G中所有顶点,且所有边都是来自图G中的边,并且满足整棵树的边权之和最小。
最小生成树三个性质:
- 最小生成树是树,因此其边数等于顶点数减一,且树内一定不会有环
- 对给定的图,其最小生成树可以不唯一,但边权之和一定唯一
- 最小生成树是在无向图上面生成的,因此其根节点可以是这颗树上面的任意一个节点。如果题目中涉及最小生成树的输出,为了让生成树唯一,一般会直接给出根节点,只需要给出节点作为根节点来求解最小生成树即可
问题种类:铺设公路
两种解决算法
-
普利姆算法(prim)
- 朴素版Prim算法(稠密图),时间复杂度O(n^2)
堆优化版Prim算法(稀疏图),时间 复杂度O(mlogn)(很少使用)
-
克鲁斯卡尔算法(Kruskal)(稀疏图)
朴素版prim算法
假如有5个节点,6条边,
A B 3
A C 1
B C 2
A E 4
C D 5
C E 6
T 表示当前包含所有节点的集合,U 表示NULL。假如从A开始,(将A加入到U中)
第一步:选中A到各个节点中权重最小的节点
第二步:判断该节点是否被访问过,如果没有被访问过,则将该节点加入到集合U中
第三步:更新其他节点到集合U的距离
第四步:选择到集合U最近的点,重复第二步
时间复杂度是 \(O(n2+m)\), n 表示点数,m 表示边数
static int n; // n表示点数
static int INF = 0x3f3f3f3f;
static int[][] g = new int[N][N]; // 邻接矩阵,存储所有边
static int[] dist = new int[N]; // 存储其他点到当前最小生成树(集合)的距离
static boolean st = new int[N]; // 存储每个点是否已经在生成树中
// 如果图不连通,则返回INF(值是0x3f3f3f3f), 否则返回最小生成树的树边权重之和
int prim(){
Arrays.fill(dist, 0x3f);
//所有生成树里的边的长度之和的最小值
int res = 0;
for (int i = 0; i < n; i ++ ){
int t = -1;
for (int j = 1; j <= n; j ++ ) {
if (st[j] != false && (t == -1 || dist[t] > dist[j])) {
t = j;
}
}
if (i != 0 && dist[t] == INF) return INF;
if (i != 0) res += dist[t];
st[t] = true;
for (int j = 1; j <= n; j ++ ) {
dist[j] = min(dist[j], g[t][j]);
}
}
return res;
}
对应题目
/**
*prime算法实现
* 输入样例:
* 4 5
* 1 2 1
* 1 3 2
* 1 4 3
* 2 3 2
* 3 4 4
* 输出样例:
* 6
*/
public class PrimAlgorithm {
int INF = 500000;
private Scanner sc = new Scanner(System.in);
public void run() {
int n = sc.nextInt();
int m = sc.nextInt();
int[][] graph = new int[n + 1][n + 1];
for (int i = 0; i < n + 1; i++) {
Arrays.fill(graph[i], INF);
}
for (int i = 0; i < m; i++) {
int x = sc.nextInt();
int y = sc.nextInt();
int z = sc.nextInt();
//无向图
graph[x][y] = Math.min(graph[x][y], z);
graph[y][x] = graph[x][y];
graph[x][x] = 0;
graph[y][y] = 0;
}
int res = prim(graph);
if (res == INF) {
System.out.println("impossible");
} else {
System.out.println(res);
}
}
public int prim(int[][] graph) {
// graph长度是n+1
int n = graph.length - 1;
// dist[i] 表示的是i到生成树集合的最小距离
int[] dist = new int[n + 1];
// 给dist初始化为无穷
Arrays.fill(dist, INF);
int res = 0;
// 存储对应的边是否被访问过
boolean[] st = new boolean[n + 1];
// 因为之前存储graph的时候,是从下标1开始的,所以这里遍历时候就是从1开始
for (int i = 1; i < n + 1; i++) {
int t = -1;
for (int j = 1; j < n + 1; j++) {
if (st[j] == false && (t == -1 || dist[t] > dist[j])) t = j;
}
// 避免第一次循环的时候,直接返回
if (i != 1 && dist[t] == INF) return INF;
// 表示节点t已经被访问过
st[t] = true;
if (i != 1) {
// 将节点添加到生成树中
res += dist[t];
// System.out.println(res);
}
// 生成树是一个集合
// 因为生成树添加了新的节点,所以更新其他未被访问的点到生成树的最小距离,
for (int j = 1; j < n + 1; j++) {
dist[j] = Math.min(dist[j], graph[t][j]);
}
}
return res;
}
public static void main(String[] args) {
new PrimAlgorithm().run();
}
}
Kruskal算法
参考:https://blog.csdn.net/coslay/article/details/47756917
前置知识:排序和并查集
基本思想:按照权值从小到大的顺序选择n-1条边,并保证这n-1条边不构成回路。
具体做法:首先构造一个只含n个顶点的森林,然后依权值从小到大从连通网中选择边加入到森林中,并使森林中不产生回路,直至森林变成一棵树为止。
时间复杂度是 \(O(mlogm)\), n 表示点数,m 表示边数
思路:
-
将所有边按权重w从小到大排序;
-
枚举每条边的顶点a,b
if (a,b 不在同一个集合中(并查集)) 将这条边加入集合中
int n, m; // n是点数,m是边数
int[] p = new int[N]; // 并查集的父节点数组
class Edge{ // 存储边
int a, b, w;
Edge(int a, int b, int c) {
this.a = a;this.b = b; this.c = c;
}
};
int find(int x){ // 并查集核心操作
if (p[x] != x) p[x] = find(p[x]);
return p[x];
}
public int kruskal(){
Arrays.sort(edge, new Comparator<Edge>() {
@Override
public int compare(Edge o1, Edge o2) {
if(o1.w<o2.w) return -1;
else if(o1.w>o2.w) return 1;
else return 0;
}
});
for (int i = 1; i <= n; i ++ ) p[i] = i; // 初始化并查集
int res = 0, cnt = 0;
for (int i = 0; i < m; i ++ ) {
int a = edges[i].a, b = edges[i].b, w = edges[i].w;
a = find(a);
b = find(b);
if (a != b) { // 如果两个连通块不连通,则将这两个连通块合并
p[a] = b;
res += w;
cnt ++ ;
}
}
if (cnt < n - 1) return INF;
return res;
}
题目:
/**
* kruskal算法实现
* 输入样例:
* 4 5
* 1 2 1
* 1 3 2
* 1 4 3
* 2 3 2
* 3 4 4
* 输出样例:
* 6
*/
public class KruskalAlgorithm {
int INF = Integer.MAX_VALUE / 2;
private Scanner sc = new Scanner(System.in);
private int n;
private int m;
public static void main(String[] args) {
new KruskalAlgorithm().run();
}
// 并查集
private int find(int[] p, int x) {
if (p[x] != x) p[x] = find(p, p[x]);
return p[x];
}
private void run() {
n = sc.nextInt();
m = sc.nextInt();
// 存储边
List<Edge> edgeList = new ArrayList<>();
// 下标从1开始,因为节点的值是从1开始的
int[] p = new int[n + 1];
for (int i = 1; i <= n; i++) {
p[i] = i;
}
for (int i = 0; i < m; i++) {
int x = sc.nextInt();
int y = sc.nextInt();
int z = sc.nextInt();
edgeList.add(new Edge(x, y, z));
}
int res = kruskal(edgeList, p);
if (res == INF) {
System.out.println("impossible");
} else {
System.out.println(res);
}
}
private int kruskal(List<Edge> edgeList, int[] p) {
int res = 0;
int cnt = 0;
// 按照w 权重的大小来排序
Collections.sort(edgeList);
for (int i = 0; i < edgeList.size(); i++) {
int a = edgeList.get(i).getA();
int b = edgeList.get(i).getB();
int w = edgeList.get(i).getW();
// 分别找到a、b对应的集合的根
a = find(p, a);
b = find(p, b);
// 如果根相同表示a、b在同一个集合中,如果添加进去,会形成回路,则该边不能添加到集合中
// 如果根不相同表示a、b不在同一个集合中,则该边能添加到集合中
if (a != b) {
p[a] = b;
res += w;
// 计算添加进去的边
cnt++;
}
}
// n 是点的数量,n个点最少是n-1条边,如果少于n-1条边,则表示 n个点并没有全部使用到,所以不成立
if (cnt < n - 1) return INF;
return res;
}
}
class Edge implements Comparable<Edge> {
int a, b, w;
public Edge(int a, int b, int w) {
// a,b 为节点
this.a = a;
this.b = b;
// w 为节点之间的权重
this.w = w;
}
public int getA() {
return a;
}
public int getB() {
return b;
}
public int getW() {
return w;
}
// 设置edge的排序规则,按照w的大小排序
@Override
public int compareTo(Edge edge) {
return Integer.compare(w, edge.w);
}
}
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步