leetcode - 17 最低加油次数

  1. 最低加油次数

汽车从起点出发驶向目的地,该目的地位于出发位置东面 target 英里处。

沿途有加油站,每个 station[i] 代表一个加油站,它位于出发位置东面 station[i][0] 英里处,并且有
station[i][1] 升汽油。

假设汽车油箱的容量是无限的,其中最初有 startFuel 升燃料。它每行驶 1 英里就会用掉 1 升汽油。

当汽车到达加油站时,它可能停下来加油,将所有汽油从加油站转移到汽车中。

为了到达目的地,汽车所必要的最低加油次数是多少?如果无法到达目的地,则返回 -1 。

注意:如果汽车到达加油站时剩余燃料为 0,它仍然可以在那里加油。如果汽车到达目的地时剩余燃料为 0,仍然认为它已经到达目的地。

示例 1:

输入:target = 1, startFuel = 1, stations = [] 输出:0 解释:我们可以在不加油的情况下到达目的地。

示例 2:

输入:target = 100, startFuel = 1, stations = [[10,100]] 输出:-1
解释:我们无法抵达目的地,甚至无法到达第一个加油站。

示例 3:

输入:target = 100, startFuel = 10, stations =
[[10,60],[20,30],[30,30],[60,40]] 输出:2 解释: 我们出发时有 10 升燃料。 我们开车来到距起点 10
英里处的加油站,消耗 10 升燃料。将汽油从 0 升加到 60 升。 然后,我们从 10 英里处的加油站开到 60 英里处的加油站(消耗
50 升燃料), 并将汽油从 10 升加到 50 升。然后我们开车抵达目的地。 我们沿途在1两个加油站停靠,所以返回 2 。

提示:

1 <= target, startFuel, stations[i][1] <= 10^9
0 <= stations.length <= 500
0 < stations[0][0] < stations[1][0] < ... < stations[stations.length-1][0] < target

这个题比较难了,是 hard 的题目,先从最直观想到的动态规划入手讲讲吧。
这个题我最开始想的是一维动态规划,直接考虑次数问题,但很快我就发现,本题不同于平常的动态规划问题的极值问题,本题是条件极值问题,因此直接最小化每个加油站的次数会遇到的问题是:尽管在第i个加油站最小化了加油次数,但从第i个加油站未必能有足够的油到最后的目标点,因此之前加油站的信息不能舍弃,必须保留。
那么很自然的我们就想到了二维dp,通过之前加油站的信息来判断,因此我们最小化在第i个加油站时加了j次油能达到的最远距离。

1. 二维dp

class Solution:
    def minRefuelStops(self, target: int, startFuel: int, stations: List[List[int]]) -> int:
        n = len(stations)
        dp = [[0 for _ in range(n + 1)] for _ in range(n + 1)]
        if startFuel >= target:
            return 0
        for i in range(n + 1):
            dp[i][0] = startFuel
        for i in range(1, n + 1):
            for j in range(1, i + 1):
                if dp[i - 1][j] >= stations[i - 1][0]:
                    dp[i][j] = dp[i - 1][j]
                if dp[i - 1][j - 1] >= stations[i - 1][0]:
                    dp[i][j] = max(dp[i][j], dp[i - 1][j - 1] + stations[i - 1][1])
        for i in range(n + 1):
            if dp[n][i] >= target:
                return i
        return -1

这里的时间复杂度是 O ( N 2 ) O(N^2) O(N2),空间复杂度是 O ( N 2 ) O(N^2) O(N2),那么我们进一步压缩空间复杂度可以压缩到 O ( N ) O(N) O(N) ,方法是直接考虑加 i 次油能到达的最远距离,但这样损失了加油站的信息,因此如果不希望加油站的信息损失,需要我们观察发现,加 i 次油到达的最远距离有后效性无前效性,即在加 i 次油之后的距离与加 i-1 次有关而与 i+1 次无关,因此我们只需要倒着遍历即可避免信息损失,这也是 dp 空间压缩滚动数组时最常见的手段。

2.一维dp

class Solution:
    def minRefuelStops(self, target: int, startFuel: int, stations: List[List[int]]) -> int:
        dp = [startFuel] + [0] * len(stations)
        for i, (j, k) in enumerate(stations):
            for t in range(i, -1, -1):
                if dp[t] >= j:
                    dp[t+1] = max(dp[t+1], dp[t] + k)

        for i, d in enumerate(dp):
            if d >= target: return i
        return -1

这里的时间复杂度是 O ( N 2 ) O(N^2) O(N2),空间复杂度是 O ( N ) O(N) O(N)
通过不断压缩我们发现,我们在dp计算中总是在最大化 ( d p [ i ] [ j ] , d p [ i − 1 ] [ j − 1 ] + s t a t i o n s [ i − 1 ] [ 1 ] ) (dp[i][j], dp[i - 1][j - 1] + stations[i - 1][1]) (dp[i][j],dp[i1][j1]+stations[i1][1]) ( d p [ t + 1 ] , d p [ t ] + k ) (dp[t+1], dp[t] + k) (dp[t+1],dp[t]+k) 因此,我们只需要最大化每一次加油的量即可,通过第二个方法的提醒,我们只需要保存之前能走到最大距离以内的油量即可,当油不够时通过找最大的油量添加即可。因此我们只需要维护加 i 次油到达距离之内的油量序列即可,因为要频繁的插入与删除,因此我们可以选择堆的方法维护。

3. 堆

class Solution:
    def minRefuelStops(self, target: int, startFuel: int, stations: List[List[int]]) -> int:
        oilseries = []  
        stations.append([target, 0])
        cur = startFuel
        ans = 0
        for s, o in stations:
            while oilseries and cur < s:  # must refuel in past
                cur += -heapq.heappop(oilseries)
                ans += 1
            if cur < s: return -1
            heapq.heappush(oilseries, -o)

        return ans

这里的时间复杂度是 O ( N l o g N ) O(NlogN) O(NlogN),空间复杂度是 O ( N ) O(N) O(N)
综上正常方法的二维dp完全可以做,最关键的是要考虑到仅仅最小化次数有可能导致无法保证油量足够到达终点。如果想优化空间,就需要考虑到油量的关系,从后向前遍历更新滚动数组。
如果还想优化时间需要考虑到无论在哪里加油,只有在能达到的最大的加油站加油才是最节省次数的,因此需要贪心。这里属于比较巧的思维环节,可能不容易构思到,即使想到了也不容易想到堆的方法(,因此我还是要多熟悉这些数据结构啊)

你可能感兴趣的:(leetcode)