返回 LeetCode 刷题

Markdown File

动态规划DP入门(记忆化搜索)

动态规划DP入门(记忆化搜索).md

1先看调用装饰器函数解决一个斐波那契求和的问题
2def memo(f):
3 cache = {}
4 def memorized(n):
5 if n not in cache:
6 cache[n] = f(n)
7 return cache[n]
8 return memorized
9
10@memo
11def fib(n):
12 if n == 0 or n == 1:
13 return 1
14 return fib(n-1) + fib(n-2)
15
16def main():
17 T = int(input().strip())
18 for _ in range(T):
19 n = int(input().strip())
20 print(fib(n))
21
22动态规划 DP(入门)
23二、前提条件
24如果没有 cache:
25
26int fib(int n)
27{
28 if(n==0||n==1)
29 return 1;
30
31 return fib(n-1)+fib(n-2);
32}
33
34例如:fib(5)
35
36会变成:fib(5)
37
38├── fib(4)
39│ ├── fib(3)
40│ └── fib(2)
41
42└── fib(3)
43 ├── fib(2)
44 └── fib(1)
45
46继续展开:
47
48fib(2)
49fib(3)
50fib(2)
51会被反复计算。
52
53三、什么叫重叠子问题
54
55观察上图:fib(3)出现两次。
56fib(2)出现三次。
57
58实际上:fib(20)
59里面:
60fib(18)
61fib(17)
62fib(16)
63...
64
65会被算无数次。
66
67这就是:重叠子问题
68定义:同一个子问题被重复求解。
69
70这是DP出现的第一个条件。
71
72四、 cache 干了什么
73
74第一次:fib(5)
75
76需要:fib(4)
77
78于是计算:fib(4)=5
79
80然后:cache[4]=5
81保存下来。
82
83以后再需要:fib(4)
84
85直接:return cache[4]
86
87不再递归。
88
89因此:
90每个 fib(k)
91只会真正计算一次
92
93五、用 C 语言模拟 cache
94
95 Python:
96
97cache[n]=f(n)
98
99在 C 里面可以写成:
100
101int cache[1000];
102
103初始化:
104
105for(int i=0;i<1000;i++)
106{
107 cache[i]=-1;
108}
109
110表示:
111-1
112代表没算过
113六、记忆化搜索
114定义缓存
115int cache[1000];
116递归
117int fib(int n)
118{
119 if(n==0||n==1)
120 return 1;
121
122 if(cache[n]!=-1)
123 return cache[n];
124
125 cache[n]=fib(n-1)+fib(n-2);
126
127 return cache[n];
128}
129或者
130int fib(int n)
131{
132 if(n==0||n==1)
133 {
134 return 1;
135 }
136
137 if(cache[n]==-1)
138 {
139 cache[n]=fib(n-1)+fib(n-2);
140 }
141
142 return cache[n];
143}
144模板就是这样:
145int dfs(...)
146{
147 if(边界)
148 return ...;
149
150 if(memo[状态]==-1)
151 {
152 memo[状态]=递归计算;
153 }
154
155 return memo[状态];
156}
157主要还是要保持状态,不太方便,更好的方式是拿DP数组
158七、执行过程
159
160例如:fib(5)
161
162第一次:cache[5]=-1
163
164说明没算过。
165
166于是:fib(4)+fib(3)
167
168算完后:cache[5]=8保存。
169
170以后再调用:
171
172fib(5)
173
174直接:return cache[5];
175
176结束。
177
178八、为什么这已经属于 DP
179
180很多教材会说:
181
182DP要写dp数组
183
184实际上不准确。
185
186写法:
187递归+缓存
188已经是DP。
189
190学术名字:
191Memoization
192记忆化搜索
193
194关系:
195
196暴力递归
197
198
199
200加缓存
201
202
203
204记忆化搜索
205
206
207
208动态规划
209九、DP到底在干什么
210实际上DP只干一件事:
211
212避免重复计算
213
214例如:fib(30)暴力递归:算几十万次
215
216记忆化搜索:
217
218fib(0)
219fib(1)
220...
221fib(30)
222
223每个只算一次。
224
225十、进一步观察
226代码里:
227
228fib(n)=fib(n-1)+fib(n-2)
229其实可以写成:
230
231dp[n]=dp[n-1]+dp[n-2]
232
233这里:fib(n)
234就是:状态 State
235
236所以:dp[i]
237表示第i项斐波那契数
238
239十一、状态转移方程
240
241DP最重要的东西:状态转移方程
242
243对于斐波那契:dp[i]=dp[i−1]+dp[i−2]
244
245意思:当前状态
246由之前状态推出来
247
248十二、为什么以后不用递归
249
250fib(100)
251
252会递归:100层
253
254虽然有cache。
255
256但还是有:函数调用开销
257
258于是可以直接写:
259
260dp[0]=1;
261dp[1]=1;
262
263然后:
264
265for(i=2;i<=n;i++)
266{
267 dp[i]=dp[i-1]+dp[i-2];
268}
269
270
Rendered Preview

先看调用装饰器函数解决一个斐波那契求和的问题
def memo(f):
cache = {}
def memorized(n):
if n not in cache:
cache[n] = f(n)
return cache[n]
return memorized

@memo
def fib(n):
if n == 0 or n == 1:
return 1
return fib(n-1) + fib(n-2)

def main():
T = int(input().strip())
for _ in range(T):
n = int(input().strip())
print(fib(n))

动态规划 DP(入门)
二、前提条件
如果没有 cache:

int fib(int n)
{
if(n==0||n==1)
return 1;

return fib(n-1)+fib(n-2);

}

例如:fib(5)

会变成:fib(5)

├── fib(4)
│ ├── fib(3)
│ └── fib(2)

└── fib(3)
├── fib(2)
└── fib(1)

继续展开:

fib(2)
fib(3)
fib(2)
会被反复计算。

三、什么叫重叠子问题

观察上图:fib(3)出现两次。
fib(2)出现三次。

实际上:fib(20)
里面:
fib(18)
fib(17)
fib(16)
...

会被算无数次。

这就是:重叠子问题
定义:同一个子问题被重复求解。

这是DP出现的第一个条件。

四、 cache 干了什么

第一次:fib(5)

需要:fib(4)

于是计算:fib(4)=5

然后:cache[4]=5
保存下来。

以后再需要:fib(4)

直接:return cache[4]

不再递归。

因此:
每个 fib(k)
只会真正计算一次

五、用 C 语言模拟 cache

Python:

cache[n]=f(n)

在 C 里面可以写成:

int cache[1000];

初始化:

for(int i=0;i<1000;i++)
{
cache[i]=-1;
}

表示:
-1
代表没算过
六、记忆化搜索
定义缓存
int cache[1000];
递归
int fib(int n)
{
if(n==0||n==1)
return 1;

if(cache[n]!=-1)
    return cache[n];

cache[n]=fib(n-1)+fib(n-2);

return cache[n];

}
或者
int fib(int n)
{
if(n==0||n==1)
{
return 1;
}

if(cache[n]==-1)
{
    cache[n]=fib(n-1)+fib(n-2);
}

return cache[n];

}
模板就是这样:
int dfs(...)
{
if(边界)
return ...;

if(memo[状态]==-1)
{
    memo[状态]=递归计算;
}

return memo[状态];

}
主要还是要保持状态,不太方便,更好的方式是拿DP数组
七、执行过程

例如:fib(5)

第一次:cache[5]=-1

说明没算过。

于是:fib(4)+fib(3)

算完后:cache[5]=8保存。

以后再调用:

fib(5)

直接:return cache[5];

结束。

八、为什么这已经属于 DP

很多教材会说:

DP要写dp数组

实际上不准确。

写法:
递归+缓存
已经是DP。

学术名字:
Memoization
记忆化搜索

关系:

暴力递归

加缓存

记忆化搜索

动态规划
九、DP到底在干什么
实际上DP只干一件事:

避免重复计算

例如:fib(30)暴力递归:算几十万次

记忆化搜索:

fib(0)
fib(1)
...
fib(30)

每个只算一次。

十、进一步观察
代码里:

fib(n)=fib(n-1)+fib(n-2)
其实可以写成:

dp[n]=dp[n-1]+dp[n-2]

这里:fib(n)
就是:状态 State

所以:dp[i]
表示第i项斐波那契数

十一、状态转移方程

DP最重要的东西:状态转移方程

对于斐波那契:dp[i]=dp[i−1]+dp[i−2]

意思:当前状态
由之前状态推出来

十二、为什么以后不用递归

fib(100)

会递归:100层

虽然有cache。

但还是有:函数调用开销

于是可以直接写:

dp[0]=1;
dp[1]=1;

然后:

for(i=2;i<=n;i++)
{
dp[i]=dp[i-1]+dp[i-2];
}