[區間DP]石子合併極其變種問題(環形,40000堆型)P1880 [NOI1995]石子合併+[Sdoi2008]石子合併/poj1738An old Stone Game
有N堆石子,現要將石子有序的合併成一堆,規則如下:
(1)每次只能移動任意相鄰的2堆石子合併,合併花費為新合成的一堆石子的數量。求將這N堆石子合併成一堆總花費,要求N<=300。
變形一:(2)每次只能移動相鄰的2堆石子合併,合併花費為新合成的一堆石子的數量。求將這N堆石子合併成一堆的總花費最小(或最大)要求N<=40000。
變形二:(3)問題(2)的是在石子排列是直線情況下的解法,石子改為環形排列
題目:
題目限制
時間限制 | 記憶體限制 | 評測方式 | 題目來源 |
1000ms | 256000KiB | 遠端評測 | CodeVS |
題目描述 Description
在一個操場上擺放著一排N堆石子。現要將石子有次序地合併成一堆。規定每次只能選相鄰的2堆石子合併成新的一堆,並將新的一堆石子數記為該次合併的得分。
試設計一個演算法,計算出將N堆石子合併成一堆的最小得分。
輸入描述 Input Description
第一行是一個數N。
以下N行每行一個數A,表示石子數目。
#include <iostream> #include <string.h> #include <stdio.h> using namespace std; const int N = 50005; int stone[N]; int n,t,ans; void combine(int k) { int tmp = stone[k] + stone[k-1]; ans += tmp; for(int i=k;i<t-1;i++) stone[i] = stone[i+1]; t--; int j = 0; for(j=k-1;j>0 && stone[j-1] < tmp;j--) stone[j] = stone[j-1]; stone[j] = tmp; while(j >= 2 && stone[j] >= stone[j-2]) { int d = t - j; combine(j-1); j = t - d; } } int main() { while(scanf("%d",&n)!=EOF) { if(n == 0) break; for(int i=0;i<n;i++) scanf("%d",stone+i); t = 1; ans = 0; for(int i=1;i<n;i++) { stone[t++] = stone[i]; while(t >= 3 && stone[t-3] <= stone[t-1]) combine(t-2); } while(t > 1) combine(t-1); printf("%d\n",ans); } return 0; }
輸出描述 Output Description
共一個數,即N堆石子合併成一堆的最小得分。
樣例輸入 Sample Input
4
1
1
1
1
樣例輸出 Sample Output
8
資料範圍及提示 Data Size & Hint
對於 30% 的資料,1≤N≤100
對於 60% 的資料,1≤N≤1000
對於 100% 的資料,1≤N≤40000
對於 100% 的資料,1≤A≤200
題目描述
在一個圓形操場的四周擺放N堆石子,現要將石子有次序地合併成一堆.規定每次只能選相鄰的2堆合併成新的一堆,並將新的一堆的石子數,記為該次合併的得分。
試設計出1個演算法,計算出將N堆石子合併成1堆的最小得分和最大得分.
區間DP 做法(要求N<=300)演算法複雜度O(N^3):
#include <iostream>
#include <cstring>
#include <cstdlib>
#include <cstdio>
#include <queue>
#include <map>
#define frein freopen("D:\\cprogram\\acmprogram\\input", "r", stdin)
#define freout freopen("D:\\cprogram\\acmprogram\\ouput", "w", stdout)
#define MAXN 1005
#define MAXM 500005
#define INF 1000000000
using namespace std;
int f[309][309];
int f2[309][309];
int a[309],sum[309];
int main(int argc, char const *argv[])
{
int n;
scanf("%d",&n);
for (int i = 1; i <= n; ++i)
{
scanf("%d",&a[i]);
a[i+n] = a[i];
}
memset(f,0x3f,sizeof(f));
memset(f2,0,sizeof(f2));
for (int i = 1; i <= n; ++i)
{
f2[i][i] = 0;
f[i][i] = 0;
sum[i]=sum[i-1]+a[i];
}
for (int len = 2; len <= n; ++len)
{
for (int l = 1; l <= n-len+1; ++l)
{
int r = l+len-1;
for (int k = l; k < r ; ++k)
{
// printf("===f[%d][%d]%d f[%d][%d]%d\n",l,k,f[l][k],k+1,r,f[k+1][r]);
f[l][r] = min(f[l][r],f[l][k]+f[k+1][r]);
f2[l][r] = max(f2[l][r],f2[l][k]+f2[k+1][r]);
}
f[l][r]+=sum[r]-sum[l-1];
f2[l][r]+=sum[r]-sum[l-1];
}
}
printf("%d\n",f[1][n]);
return 0;
}
進行四邊形優化,原狀態轉移方程中的k的列舉範圍便可以從原來的(i~j-1)變為(s[i,j-1]~s[i+1,j])。演算法複雜度進一步接近O(n^2)(逃~~~ ε=ε=ε=┏(゜ロ゜;)┛;
可以優化為O(N^2)的時間複雜度的情況,
From 黑書
凸四邊形不等式:w[a][c]+w[b][d]<=w[b][c]+w[a][d](a<b<c<d)
區間包含關係單調: w[b][c]<=w[a][d](a<b<c<d)
定理1: 如果w同時滿足四邊形不等式和決策單調性 ,則f也滿足四邊形不等式
定理2: 若f滿足四邊形不等式,則決策s滿足 s[i][j-1]<=s[i][j]<=s[i+1][j]
定理3: w為凸當且僅當w[i][j]+w[i+1][j+1]<=w[i+1][j]+w[i][j+1]
簡要證明:
若w[a][c]+w[b][d]<=w[b][c]+w[a][d],歸納證明f[a][c]+f[b][d]<=f[b][c]+f[a][d]
設f[a][d]最優決策是在s取到,f[b][c]最優決策在t取到,設s<t,反之同理
可知a<s<t<c<d
f[a][c]+f[b][d]<=f[a][s]+f[s+1][c]+w[a][c] + f[b][t]+f[t+1][d]+w[b][d]
=f[a][s]+f[s+1][c]+w[a][d] + f[b][t]+f[t+1][d]+w[b][c]
<=f[a][s]+w[a][d]+f[s+1][d] + f[b][t]+w[b][c]+f[t+1][c] 歸納得到 sc+td<sd+tc 起始條件即定理3
=f[a][d]+f[b][c]
得證.
若f[a][c]+f[b][d]<=f[b][c]+f[a][d],則s[i][j-1]<=s[i][j]<=s[i+1][j]
僅證s[i][j-1]<=s[i][j],右邊同理
記f_k[i][j]=f[i][k]+f[k+1][j]+w[i][j]
記s點為[i,j]最優點,t點為[i,j+1]最優點,
則只需證明 在[i,j+1]決策時, 取s點能夠比取在k∈[i,s-1]的點更優即可
即證明 f_s[i,j+1]<=f_k[i,j+1]
又因為f_s[i,j]<=f_k[i,j]
只需證明 0 <= f_k[i,j] - f_s[i,j] <= f_k[i,j+1] - f_s[i,j+1]
可發現右邊即 f_k[i,j] + f_s[i,j+1] <= f_k[i,j+1] + f_s[i,j]
展開後即: f[k][j] + f[s][j+1] <= f[k][j+1] + f[s][j]
正是 k<s<j<j+1 的四邊形不等式
得證.
一般利用定理3證明凸函式,然後利用定理2的結論 s[i][j-1]<=s[i][j]<=s[i+1][j]
就能夠使得複雜度由O(n^3)降低為O(n^2)
詳細證明參見《動態規劃演算法的優化技巧》--毛子青(會因為論文用i,j,i',j'搞得霧水,但是慢慢推一下就能夠出來)
#include <iostream>
#include <cstring>
#include <cstdlib>
#include <cstdio>
#include <queue>
#include <map>
#define frein freopen("D:\\cprogram\\acmprogram\\input", "r", stdin)
#define freout freopen("D:\\cprogram\\acmprogram\\ouput", "w", stdout)
#define MAXN 1005
#define MAXM 500005
#define INF 1000000000
using namespace std;
int f[309][309];
int ss[309][309];
int a[309],sum[309];
int main(int argc, char const *argv[])
{
int n;
scanf("%d",&n);
for (int i = 1; i <= n; ++i)
{
scanf("%d",&a[i]);
a[i+n] = a[i];
}
memset(f,0x3f,sizeof(f));
for (int i = 1; i <= n; ++i)
{
ss[i][i]=i;
f[i][i] = 0;
sum[i]=sum[i-1]+a[i];
}
for (int len = 2; len <= n; ++len)
{
for (int l = 1; l <= n-len+1; ++l)
{
int r = l+len-1;
for (int k = ss[l][r-1]; k <=ss[l+1][r] ; ++k)
{
if(f[l][r]>f[l][k]+f[k+1][r])
{
f[l][r] = f[l][k]+f[k+1][r];
ss[l][r]=k;
}
}
f[l][r]+=sum[r]-sum[l-1];
}
}
printf("%d\n",f[1][n]);
return 0;
}
但是當題目 N<=40000時,樸素DP不能用了。需要GarsiaWachs演算法,不會證明,直接看說步驟:以下步驟和例項為抄襲
設序列是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)。
輸入輸出格式
輸入格式:
資料的第1行試正整數N,1≤N≤100,表示有N堆石子.第2行有N個數,分別表示每堆石子的個數.
輸出格式:
輸出共2行,第1行為最小得分,第2行為最大得分.
輸入輸出樣例
輸入樣例#1: 複製
4
4 5 9 4
輸出樣例#1: 複製
43
54
環形的石子合併,想象一排有2N堆的石子,在這個2N的鏈條上,使其[i~i+N-1]合併成一個頂點就可以了;轉移方程如下
#include <iostream>
#include <cstring>
#include <cstdlib>
#include <cstdio>
#include <queue>
#include <map>
#define frein freopen("D:\\cprogram\\acmprogram\\input", "r", stdin)
#define freout freopen("D:\\cprogram\\acmprogram\\ouput", "w", stdout)
#define MAXN 1005
#define MAXM 500005
#define INF 1000000000
using namespace std;
int f[309][309];
int f2[309][309];
int a[309],sum[309];
int main(int argc, char const *argv[])
{
int n;
scanf("%d",&n);
for (int i = 1; i <= n; ++i)
{
scanf("%d",&a[i]);
a[i+n] = a[i];
}
memset(f,0x3f,sizeof(f));
memset(f2,0,sizeof(f2));
for (int i = 1; i <= 2*n; ++i)
{
f2[i][i] = 0;
f[i][i] = 0;
sum[i]=sum[i-1]+a[i];
}
for (int len = 2; len <= n; ++len)
{
for (int l = 1; l <= 2*n-len+1; ++l)
{
int r = l+len-1;
for (int k = l; k < r ; ++k)
{
f[l][r] = min(f[l][r],f[l][k]+f[k+1][r]);
f2[l][r] = max(f2[l][r],f2[l][k]+f2[k+1][r]);
}
f[l][r]+=sum[r]-sum[l-1];
f2[l][r]+=sum[r]-sum[l-1];
}
}
int ansmin = 0x3f3f3f3f,ansmax = 0;
for (int i = 1; i <= n; ++i)
{
ansmax = max(f2[i][i+n-1],ansmax);
ansmin = min( f[i][i+n-1],ansmin);
}
printf("%d\n%d\n",ansmin,ansmax);
return 0;
}