找零钱是一个经典的动态规划问题。这种问题,我建议,首先学会暴力解法,然后从暴力解法中优化出动态规划的解法,这样,更能体会动态规划的魅力。

问题描述

有n种不同币值的硬币,硬币数量无限。给定一个数量T,求用给定硬币凑出T的方法数量。

举个例子:

假设币值是: {1,2,3}

给定的T值: 5

输出所有的组合数量: 5

为啥是5呢,因为有5种不同的组合可以得到数值5,如下所示:

{1,1,1,1,1}

{1,1,1,2}

{1,2,2}

{1,1,3}

{2,3}

暴力解法

既然这是要找所有的组合数量,暴力搜索就是很自然的想法了。暴力搜索算法写起来有两个关键点:

记下已经搜索到的结果,并且越简单越好,因为空间占用小啊。

记下剩余可用的搜索空间

简单说,就是我已经有了什么,我还可以有什么。

这两点,会因为具体问题不同而有不同的表现形式。

对这道凑币值的问题。我们的暴力搜索问题,可以看作是往一个布袋中放硬币,直到布袋中硬币的币值之和等于要求的数量。

看起来,布袋中的硬币就是我们已经搜索到的结果。剩余可选的硬币和还需要凑的币值就是我们可用的搜索空间。

这个虽然没有问题。但是我们需要一个数组来存储布袋中的硬币,这个空间开销还是不小。而且,这个问题并不要求我们给出具体的组合方案,而是只要所有可能的组合数量。鉴于此,我们可以做一个优化:将已经搜索到的结果用还需要凑的币值来表示显然,这个值可以完美代表我们已有的搜索成果。

将所有可选的硬币币值放入一个数组,那么剩余可选的硬币就可以用一个index来代替。

最后,暴力搜索的终点,无非两种,一种就是找到了一种拼凑方案,一种就是没有。

把这些问题想清楚。写代码时就轻松了。

代码实现

class CoinChange {

'''

@denominations:代表所有可用的币值

@total:代表要拼凑的数值

'''

public int countChange(int[] denominations, int total) {

return this.countChangeRecursive(denominations, total, 0);

}

'''

@total:代表剩余还要拼凑的币值

@currentIndex:代表剩余可用的硬币币值

'''

private int countChangeRecursive(int[] denominations, int total, int currentIndex) {

# 找到一个拼凑方案,返回1,用于累加

if (total == 0)

return 1;

# 到达搜索终点,不是可用的拼凑方案,返回0

if(denominations.length == 0 || currentIndex >= denominations.length)

return 0;

# 递归进行暴力搜索,如果currentIndex处的币值小于total

# 则有两个选择:

# 将该币值放入布袋,即从total中减去该币值,然后继续搜索,

# 此时剩余可用币值不减少

# 不使用该币值,继续搜索,此时剩余可用币值要减去currentInde # x处的币值

int sum1 = 0;

if( denominations[currentIndex]

sum1 = countChangeRecursive(denominations, total - denominations[currentIndex], currentIndex);

// recursive call after excluding the coin at the currentIndex

int sum2 = countChangeRecursive(denominations, total, currentIndex + 1);

return sum1 + sum2;

}

public static void main(String[] args) {

CoinChange cc = new CoinChange();

int[] denominations = {1, 2, 3};

System.out.println(cc.countChange(denominations, 5));

}

}

从上到下记忆

理解了暴力搜索算法,将里面的重复计算消除,就是动态规划了。观察上面的代码,可以发现,递归调用的两个关键参数 total和currentIndex一旦确定,则该递归调用的结果就是确定的。这两个参数在暴力搜索中显然被会有重复的使用。这就是我们要消除的重复计算。

方法很简单,做个缓存就可以了。因为两个参数,用二维数组自然很合适。

class CoinChange {

public int countChange(int[] denominations, int total)

{

Integer[][] dp = new Integer[denominations.length][total + 1];

return this.countChangeRecursive(dp, denominations, total, 0);

}

private int countChangeRecursive(Integer[][] dp, int[] denominations, int total, int currentIndex)

{

if (total == 0)

return 1;

if(denominations.length == 0 || currentIndex >= denominations.length)

return 0;

# 缓存中有结果,直接返回即可

if(dp[currentIndex][total] != null)

return dp[currentIndex][total];

# 缓存中没有,继续进行递归计算

int sum1 = 0;

if( denominations[currentIndex]

sum1 = countChangeRecursive(dp, denominations, total - denominations[currentIndex], currentIndex);

int sum2 = countChangeRecursive(dp, denominations, total, currentIndex + 1);

dp[currentIndex][total] = sum1 + sum2;

return dp[currentIndex][total];

}

public static void main(String[] args) {

CoinChange cc = new CoinChange();

int[] denominations = {1, 2, 3};

System.out.println(cc.countChange(denominations, 5));

}

}

自下而上动态规划

有了以上两种方法的铺垫,再来看真正的动态规划。我们就可以理解动态规划是要更彻底的解决问题。在我们从上到下加记忆的方法中,我们使用了缓存,来存储中间结果。动态规划的思路,其实就是将缓存作为关键,将问题的求解转化为填充缓存的过程。

比如,我们现在要填充缓存dp[currentIndex][t]。这个缓存代表的问题,有两类可能的组合方案:

没有currentIndex币值的方案,这样的方案的数量就是dp[currentIndex-1][t]

至少包含一个currentIndex币值的方案,这样的方案的数量就是dp[currentIndex][t-denominations[currentIndex]]

将这两种方案的数量加起来就是dp[currentIndex][t]的值了。

有了上面的分析,代码就是手到擒来。

def count_change(denominations, total):

n = len(denominations)

dp = [[0 for _ in range(total+1)] for _ in range(n)]

# 填充total为0时数值,始终有1个方案

for i in range(n):

dp[i][0] = 1

# 双重循环填充缓存

for i in range(n):

# total=0的情况已经专门填充,这里从1开始

for t in range(1, total+1):

if i > 0:

dp[i][t] = dp[i - 1][t]

if t >= denominations[i]:

dp[i][t] += dp[i][t - denominations[i]]

# 右下角就是最终结果

return dp[n - 1][total]

def main():

print(count_change([1, 2, 3], 5))

main()

总结

动态规划是面试中的常考技能点。我推荐的学习路径就是这种,先学会暴力算法,然后利用缓存,去掉重复计算,然后学习以缓存填充为核心的动态规划。

java 动态规划找零钱_动态规划之找零钱问题相关推荐

  1. 动态规划走楼梯_动态规划问题为什么要画表格?

    ❝ 本文是我的 91 算法第一期的部分讲义内容.91 算法第一期已经接近尾声,二期的具体时间关注我的公众号即可,一旦开放,会第一时间在公众号<力扣加加>通知大家. ❞ 动态规划可以理解为是 ...

  2. python找零_用python实现零钱找零的三种方法

    1.递归(recursion)def coins_changeREC(coin_values, change): """ 递归实现零钱找零 ""&qu ...

  3. java动态规划货车运输_动态规划01背包问题_动态规划方法在配送线路优化中的应用研究...

    [摘要] 应用图论的方法对配送线路进行优化的缺点是,当线路复杂时计算较为烦琐,而应用动态规划的方法能有效解决这个问题.本文从理论上应用动态规划的方法对共同配送线路进行优化,并应用此方法对实际问题进行计 ...

  4. java动态规划鸡蛋问题_动态规划——楼层扔鸡蛋问题

    前言 大一的时候蓝桥杯省赛遇到过(作为非编程题的压轴题),这次看的别人的面经也多次出现,就写篇博文总结一下. 题目 有一栋楼共100层,一个鸡蛋从第N层及以上的楼层落下来会摔破, 在第N层以下的楼层落 ...

  5. java动态规划鸡蛋问题_动态规划系列/高楼扔鸡蛋问题.md · lipengfei/fucking-algorithm - Gitee.com...

    # 经典动态规划问题:高楼扔鸡蛋 今天要聊一个很经典的算法问题,若干层楼,若干个鸡蛋,让你算出最少的尝试次数,找到鸡蛋恰好摔不碎的那层楼.国内大厂以及谷歌脸书面试都经常考察这道题,只不过他们觉得扔鸡蛋 ...

  6. java 爬楼梯算法_动态规划-爬楼梯问题java实现

    最近开始看算法导论,研究了一下动态规划,下面就开始直入主题开始记录近期看的第一个知识点动态规划.提起动态规划就不得不提几个动态规划的金典问题爬楼梯.国王金矿.背包问题.今天就仔细分析一下爬楼梯问题. ...

  7. java经典问题国王_动态规划-国王的金矿问题java

    紧接着上一篇动态规划问题,现在我们开始探讨一个新的问题,问:有一个发现了5个金矿,每一个金矿的储量不同,需要参与挖掘的工人数也不通,参与挖矿工人的总数量是10人,每一座金矿要么全挖,要么不挖,不能派一 ...

  8. java实现背包问题例子_动态规划(背包问题)java实现

    背包问题(Knapsack problem)是一种组合优化的NP完全问题.问题可以描述为:给定一组物品,每种物品都有自己的重量和价格,在限定的总重量内,我们如何选择,才能使得物品的总价格最高.问题的名 ...

  9. java找茬_一起来找茬(1)-开发写的神奇左连接

    近期在给客户做新数据交换方案调试时发现一处视图创建语句带不出数据. 精简需求后如下:a部门从b部门获取主体数据,由于a.b两部门有些代码标准不一致需要做转换.于是开发写了个对照表做转换生成业务视图. ...

最新文章

  1. arcgis9.1下载地址
  2. visual Studio 2010 自带报表RDLC动态生成
  3. 【转】PHP面试题总结
  4. 无忧计算机二级试题题库,全国计算机二级MS Office试题
  5. 听听一个院士的故事,你也能找到自己的路。
  6. 路德维希贝多芬计算机怎么操作,贝多芬是不是听不见和看不见
  7. idea+SpringBoot+Mybatis+Mysql环境搭建
  8. 概率论第六章数理统计思维导图_【思维导图】第六章:气体灭火系统
  9. cloud源码-eureka
  10. 【译】Silverlight for Windows Phone Toolkit In Depth(五)
  11. Can not find the tag library descriptor for http://java.sun.com/jsp/jst1/core
  12. linux 下spi的使用 ,cc2500模块驱动
  13. 【大数据时代】前端数据可视化利器D3.js、highcharts、echarts(毕设调研)
  14. 设置python程序开机自启动
  15. Tk/Tkx滚动条的使用
  16. 2022.10.9-10.16 AI行业周刊(第119期):相信坚持的力量
  17. IOS 清理CALayer、CAShapeLayer的sublayers
  18. Win7 设置防火墙开放特定端口
  19. switch的简单举例
  20. IndentationError: unindent does not match any outer indentation level问题

热门文章

  1. 计算机应用软件开机自动启动设置,电脑开机软件自动启动怎么关闭 win7/win10快速关闭开机自启软件...
  2. windows无法连接到打印机_“Windows无法连接打印机,操作失败,错误为0x000003e3”...
  3. LXMERT:从Transformers学习跨模态编码器表示LXMERT: Learning Cross-Modality Encoder Representations from Transfors
  4. 阿里云腾讯云华为云端图片处理及优化
  5. 软件工程-大学体育馆管理系统交互图
  6. 捕鱼达人的算法猜测—较色碰撞算法
  7. [附源码]Java计算机毕业设计SSM动物保护资讯推荐网站
  8. SSH跨平台终端工具tabby推荐
  9. 使用Lucene开发简单的站内新闻搜索引擎(索引的搜索)
  10. Lontium 的 LT8619C 是一款基于 ClearEdge 技术的高性能 HDMI/双模 DP 接收器芯片