参考博文:区间dp小结(附经典例题)
首先,什么是区间dp?它是干什么的?
- 先在小区间进行DP得到最优解,然后再利用小区间的最优解合并求大区间的最优解
- 操作往往涉及到区间合并问题
以上。
模板如下:
1 //mst(dp,0) 初始化DP数组 2 for(int i=1;i<=n;i++) 3 { 4 dp[i][i]=初始值 5 } 6 for(int len=2;len<=n;len++) //区间长度 7 for(int i=1;i<=n;i++) //枚举起点 8 { 9 int j=i+len-1; //区间终点 10 if(j>n) break; //越界结束 11 for(int k=i;k<j;k++) //枚举分割点,构造状态转移方程 12 { 13 dp[i][j]=max(dp[i][j],dp[i][k]+dp[k+1][j]+w[i][j]); 14 } 15 }
注意区间的枚举起点。
例题1:51Nod1021 石子合并
题意:
N堆石子摆成一条线。现要将石子有次序地合并成一堆。规定每次只能选相邻的2堆石子合并成新的一堆,并将新的一堆石子数记为该次合并的代价。计算将N堆石子合并成一堆的最小代价。
首先,假如合并的次序没有限制,那么我们将每堆石子看成一个叶结点,每次合并的代价为数中结点,采用贪心的策略,则整个合并过程就是一个哈夫曼树的建树过程。
但是这里合并的时候要求每次只能合并相邻的两堆,则贪心这里就会出错了,因此我们采用dp的思想进行求解。
我们用dp[i][j]来表示合并第i堆到第j堆石子的最小代价,那么状态转移方程为:
dp[i][j]=min(dp[i][j],dp[i][k]+dp[k+1][j]+w[i][j]);
其中w[i][j]表示把两部分合并起来的代价,即从第i堆到第j堆石子个数的和,为了方便查询,我们可以用sum[i]表示从第1堆到第i堆的石子个数和,那么w[i][j]=sum[j]-sum[i-1].
代码如下:
1 #include <cstdio> 2 #include <queue> 3 #include <cstring> 4 #include <algorithm> 5 using namespace std; 6 #define mst(a,b) memset((a),(b),sizeof(a)) 7 #define rush() int T;scanf("%d",&T);while(T--) 8 9 typedef long long ll; 10 const int maxn = 205; 11 const ll mod = 1e9+7; 12 const ll INF = 1e18; 13 const double eps = 1e-9; 14 15 int n,x; 16 int sum[maxn]; 17 int dp[maxn][maxn]; 18 19 int main() 20 { 21 while(~scanf("%d",&n)) 22 { 23 sum[0]=0; 24 mst(dp,0x3f); 25 for(int i=1;i<=n;i++) 26 { 27 scanf("%d",&x); 28 sum[i]=sum[i-1]+x; 29 dp[i][i]=0; 30 } 31 for(int len=2;len<=n;len++) 32 for(int i=1;i<=n;i++) 33 { 34 int j=i+len-1; 35 if(j>n) continue; 36 for(int k=i;k<j;k++) 37 { 38 dp[i][j]=min(dp[i][j],dp[i][k]+dp[k+1][j]+sum[j]-sum[i-1]); 39 } 40 } 41 printf("%d ",dp[1][n]); 42 } 43 return 0; 44 }
当然有的时候直接这样裸着O(n^3)会T,所以我们有平行四边形优化版本:
由于状态转移时是三重循环的,我们想能否把其中一层优化呢?尤其是枚举分割点的那个,显然我们用了大量的时间去寻找这个最优分割点,所以我们考虑把这个点找到后保存下来
用s[i][j]表示区间[i,j]中的最优分割点,那么第三重循环可以从[i,j-1)优化到【s[i][j-1],s[i+1][j]】。(这个时候小区间s[i][j-1]和s[i+1][j]的值已经求出来了,然后通过这个循环又可以得到s[i][j]的值)。
代码如下:
1 #include <cstdio> 2 #include <queue> 3 #include <cstring> 4 #include <algorithm> 5 using namespace std; 6 #define mst(a,b) memset((a),(b),sizeof(a)) 7 #define rush() int T;scanf("%d",&T);while(T--) 8 9 typedef long long ll; 10 const int maxn = 205; 11 const ll mod = 1e9+7; 12 const ll INF = 1e18; 13 const double eps = 1e-9; 14 15 int n,x; 16 int sum[maxn]; 17 int dp[maxn][maxn]; 18 int s[maxn][maxn]; 19 20 int main() 21 { 22 while(~scanf("%d",&n)) 23 { 24 sum[0]=0; 25 mst(dp,0x3f); 26 for(int i=1;i<=n;i++) 27 { 28 scanf("%d",&x); 29 sum[i]=sum[i-1]+x; 30 dp[i][i]=0; 31 s[i][i]=i; 32 } 33 for(int len=2;len<=n;len++) 34 for(int i=1;i<=n;i++) 35 { 36 int j=i+len-1; 37 if(j>n) continue; 38 for(int k=s[i][j-1];k<=s[i+1][j];k++) 39 { 40 if(dp[i][k]+dp[k+1][j]+sum[j]-sum[i-1]<dp[i][j]) 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 printf("%d ",dp[1][n]); 48 } 49 return 0; 50 }
例题2:hdu3506 猴子派对
题意:
问题转化后其实就是环形石子合并,即现在有围成一圈的若干堆石子,其他条件跟其那面那题相同,问合并所需最小代价。
解法:
把前n-1堆石子一个个移到第n个后面,那样环就变成了线,即现在有2*n-1堆石子需要合并,我们只要求下面的式子即可。求法与上面那题完全一样。
这道题在uestc版本上不做平行四边形优化会T。
1 #include<cstdio> 2 #include<algorithm> 3 #include<string.h> 4 using namespace std; 5 typedef long long ll; 6 const int inf = 99999999; 7 const int maxn = 10000; 8 int a[maxn]; 9 int sum[maxn], dp[maxn][maxn], s[maxn][maxn]; 10 11 int main() { 12 int n; 13 while (scanf("%d", &n) != EOF) { 14 for (int i = 1; i <= n; i++) { 15 scanf("%d", &a[i]); 16 a[n + i] = a[i]; 17 } 18 for (int i = 1; i <= 2 * n; i++) 19 sum[i] = sum[i - 1] + a[i]; 20 for (int m = 1; m <= n; ++m) { 21 for (int i = 0; i + m - 1 < 2 * n; ++i) { 22 int j = i + m - 1; 23 if (m == 1) { 24 dp[i][i] = 0; 25 s[i][i] = i; 26 } 27 else { 28 dp[i][j] = inf; 29 for (int k = s[i][j - 1]; k <= s[i + 1][j]; ++k) { 30 if (dp[i][j] > dp[i][k] + dp[k + 1][j] + sum[j] - sum[i - 1]) { 31 dp[i][j] = dp[i][k] + dp[k + 1][j] + sum[j] - sum[i - 1]; 32 s[i][j] = k; 33 } 34 } 35 } 36 } 37 } 38 int ans = inf; 39 for (int i = 1; i <= n; ++i) { 40 ans = min(ans, dp[i][n + i - 1]); 41 } 42 printf("%d ", ans); 43 } 44 return 0; 45 }
例题3:hdu5115 Dire wolf
题意:
有一排狼,每只狼有一个伤害A,还有一个伤害B。杀死一只狼的时候,会受到这只狼的伤害A和这只狼两边的狼的伤害B的和。如果某位置的狼被杀,那么杀它左边的狼时就会收到来自右边狼的B,因为这两只狼是相邻的了。求杀掉一排狼的最小代价。
解法:
设dp[i][j]为消灭编号从i到j只狼的代价,那么结果就是dp[1][n],枚举k作为最后一只被杀死的狼,此时会受到a[k]和b[i-1] b[j+1]的伤害,取最小的即可
可列出转移方程:
dp[i][j]=min(dp[i][j], dp[i][k-1]+dp[k+1][j]+a[k]+b[i-1]+b[j+1]);
dp[i][i]=a[i]+b[i-1]+b[i+1];
代码如下:
1 #include<stdio.h> 2 #include<iostream> 3 #include<string.h> 4 #include<algorithm> 5 #include<math.h> 6 using namespace std; 7 const int N=220 8 const int INF=0xfffffff 9 10 int main() 11 { 12 int T, n, a[N], b[N], dp[N][N], t=1; 13 scanf("%d", &T); 14 while(T--) 15 { 16 memset(a, 0, sizeof(a)); 17 memset(b, 0, sizeof(b)); 18 scanf("%d", &n); 19 for(int i=1; i<=n; i++) 20 scanf("%d", &a[i]); 21 for(int i=1; i<=n; i++) 22 scanf("%d", &b[i]); 23 for(int i=1; i<=n; i++) 24 { 25 for(int j=i; j<=n; j++) 26 dp[i][j]=INF; 27 /// dp[i][i]=a[i]+b[i-1]+b[i+1]; 28 } 29 for(int l=0; l<=n; l++)//注意边界 30 { 31 for(int i=1; i+l<=n; i++) 32 { 33 int j=i+l; 34 for(int k=i; k<=j; k++) 35 { 36 dp[i][j]=min(dp[i][j], dp[i][k-1]+dp[k+1][j]+a[k]+b[i-1]+b[j+1]); 37 } 38 } 39 } 40 printf("Case #%d: %d ", t++, dp[1][n]); 41 } 42 return 0; 43 }