动态规划(Dynamic Programming)
作者:互联网
一、动态规划(Dynamic Programming)
- 动态规划,简称DP
是求解最优化问题的一种常用策略 - 通常的使用套路(一步一步优化)
① 暴力递归(自顶向下,出现了重叠子问题)
② 记忆化搜索(自顶向下)
③ 递推(自底向上)
1、动态规划的常规步骤
- 动态规划中的“动态”可以理解为是“会变化的状态”
① 定义状态(状态是原问题、子问题的解)
✓ 比如定义 dp(i) 的含义
② 设置初始状态(边界)
✓ 比如设置 dp(0) 的值
③ 确定状态转移方程
✓ 比如确定 dp(i) 和 dp(i – 1) 的关系
2、动态规划的一些相关概念
- 来自维基百科的解释
Dynamic Programming is a method for solving a complex problem by breaking it down into a
collection of simpler subproblems, solving each of those subproblems just once , and storing their
solutions.
① 将复杂的原问题拆解成若干个简单的子问题
② 每个子问题仅仅解决1次,并保存它们的解
③ 最后推导出原问题的解 - 可以用动态规划来解决的问题,通常具备2个特点
①、最优子结构(最优化原理):通过求解子问题的最优解,可以获得原问题的最优解
②、无后效性
✓ 某阶段的状态一旦确定,则此后过程的演变不再受此前各状态及决策的影响(未来与过去无关)
✓ 在推导后面阶段的状态时,只关心前面阶段的具体状态值,不关心这个状态是怎么一步步推导出来的
3、无后效性
- 从起点(0, 0)走到终点(4, 4)一共有多少种走法?只能向右、向下走
- 假设 dp(i, j) 是从(0, 0)走到(i, j)的走法
①、dp(i, 0) = dp(0, j) = 1
②、dp(i, j) = dp(i, j – 1) + dp(i – 1, j) - 无后效性
①、推导 dp(i, j) 时只需要用到 dp(i, j – 1)、dp(i – 1, j) 的值
②、不需要关心 dp(i, j – 1)、dp(i – 1, j) 的值是怎么求出来的
4、有后效性
- 如果可以向左、向右、向上、向下走,并且同一个格子不能走 2 次
- 有后效性
dp(i, j) 下一步要怎么走,还要关心上一步是怎么来的(因为以前走过的方块就不能再走啦)
✓ 也就是还要关心 dp(i, j – 1)、dp(i – 1, j) 是怎么来的?
5、练习1 — 找零钱
- 来源:力扣(LeetCode)_322_零钱兑换
链接:https://leetcode-cn.com/problems/coin-change - 假设有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
(1)找零钱 – 暴力递归
public class CoinChange {
public static void main(String[] args) {
System.out.println(coins(41));
}
/*暴力递归(自顶向下的调用,出现了重叠子问题)*/
static int coins(int n){
if (n < 1) return Integer.MAX_VALUE;
if (n == 25 || n == 20 || n == 5 || n == 1) return 1;
int min1 = Math.min(coins(n - 25), coins(n - 20));
int min2 = Math.min(coins(n - 5), coins(n - 1));
return Math.min(min1,min2) + 1 ;
}
}
(2)找零钱 – 记忆化搜索
public class CoinChange {
public static void main(String[] args) {
System.out.println(coins2(41));//3
System.out.println(coins2(19));//7
}
/*记忆化搜索(自顶向下的调用)*/
static int coins2(int n){
if (n < 1) return -1;//一开始就过滤掉不合理的条件
//数组dp存储凑到n分需要的最少硬币个数
int[] dp = new int[n + 1];
int[] faces = {1,5,20,25};
for (int face : faces) {
if (n < face) continue;
//凑够n分需要的最少硬币个数是1枚
dp[face] = 1;
}
return coins2(n,dp);
}
static int coins2(int n,int[] dp){
if (n < 1) return Integer.MAX_VALUE;//为了下面递归服务(递归基)
if (dp[n] == 0){
int min1 = Math.min(coins2(n - 25,dp), coins2(n - 20,dp));
int min2 = Math.min(coins2(n - 5,dp), coins2(n - 1,dp));
dp[n] = Math.min(min1,min2) + 1;
}
return dp[n];
}
}
(3)找零钱 – 递推
public class CoinChange {
public static void main(String[] args) {
System.out.println(coins3(41));//3
System.out.println(coins3(19));//7
}
/*递推(自底向上)*/
static int coins3(int n){
if (n < 1) return -1;
int[] dp = new int[n + 1];
for (int i = 1; i <= n; i++) {
int min = dp[i-1];
if (i >= 5) min = Math.min(dp[i-5], min);
if (i >= 20) min = Math.min(dp[i-20], min);
if (i >= 25) min = Math.min(dp[i-25], min);
dp[i] = min + 1;
}
return dp[n];
}
}
- 时间复杂度、空间复杂度:O(n)
(4)思考题:请输出找零钱的具体方案(具体是用了哪些面值的硬币)
public class CoinChange {
public static void main(String[] args) {
System.out.println(coins4(41));//3
System.out.println("--------------");
System.out.println(coins4(19));//7
}
/*具体是用了哪些面值的硬币*/
static int coins4(int n){
if (n < 1) return -1;
int[] dp = new int[n + 1];
//faces[i]是凑够i分时最后选择的那枚硬币的面值
int[] faces = new int[dp.length];
for (int i = 1; i <= n; i++) {
int min = Integer.MAX_VALUE;
if (i >= 1 && dp[i-1] < min) {
min = dp[i-1];
faces[i] = 1;
}
if (i >= 5 && dp[i-5] < min) {
min = dp[i-5];
faces[i] = 5;
}
if (i >= 20 && dp[i-20] < min) {
min = dp[i-20];
faces[i] = 20;
}
if (i >= 25 && dp[i-25] < min) {
min = dp[i-25];
faces[i] = 25;
}
dp[i] = min + 1;
//print(faces,i);
}
print(faces,n);
return dp[n];
}
static void print(int[] faces,int n){
System.out.print("["+ n +"] = ");
//faces[n]指的是最后一枚硬币
//n - faces[n]指的是除了最后一枚硬币外,剩下要凑的钱
while (n > 0){
System.out.print(faces[n]+ " ");
n -= faces[n];
}
System.out.println();
}
}
运行结果:
[41] = 1 20 20
3
--------------
[19] = 1 1 1 1 5 5 5
7
(5)找零钱 – 通用实现
public class CoinChange {
public static void main(String[] args) {
System.out.println(coins5(41, new int[]{1,5,20,25}));//3
}
static int coins5(int n,int[] faces){
if (n < 1 || faces == null || faces.length == 0) return -1;
int[] dp = new int[n + 1];
for (int i = 1; i <= n; i++) {
int min = Integer.MAX_VALUE;
for (int face : faces) {
if (i < face) continue;;
min = Math.min(dp[i-face],min);
//dp(41) = 凑够41需要最少的硬币数量
//dp(41 -1) = dp(40) = 凑够40需要最少的硬币数量
//dp(41 -5) = dp(36) = 凑够36需要最少的硬币数量
//dp(41 -20) = dp(21) = 凑够21需要最少的硬币数量
//dp(41 -25) = dp(16) = 凑够16需要最少的硬币数量
//min{dp(40) , dp(36) , dp(21) , dp(16) } + 1 ->求用到硬币数量最少的个数
}
dp[i] = min + 1;
}
return dp[n];
}
}
- 注意:对于上述通用的代码如果我们有凑够i分的面值,那么程序就不会出现错误,并且会很好的找对零钱;但是如果我们没有凑够i分的面值,那么程序就会出现错误。
- 对于上述的代码我们可以进行优化,对于我们没有面值可以凑够i分,那么就返回-1.
public class CoinChange {
public static void main(String[] args) {
System.out.println(coins5(41, new int[]{1,5,20,25}));//3
System.out.println(coins5(6, new int[]{5,20,25}));//-1
}
static int coins5(int n,int[] faces){
if (n < 1 || faces == null || faces.length == 0) return -1;
int[] dp = new int[n + 1];
for (int i = 1; i <= n; i++) {
int min = Integer.MAX_VALUE;
for (int face : faces) {
if (i < face) continue;
int v = dp[i -face];
if (v < 0 || dp[i - face] >= min) continue;
min = dp[i-face];
}
if (min == Integer.MAX_VALUE){
dp[i] = -1;
}else {
dp[i] = min + 1;
}
}
return dp[n];
}
}
6、练习2 – 最大连续子序列和
- 给定一个长度为 n 的整数序列,求它的最大连续子序列和
比如 –2、1、–3、4、–1、2、1、–5、4 的最大连续子序列和是 4 + (–1) + 2 + 1 = 6 - 状态定义
假设 dp(i) 是以 nums[i] 结尾的最大连续子序列和(nums是整个序列)
✓ 以 nums[0] –2 结尾的最大连续子序列是 –2,所以 dp(0) = –2
✓ 以 nums[1] 1 结尾的最大连续子序列是 1,所以 dp(1) = 1
✓ 以 nums[2] –3 结尾的最大连续子序列是 1、–3,所以 dp(2) = dp(1) + (–3) = –2
✓ 以 nums[3] 4 结尾的最大连续子序列是 4,所以 dp(3) = 4
✓ 以 nums[4] –1 结尾的最大连续子序列是 4、–1,所以 dp(4) = dp(3) + (–1) = 3
✓ 以 nums[5] 2 结尾的最大连续子序列是 4、–1、2,所以 dp(5) = dp(4) + 2 = 5
✓ 以 nums[6] 1 结尾的最大连续子序列是 4、–1、2、1,所以 dp(6) = dp(5) + 1 = 6
✓ 以 nums[7] –5 结尾的最大连续子序列是 4、–1、2、1、–5,所以 dp(7) = dp(6) + (–5) = 1
✓ 以 nums[8] 4 结尾的最大连续子序列是 4、–1、2、1、–5、4,所以 dp(8) = dp(7) + 4 = 5
标签:25,20,min,int,Dynamic,Programming,faces,动态,dp 来源: https://blog.csdn.net/qq_44096670/article/details/112104025