济南 CSP-J 刷题营 Day3 动态规划
Solution
T1 方格涂色
原题链接
简要思路
分列进行讨论,只有三种状态:选了上面、选了下面、上下都没选。只需讨论一下状态之间的转移即可。
完整代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int MAXN=1e6+5;
int n;
long long a[5][MAXN];
long long dp[5][MAXN][5];
//dp[i][j][k] 表示当前考虑第 i 行第 j 列的格子,在 k 的情况下(k=1 代表选了,k=0 代表未选)当前数字和的最大值
signed main(){
cin>>n;
for(int i=1;i<=2;i++)
for(int j=1;j<=n;j++)
cin>>a[i][j];
dp[2][1][1]=a[2][1];
dp[1][1][1]=a[1][1];//每一行的第一列选中的情况先提前处理
for(int j=2;j<=n;j++)
for(int i=1;i<=2;i++){
if(i==1){//第一行
dp[i][j][0]=max(dp[i][j-1][1],dp[i+1][j-1][1]);//取上一列两个数中选中的最大值
dp[i][j][1]=max(dp[i][j-1][0],dp[i+1][j-1][1])+a[i][j];//上一列第一行未选、上一列第二行已选
}
else{//第二行
dp[i][j][0]=max(dp[i][j-1][1],dp[i-1][j-1][1]);
dp[i][j][1]=max(dp[i][j-1][0],dp[i-1][j-1][1])+a[i][j];//dp 原理同上
}
//注意当第三维为 1 的时候(代表选中),勿忘加上 a[i][j]
}
cout<<max(dp[1][n][0],max(dp[1][n][1],max(dp[2][n][0],dp[2][n][1])))<<endl;//最后一列的两个数 选/不选 中取最大值作为答案进行输出
return 0;
}
T2 方格行走
原题链接
简要思路
直接在二维方格中遍历每一个可能到达的点(即保证横坐标大于等于纵坐标),用所有能够一步到达该点的(即左下、左边、下边三个位置)方案数量和最为该点的方案数。
完整代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int mod=998244353;
int n,ans;
int dp[10005][10005];//dp[i][j] 代表走到 (i,j) 这个点的方案数
signed main(){
cin>>n;
dp[0][0]=1;//初始值:待在起点不走的方案数
for(int i=1;i<=n;i++)
dp[i][0]=dp[i-1][0];//先把最下面一行的方案数赋初值
for(int i=1;i<=n;i++)
for(int j=1;j<=i;j++)//在循环枚举中保证合法(即横坐标 i 大于等于纵坐标 j)
dp[i][j]=(dp[i-1][j]%mod+dp[i][j-1]%mod+dp[i-1][j-1]%mod)%mod;
//三个可能一步到达该点的方案取和,注意及时取模
cout<<dp[n][n]%mod<<endl;//输出能够到达 (n,n) 且合法的走法方案的数量
return 0;
}
T3 下山路径
原题链接
相似题目:P1434 [SHOI2002] 滑雪
简要思路
一道中规中矩的记忆化搜索板子题。
记忆化搜索:
使用普通搜索时,有的时候要求的答案可能之前以前已经求过了,那么这时我们再次搜索一遍就会显得繁琐没有必要(主要是会浪费时间),所以我们运用一个数组,记录下之前搜索到的答案,如何求的答案在数组中已经有所记录,那么我们直接调用即可。
完整代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
int dx[4]={0,0,1,-1};
int dy[4]={1,-1,0,0};//偏移量
int n,a[1005][1005];
int Ms[1005][1005];//Memory search:记忆化搜索
int dfs(int x,int y){
if(Ms[x][y])return Ms[x][y];//如果之前求过就直接调用
Ms[x][y]=1;//初始值为 1,因为自身就是可以经过的一个地方
for(int i=0;i<4;i++){//枚举四个方向
int xx=x+dx[i];
int yy=y+dy[i];
//(xx,yy) 就是枚举到的点的坐标
if(xx>0&&yy>0&&xx<=n&&yy<=n&&a[x][y]>a[xx][yy]){
//保证 (xx,yy) 合法:
//1. 保证这个点存在
//2. (xx,yy) 的高度小于 (x,y) 的高度(这样才能走到)
dfs(xx,yy);//搜索当前位置的答案
Ms[x][y]=max(Ms[x][y],Ms[xx][yy]+1);//维护答案,注意不要忘了 +1
}
}
return Ms[x][y];
}
signed main(){
cin>>n;
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
cin>>a[i][j];
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++)
cout<<dfs(i,j)<<' ';
cout<<endl;
}
return 0;
}
T4 分配数字
原题链接
简要思路
滚动数组 + 背包。
维护一个数组 dp
,dp[i][j]
代表前 \(i\) 个数,差值为 \(j\) 的时候,两个人的较大值最大为多少。
\(dp\) 的转移有点像背包,注意讨论大小关系转变的情况。
完整代码
#include<bits/stdc++.h>
#define int long long
#define endl '\n'
using namespace std;
int dp[2][5000005];//dp[i][j] 代表前 i 个数,两个人的数字差为 j 的时候,两个人的较大值最大位多少
int a[55];
int n,ans;
signed main(){
cin>>n;
for(int i=1;i<=n;i++){
cin>>a[i];
ans+=a[i];//计算最大差值
}
int las=0,cur=1;
for(int i=1;i<=ans;i++)
dp[las][i]=-1e18;//初始赋值
for(int i=1;i<=n;i++){
for(int j=0;j<=ans;j++)
dp[cur][j]=dp[las][j];
for(int j=0;j<=ans;j++){//所有可能的差值
if(j+a[i]<=ans)dp[cur][j+a[i]]=max(dp[cur][j+a[i]],dp[las][j]+a[i]);
if(j<a[i])dp[cur][a[i]-j]=max(dp[cur][a[i]-j],dp[las][j]-j+a[i]);
else dp[cur][j-a[i]]=max(dp[cur][j-a[i]],dp[las][j]);//考虑所有选数的可能
}
las^=1,cur^=1;//滚动数组
}
cout<<dp[las][0]<<endl;//差值为 0
return 0;
}
讲课笔记
DP 的建模
-
状态
-
转移
递推类
-
Fibonacci number
-
Catalan number
-
错排记数
线性 DP 的最优化
-
链上最大独立集
-
LIS
补充
老师 PPT 笔记内容较少,可以当做复习的目录,这里再给大家推荐几个比较好的 DP 笔记。