动态规划是一种常用的优化技术,本文介绍动态规划基本原理及常见案例。
一、什么是动态规划
动态规划是一种在数学、计算机科学和经济学中使用的,通过把原问题分解为相对简单的子问题的方式求解复杂问题的方法。动态规划算法是通过拆分问题,定义问题状态和状态之间的关系(即状态转移方程或递推公式),使得问题能够以递推的方式去解决。动态规划常常适用于有重叠子问题和最优子结构性质的问题。
- 重叠子问题指的是在求解过程中,同一个子问题可能会被多次遇到并求解;
- 最优子结构指的是如果问题的最优解所包含的子问题的解也是最优的,那么这个问题就具有最优子结构性质。
动态规划算法可以分为两种基本形式:
- 自顶向下(Top-down)。
自顶向下指的是从原始问题出发,逐步将其分解为较小的子问题,并将已经求解过的子问题结果保存起来以便重复使用;
- 自底向上(Bottom-up)。
自底向上指的是从最小规模的子问题开始求解,并逐步扩大到原始问题。
二、动态规划的原理
动态规划通常遵循以下四个步骤:
- 定义状态:
状态是指描述问题的某个方面或特征的变量,例如最优解、最大值、最小值等。状态可以是一个数值、一个向量、一个矩阵等不同形式。定义状态时要注意尽量使得状态之间具有明确的联系,并且能够覆盖所有可能的情况。
- 定义状态转移方程:
状态转移方程是指描述不同状态之间如何转换(或者说递推)的公式或者规则。状态转移方程通常根据问题本身的逻辑和性质来确定,有时也可以借助数学归纳法来推导。
- 确定边界条件:
边界条件是指确定初始状态和终止状态,以及它们对应的值。边界条件通常与问题本身给出的条件或限制有关,也可以根据常识或经验来设定。
- 计算并填充表格:
根据已知的边界条件和状态转移方程,按照一定顺序(自顶向下或自底向上)计算并填充表格中每个位置对应的值。表格中最后一个位置(或者说某个特定位置)就是原始问题的解。动态规划算法在实现时通常需要考虑以下几个方面:
- 确定表格(数组)大小和结构:根据定义好的状态和边界条件来确定表格(数组)需要多大空间以及如何组织数据。
- 确定计算顺序和方式:根据定义好的状态转移方程来确定计算每个位置值时需要依赖哪些其他位置,并选择合适的循环结构和索引方式。
- 优化空间复杂度:在一些情况下,可以通过只保存部分数据或使用滚动数组等技巧来减少表格(数组)所占用的空间。
- 优化时间复杂度:在一些情况下,可以通过预处理数据、剪枝无效分支、使用哈希表等技巧来减少计算次数或加速查找过程。
三、动态规划的案例
动态规划算法可以应用于很多领域和问题,例如数学、计算机科学、工程、管理、生物等。下面给出几个常见的动态规划案例,并用python代码实现。
1.斐波那契数列
斐波那契数列是一个非常经典的数学问题,它定义如下:F(0) = 0, F(1) = 1, F(n) = F(n-1) + F(n-2), n >= 2. 即从第三项开始,每一项都等于前两项之和。求解斐波那契数列的第n项的值。
# 定义状态:dp[i]表示斐波那契数列的第i项
# 定义状态转移方程:dp[i] = dp[i-1] + dp[i-2]
# 确定边界条件:dp[0] = 0, dp[1] = 1
# 计算并填充表格(数组):从小到大依次计算并填充dp数组
def fib(n):
# 创建一个长度为n+1的数组
dp = [0] * (n + 1)
# 初始化边界条件
dp[0] = 0
dp[1] = 1
# 自底向上计算并填充数组
for i in range(2, n + 1):
# 根据状态转移方程计算当前位置的值
dp[i] = dp[i - 1] + dp[i - 2]
# 返回原始问题的解,即数组最后一个位置的值
return dp[n]
# 测试代码
print(fib(10)) # 输出55
2.最长公共子序列
最长公共子序列是一个非常经典的计算机科学问题,它定义如下:给定两个字符串s1和s2,求解它们的最长公共子序列的长度。一个字符串的子序列是指在该字符串中删除若干个字符(也可以不删除)后得到的新字符串。例如,"abc"是"abdc"的一个子序列,但不是"abd"的子序列。
# 定义状态:dp[i][j]表示s1[0...i-1]和s2[0...j-1]的最长公共子序列的长度
# 定义状态转移方程:dp[i][j] = dp[i-1][j-1] + 1, 如果s1[i-1] == s2[j-1]
# dp[i][j] = max(dp[i-1][j], dp[i][j-1]), 如果s1[i-1] != s2[j-1]
# 确定边界条件:dp[0][j] = 0, dp[i][0] = 0
# 计算并填充表格(矩阵):从小到大依次计算并填充dp矩阵
def lcs(s1, s2):
# 获取两个字符串的长度
m = len(s1)
n = len(s2)
# 创建一个(m+1) * (n+1)大小的矩阵
dp = [[0 for _ in range(n + 1)] for _ in range(m + 1)]
# 初始化边界条件
for i in range(m + 1):
dp[i][0] = 0
for j in range(n + 1):
dp[0][j] = 0
# 自底向上计算并填充矩阵
for i in range(1, m + 1):
for j in range(1, n + 1):
# 根据状态转移方程计算当前位置的值
if s1[i - 1] == s2[j - 1]:
dp[i][j] = dp[i - 1][j - 1]+1
else:
dp[i][j] = max(dp[i -1 ][j], dp[i][j -1 ])
# 返回原始问题的解,即矩阵右下角位置的值
return dp[m][n]
# 测试代码
print(lcs("abcde", "ace")) # 输出3
3.背包问题给定一组物品,每个物品有重量w和价值v,以及一个背包容量c。在不超过背包容量的前提下,选择一些物品放入背包,使得背包中物品的总价值最大,并返回其最大价值。
def knapsack(w,v,c):
# 初始化状态数组
dp = [[0] * (c + !) for _ in range(len(w) + !)]
# 状态转移方程
for i in range(!,len(w) + !):
for j in range(!,c + !):
if j < w[i-!]:
dp[i][j] = dp[i-!][j]
else:
dp[i][j] = max(dp[i-!][j],dp[i-!][j-w[i-!]]+v[i-!])
# 返回结果
return dp[-!][-!]
动态规划其实应用极其广泛,本文仅介绍了一些基础常见的例子。动态规划在数学、计算机科学、经济学和生物信息学等领域都有广泛的应用。
- 在计算机科学中,动态规划可以用来求解字符串匹配、图论、组合优化、密码学等方面的问题;
- 在经济学中,动态规划可以用来求解生产计划、资源分配、投资决策等方面的问题;
- 在生物信息学中,动态规划可以用来求解序列比对、基因寻找、蛋白质折叠等方面的问题。