货郎担问题TSP(dp解法)

来源:互联网 发布:微信宣传模版源码下载 编辑:程序博客网 时间:2024/06/05 19:33

题目链接

货郎担问题也叫旅行商问题,即TSP问题(Traveling Salesman Problem),是数学领域中著名问题之一。

题目背景

有n个城市,用1,2,…,n表示,城i,j之间的距离为dij,有一个货郎从城1出发到其他城市一次且仅一次,最后回到城市1,怎样选择行走路线使总路程最短?

货郎担问题(TSP问题)是一个组合优化问题。
该问题可以被证明具有NPC计算复杂性。

经典模型

邮路问题

假定有一辆邮车要到n个不同的地点收集邮件,
这种情况可以用n十1个结点的图来表示。
一个结点表示此邮车出发并要返回的那个邮局,
其余的n个结点表示要收集邮件的n个地点。
由地点i到地点j的距离则由边 < i,j > 上所赋予的成本来表示。
邮车所行经的路线是一条周游路线,希望求出具有最小长度的周游路线。

螺帽问题

第二个例子是在一条装配线上用一个机械手去紧固待装配部件上的螺帽问题。
机械手由其初始位置(该位置在第一个要紧固的螺帽的上方)开始,
依次移动到其余的每一个螺帽,最后返回到初始位置。
机械手移动的路线就是以螺帽为结点的一个图中的一条周游路线。
一条最小成本周游路线将使这机械手完成其工作所用的时间取最小值。

生产安排问题

第三个例子是产品的生产安排问题。
假设要在同一组机器上制造n种不同的产品,生产是周期性进行的,
即在每一个生产周期这n种产品都要被制造。
要生产这些产品有两种开销,一种是制造第i种产品时所耗费的资金(1≤i≤n),称为生产成本,
另一种是这些机器由制造第i种产品变到制造第j种产品时所耗费的开支cij称为转换成本。
显然,生产成本与生产顺序无关。
于是,我们希望找到一种制造这些产品的顺序,
使得制造这n种产品的转换成本和为最小。
由于生产是周期进行的,因此在开始下一周期生产时也要开支转换成本,
它等于由最后一种产品变到制造第一种产品的转换成本。
于是,可以把这个问题看成是一个具有n个结点,边成本为cij图的货郎担问题。

动态规划解法

现在使用最广泛的还是动态规划的解法,但也只是适用于规模较小的情况

设计状态
f(i,S),表示从起点到i经过集合S中的所有点的最短路径

f(i,S)=min{f(j,S-{j})+dis(i,j)}

初始状态:f(i,{})=dis(起点,i)
最终答案:f(起点,{1,2,3,4,…,n-1})
时间复杂度:n^2*2^n
所以n只能<=15

代码很简单
用的是递归的思想

num:还没有经过的点的个数
now:当前位置

调用的时候默认start—>now的来路已经处理好了
递归处理now—>终点
枚举和now相连的所有点
如果枚举点i在来路上没有经过,而且不是start(不能在中途就回到起点了)
那么我们就继续递归

终止条件是:num==1,返回当前点到终点的距离

//这里写代码片#include<cstdio>#include<cstring>#include<iostream>#include<algorithm>#include<cmath>using namespace std;int n;struct node{    int x,y;    bool operator < (const node &a)const    {        return (x<a.x||(x==a.x&&y<a.y));    }};node po[1005];bool p[1005];double dis(node x,node y){    return sqrt((double)(x.x-y.x)*(x.x-y.x)+(double)(x.y-y.y)*(x.y-y.y));}double doit(int num,int now){    if (num==1) return dis(po[1],po[now]);    double ans=1e9;    for (int i=2;i<=n;i++)                //不能中途回到起点         if (i!=now&&p[i])                 //除了起点,每个点只经过一次         {            p[i]=0;            ans=min(ans,dis(po[now],po[i])+doit(num-1,i));            p[i]=1;        }            return ans;}int main(){    while (scanf("%d",&n)!=EOF)    {        for (int i=1;i<=n;i++) scanf("%d%d",&po[i].x,&po[i].y);        sort(po+1,po+1+n);        memset(p,1,sizeof(p));        printf("%0.2lf\n",doit(n,1));    }    return 0;}

然而上述方法虽然简单易懂,但是没有办法记忆化搜索
非常容易就T了,难道我们就这样GG了吗
实际上我们还有一种dp方法:

我们想象有两个人同时从起点向终点走,
设计状态:f[i][j]表示第1个人走到第i个点,第2个人走到了第j个点
那我们要怎么转移呢?
换句话说,现在的状态能不能转移到f[i+1][j]?
不好说,因为上面的状态不能表示哪些点还没有经过
所以这不是一个好的状态定义

那我们修改一下:
f[i][j]表示第1个人走到第i个点,第2个人走到了第j个点,
且1~max(i,j)我们都走过了,这种情况下还有多少路要走
不难发现f[i][j]=f[j][i]
为了方便,我们规定i>j
这样我们就可以列出转移方程了:

f[i][j]=min{f[i+1][j]+dis(i,i+1),f[i+1][i]+dis(j,i+1)}

边界条件是:f[n-1][j]=dis(n-1,n)+dis(j,n)

//这里写代码片#include<cstdio>#include<cstring>#include<iostream>#include<algorithm>#include<cmath>using namespace std;int n;struct node{    int x,y;    bool operator < (const node &a)const    {        return (x<a.x||(x==a.x&&y<a.y));    }};node po[1005];double f[1005][1005];bool vis[1005][1005];double dis(node x,node y){    return sqrt((double)(x.x-y.x)*(x.x-y.x)+(double)(x.y-y.y)*(x.y-y.y));}double doit(int w1,int w2){    if (w1==n-1)    {        return dis(po[n-1],po[n])+dis(po[w2],po[n]);    }    if (vis[w1][w2]) return f[w1][w2];    vis[w1][w2]=1;    double &ans=f[w1][w2];    ans=1e9;    ans=min(doit(w1+1,w2)+dis(po[w1+1],po[w1]),doit(w1+1,w1)+dis(po[w2],po[w1+1]));    return ans;}int main(){    while (scanf("%d",&n)!=EOF)    {        for (int i=1;i<=n;i++) scanf("%d%d",&po[i].x,&po[i].y);        sort(po+1,po+1+n);        memset(vis,0,sizeof(vis));        memset(f,0,sizeof(f));        printf("%0.2lf\n",doit(1,1));    }    return 0;}
原创粉丝点击