1.概念
(1)适用场景
一个问题的最优解,包含其子问题的最优解
(2)解题步骤
分析原问题最优解的结构特征
递归的定义最优值(状态转移函数),并关注初始条件、边界
计算最优的值,通常自底向上
综合计算信息,构造最优解
2.案例
(1)矿工
1、问题描述:
给定矿产n=5、矿工数量m=10、单个矿产钻石量G=[400,500,200,300,350]、单个矿产所需工人数L=[5,5,3,4,3]
2、问题分析:
2.1、分析原问题最优解的结构特征
当前所获得钻石量为G[n],所用工人数L[n]
要获得10个矿工挖掘第5座矿产的最优解F(5,10),需要在F(4,10)和F(4,10-L[4])+G[4]中取较大值
2.2、建立递归关系,写出状态转移函数
边界条件:当n=1,m≥L[0]时,F(n,m)=G[0];
当n=1,m状态转移函数:F(n,m) = 0 (n
F(n,m) = G[0] (n==1,m>=L[0])
F(n,m) = F(n-1,m) (n>1,m
F(n,m) = max(F(n-1,m), G[n-1]+F(n-1,m-L[n-1])) (n>1,m>=L[n-1])
2.3、计算最优解的值
采用自底向上的方式进行计算,像填表过程一样从左至右、从上到下逐渐获得计算结果。
这样,可以不需要存储整个表格的内容,仅需要存储前一行的结果,就可以推出下一行的内容,避免了重复计算
2.4、构造最优解
回溯可得最优解得组合
3、代码实现:
def goldmine(n, m, g, l):
t_results = [0 for _ in range(m+1)]
for i in range(1, m+1):
if i < l[0]:
t_results[i] = 0
else:
t_results[i] = g[0]
for i in range(1, n):
results = [0 for _ in range(m+1)]
for j in range(1, m+1):
if j < l[i]:
results[j] = t_results[j]
else:
results[j] = max(t_results[j], g[i]+t_results[j-l[i]])
t_results = results
return results[-1]
print(goldmine(5, 10, [400, 500, 200, 300, 350], [5, 5, 3, 4, 3]))
(2)背包
1、问题描述:
背包承重量c=8,物品n[a,b,c,d]件,单个物品质量w[2,4,5,3],单个物品价值v[5,4,6,2]
2、问题分析
2.1、分析最优子结构性质
前i个物品,能放进承重量为j的背包中,有两种情况:包含i,不包含i
不包含i,最优价值是F(i-1,j)
包含i,最优价值是v[i-1]+F(i-1,j-w[i-1])
所以,在前i个物品中,最优价值是以上两种情况的较大值。
2.2、分析递归关系,写出状态转移函数
边界条件:F(0,j) = 0 (j>=0)
F(i,0) = 0 (i>=0)
状态转移函数:F(i,j) = F(i-1,j) (j-w[i-1]
F(i,j) = max(F(i-1,j),v[i-1]+F(i-1,j-w[i-1])) (j-w[i-1]>=0)
2.3、背包实例
2.4、回溯
若F(i,j)=F(i-1,j),则说明当前物品没有放到背包中,回溯到F(i-1,j)中
若F(i,j)=vi+F(i-1,j-wi),则说明当前物品已经放到背包中,将该物品记录为组成最优解的元素,回溯到F(i-1,j-wi)
重复上述回溯过程,直到i=0,即可获得所有组成最优解的物品集合
3、代码实现
3.1、计算最优记录表
初始化记录表
从上,从左到右,再到下,根据状态转移函数,计算记录表,最后返回表res
3.2、回溯
初始化物品是否包含的列表
初始化已包含的物品,和质量
自底向上,从右到左,条件循环,res[i][j] > res[i-1][j]时,修改物品是否包含的列表为True,同时,修改已包含的质量,修改已包含的物品,直到已包含的物品=0
可得到,True就是最优解时已包含的物品
def backpack_record(n, c, w, v):
backpack_rec = [[0 for _ in range(c+1)] for _ in range(len(n)+1)]
for i in range(len(n)+1):
for j in range(c+1):
if j < w[i-1]:
backpack_rec[i][j] = backpack_rec[i-1][j]
else:
backpack_rec[i][j] = max(backpack_rec[i-1][j], v[i-1]+backpack_rec[i-1][j-w[i-1]])
return backpack_rec
def backpack_results(n, c, w, res):
print("最大价值是:", res[len(n)][c])
x = [False for _ in range(len(n)+1)]
j = c
i = len(n)
while i >= 0:
if res[i][j] > res[i-1][j]:
x[i] = True
j -= w[i-1]
i -= 1
for i in range(len(x)):
if x[i]:
print("第", i, "个", end=" ")
n = ['a', 'b', 'c', 'd']
c = 8
w = [2, 4, 5, 3]
v = [5, 4, 6, 2]
res = backpack_record(n, c, w, v)
backpack_results(n, c, w, res)
(3)最长递增子序列
1、问题描述:
给定一个数组,找出数组中长度最长的递增子序列
2、问题分析
设f(i)表示序列中以ai为末元素的最长递增子序列的长度,则在求以ai为末元素的最长递增子序列时,找到所有序号在i前面且小于ai的元素aj,即j
如果这样的元素存在,那么对所有aj,都有一个以aj为末元素的最长递增子序列的长度f(j),把其中最大的f(j)选出来,那么f(i)就等于最大的f(j)加上1,
即以ai为末元素的最长递增子序列,等于以使f(j)最大的那个aj为末元素的递增子序列最末再加上ai;
如果这样的元素不存在,那么ai自身构成一个长度为1的以ai为末元素的递增子序列。
3、代码实现
def test1(arr):
""" 找到数组中,以每个数为结尾的递增子序列的长度,以list返回"""
n = len(arr)
list = [0] * n
for i in range(n):
list[i] = 1
for j in range(i):
if arr[i] > arr[j]:
list[i] = max(list[i], list[j]+1)
return list
def test2(arr):
list = test1(arr)
# 最长子序列的长度
n = max(list)
ans = [0] * n
# 有最长子序列的末尾元素的索引
index = list.index(n)
n -= 1
ans[n] = arr[index]
for i in range(index, -1, -1):
if arr[i] < arr[index] and list[i] == list[index] - 1:
n -= 1
ans[n] = arr[i]
index = i
return ans
arr = [3,1,4,5,9,2,6,5,0]
print(test2(arr))
(4)通配符匹配
1、问题描述
s为待匹配字符串,
p为匹配模式,'?' 匹配任意单个字符串,'*'匹配任意长度字符串
2、问题分析
dp[i][j]为s中前i个字符,与p中前j个字符,是否匹配,
状态转移函数:
p[j] = ? or p[j] = s[i],dp[i][j] = dp[i-1][j-1]
p[j] = *,dp[i][j] = dp[i-1][j] | dp[i][j-1]
边界条件:
s为空,p为空,则dp[0][0]=True
s为空,p不为空,前j个都为‘*’,则dp[0][j]=True,否则False
p为空,s不为空,则dp[i][0]=False
3、代码实现
class Solution:
def isMatch(self, s: str, p: str) -> bool:
m, n = len(s), len(p)
dp = [[False] * (n+1) for _ in range(m+1)]
dp[0][0] = True
for j in range(1, n+1):
if p[0:j] == '*' * j:
dp[0][j] = True
else:
break
for i in range(1, m+1):
for j in range(1, n+1):
if p[j-1] == '*':
dp[i][j] = dp[i][j-1] | dp[i-1][j]
elif p[j-1] == '?' or p[j-1] == s[i-1]:
dp[i][j] = dp[i-1][j-1]
return dp[m][n]