区间DP笔记

区间DP的引入

区间DP是一种特殊的DP,与普通的DP在阶段的划分上往往有所不同。

先来看个栗子:

洛谷P1808 一道人尽皆知的区间DP模板题

这道题大家都做过,通过这一道题可以体会到区间DP的特殊:

他是按照子区间的长度划分阶段,不是按找起点终点等划分。

在很多DP题中,一个区间的答案往往取决于他的子区间的答案,于是区间DP就诞生了。

这道题的分析:(做过的巨佬请绕行)

贪心显然有问题,不采用。

f i , j f_{i,j} 表示区间 i , j i, j 内能取到的最大值(在此不讨论最小值)。

f i , i = a i f_{i,i}=a_i

扫描二维码关注公众号,回复: 11605716 查看本文章

显然,我们要么先将 i + 1 j i+1\sim j 的区间合并,反之合并 i j 1 i\sim j-1 的区间,再加上区间 i j i\sim j 的总和(可以使用前缀和数组计算)。

方程: f i , j = m a x ( f i + 1 , j , f i , j 1 ) + i = i j a f_{i,j}=max(f_{i+1,j},f_{i,j-1})+\sum^j_{i=i} a

阶段(区间DP重点):

无论按照起点还是终点划分阶段,均不能保证一个状态的子状态一定在它之前计算出来。

观察发现,一个区间的最大值依赖于他的子区间的最大值,那么我们可以按照长度划分阶段

外层循环 i i 枚举序列长度,内层循环 j 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]);
	}

完整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中使用很多。

例题

另一个经典的区间DP例题

显然此题要高精(会int128也行),但是重点不在这里……

由于本人懒,没写高精,虽然之前写了个高精度类也懒得去复制,反正重点不在这里。

总之在NKOJ上水过了(众所周知NKOJ数据特别良心,给的数据才到int)。

显然不能按照上一道题的阶段来划分了。。。

大家可以思考片刻再看题解。

题目分析:

先考虑用 f i f_i 表示前 i i 位数字能取到的最大值。

……转移不动了(没了)。

考虑给状态再加一维, f i , j f_{i,j} 表示用 j j × \times 划分前 i i 位数字能取到的最大值。

显然的, i > j i > j

转移时枚举断点 k k 即可,也就是将 k + 1 i k+1\sim i 作为一个连续的区间(不加乘号),并在 k k 的后面划分一个乘号。

这里要用一个二维数组 s s 表示数字串从第 i i 位到第 j j 位的数字。

方程也就是:

f i , j = m i n ( f k , j 1 × s k + 1 , j ) f_{i,j}=min(f_{k,j-1}\times s_{k+1,j}) ( k j ) (k\geq j)

按照划分段数划分阶段,也是区间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

Sample Input 2
[]>][>]<({[]]{<){{{(})

Sample Output 2
12

分析:

f i , j f_{i,j} i i 开始 后 j j 个数字最少需要加的括号。

显然 f i , 1 = 1 f_{i,1} =1

对于其他的区间,可以选择一个断点 k k (注意,又是断点),使得它与 a i + j 1 a_{i+j-1} 相等。

那么这个点和终点是匹配的,接下来只需要匹配在 k k 之前的括号与它们之间的括号。

如果没找到这样的 k k , 给终点的括号匹配,即 f i , j = f i , j 1 + 1 f_{i,j}=f_{i,j-1}+1

找到了就等于 m i n ( f i , j , f i , k 1 + f i + k , j k 1 ) min(f_{i,j},f_{i,k-1}+f_{i+k,j-k-1})

注意代码中阶段划分的顺序,这道题是按照类似于石子合并的阶段来划分的。

#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);
}

由于本人水平经验及其有限,文章疏漏不可避免,欢迎各位大佬来踩!

猜你喜欢

转载自blog.csdn.net/jvruo_shabi/article/details/108294739