修路问题:构成最短生成树,从指定的一个点,寻找n-1条路线构成最短的路径,且不能构成回路。使用克鲁姆算法
例如指定A开始,当前最短的是2,所以修AG
A和G顶点附近最短的路径是GB,3,所以修AGB
A和G和B顶点附近最短的路径是GE,5,所以修AGBE
到了目前,AGBE四个顶点附附近最短的路径是AB和EF,但是因为连接了AB会构成ABG回路(A和B已经被访问过),因此选择EF。
后面都是上面的这种规则,直到构成n-1条边。
思路:
①先创造一个是否访问的数组visited,然后将指定的点设为已访问
②第一个循环,从1开始,因为只需要找n-1条边。
③第二个循环,寻找可能已经访问的点
④第三个循环,寻找已经访问的点到没访问的点最短的值设定为max,且记录行列
ps:不构成回路的判断,因为一个是访问,一个是为未访问的,如果两个点都是已访问,再连接才会构成回路
⑤每一次第一层的寻找后,就可以得到当前已访问的点到其他点最短的距离
代码:
代码中使用了邻接矩阵来存储图,并且提供了两个类:MGraph 和 MinTree。
MGraph 类包括三个成员变量:节点个数 verxs、节点数据 data、邻接矩阵 weight。构造函数 MGraph(int verxs) 中初始化了这些变量。
MinTree 类包括三个方法:构造函数 MinTree(MGraph graph, int verxs, char data[], int[][] weight)、showGraph(MGraph graph) 和 prim(MGraph mGraph, int v)。
构造函数 MinTree(MGraph graph, int verxs, char data[], int[][] weight) 用于创建图的邻接矩阵,将节点数据和邻接矩阵存入 MGraph 对象中。
showGraph(MGraph graph) 方法用于展示邻接矩阵。
prim(MGraph mGraph, int v) 方法是 Prim 算法的核心实现。它从指定的节点开始,逐步构造最小生成树。首先将指定节点标记为已访问,然后在未被访问过的节点中查找到已访问节点距离最近的节点。找到之后将该节点标记为已访问,然后打印出这条边的信息。重复以上步骤,直到构造完成最小生成树。
需要注意的是,Prim 算法构造最小生成树的时候,只用找 n-1 条边即可,因为最小生成树中有 n 个节点,n-1 条边。
package ysh.algorithm;
import java.util.Arrays;
public class PrimAlgorithm {
public static void main(String[] args) {
char[] data = new char[]{'A','B','C','D','E','F','G'};
int verxs = 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},};
MGraph mGraph = new MGraph(verxs);
MinTree minTree = new MinTree(mGraph, verxs, data, weight);//构造最小生成树
minTree.showGraph(mGraph);//展示
// 普利姆算法,从B节点开始
minTree.prim(mGraph, 1);
}
}
class MGraph{
int verxs;//表示图的节点个数
char[] data;//节点数据
int[][] weight;//存放边,临接矩阵
public MGraph(int verxs) {
this.verxs = verxs;
data=new char[verxs];
weight=new int[verxs][verxs];
}
}
class MinTree{
//创建图的邻接矩阵
/**
*
* @param graph 图对象
* @param verxs 图对应的顶点个数
* @param data 图的各个顶点的值
* @param weight 图的邻接矩阵
*/
public MinTree(MGraph graph, int verxs, char data[], int[][] weight) {
int i, j;
for(i = 0; i < verxs; i++) {//顶点
graph.data[i] = data[i];
for(j = 0; j < verxs; j++) {
graph.weight[i][j] = weight[i][j];
}
}
}
//显示图的邻接矩阵
public void showGraph(MGraph graph) {
for(int[] link: graph.weight) {
System.out.println(Arrays.toString(link));
}
}
/**
*
* @param mGraph //地图
* @param v //从哪个节点开始
*/
public void prim(MGraph mGraph, int v) {
int[] visited = new int[mGraph.verxs];
visited[v]=1;
int row=-1;
int column=-1;
int max=10000;
for(int i=1;i<mGraph.verxs;i++){//只用找n-1条边即可
for(int j=0;j<mGraph.verxs;j++){//遍历可能已经访问的点
for(int k=0;k<mGraph.verxs;k++){//遍历得到可能已经访问的点,其到其他点的距离,取最小去访问
if(visited[j]==1&&visited[k]==0&&max>mGraph.weight[j][k]){//得到最小值的判断,且不构成回路,因为一个是访问,一个是为未访问的,如果两个点都是已访问,再连接才会构成回路
max=mGraph.weight[j][k];
row=j;
column=k;
}
}
}
visited[column]=1;
System.out.println(mGraph.data[row]+" => "+mGraph.data[column]+"权值为"+mGraph.weight[row][column]);
max=10000;
}
}
}
公交站问题:构成最短生成树,升序每段权值构成森林,每次提取森林里最短的路径,且不能构成回路。使用克鲁斯卡尔算法
首先找到最短的路径,也就是AG
然后找到BG
然后找到DF和EG,不过D比E前,所以选DF
然后可以找到EG
然后可以找到AB或者EF,但是如果选择AB则构成AGB回路(A和B的终点都是G),所以选择EF
整体思路就是利用克鲁斯卡尔算法,按照边的权重从小到大选择边,直到选择了 n-1 条边为止,构建最小生成树。
思路:
①定义了一个 Data
类来表示边,包括起始顶点、终止顶点和权重,并重写了 toString
方法用于方便打印输出。
②使用getEdges方法得到边的数组。
③使用sortEdges将②的数组进行权值的升序排序
④构造ends数组,里面存放x索引对应的字符,其终点的索引,例如A->C,则ends[0]=2。
getEnd方法传入ends数组和i,需要寻找i的终点,如果是0则表明这条线只有一个点,他的终点是他自己
⑤循环数组里的每个边,得到里面的起始节点和结束节点。判断他们的终点是否相同,相同则构成回路,否则就没有构成回路,然后为end[起点索引]=终点索引。
代码:
- 定义了一个
KruskalCase
类,其中包括顶点数组vertexs
、邻接矩阵matrix
、边的个数edgeNum
等属性,以及构造方法和一些操作方法。 - 在构造方法中,通过传入的顶点数组和邻接矩阵计算边的个数,并进行一些初始化操作。
print
方法用于打印邻接矩阵。kruskal
方法实现了克鲁斯卡尔算法的核心逻辑,包括对边进行排序、寻找终点是否相同、判断是否产生回路等操作。sortEdges
方法用于对边进行排序。getPosition
方法用于通过字符找到其在顶点数组中的索引。getEdges
方法用于从邻接矩阵中获取边的数组。getEnd
方法用于得到索引为 i 的终点索引,通过不同的 i 返回的索引是否相同来判断是否产生回路。
package ysh.algorithm;
import java.lang.reflect.Array;
import java.util.Arrays;
public class KruskalCase {
private int edgeNum; //边的个数
private char[] vertexs; //顶点数组
private int[][] matrix; //邻接矩阵
private static final int max = 65536;
public KruskalCase(char[] vertexs, int[][] matrix) {
int len=vertexs.length;
this.vertexs=vertexs.clone();
this.matrix=matrix.clone();
for(int i =0; i < len; i++) {
for(int j = i+1; j < len; j++) {
if(this.matrix[i][j] != max) {
edgeNum++;
}
}
}
}
public void print(){
for(int[] x:matrix){
System.out.println(Arrays.toString(x));
}
}
public static void main(String[] args) {
char[] vertexs = {'A', 'B', 'C', 'D', 'E', 'F', 'G'};
int matrix[][] = {
/*A*/ {0, 12, max, max, max, 16, 14},
/*B*/ {12, 0, 10, max, max, 7, max},
/*C*/ {max, 10, 0, 3, 5, 6, max},
/*D*/ {max, max, 3, 0, 4, max, max},
/*E*/ {max, max, 5, 4, 0, 2, 8},
/*F*/ {16, 7, 6, max, 2, 0, 9},
/*G*/ {14, max, max, max, 8, 9, 0}
};
KruskalCase kruskalCase = new KruskalCase(vertexs, matrix);
System.out.println(kruskalCase.edgeNum);
kruskalCase.print();
kruskalCase.kruskal();
}
private void kruskal() {
int index=0;
int[] ends=new int[edgeNum];//寻找终点是否相同的变量
Data[] result=new Data[edgeNum];//最后的结果边
Data[] edges=getEdges();
sortEdges(edges);
System.out.println(Arrays.toString(edges));
for(int i=0;i<edgeNum;i++){
int one=getPosition(edges[i].start);
int two=getPosition(edges[i].end);
int m=getEnd(ends,one);
int n=getEnd(ends,two);
if(m!=n){//没有回路
ends[m]=n;
result[index++]=edges[i];
}
}
System.out.println("最小生成树为");
for(int i = 0; i < index; i++) {
System.out.println(result[i]);
}
}
//排序边
private void sortEdges(Data[] edges){
for(int i=0;i<edges.length-1;i++){
for(int j=0;j<edges.length-1;j++){
if(edges[j].weight>edges[j+1].weight){
Data temp=edges[j];
edges[j]=edges[j+1];
edges[j+1]=temp;
}
}
}
}
//通过字符找其在vertex的索引
private int getPosition(char ch) {
for(int i = 0; i < vertexs.length; i++) {
if(vertexs[i] == ch) {//找到
return i;
}
}
//找不到,返回-1
return -1;
}
//通过matrix与edgenum得到边的数组
private Data[] getEdges() {
int index = 0;
Data[] edges = new Data[edgeNum];
for(int i = 0; i < vertexs.length; i++) {
for(int j=i+1; j <vertexs.length; j++) {
if(matrix[i][j] != max) {
edges[index++] = new Data(vertexs[i], vertexs[j], matrix[i][j]);
}
}
}
return edges;
}
/**
* 得到索引未i的终点索引,通过不同i返回的索引是否相同,判断是否产生回路
* @param ends
* @param i
* @return
*/
private int getEnd(int[] ends,int i){
//可能①:当前i没有终点则直接返回i,表示i的终点就是他自己的索引,
// 可能②:(例如有a->b->c->d的线)当前i有终点则将i赋值为终点b,再寻找终点b是否有终点c,直到找到最终终点d的索引
while(ends[i]!=0){
i=ends[i];
}
return i;
}
}
class Data{
char start;
char end;
int weight;
public Data(char start, char end, int weight) {
this.start = start;
this.end = end;
this.weight = weight;
}
@Override
public String toString() {
return "Data{" +
"start=" + start +
", end=" + end +
", weight=" + weight +
'}';
}
}
对比:
普利姆和克鲁斯卡尔问题的区别就是前者是指定从某个点开始构成n-1条线,后者是构成森林然后寻找森林里最短的路径构成n-1条线