演算法小專欄開篇:DFS之Fibonaci數列
演算法小專欄開篇:DFS之Fibonaci數列
- Preface
- Brief Introduction to DFS
- Implementation of Fibonaci using DFS
- Memory Search in DFS
- Little Trick
- Reference
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核心函式簡化為匿名函式,這一點在以後的某些題目裡會經常使用,個人覺得還是比較方便的。
今天就先到這啦,有問題意見的仔仔可以在部落格或者公眾號提問,會及時回覆噠!