中國郵路問題(Chinese Postman Problem)是一個非常經典的圖論問題:一個郵遞員送信,要走完他負責投遞的全部街道(所有街道都是雙向通行的且每條街道可以經過不止一次),完成任務后回到郵局,應按怎樣的路線走,他所走的路程才會最短呢?如果將這個問題抽象成圖論的語言,就是給定一個連通圖,每條邊的權值就是街道的長度,本問題轉化為在圖中求一條回路,使得回路的總權值最小。
如果街道的連通圖為歐拉圖,則只要求出圖中的一條歐拉回路即可。否則,郵遞員要完成任務就必須在某些街道上重復走若干次。如果重復走一次,就加一條平行邊,於是原來對應的圖形就變成了多重圖。只是要求加進的平行邊的總權值最小就行了。於是,我們的問題就轉化為,在一個有奇度數結點的賦權連通圖中,增加一些平行邊,使得新圖不含奇度數結點,並且增加的邊的總權值最小。
求所增加邊總權值最小的方案,需要我們找出所有奇度頂點(偶數個)來兩兩分組,對每小組中的兩個點求其最短路徑,進而求出每分組的總權值。對所有分組情況,找出最小權值即是最佳方案。
下面直接上源代碼。
#include <limits.h>
#include <iostream>
#include <cstdlib>
#include <set>
#include <vector>
using namespace std;
#define MAX_NODE 26 // 最大結點數
#define COST_NO_LINK INT_MAX // 定義結點之間沒有連接的花銷為INT_MAX嗎
int Graph[MAX_NODE][MAX_NODE];
int Cost[MAX_NODE][MAX_NODE];
int V_dingdianshu, E_bianshu, Start_Point; // 頂點數和邊數,以及開始的起點(以0開始)
int Odd_Grouping[MAX_NODE]; // 為0表示不為奇,為1表示為奇,從2開始表示配對分組情況,如同為2的兩個為一組,同為3的兩個為一組,……
int Bak_Odd_Grouping[MAX_NODE]; // 最好情況下分組策略的備份,因為可能還有其他情況更好,如果有,就更新此備份。
int SHORTEST_PATH_WEIGHT(COST_NO_LINK); // 如果存在奇度數點,這里是記錄的添加最短路徑的最小值,是所有點兩兩分組的最短路徑之和的最小值。此值對應Bak_Odd_Grouping所描述的分組情況。
int Dist[MAX_NODE]; // Dijstra算法中,求從v0到v1最短路徑結果,里面包含v0到最短路徑上各點的最短權值
int ShortCache[MAX_NODE][MAX_NODE]; // Dijstra算法中,求點v0到v1的最短路徑記錄值,當第一次求時,把結果存到本數組中,下次如果還在相同調用,則直接返回本數組中相應值。
// 數據輸入,會用到Graph和Cost
void Input()
{
int i, j;
int m, n;
char cs, cm, cn;
int w;
cout << "輸入圖的頂點數:";
cin >> V_dingdianshu;
cout << "輸入圖邊的數目:";
cin >> E_bianshu;
cout << "輸入起點:";
cin >> cs;
Start_Point = cs - 'a';
for(i = 0; i < V_dingdianshu; i++)
{
for(j = 0; j < V_dingdianshu; j++)
{
Graph[i][j] = 0;
ShortCache[i][j] = 0;
Cost[i][j] = COST_NO_LINK;
}
Cost[i][i] = 0; // 置自己到自己為0
}
cout << "輸入" << E_bianshu << "條邊對應的頂點和權值(頂點從a開始編號):" << endl;
for(i = 0; i < E_bianshu; i++)
{
cin >> cm >> cn >> w;
m = cm - 'a';
n = cn - 'a';
Graph[m][n] += 1;
Graph[n][m] += 1;
Cost[m][n] = w;
Cost[n][m] = w;
}
}
// 對圖G從v0點開始到v1點算到必要各點的最短距離,結果保存在dist上面。因此不保證dist上的所有數據都是正確的(保證dist[v]是從v0到v的最短距離)
// 第三個參數指定是否使用Cache值,如果為真,則一般Dist的值不與當前調用相對應,反之則保證dist[v]是從v0到v的最短距離
// 返回dist[v1],即v0到v1的最短距離值
int Dijstra(int v0, int v1, bool useCache)
{
if(useCache && ShortCache[v0][v1] != 0) // 之前計算過了,直接返回值
{
return ShortCache[v0][v1];
}
int i, s, w, min, minIndex;
bool Final[MAX_NODE];
// 初始化最短路徑長度數據,所有數據都不是最終數據
for (s = 0; s < V_dingdianshu; s++)
{
Final[s] = false;
Dist[s] = COST_NO_LINK; // 初始最大距離
}
// 首先選v0到v0的距離一定最短,最終數據
Final[v0] = true;
Dist[v0] = 0;
s = v0; // 0 預先選中v0點
for (i = 0; i < V_dingdianshu; i++)
{
// 1 更新該點到其他未選中點的最短路徑
for(w = 0; w < V_dingdianshu; w++)
{
if(!Final[w] // w點未選中
&& Cost[s][w] < COST_NO_LINK // 更新點應該與選中點s相連
&& Dist[w] > Dist[s] + Cost[s][w]) // 通過點s會有更短的路徑
{
if(Dist[s] + Cost[s][w] <= 0)
{
cout << "求最短路徑數據溢出。" << endl;
exit(-1);
}
Dist[w] = Dist[s] + Cost[s][w];
}
}
// 1.5 如果在中間過程找到了目標點v1,則不再繼續計算了
if(s == v1)
{
ShortCache[v0][v1] = Dist[s];
ShortCache[v1][v0] = Dist[s];
return Dist[s];
}
// 2 選中相應點
min = COST_NO_LINK;
for(w = 0; w < V_dingdianshu; w++)
{
if(!Final[w] // 未選中
&& Dist[w] < min) // 值更小
{
minIndex = w;
min = Dist[w];
}
}
s = minIndex;
Final[s] = true;
}
cerr << "程序異常。。。應該早找到了最短路徑的" << endl;
exit(-1);
}
// 圖的連通性測試
// 參數start用於指定從哪個點開始找(索引從0開始),這樣在一定程序上可以提高程序效率
// 空圖返回真
// 這里對start功能的定義還應該加上:start點一定要在連通圖上
bool ConnectivityTest(int start, bool& bNoPoints)
{
set<int> nodeSet; // 連通頂點集
vector<int> for_test_nodes; // 與新加入連通點連通的未加入點集
int i, j;
set<int> singlePoints; // 圖中的單點集
// 先找出單點
bool hasEdge = false;
for(i = 0; i < V_dingdianshu; i++)
{
hasEdge = false;
for(j = 0; j < V_dingdianshu; j++) // 這里起始應該是0,不然最后一個點如果是單點則無法判斷
{
if (Graph[i][j] > 0)
{
hasEdge = true;
break;
}
}
if (!hasEdge)
{
singlePoints.insert(i);
}
}
bNoPoints = (singlePoints.size() == V_dingdianshu); // 設置bNoPoints標志
if(singlePoints.find(start) != singlePoints.end()) // start點必須在連通圖中
{
return false;
}
for_test_nodes.push_back(start); //
while(for_test_nodes.size() > 0)
{
int testNode = for_test_nodes.back();
for_test_nodes.pop_back();
for(i = 0; i < V_dingdianshu; i++)
{
if(Graph[testNode][i] > 0)
{
if(nodeSet.insert(i).second)
{
for_test_nodes.push_back(i);
}
}
}
}
for(i = 0; i < V_dingdianshu; i++)
{
if (singlePoints.find(i) == singlePoints.end()
&& nodeSet.find(i) == nodeSet.end())
// 存在點既不是單點,也不在當前連通頂點集中,則這個點一定在其他連通子圖中,返回假
{
return false;
}
}
return true;
}
// 測試圖中是否有度為奇的頂點,結果保存在中,返回奇度頂點數
int OddTest()
{
int i, j, rSum, count;
// 初始化
for(i = 0; i < V_dingdianshu; i++)
{
Odd_Grouping[i] = 0; // 0表示不為奇
Bak_Odd_Grouping[i] = 0;
}
count = 0;
for(i = 0; i < V_dingdianshu; i++)
{
rSum = 0;
for(j = 0; j < V_dingdianshu; j++)
{
rSum += Graph[i][j]; // 求i行和
}
if(rSum % 2 == 1)
{
Odd_Grouping[i] = 1;
count++;
}
}
return count;
}
void Bak_Grouping()
{
int i;
for(i = 0; i < V_dingdianshu; i++)
{
Bak_Odd_Grouping[i] = Odd_Grouping[i];
}
}
// 對奇度頂點進行分組,level值從2開始取值。
// 返回值表示當前這種分組是否是當前所找到中的最好分組。本程序中沒有采用其返回值。
bool Grouping(int level)
{
if(level < 2)
{
cerr << "小於2的level值是不允許的。" << endl;
exit(-1);
}
int i, j, findI = -1;
for(i = 0; i < V_dingdianshu; i++)
{
if(Odd_Grouping[i] == 1)
{
Odd_Grouping[i] = level; // 找到第一個組合點。
findI = i;
break;
}
}
bool re = true;
if(findI == -1) // 這里是形成一對新的組合后的地方,此時應該計算各組合最小路徑之和。
{
int weightSum = 0;
for(i = 2; i < level; i++) // 根據level的值可以知道分組的取值是從2到level-1的,所以i如是計數
{
int index[2];
int *pIndex = index;
for(j = 0; j < V_dingdianshu; j++)
{
if(Odd_Grouping[j] == i)
{
*pIndex = j;
if(pIndex == index + 1) // 設置了第二個index值
{
break;
}
pIndex++;
}
}
weightSum += Dijstra(index[0], index[1], true); // 這里暫時只計算最短路權值和,不實際上添加邊,最后才添加。這樣加邊計算只會調用一次。
}
if(weightSum < SHORTEST_PATH_WEIGHT) // 當前組合比以往要優,將當前的排列組合情況更新到全局
{
Bak_Grouping(); // 如果當前分組比以往都好,備份一下
SHORTEST_PATH_WEIGHT = weightSum;
return true; // 找到了更優組合,返回遞歸調用為真
}
else
{
return false; // 沒找到了更優組合,返回遞歸調用為假
}
}
else if(findI > -1)
{
// 上面找到了第一個點了,現在從上面繼續找第二個點。
for(/* 繼續上面的for */; i < V_dingdianshu; i++)
{
if(Odd_Grouping[i] == 1) // 找到第二個點
{
Odd_Grouping[i] = level;
re = Grouping(level + 1);
Odd_Grouping[i] = 1; // 無論當前分組是不是當前最好分組,我們都還要繼續查找剩余分組情況
}
}
}
else
{
cerr << "findCount值異常" << endl;
exit(-1);
}
if(findI > -1)
{
Odd_Grouping[findI] = 1; // 無論當前分組是不是最好分組,我們都還要繼續查找剩余分組情況
}
return re;
}
void AddShortPath(int from, int to)
{
int i, back;
Dijstra(from, to, false); // 求最短路徑,結果在dist數組中
back = to;
while(back != from) // from ... back ... to
{
for(i = 0; i < V_dingdianshu; i++)
{
if(i != back
&& Dist[i] < COST_NO_LINK // from有邊到i
&& Dist[back] < COST_NO_LINK // from有邊到back
&& Dist[i] + Cost[i][back] == Dist[back]) // from通過中繼點i再到back的長度恰好等於from到back的長度,即證明點i在最短路徑上(注,這里如果(i,back)沒有邊連接,那么Dist[i] + Cost[i][back]一定為負數)
{
Graph[i][back]++; // 添加一條邊
Graph[back][i]++;
back = i;
break;
}
}
if(i == V_dingdianshu) // 編程常識:這里break后不會再執行++
{
cerr << "程序異常,最短路徑出問題了。。。" << endl;
exit(-1);
}
}
}
// 根據odd數組的分組情況添加最短路徑
void AddShortPaths()
{
int i, j;
for(i = 0; i < V_dingdianshu; i++)
{
if(Bak_Odd_Grouping[i] > 1)
{
for(j = i + 1; j < V_dingdianshu; j++)
{
if(Bak_Odd_Grouping[j] == Bak_Odd_Grouping[i])
{
AddShortPath(i, j);
break;
}
}
}
}
}
// 處理圖中可能存在度為奇的情況
void OddDeal()
{
// 判斷是否存在為奇的點,有的話要處理
int oddCount = OddTest();
if(oddCount > 0)
{
if(oddCount % 2 == 1)
{
cerr << "這是一個奇怪的圖,存在奇數個奇度頂點的連通圖嗎?" << endl;
exit(-1);
}
// 對為奇的點進行排列組合。。。
Grouping(2); // 這里得到的odd2是最優的
AddShortPaths(); // 根據odd數組添加最短路徑
}
}
/*
用Fleury算法求最短歐拉回游
假設跡wi=v0e1v1…eivi已經選定,那么按下述方法從E-{e1,e2,…,ei}中選取邊ei+1:
1)、 ei+1與vi+1相關聯;
2)、除非沒有別的邊可選擇,否則 ei+1不能是Gi=G-{e1,e2,…,ei}的割邊。
3)、 當(2)不能執行時,算法停止。
*/
void Fleury(int start)
{
int i;
int vi = start; // v0e1v1…eivi已經選定
bool bNoPoints, bCnecTest;
cout << "你要的結果:";
while(true)
{
// 找一條不是割邊的邊ei+1
for(i = 0; i < V_dingdianshu; i++)
{
if (Graph[vi][i] > 0)
{
// 假設選定(vi,i)這條邊
Graph[vi][i]--; // 這里會破壞全局Graph的值,但暫時沒影響了,都不用了。
Graph[i][vi]--;
bCnecTest = ConnectivityTest(i, bNoPoints);
if(!bNoPoints && !bCnecTest) // 這里一定要傳i,這是欲選擇邊的末端,它應該在連通圖中
{
Graph[vi][i]++;
Graph[i][vi]++;
continue;
}
// 選定(vi,i)這條邊
cout << (char)('a' + vi) << "-" << (char)('a' + i) << " ";
vi = i;
break;
}
}
if (i == V_dingdianshu)
{
cout << endl;
break; // 這里應該是說邊找完了
}
}
}
int main()
{
Input();
bool b;
if(!ConnectivityTest(0, b)) // b是無用變量,這里不看。
{
cout << "該圖不是連通圖!\n";
exit(0);
}
OddDeal(); // 處理可能的奇度點情況
Fleury(Start_Point); // 這里應該用這個算法求歐拉回游
return 0;
}
