动态规划系列文章~~~
🌹 发现有需要纠正的地方,烦请指正!
🚀 欢迎小伙伴们的三连+关注!
【动态规划】MATLAB和Python实现-Part01
一、斐波那契数列
斐波那契数列又称兔子数列,其表达式可以用如下方法定义:
{
F
(
0
)
=
0
,
F
(
1
)
=
1
F
(
n
)
=
F
(
n
−
1
)
+
F
(
n
−
2
)
,
n
≥
2
\begin{cases} F(0)=0, F(1)=1 \\ F(n)=F(n-1)+F(n-2),n\geq 2 \end{cases}
{F(0)=0,F(1)=1F(n)=F(n−1)+F(n−2),n≥2
F
(
n
)
F(n)
F(n)表示数列的第
n
n
n 个值。
那么问题是,我们如何求出任意给定的第 n n n 个值?
二、递归
在斐波那契数列中, F ( n ) F(n) F(n)的计算要依靠于其子问题: F ( n − 1 ) F(n-1) F(n−1) 和 F ( n − 2 ) F(n-2) F(n−2),如果对于程序而言,就是程序调用自身的过程,我们可以称这个过程为 递归。
递归,能够将一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题(即:子问题)来求解。
递归策略只需要少量的程序就可以描述出解题过程中所需要的多次重复计算,大大减少了程序的代码量。
构成递归需要满足的条件:
- 子问题需要与原始问题有相同的思路,二者之间存在着联系,且逐渐变得简单;
- 不能无限调用自身,需要存在程序出口,出口就不再进行递归。
2.1 以阶乘计算为例
- 原问题:计算 f ( n ) = n ! f(n)=n! f(n)=n!
- 子问题:计算 f ( n − 1 ) = ( n − 1 ) ! f(n-1)=(n-1)! f(n−1)=(n−1)!
- 联系: f ( n ) = n × f ( n − 1 ) f(n)=n\times f(n-1) f(n)=n×f(n−1)
- 出口: f ( 1 ) = 1 f(1)=1 f(1)=1
下面我们以MATLAB
和Python
两种语言来实现该递归过程:
MATLAB
函数实现:
function f=factorial(n)
if n == 1 % 递归的出口
f = 1;
else
f = n*factorial(n-1);
end
end
函数调用:
Python
函数实现:
def factorial(n):
if n==1:
return 1
else:
return n*factorial(n-1)
函数调用:
2.2 以斐波那契数列计算为例
- 原问题:计算 F ( n ) F(n) F(n)
- 子问题:计算 F ( n − 1 ) F(n-1) F(n−1) 和 F ( n − 2 ) F(n-2) F(n−2)
- 联系: F ( n ) = F ( n − 1 ) + F ( n − 2 ) F(n)=F(n-1)+F(n-2) F(n)=F(n−1)+F(n−2)
- 出口: F ( 0 ) = 1 , F ( 1 ) = 1 F(0)=1, F(1)=1 F(0)=1,F(1)=1
以MATLAB
和Python
两种语言来实现该递归过程:
MATLAB
函数实现:
function F = fib_dg(n)
if n == 0
F = 0;
elseif n == 1
F = 1;
else
F = fib_dg(n-1) + fib_dg(n-2);
end
end
函数调用:
Python
函数实现:
def fib_dg(n):
if n==0:
return 0
elif n==1:
return 1
else:
return fib_dg(n-1)+fib_dg(n-2)
函数调用:
2.3 对递归的反思与改进
对于斐波那契数列,在将函数实现后,我们进一步研究其时间复杂度。
在MATLAB
中,我们可以用tic toc
函数来显示程序运行时间,我们借助其来比较当
n
=
20
n=20
n=20 和
n
=
40
n=40
n=40 时的运行时间:
clear;clc;
tic;
fib_dg(20)
toc;
tic;
fib_dg(40)
toc;
输出结果:
我们注意到,随着
n
n
n 不断增大,程序运行所需要的时间也急剧增加。
而且,我们在计算 fib_dg(40)
时,其实已经有了fib_dg(20)
的结果了,但程序仍然去计算,这就造成了时间的消耗。
那我们如何改进?
有两种思路:
- 带有备忘录的递归算法
- 自底向上法
递归一般是自顶向下,即从一个较大的问题的开始,向下逐步分解,直至到达程序出口,然后再逐层返回答案。
因此,带有备忘录的递归算法也称为带有备忘录的自顶向下法。
自底向上就是从子问题开始,逐步向复杂靠近。
2.3.1 带有备忘录的递归算法
直接利用递归求解耗时的原因的是重复计算,那么我们可以建立一个备忘录,每次算出某个子问题的答案后,将其记录到备忘录里面后再返回。
这样,每次遇到一个新的子问题,就可以先去备忘录里查找是否存在,如果存在,就可以直接拿来用,而不需要再耗时计算了。
接下来我们用MATLAB
和Python
实现该算法,并与先前的算法的时间复杂度进行对比:
MATLAB
函数实现:
在
MATLAB
中,为了实现备忘录功能,我们需要用global
来定义全局变量memo
来存储结果
function F = fib_with_memo(n)
global memo;
if n == 0 % 出口1
F = 0;
elseif n == 1 % 出口2
F = 1;
elseif memo(n+1) == -1 % 备忘录中没有
% 因为斐波那契数列是从0开始,而MATLAB中向量索引是从1开始,所以是n+1
F = fib_with_memo(n-1)+fib_with_memo(n-2);
memo(n+1) = F; % 将结果写入备忘录
else
F = memo(n+1);
end
end
函数调用:
clear;clc;
global memo;
memo = -1*ones(1,50);
tic;
fib_with_memo(20)
toc;
tic;
fib_with_memo(40)
toc;
输出结果:
我们我们很容易发现,相比于先前的递归算法,带有备忘录的递归算法 时间复杂度 大大降低!
在计算
n
=
40
n=40
n=40 时,由原先的 7.74秒 左右,下降到 0.003秒 左右。
Python
函数实现:
def fib_with_memo(n):
if n==0:
return 0
elif n==1:
return 1
elif memo[n]==-1:
result = fib_with_memo(n-1)+fib_with_memo(n-2)
memo[n] = result
return result
else:
return memo[n]
函数调用:
import time
start1 = time.time()
print('fib_dg(40):')
print(fib_dg(40))
end1 = time.time()
print('原递归算法用时(s):')
print(end1-start1)
print()
start2 = time.time()
print('fib_with_memo(40):')
memo = [-1 for i in range(50)]
print(fib_with_memo(40))
end2 = time.time()
print('带有备忘录的递归算法用时(s):')
print('%.8f'% (end2-start2))
输出结果:
同样发现,带有备忘录的递归算法 时间复杂度大大降低!
2.3.2 自底向上法
自底向上,就是和自顶向下相反,我们直接从问题的最小规模。逐步向上计算,直到得到我们想要的答案
接下来我们用MATLAB
和Python
实现该算法,并与先前的算法的时间复杂度进行对比:
MATLAB
函数实现:
function F = fib_botm_up(n)
result = -1*ones(1, n+1);
result(1) = 0; % n=0
result(2) = 1; % n=1
if n == 0
F = 0;
elseif n == 1
F = 1;
else
for i = 2:n+1
result(i+1) = result(i)+result(i-1);
end
F = result(n+1);
end
end
函数调用:
clear;clc;
tic;
fib_botm_up(40)
toc;
输出结果:
由结果可知,自底向上相比于原先的递归方法 时间复杂度更低!
Python
函数实现:
def fib_botm_up(n):
result = [-1 for i in range(n+1)]
result[0] = 0
result[1] = 1
if n==0:
return 0
elif n==1:
return 1
else:
for i in range(2, n+1):
result[i] = result[i-1]+result[i-2]
return result[n]
函数调用:
start1 = time.time()
print('fib_dg(40):')
print(fib_dg(40))
end1 = time.time()
print('原递归算法用时(s):')
print(end1-start1)
print()
start3 = time.time()
print('fib_botm_up(40):')
print(fib_botm_up(40))
end3 = time.time()
print('自底向上算法用时(s):')
print('%.8f' % (end3-start3))
输出结果:
同样发现,自底向上算法 的时间复杂度大大降低!