题目
硬币。给定数量不限的硬币,币值为25分、10分、5分和1分,编写代码计算n分有
几种表示法。(结果可能会很大,你需要将结果模上1000000007)
示例
输入: 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
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/coin-lcci/
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
题解
这道题属于动态规划类的题型,我们需要先了解什么是动态规划(这部分多参考其他博客,这里只是简单的总结一些)。
概念:动态规划(简称DP)是算法设计思想当中最难也是最有趣的部分了,动态规划适用于有重叠子问题和最优子结构性质的问题,是一种在数学、计算机科学和经济学中经常使用的,通过把原问题分解为相对简单的子问题的方式求解复杂问题的方法。使用动态规划方法解题有较高的时间效率,关键在于它减少了很多不必要的计算和重复计算的部分。
适用范围:在问题可分解为彼此独立且离散的子问题时,就可使用动态规划来解决不适用范围:对于问题之间存在依赖关系的情况无法用到动态规划,因为其建模很困难
一般步骤:解决动态规划类问题,分为三步:
- 1.定义dp的具体含义(最难的部分);
2.确定初始状态;
3.根据状态列状态转移方程(关键步骤);
确定该状态上可以执行的操作,然后是该状态和前一个状态或者前多个状态有什么关联,通常该状态下可执行的操作必定是关联到我们之前的几个状态。
举例子:爬楼梯问题-有n级台阶,一个人每次上一级或者两级,问有多少种走完n级台阶的方法?
分析:n阶台阶,只可能是从n-1或是n-2的台阶上走上来的,台阶n的阶段依赖的是n-1和n-2的子阶段,所以状态转移方程为dp[n] = dp[n-1] + dp[n-2],属于最简单的动态规划问题。
入门:初步入门建议看《算法图解》的动态规划篇,了解其主要的原理与思想。了解完这些以后可以进入实际的代码练习。可以尝试最经典的背包九讲问题,推荐在网上偶然发现的一本关于背包九讲的电子书,由于是github访问会比较慢,最好是下载下来。最后看完以后推荐一个刷背包问题的刷题网站。基本上搞定01背包、完全背包问题、多重背包问题后在知道如何优化此类问题的时间复杂度和空间复杂度基本就算是入门了动态规划,体会到了动态规划的奥妙之处。
解决方案
按照动态规划的传统做法,定义dp[i][j]的具体含义为:前i个硬币组合j分的方案个数。列表dp[][]二维数组的初始状态是只有硬币1分时第一行全是1分。没有思路时自己在列表画出简单的dp数组如下:
c[5]={0,1,5,10,25}
主要是 1、5、10、15、20、25、30、35分的硬币总数;
i=1时:1、1、01、01、01、01、01、01
i=2时:1、2、03、04、05、06、07、08
i=3时:1、2、04、06、09、12、16、20
i=4时:1、2、04、06、09、13、18、24
通过动态规划的现阶段依赖前面几个阶段的思想,将一个复杂的问题进行分解以及观察到的规律性知:
如算dp[4][35]时它可以分解为dp[4][35]=前3个硬币组合35分+前3个硬币组合10分加一个25硬币;
再如求dp[3][35]时可分解为=前2个硬币组合35分方案数+前2个硬币组合25分加一个10硬币方案数+前2个硬币组合15分加二个10硬币方案数+前2个硬币组合5分加三个10硬币方案数;
故状态转移方程可写成如代码中所示:
class Solution {
public:
int waysToChange(int n) {
//这是一个完全背包的问题
int dp[5][1000010]={0}; //dp[i][j]表示前i种硬币达到j分有几种分法
for(int i=0;i<=n;i++){
dp[1][i]=1;
}
for(int i=0;i<=4;i++){
dp[i][0]=1;
}
int c[5]={0,1,5,10,25};
for(int i=2;i<=4;i++){
for(int j=0;j<=n;j++){
dp[i][j]=dp[i-1][j];
if(c[i]<=j){
for(int x=1;x*c[i]<=j;x++){
dp[i][j]=(dp[i][j]+dp[i-1][j-x*c[i]])%1000000007;
}
}
}
}
return dp[4][n];
}
};
优化时间复杂度
对于前面的状态方程
for(int x=1;x*c[i]<=j;x++){
dp[i][j]=dp[i][j]+dp[i-1][j-x*c[i]];
}
可化简为
dp[i][j]=dp[i-1][j]+dp[i][j-c[i]];
其含义为dp[i][j]=前i-1个硬币组合j分方案数不要c[i]硬币+前i个硬币组合j-c[i]分加一个c[i]硬币方案数。由于该代码时状态转移,从前面的初始状态转移到最终状态。我们可以认为dp[i-1][j]与dp[i][j-c[i]]状态是已经完成了,因为现在在求dp[i][j状态。该状态可以分解成我不要c[i]硬币(c[i]硬币数为0)与要c[i]硬币(c[i]硬币数为大于等于1)的二种状态。这种状态一组合就变成了dp[i][j]。
其改良代码如下:
class Solution {
public:
int waysToChange(int n) {
//这是一个完全背包的问题
int dp[5][1000010]={0}; //dp[i][j]表示前i种硬币达到j分有几种分法
for(int i=0;i<=n;i++){
dp[1][i]=1;
}
for(int i=0;i<=4;i++){
dp[i][0]=1;
}
int c[5]={0,1,5,10,25};
for(int i=2;i<=4;i++){
for(int j=0;j<=n;j++){
dp[i][j]=dp[i-1][j];
if(c[i]<=j){
dp[i][j]=(dp[i][j]+dp[i][j-c[i]])%1000000007;
}
}
}
return dp[4][n];
}
};
优化空间复杂度
在上述代码中我们的二维数组占了较大的空间,在使用的过程中1行使用完再使用2行,故在在访问方面只访问一到二次不会在访问太多,我们是否可以在访问完了以后清空前面占用的内存呢?这里介绍一种更新一维数组的方法在实现在上述代码中要用二维数组实现的功能。
class Solution {
public:
int waysToChange(int n) {
//这是一个完全背包的问题
int dp[1000010]={0}; //dp[i][j]表示前i种硬币达到j分有几种分法
for(int i=0;i<=n;i++){
dp[i]=1;
}
int c[5]={0,1,5,10,25};
for(int i=2;i<=4;i++){
for(int j=c[i];j<=n;j++){
dp[j]=dp[j];
if(c[i]<=j){
dp[j]=(dp[j]+dp[j-c[i]])%1000000007;
}
}
}
return dp[n];
}
};
当计算dp[j]时,dp[j]及以后的数组其实都是上一行的,我们从第一列开始进行覆盖更新,完成整个算法。实现一维数组代替二维数组降低空间复杂度。