动态规划算法是一种解决最优化问题的算法,其主要思想是将原问题分解成若干个子问题,通过保存子问题的解来避免重复计算,从而得到原问题的最优解。动态规划算法通常采用自底向上的方式求解,即先求解子问题,然后根据子问题的解求解更大规模的问题,直到求解原问题。
动态规划算法的基本步骤如下:
1.定义状态:将原问题分解成若干个子问题,定义状态表示子问题的解。
2.定义状态转移方程:根据子问题的解推导出更大规模的问题的解,即定义状态转移方程。
3.定义边界条件:确定最小子问题的解。
4.计算顺序:按照计算顺序计算子问题的解,最终求解原问题。
以下是一些例子,大家不懂的时候可以一步步把dp数组画出来,会比较直观。
(1)最长上升子序列
最长上升子序列问题是指:给定一个序列,找出其中最长的上升子序列。例如,序列[10, 9, 2, 5, 3, 7, 101, 18]的最长上升子序列为[2, 5, 7, 101],长度为4。
动态规划算法的实现方法如下:
1.定义状态:设dp[i]表示以第i个元素为结尾的最长上升子序列的长度。
2.定义状态转移方程:对于第i个元素,如果它前面有元素j比它小且dp[j]的值大于等于dp[i],则dp[i] = dp[j] + 1;否则,dp[i] = 1。
3.定义边界条件:dp[0] = 1。
4.计算顺序:按照从小到大的顺序计算dp[i]的值,最终得到dp中的最大值即为最长上升子序列的长度。
def length1(nums):
n=len(nums)
dp=[1]*n
for i in range(1,n):
if nums[i]>nums[i-1]:
dp[i]=dp[i-1]+1
return max(dp)
print(length1([4,3,7,6,7,8,2,1,9])) # print 3
(2) 最长公共子序列
最长公共子序列问题是指:给定两个序列,找出它们的最长公共子序列。例如,对于序列X="ABCBDAB"和Y="BDCABA",它们的最长公共子序列为"BCBA"。
实现方法如下:
1.定义状态:设dp[i][j]表示序列X的前i个元素和序列Y的前j个元素的最长公共子序列的长度。
2.定义状态转移方程:对于序列X的第i个元素和序列Y的第j个元素,如果它们相等,则dp[i][j] = dp[i-1][j-1] + 1;否则,dp[i][j] = max(dp[i-1][j], dp[i][j-1])。
3.定义边界条件:dp[0][j] = dp[i][0] = 0。
4.计算顺序:按照从小到大的顺序计算dp[i][j]的值,最终得到dp[n][m]即为序列X和Y的最长公共子序列的长度。
def longestCommonSubsequence(X, Y):
m, n = len(X), len(Y)
dp = [[0] * (n+1) for _ in range(m+1)]
for i in range(1, m+1):
for j in range(1, n+1):
if X[i-1] == Y[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]
X = "ABCBDAB"
Y = "BDCABA"
print(longestCommonSubsequence(X, Y)) # 输出:4
(3)背包问题
背包问题是指:有一个背包,容量为C,现在有n个物品,第i个物品的重量为w[i],价值为v[i],求在不超过背包容量的前提下,能够获得的最大价值。
实现方法如下:
1.定义状态:设dp[i][j]表示前i个物品放入容量为j的背包中所能获得的最大价值。
2.定义状态转移方程:对于第i个物品,如果将其放入背包中,则dp[i][j] = dp[i-1][j-w[i]] + v[i];否则,dp[i][j] = dp[i-1][j]。因为如果第i个物品的重量已经超过了背包的容量j,则无法将其放入背包中。
3.定义边界条件:dp[0][j] = dp[i][0] = 0。
4.计算顺序:按照从小到大的顺序计算dp[i][j]的值,最终得到dp[n][C]即为能够获得的最大价值。
def knapsack(C, w, v):
n = len(w)
dp = [[0] * (C+1) for _ in range(n+1)]
for i in range(1, n+1):
for j in range(1, C+1):
if w[i-1] <= j:
dp[i][j] = max(dp[i-1][j], dp[i-1][j-w[i-1]]+v[i-1])
else:
dp[i][j] = dp[i-1][j]
return dp[n][C]
C = 10
w = [2, 3, 4, 5]
v = [3, 4, 5, 6]
print(knapsack(C, w, v)) # 输出:13
上述是,不可分解的背包问题哦,可分解的背包问题一般使用贪心算法即可(直接 价值/重量得到每个物品的优先级)。
当然,用一维数组也是可以的。
n=5
m=11
dp=[0]*(m+1)
w=[2,3,4,5,7]
t=[3,5,6,7,8]
for i in range(n):
for j in range(m,w[i]-1,-1):
dp[j]=max(dp[j],dp[j-w[i]]+t[i])
print(dp)
完全背包问题就是要倒过来啦, 因为可以重复一个物品,那么我们就应该使用这一次装填过程的最优解。
n=5
m=11
dp=[0]*(m+1)
w=[2,3,4,5,7]
t=[3,5,6,7,8]
for i in range(n):
for j in range(w[i],m+1):
dp[j]=max(dp[j],dp[j-w[i]]+t[i])
print(dp)
(4)所有点对的最短路径(Floyd算法)
算法的核心是一个二维数组dist
,其中dist[i][j]
表示节点i到节点j的最短路径长度。
算法的步骤如下:
1.初始化dist
数组,也就是不经过其他节点的,两个节点之间的距离。
2.对于每个中间节点k,遍历所有节点对(i, j),更新dist[i][j]
为dist[i][k] + dist[k][j]
和dist[i][j]
的较小值。(相当于每次使用一个节点来充当中间节点,看看加入了这个节点后,会不会使得某些点之间距离变短或者变得可达)
3.重复步骤2,直到所有节点对之间的最短路径都被计算出来。
import math
def floyd(graph):
n = len(graph)
dist = [[math.inf] * n for _ in range(n)]
for i in range(n):
for j in range(n):
if i == j:
dist[i][j] = 0
else:
dist[i][j] = graph[i][j]
for k in range(n):
for i in range(n):
for j in range(n):
dist[i][j] = min(dist[i][j], dist[i][k] + dist[k][j])
return dist
# 示例图的邻接矩阵表示
graph = [
[0, 5, math.inf, 10],
[math.inf, 0, 3, math.inf],
[math.inf, math.inf, 0, 1],
[math.inf, math.inf, math.inf, 0]
]
result = floyd(graph)
for row in result:
print(row)