1. 程式人生 > >石子合併(NOI1995)題解

石子合併(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
*/