活动地址:优快云21天学习挑战赛
学习的最大理由是想摆脱平庸,早一天就多一份人生的精彩;迟一天就多一天平庸的困扰。各位小伙伴,如果您:
想系统/深入学习某技术知识点…
一个人摸索学习很难坚持,想组团高效学习…
想写博客但无从下手,急需写作干货注入能量…
热爱写作,愿意让自己成为更好的人…
…
1.算法概述
动态规划(Dynamic programming)是一种在数学、计算机科学和经济学中使用的,通过把原问题分解为相对简单的子问题的方式求解复杂问题的方法。 动态规划常常适用于有重叠子问题和最优子结构性质的问题,动态规划方法所耗时间往往远少于朴素解法。 动态规划背后的基本思想非常简单。大致上,若要解一个给定问题,我们需要解其不同部分(即子问题),再合并子问题的解以得出原问题的解。 通常许多子问题非常相似,为此动态规划法试图仅仅解决每个子问题一次,从而减少计算量: 一旦某个给定子问题的解已经算出,则将其记忆化存储,以便下次需要同一个子问题解之时直接查表。 这种做法在重复子问题的数目关于输入的规模呈指数增长时特别有用。 关于动态规划最经典的问题当属背包问题。
2.算法步骤
① 定义状态(状态是原问题、子问题的解)
✓ 比如定义 dp(i) 的含义
② 设置初始状态(边界)
✓ 比如设置 dp(0) 的值
③ 确定状态转移方程
✓ 比如确定 dp(i) 和 dp(i – 1) 的关
3.动态规划的使用条件
◼ 可以用动态规划来解决的问题,通常具备2个特点
最优子结构(最优化原理):通过求解子问题的最优解,可以获得原问题的最优解
无后效性
✓ 某阶段的状态一旦确定,则此后过程的演变不再受此前各状态及决策的影响(未来与过去无关)
✓ 在推导后面阶段的状态时,只关心前面阶段的具体状态值,不关心这个状态是怎么一步步推导出来的
4.算法练习
找零钱
问题如下
假设有25分、20分、5分、1分的硬币,现要找给客户41分的零钱,如何办到硬币个数最少?
假设用贪心策略得到的并非是最优解(贪心得到的解是 5 枚硬币)
思路如下
◼ 假设 dp(n) 是凑到 n 分需要的最少硬币个数
如果第 1 次选择了 25 分的硬币,那么 dp(n) = dp(n – 25) + 1
如果第 1 次选择了 20 分的硬币,那么 dp(n) = dp(n – 20) + 1
如果第 1 次选择了 5 分的硬币,那么 dp(n) = dp(n – 5) + 1
如果第 1 次选择了 1 分的硬币,那么 dp(n) = dp(n – 1) + 1
所以 dp(n) = min { dp(n – 25), dp(n – 20), dp(n – 5), dp(n – 1) } + 1
实现如下
/**
* 找零钱问题动态规划实现(暴力递归 -》记忆化搜索 -》递推 逐步优化实现)
*/
public class CoinChange {
public static void main(String[] args) {
System.out.println(coinChange(new int[]{1, 2, 5}, 11));
}
private static boolean flag = true;
public static int coinChange(int[] coins, int amount) {
// 过滤不能找零的情况
if (coins == null || coins.length == 0) return -1;
// 无需找零
if(amount == 0) return 0;
// 对零钱按从小到达大排序
Arrays.sort(coins);
// 返回零钱数
int ret = coinChange3(coins, amount);
return flag ? ret : -1;
}
/**
* 使用暴力递归
*/
public static int coinChange1(int[] coins, int amount) {
// 递归底,如果最小的面值比待找零大则不能找零,如果面值和找零相同,则返回1
if (coins[0] > amount) {
flag = false;
return -1;
}
for (int coin : coins) {
if (coin == amount) return 1;
}
// 递归选取最佳方案
int minNum = Integer.MAX_VALUE;
for (int coin : coins) {
if (coin < amount) {
minNum = Math.min(minNum, coinChange(coins, amount - coin));
}
}
return minNum + 1;
}
/**
* 记忆化搜索(演进)
*/
public static int coinChange2(int[] coins, int amount) {
int[] dp = new int[amount + 1];
// 初始化
for (int coin : coins) dp[coin] = 1;
return coinChange2(coins, amount, dp);
}
public static int coinChange2(int[] coins, int amount, int[] dp) {
if (coins[0] > amount) {
flag = false;
return -1;
}
int minNum = Integer.MAX_VALUE;
if (dp[amount] == 0) {
for (int coin : coins) {
if (coin < amount) {
minNum = Math.min(minNum, coinChange2(coins, amount - coin, dp));
}
}
dp[amount] = minNum + 1;
}
return dp[amount];
}
/**
* 递推(动态规划最终实现)
*/
public static int coinChange3(int[] coins, int amount) {
// 1. 定义状态:dp(i)表示i元时的最佳找零数
int[] dp = new int[amount + 1];
// 2. 设置初始状态:dp(0) = 0,数组默认已经赋值
// 3. 确定状态转移方程
for (int i = 1; i <= amount; i++) {
int min = Integer.MAX_VALUE;
for (int coin : coins) {
if (i < coin) continue;
if (dp[i - coin] < 0 || dp[i - coin] >= min) continue;
min = dp[i - coin];
}
if (min == Integer.MAX_VALUE) {
// 找不出零钱的情况
dp[i] = -1;
} else {
dp[i] = min + 1;
}
}
return dp[amount];
}
}