题目
给定数量不限的硬币,币值为25分、10分、5分和1分,编写代码计算n分有几种表示法。(结果可能会很大,你需要将结果模上1000000007)
示例1:
输入: n = 5
输出:2
解释: 有两种方式可以凑成总金额:
5=5
5=1+1+1+1+1
示例2:
输入: n = 10
输出:4
解释: 有四种方式可以凑成总金额:
10=10
10=5+5
10=5+1+1+1+1+1
10=1+1+1+1+1+1+1+1+1+1
说明:注意,你可以假设 0 <= n (总金额) <= 1000000
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/coin-lcci
解题
动态规划
这个问题实际上是一个完全背包问题,典型的动态规划解法。
动态规划的直接思路就是当前 i 分的组合数dp[i] 从 i-1 分的组合数dp[i-1] 以某种方式递推而来。这样所求的n分就可以从0分一步步递推求来。
常规思路(错误):每次递推时判断选择4种硬币的情况
以这个题目的具体情况来说,因为coin有四种分别为1,5,10,25四种,我们把问题想象为总金额为n的组合都是对这四种硬币的不同选择(每个硬币都可以选择多次),这个动态规划可以设计为 i 分的不同组合有dp[i]次,当最后一步分别选择1,5,10,25时,表示可以从 dp[i-1],dp[i-5],dp[i-10],dp[i-25] 四种情况递推而来。dp[i] = dp[i-1]+dp[i-5]+dp[i-10]+dp[i-25]
然而这会导致乱序重复问题:上述常规思路实现是把最外层循环设置为动态规划递推的循环0到n,然后里层对硬币的四种情况循环判断,其实是每次递推都考虑四种硬币的情况,这样造成结果就是四种不同分值硬币的乱序重复问题。
比如,当每一次递归分别选择1,5,10,25时,如果是乱序的组合,前面dp[i-1]&1,dp[i-5]&5,dp[i-10]&10,dp[i-25]&25这四种情况简单相加可能会出现硬币组合的顺序不一样都是实质是一样的重复情况,比如dp[7]可以由dp[6]&1和dp[2]&5两种组合,但是dp[6]的组合可以是{1,5},加上1之后dp{7}为{1,5,1};dp[2]是{1,1},加上5后dp[7]是{1,1,5},这样dp[6]&1与dp[2]&5就重复了不能简单的相加。
这个问题如何解决?
正确思路:每次递推时只选择1种硬币的情况,将选择一种硬币的情况递推完进行选择下一个硬币的递推,总共递推c_n*n次 (c_n为硬币种类数,n为总金额) **
我们组合时依次选择四种硬币,使存储的硬币组合都是以递增/递减的顺序来排列**如{1,1,5,5,10,10,25},这样就避免了如上例{1,5,1}和{1,1,5}这种的顺序不同实质一样的重复情况。
如何实现?很简单,我们将常规思路的两层循环反过来,将对选择四种硬币的情况的循环放在最外层,递推的循环放在里层,就变成了每次先把0到n分的组合中选择1的情况递推完,再选择5、10、25,这样我们所有情况的组合最终都是以 {1,1,…,1,5,5,…,5,10,10,…,10,25,25,…,25} 这样的顺序展现,避免了乱序重复问题。
思路
dp设置:dp[i][j] 表示选择到第i个硬币的时候(表明当前有i种硬币组成),组成总金额为j的方式数。(i有四种)
例:dp[3][10]:表示选择到第3个硬币(1,5,10三种硬币组合而成的)组成总金额为10分的组合方式数。
状态转移方程:dp[i][j] = dp[i-1][j] + dp[i][j-coins[i]]
表示遍历到第i个硬币的时候有两种选择,组合数为两种选择组合数之和:一种是不选择这个硬币,组合数为dp[i-1][j];一种是选择这个硬币,组合数为dp[i][j-coins[i]],前提是当前总金额 j 要大于该硬币的值coins[i]。
边界条件: dp[0][j] = 0,0种硬币组合金额j,不可能有方案,因此是0
dp[i][0] = 1,