文章目录
什么是区间DP
区间dp就是在区间上进行动态规划,求解一段区间上的最优解。主要是通过合并小区间的最优解进而得出整个大区间上最优解的dp算法。
重点在于求出递推公式和确定边界条件。
一、石子合并
1.分析
要点:
1.断环成链:题目要求的是一个环形,我们需要借助数组模拟出一个环形结构。
在环型结构中,我们的要求是从任意起点开始均可以遍历整个环,那么我们不妨将原来的数组直接加倍,那么从数组的前半部分中的任意元素开始均可以得出一个完整的原数组。
注意此时DP数组的大小同时也要扩大为原来的两倍
2.预处理:每次得出一个新堆,最终结果要加上此区间内原来所有堆的和,所以我们需要用一个前缀和便于求出区间和,同时为了满足环形结构,我们仍然需要使用一个两倍大小的数组来存前缀和。
3.递推公式:以求该区间的最大值为例
i为起点,j为终点,k为切割点,sum为前缀和数组
dp[i][j]=max(dp[i][j],dp[i][k] + dp[k+1][j] + sum[j] - sum[i-1])
若求区间的最小值,则要先对DP[i][j]做一个预处理,使其为一个较大的值
DP[i][j]=min(DP[i][j],DP[i][k] + DP[k+1][j] + sum[j] - sum[i-1])
4.边界确定
2.代码
#include<bits/stdc++.h>
using namespace std;
const int MAN=105;
int n;
int a[MAN*2];
int s[MAN*2];
int dp[2*MAN][2*MAN];//min
int DP[2*MAN][2*MAN];//max
int main(){
cin>>n;
for(int i=1;i<=n;i++){
cin>>a[i];
a[i+n]=a[i];
}
for(int i=1;i<=2*n;i++){
s[i]=s[i-1]+a[i];
}
int mi=1e9;
int ma=0;
for(int len=1;len<n;len++){
for(int i=1;i<2*n-len;i++){
int j=i+len;
dp[i][j]=1e9;
for(int k=i;k<j;k++){
dp[i][j]=min(dp[i][j],dp[i][k]+dp[k+1][j]+s[j]-s[i-1]);
DP[i][j]=max(DP[i][j],DP[i][k]+DP[k+1][j]+s[j]-s[i-1]);
ma=max(ma,DP[i][j]);
}
}
}
for(int i=1;i<=n;i++){
mi=min(mi,dp[i][i+n-1]);
}
cout<<mi<<endl<<ma<<endl;
}
二、能量项链
1.分析
同样是一个区间处理的问题,先确定小区间再推广至大区间,跟上一题很相似,不过是递推公式有一些变化。
要点:
1.断链成环
2.条件转化:这里的每个珠子都有两个值,设有两个相邻的珠子a、b,存在条件a.rear==b.front,将两个珠子合并时需要在结果加上a.front * a.rear * b.rear,同时得出一个新珠子(a.front,b.rear),我们需要做的是将这一步与递推联系在一起。
i为起点,j为终点,k为切割点,此时求dp[i][j](范围更小的区间已有最优解)
那我们依照题目求一个区间的值的公式就是:
dp[i][k]+dp[k+1][j]+a[i] * a[k+1] * a[j+1]
对于珠子dp[i][k]、dp[k+1][j]来说:
a.front = a[i]
a.rear = b.front = a[k+1]
b.rear = a[j+1]
3.递推公式:
dp[i][j]=max(dp[i][j],dp[i][k] + dp[k+1][j] + a[i] * a[k+1] * a[j+1])
2.代码
#include<bits/stdc++.h>
using namespace std;
const int MAN=105;
int n;
int a[MAN*2];
int DP[2*MAN][2*MAN];//max
int main(){
cin>>n;
for(int i=1;i<=n;i++){
cin>>a[i];
a[i+n]=a[i];
}
int ma=0;
for(int len=1;len<n;len++){
for(int i=1;i<2*n-len;i++){
int j=i+len;
for(int k=i;k<j;k++){
DP[i][j]=max(DP[i][j],DP[i][k]+DP[k+1][j]+a[i]*a[k+1]*a[j+1]);
ma=max(ma,DP[i][j]);
}
}
}
cout<<ma<<endl;
}
三、矩阵取数游戏
1.分析
数据有n行m列,但每一行之间却不存在联系,可以分开讨论,最后的结果其实是每行最优解的总和。
要点:
1.区间dp:对于最初的一行,我们需要从首或尾选一个数出来,并且将其与对应的权值相乘加到最后的结果里,不断重复,直至行为空。我们从长度为0的小区间(即自身)开始讨论,再不断推广到大区间(由1到m);
2.递推公式:我们以dp[i][j]表示从i到j的最优解;
对于dp[i][j]来说,其可能源于两个状态dp[i+1][j] (取首点)、dp[i][j-1] (取尾点);
我们选择顺序其实是倒着选的,意味着我们所使用的权值要也要倒着使用,从大到小;
关于这个,我们具体的实现思路有两个
第一个用数组分别存下权值:
dp[i][j]=max(dp[i+1][j]+a[i]*对应权值,dp[i][j-1]+a[i] * 对应权值 )
第二个参考进制思想:
dp[i][j]=max(dp[i+1][j]*2+a[i]*2,dp[i][j-1]*2+a[j]*2)
3.高精度操作:
题目要求的数据大小太恐怖了,一般的数据类型是存不下的,所以我们需要使用高精度,在这里见识到了一种神奇的高精度写法———__int128;
详情见于一个大佬的文章:关于__int128
操作与int相同,但需要手写输入输出,可能要背下来了
void scan(__int128 &x)//输入
{
x = 0;
int f = 1;
char ch;
if((ch = getchar()) == '-') f = -f;
else x = x*10 + ch-'0';
while((ch = getchar()) >= '0' && ch <= '9')
x = x*10 + ch-'0';
x *= f;
}
void print(__int128 x)
{
if(x < 0)
{
x = -x;
putchar('-');
}
if(x > 9) print(x/10);
putchar(x%10 + '0');
}
但这个并不是所有的GCC编译器都支持,所以最好还是要掌握高精度写法。
2.代码
#include<bits/stdc++.h>
using namespace std;
const int MAN=100;
int a[MAN];
__int128 dp[MAN][MAN],ans;
int n,m;
//void scan(__int128 &x)//输入
//{
// x = 0;
// int f = 1;
// char ch;
// if((ch = getchar()) == '-') f = -f;
// else x = x*10 + ch-'0';
// while((ch = getchar()) >= '0' && ch <= '9')
// x = x*10 + ch-'0';
// x *= f;
//}
void print(__int128 x)
{
if(x < 0)
{
x = -x;
putchar('-');
}
if(x > 9) print(x/10);
putchar(x%10 + '0');
}
int main(){
cin>>n>>m;
while(n--){
for(int i=1;i<=m;i++){
cin>>a[i];
}
memset(dp,0,sizeof(dp));
for(int len=0;len<m;len++){
for(int i=1;i<=m-len;i++){
int j=i+len;
dp[i][j]=max(dp[i+1][j]*2+a[i]*2,dp[i][j-1]*2+a[j]*2);
}
}
ans+=dp[1][m];
}
print(ans);
}