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

TSP问题描述:

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

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 ] != -1 )
        return dp[ setMask ][ x ]; /* if already have value */
    mask = 1 << x;
    dp[ setMask ][ x ] = 1e9; /* set infinite */
    for ( i = 0; i < n; ++i ) {
        if ( i != x && ( setMask & ( 1 << i ) ) && map[ i ][ x ] != 0 ) { /* 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", "1010",发现了吗,其实这些集合也就是剔除了起点城市以后,将两个1与一个0进行组合的所有情况。这个规律同样可以推广到n个城市,如果要将问题拆分k层,所有的问题集合即是n-k-1个1与k个0的所有组合数

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

    while (t > nn) { /* let all thread have work to do */
        nn *= (nn - 1);
        nc += 1; /* extends layers until t > nn */
    }
    j = 0;
    candiNum = malloc(sizeof(int) * (n - 1));
    for (i = 0; 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 - 1); /* calculate combination number */
    combSet = malloc(sizeof(int) * combNum);
    combptr = combSet;
    numOf1 = n - 1 - nc; /* there should have `numOf1` of 1 in every item */
    setCombination(candiNum, n - 1, numOf1);
    posOf1 = malloc(sizeof(int) * numOf1); /* arr to store the position of 1 in each comb */

  接下来就是使用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[20];
pthread_t tid[20]; /* thread ids */

int TSP( int x, int setMask ) {
    int mask, i, minimum;
    if ( dp[ setMask ][ x ] != -1 )
        return dp[ setMask ][ x ]; /* if already have value */
    mask = 1 << x;
    dp[ setMask ][ x ] = 1e9; /* set infinite */
    for ( i = 0; i < n; ++i ) {
        if ( i != x && ( setMask & ( 1 << i ) ) && map[ i ][ x ] != 0 ) { /* 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, 0, n - 1, 0, r);
    free(data); /* avoid leak */
}

void combinationUtil(int arr[], int data[], int start, int end, int index, int r) {
    int i, j, tmpi = 0; /* init vars */
    if (index == r) {
        for (j = 0; j < r; j++) /* add nums to binay */
            tmpi |= (1 << data[j]);
        *combptr = tmpi;
        combptr++; /* next cell in array */
        return;
    }
    for (i = start; i <= end && end - i + 1 >= r - index; i++) { /* recersive */
        data[index] = arr[i];
        combinationUtil(arr, data, i + 1, end, index + 1, r); /* call selfe */
    }
}

long factorial(int m, int n) { /* caculate combination number */
    int i, j;
    long ans = 1;
    if (m < n - m) m = n - m; /* C(m,n)=C(n-m,n) */
    for (i = m + 1; i <= n; i++) ans *= i;
    for (j = 1; 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 = 0;
    scanf("%d %d %d", &t, &n, &s); /* get &t, &n, &s */
    if (t < 0 || n <= 0 || n <= s || s < 0) {printf("-1\n"); return 0;} /* error input */
    if (n == 1) {printf("0\n"); return 0;}
    nn = n - 1; /* first layer */
    map = malloc(sizeof(int *)*n); /* alloc for 2dim array: map */
    tmptr = malloc(sizeof(int) * n * n);
    for (i = 0; i < n; ++i) {
        map[i] = tmptr; /* set 2dim to 1dim */
        tmptr += n;
    }
    dp = malloc(sizeof(int *) * (1 << n)); /* alloc for 2dim array: map */
    tmptr = malloc(sizeof(int) * (1 << n) * n);
    for (i = 0; i < (1 << n); ++i) {
        dp[i] = tmptr; /* set 2dim to 1dim */
        tmptr += n;
    }
    for (i = 0; i < n; i++)  /* get the map */
        for (j = 0; j < n; j++)
            scanf("%d", &map[i][j]); /* store */

    memset(dp[0], -1, sizeof(int) * (1 << n) * n); /* fill all dp with -1 */
    for ( i = 0; i < n; ++i )
        dp[ 1 << i ][ i ] = map[ 0 ][ i ];

    while (t > nn) { /* let all thread have work to do */
        nn *= (nn - 1);
        nc += 1; /* extends layers until t > nn */
    }
    j = 0;
    candiNum = malloc(sizeof(int) * (n - 1));
    for (i = 0; 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 - 1); /* calculate combination number */
    combSet = malloc(sizeof(int) * combNum);
    combptr = combSet;
    numOf1 = n - 1 - nc; /* there should have `numOf1` of 1 in every item */
    setCombination(candiNum, n - 1, numOf1);
    posOf1 = malloc(sizeof(int) * numOf1); /* arr to store the position of 1 in each comb */
    k = 0;
    for (i = 0; i < combNum; ++i) {
        tmptr = posOf1;
        for (j = 0; j < n; ++j)    { /* go through all bits */
            if ((combSet[i] & (1 << j)) != 0) {
                *tmptr = j;
                tmptr++; /* point to next cell */
            }
        }
        for (j = 0; 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 + 1) % t;
        }
    }
    for (i = 0; i < t; ++i)/* join all thread */
        pthread_join(tid[i], NULL);
    ans = TSP(0, ( 1 << n ) - 1);
    if (ans == 1e9)
        printf("-1\n");
    else
        printf("%d\n", ans); /* final answer */
    free(dp[0]); /* free */
    free(dp);
    free(map[0]); /* free */
    free(map);
    free(posOf1); /* free */
    free(combSet);
    free(candiNum); /* free */
    return 0;
}
// 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

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

原文地址:https://www.cnblogs.com/philip/p/9074966.html

时间: 2024-11-09 06:31:28

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

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

前段时间在搞贪心算法,为了举例,故拿TSP来开刀,写了段求解算法代码以便有需之人,注意代码考虑可读性从最容易理解角度写,没有优化,有需要可以自行优化! 一.TSP问题 TSP问题(Travelling Salesman Problem)即旅行商问题,又译为旅行推销员问题.货郎担问题,是数学领域中著名问题之一.假设有一个旅行商人要拜访n个城市,他必须选择所要走的路径,路径的限制是每个城市只能拜访一次,而且最后要回到原来出发的城市.路径的选择目标是要求得的路径路程为所有路径之中的最小值. TSP问题

Complexity and Tractability (3.44) - The Traveling Salesman Problem

转自:http://csfieldguide.org.nz/en/curriculum-guides/ncea/level-3/complexity-tractability-TSP.html This is a guide for students attempting Complexity and Tractability in digital technologies achievement standard 3.44. This guide is not official, althou

动态规划算法求解0,1背包问题

首先我们来看看动态规划的四个步骤: 1. 找出最优解的性质,并且刻画其结构特性: 2. 递归的定义最优解: 3. 以自底向上的方式刻画最优值: 4. 根据计算最优值时候得到的信息,构造最优解 其中改进的动态规划算法:备忘录法,是以自顶向下的方式刻画最优值,对于动态规划方法和备忘录方法,两者的使用情况如下: 一般来讲,当一个问题的所有子问题都至少要解一次时,使用动态规划算法比使用备忘录方法好.此时,动态规划算法没有任何多余的计算.同时,对于许多问题,常常可以利用其规则的表格存取方式,减少动态规划算

java 动态规划算法求解最长公共子串

最近在项目中碰到了这样的一个问题,要比较JS和CSS是否做了修改,先是想着借助第三方工具发现没找到,后面转念一想,这个问题不就是对两个文件的第一行求最大的公共子串嘛,既然是要求公共子串的最大长度,由此想到了动态规划算法. 代码是从网上C++改写过来的,感谢那位C++的兄弟,代码如下: package dp; /** * 用动态规划算法求解 最长公共子串 * @author * */ public class LCSSuffix { private static String getLCSLeng

2018.3.12 np completed problem, vertex cover and traveling salesman problem

1.先是讲了np完全问题的一般概念.简单的说就是一般的计算问题分为p,np和npc问题,还有一类更难的nph问题不过目前不在讨论范围内.npc问题的特点是,所有的np问题都能reduce到npc问题.就比如说我们要解决让刁大大能够从1数到10的问题.虽然这个问题很难,但是我们可以换个思路解决,我们把它推演(reduce)一下,变成教他从1数到20的问题.那么如果我们解决了这个新问题,原来的老问题也就同时解决了.从1数到10和从1数到20,就相当于np问题和npc问题之间的关系. 那么这样的结果就

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

HTML5提供了Canvas对象,为绘图应用提供了便利. Javascript可运行于浏览器中, 而不需要安装特定的编译器: 基于HTML5和Javascript语言, 可随时编写应用, 为算法测试带来便利. 针对TSP问题, 编写了Ant colony algorithm, 用于演示该算法, tsp_ant_colony_algorithm.html代码如下: <html> <head> <meta charset = "utf-8" / > &l

蚁群算法求解TSP问题

蚁群算法的第一个算法就是蚂蚁系统,而蚂蚁系统有三种基本模型分别是 蚁周模型.蚁密模型.蚁量模型.三种模型的实现大致相同,主要区别是在信息素 的更新方式上.在用蚂蚁系统解决T SP问题时,蚁量模型和蚁密模型是蚂蚁在构建 一条合法路径的过程中进行信息素的更新的,当蚂蚁走过一条边之后,就对该边进 行信息素的更新,即为局部更新方式.而蚁周模型是在所有蚂蚁都构建了一条合 法路径之后才对各边进行信息素更新的,也即全局更新方式. 并且这三种模型中蚂蚁在自己所走过的路线上释放的信息素的量也是有所 不同的,在蚁密

遗传算法的C语言实现(二)-----以求解TSP问题为例

上一次我们使用遗传算法求解了一个较为复杂的多元非线性函数的极值问题,也基本了解了遗传算法的实现基本步骤.这一次,我再以经典的TSP问题为例,更加深入地说明遗传算法中选择.交叉.变异等核心步骤的实现.而且这一次解决的是离散型问题,上一次解决的是连续型问题,刚好形成对照. 首先介绍一下TSP问题.TSP(traveling salesman problem,旅行商问题)是典型的NP完全问题,即其最坏情况下的时间复杂度随着问题规模的增大按指数方式增长,到目前为止还没有找到一个多项式时间的有效算法.TS

智能优化算法对TSP问题的求解研究

要求: TSP 算法(Traveling Salesman Problem)是指给定 n 个城市和各个城市之间的距离,要 求确定一条经过各个城市当且仅当一次的最短路径,它是一种典型的优化组合问题,其最优 解得求解代价是指数级的.TSP 问题代表一类优化组合问题,在实际工程中有很多应用,如 计算机联网.电子地图.交通诱导等,具有重要的研究价值.遗传算法和禁忌搜所算法都是 是一种智能优化算法,具有全局的优化性能.通用性强.这种算法一般具有严密的理论依据, 理论上可以在一定的时间内找到最优解或近似最优