1.换钱的最少货币数:
给定数组arr,arr中所有的值都为正数且不重复,每个值都代表一种面值的货币,每种面值的货币可以任意使用,再给定一个整数aim代表要找的钱数,求组成aim的最少货币数。当无法组成时返回-1
Simple :
arr = [2, 3, 5], aim = 20; 四张5 块的可以构成20元,这个是使用的最少货币的方法了
这道题是一道典型的动态规划的题目,怎样子看出这道题是动态规划了呢?我们可以想一下,当我们已知通过arr[0...i]组成j元的最少方法数之后,我们是否可以通过这些子问题来求解组成 k 元的最少方法数呢? 也就是说最优问题是可以转换为子问题进行求解的,这也就是动态规划中的最优子结构性质和状态转移。
状态:即用来描述该问题的子问题的解
怎么找出状态呢?(这里我觉得还是要靠不断的练习来判断,刚开始学习动态规划时还是很难的)
如果我们有面值为1元、3元和5元的钱币若干张,如何用最少的钱币凑够11元? (表面上这道题可以用贪心算法,但贪心算法无法保证可以求出解,比如1元换成2元的时候,这也是动态规划的优点之一)
首先我们思考一个问题,如何用最少的钱币凑够i元 (i < 11)?为什么要这么问呢? 两个原因:1.当我们遇到一个大问题时,总是习惯把问题的规模变小,这样便于分析讨论。 2. 这个规模变小后的问题和原来的问题是同质的,除了规模变小,其它的都是一样的, 本质上它还是同一个问题(规模变小后的问题其实是原问题的子问题)。
好了,让我们从最小的i开始吧。当i = 0,即我们需要多少个钱币来凑够0元。 由于1,3,5都大于0,即没有比0小的币值,因此凑够0元我们最少需要0个钱币。 这时候我们发现用一个标记来表示这句“凑够0元我们最少需要0个硬币”会比较方便, 如果一直用纯文字来表述,不出一会儿你就会觉得很绕了。那么, 我们用d(i) = j来表示凑够i元最少需要j个钱币。于是我们已经得到了d(0)=0, 表示凑够0元需要0个钱币。当i=1时,只有面值为1元的钱币可用, 因此我们拿起一个面值为1的钱币,接下来只需要凑够0元即可,而这个是已经知道答案的, 即d(0) = 0。所以,d(1) = d(1-1) + 1 = d(0) + 1 = 0 + 1 = 1。当i = 2时, 仍然只有面值为1的钱币可用,于是我拿起一个面值为1的钱币, 接下来我只需要再凑够2 - 1 = 1元即可(记得要用最小的硬币数量),而这个答案也已经知道了。 所以d(2) = d( 2 - 1) + 1 = d(1) + 1 = 1 + 1 = 2。一直到这里,我们一直都只能操作面值为1的钱币!让我们看看i = 3时的情况。当i = 3时,我们能用的钱币就有两种了:1元的和3元的( 5元的仍然没用)。 既然能用的钱币有两种,我就有两种方案。如果我拿了一个1元的钱币,我的目标就变为了: 凑够3 - 1=2元需要的最钱币数量。即d(3) = d(3 - 1) + 1 = d(2) + 1 = 2 + 1 = 3。 这个方案说的是,我拿3个1元的钱币;第二种方案是我拿起一个3元的钱币, 我的目标就变成:凑够3 - 3 = 0元需要的最少钱币数量。即d(3) = d(3 - 3) + 1 = d(0) + 1 = 0 + 1 = 1. 这个方案说的是,我拿1个3元的硬币。好了,这两种方案哪种更优呢? 记得我们可是要用最少的硬币数量来凑够3元的。所以, 选择d(3)=1,怎么来的呢?具体是这样得到的:d(3)=min{d(3 - 1) + 1, d(3 - 3) + 1}。 (这段文字引自动态规划:从新手到专家)
看了上面的推理之后我们大致上了解的状态的定义(还是上面那句话, 在新的问题上定义状态得靠自己的经验)接下来我直接给出dp数组的定义(我这里使用的二维数组的方式)。
状态 : dp[i][j] 表示可以使用arr[0...i]货币的情况下,组成j元所需的最少货币数
动态规划很重要的另外一个点就是那么我们如何通过最优子问题来逐步的求解,也就是说状态转移方程
那么如果我们通过前面的状态求dp[i][j] 的值,按照上面的推理,我们有几种不同的方案,我们可能完全用不到arr[i],这时我们得到的是dp[i - 1][j],当然我们还可能使用arr[i], 可能使用一张,两张... 即dp[i][ j - arr[i] ] + 1,
dp[i][ j - 2 * arr[i] ] + 2 ...但是使用一张,两张...这些我们可以总结在一起dp[i] [ j - arr[i] ] + 1(想一想,我们得到该值就是通过前面的使用两张等得出来的)
所以状态转移方程得出来了
状态转移方程: dp[i] [j] = min{ dp [i - 1] [j] , dp[i] [ j - arr[i] ] + 1 } (if ( j >= arr[i])
dp[i][j] = dp[i - 1] [j] (if j < arr[i] )
(j < arr[i] 表示arr[i]太大了,一张都多了,就像我们凑成两元的时候无法使用三元的钱币)
接下来是代码(Java版):
public static int coinChange(int[] coins, int amount) {
if(coins == null || coins.length <= 0 || amount < 0){
return -1;
}
int n = coins.length;
int[][] dp = new int[n][amount + 1];
//dp[0][j]只能是那些j为coin[0]整数倍的有值,其他的置为max_value
for (int j = 1; j < dp[0].length; j++) {
dp[0][j] = Integer.MAX_VALUE;
if(j % coins[0] == 0){
dp[0][j] = j / coins[0];
}
}
int left = Integer.MAX_VALUE;
for (int i = 1; i < dp.length; i++) {
for (int j = 1; j < dp[0].length; j++) {
left = Integer.MAX_VALUE; //注意每次要重新置位
//dp[i][j - coins[i]] = Integer.MAX_VALUE表示无法组成该货币值
if( j >= coins[i] && dp[i][j - coins[i]] != Integer.MAX_VALUE){
left = dp[i][j - coins[i]] + 1;
}
dp[i][j] = Math.min(left, dp[i - 1][j]);
}
}
return dp[n - 1][amount] != Integer.MAX_VALUE ? dp[n - 1][amount] : -1;
}
2 : 换钱的方法数: 给定数组arr,同样是不重复,可以使用任意张,求换钱可以有多少种方法
Simple:
arr = [1, 5, 10, 25], aim = 15
一共有6种, 15张一元的;10张一元的,一张5元;5张一元,2张5元;5张一元,1张10元;
1张5元,2张10元;3张5元;
这里直接给出状态和状态转移方程:
状态:dp[i][j]表示使用arr[0...i]货币的时候构成j元的方法数
状态转移方程:dp[i][j] = dp[i - 1][j] + dp[i][ j - arr[i] ] (if j >= arr[i] )
dp[i][j] = dp[i - 1][j] (if j < arr[i] )
下面给出代码:
public static int coinChangeCount2(int[] arr, int aim){
if(arr == null || arr.length <= 0){
return 0;
}
int[][] dp = new int[arr.length][aim + 1];
for (int i = 0; i < dp.length; i++) {
dp[i][0] = 1;
}
for (int j = 0; j < dp[0].length; j++) {
if(j % arr[0] == 0){
dp[0][j] = 1;
}
}
int num = 0;
for (int i = 0; i < dp.length; i++) {
for (int j = 0; j < dp[0].length; j++) {
num = dp[i - 1][j];
num += j >= arr[i] ? dp[i][j - arr[i]] : 0;
dp[i][j] = num;
}
}
return dp[arr.length - 1][aim];
}