线性DP
线性dp问题是dp问题中比较简单的问题,通常一个状态转移方程就可以搞定,线性dp通常求最大值,最小值问题,下面介绍线性dp中从某点走到某点最值问题。
第一类问题(走一遍)
该类问题只走一遍,动态规划中用到的数组f(i,j)含义就是到达(i,j)点得到的最优解
例题1—数字三角形
问题描述
数字三角形题目链接
解题思路
此类题目关键在于找出动态转移方程,找出动态转移方程的关键在于分析,某点的移动方向,通常一个点只能向几个方向移动,我们在列出动态转移方程时,先确定某一点可以从那几个方向来,将几个方向的最优解进行比较选出最大或最小值,作为该点的最优解。
根据此题,从第一层一直走向最后一层所以,每次有两个放下,左下方和右下方,所以某点只可能,从左上方和右上方移动过来,进而比较左上方和右上方的最优值,得出动态转移方程!
f[i][j]=max(f[i-1][j],f[i-1][j-1])+w[i][j]
边界初始化问题
边界初始化问题一般是这类题目,需要预先处理数组初值,防止在动态转移方程比较过程中出错,比如这道题求解最大值,在边界部分都需要初始化最小值-INF(-0x7f7f7f7f)即可,如果叫不准边界,则将全部放大处理到n,后面代码有详细解释这样用的好处
运行代码
#include<bits/stdc++.h>
#define ll long long
#define INF 0x7f7f7f7f
using namespace std;
const int maxn=510;
int n,m,ans;
int f[maxn][maxn];
int w[maxn][maxn];
int main(){
cin>>n;//读入层数
for(int i=1;i<=n;i++){
for(int j=1;j<=i;j++){
cin>>w[i][j];
}
}
//边界初始化
for(int i=0;i<=n;i++){ //i可以从1开始,叫不准就从0开始
for(int j=0;j<=n;j++){ //这个j实际可以初始到i,如果叫不准放大处理到n
f[i][j]=-INF;
}
}
f[1][1]=w[1][1]; //第一层
for(int i=2;i<=n;i++){
for(int j=1;j<=i;j++){
f[i][j]=max(f[i-1][j],f[i-1][j-1])+w[i][j]; //动态转移方程
}
}
//在最后一层找到最大值
ans=-INF;
for(int i=1;i<=n;i++){
ans=max(ans,f[n][i]);
}
cout<<ans<<endl;
return 0;
}
例题2—摘花生
问题描述
题目链接
解题思路
题目中只能向东走和向南走,实际上就是向右走和向下走。所以某点只能从它的左侧和上侧移动过来所以比较,他的左侧和上侧的最优值即可。
边界问题:这道题由于没有负数,所以全局数组初始值为0,所以不用初始化处理边界问题
运行代码
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int maxn=110;
int ans,m,n,p;
int a[maxn][maxn];
int dp[maxn][maxn];
int main(){
int t;
cin>>t;
while(t--){
cin>>m>>n;
for(int i=1;i<=m;i++){
for(int j=1;j<=n;j++){
cin>>a[i][j];
}
}
for(int i=1;i<=m;i++){
for(int j=1;j<=n;j++){
dp[i][j]=max(dp[i][j-1],dp[i-1][j])+a[i][j];//动态转移方程
}
}
cout<<dp[m][n]<<endl;
}
return 0;
}
第二类问题(走两遍)
该类问题去的时候走一遍,回来的时候走一遍,我们可以统一为,从两个人从同一初始点同时出发,一起走到终点。 该类问题的遍历,正常想是枚举(x1,y1)和(x2,y2)那么需要四重for循环遍历所以情况,复杂度非常高,我们对其进行简化, 引出一个概念,两人同时走了k步,第一个人走了k步,与第二个人走了k步,这样做有什么好处呢? 这类问题经常处理两人路线有交集,两人经过同一位置,我们需要特殊处理这种位置,关键在于如何找出来呢,当两人位于同一位置,那么他们走过的步数一定相同,都为k步,那么我在枚举出两人的横坐标,就可以得出两人的纵坐标,y=k-x (题目要求从左上角走到右下角,只能向右或下走) 通过引入同时走k步概念,也就只需要三重for循环就可以达到四重for循环的效果。
动态转移方程分析
通过上面分析,得出通过k步三个for循环枚举出,第一个人和第二个人同时走k步,两人位于不同位置或者相同位置情况,我们先定义一下dp所用到的数组含义f(k,i1,i2) k是同时走k步,i1,i2为二人的横坐标,那么到达该点,可以通过四种情况到达,第一种两人同时向下移动,第二种,第一个人向右,第二个人向下,第三种第一个人向下,第二个人向右,第四种,两人同时向下,通过以上四种情况到达该点。
通过上面分析动态转移方程为:
f(k,i1,i2)=max/min(f(k-1,i1-1,i2-1),f(k-1,i1,i2-1),f(k-1,i1-1,i2),f(k-1,i1,i2))+w(i1,j1)+w(i2,j2)
i1,i2为两人的横坐标,j1,j2为两人的纵坐标 j1=k-i1,j2=k-j2
例题三—方格取数
题目描述
题目链接
题目分析
题目说从左上角,走到右下角,在从右下角走到左上角,可以简化为两人同时从左上角走到右下角。关在在于处理路线交集部分,交集部分只能加上一个权值,根据上面的分析我们得出动态转移方程
f(k,i1,i2)=max/min(f(k-1,i1-1,i2-1),f(k-1,i1,i2-1),f(k-1,i1-1,i2),f(k-1,i1,i2))+w(i1,j1)+w(i2,j2)
边界处理
由于求最大值,并且权值均大于0所以不需要额外处理
运行代码
#include<bits/stdc++.h>
#define ll long long
#define INF 0x7f7f7f7f
using namespace std;
const int maxn=10;
int n,m;
int dp[maxn*2][maxn][maxn];
int w[maxn][maxn];
int main(){
cin>>n;
while(1){
int x,y,v;
cin>>x>>y>>v;
if(x==0&&y==0)break;
w[x][y]=v;
}
for(int k=1;k<=n*2;k++){
for(int i1=1;i1<=n;i1++){
for(int i2=1;i2<=n;i2++){
int j1=k-i1,j2=k-i2;
int t;
if(j1>=1&&j1<=n&&j2>=1&&j2<=n){//判断是否越界
t=w[i1][j1];
if(i1!=i2)t+=w[i2][j2];//不在同一点
int &x=dp[k][i1][i2];//这样写方便
x=max(x,dp[k-1][i1-1][i2-1]+t);//下下
x=max(x,dp[k-1][i1-1][i2]+t);//下右
x=max(x,dp[k-1][i1][i2-1]+t);//右下
x=max(x,dp[k-1][i1][i2]+t);//右右
}
}
}
}
cout<<dp[n*2][n][n];
return 0;
}
例题四—传纸条
题目链接
题目分析
这道题,只需要注意,每人只能传一次,也就是两条路线不能有交集,两人不能在同一坐标内。特殊判断一下即可,动态转移方程和上一道题完全相同
实现代码
#include<bits/stdc++.h>
#define ll long long
#define INF 0x7f7f7f7f
using namespace std;
const int maxn=10;
int n,m;
int dp[maxn*2][maxn][maxn];
int w[maxn][maxn];
int main(){
cin>>n;
while(1){
int x,y,v;
cin>>x>>y>>v;
if(x==0&&y==0)break;
w[x][y]=v;
}
for(int k=1;k<=n*2;k++){
for(int i1=1;i1<=n;i1++){
for(int i2=1;i2<=n;i2++){
int j1=k-i1,j2=k-i2;
int t;
if(j1>=1&&j1<=n&&j2>=1&&j2<=n){//判断是否越界
t=w[i1][j1];
if(i1!=i2)t+=w[i2][j2];//不在同一点
int &x=dp[k][i1][i2];//这样写方便
x=max(x,dp[k-1][i1-1][i2-1]+t);//下下
x=max(x,dp[k-1][i1-1][i2]+t);//下右
x=max(x,dp[k-1][i1][i2-1]+t);//右下
x=max(x,dp[k-1][i1][i2]+t);//右右
}
}
}
}
cout<<dp[n*2][n][n];
return 0;#include<bits/stdc++.h>
#define ll long long
#define INF 0x7f7f7f7f
using namespace std;
const int maxn=50;
int n,m;
int dp[maxn*2][maxn][maxn];
int w[maxn][maxn];
int main(){
cin>>m>>n;
for(int i=1;i<=m;i++){
for(int j=1;j<=n;j++){
cin>>w[i][j];
}
}
for(int k=2;k<=m+n;k++){
for(int i1=1;i1<=m;i1++){
for(int i2=1;i2<=m;i2++){
int j1=k-i1,j2=k-i2;
if(j1>=1&&j1<=n&&j2>=1&&j2<=n){
int t=w[i1][j1]+w[i2][j2];
if(i1!=i2||k==2||k==n+m){ //只能传一次,起始点,终点位置
int &x=dp[k][i1][i2];
x=max(x,dp[k-1][i1-1][i2-1]+t);//下下
x=max(x,dp[k-1][i1][i2-1]+t);//右下
x=max(x,dp[k-1][i1-1][i2]+t);//下右
x=max(x,dp[k-1][i1][i2]+t);//右右
}
}
}
}
}
cout<<dp[m+n][m][m];
return 0;
}
总结
从某点走到某点是线性dp的经典问题,处理该问题需要找到合适的动态转移方程,并且对边界进行处理,笔者对此类问题进行了分类,分为走一次和走两次问题。走两次问题又对动态转移数组进行的简化处理,整体解释的比较详细,感谢大家观看,有问题评论区见。