动态规划解题一般步骤(附带案例解析)

1 题目场景

1 计数

  • 有多少种方式走到右下角;
  • 有多少种方法选出k个数使得和为sum;

2 求最值

  • 从左上角走到右下角路径的最大数字和;
  • 最长不含重复字符的子字符串;

3 求存在性

  • 取石子游戏,先手是否必胜;
  • 能不能选出k个数使得和是sum;

2 解题步骤

  1. 确定状态(两个核心:1、最后一步;2、化成子问题得状态方程);
  2. 建立状态转移方程;
  3. 确定开始和边界条件;
  4. 计算顺序;

3 案例分析

3.1 *最少的硬币组合

  1. 有三种硬币,分别是2元、5元和7元;
  2. 买一本书需要27元;
  3. 如何用最少的硬币组合正好付清,不需要对方找钱;

3.1.1 确定状态

3.1.1.1 最后一步
  1. 最优策略是k枚硬币,最后一个硬币的面值是ak,则前面的k-1枚硬币总值为27-ak;
  2. 我们不关心前面的k-1枚硬币是怎么拼出27-ak的,甚至不知道ak和k,但可以确定前面的硬币拼出了27-ak;
  3. 因为是最优策略,所以拼出27-ak的硬币数一定要最少;

在这里插入图片描述

3.1.1.2 化成子问题得状态方程
  1. 原问题是用最少多少枚硬币拼出27,现将其转化成子问题,而且规模更小:用最少多少枚硬币拼出27 - ak;
  2. 状态方程:f(x) = 最少多少枚硬币拼出x;

3.1.2 建立状态转移方程

  1. 最后那枚硬币可能是2、5或7,因此f(27) = min{f(27-2), f(27-5), f(27-7)} + 1;
  2. 可得状态转移方程:f(x) = min{f(x-2), f(x-5), f(x-7)} + 1;

3.1.3 确定开始和边界条件

3.1.3.1 确定开始

f(0) = 0,即拼出0元需要0枚硬币。

3.1.3.2 边界条件
  1. 如果x小于0,意味着不能拼出x,于是令f(x) = 无穷大;
  2. 因此f(1) = min{f(-1), f(-4), f(-6)} + 1 = 无穷大,表示拼不出1;

3.1.4 计算顺序

  1. 从f(0)开始,计算f(1)、f(2)、f(3)、…、f(27);
  2. 也就说是,当计算到f(x)时,f(x-2)、f(x-5)、f(x-7)已经得到结果了;
  3. f(x)有两种情况:1、= 最少用多少枚硬币拼出x;2、= 无穷大表示无法用硬币拼出x;

在这里插入图片描述

代码(Java)

class Solution {
    //coin代表硬币的种类[2,5,7],x代表要拼凑的值
    public int coinChange(int[] coin, int x) {
        int[] f = new int[x+1];
        f[0] = 0;
        for (int i=1; i<=x; i++) {
            f[i] = (int)Double.POSITIVE_INFINITY; // 开始默认无穷大,即不能拼凑
            for (int j=0; j<coin.length; j++) { // 三种面值的硬币各循环一次
                if ((i - coin[j]) >= 0 && f[i - coin[j]] != (int)Double.POSITIVE_INFINITY) {
                    f[i] = Math.min(f[i], f[i - coin[j]] + 1);
                }
            }
        }
        if (f[x] == (int)Double.POSITIVE_INFINITY) {
            return -1;
        }
        return f[x];
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        int[] coin = new int[] {2, 5, 7};
        System.out.println(s.coinChange(coin, 27));
    }
}
5

3.2 *多少种方式走到右下角

给定m*n的网格,机器人从左上角(0,0)出发,每一步可以向下或向右走一步,有多少种不同的方式走到右下角?

在这里插入图片描述

3.2.1 确定状态

3.2.1.1 最后一步

右下角坐标(m-1,n-1),前一步机器人向右或向下到达右下角,前一步机器人的坐标(m-2,n-1)或(m-1,n-2)。

3.2.1.2 化成子问题
  1. 设机器人有x种方式从左上角走到(m-2,n-1),有y种方式从左上角走到(m-1,n-2),因此共有x+y种方式走到(m-1,n-1);
  2. 子问题:机器人分别有多少种方式从左上角走到(m-2,n-1)和(m-1,n-2);
  3. 状态方程:设f(x)(y) = 机器人有多少种方式从左上角走到(i,j);

3.2.2 建立状态转移方程

  1. 前一步可能在最后一步的左边或上边;
  2. 可得状态转移方程:f(x)(y) = f(x-1)(y) + f(x)(y-1);

3.2.3 确定开始和边界条件

3.2.3.1 确定开始

f(0)(0) = 1,机器人只有一种方式到左上角。

3.2.3.2 边界条件

x = 0或y = 0,前一步唯一,因此f(x)(y) = 1。

3.2.4 计算顺序

  1. 从f(0)(0)开始,利用双层for循环,依次计算每一行的值;
  2. 答案即f(m-1)(n-1);

代码(Java)

class Solution {
    public int moveCounts(int m, int n) {
        int[][] matrix = new int[m][n];
        for (int i=0; i<m; i++) {
            for (int j=0; j<n; j++) {
                if (i==0 || j==0) matrix[i][j] = 1;
                else matrix[i][j] = matrix[i-1][j] + matrix[i][j-1];
            }
        }
        return matrix[m-1][n-1];
    }

    public static void main(String[] args) {
        Solution test = new Solution();
        System.out.println(test.moveCounts(3,2));
    }
}
3

3.3 *青蛙能否跳到第n个石头上

  1. 有n块石头分别置于x轴的0,1,…,n-1位置;
  2. 一只青蛙在石头0,想跳到石头n-1;
  3. 如果青蛙在第i块石头上,它最多可以向右跳a[i];
  4. 问青蛙能否跳到石头n-1;

例1:

输入:a=[2,3,1,1,4]

输出:True

例2:

输入:a=[3,2,1,0,4]

输出:False

3.3.1 确定状态

3.3.1.1 最后一步
  1. 如果青蛙能跳到最后一块石头n-1,假设最后一步是从石头i跳过来的,i < n-1;
  2. 需要满足两个条件:1、青蛙可以跳到石头i;2、从石头i可以跳到石头n-1,即n-1-i <= a[i];

在这里插入图片描述

3.3.1.2 化成子问题得状态方程
  1. 原问题求青蛙能不能跳到石头n-1,现将其转化成子问题:求青蛙能不能跳到石头i(0 <= i < n-1);
  2. 状态方程:f(x):青蛙能不能跳到石头x;

3.3.2 建立状态转移方程

得状态转移方程f(x) = OR0<=i<x(f(i) AND i + a(i) >= x)。

  • 其中OR0<=i<x表示枚举上一次跳到的石头i

3.3.3 确定开始和边界条件

3.3.3.1 确定开始

f(0) = True,因为青蛙一开始就在石头0

3.3.3.2 边界条件

3.3.4 计算顺序

  1. 初始化f(0) = True;
  2. 计算f(1)、f(2)、…、f(n-1);
  3. 答案是f(n-1);

代码(Java)

class Solution {
    public boolean canJump(int[] a) {
        int n = a.length;
        boolean[] dp = new boolean[n];
        dp[0] = true;
        for(int i=1; i<n; i++) {
            dp[i] = false;
            for (int j=i-1; j>=0; j--) {
                if(dp[j] && j + a[j] >= i) {
                    dp[i] = true;
                    break;
                }
            }
        }
        return dp[n-1];
    }

    public static void main(String[] args) {
        Solution test = new Solution();
        int[] a = {2,3,1,1,4};
        int[] b = {3,2,1,0,4};
        System.out.println(test.canJump(a));
        System.out.println(test.canJump(b));
    }
}

也可通过维护可到达的最远距离降低时间复杂度和空间复杂度,参见 https://blog.csdn.net/sc179/article/details/115413793

  • 0
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

hellosc01

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值