原理介绍
Dynamic Programming(DP):动态规划,是运筹学的一个分支,是求解决策过程最优化的数学方法。于1957年被Richard Bellman(理查德·贝尔曼)提出。其中的Programming不是编程的意思,而是指一种表格处理法,把每一步得到的子问题的结果存储在表格里,每次遇到子问题时不需要再求解一遍。其本质也是一种分治算法,其不同点在于分治算法将原问题分解为若干子问题,自顶向下求解各子问题,再合并子问题的解。动态规划也是把原问题分解为若干子问题,然后自底向上,先求解最小的子问题,把结果存储起来,求解大问题时直接查询小问题的解,避免了大量的重复计算,提高了算法效率。其本质为:递归+记忆化
问题条件
最优子结构
最优子结构性质是指问题的最优解包含其子问题的最优解。最优子结构是使用动态规划的最基本条件,如果问题不具有最优子结构性质,就不可以使用动态规划解决。
子问题重叠
子问题重叠是指再求解子问题的过程中,有大量的子问题是重复的,那么只需要求解依次,然后存储在表格中,以便使用时查询。这不是动态规划的必要条件,但是可以充分体现动态规划的优势。
算法步骤
(1)分析最优解的结构特征
(2)定义状态转移方程(递归式)
(3)自底向上计算最优解并记录
经典例题(0-1背包)
问题描述
假设山洞里有n个宝物,每种宝物有一定重量w和相应的价值v,背包的装载能力有限,只能运走重量为m的宝物,宝物不可以分割,如何使背包运走物品的价值最大?
第一行先输入宝物的数量n,和背包的承载重量m,然后每一行输出一个宝物对应的重量w和价值v(用空格分开)
1 | 5 10 # 宝物数n和背包能装载的重量m |
算法分析
和普通背包问题不同,如果盲目的装入当前单位重量价值最高的物品,可能导致背包剩余空间较大,达不到最优解。首先分析问题的结构特征,如果第i个宝物装入背包是问题的最优解,背包总重量$m-w_i$一定是问题$\lbrace a_1, a_2, \ldots, a_n \rbrace$的最优解。因此该问题具有最优子结构性质。
根据分析可知,判断第i个宝物装入重量为j的背包时会转化为两种可能,装入或者不装入,不放入时,问题变为前i-1个宝物装入重量为j的背包的最大价值。放入时,问题变为前i-1个宝物装入重量为j-wi的背包的最大价值加上第i个宝物的价值vi。即比较两者的最大值,用donkey[i][j]表示第i个宝物装入容量为j的背包里的最大价值。
$donkey[i][j] = \begin{cases} donkey[i-1][j], & j<w_i \ \max{donkey[i-1][j], donkey[i-1][j-w[i]]+v[i]}, & j \ge w_i \end{cases}$
0-1背包图解
python代码实战
1 | import sys |
代码运行结果
经典例题(最长公共序列)
问题描述
给定两个序列,如何找出最长公共子序列
第一行输入字符串s1,第二行输入字符串s2
1 | ABCADAB # 字符串s1 |
算法分析
首先分析问题是否具有最优子结构的性质,假设$Z_k={z_1,z_2,z_3,\ldots,z_k}$是$X_m={x_1,x_2,x_3,\ldots,x_m}$和$Y_n={y_1,y_2,y_3,\ldots,y_n}$的最长公共组序列,可以有三种情况讨论:
(1)当$z_k=x_m=y_n$时,则$Z_k={z_1,z_2,z_3,\ldots,z_{k-1}}$是$X_m={x_1,x_2,x_3,\ldots,x_{m-1}}$和$Y_n={y_1,y_2,y_3,\ldots,y_{n-1}}$的最长公共组序列。
(2)当$z_k \neq x_m,x_m \neq y_n$时,则$Z_k={z_1,z_2,z_3,\ldots,z_k}$是$X_m={x_1,x_2,x_3,\ldots,x_{m-1}}$和$Y_n={y_1,y_2,y_3,\ldots,y_n}$的最长公共组序列。
(3)当$z_k \neq y_n,x_m \neq y_n$时,则$Z_k={z_1,z_2,z_3,\ldots,z_k}$是$X_m={x_1,x_2,x_3,\ldots,x_m}$和$Y_n={y_1,y_2,y_3,\ldots,y_{n-1}}$的最长公共组序列。
因此问题满足最优子结构性质,用char[i][j]表示Xi和Yj的最长公共子序列长度。
$char[i][j] = \begin{cases} char[i-1][j-1]+1, & x_i=y_j \ \max{char[i][j-1], char[i-1][j]}, & x_i \neq y_j \end{cases}$
公共子序列图解
python代码实战
1 | import sys |
代码运行结果
经典例题(字符串编辑距离)
问题描述
给定两个序列,如何使用最少的增加字符,删除字符,替换字符操作,使两个序列相同?
第一行输入字符串str1,第二行输入字符串str2
1 | family # 字符串str1 |
算法分析
分析此题是否具有最优子结构性质,假设char[i][j]是$X_i={x_1,x_2,x_3,\ldots,x_i}$和$Y_j={y_1,y_2,y_3,\ldots,y_j}$的编辑距离的最优解,可以有两种情况讨论:
(1)当两个字符串满足$x_i=y_j$时,则char[i-1][j-1]是$X_m={x_1,x_2,x_3,\ldots,x_i}$和$Y_n={y_1,y_2,y_3,\ldots,y_j}$的编辑距离的最优解。
(2)当两个字符串满足$x_i \neq y_j$时,则可以删除字符xi或删除字符yj或将字符xi替换为字符yj,即满足下列条件$\max(char[i-1][j],char[i][j-1],char[i-1][j-1])+1$是$X_m={x_1,x_2,x_3,\ldots,x_i}$和$Y_n={y_1,y_2,y_3,\ldots,y_j}$的编辑距离的最优解。
因此问题满足最优子结构性质,写出其状态转移方程。
$char[i][j] = \begin{cases} char[i-1][j-1], & x_i=y_j \ \max{char[i][j-1], char[i-1][j],char[i-1][j-1]}+1, & x_i \neq y_j \end{cases}$
字符串距离图解
python代码实战
1 | import sys |
代码运行结果
算法总结
由于动态规划不局限于眼前的最优状态,而是记录了以前的所有状态,因此动态规划具有很强的大局观,可以较为容易地得到全局最优解,因此在实际的生产生活中使用较广。动态规划的关键是分析问题是否具有最优子结构,如果问题具有该性质,说明可以使用动态规划来解决问题。然后是找到其状态转移方程,这也是最难考虑的一个问题。得到了状态转移方程,此问题迎刃而解。