1. 程式人生 > 實用技巧 >[NOI1995]石子合併

[NOI1995]石子合併

題目

Description

在一個圓形操場的四周擺放N堆石子,現要將石子有次序地合併成一堆.規定每次只能選相鄰的2堆合併成新的一堆,並將新的一堆的石子數,記為該次合併的得分。

試設計出一個演算法,計算出將N堆石子合併成1堆的最小得分和最大得分。

Input

資料的第1行是正整數N,表示有NN堆石子。

2行有N個整數,第i個整數a_i表示第i堆石子的個數。

Output

輸出共2行,第1行為最小得分,第2行為最大得分。

Sample Input

4
4 5 9 4

Sample Output

43
54

思路

有一道很類似的題:

最小代價樹

首先這是一道經典的dp題;

那麼d[i][j]表示i-j的最小代價,sum[i][j]是i-j每個石頭的代價;

答案就是dp[1][n]了;

那麼每一次合併i-j就需要加上i-j的權值代價;

dp[1][3]=min(dp[1][1]+dp[2][3]+sum[1][3],dp[1][2]+dp[3][3]+sum[1][3]);

所以我們需要列舉斷開的位置k,i-j的合併就是i-k的合併加上k+1-j的合併再加上代價;

所以轉移方程就是:

dp[i][j]=min(dp[i][j],dp[i][k]+dp[k+1][j]+sum[j]-sum[i-1]);

(sum是字首和;)

然後枚舉len長度,i起點,k斷開的位置;

為什麼不是先列舉i起點,在列舉j終點呢?

for(ll i=1;i<=n;i++)

for(ll j=i+1;j<=n;j++)

for(ll k=i;k<j;k++)

假如求出dp[1][2],然後列舉到dp[1][3]=dp[1][1]+dp[2][3] ;

然鵝dp[2][3]還未求出,所以我們需要列舉區間長度;

從區間長度小列舉到區間長度大,這樣算長度大的區間時,訪問小區間,小區間就有答案;

所以要列舉len長度,i起點,k斷開的位置;

那麼求最大值則反之;

程式碼

#include<bits/stdc++.h>
typedef long long
ll; using namespace std; inline ll read() { ll a=0,f=1; char c=getchar(); while (c<'0'||c>'9') {if (c=='-') f=-1; c=getchar();} while (c>='0'&&c<='9') {a=a*10+c-'0'; c=getchar();} return a*f; } ll n; ll dp[2001][2001],f[2001][2001]; ll a[2001],s[2001]; int main() { n=read(); for(ll i=1;i<=n;i++) { a[i]=read(); a[i+n]=a[i];//破環成列 } for(ll i=1;i<=n*2;i++)//計算字首 s[i]=s[i-1]+a[i]; for(ll len=2;len<=2*n;len++)//列舉區間長度,len=1沒什麼意義額 for(ll i=1;i<=2*n-len+1;i++) { ll j=i+len-1; for(ll k=i;k<j;k++) dp[i][j]=max(dp[i][j],dp[i][k]+dp[k+1][j]+s[j]-s[i-1]);//轉移方程 f[i][j]=1<<30;//因為答案是求最小值,但是陣列一開始都為0,答案也會為0,所以我們需要統計時改為最大值 for(ll k=i;k<j;k++) f[i][j]=min(f[i][j],f[i][k]+f[k+1][j]+s[j]-s[i-1]);//轉移方程 } ll ans=0; for(ll i=1;i<=n;i++)//因為是環所以要列舉每個n長度的區間 ans=max(ans,dp[i][i+n-1]); ll answer=1<<30; for(ll i=1;i<=n;i++) answer=min(answer,f[i][i+n-1]); printf("%lld\n%lld\n",answer,ans); }