目录
一,算法总体思想
动态规划算法与分治法类似,也是将大规模不易求解的问题分解为规模较小的子问题,与分治法不同的是,动态规划算法并不要求分解出的子问题是相互独立的,即不要求没有重复的子问题。而如此分解问题,会重复求解大量相同的子问题。
为解决上述问题,动态规划算法会建立一个存储表格,将已求解的问题答案储存在表格中,以此来避免大量重复的计算。
基本要素:
1,最优子结构性质;
最优子结构性质是指一个问题的最优解可以通过一系列相关子问题的最优解来构建。简单来说,如果一个问题可以被划分为更小的子问题,并且这些子问题的最优解可以推导出原问题的最优解,那么这个问题就具有最优子结构性质。
假设有一个旅行商问题,要求找到一条路径,使得旅行商可以依次经过多个城市并回到起点城市,并且路径的总长度最短。这个问题具有最优子结构性质,因为如果我们知道了从城市A到城市B的最短路径,以及从城市B到其它城市的最短路径,那么我们可以通过将这两个最短路径连接起来得到从城市A出发经过城市B再经过其它城市的最短路径。
2,重叠子问题性质。
二,动态规划的基本步骤
1,找出最优解的性质,并刻画其结构特征;
2,递归地定义最优值;
3,以自底向上的方式计算最优值;
4,根据计算最优值是得到的信息,构造最优解。
注意在求解的过程中,不是一蹴而就地得到最优解,而是首先寻找最优值,而后根据最优值构造最优解。因此,在求解最优值时,要记录足够多的信息,方便后续最优解的构造。
动态规划也有其他思路:
将问题分为若干个阶段,在每个阶段都有相应的状态,在各个阶段所做的决策(如背包问题中第 i 个物品是否能装入背包,若能装入,是否选择装入背包)会改变当前的问题当下的状态并影响之后的决策,状态一般是资源剩余量(如背包问题中的背包承重量)这种问题的硬性要求或是当前阶段子问题最优解所满足的条件(如背包问题中的求解最大价值)。根据各阶段所做决策对状态的影响,得到问题的状态转移方程,并确定初始条件,从而递推地找到问题的最优值。需要的话,利用求解最优值时留下的信息,构造最优解。
见例题6
三,一些例子
1,矩阵连乘问题
步骤一,刻画最优解的结构特征
特征:计算A[i: j]的最优次序,包含的次序矩阵子链A[i: k]和A[k+1: j]的次序也是最优的
矩阵连乘计算次序问题的最优解包含着其子问题的最优解,即最优子结构性质,这是使用动态规划算法的显著特征。
步骤二,建立递归关系
步骤三,自下向上的计算最优值
上述步骤中,将问题分解后,m中储存了最优值,此时已经得到了最少数乘次数;
import numpy as np
def minm(p, n, m, s) :
for i in range(n) :
m[i][i] = 0
for r in range(1, n) :
for i in range(1, n - r + 1) :
j = i + r
# 先求出断点为i时的数据,为后续比较做准备
# 假设断点为i是,数乘次数最小
m[i][j] = m[i+1][j] + p[i - 1] * p[i] * p[j]
# 将断点记录为i
s[i][j] = i
# 计算断点为i + 1到j的情况
for k in range(i + 1, j) :
tmp = m[i][k] + m[k + 1][j] + p[i - 1] * p[k] * p[j]
if tmp < m[i][j] :
m[i][j] = tmp
s[i][j] = k
n = int(input())
m = np.zeros((n + 1, n + 1))
s = np.zeros((n + 1, n + 1))
p = input().split()
p = [int(ele) for ele in p]
minm(p, n, m, s)
print(m)
print(s)
6
30 35 15 5 10 20 25
[[ 0. 0. 0. 0. 0. 0. 0.]
[ 0. 0. 15750. 7875. 16500. 34000. 15125.]
[ 0. 0. 0. 2625. 6000. 13000. 10500.]
[ 0. 0. 0. 0. 750. 2500. 5375.]
[ 0. 0. 0. 0. 0. 1000. 3500.]
[ 0. 0. 0. 0. 0. 0. 5000.]
[ 0. 0. 0. 0. 0. 0. 0.]]
[[0. 0. 0. 0. 0. 0. 0.]
[0. 0. 1. 1. 1. 1. 3.]
[0. 0. 0. 2. 2. 2. 3.]
[0. 0. 0. 0. 3. 3. 3.]
[0. 0. 0. 0. 0. 4. 5.]
[0. 0. 0. 0. 0. 0. 5.]
[0. 0. 0. 0. 0. 0. 0.]]
为保持m,s的下标仍为i,j 初始化了规模为n+1的数组
步骤四:根据计算最优值时得到的信息,构造最优解
在计算最优值时,是根据断点k完成的,如果将k的值记录相应记录为s[i: j],则可由s构造最优解。
def construct(s, i, j):
if i == j:
return "A" + str(i)
else:
k = int(s[i][j])
left = construct(s, i, k)
right = construct(s, k + 1, j)
print(f'left:{left},right:{right}')
print("(" + left + " x " + right + ")")
print('******************************************')
return "(" + left + " x " + right + ")"
在这个递归函数中,s
是存储最优解断点位置的矩阵,i
是当前子问题的起点,j
是当前子问题的终点。函数首先检查起点和终点是否相等,如果相等,说明只有一个矩阵,直接返回其名称。否则,根据 s
矩阵中的断点位置 k
,递归地构造左子树和右子树,然后将它们连接起来,形成最优解的括号结构。
left:A2,right:A3
(A2 x A3)
******************************************
left:A1,right:(A2 x A3)
(A1 x (A2 x A3))
******************************************
left:A4,right:A5
(A4 x A5)
******************************************
left:(A4 x A5),right:A6
((A4 x A5) x A6)
******************************************
left:(A1 x (A2 x A3)),right:((A4 x A5) x A6)
((A1 x (A2 x A3)) x ((A4 x A5) x A6))
******************************************
((A1 x (A2 x A3)) x ((A4 x A5) x A6))
上面是动态规划算法的实现过程,采用的是非递归的方案,若使用递归思想,则可以采用动态规划的变形算法——备忘录算法:
import numpy as np
def Memorandum(i, j, m, p, s):
if m[i][j] > 0: # 先查找是否计算过当前子问题,即备忘录
return m[i][j]
if i == j:
return 0
u = float('inf') # 将u设置为无穷大,便于记录最小值
for k in range(i, j): # 断点从i到j - 1
t = Memorandum(i, k, m, p, s) + Memorandum(k + 1, j, m, p, s) + p[i - 1] * p[k] * p[j]
if t < u:
u = t
s[i][j] = k
m[i][j] = u
return u
def construct(s, i, j):
if i == j:
return 'A' + str(i)
else:
k = int(s[i][j])
left = construct(s, i, k)
right = construct(s, k + 1, j)
return '(' + left + '*' + right + ')'
n = int(input())
m = np.zeros((n + 1, n + 1))
s = np.zeros((n + 1, n + 1))
p = input().split()
p = [int(ele) for ele in p]
ans = Memorandum(1, n, m, p, s)
ans = construct(s, 1, n)
print(ans)
# 输入:
6
30 35 15 5 10 20 25
# 输出:
((A1*(A2*A3))*((A4*A5)*A6))
2,最长公共子序列
例如:
设:长度为k的序列Z,是X(长度为n)和Y(长度为m)的最长公共子序列
注:()内的数代表序列的长度
步骤一,刻画最优解的结构特征
情况1 :X[n] == Y[m] 则Z(k - 1)是X(n - 1)和Y(m - 1)的最长公共子序列;
情况2:X[n] != Y[m],且Z[k] != X[n] 则Z(k)是X(n - 1)和Y(m)的最长公共子序列;
情况3:X[n] != Y[m],且Z[k] != Y[m] 则Z(k)是X(n)和Y(m - 1)的最长公共子序列。
由此可见,两个序列的最长公共子序列包含了其子问题的最长公共子序列,即该问题符合最优子结构性质。
步骤二,建立递归关系
设c[i][j]表示X(i)和Y(j)最长公共子序列的长度
步骤三,计算最优值
def Longest_common_subsequence(c, b, n, m, x, y):
for i in range(1, n + 1):
for j in range(1, m + 1):
if x[i - 1] == y[j - 1]:
c[i][j] = c[i - 1][j - 1] + 1
b[i][j] = 1
else:
if c[i - 1][j] >= c[i][j - 1]:
c[i][j] = c[i - 1][j]
b[i][j] = 2 # 记录为上侧的数
else:
c[i][j] = c[i][j - 1]
b[i][j] = 3 # 记录为左侧的数
根据递归关系,逐行计算c矩阵,同时记录b矩阵,方便后续构造最优解
步骤四,构造最优解
def construct(b, n, m, x):
ans = ""
i = n
j = m
while i > 0 and j > 0:
if int(b[i][j]) == 1: # 只有b中的值为1时,找到了相同的字符
ans = x[i - 1] + ans # 由于先找到的是最后一个数,所以新来的数加在前面
i -= 1
j -= 1
elif int(b[i][j]) == 2: # 找到上方的数
i -= 1
else: # 找到左侧的数
j -= 1
return ans
完成的代码与运行结果:
import numpy as np
def Longest_common_subsequence(c, b, n, m, x, y):
for i in range(1, n + 1):
for j in range(1, m + 1):
if x[i - 1] == y[j - 1]:
c[i][j] = c[i - 1][j - 1] + 1
b[i][j] = 1
else:
if c[i - 1][j] >= c[i][j - 1]:
c[i][j] = c[i - 1][j]
b[i][j] = 2 # 记录为上侧的数
else:
c[i][j] = c[i][j - 1]
b[i][j] = 3 # 记录为左侧的数
def construct(b, n, m, x):
ans = ""
i = n
j = m
while i > 0 and j > 0:
if int(b[i][j]) == 1: # 只有b中的值为1时,找到了相同的字符
ans = x[i - 1] + ans # 由于先找到的是最后一个数,所以新来的数加在前面
i -= 1
j -= 1
elif int(b[i][j]) == 2: # 找到上方的数
i -= 1
else: # 找到左侧的数
j -= 1
return ans
x = input()
x = x[1: len(x) - 1].split(',')
n = len(x)
y = input()
y = y[1: len(y) - 1].split(',')
m = len(y)
c = np.zeros((n + 1, m + 1))
b = np.zeros((n + 1, m + 1))
Longest_common_subsequence(c, b, n, m, x, y)
print(c)
print('******************************************')
print(b)
ans = construct(b, n, m, x)
print(ans)
# 输入
(a, b, c, b, d, b)
(a, c, b, b, a, b, d, b, b)
# 输出
# c矩阵
[[0. 0. 0. 0. 0. 0. 0. 0. 0. 0.]
[0. 1. 1. 1. 1. 1. 1. 1. 1. 1.]
[0. 1. 1. 2. 2. 2. 2. 2. 2. 2.]
[0. 1. 2. 2. 2. 2. 2. 2. 2. 2.]
[0. 1. 2. 3. 3. 3. 3. 3. 3. 3.]
[0. 1. 2. 3. 3. 3. 3. 4. 4. 4.]
[0. 1. 2. 3. 4. 4. 4. 4. 5. 5.]]
******************************************
# b矩阵
[[0. 0. 0. 0. 0. 0. 0. 0. 0. 0.]
[0. 1. 3. 3. 3. 3. 3. 3. 3. 3.]
[0. 2. 2. 1. 1. 3. 1. 3. 1. 1.]
[0. 2. 1. 2. 2. 2. 2. 2. 2. 2.]
[0. 2. 2. 1. 1. 3. 1. 3. 1. 1.]
[0. 2. 2. 2. 2. 2. 2. 1. 3. 3.]
[0. 2. 2. 1. 1. 3. 1. 2. 1. 1.]]
# 结果
a b b d b
3,最大子段和
步骤一,刻画最优解的结构特征
在整数序列a(n)中,所有子段可分类为以n结尾,以n-1结尾......以1结尾的字段,求出上述所有字段的最大值,再从中取最大值,即是a(n)的最大子段和。
步骤二,建立递归关系
设:m[k]表示以下标k为结尾的最大字段和,则:
步骤三,计算最优值
def maximum_sub(m, a, n):
if n == 0:
m[0] = a[0]
return a[0]
else:
empty = maximum_sub(m, a, n - 1)
if empty > 0:
m[n] = empty + a[n]
else:
m[n] = a[n]
return m[n]
找到以k为终点的子序列的最大值
步骤四,构造最优解
def find_ans(m) :
ans = -1
for i in m :
if ans < int(i) :
ans = int(i)
if ans < 0 :
return 0
return ans
代码:
def maximum_sub(m, a, n):
if n == 0:
m[0] = a[0]
return a[0]
else:
empty = maximum_sub(m, a, n - 1)
if empty > 0:
m[n] = empty + a[n]
else:
m[n] = a[n]
return m[n]
def find_ans(m) :
ans = -1
for i in m :
if ans < int(i) :
ans = int(i)
if ans < 0 :
return 0
return ans
a = input()
a = a[1:len(a) - 1]
a = a.split(',')
a = [int(ele) for ele in a]
n = len(a)
m = [0] * n
maximum_sub(m, a, n - 1)
print(find_ans(m))
# 输入
(-2,11,-4,13,-5,-2)
# 输出
20
综上可见,最重要的是第一步与第二步,找到最优解的结构后,构造递归关系,也就是找到最优子结构性质,逐步缩小问题的规模,直到可以求解的边界,自下而上的求解最优值。
4,流水作业调度
问题描述:
1,n个作业{1,2,…,n},要在由机器M1和M2组成的流水线上完成加工;
2, 每个作业加工的顺序都是先在M1上加工,然后在M2上加工;
3,M1和M2加工作业i所需的时间分别为ai和bi。
要求确定这n个作业的最优加工顺序,使得从第一个作业在机器M1上开始加工,到最后一个作业在机器M2上加工完成所需的时间最少。
问题的最优子结构性质 :
直观上,一个最优调度应使机器M1没有空闲时间,且机器M2的空闲时间最少(并行度最高)。 在一般情况下,机器M2上会有机器空闲和作业积压两种情况。
设全部作业的集合为N={1,2,…,n}。S是N的作业子集。 通常,机器M1开始加工S中作业时,机器M2还在加工其它作业,要等时间t后才可利用。
将这种情况下完成S中作业所需的最短时间记为T(S, t)。 流水作业调度问题的最优值为T(N, 0)。
从表达式可以看到问题缩小了,其中a1可视为ai,即当前顺序的第一个任务,将二者联立可得
T(s, t) = ai + T({S - i} , bi + max(0, t - ai))
可见其最优子结构性质
在解决该问题时并未使用动态规划算法,代码不再给出,进入下一个问题
5,0-1背包问题
问题描述:
给定n个物品和一个背包。物品i的重量为wi,价值为vi,背包容量为c。问如何选择装入背包中的物品,使得装入背包的物品的价值最大?
问题的最优子结构性质:
若当前已有最优解S,将n个物品装入载重量为c的背包,使背包的最大价值为c_v,此时有一个重量为w,价值为v的物品m到来,要求考虑物品m后的新最优解。
对于新物品m,有两种选择:装入或不装入,如果不装入,则背包价值仍为c_v;
若装入m,此时背包载重量为c - w,如果我们知道背包在c - w的载重量下,将n个物品装入背包的最大价值,就能知道装入新物品m后的背包价值,将其与不装入m的c_v比较,保留较大值,即是考虑m后的问题最优解。
此时我们注意到,若将n视为前n - 1个物品,将新物品m视为第n个物品,则得到了物品的最优子结构性质,即更小规模问题(n - 1个物品,载重量c - w或n-1个物品,载重量c)状态下的最优解,是更大规模问题(n个物品,载重量c)的部分解。
步骤一,刻画最优解的特征
用mij记录放入背包物品的价值,其中i表示将第i个物品到第n个物品放入背包时,背包总价值,j表示背包容量为j时,背包可存放的总价值。此时i-1的最大总价值,可与i进行比较更新。
步骤二,找到递归关系
对于每一个物品,若放入记为1,不放入记为0,则0-1背包问题就是在二进制中,0到2^n的穷举
首先考虑最后一个物品,若背包在容量 j 时可以放入,则背包的价值为vn,否则为0;
再考虑 i 到n个物品时
若背包在容量 j 时可以放入第 i 个物品,则比较物品i放入与否:
放入该物品后m(i + 1, j - wi) + vi j - wi 表示放入物品,+ vi 表示当前背包的价值
而,m(i + 1, j) 不放入物品 i 时背包价值
二者取最大值;
若背包在容量 j 时放不下第 i 个物品,背包价值为m(i + 1, j),即在当前容量下上层结果,因为第 i 个物品只能取 0
步骤三,自上而下求得最优值
def Max_bag(m, n, c, w, v) :
for _ in range(c + 1) :
if _ < int(w[n]) :
m[n][_] = 0
else :
m[n][_] = v[n]
for i in range(n - 1, 0, -1) :
for j in range(0, c + 1) :
if j < w[i] :
m[i][j] = m[i + 1][j]
else :
empty = m[i + 1][j - w[i]] + v[i]
if empty < m[i + 1][j] :
m[i][j] = m[i +1][j]
else :
m[i][j] = empty
# 输入
5 10
2,2,6,5,4
6,3,5,4,6
# 输出
[[ 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.]
[ 0. 0. 6. 6. 9. 9. 12. 12. 15. 15. 15.]
[ 0. 0. 3. 3. 6. 6. 9. 9. 9. 10. 11.]
[ 0. 0. 0. 0. 6. 6. 6. 6. 6. 10. 11.]
[ 0. 0. 0. 0. 6. 6. 6. 6. 6. 10. 10.]
[ 0. 0. 0. 0. 6. 6. 6. 6. 6. 6. 6.]]
其中m[1][c] = 15 表示将1到 n 个物品放入容量为 c 的背包中,最大价值为15
步骤四,构造最优解
在得到的m表格中,通过递归式可知,若m[i][j]与m[i + 1][j]不相等,则表明第 i 个物品放入背包,可由此解得放入背包的各个物品
def construct(m, n, c, w, v) :
ans = []
i = 1
j = c
x = m[i][j]
while i < n :
if m[i][j] == m[i + 1][j] :
i += 1
else :
ans.append(i)
j -= w[i]
x -= v[i]
i += 1
if m[i][j] == x :
ans.append(i)
return ans
# 输入
5 10
2,2,6,5,4
6,3,5,4,6
# 输出m
[[ 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.]
[ 0. 0. 6. 6. 9. 9. 12. 12. 15. 15. 15.]
[ 0. 0. 3. 3. 6. 6. 9. 9. 9. 10. 11.]
[ 0. 0. 0. 0. 6. 6. 6. 6. 6. 10. 11.]
[ 0. 0. 0. 0. 6. 6. 6. 6. 6. 10. 10.]
[ 0. 0. 0. 0. 6. 6. 6. 6. 6. 6. 6.]]
# 放入背包的物品编号
[1, 2, 5]
6,最优作业调度问题
问题描述:
用 2 台处理机 A 和 B 处理 n 个作业。设第 i 个作业交给机器 A 处理时需要时间 ai,若由机器 B 来处理,则需要时间 bi。由于各作业的特点和机器的性能关系,很可能对于某些 i,有 ai>=bi,而对于某些 j,j≠i,有 aj<bj。既不能将一个作业分开由 2 台机器处理,也没有一台机器能同时处理 2 个作业。设计一个动态规划算法,使得这 2 台机器处理完这 n 个作业的时间最短(从任何一台机器开工到最后一台机 器 停 工 的 总 时 间 ) 。
问题分析:
所有任务都需要完成,且完成时间不因顺序改变,因此任务的顺序不会影响结果,即只需要对任务进行分配即可。
将问题分为几个阶段:当只有一个任务时,将任务分配到时间更短的机器上,当有两个及以上任务时,则需要考虑由哪个机器完成哪个任务。但最大不超过将所有任务全部交给机器A或机器B的时间。
解决问题的自然语言描述:
设数据结构f[i][j],其中,i表示完成第1个到第i个任务,j表示分配给机器A的时间,f中存储的是机器B需要的最短时间。a[i]表示第i个任务在机器A上的执行时间,b[i]表示第i个任务在机器B上的执行时间。
当j < a[i]时,即分配给机器A的时间不足以完成任务i,此时该任务只能由机器B完成,因此f[i][j] = f[i - 1][j] + b[i] ;
当j > a[i]时,即分配给机器A的时间足以完成任务i,此时若是将任务i交给机器A完成,则f[i][j] = f[i - 1][j - a[i]] ,f中存储的是机器B所用的时间,i - 1是完成前i - 1个任务,j - a[i] 意为分配j时间给机器A,任务i交给机器A完成后,机器A还剩余j - a[i]时间可以使用,此时也就相当于分配j - a[i]时间给机器A,完成前i - 1个任务的情况。若任务i交给机器B完成,则f[i][j] = f[i - 1][j] + b[i]。与上述二者的最小值。
综上,当j < a[i]时, f[i][j] = f[i - 1][j] + b[i];
当j > a[i]时,f[i][j] = min{ f[i - 1][j - a[i]], (f[i - 1][j] + b[i]) }.
f[i][_]中存储的是完成前i个任务,机器B所用的最短时间,遍历第n行,对于每个f[n][j]
该条的结果是ans = max(f[n][j], j]),即取完成n个任务,机器A,机器B中耗时较长的一条。再对所求ans取最小值即可。
代码:
import numpy
def dp(a, b, n) :
if n == 1 :
return min(a[n], b[n])
a_sum = 0
for i in range(n) :
a_sum += a[i]
f = numpy.zeros((n + 1, a_sum + 1))
for i in range(1, n + 1) :
for j in range(a_sum + 1) :
if j < a[i] :
f[i][j] = f[i - 1][j] + b[i]
else :
f[i][j] = min((f[i - 1][j] + b[i]), f[i - 1][j - a[i]])
print(f)
ans = float('inf')
for i in range(a_sum + 1) :
m = max(i, f[n][i])
if m < ans :
ans = m
return ans
n = int(input())
a = [int(0)] + list(map(int, input().split(',')))
b = [int(0)] + list(map(int, input().split(',')))
print(dp(a, b, n))
对于输入:
(a1,a2,a3,a4,a5,a6)=(2,5,7,10,5,2)
(b1,b2,b3,b4,b5,b6)=(3,8,4,11,3,4)
输出 f 为:
结果为:
15.0