动态规划算法的基本要素

动态规划算法的基本要素

https://www.cnblogs.com/mfrank/p/10533701.html

动态规划的解题步骤:

  • 确定状态i

  • 状态转移方程(原问题和其子问题的递归式)

  • 边界

  • 计算顺序(一般从边界出发自底向上)

    一般走一个数组

使用动态规划的前提:

  • 最优子结构(Optimal substructure)
  • 重叠子问题(Overlapping subproblems)
  • 要满足状态的无后效性【比如计算dp计算出来了结果就不能修改了】

动态规划和分治的区别就是分治有重叠子问题,动态规划没有重叠子问题

最优子结构

定义1 (最优子结构).如果一个问题的最优解包含了它的子问题的最优解, 则称此问题具有最优子结构.

  • 应用动态规划和贪心(Greedy)方法的条件
  • 是否满足重叠子问题条件

重叠子问题

定义2 (重叠子问题).如果递归算法求解一个优化问题时, 反复求解相同的子问题, 则称该优化问题有重叠子问题.
动态规划与分治法的区别:

  • 动态规划只计算每个子问题一次并保存, 避免反复计
    算相同子问题多次
  • 分治法每次都产生新问题并计算, 而不管是否该问题
    已计算过

备忘录方法-memoization

  • 对函数返回值进行缓存(一种计算机程序优化技术)
  • 备忘录方法用表格保存已解决的子问题的答案, 在下
    次需要解此问题时, 只要简单地查看该子问题的解答,
    而不必重新计算
  • 备忘录方法的递归方式是自顶向下, 而动态规划算法
    则是自底向上

动态规划与备忘录方法

  • 动态规划. 一个问题的所有子问题都至少要解一次
  • 备忘录方法. 部分子问题可不必求解

题目1. 矩阵连乘问题

首先了解什么是矩阵连乘问题:通过使用合适的加括号的方式,使得最后矩阵连乘积的乘法次数最少

完整题目:

给定n个矩阵{A1,A2,…,An},其中Ai与Ai+1是可乘的,i=1,2 ,…,n-1。如何确定计算矩阵连乘积的计算次序,使得依此次序计算矩阵连乘积需要的数乘次数最少。

由于矩阵乘法满足结合律,所以计算矩阵的连乘可以有许多不同的计算次序。这种计算次序可以用加括号的方式来确定。

若一个矩阵连乘积的计算次序完全确定,也就是说该连乘积已完全加括号,则可以依此次序反复调用2个矩阵相乘的标准算法计算出矩阵连乘积

  • 对于 p × q 矩阵 Aq × r 矩阵 BA × B 需要多少次乘法计算?p × q × r 次乘法计算
  • 例如, AB 分别是 20 × 100, 100 × 10 矩阵, 则乘法总数为 20 × 100 × 10 = 20000

图片描述

再来个例子~

图片描述

通过上面的例子可以看出加括号的顺序对运算次数的影响之大,所以我们的任务就是为了找到一个最优的加括号的顺序,来使得最后的乘法次数最少

接下来开始入手,我们先将问题一般化

图片描述

下面我们按照算法导论中的“动态规划四部曲”来一步一步的分析

  • 分析最优解的结构特征

    特征:如果A[i:j]是最优次序,那么它所包含的计算矩阵子链A[i:k]和A[k+1:j]的次序也是最优的

满足最优子结构性质:最优解包含着其子问题的最优解

  • 建立递归关系

图片描述

  • 计算最优解的值通常采用自底向上

图片描述

图片描述

动态规划实现:

图片描述

题目2. 最长公共子序列(LCS)

最长公共子序列 (Longest Common Subsequence, LCS) 的问题描述为:

给定两个字符串(或数字序列) A和B, 求二个字符串, 使得这个字符串是A和B的最长公共部分(子序列可以不连续)。

样例:

3-2

如样例所示, 字符串 "sadstory"与 "adminsorry" 的最长公共子序列为 "adsory", 长度 为6。

直接来看动态规划的做法(下文的 LCS 均指最长公共子序列)。

1. 确定状态

\(dp[i][j]\)表示字符串 A 的 i号位和字符串 B的j号位之前的 LCS 长度(下标从 1 开始), 如 \(dp[4][5]\)表示 ''sads" 与 "admin" 的 LCS 长度。

那么可以根据 A[i]和 B[j]的情况, 分为两 种决策:

  1. 若 A[i] == B[j], 则字符串 A与字符串 B 的 LCS 增加了 1 位,即有 \(dp[i][j] = dp[i- 1][j - 1] + 1\)。 例如, 样例中 \(dp[4][6]\)表示 "sads" 与 "admins" 的 LCS 长度, 比较 A[4]与 B[6],发现两者都是's', 因此 \(dp[4][6]\)就等千 \(dp[3][5]\)加1, 即为 3

  2. 若 A[i] != B[j], 则字符串 A的 i号位和字符串 B 的j号位之前的 LCS 无法延长, 因此 \(dp[i][j]\)将会继承 \(dp[i-1][j]\)与$ dp[i][j- 1]\(中的较大值, 即有\) dp[i][j]= max { dp[i - 1 ][j], dp[i][j -1]} $。

    例如, 样例中$ dp[3][3]$表示 "sad" 与 "adm" 的 LCS 长度, 我们比较 A[3]与 B[3], 发现'd'不等千m', 这样 \(dp[3][3]\)无法再原先的基础上延长, 因此继承自 "sa" 与 "adm" 的 LCS, "sad" 与 "ad" 的 LCS 中的较大值, 即 "sad" 与 "ad" 的 LCS 长度 -2。

2. 由此可以得到状态转移方程:

\[\Large dp[i][j] \begin{cases} dp[i-1][j-1]+1 &\text{,A[i] == b[j]}\\[2ex] max\{dp[i-1][j],dp[i][j-1]\}&\text{,A[i] j != B[j]} \end{cases} \]

3. 边界,

\[\Large dp[i][0] = dp[0][j] = 0 (0\leq i \leq n,0\leq j \leq m) \]

这样状态 \(dp[i][j]\)只与其之前的状态有关, 由边界出发就可以得到整个 dp 数组, 最终 \(dp[n][m]\)就是需要的答案, 时间复杂度为 \(O(nm)\)

题目3. 最长不下降子序列(LIS)

LIS的各种变形写法

具体来说就是从一个数的的大小比较变成了一个区间的大小比较

yysy,想法真的骚!

leetcode300. 最长上升子序列

最长不下降子序列 (Longest Increasing Sequence, LIS) 是这样一个问题:

在一个数字序列中,找到一个最长的子序列(可以不连续),使得这个子序列是不下降(非递减)的。

例如, 现有序列 A={12,3,-1,-2,7,9} (下标从 1 开始), 它的最长不下降子序列是{1, 2, 3, 7, 9}, 长度为 5。另外, 还有一些子序列是不下降子序列, 比如{l,2, 3}、 {-2,7, 9}等,但都不是最长的。

1. 确定状态

令 dp[i]表示以 A[i]结尾的最长不下降子序列长度(和最大连续子序列和问题一样,以 A[i]结尾是强制的要求)。 这样对 A[i]来说就会有两种可能:

  • 如果存在 A[i]之前的元素 A[j] (j < i), 使得 A[j]\(\leq\)A[i]且 dp[j] + 1\(>\) dp[i] (即把 A[i]跟在以 A[j]结尾的 LIS 后面时能比当前以 A[i]结尾的 LIS 长度更长), 那么就把A[i]跟在以 A[j]结尾的 LIS 后面, 形成一条更长的不下降子序列(令 dp[i]= dp[j] + 1)。
  • 如果 A[i]之前的元素都比 A[i]大, 那么 A[i]就只好自已形成一条 LIS, 但是长度为1, 即这个子序列里面只有一个 A[i]。

2. 由此可以得到状态转移方程:

\[\Large dp[i] = max\{1,dp[j] + 1\}\\[2ex] \Large \text{(j = 1,2, …,i-1 & &A[j] < A[i])} \]

3. 边界,

\[\large边界: dp[i]=1 \large (1\leq i \leq n)\Large \\[2ex] \]

题目4. 0/1背包问题

有 n 件物品, 每件物品的重量为 w[i], 价值为 c[i]。现有一个容量为 V 的背包, 问如何选取物品放入背包, 使得背包内物品的总价值最大。 其中每种物品都只有1件。

令$ dp[i][v]$表示前 i 件物品$1\leq i\leq n,0\leq v \leq V $ )恰好装入容量为 v 的背包中所能获得的最大 价值。 怎么求解 \(dp[i][v]\)呢?
考虑对第i件物品的选择策略, 有两种策略:

  1. 不放第i件物品,那么问题转化为前i- 1件物品恰好装入容量为v的背包中所能获得
    的最大价值, 也即 \(dp[i- 1][v]\)
  2. 放第 i 件物品, 那么问题转化为前 i- 1 件物品恰好装入容量为V - w[i]的背包中所能获得的最大价值, 也即 \(dp[i - 1 ][ v - w[i]] + c[i]\)

由于只有这两种策略, 且要求获得最大价值, 因此,

\[\large dp[i][v] = \begin{cases} dp[i- 1][ v ]&\text{,}1 \leq i \leq n, 0\leq v < w[i] \\[2ex] max\{dp[i- 1][ v ],dp[i-1][ v-w[i]] + c[i]\}&\text{,}1 \leq i \leq n, w[i] \leq v \leq V \\[2ex] \end{cases} \]

\[\large dp[i][v] = \begin{cases} dp[i- 1][ v ]&\text{,}1 \leq i \leq n, 0\leq v < w[i] 是指背包放不下所选物品的情况\\[2ex] \end{cases} \]

上面这个就是状态转移方程。 注意到$ dp[i][v]$只与之前的状态 \(dp[i - 1][ ]\)有关, 所以可以枚举 i 从 1 到 n, v从 0 到V, 通过边界$ dp[0][v] = 0 (O \leq v \leq V) \((即前 0 件物品放入任何容量v 的背包中都只能获得价值0)就可以把整个 dp 数组递推出来。而由于\) dp[i][v]$表示的是恰好 为 v 的情况, 所以需要枚举 \(dp[n][v] (O \leq v \leq V),\) 取其最大值才是最后的结果。

$ dp[i][v]$只与之前的状态 \(dp[i - 1][ ]\)有关

i:1->n

v:0->V

部分代码(伪代码):


for(int i=1,i<=n,i++){
    for(int v_1 = 0, v_1<w[i], v_1++){
        dp[i][v_1] = dp[i-1][v_1]
    }
    for(int v_2 = w[i],v_2<=V, v_2++){
        dp[i][v_2]=max(dp[i-1][v_2],dp[i-1][v_2-w[i]] + c[i],
    }           
}

实例,

3-3
package main

import (
	"fmt"
	"math"
)

func main(){
	n ,= 5
	V ,= 8
	// 物品从第1个开始计算
	w ,= []int{0,3,5,1,2,2}
	c ,= []int{0,4,5,2,1,3}
	// dp的第0行代表前0个物品放入容量为v(v:0...V)的背包中所能得到的最优解,易知必为0
	// dp的第0列表示当背包容量为0时前i(i:0...n)个物品放入背包所能得到的最优解
	dp ,= [5+1][8+1]int{}
	// 初始化边界,第0轮,因为前0个就算你有空间你也没东西拿所以一定为0
	for i:=0,i<=V,i++{
		dp[0][i] = 0
	}
	// 动态规划,代表只考虑前i个物品的情况(物品从第1个开始计算)
	for i:=1,i<=n,i++{
		// 第i个物品放不下(没有空间的情况下)的情况
		for v1 ,= 0,v1<w[i],v1++{
			dp[i][v1] = dp[i-1][v1]
		}
		//第i个物品放得下,可以选择放或者不放
		for v2 ,= w[i],v2<=V,v2++{
			dp[i][v2] = int(math.Max(float64(dp[i-1][v2]),float64(dp[i-1][v2-w[i]]+c[i])))
		}
	}
	for i:=0,i<=n,i++{
		fmt.Println(dp[i])
	}

	fmt.Println(dp[n][V])

}

输出结果:

[0 0 0 0 0 0 0 0 0]
[0 0 0 4 4 4 4 4 4]
[0 0 0 4 4 5 5 5 9]
[0 2 2 4 6 6 7 7 9]
[0 2 2 4 6 6 7 7 9]
[0 2 3 5 6 7 9 9 10]
10

动态规划是如何避免重复计算的问题在01背包问题中非常明显。在一开始暴力枚举每件物品放或者不放入背包时,其实忽略了一个特性:第i件物品放或者不放而产生的最大值是完全可以由前面i-1件物品的最大值来决定的,而暴力做法无视了这一点。

另外,01背包中的每个物品都可以看作一个阶段,这个阶段中的状态有\(dp[i][0]--> dp[i][V]\), 它们均由上一个阶段的状态得到。 事实上,对能够划分阶段的问题来说, 都可以尝试把阶段作为状态的一维, 这可以使我们更方便地得到满足无后效性的状态。 从中也可以得到这么一个技巧如果当前设计的状态不满足无后效性, 那么不妨把状态进行升维, 即增加一维或若干维来表示相应的信息, 这样可能就能满足无后效性了。

题目5. 完全背包问题

完全背包问题的叙述如下:

有 n 种物品, 每种物品的单件重量为w[i], 价值为 c[i]。现有 个容量为 V 的背包, 问如何选取物品放入背包,使得背包内物品的总价值最大。 其中每种物品都有无穷件。

可以看出,完全背包问题和 01 背包问题的唯 区别就在于:完全背包的物品数量每种有无穷件,选取物品时对同一种物品可以选1件、选2件...…只要不超过容量V即可,而01背包的物品数量每种只有1件。

同样令 \(dp[i][v]\) 表示前 i 件物品恰好放入容量为 v 的背包中能获得的最大价值。

01背包一样,完全背包问题的每种物品都有两种策略,但是也有不同点。对第 i 件物品来说:

  1. 不放第 i 件物品,那么$ dp[i][v] = dp[i-1][v]$,这步跟01背包是一样的。
  2. 放第 i 件物品。这里的处理和01背包有所不同,因为01背包的每个物品只能选择一个,因此选择放第 i 件物品就意味着必须转移到$ dp[i-1][v-w[i]] \(这个状态;但是完全背包不同,完全背包如果选择放第 i 件物品之后并不是转移到\) dp[i-1][v- w[i]]\(,而是转移到\) dp[i][v-w[i]]$,这是因为每种物品可以放任意件 (注意有容量的限制,因此还是有限的),放了第 i 件物品后还可以继续放第 i 件物品,直到第二维的 $v-w[i] $无法保持大于等于0为止。

由上面的分析可以写出状态转移方程:

\[\large dp[i][v] = \begin{cases} dp[i- 1][ v ]&\text{,}1 \leq i \leq n, 0\leq v < w[i] \\[2ex] max\{dp[i- 1][ v ],dp[i][ v-w[i]] + c[i]\}&\text{,}1 \leq i \leq n, w[i] \leq v \leq V \\[2ex] \end{cases} \]

\[\large dp[i][v] = \begin{cases} dp[i- 1][ v ]&\text{,}1 \leq i \leq n, 0\leq v < w[i] 是指背包放不下所选物品的情况\\[2ex] \end{cases} \]

边界状态(同0/1背包问题,前0个无论背包大小,dp均为0)

边界: $dp[0][v] = 0 (0\leq v < w[i]) $

题目6. 最优二叉搜索树

二叉搜索树的定义

二叉搜索树(Binary Search Tree), 或者是一棵空树, 或者是具有下列性质的二叉树,

  • 若它的左子树不空, 则左子树上所有结点的值均小于
    它的根结点的值
  • 若它的右子树不空, 则右子树上所有结点的值均大于
    它的根结点的值
  • 它的左、右子树也分别为二叉搜索树

递归式的推导

  • 设a1, a2, . . . , an 是从小到大排列的互不相等的键(二叉搜索树的节点),p1, . . . , pn 是它们的查找概率
  • \(T_j^i\)是由键ai, ai+1, . . . , aj 构成的二叉树,
  • c[i, j] 是在这棵树中成功查找的最小的平均比较次数,
  • 其中\(1 \leq i \leq j \leq n\)

最优子结构性质

从键ai, ai+1, . . . , aj 中选择一个根ak 构造一棵二叉树,它的根包含键\(a_k\),
(1) 它的左子树\(T_i^{k-1}\)中的键$a_i, a_{i+1}, . . . , a_{k-1} \(是最优排列 (2) 它的右子树\)T_{k+1}^j\(中的键\)a_{k+1}, . . . , a_j $也是最优排列.

3-4

看ppt(123页)可得到推导式:

\[c[i,j]=min_{i \leq k \leq j}\{ c[i,k-1]+c[k+1,j]\}+\sum_{s=i}^jp_s \\[2ex] (1 \leq i \leq j \leq n) \]

两个特殊位置:(初始化条件,画表格用的)

  1. \(c[i][i-1]=0\)
  2. \(c[i][i]=p_i\)

ppt上有例题

image-20201223103019441

image-20201223103029535

image-20201223103037880

image-20201223103048057

image-20201223103057196

image-20201223103108147

image-20201223103119203

image-20201223103126379

image-20201223103134800

r表示c[i,j]达到最小的时候的k值

image-20201223103149461

题目7. 编辑距离

给你两个单词 word1word2,请你计算出将 word1 转换成 word2 所使用的最少操作数 。

你可以对一个单词进行如下三种操作:

  • 插入一个字符
  • 删除一个字符
  • 替换一个字符

示例 1:

输入:word1 = "horse", word2 = "ros"
输出:3
解释:
horse -> rorse (将 'h' 替换为 'r')
rorse -> rose (删除 'r')
rose -> ros (删除 'e')

示例 2:

输入:word1 = "intention", word2 = "execution"
输出:5
解释:
intention -> inention (删除 't')
inention -> enention (将 'i' 替换为 'e')
enention -> exention (将 'n' 替换为 'x')
exention -> exection (将 'n' 替换为 'c')
exection -> execution (插入 'u')

思路

  1. 动态规划

  2. 定义 dp[i][j]

    • dp[i][j] 代表 word1 中前 i 个字符,变换到 word2 中前 j 个字符,最短需要操作的次数

    目的:word1\(\rightarrow\)word2

    i:word1中的前i个字符

    j:word2中的前j个字符

    \(dp[i][j]\):将word1 中前 i 个字符,变换到 word2 中前 j 个字符需要的最短操作数

    • 初始化条件:需要考虑 word1word2 一个字母都没有,即全增加/删除的情况,所以预留 \(dp[0][j] 和 dp[i][0]\)

      \(dp[0][j]=j\)

      \(dp[i][0]=i\)

  3. 状态转移

    • \(dp[i][j] = dp[i][j - 1] + 1\)

      先把word1的前i个变成word2的前j-1个,+1代表最后一个增加字符的操作

    • \(dp[i][j] = dp[i - 1][j] + 1\)

      先把word1的前i-1个变成word2的前j个,+1代表删除word1中的第i个元素的操作

    • \(dp[i][j] = dp[i - 1][j - 1] + 1\)

      先把word1的前i-1个变成word2的前j-1个,+1代表把word1的第i个元素修改为word2的第j个元素的操作

    • 不变\(dp[i][j]=dp[i-1][j-1]\)

      如果刚好这两个字母相同 \(word1[i - 1] = word2[j - 1]\)

      那么可以直接参考 \(dp[i - 1][j -1]\) ,操作不用加一

\[dp[i][j]= \begin{cases} dp[i-1][j-1]&\text{,word1[i] = word2[j]}\\[2ex] min(dp[i-1][j-1], dp[i-1][j], dp[i][j-1]) + 1&\text{word1[i]} \neq \text{word2[j]}\\[2ex] \end{cases} \]

配合增删改这三种操作,需要对应的 dp 把操作次数加一,取三种的最小

算法的时间复杂度:O(mn)

m,n分别为i,j的长度

image-20201224095217928

按顺序计算,当计算 dp[i][j] 时,dp[i - 1][j]dp[i][j - 1]dp[i - 1][j - 1]均已经确定了

posted @ 2019-08-19 18:05  TR_Goldfish  阅读(1805)  评论(0编辑  收藏  举报