一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第23天,点击查看活动详情。
一、题目
现有n1+n2种面值的硬币,其中前n1种为普通币,可以取任意枚,后n2种为纪念币, 每种最多只能取一枚,每种硬币有一个面值,问能用多少种方法拼出m的面值?
二、分析
普通币无重复值,纪念币可能有有重复值,方案很多
- 普通币就能拼成
- 纪念币就能拼成
- 普通币 + 纪念币
无重复数组,只考虑普通币(任意张)的情况:
0~N-1做行,拼出目标aim(m),0~aim
做列的一张二维dp表,dp[i][j]
含义:自由使用arr[0...i]的货币的情况下,搞定j元的方法数是多少
dp[i][0]
第一列的格子怎么填? 自由使用普通货币数组搞定0元,有1种方法,这种方法是一种货币都不用,也可以这么理解,在暴力递归中剩下0元需要搞定,已经搞定完了,返回1种方法,之前存在1种有效方法搞定了目标
dp[0][j]
第一行的格子怎么填? 当 j % arr[0] == 0
时,dp[0][j] = 1
,代表目标j是i的整数倍,需要一张货币、两张货币、三张货币、...等等,代表有1种方法搞定,可以重复使用同样的货币
dp其余格子怎么填?
- 情况一:
dp[i][j] = dp[i-1][j]
,i元货币不用(0张),arr[0...i-1]搞定j元 - 情况二:
dp[i][j] = dp[i-1][j - zhang * arr[i]]
,zhang:i元货币使用张数
进一步优化,省掉枚举行为,观察格子依赖关系,星号格子 = 三角格子 + a
得出转移方程:dp[i][j] = dp[i-1][j] + dp[i][j - arr[i]]
考虑纪念币问题:背包问题,当前货币用还是不用问题,用只能用一次
- 情况一:当前货币不用,
dp[i][j] = dp[i-1][j]
- 情况二:当前货币用,
dp[i][j] = dp[i-1][j - arr[i]]
有了这两张表,假如目标aim = 10,有如下可能:
- 普通硬币搞定0元的方法数为a,纪念币搞定10元的方法数为b,则总的方法数为
a * b
- 普通硬币搞定1元的方法数为c,纪念币搞定9元的方法数为d,则总的方法数为
c * d
- 普通硬币搞定2元的方法数为e,纪念币搞定9元的方法数为f,则总的方法数为
e * f
- ......
- 普通硬币搞定10元的方法数为x,纪念币搞定0元的方法数为y,则总的方法数为
x * y
总的方法数:a * b + c * d + e * f + ... + x * y
三、实现
// arbitrary:普通货币,onlyone:纪念币,money:目标aim
public static int moneyWays(int[] arbitrary, int[] onlyone, int money) {
if (money < 0) {
return 0;
}
if ((arbitrary == null || arbitrary.length == 0) && (onlyone == null || onlyone.length == 0)) {
return money == 0 ? 1 : 0;
}
// 任意张 的数组, 一张的数组,不可能都没有
int[][] dparb = getDpArb(arbitrary, money);
int[][] dpone = getDpOne(onlyone, money);
// 只有普通币
if (dparb == null) { // 任意张的数组没有,一张的数组有
return dpone[dpone.length - 1][money];
}
// 只有纪念币
if (dpone == null) { // 任意张的数组有,一张的数组没有
return dparb[dparb.length - 1][money];
}
// 既有普通币又有纪念币
int res = 0;
for (int i = 0; i <= money; i++) {
res += dparb[dparb.length - 1][i] * dpone[dpone.length - 1][money - i];
}
return res;
}
// 生成普通币的二维dp表
public static int[][] getDpArb(int[] arr, int money) {
if (arr == null || arr.length == 0) {
return null;
}
int[][] dp = new int[arr.length][money + 1];
// dp[i][j] 0..i券 自由选择张数, 搞定j元, 有多少方法?
for (int i = 0; i < arr.length; i++) { // 第一列格子都填1,代表1种方法,这种方法表示不使用任何普通币
dp[i][0] = 1;
}
// [0] 5元 0元 5元 10元 15元 20元
for (int j = 1; arr[0] * j <= money; j++) { // 第一行格子,整数倍的货币(使用1张,2张,...)格子填1
dp[0][arr[0] * j] = 1;
}
// 0行 0列 填完了,其余格子
for (int i = 1; i < arr.length; i++) {
for (int j = 1; j <= money; j++) {
dp[i][j] = dp[i - 1][j];
dp[i][j] += j - arr[i] >= 0 ? dp[i][j - arr[i]] : 0;
}
}
return dp;
}
// 生成纪念币的二维dp表
public static int[][] getDpOne(int[] arr, int money) {
if (arr == null || arr.length == 0) {
return null;
}
int[][] dp = new int[arr.length][money + 1];
for (int i = 0; i < arr.length; i++) { // 第一列格子都填1,代表1种方法,这种方法表示不使用任何纪念币
dp[i][0] = 1;
}
if (arr[0] <= money) { // 只能使用一种纪念币
dp[0][arr[0]] = 1;
}
// 其余格子
for (int i = 1; i < arr.length; i++) {
for (int j = 1; j <= money; j++) {
dp[i][j] = dp[i - 1][j];
dp[i][j] += j - arr[i] >= 0 ? dp[i - 1][j - arr[i]] : 0;
}
}
return dp;
}