一、TSP问题
TSP问题(Travelling Salesman Problem)即旅行商问题,又译为旅行推销员问题、货郎担问题,是数学领域中著名问题之一。假设有一个旅行商人要拜访n个城市,他必须选择所要走的路径,路径的限制是每个城市只能拜访一次,而且最后要回到原来出发的城市。路径的选择目标是要求得的路径路程为所有路径之中的最小值。
TSP问题是一个组合优化问题。该问题可以被证明具有NPC计算复杂性。TSP问题可以分为两类,一类是对称TSP问题(Symmetric TSP),另一类是非对称问题(Asymmetric TSP)。所有的TSP问题都可以用一个图(Graph)来描述:
V={c1, c2, …, ci, …, cn},i = 1,2, …, n,是所有城市的集合.ci表示第i个城市,n为城市的数目;
E={(r, s): r,s∈ V}是所有城市之间连接的集合;
C = {crs: r,s∈ V}是所有城市之间连接的成本度量(一般为城市之间的距离);
如果crs = csr, 那么该TSP问题为对称的,否则为非对称的。
一个TSP问题可以表达为:
求解遍历图G = (V, E, C),所有的节点一次并且回到起始节点,使得连接这些节点的路径成本最低。
二、模拟退火算法
模拟退火法是由 Metropolis 于1953 年提出的,是一种基于热力学原理的随机搜索算法,是局部搜索算法的拓展。它与局部搜索算法的不同之处在于:它以一定的概率选择邻域中目标函数值差的状态。
退火是一种物理过程,一种金属物体在加热至一定的温度后,它的所有分子在其状态空间中自由运动。随着温度的下降,这些分子逐渐停留在不同的状态。在温度最低时,分子重新以一定的结构排列。统计力学的研究表明,在同一个温度,分子停留在能量最小的状态的概率比停留在能量大的状态的概率要大。当温度相当高时,每个状态的概率基本相同,都接近平均值。当温度趋向0时,分子停留在最低能量状态的概率趋向于1。
模拟退火算法是一种基于上述退火原理建立的随机搜索算法。组合优化问题与金属物体的退火过程可进行如下类比:组合优化问题的解类似于金属物体的状态,组合优化问题的最优解类似于金属物体的能量最低的状态,组合优化问题的费用函数类似于金属物体的能量。
为了克服局部搜索算法极易陷入局部最优解的缺点,模拟退火算法使用基于概率的双方向随机搜索技术:当基于邻域的一次操作使当前解的质量提高时,模拟退火算法接受这个被改进的解作为新的当前解;在相反的情况下,算法以一定的概率exp(-ΔC/T)接受相对于当前解来说质量较差的解作为新的当前解,其中Δc为邻域操作前后解的评价值的差,T为退火过程的控制参数(即温度)。模拟退火算法已在理论上被证明是一种以概率1收敛于全局最优解的全局优化算法。
模拟退火算法的实施步:
三、模拟退火算法求解TSP问题
在该JAVA实现中我们选择使用tsplib上的数据att48,这是一个对称TSP问题,城市规模为48,其最优值为10628.其距离计算方法下图所示:
具体代码如下:
package noah; import java.io.BufferedReader; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStreamReader; import java.util.Random; public class SA { private int cityNum; // 城市数量,编码长度 private int N;// 每个温度迭代步长 private int T;// 降温次数 private float a;// 降温系数 private float t0;// 初始温度 private int[][] distance; // 距离矩阵 private int bestT;// 最佳出现代数 private int[] Ghh;// 初始路径编码 private int GhhEvaluation; private int[] bestGh;// 最好的路径编码 private int bestEvaluation; private int[] tempGhh;// 存放临时编码 private int tempEvaluation; private Random random; public SA() { } /** * constructor of GA * * @param cn * 城市数量 * @param t * 降温次数 * @param n * 每个温度迭代步长 * @param tt * 初始温度 * @param aa * 降温系数 * **/ public SA(int cn, int n, int t, float tt, float aa) { cityNum = cn; N = n; T = t; t0 = tt; a = aa; } // 给编译器一条指令,告诉它对被批注的代码元素内部的某些警告保持静默 @SuppressWarnings("resource") /** * 初始化Tabu算法类 * @param filename 数据文件名,该文件存储所有城市节点坐标数据 * @throws IOException */ private void init(String filename) throws IOException { // 读取数据 int[] x; int[] y; String strbuff; BufferedReader data = new BufferedReader(new InputStreamReader( new FileInputStream(filename))); distance = new int[cityNum][cityNum]; x = new int[cityNum]; y = new int[cityNum]; for (int i = 0; i < cityNum; i++) { // 读取一行数据,数据格式1 6734 1453 strbuff = data.readLine(); // 字符分割 String[] strcol = strbuff.split(" "); x[i] = Integer.valueOf(strcol[1]);// x坐标 y[i] = Integer.valueOf(strcol[2]);// y坐标 } // 计算距离矩阵 // ,针对具体问题,距离计算方法也不一样,此处用的是att48作为案例,它有48个城市,距离计算方法为伪欧氏距离,最优值为10628 for (int i = 0; i < cityNum - 1; i++) { distance[i][i] = 0; // 对角线为0 for (int j = i + 1; j < cityNum; j++) { double rij = Math .sqrt(((x[i] - x[j]) * (x[i] - x[j]) + (y[i] - y[j]) * (y[i] - y[j])) / 10.0); // 四舍五入,取整 int tij = (int) Math.round(rij); if (tij < rij) { distance[i][j] = tij + 1; distance[j][i] = distance[i][j]; } else { distance[i][j] = tij; distance[j][i] = distance[i][j]; } } } distance[cityNum - 1][cityNum - 1] = 0; Ghh = new int[cityNum]; bestGh = new int[cityNum]; bestEvaluation = Integer.MAX_VALUE; tempGhh = new int[cityNum]; tempEvaluation = Integer.MAX_VALUE; bestT = 0; random = new Random(System.currentTimeMillis()); System.out.println(cityNum+","+N+","+T+","+a+","+t0); } // 初始化编码Ghh void initGroup() { int i, j; Ghh[0] = random.nextInt(65535) % cityNum; for (i = 1; i < cityNum;)// 编码长度 { Ghh[i] = random.nextInt(65535) % cityNum; for (j = 0; j < i; j++) { if (Ghh[i] == Ghh[j]) { break; } } if (j == i) { i++; } } } // 复制编码体,复制编码Gha到Ghb public void copyGh(int[] Gha, int[] Ghb) { for (int i = 0; i < cityNum; i++) { Ghb[i] = Gha[i]; } } public int evaluate(int[] chr) { // 0123 int len = 0; // 编码,起始城市,城市1,城市2...城市n for (int i = 1; i < cityNum; i++) { len += distance[chr[i - 1]][chr[i]]; } // 城市n,起始城市 len += distance[chr[cityNum - 1]][chr[0]]; return len; } // 邻域交换,得到邻居 public void Linju(int[] Gh, int[] tempGh) { int i, temp; int ran1, ran2; for (i = 0; i < cityNum; i++) { tempGh[i] = Gh[i]; } ran1 = random.nextInt(65535) % cityNum; ran2 = random.nextInt(65535) % cityNum; while (ran1 == ran2) { ran2 = random.nextInt(65535) % cityNum; } temp = tempGh[ran1]; tempGh[ran1] = tempGh[ran2]; tempGh[ran2] = temp; } public void solve() { // 初始化编码Ghh initGroup(); copyGh(Ghh, bestGh);// 复制当前编码Ghh到最好编码bestGh bestEvaluation = evaluate(Ghh); GhhEvaluation = bestEvaluation; int k = 0;// 降温次数 int n = 0;// 迭代步数 float t = t0; float r = 0.0f; while (k < T) { n = 0; while (n < N) { Linju(Ghh, tempGhh);// 得到当前编码Ghh的邻域编码tempGhh tempEvaluation = evaluate(tempGhh); if (tempEvaluation < bestEvaluation) { copyGh(tempGhh, bestGh); bestT = k; bestEvaluation = tempEvaluation; } r = random.nextFloat(); if (tempEvaluation < GhhEvaluation || Math.exp((GhhEvaluation - tempEvaluation) / t) > r) { copyGh(tempGhh, Ghh); GhhEvaluation = tempEvaluation; } n++; } t = a * t; k++; } System.out.println("最佳长度出现代数:"); System.out.println(bestT); System.out.println("最佳长度"); System.out.println(bestEvaluation); System.out.println("最佳路径:"); for (int i = 0; i < cityNum; i++) { System.out.print(bestGh[i] + ","); if (i % 10 == 0 && i != 0) { System.out.println(); } } } /** * @param args * @throws IOException */ public static void main(String[] args) throws IOException { System.out.println("Start...."); SA sa = new SA(48, 40, 400, 250.0f, 0.98f); sa.init("c://data.txt"); sa.solve(); } }
运行结果截图:
四、总结
模拟算法其特点是在开始搜索阶段解的质量提高比较缓慢,但是到了迭代后期,它的解的质量提高明显,所以如果在求解过程中,对迭代步数限制比较严格的话,模拟退火算法在有限的迭代步数内很难得到高质量的解。总体而言模拟退火算法比较适合用于有充足计算资源的问题求解。
(截止目前,关于TSP问题系列目前已经使用了爬山、禁忌、蚁群和退火四种算法来求解,计划还有一篇遗传算法的,敬请各位期待!)
注:本文部分内容来源于网络,但程序以及分析结果属于本人成果,转载请注明!