1. 程式人生 > 實用技巧 >【解題報告】 [NOI1995]石子合併

【解題報告】 [NOI1995]石子合併

這道題應該說還是有一定的來頭的畢竟作為區間DP的鼻祖

首先讓我們先來介紹一下什麼叫做區間DP:

區間DP顧名思義就是在一段區間上的動態規劃。它既要滿足dp問題的最優子結構和無後效性外,還應該符合在區間上操作的特點。所以說區間DP往往會對區間進行合併操作。可看成一個小區間(通過多次的相鄰合併,最後實質上會產生跨區間的合併)。其實自己感覺這個玩意兒挺像倍增的。

那麼下面我們來講一講例題:

題目連結:
P1880 [NOI1995]石子合併

一句話題意:

給你一個環狀的陣列,然後每次我們把兩個數合併在一起,消耗值是他們兩個數之和,問最後合併成一堆的時候最小的消耗值是多少,其實後來這道題想一想也覺得並不是那麼的難,其實最難想到的並不是區間DP,而是我們如何處理這個環的問題。其實沒想到不是笨,還是見的題目太少了。我們可以把這個環拆開,拆成一個鏈,然後我們把兩個同樣的鏈連線起來,這樣我們最後尋找最優解我們只需要在這兩條鏈上比較一下從每一個點開始的當前鏈長度哪個解更優!

說不太清楚,大家看一下程式碼應該就明白了!

    for(register int i=1;i<=n;++i)
    {
        minx=min(minx,dp_min[i][i+n-1]);//刷表找最小,把環拆開! 
        maxx=max(maxx,dp_max[i][i+n-1]);// 
    }

然後其實根據剛才我們所講的區間dp思想,我們其實只要把動態規劃陣列的一維設成左區間端點二維設成右區間端點進行dp就可以了(從小到大進行合併)

具體程式碼如下:

for(register int i=(n<<1)-1;i>0;--i)  
    //for(int i=1;i<(n<<1);++i)
    {
        for(register int j=i+1;j<i+n;++j) 
        {
            dp_min[i][j]=inf;
            for(register int k=i;k<j;++k)
            {
                dp_min[i][j]=min(dp_min[i][j],dp_min[i][k]+dp_min[k+1][j]+sum[j]-sum[i-1]);  //腦洞DP 
                dp_max[i][j]=max(dp_max[i][j],dp_max[i][k]+dp_max[k+1][j]+sum[j]-sum[i-1]);  //代價還要加上這次合併後的石子數量  
            }
        } 
    }

注意:這裡表一定要倒著刷否則前面刷的表會被後面刷的表覆蓋,然後就出鍋了!

上面更正一下:

其實這樣打更保險:

    //for(register int i=(n<<1)-1;i>0;--i)  
    for(register int j=1;j<=n;++j)
    {
        //for(register int j=i+1;j<i+n;++j) 
        for(register int s=1;s+j<=n*2;++s)
        {
            int e=s+j;
            dp_min[s][e]=inf;
            for(register int k=s;k<e;++k)
            {
                dp_min[s][e]=min(dp_min[s][e],dp_min[s][k]+dp_min[k+1][e]+sum[e]-sum[s-1]);  //腦洞DP 
                dp_max[s][e]=max(dp_max[s][e],dp_max[s][k]+dp_max[k+1][e]+sum[e]-sum[s-1]);  //代價還要加上這次合併後的石子數量  
            }
        } 
    }

我們從小區間逐漸往大區間進行合併,這樣不容易錯,並且刷表也不可能刷重複。

關鍵部分相信大家應該都懂了,那麼應該就可以完成了!

參考程式碼:

#include<bits/stdc++.h>
using namespace std;
const int maxn=505,inf=0x3f3f3f3f;
int n,num[maxn*2],sum[maxn*2],dp_min[maxn][maxn*2],dp_max[maxn][maxn*2];
int minx=inf,maxx;
int main()
{
    scanf("%d",&n);
    for(int i=1;i<=n;++i)
    scanf("%d",&num[i]),num[i+n]=num[i];
    for(int i=1;i<=2*n;++i)
    sum[i]=sum[i-1]+num[i];
    //for(register int i=(n<<1)-1;i>0;--i)  
    for(register int j=1;j<=n;++j)
    {
        //for(register int j=i+1;j<i+n;++j) 
        for(register int s=1;s+j<=n*2;++s)
        {
            int e=s+j;
            dp_min[s][e]=inf;
            for(register int k=s;k<e;++k)
            {
                dp_min[s][e]=min(dp_min[s][e],dp_min[s][k]+dp_min[k+1][e]+sum[e]-sum[s-1]);  //腦洞DP 
                dp_max[s][e]=max(dp_max[s][e],dp_max[s][k]+dp_max[k+1][e]+sum[e]-sum[s-1]);  //代價還要加上這次合併後的石子數量  
            }
        } 
    }
    for(register int i=1;i<=n;++i)
    {
        minx=min(minx,dp_min[i][i+n-1]);//刷表找最小,把環拆開! 
        maxx=max(maxx,dp_max[i][i+n-1]);// 
    }
    printf("%d\n%d\n",minx,maxx);
    return 0;
}

一定要記住,我們要從小區間逐漸往大區間合併。
By njc