区间 DP 详解
文章目录
- 区间 DP
- 分割型
- 合并型
- 环形合并
区间 DP
区间 DP,就是在对一段区间进行了若干次操作后的最小代价,一般是合并和拆分类型。
分割型
分割型,指把一个区间内的几项分开拆成一份一份的,再全部合起来就是当前答案,可以理解为合并型的另一种(合并型详见下面),它的时间复杂度一般为
O
(
n
3
)
O(n^3)
O(n3),其中我们一般设 dp[i][j]
表示把前
i
i
i 项拆成
j
j
j 份的最小值,而第三层循环则是循环的分割点,表示把前
i
i
i 项从
k
k
k 这里分开来看,这就是“分割”。由此,我们可以得到这样一个状态转移方程:
d p i , j = min { d p i , j , d p k , j − 1 + 代价 } dp_{i,j}=\min\{dp_{i,j},dp_{k,j-1}+\text{代价}\} dpi,j=min{dpi,j,dpk,j−1+代价}
当然,有时我们也会把这个 k k k 当做从 i − 1 i-1 i−1 到 i i i 一共分成了 k k k 份,由此,我们可以得到另一种状态转移方程:
d p i , j = d p i − 1 , j − k + d p i , j dp_{i,j}=dp_{i-1,j-k}+dp_{i,j} dpi,j=dpi−1,j−k+dpi,j
这种情况下一般就不是讨论最小值,而是计数,我们可以看下面这道例题:
⼩明的花店新开张,为了吸引顾客,他想在花店的⻔⼝摆上⼀排花,共 m m m 盆。通过调查顾客的喜好,⼩明列出了顾客最喜欢的 n n n 种花,从 1 1 1 到 n n n 标号。为了在⻔⼝展出更多种花,规定第 i i i 种花不能超过 a i a_i ai 盆,摆花时同⼀种花放在⼀起,且不同种类的花需按标号的从⼩到⼤的顺序依次摆列。试编程计算,⼀共有多少种不同的摆花⽅案。
这就是我们刚刚说的第二种类型,直接套公式即可。注意初始化(初始化就不用我都说了吧,就是 d p i , 0 = 1 dp_{i,0}=1 dpi,0=1 呗),但这种类型很少见,我基本翻遍了全网才找到了这一道题,其余的基本都是第一种,所以各位同学终点记第一种就行,第二种做一个拓展。
AC 代码:
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int mod=1e6+7;
int n,m,a[106],dp[106][106];
signed main()
{
cin>>n>>m;
dp[0][0]=1;//初始化
for(int i=1;i<=n;i++)
{
cin>>a[i];
dp[i][0]=1;
}
for(int i=1;i<=n;i++)//终点
{
for(int j=1;j<=m;j++)//分割份数
{
for(int k=0;k<=min(a[i],j);k++)//i-1 到 i 的分割份数
{
dp[i][j]=(dp[i][j]+dp[i-1][j-k])%mod;
}
}
}
cout<<dp[n][m];
return 0;
}
下面留一道例题供大家练习:
今年是国际数学联盟确定的“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
- 31×2=62
这时,符合题⽬要求的结果是: 31 × 2 = 62 31\times2=62 31×2=62。
现在,请你帮助你的好朋友 XZ 设计⼀个程序,求得正确的答案。
合并型
合并型,一般指把这一个区间内的相邻两项合在一起,每次代价为这两项的和,求最小代价。 这种题,我们只需要一个万能公式就可以搞定,它就是:
d p j , e d = min { d p j , e d , d p j , k + d p k + 1 , e d + 代价 } dp_{j,ed}=\min\{dp_{j,ed},dp_{j,k}+dp_{k+1,ed}+\text{代价}\} dpj,ed=min{dpj,ed,dpj,k+dpk+1,ed+代价}
咳咳,不小心把祖传秘方给说出来了。
其中 j j j 是起点, e d ed ed 是终点, k k k 是分割点,表示从起点到终点中间一个把区间一分为二的点。这时再回去看看那个方程,是不是就明了多了?
这里我要说一下:这里我们一般写三层循环,最外面枚举区间长度,第二层枚举起点,第三层枚举分割点。所以时间复杂度也是 O ( n 3 ) O(n^3) O(n3)。
还有就是 DP 的初始化我们一般都是这样:
for(int i=1;i<=n;i++)
{
dp[i][i]=0;
}
表示你以当前这个点为起点同时为终点合并的代价为 0 0 0。当然,在不同的题中有不同的初始化,一般都是两个区间之间的代价等于多少这种。
为了让大家更好理解,我们拿一道例题来讲一讲(没有洛谷的可以看下面):
设有 N ( 0 ≤ N ≤ 300 ) N(0\le N\le300) N(0≤N≤300) 堆石子排成一排,其编号为 1 , 2 , 3 , … , N 1,2,3,\dots,N 1,2,3,…,N。每堆石子有一定的质量 m i ( m i ≤ 1000 ) m_i(m_i\le1000) mi(mi≤1000)。现在要将这 N N N 堆石子合并成为一堆。每次只能合并相邻的两堆,合并的代价为这两堆石子的质量之和,合并后与这两堆石子相邻的石子将和新堆相邻。合并时由于选择的顺序不同,合并的总代价也不相同。试找出一种合理的方法,使总的代价最小,并输出最小代价。
一道极其经典的合并型区间 DP,让我们套上上面的模版并把代价加入得(其中 s i s_i si 是 m i m_i mi 的前缀和):
d p j , e d = min { d p j , e d , d p j , k + d p k + 1 , e d + s e d − s j } dp_{j,ed}=\min\{dp_{j,ed},dp_{j,k}+dp_{k+1,ed}+s_{ed}-s_j\} dpj,ed=min{dpj,ed,dpj,k+dpk+1,ed+sed−sj}
AC 记录
(代码自己写)。
环形合并
有时候,我们这个合并型可能会在一个环上合并,这时我们不好考虑首位合并的情况,所以就有一种办法:断环成链!我们只需要把这个环变成一条链,然后在后面再接上一次,就可以正常的跑合并型了,具体请看这张图:
[
a
1
,
a
2
,
a
3
,
…
,
a
n
]
,
a
n
+
1
,
a
n
+
2
,
…
,
a
2
n
[a_1,a_2,a_3,\dots,a_n],a_{n+1},a_{n+2},\dots,a_{2n}
[a1,a2,a3,…,an],an+1,an+2,…,a2n
a
1
,
[
a
2
,
a
3
,
…
,
a
n
,
a
n
+
1
]
,
a
n
+
2
,
…
,
a
2
n
a_1,[a_2,a_3,\dots,a_n,a_{n+1}],a_{n+2},\dots,a_{2n}
a1,[a2,a3,…,an,an+1],an+2,…,a2n
a
1
,
a
2
,
[
a
3
,
…
,
a
n
,
a
n
+
1
,
a
n
+
2
]
,
…
,
a
2
n
a_1,a_2,[a_3,\dots,a_n,a_{n+1},a_{n+2}],\dots,a_{2n}
a1,a2,[a3,…,an,an+1,an+2],…,a2n
⋯
\cdots
⋯
a
1
,
a
2
,
a
3
,
…
,
a
n
,
[
a
n
+
1
,
a
n
+
2
,
…
,
a
2
n
]
a_1,a_2,a_3,\dots,a_n,[a_{n+1},a_{n+2},\dots,a_{2n}]
a1,a2,a3,…,an,[an+1,an+2,…,a2n]
(有点抽象,但是因为设备太简陋了,也只能这样做。)
我们还是来看一道例题:
点我跳转至例题。
因为题目是一个 pdf 文件,不好取字,所以就把链接放在上面了,偷懒的同学也可以看下面的图片。
这就是一个很经典的环形区间 DP,所以我们可以先断环成链,然后在后面拼上一截,最后直接做区间合并型 DP 就行了。
但要注意的是这里的初始化不是 0 0 0,而是从 i − 1 i-1 i−1 到 i + 1 i+1 i+1 的值为 ∏ k = i − 1 i + 1 a k \prod_{k=i-1}^{i+1}a_k ∏k=i−1i+1ak( ∏ \prod ∏ 是多个数的乘积的意思)
AC 代码:
#include<bits/stdc++.h>
#define int long long
using namespace std;
int n,mx,a[206],dp[206][206];//dp:从i到j的最大方案
signed main()
{
cin>>n;
for(int i=1;i<=n;i++)
{
cin>>a[i];
a[i+n]=a[i];//断环成链并拼上一节,方便后续处理
}
n*=2;
for(int i=2;i<n;i++)//初始化
{
dp[i-1][i+1]=a[i-1]*a[i]*a[i+1];
}
for(int i=4;i<=n;i++)//长度
{
for(int j=1;j<=n-i+1;j++)//起点
{
int ed=i+j-1;
for(int k=j+1;k<ed;k++)//分割点
{
dp[j][ed]=max(dp[j][ed],dp[j][k]+dp[k][ed]+a[j]*a[k]*a[ed]);//这里注意:是 dp[j][k]+dp[k][ed] 而不是 dp[j][k]+dp[k+1][ed],具体原因大家自己想
}
}
}
n/=2;
for(int i=1;i<=n;i++)
{
mx=max(mx,dp[i][i+n]);
}
cout<<mx;
return 0;
}