# 动态规划

dynamic programming(also know as dynamic optimization) 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 - ideally, using a memory-based data structure.

将原问题拆解成若干子问题,同时保存子问题的答案,使得每个子问题只求解一次,最终获得原问题的答案。

image-20220928213040237

动态规划解决的问题

动态规划问题的一般形式就是求最值。动态规划其实是运筹学的一种最优化方法,只不过在计算机问题上应用比较多,例如求最长递增子序列以及最小编辑距离等等。

动态规划的三要素

  1. 重叠子问题:当题目中存在重叠子问题时,利用暴力穷举的话效率会极其低下;
  2. 最优子结构:即通过子结构的最值来推导出原问题的最值;何为最优子结构,即每一个子结构之间都是独立的,例如通过已知每个班的最高分,求该年级的最高分;
  3. 状态转移方程:最为关键的部分,常常通过暴力解法来得到状态转移方程。

状态转移方程的思考思路

  • 明确 base case(边界条件)
    • 明确状态(通常是动态规划问题中会变换的那个变量)
      • 明确选择(即通过不同的选择来得到最优解)
        • 明确 dp 函数/数组的定义(即推到状态方程)

# Leetcode 70 Climbing Stairs

https://leetcode.cn/problems/climbing-stairs/

一次可以爬 1 个台阶,也可以爬 2 个台阶,也就是到 f(n) 有两种途径,那就是 f(n-1) 和 f(n-2),其实就是斐波那契数列。

image-20220928213405953

思路一:递归

func climbStairs(n int) int {
  if n == 0 {
    return 0
  }
  if n == 1 {
    return 1
  }
  if n == 2 {
    return 2
  }
  return climbStairs(n-1) + climbStairs(n-2)
}

很明显复杂度太高了,其中有过多的重复计算。

思路二:记忆化搜索

image-20220928213710571

加一个 memo []int 来存储 meno[n] 的值,这样对同一个 n,就不需要重复计算了,可以大大降低复杂度。

var memo []int

func climbStairs(n int) int {
    memo = make([]int, n + 1)
    return help(n)
}

func help(n int) int {

    if n == 0 {
        memo[n] = 0 
        return 0
    }
    if n == 1 {
        memo[n] = 1
        return 1
    }
    if n == 2 {
        memo[2] = 2
        return 2
    }
    
    // 如果已经计算过了,那就不需要再重复计算了
    if memo[n] == 0 {
        memo[n] = help(n-1) + help(n-2)
    }

    return memo[n]
}

思路三:动态规划

记忆化搜索其实是自上而下的思路,从 f(n) 出发,到 f(n-1) 和 f(n-2),一直到最后的 f(1) 和 f(0),再反推回去得到答案。

动态规划的思想跟这是相反的,动态规划是自下而上,也就是先从 f(0) 和 f(1) 开始,再到 f(2) 和 f(3),一直到 f(n)。

代码如下:

func climbStairs(n int) int {
    if n <= 2 {
        return n
    }
    memo := make([]int, n+1)
    memo[0] = 0
    memo[1] = 1
    memo[2] = 2
    for i:=3;i<=n; i++ {
        memo[i] = memo[i-1] + memo[i-2]
    }
    return memo[n]
}

# Leetcode 120 Trangle

https://leetcode.cn/problems/triangle/

  1. 明确边界条件

    每一步只能移动到下一行中相邻的结点上。相邻的结点 在这里指的是 下标 与 上一层结点下标 相同或者等于 上一层结点下标 + 1 的两个结点。也就是说,如果正位于当前行的下标 i ,那么下一步可以移动到下一行的下标 i 或 i + 1 。

  2. 明确状态

    memo[j][i] 表示 从 memo[j][i] 能找到的最小路径和。

  3. 明确选择(即通过不同的选择来得到最优解)

    从上层往下层选的两个相邻点的时候,要选那个小的。

  4. 明确 dp 函数/数组的定义(即推到状态方程)

    f(j,i) = [j,i] + min(f(j+1,i), f(j+1, i+1))

    memo[j][i] = triangle[j][i] + min(memo[j+1][i], memo[j+1][i+1])

// i: 横坐标
// j: 纵坐标
// f(j,i) = [j,i] + min(f(j+1,i), f(j+1, i+1))
func minimumTotal(triangle [][]int) int {
	level := len(triangle)

	if level == 1 {
		return triangle[0][0]
	}
	
  // memo[j][i] 表示从 memo[j][i] 到"底"能找到的最小路径和
	memo := make([][]int, len(triangle))
	for j := 0; j < len(memo); j++ {
		memo[j] = make([]int, len(triangle[j]))
	}

	// 最下面那一行是可以确定的
	for i := 0; i < len(memo[len(memo)-1]); i++ {
		memo[len(memo)-1][i] = triangle[len(memo)-1][i]
	}

	//  从下往上走
	for j := len(triangle) - 2; j >= 0; j-- {
		for i := len(triangle[j]) - 1; i >= 0; i-- {
			// 状态转移
			memo[j][i] = triangle[j][i] + int(math.Min(float64(memo[j+1][i]), float64(memo[j+1][i+1])))
		}
	}

	return memo[0][0]
}

# Leetcode 64 Minimum Path Sum

https://leetcode.cn/problems/minimum-path-sum/

上次更新: 9/29/2022, 11:38:36 PM