背景知识
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并没有严格的要求,因此每个文件在格式上都可能有些微妙的区别,这就为解析器的实现提出了一些小小的挑战。好在我们还有正则表达式可以比较轻松的解决这些问题。
解析器实现
无论是什么算法,真正需要的信息只有城市规模的维度和各个城市间的距离,如何解析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之后的内容。正则表达式本身并不复杂,相信大家应该能看明白,需要注意的地方是空格和换行的匹配。捕获到的TYPE类型用于判断文件是否是个tsp类型文件,DIMENSION用于确定数组的大小,EDGE_WEIGHT_TYPE用于判断距离类型,这里我简单的用了个switch语句。不同的类型需要不同的处理,这里我只实现了EUC_2D与EXPLICIT类型,其余的大家可以参考论文自行实现。如果是EXPLICIT类型,则还需要判断EDGE_WEIGHT_FORMAT,这里我实现了对称全矩阵和下三角矩阵两种情形,其余的大家也可自行实现。Main函数用于测试。最终得到的distMatrix就可以为各种算法所用了。比起常规的判断,正则表达式强大的表达力使得我们关心的信息的抽取变得十分容易。
最后提供一个完整的TSP实验工具https://github.com/natsu1211/JikkenTsp,支持多种算法以及图形实时生成。