推销员问题


内容

有一推销员,欲到 TS之Record遍历_编程语言(TS之Record遍历_算法_02)个城市推销产品。为了节省旅行费用,在出发前他查清了任意两个城市间的旅行费用,想找到一条旅行路线,仅经过每个城市一次,且使旅行费用最少。本问题已知城市 TS之Record遍历_编程语言,和 TS之Record遍历_TS之Record遍历_04


一、问题分析(模型、算法设计和正确性证明等)

1、遍历方法
模型:

采用矩阵表示的图结构进行存储

算法设计:

遍历算法,计算出所有的可能行进路线,将其对应权值相加取最小。其中全排列的给出使用递归方法。

正确性证明:

因为考虑到所有的可能情况,该方法正确。

2、递归方法
模型:

采用矩阵表示的图结构进行存储

算法设计:

递归算法,从一点遍历另一集合所有点再回到自身的最小花费等同于先到达该集合任意点再从该点遍历剩余的点构成的子集的最小花费。

正确性证明:

设定出发点$x$,目标遍历集合$y$,则$TSP(x,y) = \min_{i\in y}(TSP(i,y-i)+edge[x][i])$,其中,$edge[x][i]$表示从$x$到$i$的距离。则按照这种方法进行向下递归求解,最终集合会变为$\phi$,此时即为递归出口,返回值为$TSP(x,\phi) = edge[x][0]$(假定0是是初始点)。由于每一个子问题均为最优解,因此主要问题即为最优解。

二、复杂度分析

1、遍历法

假设共要去TS之Record遍历_编程语言个地方。

时间复杂度:

第一步是计算TS之Record遍历_编程语言的全排列问题,使用递归的方法为TS之Record遍历_算法_07,而当TS之Record遍历_递归_08时,TS之Record遍历_TS之Record遍历_09。因此,算法复杂度为TS之Record遍历_TS之Record遍历_10

第二步是计算给出的序列路径权重,即对算出的TS之Record遍历_全排列_11条路径进行长度计算,每条路径计算时相加TS之Record遍历_编程语言次,即算法复杂度为TS之Record遍历_全排列_13

第三步是对计算出的TS之Record遍历_全排列_11个路径权重查找最小值,TS之Record遍历_全排列_11

因此复杂度为TS之Record遍历_递归_16

空间复杂度:

采用TS之Record遍历_算法_17的矩阵记录图中边的权重。采用内含TS之Record遍历_编程语言_18数组的结构体表示一个解决方案。共有TS之Record遍历_全排列_11个解决方案(即全排列个数),故空间复杂度为TS之Record遍历_递归_16

2、递归法
时间复杂度:

为了更加方便地表示点集合,采用离散数学中特征函数表示方法表示,即第TS之Record遍历_算法_21个数字如果存在即为TS之Record遍历_编程语言_22,否则为TS之Record遍历_全排列_23。所以每一个二进制数可以表示一个集合,二进制数用十进制数进行表示。在这种表示方法下,每一个规模为TS之Record遍历_算法_21的递归程序调用TS之Record遍历_TS之Record遍历_25次比较取最小值,因此时间复杂度为TS之Record遍历_算法_26

空间复杂度:

为了避免重复递归,使用空间代替时间的方法,将每一次的递归求解结果使用一个数组进行存储再次需要时直接返回此结果,故空间复杂度为TS之Record遍历_递归_16

三、程序实现和测试过程和结果(主要描述出现的问题和解决方法)

程序实现:
1、遍历法
(1)空间爆炸问题:

程序内申请TS之Record遍历_算法_28会导致程序停止。解决方法为将数组声明为全局变量,因为局部变量存在于动态数据区中,而全局变量存在于静态存储区中。

(2)全排列的给出:

全排列利用TS之Record遍历_算法_07,进行递归程序的编写,递归方法为将每一个需要求的序列的除第一个外所有元素与第一个元素换位置之后求以该元素开头的序列全排列,递归出口为要求的全排列序列长度为TS之Record遍历_编程语言_22。代码如下:

void PREM(int lst[], int low, int high, solve T[],int N)
{
    if(low == high)
    {
        for(int i=0; i<N; i++) T[flag].path[i] = lst[i];
        flag++;
    }
    else
    {
        for(int i=low; i<=high; i++)
        {
            swap(lst[low],lst[i]);
            PREM(lst, low+1, high, T, N);
            swap(lst[low],lst[i]);
        }
    }
}
2、递归法
(1)空间爆炸问题:

同上。

(2)重复调用问题:

递归函数在调用的时候会多次调用到相同的子问题,会导致不必要的时间浪费,因此将每一次的递归求解结果使用一个数组进行存储再次需要时直接返回此结果。代码如下:

int TSP(int x, int y)
{
    int re = inf;
    if(cost[x][y]) return cost[x][y];				//代替数组
    if(y==0) return edge[x][0];						//递归出口
    for(int i=0; i<n; i++)
    {
        if(!((y>>i)&1)) continue;					//判断是否访问
        int temp = TSP(i,y-pow(2,i))+edge[x][i];	//更新条件
        if(re > temp) re = temp;
    }
    cost[x][y] = re;
    return re;
}
(3)打印路径问题

打印路径时同样使用递归函数,但是在已经计算完成的TS之Record遍历_算法_31数组下进行计算。找到每一个路径对应的TS之Record遍历_全排列_32再进行对于TS之Record遍历_全排列_32的搜索。代码如下:

void print_path(int x, int y)
{
    if(y==0) return;
    int re = inf, next;
    for(int i=0; i<n; i++)
    {
        if(!((y>>i)&1)) continue;
        if(re > cost[i][y-(int)pow(2,i)]+edge[x][i])
        {
            next=i; re=cost[i][y-(int)pow(2,i)]+edge[x][i];
        }
    }
    cout<<next+1<<" ";
    print_path(next, y-pow(2,next));
}

四、总结(经验和反思等)

1、大空间的存储:较大空间的申请最好放在全局变量进行申请。

2、可以使用空间简化时间,在递归时使用数组进行存储可以简化递归的调用次数。

3、相对于暴力求解方法,递归方法的空间复杂度更低,可以求解的问题规模也更大。