標籤:c# Regex tsp
背景知識
TSP即Travelling SalesmanProblem(旅行商人問題)的簡稱。是數學領域中的著名問題之一。有n個城市,一個旅行商人要從其中某一個城市出發,唯一走遍所有的城市,再回到他出發的城市,求最短的路線。這個問題對快遞業等行業也非常具有現實意義,當然現實中的TSP一般是動態,要更為複雜。TSP可以分為兩類,一類是對稱TSP(Symmetric TSP),另一類是非對稱TSP(Asymmetric TSP)。區別就在雩都市a到b和都市b到a的cost是否相等,相等的就是對稱TSP。這裡我們討論的都是對稱TSP。大家應該能想象的到,所有的TSP問題都可以用一個Graph來描述,當然這篇文章主要不是為了討論TSP本身,具體的定義就從略了,大家可以自行尋找。
TSP已經被證明是個NP Hard問題。目前有許多演算法能夠用來解決TSP,比如大家熟悉的動態規劃,以及諸如蟻群演算法的各種進化演算法。為了評價這些演算法的效能,德國Heidelberg University 的Gerhard Reinelt在上世紀90年代在其研究室的首頁上公開了一套TSPLIB(以TSP為主的問題庫),因其在之後被TSP方面的幾篇重要論文所採用,漸漸成為評價TSP演算法公認的BENCHMARK。
連結:http://www.iwr.uni-heidelberg.de/groups/comopt/software/TSPLIB95/
TSPLIB檔案格式
TSPLIB是一個包含了TSP及其相關問題的問題庫。其中的檔案都具有.tsp尾碼。
關於這些檔案的使用,有一篇專門的解說論文(https://docs.google.com/file/d/0B4zUGKjaO9uERU1RZDNuRkg3TW8/edit)。但是解說並不是特別詳細,另外很多同學也不知道這篇論文的存在,我在這裡還是稍微說明一下。
一個典型的TSPLIB檔案,如eil51.tsp,具有如下的格式:
NAME : eil51
COMMENT : 51-city problem (Christofides/Eilon)
TYPE : TSP
DIMENSION : 51
EDGE_WEIGHT_TYPE : EUC_2D
NODE_COORD_SECTION
1 37 52
2 49 49
3 52 64
4 20 26
5 40 30
…
50 56 37
51 30 40
EOF
NAME就是該檔案的名字。
COMMENT是對這個問題的附加說明。
TYPE描述了問題的類型,因為TSPLIB中還包含了一些其他類型的問題,但是這裡我們只關注TSP類型。
DIMENSION描述了城市的數量。
EDGE_WEIGHT_TYPE 描述了兩個城市間cost的類型,這裡是我們最為熟悉的2D歐幾裡得距離。
NODE_COORD_SECTION描述了各個城市的2D歐幾裡得座標。每一行按照城市編號,X座標,Y座標的順序。
但是需要注意的是,EDGE_WEIGHT_TYPE並不是只有EUC_2D一種,而是有13種之多。各種類型有對應的距離計算方法,如曼哈頓距離,地理距離等,這裡我就不一一列舉了,論文中有詳細的敘述。這裡我只單獨提一下出現最多的一種類型EXPLICIT,這種類型和其他的區別較大,城市間的距離是顯式給出的,無需再計算。以gr17.tsp為例:
NAME: gr17
TYPE: TSP
COMMENT: 17-city problem (Groetschel)
DIMENSION: 17
EDGE_WEIGHT_TYPE: EXPLICIT
EDGE_WEIGHT_FORMAT: LOWER_DIAG_ROW
EDGE_WEIGHT_SECTION
0633 0 257 390 0 91 661 228 0 412 227
169383 0 150 488 112 120 267 0 80 572 196
77351 63 0 134 530 154 105 309 34 29 0
259555 372 175 338 264 232 249 0 505 289 262
476196 360 444 402 495 0 353 282 110 324 61
208292 250 352 154 0 324 638 437 240 421 329
297314 95 578 435 0 70 567 191 27 346 83
4768 189 439 287 254 0 211 466 74 182 243
105150 108 326 336 184 391 145 0 268 420 53
239199 123 207 165 383 240 140 448 202 57 0
246745 472 237 528 364 332 349 202 685 542 157
289426 483 0 121 518 142 84 297 35 29 36
236390 238 301 55 96 153 336 0
EOF
需要注意的是,如果EDGE_WEIGHT_TYPE類型為EXPLICIT,那麼就沒有NODE_COORD_SECTION項,而是對應的EDGE_WEIGHT_FORMAT與EDGE_WEIGHT_SECTION,EDGE_WEIGHT_FORMAT指明了資料以何種形式呈現,這裡的LOWER_DIAG_ROW代表著下三角矩陣。也就是說EDGE_WEIGHT_SECTION所列出的資料應當這麼看,
0
633 0
257 390 0
91 661 228 0
…
城市1到城市2的距離就是633,任何城市到自己本身的距離都為0。另外除了下三角矩陣還有全矩陣,上三角矩陣等。
另外,tsp檔案對空格的數量和最終的EOF並沒有嚴格的要求,因此每個檔案在格式上都可能有些微妙的區別,這就為解析器的實現提出了一些小小的挑戰。好在我們還有Regex可以比較輕鬆的解決這些問題。
解析器實現
無論是什麼演算法,真正需要的資訊只有城市規模的維度和各個城市間的距離,如何解析tsp檔案獲得這些資訊,正是我們關心的。下面我給出一個C#實現的簡易解析器,只為了說明問題,代碼本身還有很多可以最佳化重構的地方。
using System;using System.Collections.Generic;using System.Text;using System.Text.RegularExpressions;using System.IO;namespace TspFileParser{ class Program { public static class TspParser { public static void ParseTspFile(string fileName, out int[,] distMatrix) { //string[] lines = {}; string typePattern = @"\nTYPE\s?:\s?(?<type>\w+)"; string dimPattern = @"DIMENSION\s?:\s?(?<dimension>\d+)"; string wtypePattern = @"EDGE_WEIGHT_TYPE\s?:\s?(?<wtype>\w+)"; string formatPattern = @"EDGE_WEIGHT_FORMAT\s?:\s?(?<ftype>\w+)"; //string pattern = @"(^TYPE\s*:\s*(?<type>\w+))|(DIMENSION\s*:\s*(?<dimension>\d+))|(EDGE_WEIGHT_TYPE\s*:\s*(?<wtype>\w+))"; //map dimension int dim = 0; string text = default(string); try { // lines = File.ReadAllLines(fileName); text = File.ReadAllText(fileName); } catch(DirectoryNotFoundException /*DirctNot*/) { Console.WriteLine("Directory is incorrect!"); } catch (FileNotFoundException /*fileNot*/) { Console.WriteLine("File not found!"); } Match problemType = Regex.Match(text, typePattern); if (problemType.Groups["type"].Value != "TSP") throw new Exception("Not a tsp file!");//not handled Match dimension = Regex.Match(text, dimPattern); Match weightType = Regex.Match(text, wtypePattern); string[] textSplit = text.Split(new Char[] {' ', '\n'}); dim = Convert.ToInt32(dimension.Groups["dimension"].Value); distMatrix = new int[dim, dim]; switch (weightType.Groups["wtype"].Value) { case"EXPLICIT": { Match formatType = Regex.Match(text, formatPattern); switch(formatType.Groups["ftypes"].Value) { //symmetrical full matrix case"FULL_MATRIX": { int startPos = 0; for (int i = 0; i < textSplit.Length; ++i) { if (textSplit[i] == "EDGE_WEIGHT_SECTION") { startPos = i; } } for (int j = 0, n = 0; j < dim; ++j) { for (int i = 0; i < dim; ++i) { ++n; distMatrix[i, j] = text[startPos + n]; } } break; } //lower digram matrix case"LOWER_DIAG_ROW": { int startPos = 0; for (int i = 0; i < textSplit.Length; ++i) { if (textSplit[i] == "EDGE_WEIGHT_SECTION") { startPos = i; } } for (int j = 0, n=0; j < dim; ++j) { for (int i = 0; i <=j; ++i) { ++n; distMatrix[i, j] = text[startPos + n]; } } break; } default: break; } break; } case "EUC_2D": { int startPos = 0; //array to store the cordinate of every city Tuple<int, int>[] cityCord = new Tuple<int, int>[dim]; //find the start index of cordinate region for(int i=0; i<textSplit.Length; ++i) { if( textSplit[i] == "NODE_COORD_SECTION") { startPos = i; } } //extract cordinates for (int n=0, i = startPos; n<dim; ++n ) { //plus 2 to jump over the index before cordinate i = i + 2; int x = Convert.ToInt32(textSplit[i]); ++i; int y = Convert.ToInt32(textSplit[i]); cityCord[n] = new Tuple<int, int>(x, y); } //compute distance for (int i = 0, startj = 0; i < dim; ++i) { for (int j = startj; j < dim; ++j) { distMatrix[i, j] = (int)Math.Sqrt((cityCord[i].Item1 - cityCord[j].Item1) * (cityCord[i].Item1 - cityCord[j].Item1) + (cityCord[i].Item2 - cityCord[j].Item2) * (cityCord[i].Item2 - cityCord[j].Item2)); distMatrix[j, i] = distMatrix[i, j]; } ++startj; } break; } default: break; } } } static void Main(string[] args) { //TspParser parser = new TspParser(); Console.WriteLine("Please input the path of Tsp file"); int[,] distance; TspParser.ParseTspFile(Console.ReadLine(),out distance); Console.ReadKey(); } }}
這裡我們只有一個靜態類和一個靜態方法。靜態方法ParseTspFile有兩個參數,fileName是tsp檔案的路徑與名稱,out int[,] distMatrix則是用來儲存城市間距離的二維數組。先將整個檔案讀入為一個string,然後用4種pattern分別捕獲欄位TYPE, DIMENSION, EDGE_WEIGHT_TYPE, EDGE_WEIGHT_FORMAT之後的內容。Regex本身並不複雜,相信大家應該能看明白,需要注意的地方是空格和換行的匹配。捕獲到的TYPE類型用於判斷檔案是否是個tsp類型檔案,DIMENSION用於確定數組的大小,EDGE_WEIGHT_TYPE用於判斷距離類型,這裡我簡單的用了個switch語句。不同的類型需要不同的處理,這裡我只實現了EUC_2D與EXPLICIT類型,其餘的大家可以參考論文自行實現。如果是EXPLICIT類型,則還需要判斷EDGE_WEIGHT_FORMAT,這裡我實現了對稱全矩陣和下三角矩陣兩種情形,其餘的大家也可自行實現。Main函數用於測試。最終得到的distMatrix就可以為各種演算法所用了。比起常規的判斷,Regex強大的表達力使得我們關心的資訊的抽取變得十分容易。
TSPLIB簡介與簡易解析器實現