第一章:动态规划入门

1.1 什么是动态规划

  • 科技公司面试必考算法
  • 题目类型多,没有固定模板
  • 难度属于中上
  • 根据面试经验,一半的失败都与动态规划有关
  • 必须掌握
  • 并没有那么可怕
  • 有规律可循
  • 掌握其中思想,举一反三

image.png

  • 求有多少种方式走到右下角(DP)
  • 输出所有走到右下角的路径(DFS、递归)

    1.2 题目特点

    :::info
  1. 计数
    1. 有多少种方式走到右下角
    2. 有多少种方法选出k个数使得和是Sum :::
      1. 求最大最小值
        1. 从左上角走到右下角路径的最大数字和
        2. 最长上升子序列长度
  1. 求存在性
    1. 取石子游戏,先手是否必胜
    2. 能不能选出k个数使得和是Sum

1.3 动态规划四大组成部分

1.3.1 确定状态

  • 状态在动态规划中的作用属于定海神针
  • 简单的说,解动态规划的时候需要开一个数组,数组的每个元素f[i]或者f[i][j]代表什么?
    • 类似于解数学题中,X、Y、Z代表什么
  • 确定状态需要两个意识

    • 最后一步
    • 子问题

      最后一步

  • 虽然我们不知道最优策略是什么,但是最优策略肯定是K枚硬币第一章:动态规划入门 - 图2面值加起来是27

  • 所以一定有一枚**最后的**硬币:第一章:动态规划入门 - 图3
  • 除掉这枚硬币,前面硬币的面值加起来是第一章:动态规划入门 - 图4

    关键点1:我们不关心前面的第一章:动态规划入门 - 图5枚硬币是怎么拼出第一章:动态规划入门 - 图6的(可能有1种拼法,可能有100种拼法),而且我们现在甚至还不知道第一章:动态规划入门 - 图7第一章:动态规划入门 - 图8,但是我们确定前面的硬币拼出了第一章:动态规划入门 - 图9

关键点2:因为是最优策略,所以拼出第一章:动态规划入门 - 图10的硬币数一定要最少,否则这就不是最优策略了

yuque_diagram.png

子问题

  • 所以我们就要求:最少用多少枚硬币可以拼出第一章:动态规划入门 - 图12
  • 原问题是最少用多少枚硬币拼出27
  • 我们将原问题转化为一个子问题,而且规模更小:第一章:动态规划入门 - 图13
  • 为了简化定义,我们设状态第一章:动态规划入门 - 图14= 最少用多少枚硬币拼出X
  • 等等,我们还不知道最后那枚硬币第一章:动态规划入门 - 图15是多少
  • 最后那枚硬币第一章:动态规划入门 - 图16只可能是2、5或7
  • 如果第一章:动态规划入门 - 图17f(27)应该是f(27-2) + 1(加上最后这一枚硬币2)
  • 如果第一章:动态规划入门 - 图18f(27)应该是f(27-5) + 1(加上最后这一枚硬币5)
  • 如果第一章:动态规划入门 - 图19f(27)应该是f(27-7) + 1(加上最后这一枚硬币7)
  • 除此以外,没有其他的可能了

image.png

1.3.2 转移方程

  • 设状态f[x]=最少用多少枚硬币拼出X
  • 对于任意X

image.png

1.3.3 初始条件和边界情况

  • f[X] = min{f[X-2]+1,f[X-5]+1,f[X-7]+1}
  • 两个问题:X-2,X-5或者X-7小于0怎么办?什么时候停下来?
  • 如果不能拼出Y,就定义f[Y]=正无穷
    • 例如f[-1] = f[-2] = . . . = 正无穷
  • 所以f[1] = min{f[-1]+1,f[-4]+1,f[-6]+1}=正无穷,表示拼不出来1
  • 初始条件:f[0] = 0

    • 用转移方程算不出来的,需要手工定义

      1.3.4 计算顺序

  • **拼出X所需要的最少硬币数:f[X] = min{f[X-2]+1,f[X-5]+1,f[X-7]+1}**

  • 初始条件:f[0] = 0
  • 然后计算f[1] f[2] ... f[27]
  • 当我们计算到f[X]时,f[X-2] f[X-5] f[X-7]都已经得到结果了
  • f[X] = 最少用多少枚硬币拼出X
  • f[X] = 第一章:动态规划入门 - 图22表示无法用硬币拼出X

image.png

  • 每一步尝试三种硬币,一共27步
  • 与递归算法相比,没有任何重复计算
  • 算法时间复杂度(即需要进行的步数):27*3第一章:动态规划入门 - 图24

    1.4 Onsite

    :::tips

  • 现场面试(报销食宿及飞机票)

  • 时间:8:30~22:30
  • 轮次:3~6轮 45m~1h
  • 休息好!休息状态不好影响发挥
  • 50%白板编程 :::

    1.5 入门总结

  • 四个组成部分

    • 确定状态
      • 研究最优策略的最后一步
      • 化为子问题
    • 转移方程
      • 根据子问题定义直接得到
    • 初始条件和边界情况
      • 细心,考虑周全
    • 计算顺序
      • 利用之前的计算结果

        1.6 课程FAQ (Frequently Asked Questions 常见问题解答)

  • 是否涵盖所有的动态规划考题类型

  • 常见动态规划类型
    • 坐标型动态规划(20%)
    • 序列型动态规划(20%)
    • 划分型动态规划(20%)
    • 区间型动态规划(15%)
    • 背包型动态规划(10%)
    • 最长序列型动态规划(5%)
      • 最长上升子序列
    • 博弈型动态规划(5%)
    • 综合型动态规划(5%)
      • 区间博弈、序列划分
      • DP + 二分、DP+Trie
  • 动态规划时间空间优化
    • FollowUp 常考:滚动数组、降维
    • 动态规划打印路径
  • 我需要什么基础才可以上这个班
    • 学一门基础语言,写过20~30题,想对动态规划有透彻了解
  • 这门课和九章算法班及算法强化班动态规划部分的关系
    • 内容更加丰富、涵盖类型更广、题目更多更全
    • 全面彻底解决面试中的老大难:动态规划问题
  • 上完这门课我能学到什么

    • 对于面试中常见的动态规划题目能迅速判断并找到解题要领
    • 对于动态规划变种题能找到解题的突破口并轻松解决
    • 可以对动态规划算法进行时间和空间上的优化
    • 面试中将不再有你不会做的动态规划题

      1.7 课程安排

  • 第一讲:动态规划入门

  • 第二讲:坐标型动态规划
  • 第三讲:序列型动态规划
  • 第四讲:划分型动态规划
  • 第五讲:区间和背包型动态规划
  • 第六讲:双序列型动态规划
  • 第七讲:难题专场

    1.8 例题讲解

  • 求值问题

  • 金字塔问题

    1.9 暂时看不懂的

  • [x] 1.13 跳跃游戏

  • 1.17 乘积最大子序列

    1.10 题目解析

    669 · 换硬币

    解析

    求最大最小值动态规划

:::tips

  • 你有三种硬币,分别面值为2、5、7元,每种硬币都足够多
  • 买一本书需要27元
  • 如何用最少的硬币组合正好付清,不需要对方找钱 :::

    直觉

:::tips

  • 最少硬币组合—>尽量用面值大的硬币
  • 7+7+7+5=26 :::

    改变策略

:::tips

  • 尽量用大的硬币,最后如果可以用一种硬币付清就行
  • 7+7+7+2+2+2 = 27
  • 6枚硬币,应该对了吧? :::

    正确答案:7+5+5+5+5 = 27,五枚硬币

题目

:::info 给出不同面额的硬币以及一个总金额. 写一个方法来计算给出的总金额可以换取的最少的硬币数量. 如果已有硬币的任意组合均无法与总金额面额相等, 那么返回 -1. :::

👋 你可以假设每种硬币均有无数个
总金额不会超过10000
硬币的种类数不会超过500, 每种硬币的面额不会超过100

样例

Input:

[1, 2, 5] 11

Output: :::success 3 ::: Explain: :::warning 11 = 5 + 5 + 1 :::


Input:

[2] 3

Output: :::success -1 ::: Explain: :::warning 硬币数组中的值不满足和为amount = 3,故返回-1 :::


Input:

[1, 9] 0

Output: :::success 0 ::: Explain: :::warning 硬币数组中的值不满足和为amount = 3,故返回-1 :::

代码

  1. import (
  2. "math"
  3. )
  4. func CoinChange(coins []int, amount int) int {
  5. arr := make([]int, amount+1)
  6. n := len(coins)
  7. arr[0] = 0
  8. for i := 1; i <= amount; i++ {
  9. arr[i] = math.MaxInt
  10. //last coin
  11. //arr[i] = min{arr[i-coins[0]]+1, ... , f[i-coins[n-1]] + 1}
  12. for j := 0; j < n; j++ {
  13. //i 代表当前的钱数 当前钱数要 >= 硬币数组中的值
  14. if i >= coins[j] {
  15. arr[i] = min(arr[i-coins[j]]+1, arr[i])
  16. }
  17. }
  18. }
  19. if arr[amount] == 2<<31-1 {
  20. arr[amount] = -1
  21. }
  22. return arr[amount]
  23. }
  24. func min(x, y int) int {
  25. if x > y {
  26. return y
  27. }
  28. return x
  29. }

image.png
image.png

114 · 不同的路径

解析

计数型动态规划

:::tips

  • 给定m行n列的网格,有一个机器人从左下角(0,0)出发,每一步可以向下或者向右走一步
  • 问有多少种不同的方式走到右下角 :::

    确定状态-最后一步:

    • 无论机器人用何种方式到达右下角,总有最后挪动的一步:向右或向下
    • 右下角坐标设为(m-1,n-1)
    • 那么前一步机器人一定是在(m-2,n-1)或者(m-1,n-2)

image.png

确定状态-子问题:

  • 那么,如果机器人有X种方式从左上角走到(m-2,n-1),有Y种方式从左上角走到(m-1,n-2),则机器人有X+Y种方式走到(m-1,n-1)
  • 问题转化为,机器人有多少种方式从左上角走到(m-2,n-1)和(m-1,n-2)
  • 原题要求有多少种方式从左上角走到(m-1,n-1)
  • 子问题
  • 状态:设f[i][j]为机器人有多少种方式从左上角走到(i,j)

转移方程:

image.png

初始条件和边界情况:

  • 初始条件:f[0][0] = 1,因为机器人只有一种方式到左上角
  • 边界情况:i = 0j = 0,则前一步只能有一个方向过来—>f[i][j] = 1

image.png

计算顺序:

  • f[0][0] = 1
  • 计算第0行:f[0][0] f[0][1] ... f[0][n-1]
  • 计算第1行:f[1][0] f[1][1] ... f[1][n-1]
  • ...
  • 计算第m-1行:f[m-1][0] f[m-1][1] ... f[m-1][n-1]
  • 答案是f[m-1][n-1]
  • 时间复杂度(计算步数):O(MN),空间复杂度(数组大小):O(MN)

题目

:::info 有一个机器人的位于一个 m × n 个网格左上角。
机器人每一时刻只能向下或者向右移动一步。机器人试图达到网格的右下角。
问有多少条不同的路径? :::

👋 n和m均不超过100
且答案保证在32位整数可表示范围内。

样例

Input:

n = 1 m = 3

Output: :::success 1 ::: Explain: :::warning 只有一条通往目标位置的路径。 :::


Input:

n = 3 m = 3

Output: :::success 6 ::: Explain: :::warning D : Down
R : Right

  1. DDRR
  2. DRDR
  3. DRRD
  4. RRDD
  5. RDRD
  6. RDDR :::

    代码

  1. func UniquePaths(m int, n int) int {
  2. // write your code here
  3. f := make([][]int, m)
  4. for i := 0; i < len(f); i++ {
  5. f[i] = make([]int, n)
  6. }
  7. for i := 0; i < len(f); i++ {
  8. for j := 0; j < len(f[0]); j++ {
  9. if i == 0 || j == 0 {
  10. f[i][j] = 1
  11. } else {
  12. f[i][j] = f[i-1][j] + f[i][j-1]
  13. }
  14. }
  15. }
  16. return f[m-1][n-1]
  17. }

116 · 跳跃游戏

解析

存在型动态规划

:::tips

  • n块石头分别在x轴的0 1 ... n-1位置
  • 一只青蛙在石头0,想跳到石头n-1
  • 如果青蛙在第i块石头上,它最多可以向右跳距离第一章:动态规划入门 - 图30
  • 问青蛙能否跳到石头n-1 :::

    确定状态:

    • 最后一步:如果青蛙能跳到最后一块石头n-1,我们考虑它跳的最后一步
    • 这一步是从石头i跳过来的,i<n-1
    • 这需要两个条件同时满足:
      • 青蛙可以跳到石头i
      • 最后一步不超过跳跃的最大距离:n-1-i<= 第一章:动态规划入门 - 图31

image.png

转移方程:

  • f[j]表示青蛙能不能跳到石头j

image.png

初始条件和边界情况:

  • f[j]表示青蛙能不能跳到石头j
  • f[0] = True,因为青蛙一开始就在石头0

计算顺序:

  • f[j]表示青蛙能不能跳到石头j
  • 第一章:动态规划入门 - 图34
  • 初始化f[0] = True
  • 计算f[1] f[2] ... f[n-1]
  • 答案是f[n-1]
  • 时间复杂度:第一章:动态规划入门 - 图35,空间复杂度(数组大小):第一章:动态规划入门 - 图36

题目

:::info 给出一个非负整数数组,你最初定位在数组的第一个位置。
数组中的每个元素代表你在那个位置可以跳跃的最大长度。
判断你是否能到达数组的最后一个位置。 :::

👋 数组A的长度不超过5000,每个元素的大小不超过5000

样例

Input:

A = [2,3,1,1,4]

Output: :::success true ::: Explain: :::warning 0 -> 1 -> 4(这里的数字为下标)是一种合理的方案。 :::


Input:

A = [3,2,1,0,4]

Output: :::success false ::: Explain: :::warning 不存在任何方案能够到达终点 :::

代码

DP 动态规划

  1. func CanJump(a []int) bool {
  2. // write your code here
  3. n := len(a)
  4. f := make([]bool, n)
  5. f[0] = true
  6. for j := 1; j < n; j++ {
  7. f[j] = false
  8. for i := 0; i < j; i++ {
  9. if f[i] && i+a[i] >= j {
  10. f[j] = true
  11. break
  12. }
  13. }
  14. }
  15. return f[n-1]
  16. }

贪心

  1. func CanJump(a []int) bool {
  2. far := 0
  3. n := len(a) - 1
  4. for i := 0; i < len(a); i++ {
  5. if far < i {
  6. return false
  7. }
  8. far = max(i+a[i], far)
  9. if far >= n {
  10. return true
  11. }
  12. }
  13. return false
  14. }
  15. func max(x, y int) int {
  16. if x > y {
  17. return x
  18. }
  19. return y
  20. }

191 · 乘积最大子序列

解析

:::tips


  • :::

:::tips


  • :::

:::tips


  • :::

题目

:::info

:::

👋

样例

Input:

Output: :::success

::: Explain: :::warning

:::


Input:

Output: :::success

::: Explain: :::warning

:::


Input:

Output: :::success

::: Explain: :::warning

:::

代码