Skip to content

Dynamic Programming

什么是动态规划,如何分析、求解动态规划问题

动态规划就是把一个大问题一步步拆分成越来越小的子问题,直到子问题小到可以用确定的条件来解答

动态规划的核心:如何拆分问题

1、状态的定义

给定一个长度为 n 的数列,求最长的上升子数列(LIS)的长度

如 1,5,3,4,6,9,7,8 的 LIS 为 1,3,4,6,7,8,长度为 6

这个问题在字面上看,找不出子问题

所以需要我们重新定义这个问题: 给定一个长度为 n 的数列 nums,设 dp[i] (状态) 是以 nums[i] 结尾的最长递增子序列的长度 (状态的定义) 求 dp 中最大值

2、状态转移方程的定义

定义好状态之后,状态与状态之间的关系式,叫做状态转移方程

dp[1]: 1

dp[i]: 所有 j < i & nums[j] < nums[i] 的 dp[j] + 1 最大值

这里表达了子问题与更小子问题之间的关系,状态转移方程就是带有条件的递推式

js
var lis = (nums) => {
  const dp = [] // 以 nums[i] 结尾上升子序列长度
  const len = nums.length
  for (let i = 0; i < len; i++) {
    dp[i] = 1
    for (let j = 0; j < i; j++) {
      if (nums[j] < nums[i]) {
        dp[i] = Math.max(dp[i], dp[j] + 1)
      }
    }
  }
  return Math.max(...dp)
}

// lis([4, 10, 4, 3, 8, 9])

技巧

斐波那契数列 [0, 1, 1, 2, 3, 5, 8]

Top-down with Memoization

js
const calcFib = (n) => {
  const memo = []
  const fib = (n) => {
    if (n < 2) return n
    if (memo[n]) return memo[n]
    memo[n] = fib(n - 1) + fib(n - 2)
    return memo[n]
  }
  return fib(n)
}

Bottom-up with Tabulation

从基元问题一步步解决所有相关的子问题,这通常是通过填充 n 维表来完成,然后根据表中的结果计算原始问题的解

js
const fib = (n) => {
  const dp = [0, 1]
  for (let i = 2; i <= n; i++) {
    dp[i] = dp[i - 1] + dp[i - 2]
  }
  return dp[n]
}

例子

有 1 元、3 元、5 元面值的硬币若干,要凑到 11 元需要最少几个硬币?

凑 i 元需要 n 个硬币为: dp[i] = n

js
const calcCoin = (n) => {
  const dp = [0]
  for (let i = 1; i <= n; i++) {
    dp[i] = 1 + Math.min(dp[i - 1], dp[i - 3] || Infinity, dp[i - 5] || Infinity)
  }
  return dp[n]
}

假设你正在爬楼梯。需要 n 阶你才能到达楼顶 每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?

js
var step = (n) => {
  const dp = [1, 1]
  if (n < 2) return dp[n]
  for (let i = 2; i <= n; i++) {
    dp[i] = dp[i - 1] + dp[i - 2]
  }
  return dp[n]
}

最长公共子序列问题 longestCommonSubsequence LCS

js
var lcs = (s1, s2) => {
  const table = []
  for (let i = 0; i < s1.length; i++) {
    if (!table[i - 1]) table[i - 1] = []
    if (!table[i]) table[i] = []
    for (let j = 0; j < s2.length; j++) {
      if (s1[i] === s2[j]) {
        table[i][j] = (table[i - 1][j - 1] || 0) + 1
      } else {
        table[i][j] = Math.max(table[i - 1][j] || 0, table[i][j - 1] || 0)
      }
    }
  }
  return table[s1.length - 1][s2.length - 1]
}

交通费问题

js
var prevMap = {
  S: [],
  A: [{ node: 'S', cost: 10 }],
  B: [{ node: 'S', cost: 20 }],
  C: [{ node: 'A', cost: 30 }],
  D: [
    { node: 'A', cost: 10 },
    { node: 'B', cost: 20 },
    { node: 'C', cost: 5 },
  ],
  T: [
    { node: 'C', cost: 20 },
    { node: 'D', cost: 10 },
  ],
}

const getCost = (node) => {
  const prev = prevMap[node]
  if (!prev.length) {
    return 0
  }
  return Math.min(...prev.map((x) => getCost(x.node) + x.cost))
}

getCost('T')