圖(中)
在上一節的時候曾說過了圖的兩種遍歷方式,在這一節將使用他們做更深層的應用,研究從一個點到另一個點的最短距離。
最短路徑問題
單源無權圖的最短路徑
基本思想是,按照非遞減的順序,找出各個點的最短路。
很容易想到按照非遞減的順序,也就是優先從原點開始,不斷的計算與他相距最近的點的距離,整個的過程就是一個BFS。
在bfs的過程中,我們之前是用一個布林型別的陣列來儲存一個節點是否被訪問。現在我們可以將其改成為int型別的二維陣列,同時該陣列還需要實現兩個功能,對於第I個節點Vertex i,p[ i ][ 0 ]用於儲存它到原點的距離,而p[ i ][ 1 ]用於儲存最短路徑中他被哪個節點所指。在輸出最短路徑時,我們只需要沿著它的路徑倒著走直到原點,反一下就是正確的路徑了。
//無全權圖最短路徑
int p[MAXSIZE][2];
void UnweightShortestPath(Graph G, Vertex x) {
for (int i = 0; i < MAXSIZE; i++)
{
memset(p[i], -1, sizeof(p[i]));
}
//自己到自己的距離是0
p[x][0] = 0;
Queue Q = makeempty();
QueueAdd(x, Q);
while (!isEmpty(Q))
{
Vertex v = QueueDelete(Q);
for (int i = 0; i < G->Nvertex; i++)
{
if (G->graph[v][i] != 0 && p[i][0]==-1) {
p[i][0] = p[v][0] + 1;
p[i][1] = v;
QueueAdd(i, Q);
}
}
}
int end;
scanf_s("%d", &end);
stack<int> v;
}
單源有權圖的最短路徑(Dijkstra演算法)
Dijkstra演算法的基本演算法思想是:建立一個集合S,不斷的向該集合中新增頂點,每次移出時確定了該頂點的最短的。隨著不斷的新增,移除使得所有頂點的最短路徑都被確定為最小。(貪心演算法的思想)
具體實現是首先從原點開始,將其標記為已讀(代表該點在整個圖中最短路徑確定 ),將它的所有連線點加入集合中,並將這些點到原點的最短距離設定為權值,並標記他們指向原點(在確定了最短路徑列印時需要用)。
隨後從集合中移除距離最小的一個,將該節點標記為已讀(該點最短路徑一定已經確定,因為他是原點的連線點中距離最短的那個,如果從原點經過其他的節點到他,距離必定會更大)。
將該節點的鄰接點加入集合,並將這些點到原點的最短距離設定為移除點的最短距離加上自身到移除點的距離和自己原來最短距離中的最小值。
即\(dp[ i ] = Min\){\(dp[ i ] ,dp[ v ]+weight[ v ][ i ]\)}(\(dp[ i ]\)表示i點到遠點的最短距離\(weight[ v ][ i ]\)v到i的距離)。
遞迴的則執行直到所有的點都被集合收入又移出結束。
注意到:在上面的演算法中我們是不斷的距離從近到遠,在圖中如果有負值出現的話,我們將會在這個負值圈內打轉。該演算法再存在負值的權值是無效的。
#define INFINITY 10000000
//不同的編輯器該值並不相同,可能會報導致值為0
//find MinDis
Vertex findMin(Graph G,int dis[],bool collection[]) {
Vertex minV = 0;
int minDis = INFINITY;
for (Vertex i = 0; i < G->Nvertex; i++)
{
if (collection[i] == false && dis[i] < minDis) {
minDis = dis[i];
minV = i;
}
}
//如果最短距離被改變,說明有找到
if (minDis < INFINITY) {
return minV;
}
return notFound;
}
//Dijkstra
//注意,在該有權值圖中,應是無邊連線的兩點為正無窮大
bool Dijkstra(Graph G, Vertex x) {
//save information
int shortDis[MAXSIZE];
bool collection[MAXSIZE];
int path[MAXSIZE];
memset(shortDis, INFINITY, sizeof(shortDis));
memset(collection, false, sizeof(collection));
memset(path, -1, sizeof(path));
//先將起始點的連線點距離初始化
for (Vertex i = 0; i <= G->Nvertex; i++)
{
shortDis[i] = G->graph[x][i];
if (shortDis[i] < INFINITY) {
path[i] = x;
}
}
collection[x] = true;
shortDis[x] = 0;
while (true)
{
Vertex v = findMin(G, shortDis, collection);
if (v == notFound) {
break;
}
collection[v] = true;
for (Vertex i = 0; i <= G->Nvertex; i++)
{
if (G->graph[v][i] < INFINITY && collection[i] == false) {
//存在負值圈演算法無法正常進行
if (G->graph[v][i] < 0) {
return false;
}
/*
* 移除點的最短距離加上自身到移除點的距離小於自己原來最短距離。
*/
if (shortDis[i] > shortDis[v] + G->graph[v][i]) {
shortDis[i] = shortDis[v] + G->graph[v][i];
path[i] = v;
}
}
}
}
return true;
}
多源有權圖的最短路徑(Floyd演算法)
在上面的方法中為了求某一點到與固定點的最短距離,使用Dijkstra演算法,現在我們要求任意兩點間的最短距離,我們當然可以將所有的點都用一次Dijkstra演算法來得到,以後更簡單更快的方法是使用Floyd演算法。
Floyd演算法的演算法思路是:
用一個二維陣列dp[ ][ ]來儲存第I個點到第J這個點的最短距離。
每次迴圈嘗試在I到J的路徑中,加入第K個節點(k = 1.....N)試試會不會讓路徑變短,如果路徑會變短即(\(dp[ i ][ j ]<dp[ i ][ k ]+dp[ k ][ j ]\)),就更新\(dp[ i ][ j ]=dp[ i ][ k ]+dp[ k ][ j ]\),否則不變。經過K輪迴圈之後,我們將會得到所有的任意兩點間的最短路徑同樣的 。
該演算法如何進行:
初始的時候我們令陣列儲存的是原來圖中兩點的距離,並且將自己指向自己的距離設為1。在插入第1個節點時(在程式中下標為0),我們注意到在起始點或結束點有該第一個節點的話,是不會改變的,即\(dp[ i ][ 0 ]==dp[ i ][ 0 ]+dp[ 0 ][ 0 ]\)或\(dp[ 0 ][ j ]==dp[ 0 ][ 0 ]+dp[ 0 ][ j ]\),注意到\(dp[ i ][ j ] = Min(dp[ i ][ 0 ]+dp[ 0 ][ j ],dp[ i ][ j ])\)。迴圈著往下走我們發現,每次\(dp[ i ][ j ]\)的值改變,一定是由第K行和第K列決定,而該列在此輪外迴圈中,值絕對不會發生改變。
同上面的演算法一樣該演算法在存在負值圈的時候,也會出問題。
bool Floyd(Graph G) {
int dp[MAXSIZE][MAXSIZE];
int path[MAXSIZE][MAXSIZE];
for (int i = 0; i <= G->Nvertex; i++)
{
for (int j = 0; j <= G->Nvertex; j++)
{
if (i == j) {
dp[i][j] = 0;
}
else {
dp[i][j] = G->graph[i][j];
}
}
}
for (int k = 0; k < G->Nvertex; k++)
{
for (int i = 0; i < G->Nvertex; i++)
{
for (int j = 0; j < G->Nvertex; j++)
{
if (dp[i][j] > dp[i][k] + dp[k][j]) {
dp[i][j] = dp[i][k] + dp[k][j];
if (i == j && dp[i][j] < 0) {
return false;
}
path[i][j] = k;
}
}
}
}
return true;
}