动态规划入门2:leetcode518. 零钱兑换 II

leetcode518. 零钱兑换 II

给定不同面额的硬币和一个总金额。写出函数来计算可以凑成总金额的硬币组合数。假设每一种面额的硬币有无限个。

示例 1:

输入: amount = 5, coins = [1, 2, 5]
输出: 4
解释: 有四种方式可以凑成总金额:
5=5
5=2+2+1
5=2+1+1+1
5=1+1+1+1+1

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/coin-change-2
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

我的理解

在做这道题的时候,我一开始是想按照coin change的思路,建一个一维的表来写的。比如定义一个状态函数f(x)为用【2,3,5】能凑到x元的方法,并且状态转移方程为f(x)=f(x-1)+f(x-2)+f(x-5);但是这样想是错的。原因我分析了一下,有几个方面:

1.

如果用这种方法的话,那么假设要凑5元,那么f(5)=f(2)+f(3)+f(0)=3;但是实际上只有2种方法,即2+3;和5。问题出在哪里呢?其实问题就在于2+3和3+2其实是同一种组合,也就是只有一种方法;但是在上面的过程中我其实算了2次,因此出错了。

由此可见,如果从最后一步出发,按照coin change的思路,那么每一次明确最后一枚硬币时,本身就是明确一个排列(即有顺序的),因此使用上面那种方法,更加适用于有顺序的情况。

2.

接下来,我看了一下解答,解答里面使用的是2维的dp[i][j],它的定义是,使用前面i种硬币,能够凑到j元钱有多少种方法。而我们要求的就是dp[total money type][amount]。那么这个dp[i][j]的求法,或者说分解子问题的方式有很多种,下面分2种来说明,并且说明其中的一些关键思考点。

方法1

如果要求dp[i][j],那么我们可以这么想,用i种硬币凑j元钱,那么就相当于用i-1种硬币凑j元,j-coin[i]元,j-2coin[i]元,j-3coin[i]元…j-k*coin[i]元。由于我们不确定这个组合的方案里面coin[i]有多少张,既然这样那我们就枚举好了,从一张coin[i]都没有的里面找,即d[i-1][?]里面来找。那么对应的coin[i]的钱的数量就0,1,2,…k张。

那么分解子问题的原则是不重复,不遗漏。有没有遗漏呢?应该是没有遗漏的,因为在i-1种硬币里面选对应的钱数,余下的全部是coin[i]占了。那么更难的是,有没有重复呢?即我们的同一行的方法会不会有重复,尤其是后面的方案把前面的包括了?答案是不会重复。原因是因为我们拼凑的钱的总数是不一样的,即使用了相同的钱,但是拼的总数不一样,那么用的钱的张数就不可能一样,那么相应的方案就不可能相同,即j不同,那么方案就必定不同,因此同一行的方案不可能被包含,也不可能重复!

方法2

另外一种方法是这么想的,用i种硬币凑j元钱,那么所有的方案中,一定可以分为两种,一种是这i种硬币的方案 一定含有coin[i]的,另外是不含有coin[i]的。不含有coin[i]的拼凑的方案很简单,就是d[i-1][j]。这个应该不需要解释,就是按照定义得到的。

那么我们的问题是 “一定含有coin[i]的方案” 的数量是多少呢?我们能不能从同一行(第i行)里面得到呢?或者说为什么我们不直接dp[i][j]=dp[i][j-coin[i]]呢?因为能够拼出dp[i][j-coin[i]]的方案,一定是可以拼出dp[i][j]的嘛。似乎看来dp[i][j]就是dp[i][j-coin[i]]啊?但是其实这个等式隐藏了一个细节,下面具体说明。dp[i][j-coin[i]]的具体含义是,使用i种硬币(以一定都使用,仅仅表示可以使用),凑出j-coin[i]元的方法。那么从dp[i][j-coin[i]]得到dp[i][j],真正的含义是,这种方案的转移代表了方案种必定“至少含有一枚”coin[i]硬币,因为只有这样才能从dp[i][j-coin[i]]跳到dp[i][j]。换句话说,我们限定了“最后一枚”硬币必须是coin[i]这种硬币,或者说这种方案必定至少有一枚coin[i]硬币。因此,从dp[i][j-coin[i]]得到的方案的数量是至少有一枚coin[i]硬币的方案,而dp[i-1][j]是必定不含有coin[i]硬币的方案,因此这两个加起来正好是无重复,无遗漏的,因此我们的状态方程是
dp[i][j]=dp[i-1][j]+dp[i][j-coin[i]];//if(j-coin[i]>=0)

因此,这个问题与1不同,这是一个组合数的计数问题。

方法3 压缩状态栏

状态栏压缩的本质其实和方法2是一样的,只不过这里使用了2层的循环。在循环过程种i从1->len,j从0->amount,相当于每次i的递增都需要重新刷新一次状态,越在后面的被刷新得越多。

#include <iostream>
#include <vector>

using namespace std;

class Solution {
public:
    int change(int amount, vector<int> &coins) {
        int len = coins.size();
        vector<int> dp(amount + 1, 0);
        dp[0] = 1;
        for (int i = 1; i <= len; ++i) {
            for (int j = 0; j <= amount; ++j) {
                if (j - coins[i - 1] >= 0) {
                    dp[j] += dp[j - coins[i - 1]];//不断刷新状态栏
                }
            }
        }
        return dp[amount];
    }
};

代码

#include<iostream>
#include<vector>
using namespace std;
int change(int amount, vector<int>& coins) {
	int n = coins.size();
	int** dp = new int*[coins.size() + 1];
	for (int i = 0; i<=coins.size(); i++) {
		dp[i] = new int[amount + 1];
	}
	dp[0][0] = 1;
	for (int i = 1; i <= amount; i++) {
		dp[0][i] = 0;//初始化
	}
	int temp;
	for (int i = 1; i <= n; i++) {
		for (int j = 0; j <= amount; j++) {
			dp[i][j] = dp[i - 1][j];
			if (j - coins[i - 1] >= 0)
				dp[i][j] += dp[i][j - coins[i - 1]];
		}
	}

	return dp[n][amount];
}

int main() {
	int amount = 5;
	vector<int> vec = { 1,2,5 };
	cout<<change(amount, vec);

	return 0;
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值