TSP问题描述:

  旅行商问题,即TSP问题(Travelling Salesman Problem)又译为旅行推销员问题、货郎担问题,是数学领域中著名问题之一。假设有一个旅行商人要拜访n个城市,他必须选择所要走的路径,路径的限制是每个城市只能拜访一次,而且最后要回到原来出发的城市。路径的选择目标是要求得的路径路程为所有路径之中的最小值。这篇文章解决的tsp问题最终需要输出所有路程的总长度,如果路并不是联通的,则输出-1。程序的输入描述是:

TSP问题的动态规划解法:

  引用一下这篇文章,觉得作者把动态规划算法讲的非常明白:https://blog.csdn.net/derek_tian/article/details/45057443

动态规划算法的并行:

  终于到了文章的重点,如何高效率的将主问题拆开并分配给各个线程运算是值得研究的话题。这里先引用动态规划算法的示意图:

(图2)

  图中所示的是城市数为4的问题,从第一行到第二行,把问题拆分成了三个子问题,这三个子问题互不影响互不相关。将这三个子问题再一次拆分,每个问题又能拆出两个问题。将这个规律推广到n,城市数为n的问题通过第一次拆分可以变为n-1个子问题,再拆分一次共可以变为(n-1)*(n-2)个子问题。

  有了能把问题拆成互不相干的子问题作为前提,在并行时就并不需为数据频繁的加锁解锁,能比较顺利地完成并行的操作了。接下来是具体的实现手段:

  首先在全局维护唯一一个二维表,所有子线程的读取与修改都是在这一张表上进行的。例程中就是dp变量。

  接下来定义一个函数,用来求解每个子问题,这个时候出现了另一个问题:怎么样表示一个子问题。在动态规划算法中有一个状态压缩的概念,即为用一段二进制码表示城市的集合,例如共五个城市时表示{1, 3, 4}这个集合即为二进制"11010"。相同的思路,对于d(1,{2,3})问题,我们给函数输入起点,以及目标的集合,就可以完整的表示问题。函数内选择的是递归算法,因为非递归算法很难保证子问题互不相关(可能也只是我懒得想,有思路的同学可以在评论区中讨论一下)。具体的动态规划递归实现算法很多文章中都有,我就不过多赘述了。下面是这一部分的源代码:

  *注意:我在写这个函数的时候规定的setMask是包含了起点城市的,也就是对于d(1,{2,3})这个问题传入函数的参数的是(1,0b"1110")。

int TSP( int x, int setMask ) {
int mask, i, minimum;
if ( dp[ setMask ][ x ] != - )
return dp[ setMask ][ x ]; /* if already have value */
mask = << x;
dp[ setMask ][ x ] = 1e9; /* set infinite */
for ( i = ; i < n; ++i ) {
if ( i != x && ( setMask & ( << i ) ) && map[ i ][ x ] != ) { /* if have path */
minimum = TSP( i, setMask - mask ) + map[ i ][ x ] ;
dp[ setMask ][ x ] = min( dp[ setMask ][ x ], minimum);
}
}
return dp[ setMask ][ x ];
}

  现在我们有了表达子问题的方法,就该开始解决具体怎么拆开问题的算法了。问题的要求中说明了最大可以创建的线程数,也就是说为了保证尽可能高的效率,程序中应该将问题拆到数量刚好大过线程数的情况。

  当我们计算出了应该拆分的层数后,就该为产生的一大波子问题生成描述了。

  有了之前定义子问题动态规划的算法为基础,其实可以发现,对于同一个setMask,其产生的同辈问题数是很容易得出来的。就拿刚刚的例子d(1,{2,3})来说,它的setMask是"1110",起点是1号城市。然而,对于与这个问题互为同辈关系的d(2,{1,3}),d(3,{1,3})问题来说,他们的setMask也都是"1110",只不过起点依次是2和3号城市。所以只要生成出了这个集合,其对应的问题都能很容易表示,这也就把工作的重心转义到了如何生成这个集合,也就是例程中是setMask。

  对于从同一个整道题求解的问题分离出来的子问题来说,其城市集合其实是一个组合数。我们还选取图2作为例子,现在经过计算后,我们需要把主问题拆分两层,即拆到d(2,{3})这一层。我们列出所有的集合,分别是"1100", "1010", "0110",发现了吗,其实这些集合也就是剔除了起点城市以后,将两个1与一个0进行组合的所有情况。这个规律同样可以推广到n个城市,如果要将问题拆分k层,所有的问题集合即是固定集合中对应初始点(即程序输入的s)为0将n-k-1个1与k-1个0的所有组合数。数学表示为:

  *注:公式中的${S \choose n-k-1}$表示S集合的n-k-1排列。

$Let\ S=\{0,1,2,\cdots,n-1\}\backslash\{s\}
\\
Then\ let\ C= {S \choose n-k-1}
\\
For\ every\ i \in C, its\ setmask=\overbrace{00\cdots00}^{n}+(1<<i[0])+(1<<i[1])+\cdots+(1<<i[n-k-1])$

  希望我讲清楚了里面的数学关系,如果觉得我讲的有问题或者看不懂的同学可以在评论区留言……以下是上述数学过程的c语言代码,为了逻辑更清晰我将计算组合数和求解所有组合数封成了factorial和setCombination函数:

    while (t > nn) { /* let all thread have work to do */
nn *= (nn - );
nc += ; /* extends layers until t > nn */
}
j = ;
candiNum = malloc(sizeof(int) * (n - ));
for (i = ; i < n; ++i) { /* candiNum set means all cities exclude the start piont*/
if (i != s) {
candiNum[j] = i; /* exclude the start point */
j++;
}
}
combNum = factorial(nc, n - ); /* calculate combination number */
combSet = malloc(sizeof(int) * combNum);
combptr = combSet;
numOf1 = n - - nc; /* there should have `numOf1` of 1 in every item */
setCombination(candiNum, n - , numOf1);

  接下来就是使用pthread创建线程来求解每一个子问题了。其中主函数作为调度线程来给0~t-1个线程依次分配任务。因为基本上可以认为每一个平行的子问题计算的时间是完全一样的,所以只需要先给0~t-1个线程分配0~t-1号任务,再等待到0号线程结束以后,为其分配t号任务,以此类推。当所有的子问题都被解决以后,dp这个二维表中已经有所有我们需要的子问题结论了。此时主问题的答案可以很快通过这些答案计算出来,也没有并行的必要了。最后提供整个程序的代码以及Makefile:

// file tsp.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
#include "tsp.h"
#include <time.h>
#define min(a,b) (((a)<(b))?(a):(b)) /* macro func of min */ time_t tm;
int ** map, ** dp, * combSet, * combptr, * posOf1, n; /* init vars */
trdArgs args[];
pthread_t tid[]; /* thread ids */ int TSP( int x, int setMask ) {
int mask, i, minimum;
if ( dp[ setMask ][ x ] != - )
return dp[ setMask ][ x ]; /* if already have value */
mask = << x;
dp[ setMask ][ x ] = 1e9; /* set infinite */
for ( i = ; i < n; ++i ) {
if ( i != x && ( setMask & ( << i ) ) && map[ i ][ x ] != ) { /* if have path */
minimum = TSP( i, setMask - mask ) + map[ i ][ x ] ;
dp[ setMask ][ x ] = min( dp[ setMask ][ x ], minimum);
}
}
return dp[ setMask ][ x ];
} void * trd(void * SArg) {
TSP((*(trdArgs *)SArg).startPoint, (*(trdArgs *)SArg).setMask);
return NULL;
} void setCombination(int arr[], int n, int r) { /* set all combination into combptr */
int * data = malloc(sizeof(int) * r); /* Temporary array to store current combination */
combinationUtil(arr, data, , n - , , r);
free(data); /* avoid leak */
} void combinationUtil(int arr[], int data[], int start, int end, int index, int r) {
int i, j, tmpi = ; /* init vars */
if (index == r) {
for (j = ; j < r; j++) /* add nums to binay */
tmpi |= ( << data[j]);
*combptr = tmpi;
combptr++; /* next cell in array */
return;
}
for (i = start; i <= end && end - i + >= r - index; i++) { /* recersive */
data[index] = arr[i];
combinationUtil(arr, data, i + , end, index + , r); /* call selfe */
}
} long factorial(int m, int n) { /* caculate combination number */
int i, j;
long ans = ;
if (m < n - m) m = n - m; /* C(m,n)=C(n-m,n) */
for (i = m + ; i <= n; i++) ans *= i;
for (j = ; j <= n - m; j++) ans /= j;
return ans; /* answer */
} int main() {
long combNum;
int t, s, i, j, k, nn, nc, ans, numOf1, * candiNum, * combSet, * tmptr; /* init vars */
nc = ;
scanf("%d %d %d", &t, &n, &s); /* get &t, &n, &s */
if (t < || n <= || n <= s || s < ) {printf("-1\n"); return ;} /* error input */
if (n == ) {printf("0\n"); return ;}
nn = n - ; /* first layer */
map = malloc(sizeof(int *)*n); /* alloc for 2dim array: map */
tmptr = malloc(sizeof(int) * n * n);
for (i = ; i < n; ++i) {
map[i] = tmptr; /* set 2dim to 1dim */
tmptr += n;
}
dp = malloc(sizeof(int *) * ( << n)); /* alloc for 2dim array: map */
tmptr = malloc(sizeof(int) * ( << n) * n);
for (i = ; i < ( << n); ++i) {
dp[i] = tmptr; /* set 2dim to 1dim */
tmptr += n;
}
for (i = ; i < n; i++) /* get the map */
for (j = ; j < n; j++)
scanf("%d", &map[i][j]); /* store */ memset(dp[], -, sizeof(int) * ( << n) * n); /* fill all dp with -1 */
for ( i = ; i < n; ++i )
dp[ << i ][ i ] = map[ ][ i ]; while (t > nn) { /* let all thread have work to do */
nn *= (nn - );
nc += ; /* extends layers until t > nn */
}
j = ;
candiNum = malloc(sizeof(int) * (n - ));
for (i = ; i < n; ++i) { /* candiNum set means all cities exclude the start piont*/
if (i != s) {
candiNum[j] = i; /* exclude the start point */
j++;
}
}
combNum = factorial(nc, n - ); /* calculate combination number */
combSet = malloc(sizeof(int) * combNum);
combptr = combSet;
numOf1 = n - - nc; /* there should have `numOf1` of 1 in every item */
setCombination(candiNum, n - , numOf1);
posOf1 = malloc(sizeof(int) * numOf1); /* arr to store the position of 1 in each comb */
k = ;
for (i = ; i < combNum; ++i) {
tmptr = posOf1;
for (j = ; j < n; ++j) { /* go through all bits */
if ((combSet[i] & ( << j)) != ) {
*tmptr = j;
tmptr++; /* point to next cell */
}
}
for (j = ; j < numOf1; ++j) { /* arrange jobs */
args[k].id = k; /* set thread id arg */
args[k].startPoint = posOf1[j];
args[k].setMask = combSet[i]; /* set thread set mask args */
pthread_join(tid[k], NULL);
pthread_create(&tid[k], NULL, trd, &args[k]); /* new thread */
k = (k + ) % t;
}
}
for (i = ; i < t; ++i)/* join all thread */
pthread_join(tid[i], NULL);
ans = TSP(, ( << n ) - );
if (ans == 1e9)
printf("-1\n");
else
printf("%d\n", ans); /* final answer */
free(dp[]); /* free */
free(dp);
free(map[]); /* free */
free(map);
free(posOf1); /* free */
free(combSet);
free(candiNum); /* free */
return ;
}
// file tsp.h

typedef struct threadArgs {
int id;
int startPoint;
int setMask;
} trdArgs; int TSP( int x, int setMask ); void * trd(void * SArg) ; void combinationUtil(int arr[], int data[], int start, int end,
int index, int r); void setCombination(int arr[], int n, int r) ; long factorial(int m, int n) ;

Makefile:

CC=gcc
CFLAGS=-Wpedantic -Wall -Wextra -Werror -std=c89 -pthread -D_GNU_SOURCE tsp.o: tsp.c tsp.h
${CC} ${CFLAGS} tsp_blog.c -o tsp.o

欢迎没有完全看懂的读者随时提问,文中的例程也不一定是最好的算法,还请有想法的读者能不吝赐教。

多线程动态规划算法求解TSP(Traveling Salesman Problem) 并附C语言实现例程的更多相关文章

  1. 【智能算法】用模拟退火(SA, Simulated Annealing)算法解决旅行商问题 (TSP, Traveling Salesman Problem)

    喜欢的话可以扫码关注我们的公众号哦,更多精彩尽在微信公众号[程序猿声] 文章声明 此文章部分资料和代码整合自网上,来源太多已经无法查明出处,如侵犯您的权利,请联系我删除. 01 什么是旅行商问题(TS ...

  2. 旅行商问题(Traveling Salesman Problem,TSP)的+Leapms线性规划模型及c++调用

    知识点 旅行商问题的线性规划模型旅行商问题的+Leapms模型及CPLEX求解C++调用+Leapms 旅行商问题 旅行商问题是一个重要的NP-难问题.一个旅行商人目前在城市1,他必须对其余n-1个城 ...

  3. 基于贪心算法求解TSP问题(JAVA)

    概述 前段时间在搞贪心算法,为了举例,故拿TSP来开刀,写了段求解算法代码以便有需之人,注意代码考虑可读性从最容易理解角度写,没有优化,有需要可以自行优化! 详细 代码下载:http://www.de ...

  4. 基于爬山算法求解TSP问题(JAVA)

    一.TSP问题 TSP问题(Travelling Salesman Problem)即旅行商问题,又译为旅行推销员问题.货郎担问题,是数学领域中著名问题之一.假设有一个旅行商人要拜访n个城市,他必须选 ...

  5. MIP经典问题:旅行商问题 (traveling salesman problem)

    *本文主要记录和分享学习到的知识,算不上原创. *参考文献见链接. 旅行商问题.背包问题都是0-1规划问题中最为经典的问题. 通常来说,当我们学习并熟悉一种求解混合整数问题的技巧时,可以用这种技巧来求 ...

  6. Complexity and Tractability (3.44) - The Traveling Salesman Problem

    Copied From:http://csfieldguide.org.nz/en/curriculum-guides/ncea/level-3/complexity-tractability-TSP ...

  7. 利用HTML5 Canvas和Javascript实现的蚁群算法求解TSP问题演示

    HTML5提供了Canvas对象,为画图应用提供了便利. Javascript可执行于浏览器中, 而不须要安装特定的编译器: 基于HTML5和Javascript语言, 可随时编写应用, 为算法測试带 ...

  8. TSP(Traveling Salesman Problem)-----浅谈旅行商问题(动态规划,回溯实现)

    1.什么是TSP问题 一个售货员必须访问n个城市,这n个城市是一个完全图,售货员需要恰好访问所有城市的一次,并且回到最终的城市. 城市于城市之间有一个旅行费用,售货员希望旅行费用之和最少. 完全图:完 ...

  9. 蚁群算法求解TSP问题

    一.蚁群算法简介 蚁群算法是对自然界蚂蚁的寻径方式进行模似而得出的一种仿生算法:蚂蚁在运动过程中,能够在它所经过的路径上留下信息素(pheromone)的物质进行信息传递,而且蚂蚁在运动过程中能够感知 ...

随机推荐

  1. 从Windows角度看Mac OS X上的软件开发

    如果原来从事Windows软件开发,想跨足或转换至Mac OS X环境,需要知道那些东西?有什么知识技能可以快速运用在Mac OS X环境上的?这两个问题应该是Windows开发者进入Mac OS X ...

  2. 四、删除 Delete

    文档目录 开始使用  初始化查询实例: LambdaToSql.SqlClient DB = new LambdaToSql.SqlClient(); 删除单个实体,通过Guid主键删除 var gu ...

  3. 北京一家JAVA开发公司面试题(留给后人)

    1.jsp有哪些内置对象?作用分别是什么? 2.描述一下servlet的生命周期和基本架构. 3.多线程有几种实现方法,都是什么? 同步有几种实现方法,都是什么? 4.作用域public   priv ...

  4. Day2_and_Day3 文件操作

    文件修改操作: 文件的修改操作:文件并没有修改操作,实际是将一个编写的新文件覆盖了原有的文件 替换文件中的某个内容: with open('old.txt','r',encoding='utf-8') ...

  5. Install OpenCV on Ubuntu or Debian

    http://milq.github.io/install-OpenCV-ubuntu-debian/转注:就用第一个方法吧,第二个方法的那个sh文件执行失败,因为我价格kurento.org的源,在 ...

  6. 基于Python的数据分析(3):文件和时间

    在接下来的章节中,我会重点介绍一下我自己写的基于之前做python数据分析的打包接口文件common_lib,可以认为是专用于python的第三方支持库.common_lib目前包括文件操作.时间操作 ...

  7. php判断图片是否存在的几种方法

    在我们日常的开发中,经常需要用到判断图片是否存在,存在则显示,不存在则显示默认图片,那么我们用到的判断有哪些呢?今天我们就来看下几个常用的方法: 1.getimagesize()函数 getimage ...

  8. python!!!!惊了,这世上居然还有这么神奇的东西存在

    第一次接触到python的时候实在看学习3Blue1Brown的视频线性代数的本质的时候.惊奇的是里面的视频操作,例如向量的变化,线性变换等都是由python用代码打出来的.那时的我只是以为pytho ...

  9. linux下svn(subversion)服务端添加工程及配置权限

    linux下svn(subversion)服务端添加工程及配置权限 转载请注明源地址:http://www.cnblogs.com/funnyzpc/p/9010507.html 此篇我只是将所做过的 ...

  10. java 引用数据类型(类)

    我们可以把类的类型为两种: 第一种,Java为我们提供好的类,如Scanner类,Random类等,这些已存在的类中包含了很多的方法与属性,可供我们使用. 第二种,我们自己创建的类,按照类的定义标准, ...