專題訓練之區間DP


 

例題:以下例題部分的內容來自https://blog.csdn.net/my_sunshine26/article/details/77141398

一、石子合並問題

1.(NYOJ737)http://acm.nyist.edu.cn/JudgeOnline/problem.php?pid=737

分析:我們dp[i][j]來表示合並第i堆到第j堆石子的最小代價。那么狀態轉移方程為dp[i][j]=min(dp[i][k]+dp[k+1][j]+w[i][j])  (s[i][j-1]<=k<=s[i+1][j])

 其中w[i][j]表示把兩部分合並起來的代價,即從第i堆到第j堆石子個數的和,為了方便查詢,我們可以用sum[i]表示從第1堆到第i堆的石子個數和,那么w[i][j]=sum[j]-sum[i-1].

用s[i][j]表示區間[i,j]中的最優分割點,那么第三重循環可以從[i,j-1)優化到【s[i][j-1],s[i+1][j]】

 1 #include<cstdio>
 2 #include<cstring>
 3 #include<algorithm>
 4 using namespace std;
 5 typedef long long ll;
 6 const int maxn=210;
 7 const ll inf=1e18;
 8 ll dp[maxn][maxn];
 9 ll sum[maxn],a[maxn];
10 int s[maxn][maxn];
11 
12 int main()
13 {
14     int n,i,j,k,x,y,z,len;
15     while ( scanf("%d",&n)!=EOF )
16     {
17         for ( i=1;i<=n;i++ )
18         {
19             for ( j=1;j<=n;j++ ) dp[i][j]=inf;
20             dp[i][i]=0;
21             s[i][i]=i;
22         }
23         sum[0]=0;
24         for ( i=1;i<=n;i++ ) 
25         {
26             scanf("%lld",&a[i]);
27             sum[i]=a[i]+sum[i-1];
28         }
29         for ( len=2;len<=n;len++ )
30         {
31             for ( i=1;i<=n;i++ )
32             {
33                 j=len+i-1;
34                 if ( j>n ) break;
35                 for ( k=s[i][j-1];k<=s[i+1][j];k++ ) 
36                 {
37                     if ( dp[i][j]>dp[i][k]+dp[k+1][j]+sum[j]-sum[i-1] )
38                     {
39                         dp[i][j]=dp[i][k]+dp[k+1][j]+sum[j]-sum[i-1];
40                         s[i][j]=k;
41                     }
42                 }
43             }
44         }
45         printf("%lld\n",dp[1][n]);
46     }
47     return 0;
48 } 
NYOJ737

 

2.(HDOJ3506)http://acm.hdu.edu.cn/showproblem.php?pid=3506

題意:上一題的升級版,將上一層的線性變成一個圈。這時候我們只需要將N變成n=2*N-1即可,最后ans=min(dp[i][i+n-1])

 1 #include<cstdio>
 2 #include<cstring>
 3 #include<algorithm>
 4 using namespace std;
 5 typedef long long ll;
 6 const int maxn=2010;
 7 const ll inf=1e18;
 8 ll dp[maxn][maxn];
 9 ll sum[maxn],a[maxn];
10 int s[maxn][maxn];
11 
12 int main()
13 {
14     int n,i,j,k,x,y,z,len,N;
15     ll ans;
16     while ( scanf("%d",&N)!=EOF )
17     {
18         n=2*N-1;
19         for ( i=1;i<=n;i++ )
20         {
21             for ( j=1;j<=n;j++ ) dp[i][j]=inf;
22             dp[i][i]=0;
23             s[i][i]=i;
24         }
25         sum[0]=0;
26         for ( i=1;i<=N;i++ ) 
27         {
28             scanf("%lld",&a[i]);
29             sum[i]=a[i]+sum[i-1];
30         }
31         for ( i=1;i<N;i++ ) sum[i+N]=a[i]+sum[i+N-1];
32         for ( len=2;len<=N;len++ )
33         {
34             for ( i=1;i<=n;i++ )
35             {
36                 j=len+i-1;
37                 if ( j>n ) break;
38                 for ( k=s[i][j-1];k<=s[i+1][j];k++ )
39                 {
40                     if ( dp[i][j]>dp[i][k]+dp[k+1][j]+sum[j]-sum[i-1] )
41                     {
42                         dp[i][j]=dp[i][k]+dp[k+1][j]+sum[j]-sum[i-1];
43                         s[i][j]=k;
44                     }
45                 }
46             }
47         }
48         ans=inf;
49         for ( i=1;i<=N;i++ )
50         {
51             j=i+N-1;
52             ans=min(ans,dp[i][j]);
53         }
54         printf("%lld\n",ans);
55     }
56     return 0;
57 } 
HDOJ3506

 

二、括號匹配問題

1.(POJ2955)http://poj.org/problem?id=2955

題意:給出一個的只有'(',')','[',']'四種括號組成的字符串,求最多有多少個括號滿足題目里所描述的完全匹配。

分析:用dp[i][j]表示區間[i,j]里最大完全匹配數。只要得到了dp[i][j],那么就可以得到dp[i-1][j+1]  dp[i-1][j+1]=dp[i][j]+(s[i-1]於s[j+1]匹配?2:0).

然后利用狀態轉移方程更新一下區間最優解即可。dp[i][j]=max(dp[i][j],dp[i][k]+dp[k+1][j])

 1 #include<cstdio>
 2 #include<cstring>
 3 #include<algorithm>
 4 using namespace std;
 5 typedef long long ll;
 6 const int maxn=105;
 7 char s[maxn];
 8 ll dp[maxn][maxn];
 9 
10 int main()
11 {
12     int n,i,j,k,x,y,z,len;
13     while ( scanf("%s",s+1)!=EOF && s[1]!='e' )
14     {
15         n=strlen(s+1);
16         memset(dp,0,sizeof(dp));
17         for ( len=2;len<=n;len++ )
18         {
19             for ( i=1;i<=n;i++ )
20             {
21                 j=i+len-1;
22                 if ( j>n ) break;
23                 if ( (s[i]=='('&&s[j]==')') || (s[i]=='['&&s[j]==']') ) dp[i][j]=dp[i+1][j-1]+2;
24                 for ( k=i;k<j;k++ ) dp[i][j]=max(dp[i][j],dp[i][k]+dp[k+1][j]);
25             }
26         }
27         printf("%lld\n",dp[1][n]);
28     }
29     return 0;    
30 } 
POJ2955

 

2.(NYOJ15)http://acm.nyist.edu.cn/JudgeOnline/problem.php?pid=15

分析:最少添加的括號數=總括號-最大匹配的括號數,代碼於上一題基本一致

 1 #include<cstdio>
 2 #include<cstring>
 3 #include<algorithm>
 4 using namespace std;
 5 typedef long long ll;
 6 const int maxn=105;
 7 char s[maxn];
 8 ll dp[maxn][maxn];
 9 
10 int main()
11 {
12     int n,i,j,k,x,y,z,len,T;
13     scanf("%d",&T);
14     while ( T-- )
15     {
16         scanf("%s",s+1);
17         n=strlen(s+1);
18         memset(dp,0,sizeof(dp));
19         for ( len=2;len<=n;len++ )
20         {
21             for ( i=1;i<=n;i++ )
22             {
23                 j=i+len-1;
24                 if ( j>n ) break;
25                 if ( (s[i]=='('&&s[j]==')') || (s[i]=='['&&s[j]==']') ) dp[i][j]=dp[i+1][j-1]+2;
26                 for ( k=i;k<j;k++ ) dp[i][j]=max(dp[i][j],dp[i][k]+dp[k+1][j]);
27             }
28         }
29         printf("%lld\n",n-dp[1][n]);
30     }
31     return 0;    
32 } 
NYOJ15

 

三、整數划分問題

1.(NYOJ746)http://acm.nyist.edu.cn/JudgeOnline/problem.php?pid=746

分析:用dp[i][j]表示從第一位到第i位共插入j個乘號后乘積的最大值。根據區間DP的思想我們可以從插入較少乘號的結果算出插入較多乘號的結果。

 方法是當我們要放第j的乘號時枚舉放的位置。狀態轉移方程為dp[i][j]=max(dp[i][j],dp[k][j-1]*num[k+1][i])。其中num[i][j]表示從s[i]到s[j]這段連續區間代表的數值。

 1 #include<cstdio>
 2 #include<cstring>
 3 #include<algorithm>
 4 using namespace std;
 5 typedef long long ll;
 6 const int maxn=20;
 7 ll dp[maxn][maxn];
 8 ll num[maxn][maxn];
 9 
10 int main()
11 {
12     int T,n,m,i,j,k,x,y,z;
13     char s[maxn];
14     scanf("%d",&T);
15     while ( T-- )
16     {
17         scanf("%s%d",s+1,&m);
18         n=strlen(s+1);
19         memset(dp,0,sizeof(dp));
20         for ( i=1;i<=n;i++ )
21         {
22             num[i][i]=s[i]-'0';
23             for ( j=i+1;j<=n;j++ ) num[i][j]=num[i][j-1]*10+s[j]-'0';
24         }
25         for ( i=1;i<=n;i++ ) dp[i][0]=num[1][i];
26         for ( j=1;j<m;j++ )
27         {
28             for ( i=1;i<=n;i++ )
29             {
30                 for ( k=1;k<i;k++ ) dp[i][j]=max(dp[i][j],dp[k][j-1]*num[k+1][i]);
31             }
32         }
33         printf("%lld\n",dp[n][m-1]);
34     }
35     return 0;
36 }
NYOJ746

 

 

習題:

1.(HDOJ4632)http://acm.hdu.edu.cn/showproblem.php?pid=4632

題意:給定一個字符串,求這個字符串中包含多少回文子串(子串可以不連續)

分析;dp[i][j]表示從第i個字符到第j個字符中包含的回文子串的個數。初始化時dp[i][i]=1(因為自己本身也算做一個回文串),其他dp[i][j]=0

dp[i][j]=(dp[i+1][j]+dp[i][j-1]-dp[i+1][j-1]+mod)%mod (容斥思想)

if ( s[i]==s[j] ) dp[i][j]+=dp[i+1][j-1]+1

 1 #include<cstdio>
 2 #include<cstring>
 3 #include<algorithm>
 4 using namespace std;
 5 typedef long long ll;
 6 const int maxn=1010;
 7 const int mod=10007;
 8 char s[maxn];
 9 int dp[maxn][maxn];
10 
11 int main()
12 {
13     int T,i,j,k,h,n,m,ans,len;
14     scanf("%d",&T);
15     for ( h=1;h<=T;h++ )
16     {
17         scanf("%s",s+1);
18         n=strlen(s+1);
19         memset(dp,0,sizeof(dp));
20         for ( i=1;i<=n;i++ ) dp[i][i]=1;
21         for ( len=2;len<=n;len++ )
22         {
23             for ( i=1;i<=n;i++ )
24             {
25                 j=i+len-1;
26                 if ( j>n ) break;
27                 dp[i][j]=(dp[i][j-1]+dp[i+1][j]-dp[i+1][j-1]+mod)%mod;
28                 if ( s[i]==s[j] ) dp[i][j]=(dp[i][j]+dp[i+1][j-1]+1+mod)%mod;
29             }
30         }
31         printf("Case %d: %d\n",h,dp[1][n]%mod);
32     }
33     return 0;
34 }
HDOJ4632

 

2.(HDOJ4745)http://acm.hdu.edu.cn/showproblem.php?pid=4745

題意:求最長非連續回文串

分析:先將環變成鏈,dp[i][j]表示區間(i,j)范圍內最長的非連續回文串的長度,轉移時dp[i][j]=max(max(dp[i+1][j],dp[i][j-1]),x) 當s[i]==s[j]時x=dp[i+1][j-1]+2,否則x=dp[i+1][j-1]

最后的答案在dp[i][i+N-1]中和dp[i][N-2]+1(共起點的情況)中去尋找

 1 #include<cstdio>
 2 #include<cstring>
 3 #include<algorithm>
 4 using namespace std;
 5 const int maxn=2010;
 6 int a[maxn];
 7 int dp[maxn][maxn];
 8 
 9 int main()
10 {
11     int T,i,j,k,N,n,m,x,y,z,ans,len;
12     while (  scanf("%d",&N)!=EOF && N )
13     {
14         
15         n=2*N-1;
16         for ( i=1;i<=N;i++ ) scanf("%d",&a[i]);
17         for ( i=1;i<N;i++ ) a[i+N]=a[i];
18         memset(dp,0,sizeof(dp));
19         for ( i=1;i<=n;i++ ) dp[i][i]=1;
20         ans=0;
21         for ( len=2;len<=n;len++ )
22         {
23             for ( i=1;i<=n;i++ )
24             {
25                 j=i+len-1;
26                 if ( j>n ) break;
27                 int x=dp[i+1][j-1];
28                 if ( a[i]==a[j] ) x+=2;
29                 dp[i][j]=max(max(dp[i+1][j],dp[i][j-1]),x);
30             }
31         }
32         for ( i=1;i<=N;i++ ) ans=max(ans,dp[i][i+N-1]);
33         for ( i=1;i<=N+1;i++ ) ans=max(ans,dp[i][i+N-2]+1);
34         printf("%d\n",ans);
35     }
36     return 0;
37 }
HDOJ4745

 

3.(HDOJ2476)http://acm.hdu.edu.cn/showproblem.php?pid=2476

推薦此博客:https://blog.csdn.net/a601025382s/article/details/12379565

題意:給定兩個字符串a和b,求最少需要對a進行多少次操作,才能將a變成b。每次操作時將a中任意一段變成任意一個字母所組成的段。

題解:動態規划題。dp[i][j]表示a中i到j段變成b需要的最少次數。遞推公式:dp[i][j]=min(dp[i][k]+dp[k+1][j])(i<=k<j)。接着就是判斷分界點了,對於字符串b,只有將相同字符一起刷才能減少操作數。所以每次碰到b[i]==b[k]時,可以減少一次操作,因為刷一次[i,k]再刷[i+1,k-1]和分別刷[i,i][k,k],[i+,k,k+1]是一樣的,可操作數會減少。

注意:由於如果一段子串兩端相等,會成端更新,從而改變中間子串的字符,所以處理時可假定所以a中單個字符都需要一次變化才能變成b。之后動態規划完成后再處理a和b中形同位置相同字符的情況。

另一種理解方式:不考慮起始串,將起始串默認為空串,找出所有dp值(dp[i][j]表示i到j這段空子串轉換成目標串需要的最小次數)后,再通過ans[i]來求得最小變換值。ans[i]表示前i+1長度的子串轉換成目標串需要的最小次數。

 1 #include<cstdio>
 2 #include<cstring>
 3 #include<algorithm>
 4 using namespace std;
 5 const int maxn=105;
 6 const int inf=1e9;
 7 int dp[maxn][maxn],ans[maxn];
 8 char s1[maxn],s2[maxn];
 9 
10 int main()
11 {
12     int i,j,k,x,y,z,n,len;
13     while ( scanf("%s%s",s1+1,s2+1)!=EOF )
14     {
15         n=strlen(s1+1);
16         memset(dp,0,sizeof(dp));
17         for ( i=1;i<=n;i++ ) dp[i][i]=1;
18         for ( len=2;len<=n;len++ )
19         {
20             for ( i=1;i<=n;i++ )
21             {
22                 j=i+len-1;
23                 if ( j>n ) break;
24                 dp[i][j]=dp[i+1][j]+1;
25                 for ( k=i+1;k<=j;k++ )
26                 {
27                     if ( s2[i]==s2[k] ) dp[i][j]=min(dp[i][j],dp[i+1][k]+dp[k+1][j]);
28                 }
29             }
30         }
31         for ( i=1;i<=n;i++ ) ans[i]=dp[1][i];
32         for ( i=1;i<=n;i++ )
33         {
34             if ( s1[i]==s2[i] ) 
35             {
36                 if ( i==1 ) ans[i]=0;
37                 else ans[i]=ans[i-1];
38             }
39             else
40             {
41                 for ( k=1;k<=i;k++ ) ans[i]=min(ans[i],ans[k]+dp[k+1][i]);
42             }
43         }
44         printf("%d\n",ans[n]);
45     }
46     return 0;
47 }
HDOJ2476

 

4.(HDOJ5115)http://acm.hdu.edu.cn/showproblem.php?pid=5115

題意:有一排狼,每只狼有一個傷害A,還有一個傷害B。殺死一只狼的時候,會受到這只狼的傷害A和這只狼兩邊的狼的傷害B的和。如果某位置的狼被殺,那么殺它左邊的狼時就會收到來自右邊狼的B,因為這兩只狼是相鄰的了。求殺掉一排狼的最小代價。

分析:因為殺死一只狼所受的傷害總數是固定所以單獨記錄即可,只用考慮受到旁邊狼攻擊的傷害。dp[i][j]表示殺死區間區內[i,j]內的狼所需要的最小代價.

對於區間[i,j]我們只需要枚舉出哪匹狼是需要最后殺死的即可。dp[i][j]=min(dp[i][k-1]+dp[k+1][j]+a[i-1]+a[j+1]) (i<k<j,邊界單獨考慮)

 1 #include<cstdio>
 2 #include<cstring>
 3 #include<algorithm>
 4 using namespace std;
 5 typedef long long ll;
 6 const int maxn=205;
 7 const ll inf=1e15;
 8 ll dp[maxn][maxn];
 9 int a[maxn];
10 
11 int main()
12 {
13     int T,n,m,i,j,k,h,x,y,z,len;
14     ll sum;
15     scanf("%d",&T);
16     for ( h=1;h<=T;h++ )
17     {
18         scanf("%d",&n);
19         sum=0;
20         for ( i=1;i<=n;i++ ) 
21         {
22             scanf("%d",&x);
23             sum+=x;
24         }
25         for ( i=1;i<=n;i++ ) scanf("%d",&a[i]);
26         a[0]=a[n+1]=0;
27         for ( i=1;i<=n;i++ )
28         {
29             for ( j=1;j<=n;j++ ) dp[i][j]=inf;
30             dp[i][i]=a[i-1]+a[i+1];
31         }
32         for ( len=2;len<=n;len++ )
33         {
34             for ( i=1;i<=n;i++ )
35             {
36                 j=i+len-1;
37                 if ( j>n ) break;
38                 x=a[i-1]+a[j+1];
39                 dp[i][j]=min(dp[i+1][j]+x,dp[i][j-1]+x);
40                 for ( k=i+1;k<j;k++ ) dp[i][j]=min(dp[i][j],dp[i][k-1]+dp[k+1][j]+x);
41             }
42         }
43         sum+=dp[1][n];
44         printf("Case #%d: %lld\n",h,sum);
45     }
46     return 0;
47 }
HDOJ5115

 

5.(HDOJ4283)http://acm.hdu.edu.cn/showproblem.php?pid=4283

推薦此博客:https://www.cnblogs.com/kedebug/archive/2012/12/10/2811053.html

題意:

給定一個序列,序列內的人有屌絲值Di,第i個人如果是第k個出場,那么他的屌絲值為Di * (k-1),  但是導演可以通過一個棧來調整序列里面人的出場順序。

求一個出場序列使總屌絲值最小。

思路:導演對於這個出場順序的影響只是一定程度上的。比如說:

1. 第一個人第k個出場

2. 那么要求2~k的人都要在第一個人前面出場

3. k+1~n的人都要在k以后出場

明白了上面的過程,就可以定義區間dp[i, j]表示區間[i, j]在相對於i為起點情況下i在第k個出場的最小屌絲總值。

1. dp[i, j] = (k - i) * Di + dp[i+1, k]

2. dp[i, j] += (k + 1 - i) * (sum[j] - sum[k]) + dp[k+1, j]

 1 #include<cstdio>
 2 #include<cstring>
 3 #include<algorithm>
 4 using namespace std;
 5 typedef long long ll;
 6 const int maxn=105;
 7 const ll inf=1e18;
 8 ll dp[maxn][maxn],sum[maxn];
 9 int a[maxn];
10 
11 int main()
12 {
13     int T,i,j,k,h,x,y,z,ans,len,n;
14     scanf("%d",&T);
15     for ( h=1;h<=T;h++ )
16     {
17         scanf("%d",&n);
18         sum[0]=0;
19         for ( i=1;i<=n;i++ )
20         {
21             scanf("%d",&a[i]);
22             sum[i]=a[i]+sum[i-1];
23         }
24         for ( i=1;i<=n;i++ )
25         {
26             for ( j=i;j<=n;j++ ) dp[i][j]=inf;
27             dp[i][i]=0;
28         }
29         for ( len=2;len<=n;len++ )
30         {
31             for ( i=1;i<=n;i++ )
32             {
33                 j=i+len-1;
34                 if ( j>n ) break;
35                 dp[i][j]=min(dp[i+1][j]+sum[j]-sum[i],dp[i+1][j]+(j-i)*a[i]);
36                 for ( k=i+1;k<j;k++ ) 
37                     dp[i][j]=min(dp[i][j],dp[i+1][k]+a[i]*(k-i)+dp[k+1][j]+(sum[j]-sum[k])*(k-i+1));
38             }
39         }
40         printf("Case #%d: %lld\n",h,dp[1][n]);
41     }
42     return 0;
43 }
HDOJ4283

 

 

小結:區間DP過程大致相同,大都滿足第一層循環枚舉長度,第二層循環枚舉起點。最內層往往有兩種形式,第一種是需要在[i,j]中找一個分割點k使得將[i,j]分成[i,k]和[k+1,j]這樣兩個區間能夠得到最優解

第二種形式是[i,j]可以由[i,j-1]或者[i,j+1]轉移過來.重要的是找出新添加的元素(可以是k或者i)與之前那個len-1長度區間的關系


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM