中國郵路問題編程求解


中國郵路問題(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;
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2026 CODEPRJ.COM