动态规划(2)
LCR 103. 零钱兑换
零钱找零
1.暴力
思路
大问题拆解子问题
边界条件: amount刚好等于0 说明方法起效(极限)
amount小于0 说明价格不能再减
1 + dp(coins, n - coin) 表示每次可以取的次数
class Solution {public int coinChange(int[] coins, int amount) {return dp(coins, amount);}public int dp(int[] coins,int amount){if (amount == 0) return 0;if (amount < 0) return -1;int res = Integer.MAX_VALUE;for(int coin : coins){int subproblem = dp(coins, amount - coin);if (subproblem == -1) continue;res = Math.min(res, subproblem + 1);}return res == Integer.MAX_VALUE ? -1 : res;}public static void main(String[] args) {int[] coins = {1,2,5};int amount = 11;Solution s = new Solution();System.out.println(s.coinChange(coins, amount));}
}
超时
2.剪枝
因为每个的amount都被重复计算
设置memo 如果memo被计算过直接返回
import java.util.Arrays;class Solution {int[] memo;public int coinChange(int[] coins, int amount) {memo = new int[amount + 1];/* for (int i = 0; i < amount + 1; i++) {memo[i] = -3;}*/Arrays.fill(memo,-3);return dp(coins, amount);}public int dp(int[] coins,int amount){if (amount == 0) return 0;if (amount < 0) return -1;int res = Integer.MAX_VALUE;if (memo[amount] != -3) return memo[amount];for(int coin : coins){int subproblem = dp(coins, amount - coin);if (subproblem == -1) continue;res = Math.min(res, subproblem + 1);}return memo[amount] = res == Integer.MAX_VALUE ? -1 : res;// return res == Integer.MAX_VALUE ? -1 : res;}public static void main(String[] args) {int[] coins = {1,2,5};int amount = 11;Solution s = new Solution();System.out.println(s.coinChange(coins, amount));}
}
3.灵神算法
不一样的思路cache对应dfs
如果当前的节点费用小于当前coin 则返回上一个节点的相同费用
如果C小于0 则返回-1 说明不存在刚好的策略
而对每一个选项都有选和不选的两种选择
class Solution {private int[] coins;private int[][] cache;public int coinChange(int[] coins, int amount) {this.coins = coins;int n = coins.length;cache = new int[n][amount + 1];for (int i = 0; i < n; i++)Arrays.fill(cache[i], -1); // -1 表示没用访问过int ans = dfs(n - 1, amount);return ans < Integer.MAX_VALUE / 2 ? ans : -1;}private int dfs(int i, int c) {if (i < 0) return c == 0 ? 0 : Integer.MAX_VALUE / 2; // 除 2 是防止下面 + 1 溢出if (cache[i][c] != -1) return cache[i][c];if (c < coins[i]) return cache[i][c] = dfs(i - 1, c);return cache[i][c] = Math.min(dfs(i - 1, c), dfs(i, c - coins[i]) + 1);}
}作者:灵茶山艾府
链接:https://leetcode.cn/problems/gaM7Ch/solutions/2128055/jiao-ni-yi-bu-bu-si-kao-dong-tai-gui-hua-kmrs/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
核心思路
-
状态定义:
cache[i][c]
表示使用前i
种硬币组成金额c
的最少硬币数量- 如果
cache[i][c] = -1
,表示该状态还未被计算过
-
状态转移:
- 对于每种硬币
coins[i]
,有两种选择:- 不使用当前硬币
coins[i]
,则状态转移为dfs(i-1, c)
- 使用至少一枚当前硬币
coins[i]
,则状态转移为dfs(i, c-coins[i]) + 1
- 不使用当前硬币
- 取两种选择中的较小值作为结果
- 对于每种硬币
-
初始条件:
- 当
i < 0
(没有硬币可用)时:- 如果
c == 0
,返回 0(不需要任何硬币) - 否则返回
Integer.MAX_VALUE / 2
(表示无法组成该金额) - 除以 2 是为了防止后续计算中
+1
导致整数溢出
- 如果
- 当
-
结果处理:
- 如果最终结果小于
Integer.MAX_VALUE / 2
,说明存在解,返回该结果 - 否则返回
-1
表示无法组成目标金额
- 如果最终结果小于
关键细节解释
-
Integer.MAX_VALUE/ 2 的作用:
- 在动态规划中,通常用无穷大表示不可达状态
- 这里使用
Integer.MAX_VALUE / 2
而不是Integer.MAX_VALUE
是为了避免在状态转移时发生整数溢出 - 例如,如果
dfs(i-1, c)
或dfs(i, c-coins[i])
返回Integer.MAX_VALUE
,直接加 1 会导致溢出
-
二维数组的优势:
- 相比一维数组解法,二维数组可以更清晰地表达状态转移过程
- 可以更方便地处理 “每种硬币无限使用” 的条件(通过
dfs(i, c-coins[i])
实现)
-
记忆化的作用:
- 通过
cache
数组记录已计算的状态,避免重复计算 - 时间复杂度从指数级降低到 O (n * amount),其中 n 是硬币种类
- 通过
4.压缩dp数组()
动态规划解题套路框架 | labuladong 的算法笔记
class Solution {public int coinChange(int[] coins, int amount) {int[] dp = new int[amount + 1];// 数组大小为 amount + 1,初始值也为 amount + 1Arrays.fill(dp, amount + 1);// base casedp[0] = 0;// 外层 for 循环在遍历所有状态的所有取值for (int i = 0; i < dp.length; i++) {// 内层 for 循环在求所有选择的最小值for (int coin : coins) {// 子问题无解,跳过if (i - coin < 0) {continue;}dp[i] = Math.min(dp[i], 1 + dp[i - coin]);}}return (dp[amount] == amount + 1) ? -1 : dp[amount];}
}