动态规划入门【思路及易错点解析】

动态规划的基本步骤

实际上,很多时候dp可以看作更广义的贪心,大多数情况下记忆化搜索和动态规划也可以相互转换。

选自洛谷题单上的部分动态规划入门题目进行解析

一、采药

P1048 [NOIP 2005 普及组] 采药 - 洛谷

经典的背包问题,剩余时间T,需要处理药品种类M,采用二维数组解法

  1. dp[i][j]的含义是采到第i株药草,还剩时间T,能够获得的总最大价值是多少
  2. dp[i][j]=max(dp[i-1][j],dp[i-1][j-timeCost[i]]+value[i])
  3. 第一行无法递推达到,第一行如果大于等于timeCost[0]就赋值values[0]
  4. 从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;
}

二、疯狂的采药

  1. dp[i][j] 是还有j体力值在第i种药草上可以获得的最大价值
  2. 递推公式 dp[i][j]=max(dp[i][j-cost[i]]+value[i],dp[i-1][j]);dp[i][j]=dp[i-1][j]
  3. 初始化,第0行,小于cost[0]的全置[0]大于cost[0]的就除以cost[0]看看个数
  4. 遍历顺序,从第0行到第N-1行
  5. 举例:略

经典的完全背包,用了空间压缩,或者叫滚动背包,和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倍经验日

P1802 5 倍经验日 - 洛谷s

经典背包问题,注意开long long

  1. dp[i][j]数组的意义是,还有j瓶药水碰到第i个对手的时候,能获得经验的最大值
  2. 递推公式:dp[i][j]=dp[i-1][j]+fail[i] ;  dp[i][j]=d[i-1][j-cost[i]]+win[i];
  3. 初始化,第一行初始化
  4. 遍历顺序,从第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考前临时抱佛脚

P2392 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点菜

P1164 小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;
}

六、摆花

P1077 [NOIP 2012 普及组] 摆花 - 洛谷

  1. dp[i][j]的意义是还有j盆容量的时候想要加入第i种花最多有多少种方案
  2. 递推方法:考虑第i种花放k盆,
    1. if(k==j) dp[i][j]=dp[i][j]+1;
    2. if(k<j) dp[i][j]+=dp[i-1][j-k] 
  3. 让初始化dp[0][0]=0其实不如特判 k==j的时候来的舒服,你非要去理解为什么容量为0,种类为0的时候有1种解决方案吗,感觉不如说在后边遍历的过程中,如果发现j==k,就+1,因为多的只是那唯一一种
  4. 从dp[1][0]遍历到dp[n][m]
  5. 举例略
#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;
}

总结

本文选取了七道简单的动态规划入门题,简单梳理了动态规划的一般方法,给出易错点,展示使用记忆化搜索解决相关问题的策略.

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值