学习情况:
最近学习了区间dp,感觉这个dp比普通的dp稍微难了一点。也可能是我看的题目还比较少,所以对这个内容的理解还不是非常透彻(要达到透彻也不是我看这几个题能达到的)。
现在我也就会一点例题的微微微改,一旦改动大了一点点,我就不知道该怎么办。这次的区间dp给我的感觉就是套公式,但是最重要的就是公式中间对内容的应用。其实上课之前看了几个过去的例题,感觉还是有点意思的,但是要掌握自己独立写出代码还是有点难度。
总感觉最近上课听讲越来越难以专注了,不知道是为什么。可能是太晚睡觉?果然还是尽量少熬夜的好。不只是C++,别的科目我也不想落下,所以生活艰辛呀!一定要努力!
区间dp基本内容
区间dp顾名思义就是在区间上动态规划。一般是根据问题设出一个表示状态的dp,可以是二维的也可以是三维的,一般情况下为二维,用来表示从i到j这段区间上的最优值。
dp[i][j]可以由子区间的状态转移而来,关键是dp[i][j]表示的是什么,然后去找dp[i][j]和子区间的关系。并且在求dp[i][j]之前,i到j之间的子区间都已经求出来最优解。 这就是说,由小区间的最优解递推出大区间的最优解。
有了前面的动态规划铺垫之后,这个应该比以前的稍稍好理解一点点。但是状态转移那里总感觉更难了呢……可能是我有点放弃思考了。
无脑代码:
//一般区间DP实现代码
memset(dp, 0x3f, sizeof(dp)); //求最小值时的赋值,若求最大值赋值为0
for (int i = 1; i <= n; i++) //区间长度为1的初始化
dp[i][i] = 0;
for (int len = 2; len <= n; len++) //枚举区间长度
{
for (int i = 1, j = len; j <= n; i++, j++) //区间[i,j]
{
//DP方程实现
for(int k=i;k<=j;k++)
{
}
}
}
比较经典的例题:
(1)。有N堆石子排成一排,每堆石子有一定的数量。现要将N堆石子并成为一堆。合并的过程只能每次将相邻的两堆石子堆成一堆,每次合并花费的代价为这两堆石子的和,经过N-1次合并后成为一堆。求出总的代价最小值。
如果我们有n堆石头,合并的最后一次操作一定是从两堆合并成一堆:
例如3堆,花费为min(a[1] + a[2], a[2] + a[3]) + sum[3];//sum[3]表示三堆总的花费,无论怎么摆都是不变的!所以这样加
规定dp[i][j]为合并第i堆到第j堆的最小花费
所以DP方程为:
dp[i][j] = min(dp[i][k] + dp[k+1][j]) + sum[j] - sum[i-1] (i <= k < j)
完整代码:
memset(dp, 0x3f, sizeof(dp));//求最小值
for (int i = 1; i <= n; i++)
dp[i][i] = 0;
for (int len = 2;len<=n;len++)//区间长度
{
for (int i =1,j=len;j<=n;i++,j++)//区间一点点挪动,每个区间都试试是否满足
{
for (int k=i;k<=j;k++)
{
dp[i][j] =min(dp[i][j],dp[i][k]+dp[k+1][j]+sum[j]-sum[i-1]);//sum是为了加上这个区间最后所需要的一堆体力
}
}
}
cout<<dp[1][n]<<endl;//从1到n这个大区间的最小值
(2)。给出一串的只有‘(’ ‘)’ '[‘ ']'四种括号组成的串,让你求解需要最少添加括号数让串中的所有括号完全匹配。dp[i][j]表示i到j这段的最大匹配度,检测a[i]与a[j]是否匹配,匹配的话dp[i][j]=dp[i+1][j-1]+2,否则任意分为两个区间按第一种模型处理。
代码:
int main()
{
string s;
while(cin>>s&&s!="end")
{
memset(dp,0,sizeof(dp)); //需要匹配度最大值
for(int l=1;l<s.size();l++)//因为字符串从a[0]开始,不是我熟悉的a[1],所以跟我一样的同学要注意一下这个地方。
{
for(int i=0,j=l;j<s.size();j++,i++)
{
if(s[i]=='('&&s[j]==')'||s[i]=='['&&s[j]==']')
{
dp[i][j]=dp[i+1][j-1]+2;
}
for(int k=i;k<j;k++)
{
dp[i][j]=max(dp[i][k]+dp[k+1][j],dp[i][j]);//标准状态转移方程,为了将最长的长度一个个的代下来以便于下次的加和。就是说dp[i-1][j]的值决定了dp[i][j]的值。
}
}
}
cout<<dp[0][s.size()-1]<<endl;
(3)。n个字符组成长度为m的字符串,给出增删字符的花费,可在字符串任意位置增删字符,求把字符串修改成回文串的最小花费。
规定dp[i][j]为将ij区间改成回文串的最小花费,[i+1,j]前面的字符加一个使状态方程的i后移,或是前面的i减去,结果仍然会是保存的字符后移了一位,[i][j-1]类似。
当a[i]==a[j]时,dp[i][j]=dp[i-1][j-1];
完整代码:
const int MAXN = 2005;
char a[MAXN], ch;
int dp[MAXN][MAXN];
int add[30],sub[30];
int main()
{
int n, m;
scanf("%d %d", &n, &m);
scanf("%s", a+1);
for (int i = 1; i <= n; i++)
{
scanf(" %c", &ch);
scanf("%d %d", &add[ch-'a'], &sub[ch-'a']);//增加或者减少这些ASCII码值需要的花费
}
memset(dp, 0x3f, sizeof(dp));//因为要求最小值
for (int i = 1; i <= m; i++)
dp[i][i] = 0;
for (int len = 2; len <= m; len++)
for(int i = 1, j = len; j <= m; i++, j++)
{
dp[i][j] = min(dp[i][j], min(add[a[i]-'a'],sub[a[i]-'a']) + dp[i+1][j]);//前面字符的删减
dp[i][j] = min(dp[i][j], dp[i][j-1] + min(add[a[j]-'a'],sub[a[j]-'a']));//后面字符的删减
if (a[i] == a[j])//字符相等时直接赋值
{
if (len==2) //才开始的时候要变成从零开始,否则是个极大值。
dp[i][j] = 0;
else
dp[i][j] = min(dp[i][j], dp[i+1][j-1]);
}
}
printf("%d\n", dp[1][m]);