动态规划(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、无后效性

动态规划(Dynamic Programming)

  • 从起点(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
上一篇:dlib读取68个人脸特征点


下一篇:2020-12-31