【动态规划】——最低加油次数

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))的变式题,所以对于一些基本的模型问题我们一定要滚瓜烂熟,灵活应用。

posted @ 2022-03-23 19:11  天涯海角寻天涯  阅读(772)  评论(0编辑  收藏  举报