区间DP
区间dp,顾名思义,在区间上dp,大多数题目的状态都是由区间(类似于dp[l][r]这种形式)构成的,就是我们可以把大区间转化成小区间来处理,然后对小区间处理后再回溯的求出大区间的值,主要的方法有两种,记忆化搜索和递推。
在用递推来求解时,关键在于递推是for循环里面的顺序,以及dp的关键:状态转移方程。
当然大部分的区间dp都是有特点的,我们可以考虑符合什么条件下,大区间可以转化成小区间,然后找出边界条件,进行dp求解。
memset(dp,0,sizeof(dp))//初始dp数组
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=i;k<=j;++k)//枚举最优分割点
dp[i][j]=min(dp[i][j],dp[i][k]+dp[k+1][j]+w[i][j]);//状态转移方程
}
}
这时候就要用到一个经典的优化可以把它优化到: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;
}
}
}
}
附上一些典例典例
poj1160
有一条直线公路,沿公路旁有村庄。高速公路用整数轴表示,每个村庄的位置用一个整数坐标标识。没有两个村庄处于同一位置。两个位置之间的距离是其整数坐标之差的绝对值。
邮局将在部分村庄(但不一定是全部村庄)中建立。一个村庄和其中的邮局位置相同。在建造邮局时,应选择其位置,以使每个村庄与其最近的邮局之间的所有距离的总和最小。
您将要编写一个程序,该程序会根据村庄的位置和邮局的数量,计算出每个村庄与其最近的邮局之间的所有距离中的最小和。
//经典动态规划
//dp(i,j)表示用i个邮局,从1到j村庄的最优解
//dis(i,j)表示只用1个邮局从i到j村庄的最优解,显然取i,j中点的村庄作为邮局点是最优的
//动态转移方程:dp(i,j) = min{dp(i-1,k) + dis(k+1,j)}(i <= k <= j-1)
//注意dp数组的初始化
#include<iostream>
using namespace std;
int d[305];
int dp[35][305];
int dis[305][305];
int n,m;
void cal_dis()
{
for(int i=1;i<=n;i++)
{
for(int j=i+1;j<=n;j++)
{
int mid=(i+j)/2;
for(int k=i;k<=j;k++)
{
dis[i][j]+=abs(d[k]-d[mid]);
}
}
}
}
int main()
{
cin>>n>>m;
for(int i=1;i<=n;i++)
{
cin>>d[i];
}
cal_dis();
for(int i=1;i<=n;i++)
{
dp[1][i]=dis[1][i];
}
for(int i = 2;i <= m;++i)
for(int j = 1;j <= n;++j)
dp[i][j] = 1e9;//初始化
for(int i=2;i<=m;i++)
{
for(int j=i;j<=n;j++)
{
for(int k=i-1;k<j;k++)
{
dp[i][j]=min(dp[i][j],dp[i-1][k]+dis[k+1][j]);
}
}
}
cout<<dp[m][n]<<endl;
}
合并石子
在一个圆形操场的四周摆放 N堆石子,现要将石子有次序地合并成一堆.规定每次只能选相邻的2堆合并成新的一堆,并将新的一堆的石子数,记为该次合并的得分。
试设计出一个算法,计算出将 N 堆石子合并成 1 堆的最小得分和最大得分。
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
int dpmax[300][300];//dp[i][j]表示合成i到j堆所需要的最大得分
int dpmin[300][300];
int sum[300];
int d[300];
int main()
{
int n;
cin>>n;
memset(dpmax,0,sizeof(dpmax));
for(int i=1;i<=n+n-1;i++)//注意是环,所以还要改成n+n-1
{
for(int j=i;j<=n+n-1;j++)
{
if(i==j) dpmin[i][j]=0;//注意dpmin的初始化!!
else dpmin[i][j]=1e6;
}
}
//sum[i]表示到i这个点一共的石子数
for(int i=1;i<=n;i++)
{
cin>>d[i];
sum[i]=sum[i-1]+d[i];
}
for(int i=n+1;i<n+n;i++)
{
sum[i]=sum[i-1]+d[i-n];
}
//区间dp模板
int j;
for(int len=2;len<=n+n;len++)
{
for(int i=1;i<n+n;i++)
{
j=i+len-1;
if(j>=n+n) break;
for(int k=i;k<j;k++)//注意不要等于
{
dpmax[i][j]=max(dpmax[i][j],dpmax[i][k]+dpmax[k+1][j]+sum[j]-sum[i-1]);
dpmin[i][j]=min(dpmin[i][j],dpmin[i][k]+dpmin[k+1][j]+sum[j]-sum[i-1]);
}
}
}
int ansmax=0;
int ansmin=1e9;
for(int i=1;i<=n;i++)//因为是环所以截取所有1-n区间求最大最小
{
ansmax=max(ansmax,dpmax[i][i+n-1]);
ansmin=min(ansmin,dpmin[i][i+n-1]);
}
cout<<ansmin<<endl;
cout<<ansmax<<endl;
}