1. 程式人生 > >貨郎擔問題TSP(dp解法)

貨郎擔問題TSP(dp解法)

題目連結

貨郎擔問題也叫旅行商問題,即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;
}