石子合併(NOI1995)題解
題目描述
在一個圓形操場的四周擺放N堆石子,現要將石子有次序地合併成一堆.規定每次只能選相鄰的2堆合併成新的一堆,並將新的一堆的石子數,記為該次合併的得分。
試設計出1個演算法,計算出將N堆石子合併成1堆的最小得分和最大得分.
輸入輸出格式
輸入格式:
資料的第1行試正整數N,1≤N≤100,表示有N堆石子.第2行有N個數,分別表示每堆石子的個數.
輸出格式:
輸出共2行,第1行為最小得分,第2行為最大得分.
1995的NOI題目,然而卻是一道非常水的區間DP。
區間DP,顧名思義,求區間最值問題。通過小區間來更新大區間,最後逐漸更新出答案。
區間DP常用列舉套路:
for (int len = 2;len<=n;len++)
{
for(int i = 1;i+len-1<=n;i++)
{
int j = i+len-1;
dp[i][j] = ...
}
}
外層列舉長度,下一層列舉初始端點,終點通過長度+起點-1枚舉出來,需要注意的是起點列舉範圍是i+len-1,也就是終點要在區間長度以內。
繼續說這道題目。
大區間一定是通過小區間合併出來的,這也是我們使用區間DP的原因。但是這道題並不是一條鏈上的石子,而是一個環。也就是說我們有可能在最後一個石子回頭,與前面的石子合併。
那我們只需要把原來的鏈的長度變成二倍(除了最後一位),而列舉長度仍然是一倍的長度不就好了?
舉個例子:
2,3,4,5
我們可以把它變成2,3,4,5,2,3,4
列舉的時候仍然是4的長度。這樣就完美的處理了鏈的情況。
接下來是狀態轉移方程。
不知道有沒有同學會和我開始時候有一樣的錯覺,全部合併到一起不就是所有的值相加嗎?
然而並不是這樣的,當一個區間與另一個區間合併時,原來的區間的數被算了兩次。
再舉個栗子。
[1,2] 與[2,3]合併
前者合併之後是3,後者是5
這樣在合併一次就是3+5+8
相當於1+2+2+3+1+2+2+3
雖然這個結論是錯的,但是我們從中可以得到一個結論,每次合併的時候,區間內的所有值都會被再次算一遍。
所以要預處理字首和。
接下來說方程。
dp[i][j]表示把[i][j]中的石子合併成一堆所需要的費用。
[i][j]之間我們可以選擇任意一個點,把這個區間分成兩段,通過這兩段合併成這一段區間。
所以
dpmax[i][j] = max(dpmax[i][j],dpmax[i][k-1]+dpmax[k][j]+before[j]-before[i-1]);
dpmin[i][j] = min(dpmin[i][j],dpmin[i][k-1]+dpmin[k][j]+before[j]-before[i-1]);
before代表字首和
每個點的初始值就是它自己的花費。
最後上程式碼。
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<string>
using namespace std;
int num[202];
int before[202];
int dpmax[202][202];
int dpmin[202][202];
int main()
{
int n;
scanf("%d",&n);
for(int i = 1;i<=n;i++)
{
scanf("%d",&num[i]);
num[n+i] = num[i];
before[i] = before[i-1]+num[i];
}
for(int i = n+1;i<=2*n-1;i++)
{
before[i] = before[i-1]+num[i];
}
memset(dpmin,0x3f,sizeof(dpmin));
memset(dpmax,-1,sizeof(dpmax));
for(int i = 1;i<=2*n-1;i++)
{
dpmin[i][i] = 0;
dpmax[i][i] = 0;
}
for(int len = 2;len<=n;len++)
{
for(int i = 1;i+len-1<=2*n-1;i++)
{
int j = i+len-1;
for(int k = i+1;k<=j;k++)
{
dpmax[i][j] = max(dpmax[i][j],dpmax[i][k-1]+dpmax[k][j]+before[j]-before[i-1]);
dpmin[i][j] = min(dpmin[i][j],dpmin[i][k-1]+dpmin[k][j]+before[j]-before[i-1]);
}
}
}
int ma = -1;
int mi = 2147483467;
for(int i = 1;i<=n;i++)
{
ma = max(ma,dpmax[i][i+n-1]);
mi = min(mi,dpmin[i][i+n-1]);
}
printf("%d\n%d",mi,ma);
return 0;
}
/*
3
1 2 3
*/