蒟蒻の区间dp学习总结
区间dp
定义
区间dp其实就是一种建立在线性结构上的对区间的动态规划,dp本来就是很奇妙的东西,也没有什么套路,就是一种思考的数学思维方式,只有做足够多的题并且想的足够多才可能在比赛中做出来。
区间dp,顾名思义,在区间上dp,大多数题目的状态都是由区间(类似于dp[l][r]这种形式)构成的,就是我们可以把大区间转化成小区间来处理,然后对小区间处理后再回溯的求出大区间的值,主要的方法有两种,记忆化搜索和递推。
模板
无优化
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]);//状态转移方程
}
}
四边形优化
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;
}
}
}
}
例题
P2858 [USACO06FEB]Treats for the Cows G/S
思路
我们定义f[i][j]为卖掉i到j之间的临时得到的最大收益。
另外对与临时的价格我们做一个前缀和。
转移方程就应该是f[i][j]=max(f[i][j-1],f[i+1][j])+dis[j]-dis[i-1]f[i][j]=max(f[i][j−1],f[i+1][j])+dis[j]−dis[i−1]貌似跟楼下的不是很一样。
这是什么意思呢
既然是买点i到j之间的,那么这一天卖掉的一定是i或j,同时因为多了一天,所以我之前卖的应该滞后一天卖,也就是说每一个物品再增加一个单价,同时加上我现在卖出去的i或j,去一个较大值就可以了。
C o d e Code Code
#include<iostream>
using namespace std;
int dp[2010][2010];//dp数组
int a[2010],ans;//输入数组
int main(){
int n;
cin>>n;
for(int i=1;i<=n;i++)
cin>>a[i];//数据读入
for(int i=1;i<=n;i++)
for(int j=0;j<=i;j++){
int l=i-j;//推出右边取了多少个
dp[i][j]=max(dp[i-1][j]+a[n-l+1]*i,dp[i-1][j-1]+a[j]*i);//状态转移
//n-l+1就是从右边数第l个在a数组中的下标
}
for(int i=1;i<=n;i++)
ans=max(ans,dp[n][i]);//从最终状态中取一个最大值
cout<<ans;
return 0;
}
P1063 能量项链
思路
用f[l][r]表示以a[l]开头a[r]结尾的数串的最大和,如k为i,j之间任一节点,有f[l][r]=max(f[l][r],f[l][k]+f[k][r]+a[l]*a[k]*a[r]); 对l,r的定义自己一定要十分清晰,从而确定好循环的边界。
本题的小技巧:在环形问题中,可以选择(i+1)%n的方式,但也可以将n个元素复制一遍,变成2*n个元素,简化代码。
但也有问题随之而来,在更新时要将2*n个元素都更新,而不能只更新到前n个,否则访问到n+1~2n时会出错
C o d e Code Code
#include<bits/stdc++.h>
using namespace std;
int f[405][405];
int n,a[205];
int res;
int main(){
cin >> n;
for(int i=1;i<=n;i++){ //对环形问题的处理技巧
cin >> a[i];
a[n+i]=a[i];
}
for(int i=2;i<=n+1;i++){
for(int l=1;l+i-1<=2*n;l++){ //如果采取了上述策略,一定要将2*n个点都更新
int r=l+i-1;
for(int k=l+1;k<=l+i-2;k++)
f[l][r]=max(f[l][r],f[l][k]+f[k][r]+a[l]*a[k]*a[r]);
}
}
for (int i=1;i<=n;i++) res=max(res,f[i][n+i]);
cout<<res;
return 0;
}
P3205 [HNOI2010]合唱队
思路
那么我们要怎么设计状态,我们想,每给人进入队伍里,只有2种可能,1种是从左边加入,另外1种是从右边进入,所以我们的装态是有3个数
f[i][j][0]表示的是第i人从左边进来的方案数
f[i][j][1]表示的是第j人从右边进来的方案数
从左边进来肯定前1个人比他高,前1个人有2种情况,要么在i+1号位置,要么在j号位置。
同理,从右边进来肯定前1个人比他矮,前1个人有2种情况,要么在j-1号位置,要么在i号位置。
C o d e Code Code
#include <bits/stdc++.h>
using namespace std;
const int mod=19650827;
int f[2010][2010][2],a[2010];
int main(){
int n;
cin>>n;
for(int i=1;i<=n;i++)cin>>a[i];
for(int i=1;i<=n;i++)f[i][i][0]=1;
for(int len=1;len<=n;len++)
for(int i=1,j=i+len;j<=n;i++,j++){
if(a[i]<a[i+1])f[i][j][0]+=f[i+1][j][0];
if(a[i]<a[j])f[i][j][0]+=f[i+1][j][1];
if(a[j]>a[i])f[i][j][1]+=f[i][j-1][0];
if(a[j]>a[j-1])f[i][j][1]+=f[i][j-1][1];
f[i][j][0]%=mod;
f[i][j][1]%=mod;
}
cout<<(f[1][n][0]+f[1][n][1])%mod;
return 0;
}