遞迴優化與動態規劃
遞迴淺談
談及遞迴問題,大家第一印象肯定是漢諾塔問題或者斐波那契數列問題,當然了,如果你是一位LeetCode愛好者,肯定遇到了許多遞迴問題或者遞迴的變形問題。遞迴問題的求解主要是把一個大問題分解為子問題,但在計運算元問題的時候,重複計算了大量的子問題。那麼什麼是遞迴呢,具體什麼時候用呢?請容我一一道來, 維基百科中明確給出了遞迴的定義,即遞迴(Recursion)是指在函式的定義中使用函式自身的方法。也就是函式內部存在著呼叫函式本身的情況,這種現象即為遞迴。所以,既然明確了遞迴的定義,那麼什麼時候使用呢?在應用遞迴方法求解問題時,當該問題可以分解成具有相同解決方案的子問題,甚至是子子問題的時候,換言之,所有的這些問題都呼叫同一個功能函式,這個時候可以使用遞迴的方法求解,當然了,需要注意的是求解的問題最後必須是有一個固定值的,即存在遞迴終止條件,否則,該問題無解。經過判斷問題是否可以用遞迴後,接下來便是遞迴求解思路(滿滿套路),第一步,根據問題需求,定義遞迴函式
在這裡,我們使用遞迴方法求解斐波那契數列數列問題進行闡明,求解斐波那契數列問題遞迴C++程式碼如下
int fb(int n) { if (n <= 2) return 1; return fb(n - 1) + fb(n - 2); }
雖然程式碼很簡潔,但這段程式碼存在一個致命問題,當n很大時,求解斐波那契數將耗費大量的時間,這是因為很多數都被重複計算了,比如計算fb(10)的時候,需要計算fb(9)和fb(8)。於是,遞迴分別獨立的去計算fb(9)和fb(8),但是fb(9)和fb(8)的子問題,在計算fb(9)的時候,就已經計算出來了fb(8),但這個結果未儲存,所以在計算fb(8)的時候,又要重新計算一次,這樣計算下來浪費了大量的時間。
遞迴變形記
既然我們發現了遞迴求解存在的問題,那麼就要著手解決它,可以發現,遞迴存在大量的重複計算,那麼,如果我們將計算的子問題的結果進行快取一下,是不是就可以了,當然是滴!比如對於上面的遞迴問題,我們使用以為陣列作為快取表,修改上述程式碼,可以得到優化後的遞迴程式碼
nt fb(int n, vector<int > &dp) { if (dp[n] != -1) return dp[n]; if (n == 0) return 0; if (n == 1) return 1; int res = fb(n - 1, dp) + fb(n - 2, dp); dp[n] = res; return res; }
當然了,對於遞迴問題的優化除了使用快取表外,還可以直接使用動態規劃的方式解決。遞迴是自頂向下求解問題,而動態規劃則是自底向上求解問題,所以,我們將快取冗餘的計算結果的計算過程,變為自底向上的過程就是動態規劃求解方式。
舉個栗子
最長公共子序列(LCS)求解問題
LCS定義:最長公共子序列問題是在一組序列中找到最長公共子序列,需要注意的是不同於子串問題,即LCS不需要是連續的子串。
LCS示例:
示例1
輸入:"ABCD"和"EDCA"
輸出:1
解釋:LCS是"A"或"C"或"D"
示例2
輸入:"ABCD"和"EACB"
輸出:2
解釋:LCS是"AC“
LCS實現(C++)
case1:暴力遞迴
/*暴力遞迴*/ int search1(int ida, int idb) { if (ida > len_a || idb > len_b) return 0; if (ida == len_a && idb == len_b) return 0; int res = INT_MIN; if (a[ida] == b[idb]) { res = search1(ida + 1, idb + 1) + 1; } return mymax(res, search1(ida + 1, idb), search1(ida, idb + 1)); }
case2:快取表優化
/*快取表優化*/ int dp[len_a][len_b]; int search2(int ida, int idb) { if (ida > len_a || idb > len_b) return 0; if (ida == len_a && idb == len_b) return 0; if (dp[ida][idb] != -1) return dp[ida][idb]; else { int res1 = INT_MIN; if (a[ida] == b[idb]) { res1 = search2(ida + 1, idb + 1) + 1; } int res2 = search2(ida + 1, idb); int res3 = search2(ida, idb + 1); int res = mymax(res1, res2, res3); dp[ida][idb] = res; return res; } }
case3:動態規劃
/*動態規劃*/ int search3(int ida, int idb) { //初始化最末行 for (int j = len_b - 1; j >= 0; j--) { if (b[j] != a[len_a - 1]) dp[len_a - 1][j] = 0; else { for (int k = 0; k <= j; k++) { dp[len_a - 1][k] = 1; } break; } } //初始化最右列 for (int i = len_a - 1; i >= 0; i--) { if (a[i] != b[len_b - 1]) dp[i][len_b - 1] = 0; else { for (int k = 0; k <= i; k++) { dp[k][len_b - 1] = 1; } break; } } for (int i = len_a - 2; i >= 0; i--) { for (int j = len_b - 2; j >= 0; j--) { int res1 = INT_MIN; if (a[i] == b[j]) res1 = dp[i + 1][j + 1] + 1; int res2 = dp[i+1][j]; int res3 = dp[i][j+1]; int res = mymax(res1, res2, res3); dp[i][j] = res; } } return dp[0][0]; }
小結
大家通過求解最長公共子序列的問題,是不是對遞迴的求解有了更深入的瞭解呢?對於一個問題,我們可以先從暴力演算法開始,然後再從暴力的演算法中尋找優化方案,這樣下來,問題的實現方案必定更加完美。對於求解遞迴問題,我們先使用暴力破解,然後從暴力的演算法裡面找冗餘的計算,找到冗餘之後使用快取表儲存冗餘的計算結果,這樣就避免了暴力遞迴導致的冗餘問題。