普通硬币和纪念币拼m值的方法数

383 阅读3分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 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;
}