【算法競賽刷題模板11】區間dp

【算法競賽刷題模板11】區間dp


0.總結

Get to the points first. The article comes from LawsonAbs!
  • 區間dp是常見的一種dp
  • 使用區間dp常用的套路

1.固定套路

區間dp的套路是固定的。

  • step1.確定區間長度len,一般是 len 屬於[0,n]。按照從小到大的順序遍歷一次,這個作爲dp問題處理的階段。
  • step2.然後接着確定區間的端點,又因爲區間長度已經確定好了,當知道了區間的左端點,區間的右端點就可以通過 j=i+len-1 計算出來
  • step3.這一步就是針對具體問題進行一個具體的分析,同時做一個狀態的轉移即可。
  • step4.再複雜一點兒的情況,可能給你的這個序列並不是一個線性的,而是一個環,那麼就可能需要將這個環斷成一個鏈,這個鏈是原來序列的2倍長。例如 1 2 3是一個環,那麼我們就生成一個序列1 2 3 1 2 3。以此來解決存在環的問題。

2.練習題

2.1 【洛谷】P1880 石子合併

這是一道任何書中講區間dp都會涉及到的模板題。可AC的代碼如下:

#include<iostream>  
#include<cstdio>  
#include<cmath>  
using namespace std;   
int n,minl,maxl,f1[300][300],f2[300][300],num[300];  
int s[300];  
inline int d(int i,int j){return s[j]-s[i-1];}  
//轉移方程:f[i][j] = max(f[i][k]+f[k+1][j]+d[i][j];
int main()  
{   
    scanf("%d",&n);  
    for(int i=1;i<=n+n;i++)  //因爲是一個環,所以需要開到兩倍再枚舉分界線,最後肯定是最大的 
    {  
        scanf("%d",&num[i]);  
        num[i+n]=num[i];  
        s[i]=s[i-1]+num[i];  
    }  
    for(int p=1;p<n;p++)  
    {  
        for(int i=1,j=i+p;(j<n+n) && (i<n+n);i++,j=i+p)  
        {  
            f2[i][j]=999999999;  
            for(int k=i;k<j;k++)  
            {  
                f1[i][j] = max(f1[i][j], f1[i][k]+f1[k+1][j]+d(i,j));   
                f2[i][j] = min(f2[i][j], f2[i][k]+f2[k+1][j]+d(i,j));  
            }  
        }  
    }  
    minl=999999999;  
    for(int i=1;i<=n;i++)  
    {  
        maxl=max(maxl,f1[i][i+n-1]);  
        minl=min(minl,f2[i][i+n-1]);  
    }  
    printf("%d\n%d",minl,maxl);  
    return 0;  
}
2.2 【洛谷】 P1063 能量項鍊

下面是我寫的代碼:

#include<iostream>
using namespace std;
const int N = 205;
//dp[i][j]表示左端點爲i,長度爲j時的最大值 
//arr[i][0]表示爲頭,arr[i][1]表示爲尾 
//left[i][j]表示區間[i,j] 取最大值時的左端點值; right[i][j]表示區間[i,j]取最大值是右端點的值 
int dp[N][N],lef[N][N],righ[N][N],arr[N][2];
int n;

int main(){
	cin >> n;
	for(int i = 1;i<=n;i++){
		cin >> arr[i][0]; 		
		arr[i-1][1] = arr[i][0] ;//上個珠子的尾等於次頭 		
	}
	arr[n][1] = arr[1][0] ;//重新計算第一顆珠子的尾 
	
	//預處理部分
	for(int i = 1;i<=n;i++){
	//	cout << arr[i][0]<<","<< arr[i][1]<<"\n";
		lef[i][i] = arr[i][0];
		righ[i][i] = arr[i][1];	
	}
	for(int i = 1;i<=n;i++){//斷鏈成環 
		arr[n+i][0] = arr[i][0];
		arr[n+i][1] = arr[i][1];
		lef[n+i][n+i] = lef[i][i];
		righ[n+i][n+i] = righ[i][i];
	}
		
	//開始計算 
	for(int len = 2;len<=n;len++){//len的範圍在[2,n]
		for(int i = 1;i<=2*n;i++){//區間的左端點 
			int y = i+len-1;//區間的右端點 
			for(int k = i;k<y && y<=2*n; k++){
				int temp = lef[i][k] * righ[i][k] * righ[k+1][y] + dp[i][k-i+1] + dp[k+1][y-k];
				if(dp[i][len] < temp){
					dp[i][len] = temp;
					lef[i][y] =  lef[i][k];
					righ[i][y] = righ[k+1][y]; 
				}
			}
		} 
	} 
	int res = 0;
	for(int i = 1;i<=n;i++){
		res = max(res,dp[i][n]);
	}
	cout << res<<"\n";	
}

雖然可AC,但是稍顯冗餘,其原因是,dp數組設的不好,導致需要用到lef,righ兩個數組,很明顯,這個是可以避免的。下面再給出一個精簡版的代碼:

#include <bits/stdc++.h>
using namespace std;
int f[405][405];
int n,a[205]; 
int main()
{
    cin >> n;
    for(int i=1;i<=n;i++)  //***對環形問題的處理技巧***
    {
        cin >> a[i];
        a[n+i]=a[i];
    } 
    for(int i=2;i<=n+1;i++)
    {
        for(int l=1;l+i-1<=2*n;l++)  //如果採取了上述策略,一定要將2*n個點都更新 
        {
            int r=l+i-1;
            for(int k=l+1;k<=l+i-2;k++)
                f[l][r]=max(f[l][r],f[l][k]+f[k][r]+a[l]*a[k]*a[r]); 
        }
    }
    int res=0;
    for (int i=1;i<=n;i++) res=max(res,f[i][n+i]);
    cout << res;
    return 0;
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章