动态规划
题目
零钱兑换:
给定金额amount和硬币面额值coins[],求组成金额amount所用的最少硬币数量;
条件
- 每种面额硬币值的数量不限;
思路
首先确定好状态、选择和状态转换方程式
1、判断是否动态规划问题
最优子结构
全局最优解包含子问题最优解
比如:求组成金额100所用的最少硬币数?
主问题:
组成金额100所用的最少硬币数;
子问题:
组成金额99所用的最少硬币数
组成金额98所用的最少硬币数
……
主问题最优解答案是最少硬币数,子问题最优解答案也是最少硬币数,求主问题的答案可通过子问题答案进行推导得出
存在重叠子问题
同一个子问题被多次计算
比如在计算金额100的时候,需要计算金额99、98,但是计算金额99的时候也需要计算金额98,计算金额98子问题被多次计算;
2、定义状态
明确动态规划状态函数dp
关键在于定义dp函数表示什么:
dp(i) = 组成金额i所需的最少硬币数
3、推导状态转移方程
根据状态函数的定义dp推导出:
dp(i) = min(dp(i), dp(i-coin) + 1)
代码
实现代码如下:
暴力搜索
1 |
|
重叠子
存在的问题:
假设amount = 11,coins = [1,3,5,7],如下图

通过树结构进行构建会发现,其中很多都是有重复计算,比如f(7)、f(5)、f(3)……
该问题为重叠子
时间复杂度
假设amount为a,coins为S,那么其时间复杂度为:
S^0+S^1+S^2+S^3+……= O(S^a)
空间复杂度
空间复杂度取决于树的深度,假设每次都是一个硬币,那么空间复杂度就是O(n)
记忆化搜索
因为暴力搜索方式存在
重叠子问题,整体性能很差,可以通过将f(amount)进行缓存的方式消除重叠子进行优化,代码如下:
1 | int[] dbTable; |
时间复杂度
经过加缓存优化之后,由原来的O(S^a) 降到O(S*a)
假设每次都是一元硬币,所以recursion方法需要执行S次,加上每次都需要执行遍历每种面额值,所以就是S*a
空间复杂度
O(N)
动态规划
上面求解问题是通过自顶向下思路来考虑子问题的,也就是:
想要知道最少需要多少硬币数凑成
amount金额,需要知道最少多少硬币数凑成amount - 1;想要知道最少需要多少硬币数凑成
amount - 1金额,需要知道最少多少硬币数凑成amount - 2;想要知道最少需要多少硬币数凑成
amount - 2金额,需要知道最少多少硬币数凑成amount - 3;以此类推……直到
amount = 0,然后进行回朔得出amount对应所需最少的硬币数;
而如果采用动态规划自底向上的思路,则是:
凑成
amount = 0的金额,最少需要0个硬币数;凑成
amount = 1的金额,最少需要1个硬币数;凑成
amount = 2的金额,最少需要dp(2)个硬币数;以此类推……直到算出
amount金额需要的硬币数;
使用动态规划的代码:
1 | private int coinChange(int[] coins, int amount) { |
自顶向下与自底向上的区别
自顶向上
缺点:
- 可能会内存溢出(递归,栈内存溢出)
自底向上
缺点:
- 当子问题稀疏时,会导致很多无用计算;
比如amount = 11,coins = [3,7,100],则:
1 | dp[0] = [0] |
一共需要计算12个子问题,其中有效计算只有5个,
而如果是自顶向上呢?
1 | dp[11] |
可知一共会计算dp[11]、dp[8]、dp[5]、dp[4]这四个状态;
很明显,当子问题密集时,使用自底向上效果更好


















































