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

我们已经进入动态规划有一段时间了,相信有很多小伙伴像我一样,对动态规划的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)

总结

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

相关推荐
程序员南飞1 小时前
ps aux | grep smart_webrtc这条指令代表什么意思
java·linux·ubuntu·webrtc
弥琉撒到我1 小时前
微服务swagger解析部署使用全流程
java·微服务·架构·swagger
一颗花生米。2 小时前
深入理解JavaScript 的原型继承
java·开发语言·javascript·原型模式
问道飞鱼2 小时前
Java基础-单例模式的实现
java·开发语言·单例模式
ok!ko6 小时前
设计模式之原型模式(通俗易懂--代码辅助理解【Java版】)
java·设计模式·原型模式
2402_857589366 小时前
“衣依”服装销售平台:Spring Boot框架的设计与实现
java·spring boot·后端
吾爱星辰7 小时前
Kotlin 处理字符串和正则表达式(二十一)
java·开发语言·jvm·正则表达式·kotlin
哎呦没7 小时前
大学生就业招聘:Spring Boot系统的架构分析
java·spring boot·后端
编程、小哥哥8 小时前
netty之Netty与SpringBoot整合
java·spring boot·spring
IT学长编程9 小时前
计算机毕业设计 玩具租赁系统的设计与实现 Java实战项目 附源码+文档+视频讲解
java·spring boot·毕业设计·课程设计·毕业论文·计算机毕业设计选题·玩具租赁系统