旅行商问题2-OPT算法的并行与优化

旅行商问题2-OPT算法的并行与优化

GCC-6.2.0

OpenMPI/2.0.0

OpenMp 4.5 (2015-11)

本文代码下载

介绍

废话不多说,查阅下面链接。

旅行商问题-百度百科

2-OPT贪心算法-百度百科

串行2-OPT的思路如下:

假如我们有{0, 1, 2, 3, 4, 5}这5个城市,初始路线为

0-1-2-3-4-5-0。我们通过两个指针i和k各指向一个城市进行遍历。每次遍历,我们会尝试对当前的**(i,k)对**做一次2optSwap(后面简称reverse),将ik之间的路线反转。

这个操作将会重复,直到我们无法找到更短的路径,我们便找到了局部最优解,也就是最终的结果。

  • 例如我们遍历到i=1, k=3。我们对这个(i,k)对进行一次reverse,得到一个新路线:0-3-2-1-4-5-0,如果新路线可以比之前的路线更短,我们就记下当前的i和k,以及reverse之后能缩短的距离。然后我们继续遍历,最后选择最短的那一条结果进行reverse。
  • 这里原版的串行化2-opt会在找到第一个可以缩短路径的(i, k)之后马上reverse,然后将i和k设为初始值(i=1,k=2)从头开始遍历,并不会每次遍历所有城市然后取当前的最优解。如果使用原版,我们将无法对比并行优化的结果,因为每次的跳转完全随机且取决于具体的数据。
  • 在计算新旧路线的距离差时有一个细节,即我们不需要计算路线的总长。以上面i=1,k=3为例,我们只需要计算distance(0, 1), distance(0, 3), distance(1, 4), distance(3, 4)。而其他路线是不变的。

2-opt-WikiPedia的伪代码,

//最直观的伪代码
procedure 2optSwap(route, i, k) {
    1. 将 route[0] 到 route[i-1] 按顺序加入new_route
    2. 将 route[i] 到 route[k] 倒序加入new_route
    3. 将 route[k+1] 及之后的元素按顺序加入new_route
    return new_route;
}
//稍微贴近实现的伪代码
repeat until no improvement is made {
    start_again:
    best_distance = calculateTotalDistance(existing_route)
    for (i = 1; i <= number of nodes eligible to be swapped - 1; i++) {
        for (k = i + 1; k <= number of nodes eligible to be swapped; k++) {
            new_route = 2optSwap(existing_route, i, k)
            new_distance = calculateTotalDistance(new_route)
            if (new_distance < best_distance) {//如上所述,本文中的实现在这里稍有区别
                existing_route = new_route
                best_distance = new_distance
                goto start_again 
            }
        }
    }
}

串行2-opt的C语言实现,参照伪代码并修改为每次遍历所有城市,选择这次遍历中找到的最短路径进行reverse:

#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include <limits.h>
#include <sys/time.h>
#include <stdint.h>

#define swap(a, b) {float tmp = a;  a = b;  b = tmp;}

void reverse(int len, int* route) {
    for (int i=0; i<len/2; i++) {
        swap(route[i], route[len-1-i]);
    }
}
int get_distance(int x1,int y1, int x2, int y2){
    return (int)sqrt((x2-x1)*(x2-x1) + (y2-y1)*(y2-y1));
}
int TSPDistance(int size, int route[size+1], int x[size], int y[size]) {
    int sum = 0;
    for (int i=1;i<size+1;i++) {
        sum += get_distance(x[route[i-1]], y[route[i-1]], x[route[i]], y[route[i]]);

    }
    return sum;
}
void twoOptTSP(int size, int route[size+1], int x[size], int y[size]) {
    //initial route
    for (int i=0;i<size;i++) {
        route[i] = i;
    }
    route[size]=0;
    int change;
    int minchange;
    int mini;
    int mink;
    int improvement = 0;//c
    LOOP: do {//if negative changes, then it is still improving
        minchange=0;
        mini=0;
        mink=0;
        improvement = 0;
        for (int i = 1; i < size-1; i++) {
            for (int k = i + 1; k < size; k++) {
                change = get_distance(x[route[i-1]], y[route[i-1]], x[route[k]], y[route[k]])
                + get_distance(x[route[i]], y[route[i]], x[route[k+1]], y[route[k+1]])
                - get_distance(x[route[i-1]], y[route[i-1]], x[route[i]], y[route[i]])
                - get_distance(x[route[k]], y[route[k]], x[route[k+1]], y[route[k+1]]);
                
                if (change < minchange) {
                    improvement = 1;
                    minchange=change;
                    mini=i;
                    mink=k; 
                }
            }
        }
        // printf("minchange %d i %d k %d\n", minchange, mini, mink);
        reverse(mink-mini+1, &route[mini]);
    } while(improvement);
    int curr_distance = TSPDistance(size, route, x, y);
    printf("curr_distance %d\n", curr_distance);
}

针对这个串行版本,我们将设计和实现2-OPT的分布式并行算法。算法用C语言实现,我们会使用openMP进行单机多核并行,使用openMPI进行多机并行。

实现思路

并行化这个算法,最直观的切入点便是:将指针i或k的遍历分配给不同的核。

仔细分析后,我们可以发现,因为k的初始值为i+1,所以随着i的增大,k的遍历数量会越来越小。

如果我们将i简单拆分给不同的机器,那么每个机器的工作量是不一样的。而工作量不同,必然导致有的机器先到达同步点,等待其他机器完成计算,造成资源浪费。因此我们要进行负载均衡。

i        | 1 2 3 4 | 5 6 7 8 | 9 10 11 12 | 13 14 15 16 |
machine       A         B          C             D       

本文中使用的方案是:像高斯求和等差数列一样,从头部和尾部同时分派i的任务。这样,每台机器需要计算的(i,k)对是相同的, 如果城市数无法整除机器数,最多只会造成一台机器的资源浪费。即:

i        | 1 2 | 3 4 | 5 6 | 7 8 | 9 10 | 11 12 | 13 14 | 15 16 |
machine     A     B     C     D     D       C       B       A

如果机器是多核,我们可以进一步将k的遍历拆分给每一个核。

在遍历完i,k之后,我们得到了每个核中的最小值。我们需要将他们汇总在机器A,即master上进行比较,获得全局最小值。然后将reverse的结果分发回每个机器。

这里有一个细节,即我们仅发送最后要进行变更的i和k,而不发送master变换后的整个路径。这样可以最大程度减少通信时间。

以上,我们就完成了算法的并行化。接下来我们来对这个并行算法进行针对性优化。

优化

仔细观察后我们就能够发现,2-opt算法中存在大量互相不干扰的(i,k)对(下面简称兼容),遍历一次然后reverse其中最优的一个,和在同一次遍历中reverse多个兼容的ik对,得到的结果是一样的。下面是几个兼容的情况:

(i,k)范围不重叠

假设以下的情况:

我们有路径:0-1-2-3-4-5-6-7-8-9-0,我们在master汇总了三个(i, k)对:(1,2), (4,5), (7,8)。按照上面的算法,我们只会选择那个能得到最短的路径的ik进行reverse。

但实际上,我们换了其中一个ik对,假如是(1,2),reverse后得到0-2-1-3-4-5-6-7-8-9-0。然后我们在下一次遍历后,我们仍然会得到另外两个ik对,即(4,5), (7,8)。而如果我们直接对上面的三个ik同时进行reverse,就节省了两次遍历。

因此,如果我们得到的多个ik之间的范围不重叠,就可以同时进行reverse。

  • 这里有一个细节,如果我们获得的是(1,2), (3,4), 他们看似兼容,实际上需要计算的城市是不同的。
  • reverse(1,2)之前,路径为0-1-2-3-4-5-6-7-8-9-0,计算reverse(3,4)能缩短的距离,需要计算的是distance(2,3), distance(4,5), disntance(2,4), disntance(3,5)
  • reverse(1,2)之后,路径变成了0-2-1-3-4-5-6-7-8-9-0,这时再计算(3,4)能缩短的距离,则需要计算distance(1,3), distance(4,5), distance(1,4), distance(3,5)
(i,k)有包含关系

假如我们还是有路径``0-1-2-3-4-5-6-7-8-9-0`,这次我们在master汇总了这样三个ik对:

(1,6), (3,4), (4,7)。仔细观察会发现,(3,4)是被包含在(1,6)里面的。但是(4,7)又与(1,6)不兼容。如果reverse(4,7)之后路线最短,那我们显然应该直接reverse(4,7),然后进行下一次遍历。可以,并且reverse(1,6)之后路线是最短的,我们是不是可以同时reverse(1,6), (3,4)呢?

我们需要分情况讨论:

首先,我们reverse(1,6)之后的结果是:0-6-5-4-3-2-1-7-8-9-0。那么下一次遍历,我们需要比较reverse(3,4)(4,7)谁更短。

如果(3,4)更短,我们就可以同时reverse(1,6), (3,4),反之则不行。

实现方法

将上面的情况推到一般,我们实际需要做的就是对所有汇总到master的结果按照reverse后路线长度从短到长进行排序。求出从下标0开始的最长子序列,序列中的每一个元素都与在它之前的所有元素兼容。

比如说排序后结果为[(1,2), (5,6), (4,8), (10,11)],我们从(5,6)开始遍历,发现(5,6)与(1,2)兼容,而(4,8)虽然与(1,2)兼容,但不与(5,6)兼容。所以我们就不用再检查(10,11)了,即使它与第一和第二个元素兼容,我们也不能保证在reverse第一和第二个元素后,不会出现比(10,11)更优且不兼容的解。

当然了,如果我们不在意最后的结果是否与串行化算法的结果一致,我们也可以直接reverse所有与第一个元素兼容的元素(因为烦人的死线,本文中仅实现了这种算法,当然这个得到的结果是不那么严谨的)。

伪代码

//parallel pseudocode
2opt(x, y, route):
  do:
    //phase 1
    minchange[number of threads] = (0, 0, 0);//三个值分别为:(change in distance, i, k)
    for ii = 0 to (size-1)/2 do in parallel:
      processor p thread t does:
      i = ii + 1 //负载均衡
      for k = i + 1 to size-1:
        change = get_change(route, i, k)
        if (change < minchange[0])
          minchange[t] = (change, i, k);
      i = size-2-ii //负载均衡
      for k = i + 1 to size-1:
        change = get_change(route, i, k)
        if (change < minchange[t][0])
          minchange = (change, i, k);
    //phase 2
    gather minchange from all processors to processor_results of processor 0
    processor 0 does:
      add changes that can be performed together to send_results
      broadcast send_results to all processors
    //phase 3
    all processor does locally:
      for change in send_results:
        reverse(route, change[1], change[2])
  while there is improvement in distance

      
//寻找与最优解兼容的解
buildChangeMat(idx, minchange, mat):
	If mat[idx].length == 0: 
		add minchange to mat[idx];
		return;
	Check idx:
		If (not compatible): 
			return;
		Else if (compatible): 
			add minchange to mat[idx]; 
			return;
		Else if (inside range of an element): 
			buildChangeMat(element, minchange, mat);

串行版,并行版和编译运行的shell代码,我都贴到文件里,有需要自取。

不太严谨的实验结果

城市数量串行结果 (距离)并行结果(距离)
10002528725077
20003554935373
30004295143108
50005526055240

Table 1 两台机器每个12核的运行结果

这里可以看到结果是不一样的。因为我们实现的是不严谨的版本。

城市数量串行时间(s)并行时间(s)Speedup并行效率
100016.400.5032.801.37
2000133.451.5486.663.61
3000473.464.42107.124.46
50002259.0151.0244.281.85

Table 2 两台机器,每个12核的运行时间

城市数量串行结果(距离)并行结果(距离)
50005526055235

Table 3 四台机器每个12核的运行结果

城市数量串行时间(s)并行时间(s)Speedup并行效率
50002249.8420.81108.112.25

Table 4 四台机器每个12核的运行时间

最后

总的来说,这个算法应该还有优化的空间。目前每个线程只会提供一个(i,k)对。我们可以把它变成两个或者更多,这样我们就有可能获得更多兼容的解。同时兼容的情况也可能不止上文中提到的两种。

我们可以赠机器只有n个局部最小值,我们可能错过了一些可以同时在一轮迭代中reverse的机会。如果能够储存更多的候选路径,我们可能可以获得更大的speedup。

不过,这个并行算法的缺憾也很直观:

  • 一个是speedup取决于数据是否提供了在一轮迭代中进行多次变换的可能,所以speedup并不稳定。不过如果城市数量足够大,肯定会在早期的遍历中获得巨大的speedup。
  • 另一个就是没有实现与串行化结果一致的版本。我们无法获得与串行算法相同的值,也就无法更准确地计算speedup。因为串行和并行的结果不同,所以我们无法确保双方的总计算量相同,有可能并行算法多进行了或者少进行了几轮迭代,才得出了最终的speedup。
  • 不过总的来说,我们也可以看到这个并行算法取得的speedup有时完全超出预期,并行效率(=speedup/总核数)远大于1。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值