seegongsik
算法

动态规划 怎么工作

用递归算过斐波那契数吗?要算 fib(5),得调用 fib(4) 和 fib(3);fib(4) 又要调用 fib(3) 和 fib(2)。可是 fib(2) 这里也出现,那里也出现,一次又一次。同样的东西被翻来覆去地重算。要是把算过一次的答案记在哪儿,会怎样?

01

同样的东西又算一遍

递归把大问题拆成小问题来解,斐波那契正是这样。
fib(5) 调用 fib(4) 和 fib(3),fib(4) 又调用 fib(3) 和 fib(2)。仔细一看,fib(2) 在这根枝上、那根枝上反复出现。
在下面的树里点一个节点看看。

用递归解 fib(5),就摊开成这么一棵树。点一个节点试试。
随便点一个节点。同一个小问题藏在树的各处。

同一个小问题在树上到处出现。(点节点 → 相同的值全部高亮)

同一个 fib(2) 不是算一次,而是算了好几次,fib(3) 也一样。
n 一变大,这种重复就会吓人地膨胀。
已经算出的答案还一遍遍重算,谁看了都觉得可惜。

02

把答案记下来

办法出奇地简单:把算过一次的答案记在便条上。
下次再需要同样的东西,不用重算,直接从便条上拿来用。只算第一次见到的,见过的直接拿来用就行。
点按钮,一个个处理调用。

同样的调用又来了。点一下处理下一个调用。
fib(2)
fib(3)
fib(2)
fib(4)
fib(2)
fib(3)
便条本
还是空的
按一下,调用就一个个进来。头回见的就算,见过的就从便条上拿。

第一次见到的就算出来记下,见过的就从便条上拿来用。这就是记忆化。

计算只在第一次做一回,其余全从便条上拿,同样的东西算两遍的事就一下子没了。
像这样把算好的答案记下来再用,叫作记忆化。
名字听着唬人,说穿了就是“记下来”。

03

从下往上填表

同样的想法也可以倒过来用:不从上往下拆,而是从最底下的小值开始,往上填表。
fib(0) 和 fib(1) 本来就知道。两个相加得 fib(2),fib(1) 和 fib(2) 相加得 fib(3)。
下面的格子已经写好了,上面的格子只要做加法。

反过来,从最底下的 0 和 1 开始把表往上填。
fib(0)0一开始就知道
fib(1)1一开始就知道
fib(2)?
fib(3)?
fib(4)?
fib(5)?
fib(6)?
fib(0)=0、fib(1)=1 我们直接就知道。从这儿往上一格一格填。

从 0,1 开始,一格格往上填。(下一格 = 正下方两格之和 · 自底向上)

这种办法连便条都用不着,因为表本身就是便条。
从下往上按顺序扫一遍,就再也不用重算。
记忆化是“需要时再记”,这个则是“事先全记好”,道理是一样的。

04

一下子快了

那么能快多少?光是记下来这一招,差别就大得惊人。
单纯递归连重复的也全算,n 越大,活儿就像爆炸一样暴涨;记忆化每格只算一次,活儿只和 n 成正比。
把 n 一点点加大,比一比两者,差距一目了然。

把 n 调大,比一比两种方式的工作量。
n = 5
纯递归15 次
记忆化6 次
算 fib(5),纯递归要解 15 次,记忆化只解 6 次。整整差了 3 倍。n 越大,差距越爆炸。

同一个 n 下要算的次数。(单纯递归 = 指数爆炸 · 记忆化 = 与 n 成正比的线性)

n 只要稍微大一点,单纯递归的柱子就像要冲破屏幕一样往上蹿,记忆化的柱子却几乎不动。
同样的答案不再重算,就这一点,分出了慢和快。

05

总结

动态规划名字听着难,说穿了只有三步。
发现同样的东西在重算,把算过一次的答案记下来,再拿来用,于是一下子快了。
把下面的卡片一张张点开,总结这三步。

动态规划就三步。一个个点亮来看。
1
又解一遍
递归把同一个小问题反复地解
2
记下来
把解过的答案记在便条上再拿出来用
3
快得多
指数变成线性,差距爆炸
按按钮,把三步一个个点亮。

同样的东西又算一遍 → 记下来 → 一下子快了。也顺带回顾:分治的块互不重叠,所以用不着记。

前面看过的分治,切开的块互不重叠,所以没什么可记的。
岔路口就在这里。块和块重叠时,也就是同样的小问题一再出现时,“记下来”这一招才会改变一切。

一句话总结递归常常把同一个小问题翻来覆去地重算,光看斐波那契树,fib(2) 就到处出现。把算过一次的答案记在便条上再拿来用,同样的东西就不用算两遍,这就是记忆化。倒过来,从最底下的小值往上填表,也是同一个想法。这样一来,原本指数级慢的,一下子变成线性,快多了。分治的块互不重叠,所以没什么可记;块和块一重叠,记下答案这一招就会改变一切。
← 算法
如果有帮助,请支持我们