1. 程式人生 > 實用技巧 >蠻力法解 TSP 問題

蠻力法解 TSP 問題

目錄

蠻力法

蠻力法也稱窮舉法或列舉法,是一種簡單直接地解決問題的方法,常常直接基於問題的描述,所以蠻力法也是最容易應用的方法。蠻力法所依賴的基本技術是遍歷,也稱掃描,即採用一定的策略依次理待求解問題的所有元素,從而找出問題的解。依次處理所有元素是蠻力法的關鍵,為了避免陷人重複試探,應保證處理過的元素不再被處理。

TSP 問題

TSP 問題(Traveling Salesman Problem)又譯為旅行推銷員問題、貨郎擔問題,是數學領域中著名問題之一。假設有一個旅行商人要拜訪 n 個城市,他必須選擇所要走的路徑,路徑的限制是每個城市只能拜訪一次,而且最後要回到原來出發的城市。路徑的選擇目標是要求得的路徑路程為所有路徑之中的最小值。——

百度百科

使用蠻力法求解 TSP 問題的思想是,通過窮舉的方式把所有可能的路徑找出來,然後對每一條路徑都計算開銷,最終找出開銷最小的路徑。

例如對於如圖 4 個城市的拓撲,使用蠻力法求解的過程如表格所示。

序號 路徑 路徑長度 是否最短
1 a->b->c->d->a 18
2 a->b->d->c->a 11
3 a->c->b->d->a 23
4 a->c->d->b->a 11
5 a->d->b->c->a 23
6 a->d->c->b->a 18

當城市規模增大時,存在的路徑數會呈現指數型增長,例如 11 個城市的拓撲圖如下所示。

實驗程式編寫

圖結構體定義

TSP 是個 NP 完全問題,我們需要圖結構來進行儲存。我選擇鄰接矩陣儲存城市拓撲圖,定義的圖結構體如下。

typedef struct    //圖的定義
{
      int edges[MAXV][MAXV];    //鄰接矩陣
      int n;    //頂點數
} MGraph;

城市拓撲的建立

接下來就需要把城市拓撲儲存在鄰接矩陣中,因為城市拓撲是完全圖,因此我們需要儲存所有城市之間的距離。

MGraph CreateMGraph(int num)    //建圖 
{
	MGraph topography;
	
	for (int i = 1; i <= num; i++)
	{
		for (int j = 1; j <= num; j++)
		{
			topography.edges[i][j] = 0;
		}
	}
	for (int i = 1; i <= num; i++)
	{
		for (int j = i + 1; j <= num; j++)
		{
			printf("城市%d和城市%d之間的距離為:",i,j);
                        cin >> topography.edges[i][j];
                        topography.edges[j][i] = topography.edges[i][j];
		}
	}
	topography.n = num;
	return topography;
}

DFS

想要獲取最短的路線,使用蠻力法進行分析時需要先獲取所有的路徑。DFS 可以獲取所有的路徑,編寫的程式碼如下。注意當獲取一條新路徑時,需要先把該路徑拷貝到下一個路徑,因為遞迴實現的 DFS 無法返回上一層遞迴執行填充操作。這麼做是可行的,因為相鄰路徑不需要回溯的路線是一樣的,而回溯的部分會直接覆蓋原來的路線。

void DFS(int new_point, int cities_visited, int &path_index)    //深度遍歷 
{
	count++;
	if (cities_visited == topography.n)    //所有城市都走一遍 
	{
        path[path_index][cities_visited] = new_point;
        path[path_index][cities_visited + 1] = start_point;    //回到出發點
        for(int i = 1; i <= topography.n; i++)
        {
            path[path_index + 1][i] = path[path_index][i];    //下一條路徑拷貝上一條 
        }
        path_index++;
	}
	else
	{
		for (int i = 1; i <= topography.n; i++)
		{
			if (visited[i] == 0)
			{
				visited[i] = 1;
                                path[path_index][cities_visited] = new_point;
                                DFS(i, cities_visited + 1, path_index);
                                visited[i] = 0;    //回溯到上一城市 
			}
		}
	}
	return;
}

主函式

接下來要計算所有路徑的長度,並且得出最短的路線。同時我們也需要確定 DFS 執行了多少次,方便我們分析時間複雜度。

int main()
{
	int cities_num = 0;    //城市數量 
	int path_num = 1;    //路徑數 
	int cities_visited = 1;    //已訪問城市數
	int path_index = 1;    //已獲取的路徑數 
	int min_path = 0;
	int min_sum = 9999999;
	int sum; 
	
	cout << "城市數量為:"; 
	cin >> cities_num;
	//建圖
	topography = CreateMGraph(cities_num);
	for(int i = cities_num - 1; i > 1; i--)
	{
		path_num *= i;
	} 
        //初始化訪問狀態 
	for(int i = 1; i <= topography.n; i++) 
	{
		visited[i] = 0;
	}
	//出發 
	cout << "從哪個城市出發:";
	cin >> start_point; 
	visited[start_point] = 1;
	//獲取所有路徑 
	DFS(start_point, cities_visited, path_index);
	//得出最短路徑 
	ofstream outfile;
	outfile.open("11.txt");
	for (int i = 1; i < path_index; i++)
	{
		sum = 0;
		outfile << "路徑" << i << ":"; 
		for (int j = 1; j <= cities_num; j++)
		{
			sum += topography.edges[ path[i][j] ][ path[i][j + 1] ];
		}
		if(sum < min_sum)
		{
			min_sum = sum;
			min_path = i;
		}
	}
	cout << "\n最短路徑為路徑" << min_path << ":"; 
	for (int j = 1; j <= cities_num; j++)
	{
		cout << path[min_path][j] << " -> ";
		outfile << path[min_path][j] << " -> ";
		sum += topography.edges[ path[min_path][j] ][ path[min_path][j + 1] ];
	} 
	cout << path[min_path][cities_num + 1] << endl;
	cout << "最短路徑長度為:" << min_sum << endl;
	cout << "DFS 次數為:" << count;
        return 0;
}

獲取實驗資料

使用上述不同規模的城市拓撲分析TSP問題,得出的實驗資料如下。

城市規模(個) 路線數(條) DFS次數(次) 資料檔案大小(KB)
4 6 16 1
5 24 65 2
6 120 326 7
7 720 1957 45
8 5070 13700 348
9 40320 109601 3050
10 362880 986410 30260
11 3628800 9864101 330943



實驗資料分析

當使用蠻力法解決TSP問題時,需要考慮從某個城市出發的所有路線。由於城市之間彼此互通,城市拓撲是個完全圖,因此所有路線的數量規模是 n-1 個城市的全排列。
當輸入城市數量n時,會產生n!條路線,從而計算路徑長度的操作就需要執行n!次。也就是說蠻力法解決TSP 問題的 T(n) = n!,從而得出 O(n) = n!。無論是路線數、DFS 次數還是資料檔案大小,都能明顯地體現這個趨勢。

路線數


DFS 次數


資料檔案大小


總結

我一開始使用的是 C++ 的 new 運算子動態記憶體分配二維陣列。但是除了城市規模 4 的資料下,其他的規模均無法正常執行,並且主函式 “return value 3221225477”。經過查閱資料得知這可能和未初始化的變數或指標引發的,但是我並不知道問題程式碼及其原因,無奈之下只好直接定義了一個較大的二維陣列進行儲存。
在測試城市規模 12 的資料時,由於棧區空間已經用盡,我打算使用動態記憶體分配使用堆區記憶體。結果需要分配的記憶體過多,導致所有記憶體空間全部被 C++ 佔用,而 C++ 並沒有智慧保護記憶體的機制,導致我的電腦直接宕機。在強行斷電並修復電腦之後覺定放棄 12 個城市的 TSP 問題求解。
蠻力法是解決問題明確而直接的手法,程式編寫較為簡單,思路是模擬情景下的所有可能性進行分析,在時間允許下是極佳的演算法。但是在不同的情景下會存在效率低下的情況,我們會需要更加巧妙的演算法提高解決問題的效率,期待接下來對演算法的進一步學習,使用其他的演算法對這些問題進行求解。

參考資料

《資料結構(C語言版|第二版)》—— 嚴蔚敏 李冬梅 吳偉民 編著,人民郵電出版社
《演算法設計與分析(第二版)》——王紅梅,胡明 編著,清華大學出版社
Graphviz 安裝並使用 (Python)
C++檔案和流
百度百科:TSP問題