前置知识
前缀和:指的是前n个数的和,通常可以用来求区间和
DP:动态规划,本质上仍然是暴力枚举,只不过利用空间换时间的方式,减少了重复的计算;DP需要知道转移方程、初始值、遍历顺序等信息才能正确使用
区间DP的概念
顾名思义就是对区间求DP,如dp[i][j]表示为区间[i,j]所求的值;通常可以将区间内的求解转化为两个部分分开求解,如dp[i][j] = dp[i][k]+dp[k+1][j]+区间的前缀和
模板题:洛谷P1775 石子合并弱化版
题目解析:从题目的直接感受,要求最小代价,肯定用贪心,想用局部最优推出全局最优;但实际上,贪心未必能次次取到最小代价,贪心只能从表面上的两个最小值来推出局部最小值,实际情况有时是稍微取到较大值,然后在下一次合并取到更小的值,考虑到不管怎样合并,都存在子问题到大问题的过程,如不管哪两堆合并都必然影响到其他堆的合并,所以这道题可以用DP暴力枚举所有情况
还考虑到堆的合并可以抽象为区间内的合并,所以这道题实质是区间DP
解题思路
既然DP就是暴力枚举,必然需要枚举所有的情况,而对于任意区间的合并,实际在最后两堆合并的时候是固定值,即这个区间的和,而区间和可以由前缀和相减得到
状态定义:由于是区间DP,自然是将状态定义为区间值,所以定义dp[i][j]为[i,j]区间的最小合并值
DP数组的转移方程:显然由于是两两堆合并,那么对于dp[i][j]就必然等于[i,j]区间内任意两大堆的最小值合并得到,即dp[i][j] = min(dp[i][j], dp[i][k]+dp[k+1][j]+sum[j]-sum[i-1]);区间和自然是由前缀和相减得到
DP数组初始值:对于求最小值,显然要将所有值初始为最大值,但对于区间长度为1的格子显然应该初始化为0,毕竟自己与自己合并的代价为0
遍历:
1.由于DP需要从子问题才能到下一个状态,需要枚举区间长度,对于长度为1的区间已经初始化过了,所以区间长度从2开始,知道所有堆的长度n
2.枚举所有区间的左端点,毕竟只有这样才能通过左端点和区间长度知道右端点
3.再枚举分割点k,由于当区间[i,j]较大时也是由子问题的最优解得来,所以仍然需要枚举分割点k来保证取到最优解
C++代码
#include<bits/stdc++.h>
typedef long long ll;
using namespace std;
const int N = 310, INF = 0x3f3f3f3f;
ll sum[N], a[N];
ll f1[N][N];
int n;
int main(){
cin >> n;
memset(f1, INF, sizeof f1); //最小
for(int i=1; i<=n; ++i){
cin >> a[i];
sum[i] = sum[i-1]+a[i];
f1[i][i] = 0;
}
for(int len=2; len<=n; ++len){
for(int l=1; l+len-1<=n; ++l){
int r = l+len-1;
for(int k=l; k<r; ++k){
f1[l][r] = min(f1[l][r], f1[l][k]+f1[k+1][r]+sum[r]-sum[l-1]);
}
}
}
cout << f1[1][n];
return 0;
}
时间复杂度:O(n^3)
总结
1.对于区间DP问题,往往需要通过观察法看出存在合并两个或多个的问题,并且通常有最值问题,这样往往就是八九不离十了
2.题目给的样例可能会诱导你去使用贪心,但实际情况可能更复杂
3.区间DP通常可以套模板,模板也比较好记,算是简单的DP