【保姆级】手把手详解动态规划二维动态数组推导及优化过程

我们已经进入动态规划有一段时间了,相信有很多小伙伴像我一样,对动态规划的dp数组是如何从二维优化成一维的产生过很大的疑惑,本篇文章我们通过Leetcode的322.零钱兑换(Coin Change)这道题,一步一步分析二维数组的推导过程以及如何通过二维数组优化成一维数组,希望这篇文章能帮助到有同样困惑的小伙伴(。・ω・。)ノ

题目描述

给你一个整数数组 coins ,表示不同面额的硬币;以及一个整数 amount ,表示总金额。

计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额,返回-1

你可以认为每种硬币的数量是无限的。

示例 1:

ini 复制代码
输入: coins =[1, 2, 5], amount =11
输出: 3 
解释: 11 = 5 + 5 + 1

示例 2:

ini 复制代码
输入: coins =[2], amount =3
输出: -1

示例 3:

输入: coins = [1], amount = 0 输出: 0

提示:

  • 1 <= coins.length <= 12
  • 1 <= coins[i] <= 2^31- 1
  • 0 <= amount <= 10^4

题目地址

leetcode.cn/problems/co...

解题思路

二维数组

我们拿示例 1做详细的详细步骤拆解的分析,看看二维dp数组是如何推出的,进一步又是如何优化成一位数组的

输入: coins =[1, 2, 5], amount =11

输出: 3

解释: 11 = 5 + 5 + 1

由于题目中求得是最小值,所以需要把数组初始化为最大值,但是因为题目的实际含义,我们可以省点儿事儿,就初始化为amount+112,如下图

当使用二维数组进行推导时,此时每个值(dp[i][j])代表在数组coins[0,i]范围内取硬币(每个硬币不限数量)总值为j 时的最少硬币数

然后对初始值进行初始化,先初始化第一行的数据,由于第一个硬币面值是1,总值为多少,就需要多少个,如下图:

再初始化第一列的数据,总值为0,无论硬币面值是多少,方法都是0,如下图进行初始化:

初始化完成后我们开始遍历:

当新增硬币面值>总值时:取上一行相同总值所需的最少硬币数(新的硬币无法加入)

当新增硬币面值<=总值时:取上一行相同总值所需的最少硬币数 (新的硬币无法加入)和总值减去新增硬币面值后得到的"总值"所需要的最少硬币数中的较小值

同理推出第二行后面的值

同理推出第三行

按照上述逻辑,具体代码如下:

java 复制代码
class Solution {
    public int coinChange(int[] coins, int amount) {
        if (amount == 0) {
            return 0;
        }
        // dp[i][j] 表示从[0,i]中任选硬币总和为j的最少硬币数为dp[i][j]
        int[][] dp = new int[coins.length][amount + 1];
        // 初始化
        for (int i = 0; i < coins.length; i++) {
            for (int j = 0; j < amount + 1; j++) {
                dp[i][j] = amount + 1;
            }
        }
        for (int j = 0; j < amount + 1; j++) {
            if (j >= coins[0] && j % coins[0] == 0) {
                dp[0][j] = j / coins[0];
            }
        }
        for (int i = 0; i < coins.length; i++) {
            dp[i][0] = 0;
        }

        for (int i = 1; i < coins.length; i++) {
            for (int j = 1; j < amount + 1; j++) {
                if (j < coins[i]) {
                    dp[i][j] = dp[i - 1][j];
                } else {
                    dp[i][j] = Math.min(dp[i - 1][j], dp[i][j - coins[i]] + 1);
                }
            }
        }
        
        return dp[coins.length - 1][amount] > amount ? -1 : dp[coins.length - 1][amount];
    }
}

执行结果

复杂度分析

  • 时间复杂度:O(n*amount),其中 amount是总金额,n 是数组 coins 的长度
  • 空间复杂度:O(n*amount)

接下来,我们看看如何将二维数组优化为一位数组

一维数组

通过上面的分析我们可以看到整个推导顺序是从上至下从左到右

并且总值的增长正好与序号的增长一致,因此我们可以使用一位数组的序号作为总值,一个一个硬币来进行最少硬笔数的计算,不断更新一维数组值,将二维数组压缩成一维数组。此时每个值(dp[j])代表在数组coins[0,i]范围内取硬币(每个硬币不限数量)总值为j 时的最少硬币数

i=0时,我们更新一维数组,如下图:

i=1时,我们更新一维数组,如下图:

i=2时,我们更新一维数组,如下图:

完整代码如下:

java 复制代码
class Solution {
    public int coinChange(int[] coins, int amount) {
         int[] dp = new int[amount + 1];
        Arrays.fill(dp, amount + 1);
        dp[0] = 0;
        for (int i = 0; i < coins.length; i++) {
            for (int j = 1; j < amount + 1; j++) {
                if (j >= coins[i]) {
                    dp[j] = Math.min(dp[j], dp[j - coins[i]] + 1);
                }
            }
        }
        return dp[amount] > amount ? -1 : dp[amount];
    }
}

执行结果

复杂度分析

  • 时间复杂度:O(n*amount),其中 amount是总金额,n 是数组 coins 的长度
  • 空间复杂度:O(amount)

总结

可以看到其实无论是使用一维数组还是二维数组,整体的逻辑以及时间复杂度并没有变化,只是在空间复杂度上进行了优化,而能进行优化的本质原因也是因为在本行的值只和它上一行的值以及当前行的值相关(详见上述推导图),其实就是数组的复用。

相关推荐
Ch.yang2 分钟前
【Spring】 Bean 注入 HttpServletRequest 能保证线程安全的原理
java·spring·代理模式
web150850966413 分钟前
基于Mysql、JavaScript、PHP、ajax开发的MBTI性格测试网站(前端+后端)
java
昙鱼11 分钟前
springboot创建web项目
java·前端·spring boot·后端·spring·maven
eternal__day11 分钟前
数据结构(哈希表(中)纯概念版)
java·数据结构·算法·哈希算法·推荐算法
天之涯上上15 分钟前
JAVA开发 在 Spring Boot 中集成 Swagger
java·开发语言·spring boot
2402_8575834916 分钟前
“协同过滤技术实战”:网上书城系统的设计与实现
java·开发语言·vue.js·科技·mfc
白宇横流学长17 分钟前
基于SpringBoot的停车场管理系统设计与实现【源码+文档+部署讲解】
java·spring boot·后端
APP 肖提莫21 分钟前
MyBatis-Plus分页拦截器,源码的重构(重构total总数的计算逻辑)
java·前端·算法
kirito学长-Java22 分钟前
springboot/ssm太原学院商铺管理系统Java代码编写web在线购物商城
java·spring boot·后端
爱学习的白杨树23 分钟前
MyBatis的一级、二级缓存
java·开发语言·spring