1. 程式人生 > >leetcode 746.爬樓梯的最小代價(從暴力遞迴到動態規劃)

leetcode 746.爬樓梯的最小代價(從暴力遞迴到動態規劃)

題目:

On a staircase, the i-th step has some non-negative cost cost[i] assigned (0 indexed).

Once you pay the cost, you can either climb one or two steps. You need to find minimum cost to reach the top of the floor, and you can either start from the step with index 0, or the step with index 1.

題解:

題目要求起點是0或1下標位置處,走到最頂樓梯花費的最小代價是多少。cost[i]=j表示第i節樓梯需要花費的代價是j。每到一層可以跳一階到下一個樓梯,也可以跳二階到下下個樓梯。

輸入輸出描述:

input:cost=[10,15,20]。

output:15

解釋:

      最小代價是15。具體來說,從1下標開始,此時需要花費15,然後跳2階到頂層(跳1階到下標2位置,花費代價增加)。

解題思路:

第一個想法是暴力遞迴(說實話深度優先搜尋自己個人已經比較熟練了,很多時候看到這個題目第一想法就是暴力遞迴,深度優先搜尋,已經形成慣性思維了)。

深度優先搜尋的思路是:“不到南牆不回頭”。具體來說,深度優先搜尋並不管最終決策如何,只管當前決策有幾種可能性,就搜尋多少次。

在這個地方也總結一下暴力遞迴的套路:

暴力遞迴模板:

void depthSearch(a,b):

    if(condition):

          // 達到遞迴終止條件(也就是到了“南牆”),進行相應的操作,比如更新結果陣列,更新其他變數

    else

    //狀態改變

    depthSearch(a+1,b)

    //狀態恢復(也就是“回頭”的過程)

基本上深度優先搜尋就是這麼個流程走,在這個函式的模板中,我定義了兩個引數a,b,實際應用中可以有多個引數,而且a,b引數型別也可以多種多樣,有時候為了儲存結果變數,比如,步數最少,代價最小,這個時候引數就可以宣告成一個int[]result,它的長度是1,因為java中基本資料型別引數傳值是值傳遞,對形參的改變不影響實參,有些時候我們需要對某一層遞迴的引數進行全域性修改,因此可以考慮使用一個長度為1的陣列,這個技巧一定要學會。具有類似性質的技巧還有StringBuilder(需要對String變數進行修改)。

第二個需要考慮的點是,狀態改變,繼續搜尋,狀態恢復這三個步驟怎麼理解。

在上面的部分我已經說過深度優先搜尋只關注當前如何決策,其實決策的過程就是狀態改變的過程,當前決策完畢,就開始繼續深度搜索,那麼,為什麼需要狀態恢復這個過程呢?

我們知道,由於深度優先搜尋只關注當前的決策,對於全域性是否是最優或者最佳決策深度優先並不關心,因此,就需要遍歷所有的情況,當前位置決策結束後,把狀態恢復,就可以繼續進行搜尋。

好了,理論部分就在這裡,下面直接給出程式碼,結合程式碼與理論解釋深度優先搜尋的過程。

    // totalPay表示走到i位置之前需要的總的花費
    // i表示當前走到的位置
    // cost陣列表示每一階樓梯花費的代價
    // result是一個長度為1的整形陣列,表示最小的花費,由於它需要全域性修改,
    // 如果只宣告一個int 變數,由於java值傳遞的特性,在下一次遞迴呼叫時
    // 做不到全域性修改,因此需要弄成陣列,這個技巧一定要會
    public void depthSearch(int totalPay,int i,int[]cost,int[]result){
        if(i>=cost.length){  //這個地方就是撞到“南牆”,也就是遞迴結束條件
        // 可能有人會問,為什麼結束條件是i>=cost.length,i有可能大於cost.length嗎
        // 回答是,可能,我們知道cost陣列是每個樓梯在某一層的花費代價,也就是說
        // cost的長度代表了有幾階樓梯,如果搜尋到i>=cost.length,說明早就到了頂
        // 端,按照題意確實應該停止,那麼什麼情況會出現i>cost.length呢。
        // 按照題目意思,在每一階樓梯都可以往上(或者說往右)走一步或者兩步,如果
        // 我在最後一階樓梯走一步,那麼就會到達cost.lenth位置,如果在最後一階樓
        // 梯走兩步,就會到達cost.length+1位置,也就是到了>cost.length的情況
            if(totalPay<result[0])
                result[0]=totalPay;
        // 上述部分的第二個if(totalPay<result[0])作何解釋呢?
        // 如果i>=cost.length,說明此時已經到達樓頂,也就是說搜尋到一條合法的路徑
        // 我們比較當前這條路徑的花費totalPay和已經搜尋到的花費result[0]哪個更少
        // 繼續更新result[0],result[0]始終儲存著當前搜尋到的合法路徑中花費最小的。
        }
        else{
            // 從這個else開始,是遞迴呼叫,也就是深度優先搜尋的過程
            totalPay+=cost[i];i++; // 狀態改變,表示往上走一步(位置改變),其實還有一個變數也改變
            // 了,就是totalPay,表示花費增加了,因為走了當前位置i,就需要花費cost[i]
            depthSearch(totalPay, i, cost, result); // 深度搜索
            // 在這裡,我為了演示狀態改變和恢復的過程,程式碼多寫了幾行
            // 其實更簡潔的語句是depthSearch(totalPay+cost[i], i+1, cost, result); 
            i--; // 狀態恢復
            i+=2; // 狀態改變,表示往上走兩步
            depthSearch(totalPay+cost[i], i, cost, result);
            i-=2; // 狀態恢復
        }
    }
    public int minCostClimbingStairs(int[] cost) {
        if(cost==null||cost.length==0)
            return 0;
        int[]result=new int[1];
        result[0]=Integer.MAX_VALUE;
        depthSearch(0, 0, cost, result); // 題目中說了可以從0開始,也可以從1開始
        depthSearch(0, 1, cost, result);
        System.out.println(result[0]);
        return result[0];
    }

非常興奮提交,然後,喜聞樂見的超時了。

既然暴力遞迴超時了,一個想法就是將其轉化成動態規劃,那麼,動態規劃該如何設計呢?

動態規劃思想主要是以空間換時間,在這道題中,就是使用一個數組dp,它和cost具有相同的長度

dp[i]=j表示走到位置i需要的最小花費為j

我們知道,每一個位置在到下一個位置的時候都有兩種情況,要麼走一步,也就是走到i+1的位置,

要麼走兩步,也就是走到i+2的位置,換句話說,當前的位置i一定是從i-1走一步過來或者是i-2走兩

步過來的,只有可能是這兩種情況,由於dp[i]需要是最小花費代價,因此上述兩種情況取最小值就

是dp[i]的值,不難寫出下面的程式碼

    public int minCostClimbingStairs(int[] cost) {
        if(cost==null||cost.length==0)
            return 0;
        if(cost.length==1)
            return cost[0];
        if(cost.length==2)
            return Math.min(cost[0], cost[1]);
        int[]dp=new int[cost.length];
        dp[0]=cost[0];
        dp[1]=cost[1];
        int i;
        for(i=2;i<dp.length;i++)
            dp[i]=Math.min(dp[i-1], dp[i-2])+cost[i];
        i=dp.length-1;
        return Math.min(dp[i], dp[i-1]);
    }

總結:很多時候,動態規劃都是因為暴力遞迴耗時太長而想出來的優化演算法,

以空間換時間,從而降低時間複雜度。