一、动态规划问题
- 动态规划问题的核心,其实就是穷举,列举出所有的情况,并选择最优答案。
- 动态规划问题的穷举会存在“重叠子问题”,如果暴力破解,会有很多重复计算,造成效率低下。
- 动态规划问题一般都具有“最优子结构”,即通过子问题的最值得到最终问题的最值,要求每个子问题之间必须相互独立。
- 动态规划三要素:重叠子问题、最优子结构、状态转移方程
- 思考以下几点:
- 问题的base case(最简单情况,初值)是什么?
- 问题的状态?
- 做什么“选择”使得“状态”改变?
- 如何定义dp数组/函数来表示出“状态”和“选择”?
- 代码框架如下:
dp[0][0][...] = base case;
for 状态1 in 状态1所有取值:
for 状态2 in 状态3所有取值:
for ...
dp[状态1][状态2][...] = 求最值(选择1,选择2,...)
二、凑零钱问题
- 问题描述:给定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1。
1、暴力破解
- 确定问题的base case,目标金额amount为0时,返回0,因为不需要硬币就能凑出0元
- 问题的状态就是总金额
- 做选择,就是选择硬币,选择了硬币以后导致金额变化,也就是问题的状态变化了
- 明确dp定义(此例中直接使用函数签名coinChange):输入硬币列表和目标金额amount,返回凑齐目标金额amount所需的最小硬币数量
/**
* 暴力破解
* @param coins
* @param amount
* @return
*/
public int coinChange(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 = coinChange(coins, amount - coin);
if (subProblem == -1) {
continue;
}
res = Math.min(res, 1 + subProblem);
}
return res == Integer.MAX_VALUE ? -1 : res;//返回当前子问题的最优解
}
2、剪枝优化
- 以暴力破解为基础,添加“备忘录”,避免重复计算
/**
* “备忘录”剪枝,避免重复计算
* @param coins
* @param amount
* @return
*/
Map<Integer, Integer> memo = new HashMap<>();
public int coinChange(int[] coins, int amount){
//如果备忘录中已经存储有该目标值amount所需最小硬币数,那么直接返回即可
if(memo.keySet().contains(amount)) {
return memo.get(amount);
}
if(amount == 0) {
return 0;
}
if(amount < 0) {
return -1;
}
int res = Integer.MAX_VALUE;//子问题的最优解
for (int coin : coins) {
int subProblem = coinChange(coins, amount - coin);
if (subProblem == -1) {
continue;
}
res = Math.min(res, 1 + subProblem);
}
//将新的值存储备忘录
memo.put(amount, res);
return res == Integer.MAX_VALUE ? -1 : res;//返回当前子问题的最优解
}
3、dp数组迭代法
- 暴力破解和“剪枝”优化都属于“自顶向下”,即从总问题开始不断分解子问题。
- dp数组迭代法属于“自底向上”,直接从子问题开始迭代,直到解决总问题
- dp函数定义如下:当目标金额为i时,至少需要dp[i]枚硬币
/**
* dp数组迭代法
* @param coins
* @param amount
* @return
*/
public int coinChange(int[] coins, int amount) {
int[] dp = new int[amount+1];
for (int i = 0; i < dp.length; i++) {
dp[i] = amount+1;
}
dp[0] = 0;
for (int i = 0; i < dp.length; i++) {
for (int coin : coins) {
if (i - coin < 0) {
continue;
}//i每次递增1,所以每次i-coin以后得到的,只要不是小于0,那么肯定是等于0的
dp[i] = Math.min(dp[i], 1 + dp[i - coin]);
}
}
return (dp[amount] == amount+1) ? -1:dp[amount];
}