區間dp模型(石子歸併,括號匹配,整數劃分)
區間dp顧名思義就是在一個區間上進行的一系列動態規劃。對一些經典的區間dp總結在這裡。
1) 石子歸併問題
描述:有N堆石子排成一排,每堆石子有一定的數量。現要將N堆石子併成為一堆。合併的過程只能每次將相鄰的兩堆石子堆成一堆,每次合併花費的代價為這兩堆石子的和,經過N-1次合併後成為一堆。求出總的代價最小值。
分析:要求n個石子歸併,我們根據dp的思想劃分成子問題,先求出每兩個合併的最小代價,然後每三個的最小代價,依次知道n個。
定義狀態dp [ i ] [ j ]為從第i個石子到第j個石子的合併最小代價。
那麼dp [ i ] [ j ] = min(dp [ i ] [ k ] + dp [ k+1 ] [ j ])
那麼我們就可以從小到大依次列舉讓石子合併,直到所有的石子都合併。
這個問題可以用到平行四邊形優化,用一個s【i】【j】=k 表示區間 i---j 從k點分開才是最優的,這樣的話我們就可以優化掉一層複雜度,變為O(n^2).
程式碼:
#include <cstdio> #include <cstring> #include <algorithm> #define N 210 int dp[N][N],sum[N]; int main() { int n; while(~scanf("%d",&n)) { int a[N];sum[0]=0; for(int i=1;i<=n;i++){ scanf("%d",&a[i]); sum[i]=sum[i-1]+a[i]; } memset(dp,0,sizeof(dp)); int i,j,l,k; for(l = 2; l <= n; ++l) { for(i = 1; i <= n - l + 1; ++i) { j = i + l - 1; dp[i][j] = 2100000000; for(k = i; k < j; ++k) { dp[i][j] = std::min(dp[i][j],dp[i][k] + dp[k + 1][j] + sum[j] - sum[i-1]); } } } printf("%d\n", dp[1][n]); } return 0; }
平行四邊形優化程式碼:
#include <cstdio> #include <cstring> #include <algorithm> #define N 210 int dp[N][N],sum[N],s[N][N]; int main() { int n; while(~scanf("%d",&n)) { int a[N];sum[0]=0; memset(s,0,sizeof(s)); for(int i=1;i<=n;i++){ scanf("%d",&a[i]); s[i][i]=i; sum[i]=sum[i-1]+a[i]; } memset(dp,0,sizeof(dp)); int i,j,l,k; for(l = 2; l <= n; ++l) { for(i = 1; i <= n - l + 1; ++i) { j = i + l - 1; dp[i][j] = 2100000000; for(k = s[i][j-1]; k <= s[i+1][j]; ++k) { if(dp[i][j]>dp[i][k] + dp[k + 1][j] + sum[j] - sum[i-1]) { dp[i][j]=dp[i][k] + dp[k + 1][j] + sum[j] - sum[i-1]; s[i][j]=k; } } } } printf("%d\n", dp[1][n]); } return 0; }
2)括號匹配
題目連結:
描述:給出一串的只有‘(’ ‘)’ '[‘ ']'四種括號組成的串,讓你求解需要最少新增括號數讓串中的所有括號完全匹配。
分析:我們求出這個串的最大匹配,然後串的總長度-最大匹配就是答案。
方法1:首先能想到的是轉化成LCS(最長公共子序列),列舉中間點,求所有的LCS中的最大值 * 2就是最大匹配。但是複雜度較高,光LCS一次就O(n^2)的複雜度。
方法2:
首先考慮怎麼樣定義dp讓它滿足具有通過子結構來求解、
定義dp [ i ] [ j ] 為串中第 i 個到第 j 個括號的最大匹配數目
那麼我們假如知道了 i 到 j 區間的最大匹配,那麼i+1到 j+1區間的是不是就可以很簡單的得到。
那麼 假如第 i 個和第 j 個是一對匹配的括號那麼dp [ i ] [ j ] = dp [ i+1 ] [ j-1 ] + 2 ;
那麼我們只需要從小到大列舉所有 i 和 j 中間的括號數目,然後滿足匹配就用上面式子dp,然後每次更新dp [ i ] [ j ]為最大值即可。
更新最大值的方法是列舉 i 和 j 的中間值,然後讓 dp[ i ] [ j ] = max ( dp [ i ] [ j ] , dp [ i ] [ f ] + dp [ f+1 ] [ j ] ) ;
程式碼:
#include <iostream>
#include <cstring>
#include <algorithm>
#include <string>
using namespace std;
const int N = 120;
int dp[N][N];
int main()
{
string s;
while(cin>>s)
{
if(s=="end")
break;
memset(dp,0,sizeof(dp));
for(int i=1;i<s.size();i++)
{
for(int j=0,k=i;k<s.size();j++,k++)
{
if(s[j]=='('&&s[k]==')' || s[j]=='['&&s[k]==']')
dp[j][k]=dp[j+1][k-1]+2;
for(int f=j;f<k;f++)
dp[j][k]=max(dp[j][k],dp[j][f]+dp[f+1][k]);
}
}
cout<<dp[0][s.size()-1]<<endl;
}
return 0;
}
3)整數劃分問題
題目描述:給出兩個整數 n , m ,要求在 n 中加入m - 1 個乘號,將n分成m段,求出這m段的最大乘積
分析:根據區間dp的思想,我們定義dp [ i ] [ j ]為從開始到 i 中加入 j 個乘號得到的最大值。
那麼我們可以依次計算加入1----m-1個乘號的結果
而每次放入x個乘號的最大值只需列舉第x個乘號的放的位置即可
dp [ i ] [ j ] = MAX (dp [ i ] [ j ] , dp [ k ] [ j-1 ] * a [ k+1 ] [ i ] ) ;
程式碼:
#include <cstdio>
#include <cstring>
#define MAX(a,b) a>b?a:b
long long a[20][20];
long long dp[25][25];
int main()
{
int T,m;
scanf("%d",&T);
getchar();
while(T--)
{
char s[22];
scanf("%s",s+1);
scanf("%d",&m);
int l=strlen(s),ok=1;
memset(a,0,sizeof(a));
for(int i=1;i<l;i++)
{
if(s[i]=='0')
ok=0;
for(int j=i;j<l;j++)
{
a[i][j]=a[i][j-1]*10+(s[j]-'0');
}
}
if(ok==0&&l-1==m||l-1<m)
{
printf("0\n");continue;
}
long long x,ans;
memset(dp,0,sizeof(dp));
for(int i=0;i<l;i++)
dp[i][1]=a[1][i];
ans=0;
if(m==1)
ans=dp[l-1][1];
for(int j=2;j<=m;j++)
{
for(int i=j;i<l;i++)
{
ans=a[i][i];
for(int k=1;k<i;k++)
{
dp[i][j]=MAX(dp[i][j],dp[k][j-1]*a[k+1][i]);
}
}
}
printf("%lld\n",dp[l-1][m]);
}
return 0;
}