例題:最優配對問題
空間裡有n個點P0,P1,...,Pn-1,你的任務是把他們配成n/2對(n是偶數),使得每個點恰好在一個點對中。所有點對中兩點的距離之和應儘量小。n<=20,|xi|,|yi|,|zi|<=1000.
樣例輸入:
20
1 2 3
1 1 1
5 6 2
4 7 8
2 3 1
1 4 7
2 5 8
3 6 9
1 2 5
2 3 6
4 5 2
7 8 5
4 5 1
-1 2 3
-1 -9 -7
0 0 0
100 0 0
9 5 1
7 5 3
5 5 5
分析:
既然每個點都要配對,很容易把問題看成如下多階段決策過程:先確定P0和誰配對,然後是P1,接下來是P2,……,最後是Pn-1.按照前面的思路,設d(i)表示把前i個點兩兩配對的最小距離和,然後考慮第i個點的決策——它和誰配對呢?假設它和點j配對(j< i),那麼接下來的問題應是“把前i-1個點中除了j之外的其它點兩兩配對”,它顯然無法用任何一個d值來刻畫——我們的狀態定義無法體現出“除了一些點之外”這樣的限制。
當發現狀態無法轉移後,常見的方法是增加維度,即增加新的因素,更細緻地描述狀態。既然剛才提到了“除了某些元素之外”,不妨把它作為狀態的一部分,設d(i,S)表示把前i個點中,位於集合S中的元素兩兩配對的最小距離,則狀態轉移方程為:
d(i,S) = min{|PiPj|+ d(i-1,S-{i}-{j}) | j屬於S}
其中|PiPj表示Pi和Pj之間的距離。邊界是d(-1,S )= 0.
記憶化搜尋的程式碼如下:
1 #include<cstdio> 2 #include<cstdlib> 3 #include<cmath> 4 #define maxn 20 5 #define INF (1<<30) 6 int n; 7 struct Point{ 8 double x, y, z; 9 } p[maxn]; 10 double f[(1<<maxn)]; 11 inline double dist(int i, int j) 12 { 13 return sqrt((p[i].x-p[j].x)*(p[i].x-p[j].x) + (p[i].y-p[j].y)*(p[i].y-p[j].y) + (p[i].z-p[j].z)*(p[i].z-p[j].z)); 14 } 15 inline double min(double x, double y) 16 { 17 if(x < y) return x; 18 return y; 19 } 20 double d(int S){ 21 //記憶化搜尋快得要逆天了!!(因為奇數的都沒有算!! ) 22 if(f[S] != -1) return f[S]; 23 int i, j; 24 double& ans = f[S]; 25 ans = (S == 0? 0 : INF); 26 for(i = n-1; i >= 0; --i) if(S & (1<<i)) break; 27 for(j = 0; j < i; ++j) if(S & (1<<j)) 28 ans = min(ans, d(S^(1<<i)^(1<<j)) + dist(i, j)); 29 return ans; 30 } 31 32 int main(){ 33 int i; 34 scanf("%d", &n); 35 if(n%2) {printf("No answer!\n"); return 0;} 36 for(i = 0; i < n; ++i) scanf("%lf%lf%lf", &p[i].x, &p[i].y, &p[i].z); 37 for(i = 0; i < (1 << n); ++i) f[i] = -1; 38 printf("%.3lf\n", d((1<<n)-1)); 39 /* 40 int cnt = 0; 41 for(i = 0; i < (1 << n); ++i) if(f[i]!=-1) ++cnt; 42 printf("%d\n", cnt); 43 */ 44 return 0; 45 }
動態規劃的程式碼如下:
1 #include<cstdio> 2 #include<cstdlib> 3 #include<cmath> 4 #define maxn 20 5 #define INF (1<<30) 6 int n; 7 struct Point{ 8 double x, y, z; 9 } p[maxn]; 10 double f[maxn][(1<<maxn)]; 11 inline double dist(int i, int j) 12 { 13 return sqrt((p[i].x-p[j].x)*(p[i].x-p[j].x) + (p[i].y-p[j].y)*(p[i].y-p[j].y) + (p[i].z-p[j].z)*(p[i].z-p[j].z)); 14 } 15 inline double min(double x, double y) 16 { 17 if(x < y) return x; 18 return y; 19 } 20 int main(){ 21 int i,j; 22 scanf("%d", &n); 23 if(n%2) {printf("No answer!\n"); return 0;} 24 for(i = 0; i < n; ++i) scanf("%lf%lf%lf", &p[i].x, &p[i].y, &p[i].z); 25 for(i = 0; i < n; ++i) 26 for(int S = 0; S < (1 << (i+1)); ++S){ 27 double& ans = f[i][S]; 28 ans = (S==0 ? 0 : INF); 29 if(S & (1 << i)) { 30 for(j = 0; j < i; ++j) if(S & (1 << j)) 31 ans = min(ans, f[i-1][S^(1<<i)^(1<<j)] + dist(i, j)); 32 } 33 else if(i!=0) f[i][S] = f[i-1][S]; 34 } 35 printf("%.3lf\n", f[n-1][(1<<n)-1]); 36 return 0; 37 }
隱含的階段
上面的方程可以進一步簡化。事實上,階段 i 根本不用儲存,它已經隱含在S中了——S中最大的元素就是i。這樣,可直接用d(S)表示“把S中的元素兩兩配對的最小距離和”,則狀態轉移方程為:
d(S)=min{|PiPj|+d(S-{i}-{j}) | j屬於S,i=max(S)}
狀態有2n個,每個狀態有O(n)種轉移方式,總時間複雜度為O(n2n).
值得一提的是,不少人一直在用這樣的狀態轉移方程:
d(S)=min{|PiPj|+d(S-{i}-{j}) | i,j屬於S}
它和剛才的方程很類似,唯一不同是:i和j都是需要列舉的。這樣做雖然也沒錯,但每個狀態的轉移次數高達O(n2),總時間複雜度為O(n22n),比剛才的方法慢。這個例子說明:即使用相同的狀態描述,減少決策也很重要。
那麼如何求出S中的最大元素呢?用一個迴圈判斷即可。當S取遍{0,1,2,...,n-1}的所有子集時,平均判斷次數僅為2。
1 for(int S=0; S<(1<<n); S++) 2 { 3 int i,j; 4 d[S] = INF; 5 for(i=0; i<n; i++) 6 if(S && (1<<i)) break; 7 for(j=i+1; j<n; j++) 8 if(S & (1<<j)) d[S] <?= d[S^(1<<i)^(1<<j)] + dist(i, j)); 9 }
注意,在上述的程式中求出的i是S中的最小元素,而不是最大元素,但這並不影響答案。另外,j的列舉只需從 i+1開始——既然 i 是S中最小元素,則說明其他元素自然均比i大。最後需要說明的是S的列舉順序。不難發現:如果S‘是S的真子集,則一定有S'<S,因此S遞增的順序計算,需要用到某個d值時,它一定已經算出來了。
如果S'是S的真子集,則一定有S'<S。在用遞推法實現子集動態規劃時,該規則往往可以確定計算部分。
優化後程式碼如下:
1 #include<cstdio> 2 #include<cstdlib> 3 #include<cmath> 4 #define maxn 22 5 #define maxs (1<<maxn) 6 #define INF 1e10 7 int n; 8 double f[maxs], x[maxn], y[maxn], z[maxn]; 9 inline double min(double x, double y) 10 { 11 return x<y?x:y; 12 } 13 inline double dist(int i, int j) 14 { 15 return sqrt((x[i]-x[j])*(x[i]-x[j]) + (y[i]-y[j])*(y[i]-y[j]) + (z[i]-z[j])*(z[i]-z[j])); 16 } 17 int main(){ 18 scanf("%d", &n); 19 for(int i = 0; i < n; ++i) scanf("%lf%lf%lf", x+i, y+i, z+i); 20 int U = (1 << n); 21 for(int S = 0; S < U; ++S){ 22 int i; 23 for(i = n-1; i >= 0; --i) if(S & (1<<i)) break; 24 f[S] = (S==0) ? 0 : INF; 25 for(int j = 0; j < i ; ++j) if(S & (1<<j)){ 26 f[S] = min(f[S], f[S^(1<<i)^(1<<j)] + dist(i, j)); 27 } 28 } 29 printf("%.3lf\n", f[U-1]); 30 return 0; 31 }