动态规划的基本步骤

实际上,很多时候dp可以看作更广义的贪心,大多数情况下记忆化搜索和动态规划也可以相互转换。
选自洛谷题单上的部分动态规划入门题目进行解析
一、采药
经典的背包问题,剩余时间T,需要处理药品种类M,采用二维数组解法
- dp[i][j]的含义是采到第i株药草,还剩时间T,能够获得的总最大价值是多少
- dp[i][j]=max(dp[i-1][j],dp[i-1][j-timeCost[i]]+value[i])
- 第一行无法递推达到,第一行如果大于等于timeCost[0]就赋值values[0]
- 从dp[1][0]遍历到dp[M-1][T]
- 略
#include <iostream>
#include<vector>
using namespace std;
int main()
{
//每一株药草有价值和自身消费,给定一段时间,求能拿到药草价值的最大值
int T; scanf("%d",&T); //1~1000
int M; scanf("%d",&M); //1~100
//二维数组,横轴是药草种类,纵轴是剩余时间
vector<int>values(M,0);
vector<int>timeCost(M,0);
vector<vector<int>> dp(M,vector<int>(T+1,0));
for(int i=0;i<M;i++)
{
scanf("%d",&timeCost[i]);
scanf("%d",&values[i]);
}
for(int i=1;i<=T;i++)
{
if(i>=timeCost[0]) dp[0][i]=values[0];
}
for(int i=1;i<M;i++)
{
for (int j = 1; j <= T; j++)
{
if (j < timeCost[i]) {
dp[i][j] = dp[i - 1][j];
}
else
{
dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - timeCost[i]] + values[i]);
}
}
}
printf("%d\n",dp[M-1][T]);
return 0;
}
二、疯狂的采药
- dp[i][j] 是还有j体力值在第i种药草上可以获得的最大价值
- 递推公式 dp[i][j]=max(dp[i][j-cost[i]]+value[i],dp[i-1][j]);dp[i][j]=dp[i-1][j]
- 初始化,第0行,小于cost[0]的全置[0]大于cost[0]的就除以cost[0]看看个数
- 遍历顺序,从第0行到第N-1行
- 举例:略
经典的完全背包,用了空间压缩,或者叫滚动背包,和01背包压缩的区别在于遍历的顺序不同,完全背包是正向遍历,01背包反之
#include <vector>
#include <stdio.h>
using namespace std;
//全开long long 就好了
//完全背包压缩和01背包压缩的区别在于遍历的顺序不同
int main()
{
long long t,n;
scanf("%lld %lld",&t,&n);
vector<long long> timeCost(n,0);
vector<long long> value(n,0);
vector<long long> dp(t+1,0);
for(int i=0;i<n;i++)
{
scanf("%lld %lld",&timeCost[i],&value[i]);
}
for(int i=0;i<=t;i++)
{
dp[i]=i/timeCost[0]*value[0];
}
for(int i=1;i<n;i++)
{
for(int j=0;j<=t;j++)
{
if(j<timeCost[i])
{
dp[j]=dp[j];
}
else
{
dp[j]=max(dp[j],dp[j-timeCost[i]]+value[i]);
}
}
}
printf("%lld\n",dp[t]);
return 0;
}
三、5倍经验日
经典背包问题,注意开long long
- dp[i][j]数组的意义是,还有j瓶药水碰到第i个对手的时候,能获得经验的最大值
- 递推公式:dp[i][j]=dp[i-1][j]+fail[i] ; dp[i][j]=d[i-1][j-cost[i]]+win[i];
- 初始化,第一行初始化
- 遍历顺序,从第0行到第n-1行
- 略
//输出的是5s
//如果你第十个点过不了,请检查你的long long
#include <stdio.h>
#include <vector>
using namespace std;
int main()
{
int n,x;
scanf("%d %d",&n,&x);
vector<int> cost(n,0);
vector<int> win(n,0);
vector<int> fail(n,0);
vector<vector<long>>dp(n,vector<long>(x+1,0));
for(int i=0;i<n;i++)
{
scanf("%d",&fail[i]);
scanf("%d",&win[i]);
scanf("%d",&cost[i]);
}
//dp[i][j] 标识还有j个药物碰到第i个人能获得的最大经验
for(int j=0;j<=x;j++)
{
if(j>=cost[0])
{
dp[0][j]=win[0];
}
else
{
dp[0][j]=fail[0];
}
}
for(int i=1;i<n;i++)
{
for(int j=0;j<=x;j++)
{
if(j>=cost[i])
{
dp[i][j]=max(dp[i-1][j]+fail[i],dp[i-1][j-cost[i]]+win[i]);
}
else
{
dp[i][j]=dp[i-1][j]+fail[i];
}
}
}
printf("%lld\n",5*dp[n-1][x]);
}
四、kkksc03考前临时抱佛脚
这题,需要想好怎么dp,然后就是一个经典的01背包
首先对于同一本习题册,两脑并用,所以需要尽量把两个脑袋计算的时间调平来达到最高速率
计算总的时间,把总的时间分成两块;由于题目不一定完全适配这“一半时间”的容器,所以肯定有一个脑袋少一个脑袋多;
我们计算少用那个脑袋最贴近一半时间的情况,这个时候多用的那个脑袋也最贴近一半时间,达到总时间最少的效果。现在看看,是不是经典的01背包问题呢,总时长为j,有k个问题,每个问题的消费是t,价值也是t,开启dp即可。
#include <stdio.h>
#include <vector>
#include <algorithm>
using namespace std;
int findMinTime(vector<int>& S, int halftime) {
int k = S.size();
if (k == 0) return 0; // 避免空数组访问
vector<vector<int>> dp(k, vector<int>(halftime + 1, 0));
for (int j = 0; j <= halftime; j++) {
if (j >= S[0]) {
dp[0][j] = S[0];
}
}
for (int i = 1; i < k; i++) {
for (int j = 0; j <= halftime; j++) {
if (j < S[i]) {
dp[i][j] = dp[i - 1][j];
} else {
dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - S[i]] + S[i]);
}
}
}
return dp[k - 1][halftime];
}
int main() {
int s1, s2, s3, s4;
scanf("%d %d %d %d", &s1, &s2, &s3, &s4);
vector<vector<int>> S(4);
int sizes[4] = {s1, s2, s3, s4};
int ans = 0;
for (int i = 0; i < 4; i++) {
int tmpSum = 0;
S[i].resize(sizes[i]);
for (int j = 0; j < sizes[i]; j++) {
scanf("%d", &S[i][j]);
tmpSum += S[i][j];
}
ans += tmpSum - findMinTime(S[i], tmpSum / 2);
}
printf("%d", ans);
return 0;
}
五、小A点菜
分析基本和之前类似,经典的01背包
#include <stdio.h>
using namespace std;
#include <vector>
int main()
{
int N,M; scanf("%d %d",&N,&M);
vector<int> a(N,0);
for(int i=0;i<N;i++)
{
scanf("%d",&a[i]);
}
//其实是01背包
vector<int> dp(M+1,0);
// 递推
for(int i=0;i<N;i++)
{
for(int j=M;j>=0;j--)
{
if(j<a[i])
{
dp[j]=dp[j];
}
else if(j>a[i])
{
dp[j]=dp[j]+dp[j-a[i]];
}
else
{
dp[j]=1+dp[j];
}
}
}
printf("%d",dp[M]);
return 0;
}
六、摆花
- dp[i][j]的意义是还有j盆容量的时候想要加入第i种花最多有多少种方案
- 递推方法:考虑第i种花放k盆,
- if(k==j) dp[i][j]=dp[i][j]+1;
- if(k<j) dp[i][j]+=dp[i-1][j-k]
- 让初始化dp[0][0]=0其实不如特判 k==j的时候来的舒服,你非要去理解为什么容量为0,种类为0的时候有1种解决方案吗,感觉不如说在后边遍历的过程中,如果发现j==k,就+1,因为多的只是那唯一一种
- 从dp[1][0]遍历到dp[n][m]
- 举例略
#include <iostream>
#include <vector>
using namespace std;
const int MOD = 1e6 + 7;
int main() {
int n, m;
cin >> n >> m;
vector<int> a(n);
for (int i = 0; i < n; ++i) {
cin >> a[i];
}
vector<vector<int>> dp(n + 1, vector<int>(m + 1, 0));
dp[0][0] = 1; // 初始化:前0种花摆0盆有1种方案
for (int i = 1; i <= n; ++i) { // 遍历每种花
int max_k = a[i - 1]; // 当前花最多能选的数量
for (int j = 0; j <= m; ++j) { // 遍历总盆数
for (int k = 0; k <= max_k && k <= j; ++k) { // 当前花选k盆
dp[i][j] = (dp[i][j] + dp[i - 1][j - k]) % MOD;
}
}
}
cout << dp[n][m] % MOD << endl;
return 0;
}
七、数字三角形
P1216 [IOI 1994] 数字三角形 Number Triangles - 洛谷
有的时候其实写记忆化搜索比之前的更加好写,并且方便记录路径(只需要在递归函数中加入,逻辑更为清晰),这里以一道简单的数字三角线为例,进行讲解
#include <bits/stdc++.h>
using namespace std;
vector<vector<int>> memo; // 备忘录:memo[i][j]表示从(i,j)到底部的最大路径和
int dfs(int i, int j, vector<vector<int>>& triangle) {
if (i >= triangle.size() || j >= triangle[i].size()) return 0;
if (i == triangle.size()-1) return triangle[i][j];
if (memo[i][j] != -1) return memo[i][j];
int left = dfs(i+1, j, triangle);
int right = dfs(i+1, j+1, triangle);
return memo[i][j] = triangle[i][j] + max(left, right);
}
int main() {
int r;
scanf("%d", &r);
vector<vector<int>> triangle(r);
// 输入三角形数据
for (int i=0; i<r; i++) {
for (int j=0; j<=i; j++) {
int k;
scanf("%d", &k);
triangle[i].push_back(k);
}
}
// 初始化备忘录
memo.resize(r);
for (int i=0; i<r; i++) {
memo[i].resize(triangle[i].size(), -1);
}
// 计算最大和
int maxSum = dfs(0, 0, triangle);
printf("%d\n", maxSum);
return 0;
}
总结
本文选取了七道简单的动态规划入门题,简单梳理了动态规划的一般方法,给出易错点,展示使用记忆化搜索解决相关问题的策略.
912

被折叠的 条评论
为什么被折叠?



