【java】最小生成树

来源:互联网 发布:php 数组的数组 编辑:程序博客网 时间:2024/06/03 19:23

航海家们在太平洋上发现了几座新岛屿,其中最大的一个岛已经连接到Internet,但是其它岛和主岛之间没有电缆连接,所以无法入网。我们的目的是让所有岛上的居民都能上网,即每个岛和主岛之间都有直接或间接的电缆连接。输入每两个岛屿之间的连接成本,要求给出最节省成本的方案。


先输入点的数量n,边的数量m,然后输入m行数据,每行数据包括起点u,终点v,成本weight,最后输出最优方案所选的边;

样例输入:

7 11
0 1 7
0 3 5
1 2 8
1 3 9
1 4 7
2 4 5
3 4 15
3 5 6
4 5 8
4 6 9
5 6 11

样例输出:

0 3 5
2 4 5
3 5 6
0 1 7
1 4 7
4 6 9

分析:本题的本质是无向图求最小生成树MST(Minimal Spanning Tree)。构造最小生成树的方法有很多,常见的有两个:Kruskal算法和Prim算法。

Kruskal算法

算法的第一步是把所有的边按权值从小到大排序,接下来依次遍历每条边(u,v),分两种情况:

情况1:最小生成树中已经包含u和v,则跳过;

情况2:最小生成树不包含u或v,则加入该边。

性质:如果(u,v)的权值是所有边中最小的,则最小生成树一定包含该边。可用反正法证明,若不加入该边,那么对于没有该边的最优解T,加入该边将有一个环,该环中至少有一条边的权值比(u,v)大,因此加入(u,v)一定是最优的。

算法伪代码:

把所有边排序,记第i小的边为e[i]初始化MST为空for(int i=0;i<m;i++){初始化连通分量,让每个点自成一个独立的连通分量   if(e[i].u或e[i].v不在同一个连通分量中){    把e[i]加入MST    合并e[i].u和e[i].v所在的连通分量   }}

在算法实现中,边的排序可直接使用库函数,判断两个点是否在同一个连通分量中,可使用并查集方法。

并查集:把每个连通分量看成一个集合,该集合包含了连通分量中的所有点,每个连通分量用树来表示该集合,每棵树的根节点是这棵树的代表元。

如果两个节点所在树的根节点相同,则代表这两个点在同一个连通分量中。使用路径压缩可加快遍历过程。

import java.util.Arrays;  import java.util.Scanner;    class Edge implements Comparable<Edge>{      int u;      int v;      int weight;      Edge(int u,int v,int weight){          this.u=u;          this.v=v;          this.weight=weight;      }      public int compareTo(Edge e) {          if(weight>e.weight)              return 1;          else if(weight<e.weight)              return -1;          return 0;      }  }  public class Main {public static int find(int[] p,int x){if(p[x]==x)return x;else return p[x]=find(p,p[x]); //查询父节点}    public static void main(String[] args){          Scanner scanner = new Scanner(System.in);          while(scanner.hasNext()){              int n=scanner.nextInt();              int m=scanner.nextInt();              Edge[] edges=new Edge[m];              for(int i=0;i<m;i++){                  int u=scanner.nextInt();                  int v=scanner.nextInt();                  int weight=scanner.nextInt();                  edges[i]=new Edge(u,v,weight);              }              Arrays.sort(edges);            int[] p=new int[n];            for(int i=0;i<n;i++)//初始化连通分量,每个点自成一个连通分量            p[i]=i;            for(int i=0;i<m;i++){                  int x=find(p,edges[i].u);                  int y=find(p,edges[i].v);                  if(x==y){                      continue;                  }else{                      p[x]=y;                     System.out.println(edges[i].u+" "+edges[i].v+" "+edges[i].weight);                  }              }          }          scanner.close();      }  }  

Prim算法

Prim算法

把点分成两个集合,A为已选中(已经在最小生成树中)的顶点,B未选中的顶点,循环n次,每次都是选择是跨越这两个集合最小的边。

先输入点的数量n,边的数量m,然后输入m行数据,每行数据包括起点u,终点v,成本weight,最后输出最小生成树的总权值;

样例输入:

7 11
0 1 7
0 3 5
1 2 8
1 3 9
1 4 7
2 4 5
3 4 15
3 5 6
4 5 8
4 6 9
5 6 11

样例输出:

39

import java.util.Arrays;import java.util.Scanner;public class Main {public static void main(String[] args){Scanner scanner = new Scanner(System.in);while(scanner.hasNext()){int n=scanner.nextInt();int m=scanner.nextInt();int[][] edges=new int[n][n];for(int i=0;i<m;i++){int u=scanner.nextInt();int v=scanner.nextInt();int weight=scanner.nextInt();edges[u][v]=weight;edges[v][u]=weight;}int[] dist=new int[n];//dist[i]存放结点i到已访问结点的最小权值Arrays.fill(dist, Integer.MAX_VALUE);dist[0]=0;int[] visited=new int[n];int result=0;for(int i=0;i<n;i++){//循环n次,每次选出一个点加入到最小生成树int x=-1,min=Integer.MAX_VALUE;for(int j=0;j<n;j++){//选出未访问的结点中dist最小的结点if(visited[j]==0&&dist[j]<min){x=j;min=dist[j];}}result+=min;visited[x]=1;for(int k=0;k<n;k++){//更新dist值if(visited[k]==0&&edges[x][k]>0){if(dist[k]>edges[x][k])dist[k]=edges[x][k];}}}System.out.println(result);}scanner.close();}}

Prim算法和Dijkstra算法的异同

相同点:

二者都是采用贪心策略,代码结构类似;

不同点:

①Prim算法用于无权图生成最小生成树,Dijkstra算法用于无权图/带权图的单源最短路径。

②Prim算法中,dist数组保存的是结点i到已访问结点的最小权值;相应地,更新过程是dist[k]=edges[x][k];

   Dijkstra算法中,dist数组保存的是源结点到结点i的最短路径长度;相应地,更新过程是dist[k] = dist[x]+edges[x][k];

③Prim算法可以任选一个顶点作为起点,Dijkstra算法要有一个确定的起点。



0 0