根本原因是解决思路是反的。以Fib为例:
- 递归+memo:
f(20) -> f(19) -> f(18) -> .... f(3) -> f(2) -> f(1)
:自顶向下:那算前面的最终res的时候,可不就要去查一查小值对应的f(n)
之前有没有计算过;所以在状态转移之前有lookup的过程; - dp:
f(1) -> f(2) -> f(3) -> .... f(18) -> f(19) -> f(20)
: 自底向上:本来就是从下面往上增加:内含用到了前面temporary计算中间值,就没有lookup过程。
注:dp虽然没lookup过程,但是需要额外数组记录中间值所以额外空间和递归+memo是一样的:但是含义完全不同,虽然说最后表现出来是一样的。
递归中memo的位置:在最前面初始化;每次递归都会经过base case
e.g. Coin change:
递归+memo
def coinChange(coins: List[int], amount: int):
memo = dict()
def dp(n):
if n in memo: return memo[n]. # 查备忘录,避免重复计算
# base case
if n == 0: return 0
if n < 0: return -1
res = float('INF')
for coin in coins:
subproblem = dp(n - coin)
if subproblem == -1: continue
res = min(res, 1 + subproblem)
# 记入备忘录
memo[n] = res if res != float('INF') else -1
return memo[n]
return dp(amount)
dp:
int coinChange(vector<int>& coins, int amount) {
// 数组大小为 amount + 1,初始值也为 amount + 1
vector<int> dp(amount + 1, amount + 1);
// base case
dp[0] = 0;
for (int i = 0; i < dp.size(); i++) {
for (int coin : coins) {
if (i - coin < 0) continue; // 子问题无解,跳过
dp[i] = min(dp[i], 1 + dp[i - coin]);
}
}
return (dp[amount] == amount + 1) ? -1 : dp[amount];
}