1. 程式人生 > 實用技巧 >遞迴優化與動態規劃

遞迴優化與動態規劃

遞迴淺談

談及遞迴問題,大家第一印象肯定是漢諾塔問題或者斐波那契數列問題,當然了,如果你是一位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];
}

小結

大家通過求解最長公共子序列的問題,是不是對遞迴的求解有了更深入的瞭解呢?對於一個問題,我們可以先從暴力演算法開始,然後再從暴力的演算法中尋找優化方案,這樣下來,問題的實現方案必定更加完美。對於求解遞迴問題,我們先使用暴力破解,然後從暴力的演算法裡面找冗餘的計算,找到冗餘之後使用快取表儲存冗餘的計算結果,這樣就避免了暴力遞迴導致的冗餘問題。