【旅行商问题】动态规划和贪心算法的实现

第1章概述

1.1问题名称

旅行商问题的两种算法设计与分析(Travelling Sales Problem)

1.2问题描述

假设有一个旅行商人要拜访N个城市,他必须选择所要走的路径,路径的限制是每个城市只能拜访一次,而且最后要回到原来出发的城市。路径的选择目标是要求得的路径路程为所有路径之中的最小值。
在这里插入图片描述
在这里插入图片描述

再抽象一些,相当于给定一系列点集V(|V|=n),在点集中从一点出发,寻找一条最短路径,该路径经过点集中的所有点,并且每个点只经过一次。
在这里插入图片描述

第2章动态规划算法求解方案

2.1过程分析

动态规划算法(Dynamic Programming,简称DP)通常用于求解具有某种最优性质的问题,其基本思想是将待求解问题分解成若干个子问题,先求解子问题,然后由这些子问题的解再得到原问题的解。
因此动态规划是一种求解TSP问题的好方法。
根据问题描述中的例子,我们要从城市0出发,期间1,2,3,4每个城市都必须经过并且只能经过一次,最后回到0,使得路上花费的代价最小。这是一个最基本的TSP问题,且构成最优子结构性质,所以可以使用动态规划求解,下面来验证一下此方法求解的可行性。
设 s,s1,s2…s为满足题意的最短回路。假设从s到s1的路径已经确定,则问题转化为从s1到s的最短路径问题。而很显然,s1,s2…s一定可以构成一条最短路径,所以构成最优子结构性质,可以用动态规划求解。

2.2算法的具体设计

1.最优子结构

假设从顶点s出发,令d(i, V)表示从顶点i出发经过V(是一个点的集合)中各个顶点一次且仅一次,最后回到出发点s的最短路径长度。
推导:(分情况来讨论)
1.当V为空集,那么d(i,V),表示直接从i回到s了,此时d(i,V) =Cis且(i != s)
2.如果V不为空,那么就是对子问题的最优求解。你必须在V这个城市集合中,尝试每一个,并求出最优解。

       注:表示选择的城市和城市i的距离,d(k,V-(k))是一个子问题。
    综上所述,TSP问题的动态规划方程是:

在这里插入图片描述

2.重叠子问题

从下往上构造备忘录(这里为一个压缩二维矩阵)

2.3算法实现

#include <iostream>
#include <stdio.h>
#include<iomanip>
#define MAX 12
using namespace std;
 
int dis[MAX][MAX], d[MAX][1 << (MAX-1)];//距离地图表,d函数表,d[]为动态规划存储的城市经过矩阵
int route[MAX][1 << (MAX-1)];//记录最优路线点 
int p[MAX];//记录最优路线 
 
int main()
{
    int n, temp,minDis,minPoint,count;
    给距离地图表赋值
    cout<<"请输入城市个数:" ;
    cin >> n;
    cout<<"输入各城市之间的路程费用:"<<endl ;
    for (int i = 0; i < n; i++){
        for (int j = 0; j < n; j++){
            scanf("%d", &dis[i][j]);
        }
    }
 
    //给d函数表赋值
    for (int i = 1; i<n; i++){     //将所有城市到第0个城市的路径初始化为两市间的距离
        d[i][0] = dis[i][0];
        route[i][0] = 0;
    }
    //给除了第0行以外的其余部分赋值 
    for (int j = 1; j<(1 <<(n-1)); j++){//j用二进制表示的城市集合,先说j再说i,动态规划从j的底层到上层,j数值越来越大
        for (int i = 1; i<n; i++){    
            //i不在j表示的城市集合中,可以对d[i][j]赋值,否则赋0;
            if (((1 << (i - 1))&j) == 0){         
                //temp=dis[i][k]+d[k][V-{k}]开始对V中所有的k进行遍历
                int minDis = 60000;
                for (int k = 1; k<n; k++){
                    if ((1 << (k - 1))&j)  {//k表示的城市在j表示的城市集合中 
                        temp = dis[i][k] + d[k][j - (1 << (k - 1))];
                        if (temp<minDis){//d(1,{2,3})=min{ C12+d(2,{3})||C13+d(3,{2})}
                            minDis = temp;   //所有k中最小的距离
                            minPoint=k; 
                        }
                    }
                }
                d[i][j] = minDis;//给d函数表每个位置赋值
//              cout<< d[i][j]<<" ";
                route[i][j]=minPoint;//从城市集j中选出minPoint点递归为最优 
            }
            
        }
    }
    
    //对第0行最后一个d(0,{123})赋值,d(0,{1,2,3})=min {C01+d(1,{2,3})|| C02+d{2,{1,3}}||C03+d{3,{1,2}})
    minDis = 600;
    for (int k = 1; k<n; k++){
        temp = dis[0][k] + d[k][((1 << (n - 1)) - 1) - (1 << (k - 1))];//d[k][{123}-{k}]
        if (minDis>temp){
            minDis = temp;
            minPoint=k; 
        }
    }
    d[0][(1 << (n - 1)) - 1] = minDis;//数组从零开始,所以-1 
    route[0][(1 << (n - 1)) - 1] = minPoint;
    
    //此部分可以输出看看形成的d[][]矩阵,便于理解执行过程
    for(int i=0;i<n;i++){    
        for(int j=0;j<(1<<(n-1));j++){
            printf("%3d",d[i][j]);
        }
        cout<<endl;
    } 
 
    cout <<"最小代价:"<< d[0][(1 << (n - 1)) - 1]<<endl;
    
    //求出最优路径p[12]
    count=0;p[count++]=0;
    int flat=(1 << (n - 1)) - 1;
    do
    {
        temp=route[p[count-1]][flat];
        p[count++]=temp;
        flat-=(1<<(temp-1));
    } 
    while(flat>0);//所有点都被查找过一遍,最后一次是空集0要先执行一遍(所以用do-while,记得加分号) 
    
    //打印最优路径
    cout<<"最优路线:" ;
    cout<<p[0];
    for(int i=1;i<=n;i++)
    {
        cout<<"->"<<p[i];   
    } 
    
    return 0;
}

2.4调试结果

在这里插入图片描述

2.5复杂度分析

抛开复杂的时间复杂度分析理论不谈,我们知道一般一层for循环就是一个乘数,把每个嵌套的循环次数乘起来最大的那个就是我们的时间复杂度。所以我们上面的代码的时间复杂度应该是O(MN^2),其中N是给定算例的点的个数,而M = 2N,所以总体的时间复杂度为O(N2*2^N),是一个指数级的时间复杂度,随着N的增大,其时间代价会顺势快速增长。

第3章贪心算法求解方案

3.1过程分析

贪心法求解TSP问题的贪心策略是显然的至少有两种贪心策略是合理的
最近邻点策略和最短链接策略。本文仅重点讨论最近邻点策略及其求解过程。
最近邻点策略,从任意城市出发,每次在没有到过的城市中选择距离已选择
的城市中最近的一个,直到经过了所有的城市,最后回到出发城市。

3.2算法的具体设计

1.初始化距离数组D,已访问数组S;
2.设置起始城市start已被访问;
3.如果还有没有访问的城市
3.1找到start到其他没有被访问城市中距离最短的城市next_city;
3.2加入next_city到已访问数组中;
3.3更新start为next_city;
3.4累加当前路径和;
4.当所有城市全被访问后
5.输出遍历序列
6.算法结束

3.3算法实现

#include <stdio.h>
#define max 100
int num ;
int g[max][max], x[max], bestx[max]; //best[]是用来存储最终的最短路径 
int cl=0;
int best = max;

//交换函数 
void swap(int &a, int &b)
{
	int tem = a;
	a= b;
	b =tem;
}


//下界公式为 min{cl + min1 + temmin}解释一下就是当前走过的路径+下一个要找的最短路径+剩下点的到其他点的最短路径(最后这个temmin是用贪心的思想求剩下没走过城市的最路径)
//当然,贪心毕竟不是从整体最优考虑,而是从局部最优考虑,所以,在后来的寻找中,如果找到比 cl + min1 + temmin求解出来的最短路径,则可以考虑往下找,否则剪枝。 
int  bound( int t)
{
	int min1=max ,min2=max, temmin=0;  //这里min与min2初始化一下 
	for(int i=t; i<=num; i++)
	{
		//这里求min1 
		if( g[x[t-1]][x[i]] != -1 && g[x[t-1]][x[i]]  < min1)
		{
			min1 =g[x[t-1]][x[i]]; //这里求要去的第t城市。选择当前到t路径最短的 
		}
		
		//这里求min2 
		for(int j =1; j<=num; j++) //贪心的思想,求剩下的路径的下界 
	     {
	     	if( g[x[i]][x[j]]!=-1 &&  g[x[i]][x[j]]!=0  && g[x[i]][x[j]] < min2)
	     	{
	     		min2 = g[x[i]][x[j]];
			 }
		 }
		 temmin += min2; //这里是剩下点的最短出出路径的和的累加 
	}
	return cl+ min1 + temmin;
}

//这里选择城市的方式类似于全排列问题,只不过这里加了 一个判断条件就是t与之前的城市t-1有路径,这里是有选择性的选择点(这里也叫剪枝)。 
void prim(int t)
{
	//如果到叶节点,则表示所有城市都走过了,这个时候就保存一下最短路径以及相应的城市顺序 ,当然是满足条件就是比之前的bext还要小的路径的长度,每次取最优 
	if(t>num )
	{
		if(g[x[num]][1]!=-1 &&  cl + g[x[num]][1] <best) //记得还有最后回到原点 ,这里必须是有到原点的一条路径 
		{
		  for(int i =1; i<=num; i++)
			{
			   bestx[i] =x[i];	 //将走过的顺序放入best数组中 
		    }
			best = cl + g[x[num]][1]; //保存加上回到原点的的路径 
		}
	}
	
	else{
		for(int j =t; j<=num; j++)
		{
			if( g[x[t-1]][x[j]] !=-1 && (bound(t)<best)) //满足条件的选择要选的城市的编号 
			{
				swap(x[j],x[t]);
				cl += g[x[t-1]][x[t]]; //注意,这里不是g[x[t-1]x[j]],j改成t,因为上面的j与t的位置的数换了。 
				
				prim(t+1);
				
				//回溯,恢复走过的。 
				cl -= g[x[t-1]][x[t]];
				swap(x[j],x[t]);
			}
		}
	}
}


int main()
{
	printf("输入城市数: ");
	scanf("%d",&num);
	
	for(int i =1; i<=num; i++)
	{
		x[i] =i; //将城市编号放入一个数组中,初始化 
		bestx[i] = 0;
	}
	
	for(int i =1; i<=num; i++)
	{
		for(int j =1; j<=num; j++)
		{
			scanf("%d",&g[i][j]);
		}
	 } 
	 
	 prim(2); //这里是从第一个城市出发,开始要寻找第二个要走的城市 
	 
	printf("从1号城市出发,经过所有城市回到起点顺序为: "); 
	 for(int i =1; i<=num ;i++)
	 {
         printf("%d --> ", bestx[i]);
	 }
	 
	 if(g[x[num]][1]!=-1);
	 {
	 	printf("%d",bestx[1]); 
	 }
	 
	 printf("\n"); 
	 printf("\n最短路径为:  %d", best); 
	 return 0;
 } 

3.4调试结果

在这里插入图片描述

3.5复杂度分析

O(n!)
如果使用完全搜索,对于n个城市,从任意一个城市出发后可以有了路线为(n - 1)个城市允许的所有路线,以此类推(类似于递归的思路)。因而对于n个城市时间复杂度为O(n!)。阶乘的增长速度极快,所以只需要少量的城市将会使该方法无解

第4章总结

4.1两种方案的对比分析

动态规划法相对贪心法来说虽然要精确些,但代码相对繁杂很多,对时间和
空间要求很多,仅适用于城市数量较小的情况。贪心法虽然比较简单,实现起来
比较容易,但不是很精确。当图中顶点个数较多并且各边的代价值分布比较均匀
时。贪心法可以给出较好的近似解。不过,这个近似解以何种程度近似于最优解
却难以保证。
另外,动态规划法有一个明显的缺点。就是出发城市只能是第0个城市(城
市从0开始编号)若出发城市改变,则必须以该城市为第0个城市顺序给其他
城市编号,输入城市间距离。由于若出发城市任意,编码的难度大大增加,所以
最后不得已放弃,但这大大地限制了程序的通用性。而对于贪心法,本文很好地
避免了这个问题,一旦城市编号确定,可以从任意城市出发,这也是本文中贪心
法优于动态规划法的一点。

4.2展望

在本程序中,各个子函数功能分隔很明显,没有大量集中在一个函数里面
而是分成了几个不同功能的小函数,这样程序可阅读性提高。另外,程序中有详
细注释,程序中变量取名都是根据变量的性质和所代表的含义命名的,也相应提
高了程序的可读性。
对于动态规划法,城市个数可以在算法时间允许的范围内任意,于这点来说
通用性较好;对于贪心法,出发城市可以任意,城市个数也可以任意,通用性较
好。
当城市个数较少时,用动态规划法求出最优解,当城市个数较多并且各边的
代价值分布比较均匀时,贪心法可以给出较好的近似解。

  • 9
    点赞
  • 51
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
以下是一个简单的TSP贪心算法C语言实现: ```c #include <stdio.h> #include <stdlib.h> #include <math.h> #define N 10 typedef struct { double x; double y; } city; double distance(city a, city b) { return sqrt(pow(a.x-b.x,2) + pow(a.y-b.y,2)); } int main() { // 生成城市坐标 city coord[N]; for (int i=0; i<N; i++) { coord[i].x = (double)rand() / RAND_MAX * 10; coord[i].y = (double)rand() / RAND_MAX * 10; } // 计算任意两个城市之间的距离 double dist[N][N]; for (int i=0; i<N; i++) { for (int j=i+1; j<N; j++) { dist[i][j] = distance(coord[i], coord[j]); dist[j][i] = dist[i][j]; } } // 初始化访问城市的顺序 int route[N]; int visited[N]; visited[0] = 1; route[0] = 0; // 选择最近的未访问城市作为下一个访问城市 for (int i=1; i<N; i++) { int last = route[i-1]; int next = -1; double minDist = INFINITY; for (int j=0; j<N; j++) { if (j == last) { continue; } if (visited[j] == 1) { continue; } if (dist[last][j] < minDist) { minDist = dist[last][j]; next = j; } } route[i] = next; visited[next] = 1; } // 计算总的行走距离 double totalDist = 0; for (int i=0; i<N-1; i++) { totalDist += dist[route[i]][route[i+1]]; } totalDist += dist[route[N-1]][route[0]]; // 输出结果 printf("访问城市的顺序:\n"); for (int i=0; i<N; i++) { printf("%d ", route[i]); } printf("\n"); printf("总行走距离: %lf\n", totalDist); return 0; } ``` 该算法的基本思路与Matlab实现类似,从起点出发,每次选择最近的未访问城市作为下一个访问城市,直到所有城市都被访问过为止。同样地,这个算法也只考虑了当前最优解,而没有考虑全局最优解,因此不能保证得到最优解。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值