【算法 - 动态规划】找零钱问题Ⅲ

在前面的动态规划系列文章中,关于如何对递归进行分析的四种基本模型都介绍完了,再来回顾一下:

  1. 从左到右模型arr[index ...]index 之前的不用考虑,只考虑后面的该如何选择
  2. 范围尝试模型 :思考 [L ,R] 两端,即 开头和结尾 处分别该如何取舍。
  3. 样本对应模型 :以 结尾位置 为出发点,思考两个样本的结尾都会产生哪些可能性 。
  4. 业务限制模型 :不能够明确的知道一个参数的变化范围,通过业务的限制找到 最差情况 进行估计。

前两篇文章我们讲解了两道相似的找零钱问题。今天我们继续“找零钱”。

找零钱问题 Ⅲ

给定一个 面值 数组 arr ,其中的值均为无重复的正数,每一个值代表一种面值,张数无限。求能够组成 aim 最少的货币数量。

示例 1:

输入: arr = {1, 2} ,aim = 4 。

输出: 2

解释: 共四种组合方式,其中最少需要两张就能组成 4。

  • 1 + 1 + 1 + 1 = 4
  • 1 + 1 + 2 = 4
  • 2 + 2 = 4

示例 2:

输入: arr = {1, 2, 5} ,aim = 6 。

输出: 2

解释: 共五种组合方式,其中最少需要两张就能组成 6。

  • 1 + 1 + 1 + 1 + 1 + 1 = 6
  • 1 + 1 + 1 + 1 + 2 = 6
  • 1 + 1 + 2 + 2 = 6
  • 2 + 2 + 2 = 6
  • 1 + 5 = 6

注意: 要区分好与 前两篇零钱问题 的区别哦!

首先我们依然采用最朴素的 暴力递归 来思考这道题目。

思路

这三道题目都是典型的 从左到右模型 ,因此,递归就可以按照 在 arr[index ...] 数组中,index 之前的不用考虑,只考虑后面的该如何选择 的思路来划分情况:

  • 当前 index 下标对应的面值 参与 组合,选择不同的张数 ,之后能有多少种情况。

因为要求张数最小的情况,因此要返回所有情况的 最小值

代码

public static int minCoins(int[] arr, int aim) {
    return process(arr, 0, aim);
}

public static int process(int[] arr, int index, int rest) {
    if (index == arr.length) {
        return rest == 0 ? 0 : Integer.MAX_VALUE;
    } else {
        int ans = Integer.MAX_VALUE;
        for (int zhang = 0; zhang * arr[index] <= rest; zhang++) {
            int next = process(arr, index + 1, rest - zhang * arr[index]);
            if (next != Integer.MAX_VALUE) {
                ans = Math.min(ans, zhang + next);
            }
        }
        return ans;
    }
}

代码解释

递归中,base case 为下标来到最后时后边没有货币了,如果此时剩余的钱数也为 0 ,说明不需要任何一张钱了,即返回 0。

选择多少张数 体现在 zhang 从 0 开始,直到该张数的面值超过了剩余钱数 rest 为止。

继续调用递归且下标 index + 1 ,剩余钱数也相应减少。如果之后的返回值不为 系统最大值 ,说明之后的情况中有可以选择的方式,那就和 ans 一起取两者中更小的即为答案。


写出该暴力版的递归之后修改出动态规划版的就很容易了。

动态规划版

public static int dp(int[] arr, int aim) {
    if (aim == 0) {
        return 0;
    }
    int N = arr.length;
    int[][] dp = new int[N + 1][aim + 1];
    dp[N][0] = 0;
    for (int j = 1; j <= aim; j++) {
        dp[N][j] = Integer.MAX_VALUE;
    }
    for (int index = N - 1; index >= 0; index--) {
        for (int rest = 0; rest <= aim; rest++) {
            int ans = Integer.MAX_VALUE;
            for (int zhang = 0; zhang * arr[index] <= rest; zhang++) {
                int next = dp[index + 1][rest - zhang * arr[index]];
                if (next != Integer.MAX_VALUE) {
                    ans = Math.min(ans, zhang + next);
                }
            }
            dp[index][rest] = ans;
        }
    }
    return dp[0][aim];
}

代码解释

可变的参数有两个:总的面值个数 N 和 剩余的目标钱数 rest 。因此,需要设置一个二维的 dp 表数组,由于 N, rest 的取值范围是 0~N 、0~aim ,因此数组大小设置为 dp[N + 1][aim + 1]

递归代码 index == arr.length 可知,初始只有 dp[N][0] 的值为 0 ,其余的值均设置为系统最大值。

因为递归中只依赖 index + 1 的值,所以 dp 表倒着填写。

写法和递归中的一样,只需将递归调用换成从 dp 数组中取值就行。

根据递归调用 process(arr, 0, aim) 可知最终返回 dp[0][aim]


观察递归的代码,发现竟然有 3 层 for 循环。为什么呢?

思考后发现, dp 表中的每个位置同样需要 枚举 后才能知道(从 0 张开始一直枚举到超过剩余值 rest)。那有没有办法消掉这层枚举的 for 循环呢?答案是有的!

下面我们通过画 dp 表,探寻该动态规划应如何进一步优化。

假设此时剩余的总钱数 rest = 10,面值数 arr[i] = 3 。

一图胜千言~

通过枚举代码可知,arr[i][10] 的值,红色 = min(黄色+0, 紫色+1, 紫色+2, 紫色+3,)

黄色:不选面值为 3 的钱币时,rest 仍为 10,依赖下一格 i + 1。

紫色:分别选 1 张、2 张、3张…时,rest 对应每次减 3 ,且依赖下一格 i + 1 行。那么所用的总张数就需要分别加上1,2,3再来比较最小值。

稍加思考发现,蓝色的位置即 arr[i][10 - 3] 位置的值正是 3 个紫色加完0,1,2之后的最小值。

那么,就可以改为 红色 = (黄色, 蓝色+1),这样就不需要一直往前寻找了,减少一个 for 循环

情况 2:

如果蓝色 位置本身不存在(越界了,小于 0 了)或者蓝色的 值不存在(没有有效方案,值为系统最大值)。

这里只需稍加判断一下即可!

最终优化版动态规划

public static int dp(int[] arr, int aim) {
    if (aim == 0) {
        return 0;
    }
    int N = arr.length;
    int[][] dp = new int[N + 1][aim + 1];
    dp[N][0] = 0;
    // 除了 dp[N][0] 外都设置为 系统最大值
    for (int j = 1; j <= aim; j++) {
        dp[N][j] = Integer.MAX_VALUE;
    }
    for (int index = N - 1; index >= 0; index--) {
        for (int rest = 0; rest <= aim; rest++) {
            // 先把 红色 设置为 黄色
            dp[index][rest] = dp[index + 1][rest];
            // 如果有蓝色位置 且蓝色位置有效(不是系统最大)
            // 那就再比较黄色 和 蓝色 + 1 的大小 ,取小者
            if (rest - arr[index] >= 0 && dp[index][rest - arr[index]] != Integer.MAX_VALUE) {
                dp[index][rest] = Math.min(dp[index][rest], dp[index][rest - arr[index]] + 1);
            }
        }
    }
    return dp[0][aim];
}

注意看越界的判断哦,黄色、蓝色分开计算。这样就完成了最终版的动态规划~

通过本文的学习相信小伙伴对为什么有了记忆化搜索还要写出 严格的表依赖 有了更加深刻的理解!!

为了避免枚举行为多产生的 for 循环,有了 表依赖 才能找到如何 优化枚举 !这种方法也叫做 斜率优化
因此,前面学习的如何一步步的将暴力递归修改为严格表依赖动态规划的基础要打牢哦!还不会的赶快关注一下回顾前面的几篇文章吧!

~ 点赞 ~ 关注 ~ 评论 ~ 不迷路 ~

------------- 往期回顾 -------------
【算法 - 动态规划】找零钱问题Ⅰ
【算法 - 动态规划】找零钱问题Ⅱ
【算法 - 动态规划】原来写出动态规划如此简单!
【算法 - 动态规划】最长公共子序列问题
【算法 - 动态规划】最长回文子序列
【算法 - 动态规划】力扣 691. 贴纸拼词
【算法 - 动态规划】京东面试题 - 洗咖啡杯问题
【堆 - 专题】“加强堆” 解决 TopK 问题!
AC 此题,链表无敌!!!

  • 28
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值