区间dp
区间性动态规划
前面我们学习的线性dp以及常见的背包问题,都和递推有很大关系,主要考虑当前状态可以由前面某种状态转移过来,前面的一定会比后面的先选择。一般状态转移都是从前往后或者从后往前。而很多时候选择并没有先后顺序,选择后面的时候并不一定要选前面的,比如区间型动态规划。区间型动态规划套路也比较固定,一般是从小到大,从小区间到大区间,他一般以区间长度为阶段。解决办法也比较简单,一般都是先枚举区间长度,再枚举左端点,确定区间右端点后进行状态转移。
区间型动态规划是线性动态规划的拓展,他在分阶段划分问题时,与阶段中元素出现的顺序和由前一阶段的那些元素合并而来有很大关系。
区间型动态规划的特点:
合并/拆分
:如果是合并,那么就是将两个或多个部分进行整合,如果为拆分,就是把一个问题分解成两个或者多个部分。
特征
:能将问题分解成为两两合并的形式
求解
:对整个问题设最优值,枚举合并点,将问题分解成左右两个部分,最后合并左右两个部分的最优值得到原问题的最优值,有点类似于分治算法的解题思想。
例题
这种类型没有什么固定的套路,或者代码之类的,所以我们只能从例题从去感受
一本通改编——衣服合并
题目描述
又是wjq~~
wjq是520女寝的一位少女,在她的床上摆了一排衣服,例如黑丝,jk,短裙等,总计有
N
N
N 件衣服,每件衣服有一个遮挡程度,现要将衣服有次序地合并成一堆,规定每次只能选相邻的
2
2
2 件合并成新的一堆,并将新的一堆的遮挡程度,记为该次合并的得分。
熟悉她的人知道,她喜欢性感,所以她跑过来问你,请你设计一个算法,使 N N N 件衣服合并成 1 1 1 堆的最小遮挡程度,如果你能完成,她会给你一个惊喜~~
输入格式
数据的第 1 1 1 行是正整数 N N N,表示有 N N N 件衣服。
第 2 2 2 行有 N N N 个整数,第 i i i 个整数 a i a_i ai 表示第 i i i件衣服的遮挡程度。
输出格式
输出共 2 2 2 行,第 1 1 1 行为最小遮挡程度。
样例 #1
样例输入 #1
4
4 5 9 4
样例输出 #1
43
提示
1 ≤ N ≤ 100 1\leq N\leq 100 1≤N≤100, 0 ≤ a i ≤ 20 0\leq a_i\leq 20 0≤ai≤20。
思路
该题和前面做过的一道合并果子的题类似,但是不一样的地方在于每次只能合并相邻两堆,如果我们贪心的每次选择相邻的最小的两堆合并,那么答案明显是错误的,因为前面合并顺序的不同会影响后面每堆的数量,所以我们要考虑动态规划。
我们思考问题的时候不能笼统,需要一步一步来,这和动态规划中的分阶段决策的思想是一致的。这里的每一阶段就是每一次合并,
n
n
n件衣服需要合并
n
−
1
n-1
n−1次,那么一共就有
n
−
1
n-1
n−1个阶段。
对于合并之后的区间
[
l
,
r
]
[l,r]
[l,r],我们先思考最后一次合并,肯定是合并区间
[
l
,
k
]
[l,k]
[l,k]和
[
k
+
1
,
r
]
[k+1,r]
[k+1,r],而最优解中的点
k
k
k可能是
[
l
,
r
]
[l,r]
[l,r]中的任意一个位置,所以我们需要枚举
k
k
k,这样一个完整的区间就被我们划分成了两个小区间,那么对于划分成的两个小区间
[
l
,
k
]
[l,k]
[l,k]和
[
k
+
1
,
r
]
[k+1,r]
[k+1,r],我们还需要求出合并的最小代价,怎么求呢?还是一样,把这个区间划分成
[
l
,
k
]
[l,k]
[l,k]和
[
k
+
1
,
r
]
[k+1,r]
[k+1,r]两个区间,只要这两个小区间的答案我们知道了,那么
[
l
,
r
]
[l,r]
[l,r]的最小代价我们也就知道了。
先从小到大枚举阶段,也就是区间长度l,当区间长度为1的时候,区间内只有一个点,答案就是该点的值。当确定区间长度后,再枚举区间左端点i,这样就能算出右端点j=i+l-1,表示要求出合并区间[i,i+l-1]的最优解。最后枚举区间断点k(左区间的右端点),从而得到状态转移方程为:
d
p
[
i
]
[
j
]
=
m
i
n
(
d
p
[
i
]
[
k
]
+
d
p
[
k
+
1
]
[
j
]
+
x
)
dp[i][j]=min(dp[i][k]+dp[k+1][j]+x)
dp[i][j]=min(dp[i][k]+dp[k+1][j]+x)
其中x为最后一次合并的代价,对于区间[l,r],无论怎么合并,最后合并的代价都是区间[l,r]之和,为了方便,我们维护一个前缀和。
#include<bits/stdc++.h>
using namespace std;
int a[110],sum[110];
int dp[110][110];
int main(){
int n;
cin>>n;
for (int i=1;i<=n;i++){
cin>>a[i];
sum[i]=sum[i-1]+a[i];//维护前缀和
}
//dp[i][j]表示区间i,j所合并的最小值
memset(dp,0x7f7f7f,sizeof(dp));//求最小值,所以附成极大值
for (int i=1;i<=n;i++){
dp[i][i]=0;
}
for (int k=1;k<=n;k++){//枚举长度
for (int l=1;l+k-1<=n;l++){//枚举左端点
int r=l+k-1;//右端点
for (int i=l;i<r;i++){//断点,即左区间的右端点
dp[l][r]=min(dp[l][r],dp[l][i]+dp[i+1][r]+sum[r]-sum[l-1]);
}
}
}
cout<<dp[1][n];
return 0;
}
小结1
在做区间型动态规划的时候,一定要注意状态转移的过程,一旦转移方向错了,那么写出来的状态转移方程虽然是对的,但是结果肯定是错误的,转移过程中用到的数组值都必须是已知的。在思考问题的时候着重思考每一阶段,即每一步,每一次操作,有的时候也可以先用记忆化搜索的方式思考,然后用动态规划的写法来实现。但是写的时候还是建议用循环实现,因为有的时候一些比较难的动态规划用记忆化搜索的方式不是很容易实现,或者冗余相对比较大。 实际写代码过程中一定要注意初始值以及边界问题,以免造成不必要的过失
1275:【例9.19】乘积最大
题目描述
今年是国际数学联盟确定的“2000——世界数学年”,又恰逢我国著名数学家华罗庚先生诞辰 90 周年。在华罗庚先生的家乡江苏金坛,组织了一场别开生面的数学智力竞赛的活动,你的一个好朋友 XZ 也有幸得以参加。活动中,主持人给所有参加活动的选手出了这样一道题目:
设有一个长度为 N N N 的数字串,要求选手使用 K K K 个乘号将它分成 K + 1 K+1 K+1 个部分,找出一种分法,使得这 K + 1 K+1 K+1 个部分的乘积能够为最大。
同时,为了帮助选手能够正确理解题意,主持人还举了如下的一个例子:
有一个数字串: 312 312 312,当 N = 3 , K = 1 N=3,K=1 N=3,K=1 时会有以下两种分法:
- 3 × 12 = 36 3 \times 12=36 3×12=36
- 31 × 2 = 62 31 \times 2=62 31×2=62
这时,符合题目要求的结果是: 31 × 2 = 62 31 \times 2 = 62 31×2=62。
现在,请你帮助你的好朋友 XZ 设计一个程序,求得正确的答案。
输入格式
程序的输入共有两行:
第一行共有 2 2 2 个自然数 N , K N,K N,K。
第二行是一个长度为 N N N 的数字串。
输出格式
结果显示在屏幕上,相对于输入,应输出所求得的最大乘积(一个自然数)。
样例 #1
样例输入 #1
4 2
1231
样例输出 #1
62
提示
数据范围与约定
对于所有测试数据,
6
≤
N
≤
10
,
1
≤
K
≤
6
6≤N≤10,1≤K≤6
6≤N≤10,1≤K≤6。
思路
该题和前一道题最大的区别在于前面的题是区间合并,而该题相当于是区间拆分,用乘号把一个完整的数分割成多个独立的数。区间拆分型动态规划和区间合并型动态规划的做法有相似的地方,但是也有不一样的地方。我们以放置一个乘号作为阶段,最开始是一个完整的区间,放置一个乘号后的最大值我们可以求出来,只需要枚举乘号的位置即可。接下来我们分别再考虑两个区间,就会发现有点问题,两个区间的乘号之和应该等于总的乘号,但是左右两边各有多少个乘号是未知的,又要枚举,就很麻烦,需要改变一下思路。
对于一个数,当他把所有的乘号都添加好之后,这些乘号之间是有先后顺序的,我们依次把乘号放进去,即每次放进去的都是最后一个乘号。那么,当我们枚举最后一个乘号的位置k时,说明前面区间[1,k-1]一共放了j-1个乘号,后面是一个整体。
我们设dp[i][j]表示前i个数中放置j个乘号的最大值,接下来我们枚举最后一个乘号的位置。那么就有
d
p
[
i
]
[
j
]
=
m
a
x
(
d
p
[
k
]
[
j
−
1
]
∗
s
u
m
[
k
+
1
]
[
n
]
,
d
p
[
i
]
[
j
]
)
dp[i][j]=max(dp[k][j-1]*sum[k+1][n],dp[i][j])
dp[i][j]=max(dp[k][j−1]∗sum[k+1][n],dp[i][j])
要保证我们的状态转移方程中的dp的结果都是已知的,我们应该先枚举前i个数,再枚举放置j个乘号,这样就没有问题。
只不过这里要注意,初始值为前
i
i
i个数中放0个乘号的最大值,即
d
p
[
i
]
[
0
]
=
s
u
m
[
i
]
dp[i][0]=sum[i]
dp[i][0]=sum[i]。
#include<bits/stdc++.h>
using namespace std;
int a[50],num[50][50];//num[i][j]表示区间i,j的数字大小
int dp[50][50];
int main(){
long long n,k,m;
cin>>n>>k>>m;
//分段
int tmp=n;
while (m){
a[tmp]=m%10;
m/=10;
tmp--;
}
for (int i=1;i<=n;i++){
for (int j=i;j<=n;j++){
num[i][j]=num[i][j-1]*10+a[j];
}
}
for (int i=1;i<=n;i++){
dp[i][0]=num[1][i];
}
for (int i=1;i<=n;i++){//数字的长度,即前i个数
for (int j=1;j<=k;j++){//乘号的个数
for (int m=j-1;m<i;m++){//乘号的位置
dp[i][j]=max(dp[i][j],dp[m][j-1]*num[m+1][i]);
}
}
}
cout<<dp[n][k];
return 0;
}
小结2
对于拆分型区间dp的做法大部分都和乘积最大的做法差不多,考虑每一次拆分。但是和前面合并型区间dp的思考方式有一点区别,我们并不是考虑最后一次拆分,然后前面有k个乘号,那么后面区间就有j-k-1个乘号,然后两这相乘的最大值,如果是这样,那么数组就需要dp[i][j][k]来表示,虽然这样也可以做,但是效率上会低一些,而且消耗的空间也比较大。我们同样是考虑最后一次拆分,但是我们这里让乘号是有序的,因为乘号放置的位置一样,顺序不同不影响答案,我们的最后一次拆分就是最后一个乘号的位置,这样我们就是一个前缀型dp数组,就不需要区间型的数组来记录。
[IOI2000] 回文字串
题目背景
IOI2000 第一题
题目描述
回文词是一种对称的字符串。任意给定一个字符串,通过插入若干字符,都可以变成回文词。此题的任务是,求出将给定字符串变成回文词所需要插入的最少字符数。
比如 Ab3bd \verb!Ab3bd! Ab3bd 插入 2 2 2 个字符后可以变成回文词 dAb3bAd \verb!dAb3bAd! dAb3bAd 或 Adb3bdA \verb!Adb3bdA! Adb3bdA,但是插入少于 2 2 2 个的字符无法变成回文词。
注意:此问题区分大小写。
输入格式
输入共一行,一个字符串。
输出格式
有且只有一个整数,即最少插入字符数。
样例 #1
样例输入 #1
Ab3bd
样例输出 #1
2
提示
数据范围及约定
记字符串长度为 l l l。
对于全部数据, 0 < l ≤ 1000 0<l\le 1000 0<l≤1000。
思路
该题看上去和DP毫无关系,但是仔细分析后发现就是一个简单的区间DP的题。
对于一个字符串,如果他是回文字符串,那么首尾是相等的。如果某个字符串s[1]==s[n],那么求这个字符串需要经过多少次转换,相当于就是求字符串s[2…n-2]的转换次数,因为首尾已经相等了,不需要再去转换。
但是如果s[1]!=s[n],那么我们可以在最前面添加一个s[n]或者最后添加一个s[1]右边长首尾相等,如果在开头添加,那么接下来就是求s[1,n-1]的转换次数,如果在末尾添加,那么就下来求[2,n]的转换次数,同时,当前增加一次转换次数。
状态
:dp[i][j]表示区间[i,j]变为回文串的最少操作数。
状态转移
:如果s[i]==s[j],那么首尾两个字符已经回文了,我们只需要考虑[i+1,j-1],并且操作次数不变;如果s[i]!=s[j],那么要么在前面添加,要么在末尾添加尾或者首字符,又构成首尾相等的字符串,操作次数+1,即
d
p
[
i
]
[
j
]
=
m
i
n
(
d
p
[
i
+
1
]
[
j
]
,
d
p
[
i
]
[
j
−
1
]
)
+
1
dp[i][j]=min(dp[i+1][j],dp[i][j-1])+1
dp[i][j]=min(dp[i+1][j],dp[i][j−1])+1
要求长度为l的字符串操作次数,就需要知道长度为l-1的字符串的操作次数,所以该题仍然是一道区间dp的题,以字符串长度作为阶段进行转移。初始时字符串长度为1,自身就是回文串。
#include<bits/stdc++.h>
using namespace std;
char a[3000];
int dp[2100][2100];
int main(){
cin>>a+1;
int len=strlen(a+1);//这样处理可以使字符串从1开始存储
for (int i=1;i<=len;i++){//长度
for (int j=1;j+i-1<=len;j++){//起点
int r=j+i-1;
if (a[j]==a[r])dp[j][r]=dp[j+1][r-1];
else dp[j][r]=min(dp[j+1][r],dp[j][r-1])+1;
}
}
cout<<dp[1][len];
return 0;
}