赞
踩
正确的思路,就是尽可能的选择少的路线,并且每条路线最小,保证总里程数最少
修路问题本质就是就是最小生成树问题, 先介绍一下最小生成树(Minimum Cost Spanning Tree),简称 MST。给定一个带权的无向连通图,如何选取一棵生成树,使树上所有边上权的总和为最小,这叫最小生成树
特点如下:N 个顶点,一定有 N-1 条边;包含全部顶点;N-1 条边都在图中
普利姆(Prim)算法求最小生成树,也就是在包含 n 个顶点的连通图中,找出只有(n-1)条边包含所有 n 个顶点的连通子图,也就是所谓的极小连通子图
图解说明:
package com.atguigu.prim; import java.util.Arrays; /** * @ClassName PrimDemo * @Author Jeri * @Date 2022-03-04 16:30 * @Description TODO */ //创建图类 class Graph{ int verCount;//图的顶点个数 char[] data;//存放顶点数据 int[][] weight;//存放邻接矩阵 boolean[] isVisited;//图的顶点是否被访问 public Graph(int vertexCount,char[] data,int[][] weight){ this.verCount = vertexCount; this.data = new char[verCount]; this.weight = new int[verCount][verCount]; this.isVisited = new boolean[verCount]; //使用 .clone() 方法进行深拷贝 this.data = data.clone(); this.weight = weight.clone(); } //显示图的邻接矩阵 public void show(){ for(int i = 0;i < verCount;i++){ for(int j = 0;j < verCount;j++){ System.out.printf("%-8d",weight[i][j]); } System.out.println(); } } } public class PrimDemo { public static void prim(Graph graph,int startVer){ //将开始顶点设置为已访问 graph.isVisited[startVer] = true; System.out.println("初始顶点为" + graph.data[startVer]); //使用 h1 和 h2 记录两个顶点的下标 int h1 = -1;//h1从已访问顶点集合中取 int h2 = -1;//h2从未访问顶点集合中取 //使用 minWeight 记录每一轮寻找到的最小路径 int minWeight = 10000; for(int k = 1; k < graph.verCount;k++){ //K表示未访问的节点数目 每进行一轮循环找到一个节点,并设置为已访问 for(int i = 0;i < graph.verCount;i++){ //i表示从已访问的节点出发 寻找未访问节点路径 if(graph.isVisited[i] == false) { continue;} for(int j = 0;j < graph.verCount;j++){ //j表示未访问的节点 if(graph.isVisited[j] == true) { continue;} if(minWeight > graph.weight[i][j]){ minWeight = graph.weight[i][j]; h1 = i; h2 = j; } } } //双 for 循环结束后 找到权值最小的边 System.out.println("当前添加 边:<" + graph.data[h1] + "," + graph.data[h2] + ">,权重为" + minWeight); //将找到的节点设置为已访问 graph.isVisited[h2] = true; //状态清零 minWeight = 10000; h1 = -1; h2 = -1; } } public static void main(String[] args) { //顶点集合 char[] data = new char[]{'A','B','C','D','E','F','G'}; //顶点个数 int vertexCount = data.length; //邻接矩阵 其中 10000 表示两个顶点之间不能连通 int[][] weight = new int[][]{ {10000,5,7,10000,10000,10000,2}, {5,10000,10000,9,10000,10000,3}, {7,10000,10000,10000,8,10000,10000}, {10000,9,10000,10000,10000,4,10000}, {10000,10000,8,10000,10000,5,4}, {10000,10000,10000,4,5,10000,6}, {2,3,10000,10000,4,6,10000}, }; //创建图对象 Graph graph = new Graph(vertexCount,data,weight); System.out.println("图的邻接矩阵如下:----"); graph.show(); System.out.println(); System.out.println("prim算法求得最小生成树结果如下:-----"); prim(graph,6); } }
图的邻接矩阵如下:---- 10000 5 7 10000 10000 10000 2 5 10000 10000 9 10000 10000 3 7 10000 10000 10000 8 10000 10000 10000 9 10000 10000 10000 4 10000 10000 10000 8 10000 10000 5 4 10000 10000 10000 4 5 10000 6 2 3 10000 10000 4 6 10000 prim算法求得最小生成树结果如下:----- 初始顶点为G 当前添加 边:<G,A>,权重为2 当前添加 边:<G,B>,权重为3 当前添加 边:<G,E>,权重为4 当前添加 边:<E,F>,权重为5 当前添加 边:<F,D>,权重为4 当前添加 边:<A,C>,权重为7
并查集是一种树型结构,并查集可以高效的进行如下操作:
并查集也是一种树形结构,要求比较简单:
并查集的实现:
package com.atguigu.kruskal; import java.util.Scanner; //并查集代码 class UF { //记录节点元素和该元素所在分组的表示 private int[] elementGroup; //记录并查集中数据的分组个数 private int count; //初始化并查集 public UF(int N) { //初始情况下 每个元素默认为一个独立的分组 this.count = N; //初始化数组 elementGroup = new int[N]; //elementGroup 数组 索引为其元素 索引值为其分组 for (int i = 0; i < N; i++) { elementGroup[i] = i; } } //获取当前并查集数据的分组总数 public int getCount() { return count; } //获取元素 p 所在分组标识符 public int find(int p) { return elementGroup[p]; } //判断并查集中元素p和元素q 是否在同一分组 public boolean connected(int p, int q) { return find(p) == find(q); } //把元素p所在分组和元素q所在分组进行合并 public void union(int p, int q) { if (connected(p, q)) { return; } int pGroup = find(p); int qGroup = find(q); for (int i = 0; i < elementGroup.length; i++) { if (elementGroup[i] == pGroup) { elementGroup[i] = qGroup; } } //分组数量 -1 count--; } } public class UTDemo { public static void main(String[] args) { Scanner sc = new Scanner(System.in); System.out.println("请录入并查集中元素的个数:"); int N = sc.nextInt(); UF uf = new UF(N); while (true) { System.out.println("请录入您要合并的第一个点:"); int p = sc.nextInt(); System.out.println("请录入您要合并的第二个点:"); int q = sc.nextInt(); //判断p和q是否在同一个组 if (uf.connected(p, q)) { System.out.println("结点:" + p + "结点" + q + "已经在同一个组"); continue; } uf.union(p, q); System.out.println("总共还有" + uf.getCount() + "个分组"); } } }
请录入并查集中元素的个数: 5 请录入您要合并的第一个点: 0 请录入您要合并的第二个点: 2 总共还有4个分组 请录入您要合并的第一个点: 1 请录入您要合并的第二个点: 2 总共还有3个分组 请录入您要合并的第一个点: 1 请录入您要合并的第二个点: 0 结点:1结点0已经在同一个组
并查集代码的优化:
存在问题:上述代码在并查集合并时,需要遍历所有的元素,在实际中不可行
解决:eleAndGroup[i]的值不再是当前结点所在的分组标识,而是该结点的父结点
find(int p)查询方法实现:
union(int p,int q)合并方法实现:
package com.atguigu.kruskal; import java.util.Scanner; class UTTree{ //记录节点元素和该元素所在分组的表示 private int[] elementGroup; //记录并查集中数据的分组个数 private int count; //初始化并查集 public UTTree(int N) { //初始情况下 每个元素默认为一个独立的分组 this.count = N; //初始化数组 elementGroup = new int[N]; //elementGroup 数组 索引为其元素 索引值为其分组 for (int i = 0; i < N; i++) { elementGroup[i] = i; } } //获取当前并查集数据的分组总数 public int getCount() { return count; } //获取元素 p 所在分组标识符 public int find(int p) { while (true){ if(p == elementGroup[p]){ //当前节点的父节点就是自己 返回 return p; } //当前节点的父节点不是自己,让p = elementGroup[p] 继续查找父节点的父节点 //直到找到根节点为止 p = elementGroup[p]; } } //判断并查集中元素p和元素q 是否在同一分组 public boolean connected(int p, int q) { return find(p) == find(q); } //把元素p所在分组和元素q所在分组进行合并 public void union(int p, int q) { if(connected(p,q)){ return; } int pRoot = find(p); int qRoot = find(q); elementGroup[pRoot] = qRoot; count--; } } public class UTTreeDemo { public static void main(String[] args) { Scanner sc = new Scanner(System.in); System.out.println("请录入并查集中元素的个数:"); int N = sc.nextInt(); UTTree utTree = new UTTree(N); while (true) { System.out.println("请录入您要合并的第一个点:"); int p = sc.nextInt(); System.out.println("请录入您要合并的第二个点:"); int q = sc.nextInt(); //判断p和q是否在同一个组 if (utTree.connected(p, q)) { System.out.println("结点:" + p + "结点" + q + "已经在同一个组"); continue; } utTree.union(p, q); System.out.println("总共还有" + utTree.getCount() + "个分组"); } } }
请录入并查集中元素的个数: 5 请录入您要合并的第一个点: 0 请录入您要合并的第二个点: 2 总共还有4个分组 请录入您要合并的第一个点: 1 请录入您要合并的第二个点: 2 总共还有3个分组 请录入您要合并的第一个点: 1 请录入您要合并的第二个点: 0 结点:1结点0:已经在同一个组
package com.atguigu.kruskal; import java.util.ArrayList; import java.util.Collections; /** * @ClassName KruskalDemo * @Author Jeri * @Date 2022-03-04 17:20 * @Description 克鲁斯卡尔算法求最小生成树 */ //创建图类 class Graph{ int verCount;//图的顶点个数 char[] data;//存放顶点数据 int[][] weight;//存放邻接矩阵 boolean[] isVisited;//图的顶点是否被访问 public Graph(int vertexCount,char[] data,int[][] weight){ this.verCount = vertexCount; this.data = new char[verCount]; this.weight = new int[verCount][verCount]; this.isVisited = new boolean[verCount]; //使用 .clone() 方法进行深拷贝 this.data = data.clone(); this.weight = weight.clone(); } //显示图的邻接矩阵 public void show(){ for(int i = 0;i < verCount;i++){ for(int j = 0;j < verCount;j++){ if(weight[i][j] == Integer.MAX_VALUE){ System.out.printf("%-5s","INF"); }else{ System.out.printf("%-5d",weight[i][j]); } } System.out.println(); } } } //创建边类 class EdgeData implements Comparable<EdgeData>{ int start;//边的起点 int end;//边的终点 int weight;//边的权值 public EdgeData(int start, int end, int weight) { this.start = start; this.end = end; this.weight = weight; } @Override public String toString() { return "EdgeData{ <" + start + "," + end + "> " + "weight = " + weight + "}"; } //从小到大进行排序 @Override public int compareTo(EdgeData obj) { return this.weight - obj.weight; } } public class KruskalDemo { public static void kruskal(Graph graph){ //获取边的集合 ArrayList<EdgeData> edgeLists = new ArrayList<>(); for(int i = 0;i < graph.verCount;i++){ for(int j = i + 1;j < graph.verCount;j++){ if(graph.weight[i][j] != Integer.MAX_VALUE){ edgeLists.add(new EdgeData(i,j,graph.weight[i][j])); } } } System.out.println("边集 排序之前:" + edgeLists); //对边进行排序 Collections.sort(edgeLists); System.out.println("边集 排序之后:" + edgeLists); //创建并查集对象 UF uf = new UF(graph.verCount); for(int i = 0;i < edgeLists.size();i++){ if(uf.getCount() == 1){ //并查集中只有一个分组 说明最小生成树已经构建完成 break; } //得到边的起点和终点 EdgeData curretnEgde = edgeLists.get(i); int start = curretnEgde.start; int end = curretnEgde.end; if(uf.connected(start,end)){ // start end 在同一分组 不能添加 continue; }else{ uf.union(start,end); System.out.println("当前添加边 <" + graph.data[start] + "," + graph.data[end] + "> 权重为 " + graph.weight[start][end]); } } } //使用 INF 表示两个顶点不能连通 private static final int INF = Integer.MAX_VALUE; public static void main(String[] args) { char[] vertexs = {'A', 'B', 'C', 'D', 'E', 'F', 'G'}; int matrix[][] = { {0, 12, INF, INF, INF, 16, 14}, {12, 0, 10, INF, INF, 7, INF}, {INF, 10, 0, 3, 5, 6, INF}, {INF, INF, 3, 0, 4, INF, INF}, {INF, INF, 5, 4, 0, 2, 8}, {16, 7, 6, INF, 2, 0, 9}, {14, INF, INF, INF, 8, 9, 0} }; Graph graph = new Graph(vertexs.length,vertexs,matrix); System.out.println("图的邻接矩阵如下:-----------"); graph.show(); System.out.println("克鲁斯卡尔 排序结果:-------"); kruskal(graph); } }
图的邻接矩阵如下:----------- 0 12 INF INF INF 16 14 12 0 10 INF INF 7 INF INF 10 0 3 5 6 INF INF INF 3 0 4 INF INF INF INF 5 4 0 2 8 16 7 6 INF 2 0 9 14 INF INF INF 8 9 0 克鲁斯卡尔 排序结果:------- 边集 排序之前:[EdgeData{ <0,1> weight = 12}, EdgeData{ <0,5> weight = 16}, EdgeData{ <0,6> weight = 14}, EdgeData{ <1,2> weight = 10}, EdgeData{ <1,5> weight = 7}, EdgeData{ <2,3> weight = 3}, EdgeData{ <2,4> weight = 5}, EdgeData{ <2,5> weight = 6}, EdgeData{ <3,4> weight = 4}, EdgeData{ <4,5> weight = 2}, EdgeData{ <4,6> weight = 8}, EdgeData{ <5,6> weight = 9}] 边集 排序之后:[EdgeData{ <4,5> weight = 2}, EdgeData{ <2,3> weight = 3}, EdgeData{ <3,4> weight = 4}, EdgeData{ <2,4> weight = 5}, EdgeData{ <2,5> weight = 6}, EdgeData{ <1,5> weight = 7}, EdgeData{ <4,6> weight = 8}, EdgeData{ <5,6> weight = 9}, EdgeData{ <1,2> weight = 10}, EdgeData{ <0,1> weight = 12}, EdgeData{ <0,6> weight = 14}, EdgeData{ <0,5> weight = 16}] 当前添加边 <E,F> 权重为 2 当前添加边 <C,D> 权重为 3 当前添加边 <D,E> 权重为 4 当前添加边 <B,F> 权重为 7 当前添加边 <E,G> 权重为 8 当前添加边 <A,B> 权重为 12
迪杰斯特拉(Dijkstra)算法是典型最短路径算法,用于计算一个结点到其他结点的最短路径。它的主要特点是以起始点为中心向外层层扩展(广度优先搜索思想),直到扩展到终点为止
package com.atguigu.dijkstra; import java.util.Arrays; import java.util.Scanner; /** * @ClassName DijkstraDemo * @Author Jeri * @Date 2022-03-04 21:03 * @Description 迪杰斯特拉 算法 */ class Graph{ int verCount;//图的顶点个数 char[] data;//存放顶点数据 int[][] weight;//存放邻接矩阵 VisitedVertex vv; public Graph(int vertexCount,char[] data,int[][] weight,VisitedVertex vv){ //使用 .clone() 方法进行深拷贝 this.verCount = vertexCount; this.data = new char[verCount]; this.data = data.clone(); this.weight = new int[verCount][verCount]; this.weight = weight.clone(); this.vv = vv; } //显示图的邻接矩阵 public void show(){ for(int i = 0;i < verCount;i++){ for(int j = 0;j < verCount;j++){ if(weight[i][j] == 65535){ System.out.printf("%-5s","INF"); }else{ System.out.printf("%-5d",weight[i][j]); } } System.out.println(); } } // 通过 tempVertex 更新 初始节点到其他节点之间的距离 public void updateVertex(int tempVertex){ //tempVertex 表示中转节点 int directDis = 0; int indirectDis = 0; //遍历邻接节点所在的行 得到非直连距离和直连距离 // min((Lsk + Lkj) , Lsj) k = tempVertex // Lsk = vv.getDis(k) Lsj = vv.getDis(j) Lkj = weight[k][j] for(int j = 0;j < weight[tempVertex].length;j++){ // j表示当前顶点 indirectDis = vv.getDis(tempVertex) + weight[tempVertex][j]; directDis = vv.getDis(j); if( !vv.isVisited[j] && indirectDis < directDis){ //非直连 小于 直连 需要更新 //同时需要注意 当前的直连路径直接从 distance数组中返回 vv.updateDis(j,indirectDis); vv.updatePre(j,tempVertex); } } System.out.println("经过当前访问节点的距离数组:"); System.out.println(Arrays.toString(vv.distance)); } public void dijkstra(int startVertex){ //更新初始节点 System.out.println("初始访问节点为" + data[startVertex]); updateVertex(startVertex); for(int i = 1;i < verCount;i++){ System.out.println(); int nextVertex = vv.updateVisted(); System.out.println("当前选择访问顶点为:" + data[nextVertex]); updateVertex(nextVertex); } } public void showPath(char end){ String path = ""; int startVertex = 6; int endVertex = 0; for(int i = 0;i < data.length;i++){ if(data[i] == end){ endVertex = i; break; } } int temp = endVertex; path = path + "--->" + data[endVertex]; while (vv.preVisited[temp] != startVertex){ temp = vv.preVisited[temp]; path = "--->" + data[temp] + path; } path = data[startVertex] + path; System.out.println("路径为" + path + ",距离为" + vv.getDis(endVertex)); } } class VisitedVertex{ //记录图中顶点是否被访问 public boolean[] isVisited ; //记录出发顶点到其他所有的顶点的距离 public int[] distance; //索引值作为前一个顶点的下标 寻找访问路径 public int[] preVisited; public VisitedVertex(int vertexCount,int startVertex){ isVisited = new boolean[vertexCount]; isVisited[startVertex] = true; distance = new int[vertexCount]; Arrays.fill(distance,65535);//初始化距离数组 distance[startVertex] = 0; preVisited = new int[vertexCount]; } //返回 初始顶点到当前顶点之间的距离 public int getDis(int tempVertex){ return distance[tempVertex]; } //更新 初始顶点到当前顶点之间的距离 public void updateDis(int CurrentVertex, int dis) { distance[CurrentVertex] = dis; } //更新 当前顶点之间为中继节点 public void updatePre(int CurrentVertex, int tempVertex) { preVisited[CurrentVertex] = tempVertex; } //选择新的访问顶点 public int updateVisted(){ int min = 65535; int nextVertex = 0; for(int i = 0;i < distance.length;i++){ if(!isVisited[i] && min > distance[i]){ min = distance[i]; nextVertex = i; } } //选择结束之后 //更新 访问顶点数组 isVisited[nextVertex] = true; return nextVertex; } } public class DijkstraDemo { public static final int N = 65535;// 表示不可以连接 public static void main(String[] args) { //顶点集合 char[] data = new char[]{ 'A', 'B', 'C', 'D', 'E', 'F', 'G'}; //邻接矩阵 int[][] matrix = new int[][]{ {N,5,7,N,N,N,2}, {5,N,N,9,N,N,3}, {7,N,N,N,8,N,N}, {N,9,N,N,N,4,N}, {N,N,8,N,N,5,4}, {N,N,N,4,5,N,6}, {2,3,N,N,4,6,N} }; int startVertex = 6; VisitedVertex vv = new VisitedVertex(data.length,startVertex); Graph graph = new Graph(data.length,data,matrix,vv); System.out.println("图的邻接矩阵展示如下:------"); graph.show(); System.out.println("迪杰斯特拉算法运算结果如下:-----"); graph.dijkstra(startVertex); System.out.println(); Scanner scan = new Scanner(System.in); System.out.println("初始顶点:" + graph.data[startVertex]); System.out.println("请输入到达顶点(A-G):"); char end = scan.next().charAt(0); graph.showPath(end); } }
图的邻接矩阵展示如下:------ INF 5 7 INF INF INF 2 5 INF INF 9 INF INF 3 7 INF INF INF 8 INF INF INF 9 INF INF INF 4 INF INF INF 8 INF INF 5 4 INF INF INF 4 5 INF 6 2 3 INF INF 4 6 INF 迪杰斯特拉算法运算结果如下:----- 初始访问节点为G 经过当前访问节点的距离数组: [2, 3, 65535, 65535, 4, 6, 0] 当前选择访问顶点为:A 经过当前访问节点的距离数组: [2, 3, 9, 65535, 4, 6, 0] 当前选择访问顶点为:B 经过当前访问节点的距离数组: [2, 3, 9, 12, 4, 6, 0] 当前选择访问顶点为:E 经过当前访问节点的距离数组: [2, 3, 9, 12, 4, 6, 0] 当前选择访问顶点为:F 经过当前访问节点的距离数组: [2, 3, 9, 10, 4, 6, 0] 当前选择访问顶点为:C 经过当前访问节点的距离数组: [2, 3, 9, 10, 4, 6, 0] 当前选择访问顶点为:D 经过当前访问节点的距离数组: [2, 3, 9, 10, 4, 6, 0] 初始顶点:G 请输入到达顶点(A-G): C 路径为G--->A--->C,距离为9
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。