6、普利姆算法(Prim)
最小生成樹:
修路問題本質就是就是最小生成樹問題, 先介紹一下 最小生成樹 (Minimum Cost Spanning Tree),簡稱MST。
- 給定一個帶權的無向連通圖,如何選取一棵生成樹,使樹上所有邊上權的總和爲最小,這叫最小生成樹
- N個頂點,一定有N-1條邊
- 包含全部頂點
- N-1條邊都在圖中
- 舉例說明(如圖)
- 求最小生成樹的算法主要是普里姆算法和克魯斯卡爾算法
普里姆算法介紹:
- 普利姆(Prim)算法求最小生成樹,也就是在包含n個頂點的連通圖中,找出只有(n-1)條邊包含所有n個頂點的連通子圖,也就是所謂的極小連通子圖。
- 普利姆的算法如下:
(1)設G=(V,E)是連通網,T=(U,D)是最小生成樹,V,U是頂點集合,E,D是邊的集合。
(2)若從頂點u開始構造最小生成樹,則從集合V中取出頂點u放入集合U中,標記頂點v的visited[u]=1。
(3)若集合U中頂點ui與集合V-U中的頂點vj之間存在邊,則尋找這些邊中權值最小的邊,但不能構成迴路,將頂點vj加入集合U中,將邊(ui,vj)加入集合D中,標記visited[vj]=1。
(4)重複步驟②,直到U與V相等,即所有頂點都被標記爲訪問過,此時D中有n-1條邊。
普里姆算法最佳實踐(修路問題):
有勝利鄉有7個村莊(A, B, C, D, E, F, G) ,現在需要修路把7個村莊連通,各個村莊的距離用邊線表示(權) ,比如 A – B 距離 5公里,問:如何修路保證各個村莊都能連通,並且總的修建公路總里程最短?
圖解:
代碼:
public class PrimAlgorithm {
public static void main(String[] args) {
char[] data=new char[] {'A','B','C','D','E','F','G'};
int vertexs = data.length;
//用二維數組來表示鄰接矩陣
int[][] weight = new int[][] {//用10000這個較大的數表示不連通
{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 graph = new MGraph(vertexs);
//創建最小生成樹對象
MinTree minTree = new MinTree();
minTree.createGraph(graph, vertexs, data, weight);
//輸出最小生成樹
minTree.showGraph(graph);
//測試算法
minTree.prim(graph, 1);
}
}
// 創建最小生成樹
class MinTree {
// 創建圖的鄰接矩陣
/**
*
* @param graph
* 圖對象
* @param vertexs
* 圖對應的頂點個數
* @param data
* 圖對應的各個頂點的值
* @param weight
* 圖的鄰接矩陣
*/
public void createGraph(MGraph graph, int vertexs, char[] data, int[][] weight) {
for (int i = 0; i < vertexs; i++) {
graph.data[i] = data[i];
for (int j = 0; j < vertexs; j++) {
graph.weight[i][j] = weight[i][j];
}
}
}
// 顯示圖的鄰接矩陣
public void showGraph(MGraph graph) {
for(int i=0;i<graph.vertexs;i++) {
for(int j=0;j<graph.vertexs;j++) {
System.out.printf("%6d",graph.weight[i][j]);
}
System.out.println();
}
}
//編寫Prim算法得到最小生成樹
/**
*
* @param graph 圖
* @param v 表示從圖的第幾個頂點開始生成'A'->0, 'B'->1 ...
*/
public void prim(MGraph graph, int v) {
//visited數組表示頂點是否被訪問過
int[] visited = new int[graph.vertexs];//默認就爲0
//把當前節點標記爲已經訪問
visited[v]=1;
//h1和h2記錄兩個頂點的下標
int h1=-1;
int h2=-1;
int minWeight=10000;//將其設置爲一個比較大的數,在後面的遍歷過程中會被替換
for(int k=1;k<graph.vertexs;k++) {//因爲有vertexs個頂點,則經過prim算法過後一定有vertexs-1條邊
//確定每一次生成的子圖,和哪個結點距離最近
for(int i=0;i<graph.vertexs;i++) {//i表示被訪問過的點
for(int j=0;j<graph.vertexs;j++) {//j表示與i結點相連且未被訪問過的點
if(visited[i]==1&&visited[j]==0&&graph.weight[i][j]<minWeight) {
//替換minWeight,由此來尋找滿足條件兩邊中權值最小的
minWeight=graph.weight[i][j];
h1=i;
h2=j;
}
}
}
//此時已經找到了一條最小的邊,則將這個邊輸出
System.out.println("在邊 <"+graph.data[h1]+"-"+graph.data[h2]+">修路 權值爲:"+minWeight);
//將當前這個新點標記爲已訪問過
visited[h2]=1;
//重置minWeight
minWeight=10000;
}
}
}
class MGraph {
int vertexs;// 表示節點數
char[] data;// 存放節點數據
int[][] weight;// 存放邊
public MGraph(int vertexs) {
this.vertexs = vertexs;
data = new char[vertexs];
weight = new int[vertexs][vertexs];
}
}
結果:
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
在邊 <B-G>修路 權值爲:3
在邊 <G-A>修路 權值爲:2
在邊 <G-E>修路 權值爲:4
在邊 <E-F>修路 權值爲:5
在邊 <F-D>修路 權值爲:4
在邊 <A-C>修路 權值爲:7
7、克魯斯卡爾算法(Kruskal)
克魯斯卡爾算法介紹:
- 克魯斯卡爾(Kruskal)算法,是用來求加權連通圖的最小生成樹的算法。
- 基本思想:按照權值從小到大的順序選擇n-1條邊,並保證這n-1條邊不構成迴路。
- 具體做法:首先構造一個只含n個頂點的森林,然後依權值從小到大從連通網中選擇邊加入到森林中,並使森林中不產生迴路,直至森林變成一棵樹爲止。
克魯斯卡爾最佳實踐-公交站問題:
有北京有新增7個站點(A, B, C, D, E, F, G) ,現在需要修路把7個站點連通,各個站點的距離用邊線表示(權) ,比如 A – B 距離 12公里,問:如何修路保證各個站點都能連通,並且總的修建公路總里程最短?
圖解:
代碼:
import java.util.Arrays;
public class KruskalCase {
private int edgeNum;//記錄有多少條邊
private char[] vertexs;//記錄頂點
private int[][] matrix;//鄰接矩陣
//使用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= {
/*A*//*B*//*C*//*D*//*E*//*F*//*G*/
/*A*/ { 0, 12, INF, INF, INF, 16, 14},
/*B*/ { 12, 0, 10, INF, INF, 7, INF},
/*C*/ { INF, 10, 0, 3, 5, 6, INF},
/*D*/ { INF, INF, 3, 0, 4, INF, INF},
/*E*/ { INF, INF, 5, 4, 0, 2, 8},
/*F*/ { 16, 7, 6, INF, 2, 0, 9},
/*G*/ { 14, INF, INF, INF, 8, 9, 0}
};
//創建KruskalCase對象實例
KruskalCase kruskalCase = new KruskalCase(vertexs, matrix);
//輸出圖
kruskalCase.print();
kruskalCase.kruskal();
}
//構造器
public KruskalCase(char[] vertexs ,int[][] matrix) {
//初始化頂點個數,和邊的個數
int vlen = vertexs.length;
//初始化頂點
this.vertexs=vertexs.clone();
//初始化邊
this.matrix=matrix.clone();
//統計邊的個數
for(int i=0;i<vlen;i++) {
for(int j=i+1;j<vlen;j++){
if(this.matrix[i][j]!=INF) {
edgeNum++;
}
}
}
}
public void kruskal() {
int index=0;//表示最後結果數組的索引
int[] ends = new int[vertexs.length];//用於保存“已有最小生成樹”中的每個頂點在最小生成樹中的終點
//創建結果數組,用於保存最後的最小生成樹
EData[] res = new EData[vertexs.length-1];
//獲取圖中所有的邊的集合(12條邊)
EData[] edges = getEdges();
//按邊的權值大小排序(從小到大)
sortEdges(edges);
//遍歷edges數組,添加邊入最小生成樹。判斷準備加入的邊是否形成了迴路,如果沒形成則加入最小生成樹中,否則繼續遍歷
for(int i=0;i<edgeNum;i++) {
//獲取第i條邊的第一個頂點
int p1 = getPosition(edges[i].start);
//獲取第i條邊的第二個頂點
int p2 = getPosition(edges[i].end);
//獲取p1頂點在已有最小生成樹中的終點
int m = getEnd(ends, p1);
//獲取p2頂點在已有最小生成樹中的終點
int n = getEnd(ends, p2);
//判斷是否構成迴路
if(m!=n) {//沒有構成迴路
ends[m]=n;
res[index++]=edges[i];
}
}
//打印最小生成樹
System.out.println("最小生成樹:");
for(int i=0;i<res.length;i++) {
System.out.println(res[i]);
}
System.out.println(Arrays.toString(ends));
}
//打印鄰接矩陣
public void print() {
System.out.println("鄰接矩陣爲:");
for(int i=0;i<vertexs.length;i++) {
for(int j=0;j<vertexs.length;j++) {
System.out.printf("%12d",matrix[i][j]);
}
System.out.println();
}
}
//對邊進行排序處理(冒泡)
private void sortEdges(EData[] edges) {
for(int i=0;i<edges.length-1;i++) {
for(int j=0;j<edges.length-1-i;j++) {
if(edges[j].weight>edges[j+1].weight) {
EData temp = edges[j];
edges[j]=edges[j+1];
edges[j+1]=temp;
}
}
}
}
//返回傳入頂點對應的下標
private int getPosition(char ch) {
for(int i=0;i<vertexs.length;i++) {
if(vertexs[i]==ch) {
return i;
}
}
return -1;
}
//獲取圖中的所有邊,放入EData數組中,通過鄰接矩陣來獲取
private EData[] getEdges() {
int index=0;
EData[] edges = new EData[edgeNum];
for(int i=0;i<vertexs.length;i++) {
for(int j=i+1;j<vertexs.length;j++) {
if(matrix[i][j]!=INF) {
edges[index++]=new EData(vertexs[i],vertexs[j],matrix[i][j]);
}
}
}
return edges;
}
//獲取下標爲i的頂點的終點的下標
//ends數組用來記錄各個頂點的終點是哪個,它是在遍歷過程中逐步生成的
private int getEnd(int[] ends,int i) {
while(ends[i]!=0) {
i=ends[i];
}
return i;
}
}
//創建一個類,其對象實例表示一條邊
class EData{
char start;//邊的一個點
char end;//邊的另一個點
int weight;//邊的權值
//構造器
public EData(char start, char end, int weight) {
super();
this.start = start;
this.end = end;
this.weight = weight;
}
//輸出邊的信息
@Override
public String toString() {
return "EData [<" + start + ", " + end + "> =" + weight + "]";
}
}
結果:
鄰接矩陣爲:
0 12 2147483647 2147483647 2147483647 16 14
12 0 10 2147483647 2147483647 7 2147483647
2147483647 10 0 3 5 6 2147483647
2147483647 2147483647 3 0 4 2147483647 2147483647
2147483647 2147483647 5 4 0 2 8
16 7 6 2147483647 2 0 9
14 2147483647 2147483647 2147483647 8 9 0
最小生成樹:
EData [<E, F> =2]
EData [<C, D> =3]
EData [<D, E> =4]
EData [<B, F> =7]
EData [<E, G> =8]
EData [<A, B> =12]
8、迪傑斯特拉算法(Dijkstra)
迪傑斯特拉(Dijkstra)算法介紹:
迪傑斯特拉(Dijkstra)算法是典型最短路徑算法,用於計算一個結點到其他結點的最短路徑。 它的主要特點是以起始點爲中心向外層層擴展(廣度優先搜索思想),直到擴展到終點爲止。
迪傑斯特拉(Dijkstra)算法過程:
設置出發頂點爲v,頂點集合V{v1,v2,vi…},v到V中各頂點的距離構成距離集合Dis,Dis{d1,d2,di…},Dis集合記錄着v到圖中各頂點的距離(到自身可以看作0,v到vi距離對應爲di)
- 從Dis中選擇值最小的di並移出Dis集合,同時移出V集合中對應的頂點vi,此時的v到vi即爲最短路徑。
- 更新Dis集合,更新規則爲:比較v到V集合中頂點的距離值,與v通過vi到V集合中頂點的距離值,保留值較小的一個(同時也應該更新頂點的前驅節點爲vi,表明是通過vi到達的)。
- 重複執行兩步驟,直到最短路徑頂點爲目標頂點即可結束。
迪傑斯特拉(Dijkstra)算法最佳應用-最短路徑:
戰爭時期,勝利鄉有7個村莊(A, B, C, D, E, F, G) ,現在有六個郵差,從G點出發,需要分別把郵件分別送到 A, B, C , D, E, F 六個村莊,各個村莊的距離用邊線表示(權) ,比如 A – B 距離 5公里,問:如何計算出G村莊到 其它各個村莊的最短距離?
圖解:
代碼:
import java.util.Arrays;
public class DijkstraAlgorithm {
public static void main(String[] args) {
char[] vertex = { 'A', 'B', 'C', 'D', 'E', 'F', 'G' };
// 鄰接矩陣
int[][] matrix = new int[vertex.length][vertex.length];
final int N = 65535;// 表示不可連接
matrix[0] = new int[] { N, 5, 7, N, N, N, 2 };
matrix[1] = new int[] { 5, N, N, 9, N, N, 3 };
matrix[2] = new int[] { 7, N, N, N, 8, N, N };
matrix[3] = new int[] { N, 9, N, N, N, 4, N };
matrix[4] = new int[] { N, N, 8, N, N, 5, 4 };
matrix[5] = new int[] { N, N, N, 4, 5, N, 6 };
matrix[6] = new int[] { 2, 3, N, N, 4, 6, N };
// 創建Graph對象
Graph graph = new Graph(vertex, matrix);
graph.showGraph();
graph.dijkstra(6);
graph.showDijkstra();
}
}
class Graph {
private char[] vertex;// 存放各個頂點
private int[][] matrix;// 鄰接矩陣
private VisitedVertex visitedVertex;// 已經訪問的定點的集合
// 構造器
public Graph(char[] vertexs, int[][] matrix) {
this.vertex = vertexs;
this.matrix = matrix;
}
// 顯示最後結果
public void showDijkstra() {
visitedVertex.show();
}
// 顯示圖
public void showGraph() {
for (int[] link : matrix) {
System.out.println(Arrays.toString(link));
}
}
// 地傑斯特拉算法實現,index表示出發頂點的下標
public void dijkstra(int index) {
visitedVertex = new VisitedVertex(vertex.length, index);
update(index);// 更新距離和前驅頂點
for (int i = 1; i < vertex.length; i++) {
index = visitedVertex.updateArr();// 選擇並訪問新的訪問頂點
update(index);
}
}
// 同時更新index下標頂點到周圍相連頂點的距離,和周圍相連頂點的前驅頂點
public void update(int index) {
int len = 0;
// 遍歷鄰接矩陣的index行
for (int i = 0; i < matrix[index].length; i++) {
// len表示出發頂點到index頂點的距離 + index頂點到i頂點的距離
len = visitedVertex.getDis(index) + matrix[index][i];
// 如果i頂點未被訪問過,並且len小於出發頂點到i頂點的距離,就要更新
if (!visitedVertex.in(i) && len < visitedVertex.getDis(i)) {
visitedVertex.updatePre(i, index);// 更新i頂點的前驅頂點爲index頂點
visitedVertex.updateDis(i, len);// 更新出發頂點到i頂點的距離爲len
}
}
}
}
// 已訪問頂點集合
class VisitedVertex {
public int[] already_arr;// 記錄各個頂點是否爲訪問過
public int[] pre_visited;// 記錄本下標頂點所對應的前一個頂點的下標
public int[] dis;// 記錄出發頂點到與其相連的所有頂點的距離,將距離最短的放入dis中
// 構造器
/**
*
* @param length
* 頂點的個數
* @param index
* 出發頂點對應的下標
*/
public VisitedVertex(int length, int index) {
this.already_arr = new int[length];//標記某個頂點是否已經被訪問
this.pre_visited = new int[length];//記錄每個頂點的上一個頂點是哪一個(用來確定最短路徑是哪一條)
this.dis = new int[length];//起始點到每一個點的最短路徑長度
// 初始化dis數組,全填爲最大值(除了出發頂點)
Arrays.fill(dis, 65535);
this.already_arr[index] = 1;// 設置出發頂點已經被訪問過
this.dis[index] = 0;// 設置出發頂點的訪問距離爲0
}
// 判斷index頂點是否被訪問過
public boolean in(int index) {
return already_arr[index] == 1;
}
// 更新出發頂點到index頂點的距離,更新爲len
public void updateDis(int index, int len) {
dis[index] = len;
}
// 更新pre頂點的前驅頂點,更新爲index
public void updatePre(int pre, int index) {
pre_visited[pre] = index;
}
// 返回出發頂點到index頂點的距離
public int getDis(int index) {
return dis[index];
}
// 繼續選擇並返回新的訪問頂點,比如G完之後就是A作爲一個新的訪問頂點(並非出發頂點)
public int updateArr() {
int min = 65535, index = 0;
for (int i = 0; i < already_arr.length; i++) {
if (already_arr[i] == 0 && dis[i] < min) {
min = dis[i];
index = i;
}
}
// 更新index被訪問過
already_arr[index] = 1;
return index;
}
// 顯示最後的結果
public void show() {
System.out.println("==========================");
// 輸出already_arr
for (int i : already_arr) {
System.out.print(i + " ");
}
System.out.println();
// 輸出pre_visited
for (int i : pre_visited) {
System.out.print(i + " ");
}
System.out.println();
// 輸出dis
for (int i : dis) {
System.out.print(i + " ");
}
System.out.println();
// 爲了好看最後的最短距離,我們處理
char[] vertex = { 'A', 'B', 'C', 'D', 'E', 'F', 'G' };
int count = 0;
for (int i : dis) {
if (i != 65535) {
System.out.print(vertex[count] + "(" + i + ") ");
} else {
System.out.println("N ");
}
count++;
}
System.out.println();
}
}
結果:
[65535, 5, 7, 65535, 65535, 65535, 2]
[5, 65535, 65535, 9, 65535, 65535, 3]
[7, 65535, 65535, 65535, 8, 65535, 65535]
[65535, 9, 65535, 65535, 65535, 4, 65535]
[65535, 65535, 8, 65535, 65535, 5, 4]
[65535, 65535, 65535, 4, 5, 65535, 6]
[2, 3, 65535, 65535, 4, 6, 65535]
==========================
1 1 1 1 1 1 1
6 6 0 5 6 6 0
2 3 9 10 4 6 0
[65535, 5, 7, 65535, 65535, 65535, 2]
[5, 65535, 65535, 9, 65535, 65535, 3]
[7, 65535, 65535, 65535, 8, 65535, 65535]
[65535, 9, 65535, 65535, 65535, 4, 65535]
[65535, 65535, 8, 65535, 65535, 5, 4]
[65535, 65535, 65535, 4, 5, 65535, 6]
[2, 3, 65535, 65535, 4, 6, 65535]
==========================
1 1 1 1 1 1 1
6 6 0 5 6 6 0
2 3 9 10 4 6 0
A(2) B(3) C(9) D(10) E(4) F(6) G(0)
其中 A(2) B(3) C(9) D(10) E(4) F(6) G(0) 括號中的數字表示G點與括號前的點最短的距離。
9、弗洛伊德算法(Floyd)
弗洛伊德(Floyd)算法介紹:
- 和Dijkstra算法一樣,弗洛伊德(Floyd)算法也是一種用於尋找給定的加權圖中頂點間最短路徑的算法。該算法名稱以創始人之一、1978年圖靈獎獲得者、斯坦福大學計算機科學系教授羅伯特·弗洛伊德命名。
- 弗洛伊德算法(Floyd)計算圖中各個頂點之間的最短路徑。
- 迪傑斯特拉算法用於計算圖中某一個頂點到其他頂點的最短路徑。
- 弗洛伊德算法 VS 迪傑斯特拉算法:迪傑斯特拉算法通過選定的被訪問頂點,求出從出發訪問頂點到其他頂點的最短路徑;弗洛伊德算法中每一個頂點都是出發訪問點,所以需要將每一個頂點看做被訪問頂點,求出從每一個頂點到其他頂點的最短路徑。
弗洛伊德(Floyd)算法分析:
- 設置頂點vi到頂點vk的最短路徑已知爲Lik,頂點vk到vj的最短路徑已知爲Lkj,頂點vi到vj的路徑爲Lij,則vi到vj的最短路徑爲:min((Lik+Lkj),Lij),vk的取值爲圖中所有頂點,則可獲得vi到vj的最短路徑。
- 至於vi到vk的最短路徑Lik或者vk到vj的最短路徑Lkj,是以同樣的方式獲得。
弗洛伊德(Floyd)算法最佳應用-最短路徑:
勝利鄉有7個村莊(A, B, C, D, E, F, G),各個村莊的距離用邊線表示(權) ,比如 A – B 距離 5公里,問:如何計算出各村莊到 其它各村莊的最短距離?
圖解:
代碼實現:
package floyd;
import java.util.Arrays;
public class FloydAlgorithm {
public static void main(String[] args) {
char[] vertex = { 'A', 'B', 'C', 'D', 'E', 'F', 'G' };
int[][] matrix = new int[vertex.length][vertex.length];
final int N = 65535;
matrix[0] = new int[] { 0, 5, 7, N, N, N, 2 };
matrix[1] = new int[] { 5, 0, N, 9, N, N, 3 };
matrix[2] = new int[] { 7, N, 0, N, 8, N, N };
matrix[3] = new int[] { N, 9, N, 0, N, 4, N };
matrix[4] = new int[] { N, N, 8, N, 0, 5, 4 };
matrix[5] = new int[] { N, N, N, 4, 5, 0, 6 };
matrix[6] = new int[] { 2, 3, N, N, 4, 6, 0 };
Graph graph = new Graph(vertex.length, matrix, vertex);
graph.floyd();
graph.show();
}
}
class Graph {
private char[] vertex;// 存放頂點
private int[][] dis;// 保存從各個頂點出發到其它頂點的距離
private int[][] pre;// 保存前驅頂點
// 構造器
/**
*
* @param length
* 大小
* @param matrix
* 鄰接矩陣
* @param vertex
* 頂點數組
*/
public Graph(int length, int[][] matrix, char[] vertex) {
this.vertex = vertex;
this.dis = matrix;
this.pre = new int[length][length];
// 對pre數組初始化,存放的是頂點下標
for (int i = 0; i < length; i++) {
Arrays.fill(pre[i], i);
}
}
// 顯示dis數組和pre數組
public void show() {
// 爲了顯示便於閱讀,我們優化一下輸出
char[] vertex = { 'A', 'B', 'C', 'D', 'E', 'F', 'G' };
for (int k = 0; k < dis.length; k++) {
// 先將pre數組輸出的一行
for (int i = 0; i < dis.length; i++) {
System.out.print(vertex[pre[k][i]] + " ");
}
System.out.println();
}
for (int k = 0; k < dis.length; k++) {
// 輸出dis數組的一行數據
for (int i = 0; i < dis.length; i++) {
System.out.print("(" + vertex[k] + "到" + vertex[i] + "的最短路徑是" + dis[k][i] + ") ");
}
System.out.println();
}
}
//弗洛伊德算法
public void floyd() {
int len=0;//保存變量距離
//遍歷中間頂點,k爲其下標
for(int k=0;k<dis.length;k++) {
//從i頂點出發
for(int i=0;i<dis.length;i++) {
//到達j頂點
for(int j=0;j<dis.length;j++) {
len = dis[i][k]+dis[k][j];//從i頂點出發,經過中間頂點k,到達j頂點的距離
if(len<dis[i][j]) {//如果len小於i到j的直連距離,則更新len;否則就不更新
dis[i][j]=len;
pre[i][j]=pre[k][j];
}
}
}
}
}
}
結果:
A A A F G G A
B B A B G G B
C A C F C E A
G D E D F D F
G G E F E E E
G G E F F F F
G G A F G G G
(A到A的最短路徑是0) (A到B的最短路徑是5) (A到C的最短路徑是7) (A到D的最短路徑是12) (A到E的最短路徑是6) (A到F的最短路徑是8) (A到G的最短路徑是2)
(B到A的最短路徑是5) (B到B的最短路徑是0) (B到C的最短路徑是12) (B到D的最短路徑是9) (B到E的最短路徑是7) (B到F的最短路徑是9) (B到G的最短路徑是3)
(C到A的最短路徑是7) (C到B的最短路徑是12) (C到C的最短路徑是0) (C到D的最短路徑是17) (C到E的最短路徑是8) (C到F的最短路徑是13) (C到G的最短路徑是9)
(D到A的最短路徑是12) (D到B的最短路徑是9) (D到C的最短路徑是17) (D到D的最短路徑是0) (D到E的最短路徑是9) (D到F的最短路徑是4) (D到G的最短路徑是10)
(E到A的最短路徑是6) (E到B的最短路徑是7) (E到C的最短路徑是8) (E到D的最短路徑是9) (E到E的最短路徑是0) (E到F的最短路徑是5) (E到G的最短路徑是4)
(F到A的最短路徑是8) (F到B的最短路徑是9) (F到C的最短路徑是13) (F到D的最短路徑是4) (F到E的最短路徑是5) (F到F的最短路徑是0) (F到G的最短路徑是6)
(G到A的最短路徑是2) (G到B的最短路徑是3) (G到C的最短路徑是9) (G到D的最短路徑是10) (G到E的最短路徑是4) (G到F的最短路徑是6) (G到G的最短路徑是0)
10、馬踏棋盤算法
馬踏棋盤算法介紹:
- 馬踏棋盤算法也被稱爲騎士周遊問題
- 將馬隨機放在國際象棋的8×8棋盤Board [0~7] [0~7] 的某個方格中,馬按走棋規則(馬走日字)進行移動。要求每個方格只進入一次,走遍棋盤上全部64個方格。
- 遊戲演示:馬踏棋盤小遊戲(6x6棋盤)
馬踏棋盤遊戲分析:
- 馬踏棋盤問題(騎士周遊問題)實際上是圖的深度優先搜索(DFS)的應用。
- 如果使用回溯(就是深度優先搜索)來解決,假如馬兒踏了53個點,如圖:走到了第53個,座標(1,0),發現已經走到盡頭,沒辦法,那就只能回退了,查看其他的路徑,就在棋盤上不停的回溯……
- 分析這種方式,可以再使用使用貪心算法(greedyalgorithm)的思想進行優化。即每次優先走再下一步走法較少的點。
圖解:
代碼:
import java.awt.Point;
import java.util.ArrayList;
public class HorseChessBoard {
private static int X;// 棋盤列數
private static int Y;// 棋盤行數
// 創建一個數組標記棋盤的各個位置是否被訪問過
private static boolean visited[][];
// 使用一個屬性,標記棋盤的所有位置是否都被訪問過
private static boolean finished;
public static void main(String[] args) {
X = 8;
Y = 8;
int row = 1;// 馬初始位置的行,從1開始
int col = 1;// 馬初始位置的列,從1開始
// 創建棋盤
int[][] chessBoard = new int[X][Y];
visited = new boolean[X][Y];// 初始值均爲false
long star = System.currentTimeMillis();
travelChessBoard(chessBoard, row - 1, col - 1, 1);
long end = System.currentTimeMillis();
System.out.println("共耗時:" + (end - star) + " ms");
//輸出棋盤最後情況
for(int[] rows:chessBoard) {
for(int cols:rows) {
System.out.print(cols+"\t");
}
System.out.println();
}
}
/**
*
* @param chessBoard
* 棋盤
* @param row
* 馬當前位置的行,從0開始
* @param col
* 馬當前位置的列,從0開始
* @param step
* 第幾步,從1開始
*/
public static void travelChessBoard(int[][] chessBoard, int row, int col, int step) {
chessBoard[row][col] = step;
visited[row][col] = true;// 標記該位置已經被訪問
// 獲取當前位置可以走的下一個位置的集合
ArrayList<Point> points = next(new Point(col, row));
// 遍歷points
while (!points.isEmpty()) {
Point point = points.remove(0);
// 判斷該點是否已經被訪問過
if (!visited[point.y][point.x]) {
travelChessBoard(chessBoard, point.y, point.x, step + 1);
}
}
// 判斷馬是否完成任務(用step和應走的步數進行比較)
// 如果沒有達到數量,則表示未完成任務,則將整個棋盤置0
// 如果step<X*Y成立,則有兩種情況
// 1. 棋盤到目前位置仍然沒有走完
// 2. 棋盤處於一個回溯過程
if (step < X * Y && !finished) {
chessBoard[row][col] = 0;
visited[row][col] = false;
} else {
finished = true;
}
}
// 根據當前位置(Point對象),計算馬還能走那些位置(Point),並放入List集合中(最多有8個位置)
public static ArrayList<Point> next(Point curPoint) {
// 創建一個集合
ArrayList<Point> points = new ArrayList<Point>();
Point p1 = new Point();
// 表示馬兒可以走5這個位置
if ((p1.x = curPoint.x - 2) >= 0 && (p1.y = curPoint.y - 1) >= 0) {
points.add(new Point(p1));
}
// 判斷馬兒可以走6這個位置
if ((p1.x = curPoint.x - 1) >= 0 && (p1.y = curPoint.y - 2) >= 0) {
points.add(new Point(p1));
}
// 判斷馬兒可以走7這個位置
if ((p1.x = curPoint.x + 1) < X && (p1.y = curPoint.y - 2) >= 0) {
points.add(new Point(p1));
}
// 判斷馬兒可以走0這個位置
if ((p1.x = curPoint.x + 2) < X && (p1.y = curPoint.y - 1) >= 0) {
points.add(new Point(p1));
}
// 判斷馬兒可以走1這個位置
if ((p1.x = curPoint.x + 2) < X && (p1.y = curPoint.y + 1) < Y) {
points.add(new Point(p1));
}
// 判斷馬兒可以走2這個位置
if ((p1.x = curPoint.x + 1) < X && (p1.y = curPoint.y + 2) < Y) {
points.add(new Point(p1));
}
// 判斷馬兒可以走3這個位置
if ((p1.x = curPoint.x - 1) >= 0 && (p1.y = curPoint.y + 2) < Y) {
points.add(new Point(p1));
}
// 判斷馬兒可以走4這個位置
if ((p1.x = curPoint.x - 2) >= 0 && (p1.y = curPoint.y + 1) < Y) {
points.add(new Point(p1));
}
return points;
}
}
結果:
共耗時:69672 ms
1 8 11 16 3 18 13 64
10 27 2 7 12 15 4 19
53 24 9 28 17 6 63 14
26 39 52 23 62 29 20 5
43 54 25 38 51 22 33 30
40 57 42 61 32 35 48 21
55 44 59 50 37 46 31 34
58 41 56 45 60 49 36 47
可以明顯看到需要較長時間,才能運行出結果。
下面我們用貪心算法的思想進行優化,優先走再下一步走法較少的點:
代碼:
import java.awt.Point;
import java.util.ArrayList;
import java.util.Comparator;
public class HorseChessBoard {
private static int X;// 棋盤列數
private static int Y;// 棋盤行數
// 創建一個數組標記棋盤的各個位置是否被訪問過
private static boolean visited[][];
// 使用一個屬性,標記棋盤的所有位置是否都被訪問過
private static boolean finished;
public static void main(String[] args) {
X = 8;
Y = 8;
int row = 1;// 馬初始位置的行,從1開始
int col = 1;// 馬初始位置的列,從1開始
// 創建棋盤
int[][] chessBoard = new int[X][Y];
visited = new boolean[X][Y];// 初始值均爲false
long star = System.currentTimeMillis();
travelChessBoard(chessBoard, row - 1, col - 1, 1);
long end = System.currentTimeMillis();
System.out.println("共耗時:" + (end - star) + " ms");
//輸出棋盤最後情況
for(int[] rows:chessBoard) {
for(int cols:rows) {
System.out.print(cols+"\t");
}
System.out.println();
}
}
/**
*
* @param chessBoard
* 棋盤
* @param row
* 馬當前位置的行,從0開始
* @param col
* 馬當前位置的列,從0開始
* @param step
* 第幾步,從1開始
*/
public static void travelChessBoard(int[][] chessBoard, int row, int col, int step) {
chessBoard[row][col] = step;
visited[row][col] = true;// 標記該位置已經被訪問
// 獲取當前位置可以走的下一個位置的集合
ArrayList<Point> points = next(new Point(col, row));
//對points中元素進行排序
sort(points);
// 遍歷points
while (!points.isEmpty()) {
Point point = points.remove(0);
// 判斷該點是否已經被訪問過
if (!visited[point.y][point.x]) {
travelChessBoard(chessBoard, point.y, point.x, step + 1);
}
}
// 判斷馬是否完成任務(用step和應走的步數進行比較)
// 如果沒有達到數量,則表示未完成任務,則將整個棋盤置0
// 如果step<X*Y成立,則有兩種情況
// 1. 棋盤到目前位置仍然沒有走完
// 2. 棋盤處於一個回溯過程
if (step < X * Y && !finished) {
chessBoard[row][col] = 0;
visited[row][col] = false;
} else {
finished = true;
}
}
// 根據當前位置(Point對象),計算馬還能走那些位置(Point),並放入List集合中(最多有8個位置)
public static ArrayList<Point> next(Point curPoint) {
// 創建一個集合
ArrayList<Point> points = new ArrayList<Point>();
Point p1 = new Point();
// 表示馬兒可以走5這個位置
if ((p1.x = curPoint.x - 2) >= 0 && (p1.y = curPoint.y - 1) >= 0) {
points.add(new Point(p1));
}
// 判斷馬兒可以走6這個位置
if ((p1.x = curPoint.x - 1) >= 0 && (p1.y = curPoint.y - 2) >= 0) {
points.add(new Point(p1));
}
// 判斷馬兒可以走7這個位置
if ((p1.x = curPoint.x + 1) < X && (p1.y = curPoint.y - 2) >= 0) {
points.add(new Point(p1));
}
// 判斷馬兒可以走0這個位置
if ((p1.x = curPoint.x + 2) < X && (p1.y = curPoint.y - 1) >= 0) {
points.add(new Point(p1));
}
// 判斷馬兒可以走1這個位置
if ((p1.x = curPoint.x + 2) < X && (p1.y = curPoint.y + 1) < Y) {
points.add(new Point(p1));
}
// 判斷馬兒可以走2這個位置
if ((p1.x = curPoint.x + 1) < X && (p1.y = curPoint.y + 2) < Y) {
points.add(new Point(p1));
}
// 判斷馬兒可以走3這個位置
if ((p1.x = curPoint.x - 1) >= 0 && (p1.y = curPoint.y + 2) < Y) {
points.add(new Point(p1));
}
// 判斷馬兒可以走4這個位置
if ((p1.x = curPoint.x - 2) >= 0 && (p1.y = curPoint.y + 1) < Y) {
points.add(new Point(p1));
}
return points;
}
//根據當前這一步的所有下一步的選擇位置,進行非遞減排序(貪心算法思想優化)
public static void sort(ArrayList<Point> points) {
points.sort(new Comparator<Point>() {
@Override
public int compare(Point o1, Point o2) {
int count1=next(o1).size();
int count2=next(o2).size();
return count1-count2;
}
});
}
}
結果:
共耗時:24 ms
1 16 37 32 3 18 47 22
38 31 2 17 48 21 4 19
15 36 49 54 33 64 23 46
30 39 60 35 50 53 20 5
61 14 55 52 63 34 45 24
40 29 62 59 56 51 6 9
13 58 27 42 11 8 25 44
28 41 12 57 26 43 10 7
可以明顯看出這樣優化大大提高了速度(由於改變了行走的策略,會導致走法與未優化前的不同)