一、题目
硬币。给定数量不限的硬币,币值为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
二、C# 题解
2.1 数学解法
使用数学方法解答,将 n 看作是 25、10、5 和 1 的组合,即 n = 25a + 10b + 5c + d
。因为给定 n 后,d 由 a、b、c 决定,因此仅讨论 a、b、c 之间的关系即可。
将 a、b、c 降序排列,其关系如下所示:
-
给定 a 不动,讨论 b 和 c 的组合数。
- b 不动,c 依次递减(每次 c 减 1,少的面值加到了 d 上,即 d += 5),共 c + 1 种。
- b 减 1,则 c += 2,共 c + 3 种。
- b 减 2,则 c += 4,共 c + 5 种。
- …
- 直至 b 减为 0,此时 c + b * 2,共 c + b * 2 + 1种。
共计 (b + 1)(b + c +1) 种。
-
a 减 1,则多出 25 分,总面值为 25 + 10b + 5c。由 b、c 重新降序排列,即10b’ + 5c’ = 25 + 10b + 5c,解得 b’ = b + (5 + c) / 2,c’ = (c + 5) % 2。
public class Solution {
public int WaysToChange(int n) {
int a = n / 25, b = n % 25 / 10, c = n % 25 % 10 / 5;
return (int)Partition(a, b, c);
}
public long Partition(int a, int b, int c) {
if (a < 0) return 0;
long n = (b + 1L) * (b + c + 1L) % 1000000007; // 使用 1L 进行 long 运算防止结果溢出
return (Partition(a - 1, b + (c + 5) / 2, (c + 5) % 2) + n) % 1000000007;
}
}
- 时间:24 ms,击败 100.00% 使用 C# 的用户
- 内存:27.60 MB,击败 57.14% 使用 C# 的用户
2.2 动态规划
使用动态规划解答,记 f(i,j)
表示用前 i
种硬币构成面值 j
的种类数,因此有
f ( i , j ) = f ( i − 1 , j ) + f ( i − 1 , j − c i ) + f ( i − 1 , j − 2 ⋅ c i ) + ⋯ + f ( i − 1 , j − k ⋅ c i ) f(i,j)=f(i-1,j)+f(i-1,j-c_i)+f(i-1,j-2\cdot c_i)+\cdots+f(i-1,j-k\cdot c_i) f(i,j)=f(i−1,j)+f(i−1,j−ci)+f(i−1,j−2⋅ci)+⋯+f(i−1,j−k⋅ci)
其中,c_i
表示第 i 种硬币的面值,k = j / c_i
。
进行优化:
- 时间复杂度:通过上式可以看出, f ( i , j ) = f ( i − 1 , j ) + f ( i , j − c i ) f(i,j)=f(i-1,j)+f(i,j-c_i) f(i,j)=f(i−1,j)+f(i,j−ci),避免了多一层的遍历
- 空间复杂度:由于上式为递推式,仅和
i
、i - 1
有关,类似斐波那契数列的求解,只需要一个一维数组存储数据即可,数组内容不断更新 1 、 2 、 ⋯ 、 i 1、2、\cdots、i 1、2、⋯、i 对应的值。
事实上,动态规划的解法思想和数学解法很类似,即 c 1 , c 2 , c 3 , c 4 c_1,c_2,c_3,c_4 c1,c2,c3,c4 对应于数学解法的 d , c , b , a d,c,b,a d,c,b,a。
public class Solution {
public int WaysToChange(int n) {
int[] coins = { 1, 5, 10, 25 };
int[] f = new int[n + 1];
f[0] = 1;
for (int i = 0; i < 4; i++)
for (int j = coins[i]; j < n + 1; j++) {
f[j] += f[j - coins[i]];
f[j] %= 1000000007;
}
return f[n];
}
}
说明:f[j] += f[j - coins[i]]
为化简写法,即 f[j] = f[j] + f[j - coins[i]]
,对应于
f
(
i
,
j
)
=
f
(
i
−
1
,
j
)
+
f
(
i
,
j
−
c
i
)
f(i,j)=f(i-1,j)+f(i,j-c_i)
f(i,j)=f(i−1,j)+f(i,j−ci)。
- 时间:44 ms,击败 42.86% 使用 C# 的用户
- 内存:29.33 MB,击败 28.57% 使用 C# 的用户
动态规划的方法没有数学解法快,这里的解释是:
- 动态规划为通解。即,coins 可以换成任意其他数组,更具有普遍性。
- 数学方法为特例。即,利用了 1、5、10、25 之间的数学关系(b 减 1,c += 2),相当于对特例进行特殊优化。