1. 程式人生 > 其它 >【動態規劃】——最低加油次數

【動態規劃】——最低加油次數

871. 最低加油次數 - 力扣(LeetCode) (leetcode-cn.com)

汽車從起點出發駛向目的地,該目的地位於出發位置東面 target 英里處。

沿途有加油站,每個 station[i] 代表一個加油站,它位於出發位置東面 station[i][0] 英里處,並且有 station[i][1] 升汽油。

假設汽車油箱的容量是無限的,其中最初有 startFuel 升燃料。它每行駛 1 英里就會用掉 1 升汽油。

當汽車到達加油站時,它可能停下來加油,將所有汽油從加油站轉移到汽車中。

為了到達目的地,汽車所必要的最低加油次數是多少?如果無法到達目的地,則返回 -1 。

注意:如果汽車到達加油站時剩餘燃料為 0,它仍然可以在那裡加油。如果汽車到達目的地時剩餘燃料為 0,仍然認為它已經到達目的地。

分析:這道題一開始我想了很久,單純考慮在某一個加油站加油後能否撐到下一個加油站,又或者是否能撐到終點,結果發現情況很複雜,程式碼越加越長,卻依然過不了。於是只能去學習大佬解法。不得不說,解決動態規劃問題設定狀態真是一門技術活兒,狀態量設定得巧妙題目難度就會直線下降。

設dp[i][j]表示經過i個加油站,加了j次油後車輛能行駛的最大距離。那麼,按照題目要求,我們只需要找出經過n個加油站後能夠到達目的地的最少加油次數j。即dp[n][j]>=target的最小j值。對於途徑的加油站i,我們都有兩個選擇:加油或不加油,如果我們在第i站選擇不加油,那麼我們經過第i個加油站後能行駛的最大距離dp[i][j]=dp[i-1][j]。如果我們選擇在第i個加油站加油,那麼我們能行駛的最大距離dp[i][j]=dp[i-1][j-1]+stations[i][1]。以上兩種情況成立的前提是我們上一狀態的最大距離能夠撐到第i個加油站。

class Solution {
public:
    int minRefuelStops(int target, int startFuel, vector<vector<int>>& stations) {
        int n=stations.size();
        if(n==0){
            if(startFuel>=target) return 0;
            else return -1;
        }
        vector<vector<long long>> dp(n+1,vector<long long>(n+1,0)); //將起點記作第零個加油站,便於處理。
        //dp[i][j]:經過i個加油站前一共加了j次油能跑的最遠距離
        for(int i=0;i<=n;i++){
            dp[i][0]=startFuel;  //如果一次油都不加,那麼無論經過多少個加油站最遠距離都只有startFuel。
        }
        for(int i=1;i<=n;i++){
            for(int j=1;j<=i;j++){
                if(dp[i-1][j]>=stations[i-1][0]){ //第i站不加油
                    dp[i][j]=dp[i-1][j];
                }
                if(dp[i-1][j-1]>=stations[i-1][0]){ //第i站加油
                    dp[i][j]=max(dp[i][j],dp[i-1][j-1]+stations[i-1][1]);
                }
            }
        }
        for(int j=0;j<=n;j++){
            if(dp[n][j]>=target){
                return j;
            }
        }
        return -1;
    }
};

空間優化:觀察到上述的狀態轉換式,不難看出當前狀態dp[i][j]只與i-1時的狀態有關,因此我們可以降維:將i維度隱去,只考慮dp[j]:加j次油後能夠跑的最遠距離。這裡關鍵點是內層迴圈的遍歷順序:逆序遍歷。這是因為dp[i][j]只與dp[i-1][j-1]相關。我們在更新狀態時,需要用到第i-1個狀態時的最遠距離。舉個例子,使用dp[0]和dp[1]來代替dp[1][0]和dp[1][1],現在我們想用新的dp[1]和dp[2]來代替原來的dp[2][1]和dp[2][2]。根據上面的二維的情況,dp[2][2]的更新需要用到的是dp[1][1]的情況,我們如果正序遍歷,那麼dp[1]就會先被更新為[2][1]的狀態,顯然就會發生錯誤,所以我們就需要從後往前進行狀態轉移。

class Solution {
public:
    int minRefuelStops(int target, int startFuel, vector<vector<int>>& stations) {
        int n=stations.size();
        if(n==0){
            if(startFuel>=target) return 0;
            else return -1;
        }
        vector<long long> dp(n+1,0);
        dp[0]=startFuel;
        for(int i=1;i<=n;i++){
            for(int j=i;j>=1;--j){
                if(dp[j-1]>=stations[i-1][0]){
                    dp[j]=max(dp[j],dp[j-1]+stations[i-1][1]);
                }
            }
        }
        for(int j=0;j<=n;j++){
            if(dp[j]>=target){
                return j;
            }
        }
        return -1;
    }
};

最後,我們其實不難看出這題是一道0/1揹包問題(最簡單的揹包問題——0/1揹包問題 - 天涯海角尋天涯 - 部落格園 (cnblogs.com))的變式題,所以對於一些基本的模型問題我們一定要滾瓜爛熟,靈活應用。