区间DP常用模版
所有的区间dp问题,第一维都是枚举区间长度,一般 len = 1 用来初始化,枚举从 len = 2 开始,第二维枚举起点 i (右端点 j 自动获得,j = i + len - 1)
"预处理长度为1的情况"
for (int i = 1; i <= n; i++) {
dp[i][i] = "初始值";
}
"从长度为2的情况开始地推处理"
for (int len = 2; len <= n; len++) "区间长度"
for (int i = 1; i + len - 1 <= n; i++) {
"枚举起点"
int j = i + len - 1; "区间终点"
for (int k = i; k < j; k++) "枚举分割点,构造状态转移方程"
dp[i][j] = max(dp[i][j], dp[i][k] + dp[k + 1][j] + i到j的代价);
}
- 假设字符串起点下标为s,长度为len,终点下标为s+len-1
- 具体是求max还是求min依据题意而定
- 从i到j共有j-i种划分方式,最里面的for循环也可以写成
for(int k=i+1;k<=j;k++) dp[i][j]=max(dp[i][j], dp[i][k-1] + dp[k][j] + i到j的代价);
282. 石子合并
题目描述
设有N堆石子排成一排,其编号为1,2,3,…,N。
每堆石子有一定的质量,可以用一个整数来描述,现在要将这N堆石子合并成为一堆。
每次只能合并相邻的两堆,合并的代价为这两堆石子的质量之和,合并后与这两堆石子相邻的石子将和新堆相邻,合并时由于选择的顺序不同,合并的总代价也不相同。
问题是:找出一种合理的方法,使总的代价最小,输出最小代价。
输入格式
第一行一个数N表示石子的堆数N。
第二行N个数,表示每堆石子的质量(均不超过1000)。
输出格式
输出一个整数,表示最小代价。
数据范围
1≤N≤300
输入样例:
4
1 3 5 2
输出样例:
22
Hint:
有4堆石子分别为 1 3 5 2, 我们可以先合并1、2堆,代价为4,得到4 5 2, 又合并 1,2堆,代价为9,得到9 2 ,再合并得到11,总代价为4+9+11=24;
如果第二步是先合并2,3堆,则代价为7,得到4 7,最后一次合并代价为11,总代价为4+7+11=22。
分析:
核心: 这是每次合并都拆成两部分进行合并,进行合并的两部分里选取的又都是该部分中能选择的最小合并方式,最后一次的合并一定是左边连续的一部分和右边连续的一部分进行合并,选择其中的最小值。
但是不管k从哪里断开,分成怎样的两部分,从i到j最后一次合并的代价一定是a[i]+a[i+1]+……+a[j]
的和,这里采用前缀和的思想,s[j]-s[i-1]
即可。
从3到6合并的方式:(3,6) i=3,j=6
一、(3,3)+(4,6)
二、(3,4)+(5,6)
三、(3,5)+(6,6)
四、(3,6),等价于三,从3到6顺序合并,所以共有三种方式:j-i=6-3=3
初始化问题:
初始化长度为1的合并情况时,只有自己和自己,无法合并,所以代价为0,即dp[i][[i]=0
。
由于是求最小值,长度下都是未知的,所以初始化为无穷大。
代码实现
#include <iostream>
#include <cstring>
#define read(x) scanf("%d",&x)
using namespace std;
const int N=310,INF=0x3f3f3f3f;
int dp[N][N];
int s[N]; "s数组求前缀和"
int main() {
int n;
read(n);
for (int i=1;i<=n;i++) read(s[i]),s[i]+=s[i-1];
"输入数据并求出前缀和。"
"先初始化长度为1的情况,即dp[i][i],dp[i][i]初始化为0,其它情况下都是无穷大"
memset(dp,0x3f,sizeof dp);
for (int i=1;i<=n;i++) dp[i][i]=0;
"然后从长度2开始递推处理"
for (int len=2;len<=n;len++) "长度"
for (int i=1;i+len-1<=n;i++) {
"起点"
int l=i,r=i+len-1; "起点为i,终点为i+len-1"
for (int k=l;k<r;k++)
dp[l][r]=min(dp[l][r],dp[l][k]+dp[k+1][r]+s[r]-s[l-1]);
}
printf("%d",dp[1][n]);
return 0;
}
最终dp数组是一个上三角矩阵,上三角存储各种起点、终点下的最小合并代价,f[1][n]
就是我们需要的以1号为起点,n号为终点,长度为n的最小合并代价。
且斜对角线就表示长度,每一次for的len循环,就更新一次斜对角线,直到更新到最后一次dp[1][n]
。
除了按长度枚举,也可以倒着枚举,因为只要保证每种状态都被提前计算即可。
我们注意到求dp[i][j]
时,会用到dp[i-1]到dp[n]行的数据以及dp[i]行的dp[i][i]
到dp[i][j-1]
,我们也可以从上三角的最下面,dp[n][n]
开始枚举,从下到上逐行更新。
求dp[3,6]时,可以分为上面提到的三种合并方式,因此只会用到dp[3,3]+dp[4,6] 、 dp[3,4]+dp[5,6]、dp[3,5]+dp[6,6]
由于第n行只有dp[n][n]
,可以用以初始化,然后从第n-1行开始处理。
这样i就是起点,j就是终点。
for (int i=n-1;i>=1;i--) "起点"
for (int j=i;j<=n;j++) "终点"
for (int k=i;k<j;k++) //当j==i时就不会走这个for循环,或者直接让j=i+1开始循环
dp[i][j]=min(dp[i][j],dp[i][k]+dp[k+1][j]+s[j]-s[i-1]);
记忆化搜索
#include <iostream>
#include <cstring>
#define read(x) scanf("%d",&x)
using namespace std;
const int N=310,INF=0x3f3f3f3f;
int f[N][N];
int s[N];
int dp(int i,int j)
{
if (i==j) return 0;//长度为1的情况
int &v=f[i][j]; //v就是f[i][j]
if (v!=-1) return v; "该位置被处理过,就不再处理了,记忆化的体现"
v=INF;
for (int k=i;k<j;k++)
v=min(v,dp(i,k)+dp(k+1,j)+s[j]-s[i-1]);
return v;
}
int main() {
int n;
read(n);
for (int i=1;i<=n;i++) read(s[i]),s[i]+=s[i-1];
memset(f,-1,sizeof f);
printf("%d",dp(1,n));
return 0;
}