解决递归中的重复计算问题
一、重疊子問題
斐波那契數列沒有求最值的問題,因此嚴格來說它不是最優解問題,當然也就不是動態規劃問題。但它能幫助你理解什么是重疊子問題。首先,它的數學形式即遞歸表達是這樣的:
這個代碼本身沒有問題, 但是它的效率極低。假設上面的函數調用輸入是10,把遞歸樹畫出來:
如果要計算原問題F(10), 就需要先計算出問題F(9)和F(8), 如果要計算F(9), 就需要先計算出子問題F(8) 和 F(7),以此類推。這個遞歸的終止條件是當F(1)=1 或 F(0)=0時結束。
看完斐波那契數列的求解樹之后,就會發現問題:
- 用紅色標出的兩個區域中,它們的遞歸計算過程完全相同
這意味著,第2個紅色區域的計算是 “完全沒有必要的”,它是重復的計算。因為我們已經在求解F(7)的時候把F(6)的所有情況算過了。因此我們把第2個紅色區域的計算稱為重疊子問題。
二、使用備忘錄解決重復問題
既然存在重復的子問題,那我們在遇到這些重復的子問題時,只需要執行一次即可,即消滅重復計算的過程。
我們可以創建一個備忘錄(memorization),在每次計算出某個子問題的答案后,將這個臨時的中間結果記錄到備忘錄里,然后再返回。
接著,每當遇到一個子問題時,我們不是按照原有的思路開始對子問題進行遞歸求解,而是先去這個備忘錄中查詢一下。如果發現之前已經解決過這個子問題了,那么就直接把答案取出來復用,沒有必要再遞歸下去耗時的計算了。
對于備忘錄,可以考慮使用以下兩種數據結構:
- 數組(Array),通常對于簡單的問題來說,使用一維數組就足夠了。
- 哈希表(Hash table),如果你存儲的狀態不能直接通過索引找到需要的值(比如斐波那契數列問題,你可以直接通過數組的索引確定其對應子問題的解是否存在,如果存在你就拿出來直接使用),比如你使用了更高級的數據結構而非簡單的數字索引,那么你還可以考慮使用哈希表,即字典來存儲中間狀態,來避免重復計算的問題。
下面看看用數組實現的備忘錄來解決斐波那契數列的代碼:
def Fibonacci(n, memo):if (n == 0 or n == 1):return n# 如果備忘錄中找到了之前計算的結果,那就直接返回,避免重復計算if (memo[n] != None):return memo[n]if (n > 1):memo[n] = Fibonacci(n - 1, memo) + Fibonacci(n - 2, memo)return memo[n]# 如果數值無效(比如 < 0),則返回0return 0def FibonacciAdvance(n):memo = [None] * (n + 1)return Fibonacci(n, memo)def main():result = FibonacciAdvance(4)print(result)if __name__ == "__main__":main()實際上,這就是我們所熟知的“剪枝與優化”,把一棵存在巨量冗余的遞歸樹通過剪枝,改造成了一幅不存在冗余的遞歸圖,極大減少了子問題(即遞歸圖中節點)的個數。通過這種方式,我們大幅縮減了算法的計算量,因為所有重復的部分都被跳過了
三、重疊子問題的限制
有些問題雖然看起來像包含“重疊子問題”的子問題,但是這類子問題可能具有后效性,但我們追求的是無后效性。
所謂無后效性,指的是在通過 A 階段的子問題推導 B 階段的子問題的時候,我們不需要回過頭去再根據 B 階段的子問題重新推導 A 階段的子問題,即子問題之間的依賴是單向性的。
換句話說,如果一個問題可以通過重疊子問題緩存進行優化,那么它肯定都能被畫成一棵樹。
四、方法的弊端
通過重疊子問題緩存可以極大加速我們的代碼執行效率。但是凡事都有兩面性,毋庸置疑,這種方案肯定是通過某種犧牲換取了性能的提升。
在硬幣找零問題中,我們就可以利用備忘錄來避免重復計算。但這樣有個問題,如果我們的錢幣總額數量非常巨大,那這個數組的大小就會非常巨大,導致的結果就是會占據大量的內存存儲空間,而且有很多的數字其實是不會被求解的,存在很多的“存儲空洞”。顯然,這是一種浪費。
所以在解題的過程中,你需要根據實際情況,在空間和時間中尋求一個平衡,將這個問題考慮在內。
五、總結與升華
備忘錄的思想極為重要,特別是當求解的問題包含重疊子問題時,只要問題包含重復計算,你就應該考慮使用備忘錄來對算法時間復雜度進行簡化。具體來說,備忘錄解法可以歸納為:
1.用數組或哈希表來緩存已解的子問題答案,并使用自頂向下的遞歸順序遞歸數據;
2.基于遞歸實現,與暴力遞歸的區別在于備忘錄為每個求解過的子問題建立了備忘錄(緩存);
3.為每個子問題的初始記錄存入一個特殊的值,表示該子問題尚未求解;
4.在求解過程中,從備忘錄中查詢。如果未找到或是特殊值,表示未求解;否則取出該子問題的答案,直接返回。
總結
以上是生活随笔為你收集整理的解决递归中的重复计算问题的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: python 接雨水
- 下一篇: 今天休息xxxxx