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

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

总结

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

相关推荐
Dream it possible!1 小时前
LeetCode 热题 100_在排序数组中查找元素的第一个和最后一个位置(65_34_中等_C++)(二分查找)(一次二分查找+挨个搜索;两次二分查找)
c++·算法·leetcode
夏末秋也凉1 小时前
力扣-回溯-46 全排列
数据结构·算法·leetcode
南宫生1 小时前
力扣每日一题【算法学习day.132】
java·学习·算法·leetcode
柠石榴1 小时前
【练习】【回溯No.1】力扣 77. 组合
c++·算法·leetcode·回溯
Leuanghing1 小时前
【Leetcode】11. 盛最多水的容器
python·算法·leetcode
qy发大财1 小时前
加油站(力扣134)
算法·leetcode·职场和发展
qy发大财1 小时前
柠檬水找零(力扣860)
算法·leetcode·职场和发展
计算机毕设定制辅导-无忧学长1 小时前
Maven 基础环境搭建与配置(一)
java·maven
风与沙的较量丶2 小时前
Java中的局部变量和成员变量在内存中的位置
java·开发语言
m0_748251722 小时前
SpringBoot3 升级介绍
java