算法小专栏开篇:DFS之Fibonaci数列
Preface
从今天开始会给大家介绍一些我在学习软件算法时期的一些题目或者笔记总结,本身我也不是计软班科的,也不是打ACM的,所以也不要指望我来给你讲解Leetcode或者Linkcode了,遇到Hard题在下实在也是爱莫能助。
一些关于计算机专业术语的词我会比较少用,采用一些直观的数据来体现~
年轻人不讲code德,各种剪枝优化,各种记忆法,各种退出条件,咳咳咳……(吐血ing
Brief Introduction to DFS
今天从**DFS(深度优先搜索)**开始,一开始学的时候老师给了道测试题,我直接懵了,DFS?可能大计基的时候或者学C语言的时候讲过,简单的结构还是会的。后面开始去刷题,啊嘞?
但是现在来看其实DFS其是以套路为先,判断逻辑为主,剪枝优化为辅的一种算法思路。
好啦,我们来大概介绍一下,深度优先搜索DFS和广度优先搜索BFS是两种很重要的软件算法,在图论里面有很多应用,像最短路径,全局查找等优化问题。
深度优先,即是在当前既定的搜索策略下不断进行下去,一条路走到黑。可以这么说,如果行不通了,退出当前的循环,改变搜索策略,继续循环往复,直到找到出口,是不是很像小白鼠走迷宫。
意思很好懂,我认为有关键三个点:
-
第一个是搜索策略,这是核心算法,自不用多说
-
第二个是循环的前后参量传递,即我走过的路我不走了,或者通过一条失败的路pass掉其他n条类似的路直接不走,节省时间空间,其中一些变量的传递。
-
第三个是退出条件,没有退出条件,就是无限循环,Happy Ending!
上面的 Key Point 会在以后的内容里在逐步体现~大家也不用有太多心里负担,就当小百科。
Implementation of Fibonaci using DFS
今天我还是从 Fibonaci(斐波那契数列) 开始,给大家介绍DFS。
所用环境是 Windows10+Python3.7。
语法都是很简单的,本身就是解释性语言,通俗易懂。
斐波那契数列很常见:1,1,2,3,5,……
从第三项开始,每一项都是前两项的和。假设我要求第n项,我是不是要知道第n-1项和第n-2项,要求第n-1项,又要知道第n-2项和第n-3项,以此类推,就是一个不断循环的过程,啥时候是个头呢?
我们知道第一项和第二项都是1,那当递归到这两项时,直接返回1便可以了。就好像在一个迷宫,每一层有一个房间,每一层的房间里有打开上一层需要的钥匙,最上面一层的房间有奥利奥,怎么吃到奥力给呢?
直观思路:你只需要走到最低的一层,拿到最低两层的钥匙,便可以一直往上走,期间不断开门,收集钥匙,直到打开最上面一层的房间。
来看看code:
def fibonaci():
"""
Function: get n_th value in Fibonacci_array using DFS
Returns: None
"""
import time
# input the index
n=int(input())
# core function
def dfs(x):
if x == 1 or x == 2:
return 1
else:
# recursion step
return dfs(x - 1) + dfs(x - 2)
# record processing time
start=time.time()
# print answer
print(dfs(n))
print("Using {:.5f} s".format(time.time() - start))
return
>>> fibonaci()
20
6765
Using 0.01405 s
>>> fibonaci()
35
9227465
Using 2.22722 s
上面便是按照我们的思路写出的DFS求解第n项斐波那契数列的函数,其中time库是为了计算程序执行的时间,核心函数的思路便是当递归到第一项或者第二项直接返回1,否则通过函数递归求解对应前两项的值。
但是当n太大的时候,你会发现很慢出结果。求解第35项用了2.22秒,简直不能忍。
原因是,每求一个第n项,我们都必须求到0~n-1项,而且这个过程在子过程里,即求第n-1项的时候也是一样,算法的时间复杂度随着N的增大呈现指数增长,时间的复杂度为O(2^n),即2的n次方,算法进行的过程形象上说可以看成是在遍历一棵二叉树。
Memory Search in DFS
改进方法有很多,比如化为矩阵乘法并应用快速幂运算等,还有记忆化搜索。
记忆化搜索是DFS常用的一种优化剪枝策略,剪枝这里的意思就很形象了,就是把没必要的节点给去掉,不去递归到那里。
我们可以容易知道,求解第n项和第n-1项在上面的函数里的过程基本上是一样的,也就是说程序做了很多无用功,要是我们在求解过程中把中间的过程记录下来,如果再次需要到该值直接调用,就可以提高效率,即用空间换时间。
说干就干!
def fibonaci():
"""
Function: get n_th value in Fibonacci Array using DFS with memory search
Returns: None
"""
import time
# input the index
n=int(input())
# store intermediate results
ans=[False for i in range(n + 1)]
# core function
def dfs(x):
nonlocal ans
if ans[x]:
return ans[x]
if x == 1 or x == 2:
ans[x] = 1
return 1
else:
ans[x] = dfs(x - 1) + dfs(x - 2)
return ans[x]
# record processing time
start=time.time()
# print answer
print(dfs(n))
print("Using {:.5f} s".format(time.time() - start))
return
>>> fibonaci()
35
9227465
Using 0.00751 s
>>> fibonaci()
50
12586269025
Using 0.00795 s
>>> fibonaci()
100
354224848179261915075
Using 0.01003 s
可以看到记忆化搜索的方法在求解第35项时只用了0.01153,实际上的时间复杂度基本维持在线性 O(N) 的级别,nice!
Little Trick
实际上用递归的方法会受到函数递归栈数量的限制,默认是1000,可以采用非递归的形式求解,但是不在今天的范围,就不做介绍,大家可以自行查阅相关资料喔~
另外有个小技巧,在有些时候确实是需要求解斐波那契数列第n项,但是n的不是太大(less than 50),那可以用下面这种较快的lambda匿名函数方式,简洁~
# Lambda function definition
Fibo = lambda x: 1 if x == 1 or x == 2 else Fibo(x-1) + Fibo(x-2)
# test
>>> Fibo(20)
6765
>>> Fibo(35)
9227465
其实就是把非记忆化搜索的DFS核心函数简化为匿名函数,这一点在以后的某些题目里会经常使用,个人觉得还是比较方便的。
今天就先到这啦,有问题意见的仔仔可以在博客或者公众号提问,会及时回复哒!