区间dp
到目前为止,我们介绍的 线性DP一般 从初态开始,沿着阶段的扩张向某个方向递推,直至计算出目标状态。
区间DP也属于 线性DP中的一种,它以“区间长度”作为DP的“阶段”,使用两个坐标(区间的左、右端点)描述每个维度。
在 区间DP 中,一个状态由若干个比它更小且包含于它的区间所代表的状态转移而来,因此 区间DP的决策往往就是划分区间的方法。
区间DP 的 初态一般由长度为1的“元区间”构成。这种 向下划分、再向上递推 的模式与某些树形结构,例如 线段树,有很大的相似之处。把 区间DP作为线性DP中一类重要的分支单独进行分析,将更容易理解 树形DP 的内容。
同时,借助 区间DP 这种 与树形相关 的结构,我们也将提及 记忆化搜索——其本质是动态规划的递归实现方法。
题意:
给定n
堆石子,排成一排,编号为1~n
,每一堆石子有一定的质量,现在我们要把n
堆石子合并成一堆,每次只能合并相邻两堆(若不加此限制就成了一道贪心题,比如AcWing 148. 合并果子),每次合并相邻两堆的代价是:两堆的石子质量之和,问:将所有石子堆合并成一堆的最小代价是多少?
以题中给出样例举例:4
堆石子:1、3、5、2
,先将1
、3
合并成4
(消耗4
),后将5、2
合并成7
(消耗7
),最后将4、7
合并为11
(消耗11
),三次消耗总和为4+7+11 = 22
,此为最小的情况。
思路:
由于每次只可以合并相邻两堆,我们可以发现:对于任何一个区间,如果目标将这区间内的石子合并成一堆的话,最后一步一定是将某左右两堆(左右两部分都是连续的,很关键)合并成一堆。
本题实际上是问:所有合并方案中的最小消耗体力。
注意,在本题中所有不同的合并方式或顺序一定是有限的(合并一次 就少一堆,所以一定会合并完),且合并方案的数量级达到了n!
,非常的大,当然不可能枚举所有的方案(n<=300
, 300!
必超时),我们的想法是用dp
来解题。
闫氏dp分析法:从两个角度来分析,状态表示和状态计算。
状态表示:(化零为整)
集合:用f[i, j]
表示一类集合(表达形式为:所有满足 条件1
、条件2
、… 选法的集合,每一个条件都对应这状态表示的一个维度):所有将[i, j]
这个区间所有石子合并成一堆的方案的集合
(f[i, j]
可以表示(j-i)!
个方案,因为从i~j
这个区间内一共有j-i+1
堆,这样才得以优化问题)
属性:集合中每一种方案的最小代价min
状态计算:(化整为零)
将f[i, j]
所有方案划分成若干类,每一类分别求min
,逐个击破。
划分依据:一般是找最后一个不同点,本题则依据“分界点”(左边部分 的 最后一堆)作为划分集合的依据
现在我们要做的就是将每一类都求出来取min
即可,最后的答案就是整个集合的最小值。
如上图,如果是第k
类的话,则分为:左边部分的区间为[i, k]
,右边部分的区间为[k+1, j]
,我们发现即将合并的两部分是完全独立的,显然是使得左边部分和右边部分最小:
- 对于左边部分,即为所有将
[i, k]
这个区间所有石子合并成一堆的方案中的最小代价,根据定义恰好为f[i, k]
, - 右边同理,恰为
f[k+1, j]
。
而对于整个区间还需要加上[i, j]
的部分和,因为每次合并的时候是要消耗能量的,即为:f[i, k] + f[k+1, j] + s[j] - s[i-1]
。
最后,枚举k = i、i+1、...、j-1
,取min
即可得到答案。
时间复杂度分析:
所有状态数量是n^2
,枚举的时候共k
次,所以是n ^ 3
的复杂度,n<=300
,大概27e6
的复杂度,是合法的
代码:
写法一:
细节:
一般的区间 dp
问题都是如此:先枚举区间长度 len
,后枚举左端点 left
,枚举的范围由右端点 right
是否超过最大范围决定,最后枚举分界点 k
。
len
从 2
开始枚举,因为为 1
的时候花费为 0
,而二维数组 f
本来就是全局变量初始化为 0
,
所以无需从 len=1
开始枚举
#include<bits/stdc++.h>
using namespace std;
const int N = 310;
int s[N], n;
int dp[N][N];
int main()
{
cin>>n;
for(int i=1; i<=n; ++i) cin>>s[i], s[i] += s[i-1];
for(int len = 2; len<=n; ++len)
{
for(int left=1; left+len-1<=n; ++left)
{
int right = left+len-1;
dp[left][right] = 1e9;//需要赋值一个无穷大,因为后续需要取最小值
for(int k=left; k<right; ++k)
dp[left][right] = min(dp[left][right], dp[left][k]+dp[k+1][right]+s[right]-s[left-1]);
}
}
cout<<dp[1][n]<<endl;
return 0;
}
写法二:
细节:
在递推过程之外进行赋值为无穷大,len
改为从1
开始循环,不过需要在内层特殊判断当len = 1
时dp[left][right]为0
#include<bits/stdc++.h>
using namespace std;
const int N = 310;
int s[N], n;
int dp[N][N];
int main()
{
cin>>n;
for(int i=1; i<=n; ++i) cin>>s[i], s[i] += s[i-1];
memset(dp, 0x3f, sizeof dp);
for(int len = 1; len<=n; ++len)
{
for(int left=1; left+len-1<=n; ++left)
{
int right = left+len-1;
if(len==1) {dp[left][right] = 0; continue; }
for(int k=left; k<right; ++k)
dp[left][right] = min(dp[left][right], dp[left][k]+dp[k+1][right]+s[right]-s[left-1]);
}
}
cout<<dp[1][n]<<endl;
return 0;
}