石子合並問題(直線版)


首先來個題目鏈接:http://acm.nyist.net/JudgeOnline/problem.php?pid=737

有個更難的版本(不過很好玩):http://www.lydsy.com/JudgeOnline/problem.php?id=3229

題目:

石子合並(一)

時間限制: 1000 ms  |  內存限制:65535 KB
難度: 3
 
描述
    有N堆石子排成一排,每堆石子有一定的數量。現要將N堆石子並成為一堆。合並的過程只能每次將相鄰的兩堆石子堆成一堆,每次合並花費的代價為這兩堆石子的和,經過N-1次合並后成為一堆。求出總的代價最小值。
 
輸入
有多組測試數據,輸入到文件結束。
每組測試數據第一行有一個整數n,表示有n堆石子。
接下來的一行有n(0< n <200)個數,分別表示這n堆石子的數目,用空格隔開
輸出
輸出總代價的最小值,占單獨的一行
樣例輸入
3
1 2 3
7
13 7 8 16 21 4 18
樣例輸出
9
239


最普通的算法O(n^3):

 1 #include <fstream>
 2 #include <iostream>
 3 #include <cstdio>
 4 #include <cstring>
 5 #include <cstdlib>
 6 #include <cmath>
 7 using namespace std;
 8 
 9 const int N=205;
10 const int INF=0x7fffffff;
11 int n;
12 int a[N],sum[N],dp[N][N];
13 
14 void f();
15 
16 int main(){
17     //freopen("D:\\input.in","r",stdin);
18     while(~scanf("%d",&n)){
19         sum[0]=0;
20         for(int i=1;i<=n;i++){
21             scanf("%d",&a[i]);
22             sum[i]=sum[i-1]+a[i];
23         }
24         f();
25         printf("%d\n",dp[1][n]);
26     }
27     return 0;
28 }
29 void f(){
30     for(int i=1;i<=n;i++) dp[i][i]=0;
31     for(int r=1;r<n;r++){
32         for(int i=1;i<n;i++){
33             int j=i+r;
34             if(j>n) break;
35             dp[i][j]=INF;
36             for(int k=i;k<=j;k++){
37                 dp[i][j]=min(dp[i][j],dp[i][k]+dp[k+1][j]);
38             }
39             dp[i][j]+=sum[j]-sum[i-1];
40         }
41     }
42 }        
224ms

其中,dp[i][j]代表i到j堆的最優值,sum[i]代表第1堆到第i堆的數目總和。有:dp[i][j]=min(dp[i][j],dp[i][k]+dp[k+1][j])+sum[j]-sum[i-1]。

 

考慮四邊形不等式優化:接近O(n^2):

 1 #include <fstream>
 2 #include <iostream>
 3 #include <cstdio>
 4 #include <cstring>
 5 #include <cstdlib>
 6 #include <cmath>
 7 using namespace std;
 8 
 9 const int N=205;
10 const int INF=0x7fffffff;
11 int n;
12 int a[N],sum[N],dp[N][N],s[N][N];
13 
14 void f();
15 
16 int main(){
17     //freopen("D:\\input.in","r",stdin);
18     while(~scanf("%d",&n)){
19         sum[0]=0;
20         for(int i=1;i<=n;i++){
21             scanf("%d",&a[i]);
22             sum[i]=sum[i-1]+a[i];
23         }
24         f();
25         printf("%d\n",dp[1][n]);
26     }
27     return 0;
28 }
29 void f(){
30     for(int i=1;i<=n;i++) dp[i][i]=0,s[i][i]=i;
31     for(int r=1;r<n;r++){
32         for(int i=1;i<n;i++){
33             int j=i+r;
34             if(j>n) break;
35             dp[i][j]=INF;
36             for(int k=s[i][j-1];k<=s[i+1][j];k++){
37                 if(dp[i][j]>dp[i][k]+dp[k+1][j]){
38                     dp[i][j]=dp[i][k]+dp[k+1][j];
39                     s[i][j]=k;
40                 }
41             }
42             dp[i][j]+=sum[j]-sum[i-1];
43         }
44     }
45 }        
32ms

優化:原狀態轉移方程中的k的枚舉范圍便可以從原來的(i~j-1)變為(s[i,j-1]~s[i+1,j])。

解釋下:

四邊形不等式優化動態規划原理:

1.當決策代價函數w[i][j]滿足w[i][j]+w[i’][j’]<=w[I;][j]+w[i][j’](i<=i’<=j<=j’)時,稱w滿足四邊形不等式.當函數w[i][j]滿足w[i’][j]<=w[i][j’] i<=i’<=j<=j’)時,稱w關於區間包含關系單調.

2.如果狀態轉移方程m為且決策代價w滿足四邊形不等式的單調函數(可以推導出m亦為滿足四邊形不等式的單調函數),則可利用四邊形不等式推出最優決策s的單調函數性,從而減少每個狀態的狀態數,將算法的時間復雜度由原來的O(n^3)降低為O(n^2).方法是通過記錄子區間的最優決策來減少當前的決策量.令:

s[i][j]=max{k | ma[i][j] = m[i][k-1] + m[k][j] + w[i][j]}

由於決策s具有單調性,因此狀態轉移方程可修改為:

證明過程: (轉載)

m[i,j]表示動態規划的狀態量。

m[i,j]有類似如下的狀態轉移方程:

m[i,j]=opt{m[i,k]+m[k,j]}(ikj)

如果對於任意的abcd,有m[a,c]+m[b,d]m[a,d]+m[b,c],那么m[i,j]滿足四邊形不等式。

以上是適用這種優化方法的必要條件

對於一道具體的題目,我們首先要證明它滿足這個條件,一般來說用數學歸納法證明,根據題目的不同而不同。

通常的動態規划的復雜度是O(n3),我們可以優化到O(n2)

s[i,j]m[i,j]的決策量,即m[i,j]=m[i,s[i,j]]+m[s[i,j]+j]

我們可以證明,s[i,j-1]s[i,j]s[i+1,j]  (證明過程見下)

那么改變狀態轉移方程為:

m[i,j]=opt{m[i,k]+m[k,j]}      (s[i,j-1]ks[i+1,j])

復雜度分析:不難看出,復雜度決定於s的值,以求m[i,i+L]為例,

(s[2,L+1]-s[1,L])+(s[3,L+2]-s[2,L+1])…+(s[n-L+1,n]-s[n-L,n-1])=s[n-L+1,n]-s[1,L]n

所以總復雜度是O(n2)

s[i,j-1]s[i,j]s[i+1,j]的證明:

mk[i,j]=m[i,k]+m[k,j]s[i,j]=d

對於任意k<d,有mk[i,j]md[i,j](這里以m[i,j]=min{m[i,k]+m[k,j]}為例,max的類似),接下來只要證明mk[i+1,j]md[i+1,j],那么只有當s[i+1,j]s[i,j]時才有可能有ms[i+1,j][i+1,j]md[i+1,j]

(mk[i+1,j]-md[i+1,j]) - (mk[i,j]-md[i,j])

=(mk[i+1,j]+md[i,j]) - (md[i+1,j]+mk[i,j])

=(m[i+1,k]+m[k,j]+m[i,d]+m[d,j]) - (m[i+1,d]+m[d,j]+m[i,k]+m[k,j])

=(m[i+1,k]+m[i,d]) - (m[i+1,d]+m[i,k])

m滿足四邊形不等式,∴對於i<i+1k<dm[i+1,k]+m[i,d]m[i+1,d]+m[i,k]

(mk[i+1,j]-md[i+1,j])(mk[i,j]-md[i,j])0

s[i,j]s[i+1,j],同理可證s[i,j-1]s[i,j]

證畢

擴展:

以上所給出的狀態轉移方程只是一種比較一般的,其實,很多狀態轉移方程都滿足四邊形不等式優化的條件。

解決這類問題的大概步驟是:

0.證明w滿足四邊形不等式,這里wm的附屬量,形如m[i,j]=opt{m[i,k]+m[k,j]+w[i,j]},此時大多要先證明w滿足條件才能進一步證明m滿足條件

1.證明m滿足四邊形不等式

2.證明s[i,j-1]s[i,j]s[i+1,j]

 

GarsiaWachs算法優化:

 1 #include <iostream>
 2 #include <cstring>
 3 #include <cstdio>
 4 using namespace std;
 5 
 6 const int N = 205;
 7 int stone[N];
 8 int n,t,ans;
 9 
10 void combine(int k);
11 
12 int main(){
13      while(cin>>n){
14         for(int i=0;i<n;i++)
15             scanf("%d",stone+i);
16         t = 1;
17         ans = 0;
18         for(int i=1;i<n;i++){
19             stone[t++] = stone[i];
20             while(t >= 3 && stone[t-3] <= stone[t-1])
21                 combine(t-2);
22         }
23         while(t > 1)
24             combine(t-1);
25         printf("%d\n",ans);
26      }
27      return 0;
28 }
29 void combine(int k){
30     int tmp = stone[k] + stone[k-1];
31     ans += tmp;
32     for(int i=k;i<t-1;i++)
33         stone[i] = stone[i+1];
34     t--;
35     int j = 0;
36     for(j=k-1;j>0 && stone[j-1] < tmp;j--)
37         stone[j] = stone[j-1];
38     stone[j] = tmp;
39     while(j >= 2 && stone[j] >= stone[j-2]){
40         int d = t - j;
41         combine(j-1);
42         j = t - d;
43     }
44 }
4ms

解釋:

對於石子合並問題,有一個最好的算法,那就是GarsiaWachs算法。時間復雜度為O(n^2)。

它的步驟如下:

設序列是stone[],從左往右,找一個滿足stone[k-1] <= stone[k+1]的k,找到后合並stone[k]和stone[k-1],再從當前位置開始向左找最大的j,使其滿足stone[j] > stone[k]+stone[k-1],插到j的后面就行。一直重復,直到只剩下一堆石子就可以了。在這個過程中,可以假設stone[-1]和stone[n]是正無窮的。

舉個例子:
186 64 35 32 103
因為35<103,所以最小的k是3,我們先把35和32刪除,得到他們的和67,並向前尋找一個第一個超過67的數,把67插入到他后面,得到:186 67 64 103,現在由5個數變為4個數了,繼續:186 131 103,現在k=2(別忘了,設A[-1]和A[n]等於正無窮大)234 186,最后得到420。最后的答案呢?就是各次合並的重量之和,即420+234+131+67=852。
 
基本思想是通過樹的最優性得到一個節點間深度的約束,之后證明操作一次之后的解可以和原來的解一一對應,並保證節點移動之后他所在的深度不會改變。具體實現這個算法需要一點技巧,精髓在於不停快速尋找最小的k,即維護一個“2-遞減序列”朴素的實現的時間復雜度是O(n*n),但可以用一個平衡樹來優化,使得最終復雜度為O(nlogn)。
 
GarsiaWachs算法優化+小細節優化:
 1 #include <fstream>
 2 #include <iostream>
 3 #include <cstdio>
 4 #include <cstring>
 5 #include <cstdlib>
 6 #include <cmath>
 7 using namespace std;
 8 
 9 const int N = 205;
10 const int INF = 0x7fffffff;
11 
12 int stone[N];
13 int n,t,ans;
14 
15 void combine(int k)
16 {
17     int tmp = stone[k] + stone[k-1];
18     ans += tmp;
19     for(int i=k;i<t-1;i++)
20         stone[i] = stone[i+1];
21     t--;
22     int j = 0;
23     for(j=k-1;stone[j-1] < tmp;j--)
24         stone[j] = stone[j-1];
25     stone[j] = tmp;
26     while(j >= 2 && stone[j] >= stone[j-2])
27     {
28         int d = t - j;
29         combine(j-1);
30         j = t - d;
31     }
32 }
33 
34 int main()
35 {
36     //freopen("D:\\input.in","r",stdin);
37     while(~scanf("%d",&n))
38     {
39         for(int i=1;i<=n;i++)
40             scanf("%d",stone+i);
41         stone[0]=INF;
42         stone[n+1]=INF-1;
43         t = 3;
44         ans = 0;
45         for(int i=3;i<=n+1;i++)
46         {
47             stone[t++] = stone[i];
48             while(stone[t-3] <= stone[t-1])
49                 combine(t-2);
50         }
51         while(t > 3) combine(t-1);
52         printf("%d\n",ans);
53     }
54     return 0;
55 }        
0ms

小細節在於把數列前后加兩個值INF和INF-1。這樣就不需要每次判別上下界。至於-1,組合堆的最后一步里體現。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM