区间DP的引入
区间DP是一种特殊的DP,与普通的DP在阶段的划分上往往有所不同。
先来看个栗子:
这道题大家都做过,通过这一道题可以体会到区间DP的特殊:
他是按照子区间的长度划分阶段,不是按找起点终点等划分。
在很多DP题中,一个区间的答案往往取决于他的子区间的答案,于是区间DP就诞生了。
这道题的分析:(做过的巨佬请绕行)
贪心显然有问题,不采用。
表示区间 内能取到的最大值(在此不讨论最小值)。
显然,我们要么先将 的区间合并,反之合并 的区间,再加上区间 的总和(可以使用前缀和数组计算)。
方程:
阶段(区间DP重点):
无论按照起点还是终点划分阶段,均不能保证一个状态的子状态一定在它之前计算出来。
观察发现,一个区间的最大值依赖于他的子区间的最大值,那么我们可以按照长度划分阶段。
外层循环 枚举序列长度,内层循环 枚举序列起点。
for (int i = 2; i <= n; i ++)
for (int j = 1; j <= n; j ++)
{
int L = (j + i - 1) % n;//注意此题含环,显然用链表形式可以解决
if (L == 0) L = n;
for (int k = j; k != L; k = r[k])
d[j][L] = min(d[j][L], d[j][k] + d[r[k]][L] + s[j][L]);
}
完整AC代码:
#include <cstdio>
#include <algorithm>
#include <cstring>
using namespace std;
int d[105][105], a[105], s[105][105], l[105], r[105];
int main()
{
memset(d, 0x3f, sizeof(d));
int n;
scanf("%d", &n);
for (int i = 1; i <= n; i ++)
scanf("%d", a + i), d[i][i] = 0, r[i] = i + 1, l[i] = i - 1;
r[n] = 1, l[1] = n;
for (int i = 1; i <= n; i ++)
{
s[i][i] = a[i];
int j = r[i];
while (j != i)
s[i][j] = s[i][l[j]] + a[j], j = r[j];
}//前缀和数组
for (int i = 2; i <= n; i ++)
for (int j = 1; j <= n; j ++)
{
int L = (j + i - 1) % n;
if (L == 0) L = n;
for (int k = j; k != L; k = r[k])
d[j][L] = min(d[j][L], d[j][k] + d[r[k]][L] + s[j][L]);
}
int ans = 0x7fffffff, ans2 = -0x7fffffff;
for (int i = 1; i <= n; i ++)
ans = min(ans, d[i][l[i]]);
for (int i = 1; i <= n; i ++)
for (int j = 1; j <= n; j ++)
d[i][j] = -0x3fffffff;
for (int i = 1; i <= n; i ++)
d[i][i] = 0;
for (int i = 2; i <= n; i ++)
for (int j = 1; j <= n; j ++)
{
int L = (j + i - 1) % n;
if (L == 0) L = n;
for (int k = j; k != L; k = r[k])
d[j][L] = max(d[j][L], d[j][k] + d[r[k]][L] + s[j][L]);
}
for (int i = 1; i <= n; i ++)
ans2 = max(ans2, d[i][l[i]]);
printf("%d\n%d", ans, ans2);
}
注意对环的处理,此处使用的是链表。
此题应用的前缀和,很多DP题目都会用到,使用比较方便,优化效果明显,适用范围不是特别大,但在DP中使用很多。
例题
显然此题要高精(会int128也行),但是重点不在这里……
由于本人懒,没写高精,虽然之前写了个高精度类也懒得去复制,反正重点不在这里。
总之在NKOJ上水过了(众所周知NKOJ数据特别良心,给的数据才到int)。
显然不能按照上一道题的阶段来划分了。。。
大家可以思考片刻再看题解。
题目分析:
先考虑用 表示前 位数字能取到的最大值。
……转移不动了(没了)。
考虑给状态再加一维, 表示用 个 划分前 位数字能取到的最大值。
显然的, 。
转移时枚举断点 即可,也就是将 作为一个连续的区间(不加乘号),并在 的后面划分一个乘号。
这里要用一个二维数组 表示数字串从第 位到第 位的数字。
方程也就是:
按照划分段数划分阶段,也是区间DP常用的手段。
Code:
//懒得写高精QAQ
#include <cstdio>
#include <algorithm>
#include <cstring>
#define int long long
using namespace std;
int s[60][60], dp[60][60];
int a[105];
signed main()
{
int n, K;
scanf("%lld%lld", &n, &K);
getchar();
for (int i = 1; i <= n; i ++)
a[i] = getchar() - 48;
for (int i = 1; i <= n; i ++)
for (int j = i; j <= n; j ++)
s[i][j] = s[i][j - 1] * 10 + a[j];//预处理出s数组
dp[1][0] = a[1];//这肯定的嘛(是人都看得懂)
for (int i = 2; i <= n; i ++)
for (int j = 1; j <= min(i - 1, K); j ++)
{
dp[i][0] = s[1][i];
for (int k = i - 1; k >= j; k --)
dp[i][j] = max(dp[i][j], dp[k][j - 1] * s[k + 1][i]);
}
printf("%lld", dp[n][K]);
}
例2:
做错的作业
Description
小影(人名) 的数学作业错误百出.
老师:“你作业的表达式里面只有n个括号,怎么就写错了这么多?”
“n那么大,都到300了。”
老师:“那第一个小题只有一个括号你都写错了,这怎么解释?你看,你就写了一个‘(’,明显漏掉了一个‘)’吧?”
“这……”小影似乎很清楚的记得自己是写的“()”,可是现在怎么只剩下“(”了呢?
老师:“你看别人某某,写的多好?十道题都做对了。这样吧,我不看你的计算结果了,只看你的括号匹配得正不正确,你今天之内把你的括号修改正确就可以了……”
“嘿嘿……我把括号全部划掉不就全对了?”小影阴险地想。
老师:“……不过有一个条件:你只能添加括号而不能把括号划掉,并且你只能添加最少数量的括号。”
“天呐!”小影瘫倒了,要知道十个题目里面只有第一题很简单,其他的题目括号数目是巨多的。
看来,只能请善良的你帮帮小影了。
Input Format
只有一行,为一个长度为n的字符串,代表小影作业中写的括号。其中有4类括号“()”“[]”“<>”“”,“(”和“)”匹配,“[”和“]”匹配,“<”和“>”匹配,“{”和“}”匹配,左括号必须在左,右括号必须在右,其他组合是不匹配的。两个匹配的括号中间可以夹有其他已经匹配的括号。如“([<>])()”就是匹配的。
Output Format
一个整数,为小影最少需要添加的括号数目。
Sample Input 1 ([(]{})(<>))
([(]{})(<>))
Sample Output 1 2
2
Sample Input 2 []>][>]<({[]]{<){{{(})
[]>][>]<({[]]{<){{{(})
Sample Output 2 12
12
分析:
为 开始 后 个数字最少需要加的括号。
显然
对于其他的区间,可以选择一个断点 (注意,又是断点),使得它与 相等。
那么这个点和终点是匹配的,接下来只需要匹配在 之前的括号与它们之间的括号。
如果没找到这样的 , 给终点的括号匹配,即
找到了就等于
注意代码中阶段划分的顺序,这道题是按照类似于石子合并的阶段来划分的。
#include <cstdio>
#include <algorithm>
#include <cstring>
using namespace std;
char ch[105];
int dp[305][305];
inline bool check(int x, int y)
{
if (ch[x] == '<' && ch[y] == '>') return 1;
if (ch[x] == '(' && ch[y] == ')') return 1;
if (ch[x] == '[' && ch[y] == ']') return 1;
if (ch[x] == '{' && ch[y] == '}') return 1;
return 0;
}
int main()
{
memset(dp, 0x3f, sizeof(dp));
int n = 1;
while ((ch[n] = getchar()) != '\n') n ++;
n --;
for (int i = 1; i <= n; i ++)
dp[i][0] = 0, dp[i][1] = 1;
for (int j = 2; j <= n; j ++)//按照子区间长度划分阶段
for (int i = 1; i <= n - j + 1; i ++)
{
for (int k = 1; k < j; k ++)
if (check(i + k - 1, i + j - 1) && dp[i][k - 1] + dp[i + k][j - k - 1] < dp[i][j])//如果匹配
dp[i][j] = dp[i][k - 1] + dp[i + k][j - k - 1];
if (dp[i][j - 1] + 1 < dp[i][j])
dp[i][j] = dp[i][j - 1] + 1;
}
printf("%d", dp[1][n]);
}
总结:
常用手段:处理环的破环成链,有时可以代替链表甚至更为好用。
方法就是开两倍数组再加前缀和,详细见习题“数字游戏”的代码。
前缀和是很常用很好用很实用的一种手段。
常用的阶段划分的顺序:
第一种,按照子序列长度划分,例如例题“石子合并”。
第二种,按照子序列划分数量划分,例如“乘积最大"。
当然还有很多种方法,题做多了就有经验了,我现在还在小白阶段中。
习题:
数字游戏代码:
#include <cstdio>
#include <algorithm>
#include <cstring>
using namespace std;
int a[105], dp[105][105][105], f[105][105][105], n, m;
int main()
{
scanf("%d%d", &n, &m);
memset(f, 0x3f, sizeof(f));
for (int i = 1; i <= n; i ++)
scanf("%d", a + i), a[i + n] = a[i];
for (int i = 1; i <= (n << 1); i ++)
a[i] += a[i - 1];//破环成链真香啊
for (int i = 1; i <= (n << 1); i ++)
for (int j = i; j <= (n << 1); j ++)
dp[i][j][1] = f[i][j][1] = ((a[j] - a[i - 1]) % 10 + 10) % 10;
for (int l = 2; l <= m; l ++)
for (int i = 1; i <= (n << 1); i ++)
for (int j = l + i - 1; j <= (n << 1); j ++)
{
for (int k = l + i - 2; k < j; k ++)
{
f[i][j][l] = min(f[i][j][l], f[i][k][l - 1] * (((a[j] - a[k]) % 10 + 10) % 10));
dp[i][j][l] = max(dp[i][j][l], dp[i][k][l - 1] * (((a[j] - a[k]) % 10 + 10) % 10));
}
}
int Max = -0x7fffffff, Min = 0x7fffffff;
for (int i = 1; i <= n; i ++)
{
Max = max(Max, dp[i][i + n - 1][m]);
Min = min(Min, f[i][i + n - 1][m]);
}
printf("%d\n%d", Min, Max);
}
由于本人水平经验及其有限,文章疏漏不可避免,欢迎各位大佬来踩!