内卷大厂系列《货币问题四连击》

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第5天,点击查看活动详情

大厂高频算法面试题:《货币问题系列》,通过货币问题四连击,您将学到如何从暴力递归改动态规划,而不是一上来就硬憋dp,一切从尝试入手,从暴力递归开始,通过暴力递归填dp格子,进一步看有没有优化空间,需不需要斜率优化。

一、货币问题I

给你一个整数数组 coins ,表示不同面额的硬币;以及一个整数 amount ,表示总金额。

计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额,返回 -1 。

你可以认为每种硬币的数量是无限的。

示例 1:

输入:coins = [1, 2, 5], amount = 11
输出:3 
解释:11 = 5 + 5 + 1
复制代码

示例 2:

输入:coins = [2], amount = 3
输出:-1
复制代码

示例 3:

输入:coins = [1], amount = 0
输出:0
复制代码

提示:

  • 1 <= coins.length <= 12
  • 1 <= coins[i] <= 2^31 - 1
  • 0 <= amount <= 10^4

leetcode

1、分析

从左往右的尝试模型,每种货币都尝试,用0张、用1张、用2张...,最后求min就是组成aim(amount)的最少货币数

2、实现

2.1、暴力递归

暴力实现测试会超时,但思路是对的,需要进一步优化。

public static int coinChange(int[] coins, int amount) {
    int ans = process(coins, 0, amount);
    return ans == Integer.MAX_VALUE ? -1 : ans;
}

// arr[index...]面值,每种面值张数自由选择,
// 搞出rest正好这么多钱,返回最小张数
// 拿Integer.MAX_VALUE标记怎么都搞定不了
private static int process(int[] arr, int index, int rest) {
    // 当前index来到数组越界位置,如果剩余rest等于0,说明本次货币不需要用,前边已经搞定了
    if (index == arr.length) { // base case 
        return rest == 0 ? 0 : Integer.MAX_VALUE;
    } 
    int ans = Integer.MAX_VALUE;
    for (int zhang = 0; zhang * arr[index] <= rest; zhang++) {
        int next = process(arr, index + 1, rest - zhang * arr[index]);
        if (next != Integer.MAX_VALUE) {
            ans = Math.min(ans, zhang + next);
        }
    }
    return ans;
}
复制代码

2.2、dp(填表)

由暴力递归改动态规划,通过递归分析得出index位置依赖于index+1,所以从下往上推,填格子

public static int coinChange(int[] coins, int amount) {
  int ans = dp(coins, amount);
  return ans == Integer.MAX_VALUE ? -1 : ans;
}

public static int dp(int[] arr, int aim) {
  if (aim == 0) {
      return 0;
  }
  int N = arr.length;
  int[][] dp = new int[N + 1][aim + 1];
  dp[N][0] = 0;
  for (int j = 1; j <= aim; j++) {
      dp[N][j] = Integer.MAX_VALUE;
  }
  for (int index = N - 1; index >= 0; index--) {
      for (int rest = 0; rest <= aim; rest++) {
          int ans = Integer.MAX_VALUE;
          for (int zhang = 0; zhang * arr[index] <= rest; zhang++) {
              int next = dp[index + 1][rest - zhang * arr[index]];
              if (next != Integer.MAX_VALUE) {
                  ans = Math.min(ans, zhang + next);
              }
          }
          dp[index][rest] = ans;
      }
  }
  return dp[0][aim];
}

复制代码

2.3、dp(斜率优化)

发现有枚举行为的动态规划,可以进一步优化,把最内层的for循环去掉,观察临近位置,看哪些位置能替换枚举行为,变成几个有限位置做决策,在dp中可以认为这种操作为斜率优化。

public static int coinChange(int[] coins, int amount) {
    int ans = dp(coins, amount);
    return ans == Integer.MAX_VALUE ? -1 : ans;
}

public static int dp(int[] arr, int aim) {
    if (aim == 0) {
        return 0;
    }
    int N = arr.length;
    int[][] dp = new int[N + 1][aim + 1];
    dp[N][0] = 0;
    for (int j = 1; j <= aim; j++) {
        dp[N][j] = Integer.MAX_VALUE;
    }
    for (int index = N - 1; index >= 0; index--) {
        for (int rest = 0; rest <= aim; rest++) {
            dp[index][rest] = dp[index + 1][rest];
            if (rest - arr[index] >= 0
                && dp[index][rest - arr[index]] != Integer.MAX_VALUE) {
                dp[index][rest] = Math.min(dp[index][rest], dp[index][rest - arr[index]] + 1);
            }
        }
    }
    return dp[0][aim];
}
复制代码

二、货币问题II

给你一个整数数组 coins 表示不同面额的硬币,另给一个整数 amount 表示总金额。

请你计算并返回可以凑成总金额的硬币组合数。如果任何硬币组合都无法凑出总金额,返回 0 。

假设每一种面额的硬币有无限个。 

题目数据保证结果符合 32 位带符号整数。

示例 1:

输入:amount = 5, coins = [1, 2, 5]
输出:4
解释:有四种方式可以凑成总金额:
5=5
5=2+2+1
5=2+1+1+1
5=1+1+1+1+1
复制代码

示例 2:

输入:amount = 3, coins = [2]
输出:0
解释:只用面额 2 的硬币不能凑成总金额 3复制代码

示例 3:

输入:amount = 10, coins = [10] 
输出:1
复制代码

提示:

  • 1 <= coins.length <= 300
  • 1 <= coins[i] <= 5000
  • coins 中的所有值 互不相同
  • 0 <= amount <= 5000

leetcode

1、分析

从左往右的尝试模型:每种货币用0张,用1张,用2张......

2、实现

2.1、暴力递归

暴力实现测试会超时,但思路是对的,需要进一步优化。

public int change(int amount, int[] coins) {
    return coinsWay(coins, amount);
}

public static int coinsWay(int[] arr, int aim) {
    if (arr == null || arr.length == 0 || aim < 0) {
        return 0;
    }
    return process(arr, 0, aim);
}

// arr[index....] 所有的面值,每一个面值都可以任意选择张数,组成正好rest这么多钱,方法数多少?
private static int process(int[] arr, int index, int rest) {
    if (index == arr.length) { // base case 没钱了
        return rest == 0 ? 1 : 0;
    }
    int ways = 0;
    for (int zhang = 0; zhang * arr[index] <= rest; zhang++) {
        ways += process(arr, index + 1, rest - (zhang * arr[index]));
    }
    return ways;
}
复制代码

2.2、dp(填表)

由暴力递归改动态规划,通过递归分析得出index位置依赖于index+1,所以从下往上推,填格子

public int change(int amount, int[] coins) {
    return dp(coins, amount);
}

public static int dp(int[] arr, int aim) {
    if (arr == null || arr.length == 0 || aim < 0) {
        return 0;
    }
    int N = arr.length;
    int[][] dp = new int[N + 1][aim + 1];
    dp[N][0] = 1;
    for (int index = N - 1; index >= 0; index--) {
        for (int rest = 0; rest <= aim; rest++) {
            int ways = 0;
            for (int zhang = 0; zhang * arr[index] <= rest; zhang++) {
                ways += dp[index + 1][rest - (zhang * arr[index])];
            }
            dp[index][rest] = ways;
        }
    }
    return dp[0][aim];
}
复制代码

2.3、dp(斜率优化)

发现有枚举行为(最内层for循环),可进一步通过观察邻近位置进行斜率优化

public int change(int amount, int[] coins) {
    return dp(coins, amount);
}

public static int dp(int[] arr, int aim) {
    if (arr == null || arr.length == 0 || aim < 0) {
        return 0;
    }
    int N = arr.length;
    int[][] dp = new int[N + 1][aim + 1];
    dp[N][0] = 1;
    for (int index = N - 1; index >= 0; index--) {
        for (int rest = 0; rest <= aim; rest++) {
            dp[index][rest] = dp[index + 1][rest];
            if (rest - arr[index] >= 0) {
                dp[index][rest] += dp[index][rest - arr[index]];
            }
        }
    }
    return dp[0][aim];
}
复制代码

三、货币问题III

arr是货币数组,其中的值都是正数。再给定一个正数aim。

每个值都认为是一张货币,

认为值相同的货币没有任何不同,

返回组成aim的方法数

例如:arr = {1,2,1,1,2,1,2},aim = 4

方法:1+1+1+1、1+1+2、2+2

一共就3种方法,所以返回3

1、分析

货币问题II中的货币是无限张,此题的货币张数是有限的,这是关键点

统计不同面值的货币出现的次数

2、实现

2.1、暴力递归

public static class Info {
    public int[] coins; // 货币数组
    public int[] zhangs; // 张数数组

    public Info(int[] c, int[] z) {
        coins = c;
        zhangs = z;
    }
}

public static int coinsWay(int[] arr, int aim) {
    if (arr == null || arr.length == 0 || aim < 0) {
        return 0;
    }
    Info info = getInfo(arr); // 统计不同货币的张数信息
    return process(info.coins, info.zhangs, 0, aim);
}

// coins 面值数组,正数且去重
// zhangs 每种面值对应的张数
private static int process(int[] coins, int[] zhangs, int index, int rest) {
    if (index == coins.length) {
        return rest == 0 ? 1 : 0;
    }
    int ways = 0;
    for (int zhang = 0; zhang * coins[index] <= rest && zhang <= zhangs[index]; zhang++) {
        ways += process(coins, zhangs, index + 1, rest - (zhang * coins[index]));
    }
    return ways;
}

private static Info getInfo(int[] arr) {
    // key:货币面值,value:货币张数
    HashMap<Integer, Integer> counts = new HashMap<>();
    for (int value : arr) {
        if (!counts.containsKey(value)) {
            counts.put(value, 1);
        } else {
            counts.put(value, counts.get(value) + 1);
        }
    }
    int N = counts.size();
    int[] coins = new int[N];
    int[] zhangs = new int[N];
    int index = 0;
    for (Map.Entry<Integer, Integer> entry : counts.entrySet()) {
        coins[index] = entry.getKey();
        zhangs[index++] = entry.getValue();
    }
    return new Info(coins, zhangs);
}
复制代码

2.2、dp(填表)

根据暴力递归改动态规划

public static int dp(int[] arr, int aim) {
    if (arr == null || arr.length == 0 || aim < 0) {
        return 0;
    }
    Info info = getInfo(arr);
    int[] coins = info.coins;
    int[] zhangs = info.zhangs;
    int N = coins.length;
    int[][] dp = new int[N + 1][aim + 1];
    dp[N][0] = 1;
    for (int index = N - 1; index >= 0; index--) {
        for (int rest = 0; rest <= aim; rest++) {
            int ways = 0;
            for (int zhang = 0; zhang * coins[index] <= rest && zhang <= zhangs[index]; zhang++) {
                ways += dp[index + 1][rest - (zhang * coins[index])];
            }
            dp[index][rest] = ways;
        }
    }
    return dp[0][aim];
}
复制代码

2.3、dp(斜率优化)

发现有内层for循环,画图找邻居位置,得出依赖关系,进一步斜率优化,省掉内层for循环

public static int dp(int[] arr, int aim) {
    if (arr == null || arr.length == 0 || aim < 0) {
        return 0;
    }
    Info info = getInfo(arr);
    int[] coins = info.coins;
    int[] zhangs = info.zhangs;
    int N = coins.length;
    int[][] dp = new int[N + 1][aim + 1];
    dp[N][0] = 1;
    for (int index = N - 1; index >= 0; index--) {
        for (int rest = 0; rest <= aim; rest++) {
            dp[index][rest] = dp[index + 1][rest];
            if (rest - coins[index] >= 0) {
                dp[index][rest] += dp[index][rest - coins[index]];
            }
            if (rest - coins[index] * (zhangs[index] + 1) >= 0) {
                dp[index][rest] -= dp[index + 1][rest - coins[index] * (zhangs[index] + 1)];
            }
        }
    }
    return dp[0][aim];
}
复制代码

四、货币问题IV

arr是货币数组,其中的值都是正数。再给定一个正数aim。

每个值都认为是一张货币,

即便是值相同的货币也认为每一张都是不同的,

返回组成aim的方法数

例如:arr = {1,1,1},aim = 2

第0个和第1个能组成2,第1个和第2个能组成2,第0个和第2个能组成2

一共就3种方法,所以返回3

1、分析

货币问题III是值相同的货币没有任何不同,而此题值相同的货币认为是不同的,这是这道题的关键。

2、实现

2.1、暴力递归

先用暴力解求出,再进一步优化

从左往右的尝试模型:每张货币要不要问题

public static int coinWays(int[] arr, int aim) {
    return process(arr, 0, aim);
}

// arr[index....] 组成正好rest这么多的钱,有几种方法
private static int process(int[] arr, int index, int rest) {
    if (rest < 0) {
        return 0;
    }
    if (index == arr.length) { // 没钱了!
        return rest == 0 ? 1 : 0;
    } 
    // 可能性一:要当前货币
    // 可能性二:不要当前货币
    return process(arr, index + 1, rest) + process(arr, index + 1, rest - arr[index]);
}
复制代码

2.2、dp(填表)

根据暴力递归改动态规划,没有枚举行为,填好dp格子就是最优解

填表:index位置依赖index+1位置,所以从下往上填,从左往右填

public static int dp(int[] arr, int aim) {
    if (aim == 0) {
        return 1;
    }
    int N = arr.length;
    int[][] dp = new int[N + 1][aim + 1];
    dp[N][0] = 1;
    for (int index = N - 1; index >= 0; index--) {
        for (int rest = 0; rest <= aim; rest++) {
            dp[index][rest] = dp[index + 1][rest] + (rest - arr[index] >= 0 ? dp[index + 1][rest - arr[index]] : 0);
        }
    }
    return dp[0][aim];
}
复制代码

猜你喜欢

转载自juejin.im/post/7083140303971942414