算法教程-动态规划

博客围绕复杂依赖及其记忆体化展开,介绍了问题分解允许子问题重叠,DP算法通过反转递归函数迭代填充数据结构。还阐述了Fibonacci、有向无环图最短路径、最长递增子序列、最长公共子序列、背包问题及序列二元分割等问题的解决方法,涉及递归、迭代及记忆体化等。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

第8章 复杂依赖及其记忆体化

  1. 问题分解依旧使用递归/归纳那一套,但是我们允许子问题之间出现重叠。
  2. 通常情况下,DP算法都是通过反转相应的递归函数,使其对某些数据结构进行逐步迭代和填充(如某种多维数组)。
  3. 对于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)

1
0
7
2
8
3
4
9

从归纳法开始,先对子问题进行定义,我们需要强化我们设定的归纳前提。以各个指定的位置为终点,来找各序列的最长递增子序列。如果我们已经知道了找出前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
用递归,记忆体化的方式解决:
我们的序列分别为 aaabbb,从任意的前置对开始,将这两个序列的程度标识为 iiijjj

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矩阵当前行以及前一行的版本,以节省内存的消耗。

8.5 背包问题的反击

8.6 序列的二元分割

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值