第8章 复杂依赖及其记忆体化
- 问题分解依旧使用递归/归纳那一套,但是我们允许子问题之间出现重叠。
- 通常情况下,DP算法都是通过反转相应的递归函数,使其对某些数据结构进行逐步迭代和填充(如某种多维数组)。
- 对于python来说,在实现相关递归函数时直接从缓存中返回值。如果我们使用 同一个参数执行多次调用,其返回结果就会直接来自于缓存。记忆体化(memoization)
8.1 不要重复自己
Fibonacci
import datetime
def fib(i):
if i < 2:
return 1
else:
return fib(i-1)+fib(i-2)
start = datetime.datetime.now()
print(fib(34))
end = datetime.datetime.now()
print(end-start)
9227465
0:00:01.532390
当你使用fib(34)fib(34)fib(34),用时如上,这是指数级的算法。
我们实现用嵌套域封装一个临时函数
import datetime
from functools import wraps
def fib(i):
if i < 2:
return 1
else:
return fib(i-1)+fib(i-2)
def memo(func):
cache = {}
@wraps(func)
def wrap(*args):
if args not in cache:
cache[args] = func(*args)
return cache[args]
return wrap
start = datetime.datetime.now()
fib = memo(fib)
print(fib(34))
end = datetime.datetime.now()
print(end-start)
9227465
0:00:00
该内存华函数的思路就是缓存其自身的返回值。
memo函数是一个可重用性更好得解决方案,它被当做一种装饰器来设计:
import datetime
from functools import wraps
def memo(func):
cache = {}
@wraps(func)
def wrap(*args):
if args not in cache:
cache[args] = func(*args)
return cache[args]
return wrap
@memo
def fib(i):
if i<2:
return 1
else:
return fib(i-1)+fib(i-2)
start = datetime.datetime.now()
print(fib(34))
end = datetime.datetime.now()
print(end-start)
9227465
0:00:00
fib()函数被直接标志成了@memo,这样就能有效地大幅降低一些运行时间。
这与分治问题不同的在于其存在一些复杂的依赖关系,或者说我们面对的是一些互相重叠的子问题。
写成迭代版(DP版本),不仅更快,还可以避免因为递归深度过大而导致堆栈空间耗尽。迭代版本的实现通常会有一个专属构造的缓存,而不是@memo中看到的那种通用的“由参数元组组成的键值字典”。
这种自定义的缓存设计可以使DP算法应用于更多的低级编程语言,像我们@memo装饰器中这种抽象结构通常是不可用的。
实用数据结构 collections.defaultdict
若使用dict()直接操作不存在的key
d = dict()
d['a'] += 1
Traceback (most recent call last):
File "C:/Users/Administrator/PycharmProjects/untitled/dp/test.py", line 2, in <module>
d['a'] += 1
KeyError: 'a'
此时为了省去 d[‘a’] = int() 设置默认值的步骤, 可以使用defaultdict
from collections import defaultdict
df = defaultdict(int)
df['a'] += 1
print(df['a'])
1
8.2 有向无环图中的最短路径问题
8.3 最长递增子序列问题(LIS)
从归纳法开始,先对子问题进行定义,我们需要强化我们设定的归纳前提。以各个指定的位置为终点,来找各序列的最长递增子序列。如果我们已经知道了找出前k位子序列的方法,会不知道如何找出k+1位的子序列吗? 我们只需要看看当前元素之前的元素是否都小于当前元素即可。
from functools import wraps
def memo(func):
cache = {}
@wraps(func)
def wrap(*args):
if args not in cache:
cache[args] = func(*args)
return cache[args]
return wrap
def rec_lis(seq): # Longest Increasing SubSequence
@memo
def L(cur):#Longest ending at seq[cur]
res = 1
for pre in range(cur):# 考察前面的元素
if seq[pre]<=seq[cur]:#是不是小于当前元素
res = max(res, 1+L(pre))# 看看哪个大就保留哪个,这里有递归
return res
return max(L(i) for i in range(len(seq)))
seq = [1, 0, 7, 2, 8, 3, 4, 9]
output: 5
迭代版本:
在递归版本中,rec_lis()是按照(0,1,2…)的顺序解决问题的,我们只需要将递归调用切换成一个查找过程,封装成循环即可。
def basic_lis(seq):
L = [1]*len(seq)
for cur, val in enumerate(seq):
for pre in range(cur):
if seq[pre] <= val:
L[cur] = max(L[cur], 1+L[pre])
return max(L)
这个好理解多了
常用函数 enumerate()
seq = [1, 0, 7, 2, 8, 3, 4, 9]
for index, value in enumerate(seq):
print([index, value], end = " ")
output:[0, 1] [1, 0] [2, 7] [3, 2] [4, 8] [5, 3] [6, 4] [7, 9]
常用函数 bisect()
import bisect
print(dir(bisect))
['__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__', 'bisect', 'bisect_left', 'bisect_right', 'insort', 'insort_left', 'insort_right']
使用这个模块的函数前先确保操作的列表是已排序的。
data = [4, 2, 9, 7]
data.sort()
其目的在于查找该数值将会插入的位置并返回,而不会插入。
print(bisect.bisect(data, 1))
0
8.4 序列对比问题
最长公共子序列(LCS)
Starwalker 和 Starbuck 中的 Stark 就是一个LCS
用递归,记忆体化的方式解决:
我们的序列分别为 aaa 和 bbb,从任意的前置对开始,将这两个序列的程度标识为 iii 和 jjj
from functools import wraps
def memo(func):
cache = {}
@wraps(func)
def wrap(*args):
if args not in cache:
cache[args] = func(*args)
return cache[args]
return wrap
def rec_lcs(a, b):
@memo
def L(i, j):
if min(i, j) < 0:
return 0
if a[i] == b[i]:
return 1+L(i-1, j-1)
return max(L(i-1, j), L(i, j-1))
用迭代的方式解决LCS问题:
def lcs(a, b):
n = len(a)
m = len(b)
pre, cur = [0]*(n+1), [0]*(n+1)
for j in range(1, m+1):
pre, cur = cur, pre
for i in range(1, n+1):
if a[i-1] == b[j-1]:
cur[i] = pre[i-1] + 1
else:
cur[i] = max(pre[i], cur[i-1])
return cur[n]
我们只保留了DP矩阵当前行以及前一行的版本,以节省内存的消耗。