【LeetCode-面試演算法經典-Java實現】【134-Gas Station(加油站問題)】
原題
There are N gas stations along a circular route, where the amount of gas at station i is gas[i].
You have a car with an unlimited gas tank and it costs cost[i] of gas to travel from station i to its next station (i+1). You begin the journey with an empty tank at one of the gas stations.
Return the starting gas station’s index if you can travel around the circuit once, otherwise return -1.
Note:
The solution is guaranteed to be unique.
題目大意
沿環形路線有N個加油站,其中氣體在車站i是量是gas[i]。你有車有無限容量的氣罐,從加油站i到下一個加油站站點i+1,要消耗cost[i]的氣體。你開始旅程時,氣罐是空的。回到起始加油站的指數,選擇一個起點開始旅遊,如果你能在周圍環形旅行一次,就返回開始的加油站索引,否則返回-1。
注意: 答案保證是唯一的。
解題思路
假設從站點 i 出發,到達站點 k 之前,依然能保證油箱裡油沒見底兒,從k 出發後,見底兒了。那麼就說明 diff[i] + diff[i+1] + … + diff[k] < 0,而除掉diff[k]以外,從diff[i]開始的累加都是 >= 0的。也就是說diff[i] 也是 >= 0的,這個時候我們還有必要從站點 i + 1嘗試嗎?仔細一想就知道:車要是從站點 i+1出發,到達站點k後,甚至還沒到站點k,油箱就見底兒了,因為少加了站點 i 的油。。。
因此,當我們發現到達k 站點郵箱見底兒後,i 到 k 這些站點都不用作為出發點來試驗了,肯定不滿足條件,只需要從k+1站點嘗試即可!因此解法時間複雜度從O(n2)降到了 O(2n)。之所以是O(2n),是因為將k+1站作為始發站,車得繞圈開回k,來驗證k+1是否滿足。
等等,真的需要這樣嗎?
我們模擬一下過程:
a. 最開始,站點0是始發站,假設車開出站點p後,油箱空了,假設sum1 = diff[0] +diff[1] + … + diff[p],可知sum1 < 0;
b. 根據上面的論述,我們將p+1作為始發站,開出q站後,油箱又空了, 設sum2 = diff[p+1] +diff[p+2] + … + diff[q],可知sum2 < 0。
c. 將q+1作為始發站,假設一直開到了未迴圈的最末站,油箱沒見底兒,設sum3 = diff[q+1] +diff[q+2] + … + diff[size-1],可知sum3 >= 0。
要想知道車能否開回 q 站,其實就是在sum3 的基礎上,依次加上 diff[0] 到 diff[q],看看sum3在這個過程中是否會小於0。但是我們之前已經知道 diff[0] 到 diff[p-1] 這段路,油箱能一直保持非負,因此我們只要算算sum3 + sum1是否 <0,就知道能不能開到 p+1站了。
如果能從p+1站開出,只要算算sum3 + sum1 + sum2 是否 < 0,就知都能不能開回q站了。
因為 sum1, sum2 都 < 0,因此如果 sum3 + sum1 + sum2 >=0 那麼sum3 + sum1 必然 >= 0,也就是說,只要sum3 + sum1 + sum2 >=0,車必然能開回q站。而sum3 + sum1 + sum2 其實就是 diff陣列的總和 Total,遍歷完所有元素已經算出來了。
因此 Total 能否 >= 0,就是是否存在這樣的站點的 充分必要條件。
這樣時間複雜度進一步從O(2n)降到了 O(n)。
程式碼實現
演算法實現類
public class Solution {
public int canCompleteCircuit(int[] gas, int[] cost) {
// 引數檢驗
if (gas == null || cost == null || gas.length == 0 || gas.length != cost.length) {
return -1;
}
// 記錄訪問的起始點
int start = 0;
// 加的氣和消耗的氣的總差值
int total = 0;
// 從start位置開始,加的氣和消耗的氣的總差值
int sum = 0;
for (int i = 0; i < gas.length; i++) {
total += (gas[i] - cost[i]);
// 如是油箱沒有油了
if (sum < 0) {
// 重新設定油箱中的油
sum = gas[i] - cost[i];
// 記錄新的起點位置
start = i;
} else {
// 油箱中還有油,更新油箱中的油數
sum += (gas[i] - cost[i]);
}
}
return total >= 0 ? start : -1;
}
// 下面的方法會超時O(N^2)時間複雜度
public int canCompleteCircuit2(int[] gas, int[] cost) {
// 引數檢驗
if (gas == null || cost == null || gas.length == 0 || gas.length != cost.length) {
return -1;
}
// 剩下的氣體,開始時為0
int leftGas = 0;
// 開始出發的站點
int start = 0;
// 結束的站點
int end = 1;
// 未走一週
while (start < gas.length) {
// 到達下一個站後的氣體簡便量
leftGas = gas[start] - cost[start];
// 可以走到下一個站
if (leftGas > 0) {
// 記錄下一個站
end = (start + 1) % gas.length;
// 如果一直可以到下一個站就持續進行操作
while (start != end && (leftGas += (gas[end] - cost[end])) >= 0) {
end = (end + 1) % gas.length;
}
// 說明已經遍歷了一週
if (start == end) {
return start;
}
}
start++;
}
return -1;
}
}
評測結果
點選圖片,滑鼠不釋放,拖動一段位置,釋放後在新的視窗中檢視完整圖片。