这几天在做有关dp的题,看到一个石子合并的问题,本来以为是个贪心,后来仔细一想压根不是贪心。贪心算法的思路是每次都取最大的,然而石子合并问题有个限制条件就是每次只能取相邻的,这就决定了它不是个贪心…
我们接下来会先直接上区间DP的模板,然后结合石子合并这道经典例题来讲解。
一、区间DP
区间dp其实就是一种建立在线性结构上的对区间的动态规划。主要的方法有两种,记忆化搜索和递推。
区间dp,顾名思义,在区间上dp,大多数题目的状态都是由区间(类似于dp[l][r]这种形式)构成的,就是我们可以把大区间转化成小区间来处理,然后对小区间处理后再回溯的求出大区间的值,因为大区间的最优必须要保证小区间也是最优。
反证法:例如在一个长度为6的区间中求解最大score,假设现在在计算将两个长度为3的区间合并是否是最大score,假设其中一个长度为3的区间不是最优,这个区间是由数字1 2 3 组成,1与2合并后在与3合并得到这个长度为3区间的score,然而这个score与另一个长度为3的区间合并,并不能得到长度为6区间的最优值,其差错就出在那个长度为3的区间的score不是最优。
啊~,解释了一番,也不知道说清楚了吗…如果不清楚就直接记住大区间的最优必须要保证小区间最优就行了叭…
区间dp也有很经典的板子部分,下面抛出代码和解析:
memset(dp,0,sizeof(dp))//初始dp数组,最重要的是让dp[i][i]=0,防止在用区间为1和其他长度的区间合并时,重复计算(即转移方程中的d[i][j])。
for(int len=2;len<=n;len++){//枚举区间长度
for(int i=1;i<n;++i){//枚举区间的起点
int j=i+len-1;//根据起点和长度得出终点
if(j>n) break;//符合条件的终点
dp[i][j] = INF;//初始化
for(int k=i;k<=j;++k)//枚举最优分割点
dp[i][j]=min(dp[i][j],dp[i][k]+dp[k+1][j]+d[i][j]);//状态转移方程
}
}
当然dp数组的维度和边界条件以及转移方程都是可变的,但是很多简单题都是这样可以做出来的,难题也都是情况更复杂了些,其最基本的思想还是不变的。
区间dp大部分的题目还有一种优化,因为我们很容易知道正常的区间dp时间复杂度为O(n^3)的,对于有的n是1000的,会超时
这时候就要用到一个经典的优化可以把它优化到:O(n^2),其实证明很难理解,但是大部分题都不会卡,因为dp已经很难了。
就算需要四边形优化,也就是多开一个数组s的事,在枚举最优分割点时,再缩小一下枚举范围,经典的用空间换时间的做法。
下面抛出基于四边形优化的代码。
for(int len=2;len<=n;len++){
for(int i = 1;i<=n;i++){
int j = i+len-1;
if(j>n) break;
for(int k = s[i][j-1];k<=s[i+1][j];k++){
if(dp[i][j]>dp[i][k]+dp[k+1][j]+w[i][j]){
dp[i][j]=dp[i][k]+dp[k+1][j]+w[i][j];
s[i][j]=k;
}
}
}
}
原谅我太弱了,实在理解不了。证明可以看看这篇文章:戳我戳我
二、石子合并
题目描述
在一个圆形操场的四周摆放 N 堆石子,现要将石子有次序地合并成一堆.规定每次只能选相邻的2堆合并成新的一堆,并将新的一堆的石子数,记为该次合并的得分。
试设计出一个算法,计算出将 N 堆石子合并成 1 堆的最小得分和最大得分。
输入格式
数据的第 1 行是正整数 N,表示有 N 堆石子。
第 2 行有 N 个整数,第 i 个整数 ai
表示第 i 堆石子的个数。
输出格式
输出共 2 行,第 1 行为最小得分,第 2 行为最大得分。
注意注意注意(重要的话说三遍):这些石子构成了一个环!!!因此要处理一下数组。
上代码!
#include <iostream>
const int INF = 9999;
int num[INF];
int dp1[INF][INF];
int dp2[INF][INF];
int d(int i, int j){
return num[j] - num[i-1];
}
using namespace std;
int main(){
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
int n;
cin >> n;
for(int i=1;i<=n;++i){
cin >> num[i];
//构成环
num[n+i] = num[i];
}
//前缀和,为了求i到j之间的石子和
for(int i=1;i<=n+n;++i){
num[i] += num[i-1];
}
for(int i=1;i<=n+n;++i){
dp1[i][i] = 0;
dp2[i][i] = 0;
}
//区间大小
for(int r=2;r<=n;++r){
//起始点
for(int i=1;i<=n+n;++i){
int j = i+r-1; //终点
if(j>n+n) break;
dp1[i][j] = INF;
dp2[i][j] = -INF;
//分割点
for(int k=i;k<j;++k){
dp1[i][j] = min(dp1[i][j], dp1[i][k]+dp1[k+1][j]+d(i,j));
dp2[i][j] = max(dp2[i][j], dp2[i][k]+dp2[k+1][j]+d(i,j));
}
}
}
int minn = INF, maxn = -INF;
//看区间长度为n的最优解
for(int i=1;i<=n;++i){
minn = min(minn, dp1[i][i+n-1]);
maxn = max(maxn, dp2[i][i+n-1]);
}
cout << minn << endl;
cout << maxn << endl;
return 0;
}