算法系列-動態規劃(1):初識動態規劃

昨天,羅拉去面試回來,垂頭喪氣。顯然是面試不順利,我趕緊過去安慰。面試

通過詢問才知道,羅拉麪試掛在了動態規劃。算法

說到動態規劃,八哥可就來精神了,因而就結合勞拉的面試題簡單的和她介紹了動態規劃。數組

事情是這樣的,勞拉的面試官給了她一道題,題目以下:優化

有一個數列,規律以下:一、一、二、三、五、八、13....
若是要求第N個數值,用代碼如何實現。

羅拉一看這題,內心一喜,「這題目,不簡單嗎?」。3d

因而和麪試官賣弄道:「這不是斐波那契數列嗎?這個數列從第3項開始,每一項都等於前兩項之和」。code

面試官笑笑,「沒錯,那麼如何實現求第n個數呢?」blog

「這簡單,稍後」,羅拉絕不含糊,在紙上啪啪寫下幾行代碼,很快哈,兩分鐘不到,她就寫出來了,只用了兩行代碼。遞歸

public class Fibonacci {
    public int rec_fib(int n) {
        if (n == 1 || n == 2) return 1;
        else return rec_fib(rec_fib(n - 1) + rec_fib(n - 2));
    }
}

八哥仔細一看,好傢伙,年輕人不講碼德啊,直接遞歸。ci

在羅拉仔細準備迎接面試官得誇獎的時候。入門

面試官問:「遞歸,不錯,還有更好的方法嗎?」

羅拉懵了,她以爲本身的代碼夠簡單,應該沒啥問題吧。

仔細想了一下子,也沒想出其餘的辦法。最後只能和麪試官互道珍重回家等通知了。


那麼,你們發現這個寫法的問題了嗎?

下面八哥就和你們嘮嗑嘮嗑。

首先,寫法確定是沒問題的,可是問題出在遞歸上面。

下面,咱們分別計算一下n=10n=45 的時候,看看這個程序耗費的時間

public class Fibonacci {
    public static void main(String[] args) {
        long star = System.currentTimeMillis();
        System.out.println(rec_fib(10));
        long end = System.currentTimeMillis();
        System.out.println("計算n=10 耗時:"+(end - star)/1000 + "s");

        star = System.currentTimeMillis();
        System.out.println(rec_fib(45));
        end = System.currentTimeMillis();
        System.out.println("計算n=45 耗時:"+(end - star)/1000 + "s");
    }

    public static long rec_fib(int n) {
        if (n == 1 || n == 2) return 1;
        else return rec_fib(n - 1) + rec_fib(n - 2);
    }
}

輸出結果以下:

55
計算n=10 耗時:0s
1134903170
計算n=45 耗時:3s

發現沒?計算fn(45)的竟然花了三秒多,若是咱們計算100,1000那豈不是原地螺旋爆炸?

那爲啥會計算fn(45)會花這麼多時間呢?接下來咱們就分析分析。

首先咱們根據這個數列的特色,很容易寫出下面的推導公式。

推導公式

而後,咱們能夠畫一下遞歸圖
遞歸圖

發現問題沒有?是否是發現有些數據被屢次計算?好比f(48)被算了兩次,f(47)會被算3次,越往下算的越多。
重複計算

仔細想一想,按照這樣重複計算,n = 50那得重複多少次啊。

咱們再來分析一下羅拉寫的這個算法的時間複雜度。

按照咱們這麼拆分下去,很容易發現,這玩意就基本等於一顆徹底二叉樹了。天然時間複雜度就是:

指數級別的時間複雜度,不爆炸都對不起遞歸了好吧。


出了問題,咱們就要解決問題。

打蛇打七寸,既然知道痛點是重複計算,那咱們從重複計算的地方着手就行了。

咱們很容易想到把計算過的值存起來,用的時候直接用就行了。

好比咱們能夠用數據記錄計算過的值。

羅拉聽完,如有所思,隨後啪啪一份代碼就出來了。

public class Fibonacci {
    public static long men_fib(int n) {
        if (n < 0) return 0;
        if (n <= 2) return 1;
        long[] men = new long[n + 1];
        men[1] = 1;
        men[2] = 1;
        menHelper(men, n);
        return men[n];
    }

    public static long menHelper(long[] men, int n) {
        if (n == 1 || n == 2) return 1;
        if (men[n] != 0) return men[n];
        men[n] = menHelper(men, n - 1) + menHelper(men, n - 2);
        return men[n];
    }
}

使用一個men[n]數組記錄計算過的值,這樣避免了重複計算。

這個時候羅拉又從新執行f(10)和fn(45),查看執行時間.

public class Fibonacci {
    public static void main(String[] args) {
        long star = System.currentTimeMillis();
        System.out.println(men_fib(10));
        long end = System.currentTimeMillis();
        System.out.println("計算n=10 耗時:" + (end - star) / 1000 + "s");

        star = System.currentTimeMillis();
        System.out.println(men_fib(45));
        end = System.currentTimeMillis();
        System.out.println("計算n=45 耗時:" + (end - star) / 1000 + "s");
    }
    public static long men_fib(int n) {
        if (n < 0) return 0;
        if (n <= 2) return 1;
        long[] men = new long[n + 1];
        men[1] = 1;
        men[2] = 1;
        menHelper(men, n);
        return men[n];
    }

    public static long menHelper(long[] men, int n) {
        if (n == 1 || n == 2) return 1;
        if (men[n] != 0) return men[n];
        men[n] = menHelper(men, n - 1) + menHelper(men, n - 2);
        return men[n];
    }
}

執行結果

55
計算n=10 耗時:0s
1134903170
計算n=45 耗時:0s

看,基本都是瞬間執行完。

即便計算f(100),也很快。

3736710778780434371
計算n=100 耗時:0s

效率提高可觀吧,若是羅拉當時這麼作了,至少還能再蹭一杯茶。而後再相忘江湖吧。

咱們使用一個數據記錄計算過的值,至關於整了一個備忘錄,這是遞歸常見的優化方式。這個其實已經有了一點動態規劃的味道。

不過呢,這個帶備忘錄的遞歸屬於自頂向下的方法。那怎麼理解自頂向下呢?廢話很少說,上圖

自頂向下

看這個圖,咱們執行的時候是按照這個順序f(50),f(49)...f(1),f(1)執行的吧,從上往下計算,能夠粗略的認爲這就是自頂向下。

咱們還能夠採用自底向上的方式,也就是按照下面的形式
自底向上

咱們仍是用一個數組dp記錄計算過值,由於咱們已經知道了,第1個和第2個數。因此咱們能夠經過第1個和第2個數。從1開始,遞推出50,這個就是自底向上。

按照這個思路,羅拉很快,一分鐘不到哈,就寫出了代碼,年輕人就是雷厲風行。

public static long fib(int n) {
        if (n == 1 || n == 2) return 1;
        int[] dp = new int[n + 1];
        dp[1] = 1;
        dp[2] = 1;
        for (int i = 3; i <= n; i++)
            dp[i] = dp[i - 1] + dp[i - 1];
        return dp[n];
    }

一樣執行了執行f(10)和fn(45)

public class Fibonacci {
    public static void main(String[] args) {
        long star = System.currentTimeMillis();
        System.out.println(fib(10));
        long end = System.currentTimeMillis();
        System.out.println("計算n=10 耗時:" + (end - star) / 1000 + "s");

        star = System.currentTimeMillis();
        System.out.println(fib(45));
        end = System.currentTimeMillis();
        System.out.println("計算n=100 耗時:" + (end - star) / 1000 + "s");
    }

    public static long fib(int n) {
        if (n == 1 || n == 2) return 1;
        int[] dp = new int[n + 1];
        dp[1] = 1;
        dp[2] = 1;
        for (int i = 3; i <= n; i++)
            dp[i] = dp[i - 1] + dp[i - 1];
        return dp[n];
    }

}

查看執行時間。

55
計算n=10 耗時:0s
1134903170
計算n=45 耗時:0s

答案顯而易見,效果與備忘錄同樣,這個時候咱們再分析一下時間複雜度。

這種自底向上方式就是動態規劃。(ps:自頂向上不等於動態規劃)

整個過程,咱們就用了一個額外數組dp,和一個for循環,那麼很容易獲得時間複雜度爲

這對指數級別的時間複雜度,在N比較大的狀況下,就是降維打擊啊。

可能有人有疑問了,我若是對遞歸用了備忘錄優化,不是能夠達到同樣的效果嗎?這樣的話動態規劃有什麼優點呢?

年輕人別急嘛,動態規劃沒那麼簡單,固然掌握核心思想也不難。

我這只是舉個例子,其實斐波那契數列不必用動態規劃,只是這個例子比較簡單而已,恰好能夠用來入門。

動態規劃也不是用於解決這類問題的。

動態規劃一般用來求解最優化問題,通常此類問題有不少的解,咱們但願找到一個最優的解(好比最大值、最小值)

注意我說的是咱們找的解是一個最優解,而不是最優解,由於一個問題可能有多個解都是最優解。

是否是有點難以理解?那我舉個例子:

好比,我有100米的鋼材,能夠切成不一樣的長度出售,不一樣長度價格不一樣。
就像圖中劃分那樣,若是咱們要賺最多錢,怎麼賣比較好呢?

這個時候你用備忘錄就很難作了吧。

怎樣,沒頭緒了吧,別急用動態規劃就很容易作這類題目,至於怎麼作,且聽下回分解。

歡迎關注八哥:兔八哥雜談,會持續更新一些文章。

此文爲原創文章,轉自啊請註明出處!!!

相關文章
相關標籤/搜索