题目:
在一条环路上有 n 个加油站,其中第 i 个加油站有汽油 gas[i] 升。
你有一辆油箱容量无限的的汽车,从第 i 个加油站开往第 i+1 个加油站需要消耗汽油 cost[i] 升。你从其中的一个加油站出发,开始时油箱为空。
给定两个整数数组 gas 和 cost ,如果你可以绕环路行驶一周,则返回出发时加油站的编号,否则返回 -1 。返回所有的可能性。
输入: gas = [1,2,3,4,5], cost = [3,4,5,1,2]
输出: 3
解释:
从 3 号加油站(索引为 3 处)出发,可获得 4 升汽油。此时油箱有 = 0 + 4 = 4 升汽油
开往 4 号加油站,此时油箱有 4 - 1 + 5 = 8 升汽油
开往 0 号加油站,此时油箱有 8 - 2 + 1 = 7 升汽油
开往 1 号加油站,此时油箱有 7 - 3 + 2 = 6 升汽油
开往 2 号加油站,此时油箱有 6 - 4 + 3 = 5 升汽油
开往 3 号加油站,你需要消耗 5 升汽油,正好足够你返回到 3 号加油站。
因此,3 可为起始索引。
思路:
如何从题目中解读出是使用窗口问题进行解决的呢?
思考:如果从任意个点出发,每经过一个加油站剩余的油量,只要存在经过某一个加油站后,剩余的油为负数,那就说明从这个加油站出发走不通,所以只要求出所经过的加油站中最小值,判断正负即可。如何求经过加油站后的最小值呢?窗口双端队列即可解决。
一、求出 i 加油站到 i + 1 加油站的净油量(i加油站油量 - i加油站到i+1加油站耗油量)
二、求出从任一点出发的前缀和数组 i 加油站的前缀和 = (i - 1加油站的前缀和 + i加油站的净油量)
以gas = [1,2,3,4,5], cost = [3,4,5,1,2]为例子
净油量数组 = -2,-2,-2, 3, 3
数组下标 = 0, 1, 2, 3, 4
为了使得循环依次进展下去,我们把数组翻倍:
净油量数组 = -2,-2,-2, 3, 3,-2,-2,-2, 3, 3
数组下标 = 0, 1, 2, 3, 4, 5, 6, 7, 8, 9
计算前缀和arr= -2,-4,-6,-3, 0,-2,-4,-6,-3, 0
arr下标 = 0, 1, 2, 3, 4, 5, 6, 7, 8, 9
分析:0 1 2 4都不是良好的出发点,只有3才是
三、生成长度为N的窗口
Deque<Integer> w = new LinkedList<>();
for (int i = 0; i < N; i++) {
while (!w.isEmpty() && arr[w.peekLast()] >= arr[i]) {
w.pollLast();
}
w.offerLast(i);
}
遍历N(N为给定数组长度即5次遍历)个数遍历窗口过程:
第一步:
arr= [-2,-4,-6,-3, 0,]-2,-4,-6,-3, 0
下标 = 0, 1, 2, 3, 4, 5, 6, 7, 8, 9
遍历结果:
index = 2, 3, 4
value = -6,-3, 0
左边是头,右边是尾,从左到右严格变大
累加和最薄弱的点,就是w中最左信息,即-6这个值,由此看出0位置不是良好出发点
第二步:
arr= -2,[-4,-6,-3, 0,-2,]-4,-6,-3, 0
下标 = 0 , 1, 2, 3, 4, 5, 6, 7, 8, 9
遍历结果:
index = 2, 3, 5
value = -6,-3,-2
左边是头,右边是尾,从左到右严格变大
累加和最薄弱的点,就是w中最左信息,即-6这个值,但是此时遍历的结果是包括1位置的前面的数和的,所以需要把1位置前的前缀和减去,
即-6-(-2) = -4,由此看出1位置不是良好出发点
第三步:
arr= -2,-4,[-6,-3, 0,-2,-4,]-6,-3, 0
下标 = 0, 1, 2, 3, 4, 5, 6, 7, 8, 9
遍历结果:
index = 2, 6
value = -6,-4
左边是头,右边是尾,从左到右严格变大
累加和最薄弱的点,就是w中最左信息,即-6这个值,但是此时遍历的结果是包括2位置的前面的数和的,所以需要把2位置前的前缀和减去,
即-6-(-4) = -2,由此看出2位置不是良好出发点
第四步:
arr= -2,-4,-6,[-3, 0,-2,-4,-6],-3, 0
下标 = 0, 1, 2, 3, 4, 5, 6, 7, 8, 9
遍历结果:
index = 7
value = -6
左边是头,右边是尾,从左到右严格变大
累加和最薄弱的点,就是w中最左信息,即-6这个值,但是此时遍历的结果是包括3位置的前面的数和的,所以需要把3位置前的前缀和减去,
即-6-(-6) = 0,由此看出3位置是良好出发点
第四步:
arr= -2,-4,-6,-3, [0,-2,-4,-6,-3], 0
下标 = 0, 1, 2, 3, 4, 5, 6, 7, 8, 9
遍历结果:
index = 7, 8
value = -6,-3
左边是头,右边是尾,从左到右严格变大
累加和最薄弱的点,就是w中最左信息,即-6这个值,但是此时遍历的结果是包括4位置的前面的数和的,所以需要把4位置前的前缀和减去,
即-6-(-3) = -3,由此看出4位置不是良好出发点
所以满足条件的只有3位置
四、代码实现
public static int[] findGasStation(int[] gas, int[] cost){
//边界判断
if(gas == null || cost == null || gas.length != cost.length){
return null;
}
int N = gas.length; //加油站数量
int M = N << 1; //数组翻倍,方便窗口往后移动
int[] newGas = new int[M]; //新建数组,装净油量(i加油站的含油量 - i+1加油站的耗油量 即 gas[i] - cost[i])
for (int i = 0; i < N; i++) { //存净油量到newGas数组
newGas[i] = gas[i] - cost[i];
newGas[i+N] = gas[i] - cost[i];
}
//计算前缀和,直接用newGas数组
for (int i = 1; i < M; i++) {
newGas[i] = newGas[i] + newGas[i-1];
}
//双端队列
Deque<Integer> dequeMin = new LinkedList<>();
int right = 0; //控制右移
int[] success = new int[N]; //新建数组用来存放可能成哥的加油站
int index = 0; //成功数组下标
//窗口依次右移,计算窗口最小值
for (int i = 0; i < N; i++) {
while(right < N+i){
while(!dequeMin.isEmpty() && newGas[dequeMin.peekLast()] >= newGas[right]){
dequeMin.pollLast();
}
dequeMin.offerLast(right);
right++;
}
if(i > 0){
if(newGas[dequeMin.peekFirst()] - newGas[i-1] >= 0){
success[index++] = i;
}
}else{ //i == 0的情况
if(newGas[dequeMin.peekFirst()] >= 0){
success[index++] = i;
}
}
//L过期问题
if(dequeMin.peekFirst() == i){
dequeMin.pollFirst();
}
}
return success;
}
五、测试
public class GoodGasStation {
public static int[] findGasStationOne(int[] gas, int[] cost){
//边界判断
if(gas == null || cost == null || gas.length != cost.length){
return null;
}
int N = gas.length; //加油站数量
int M = N << 1; //数组翻倍,方便窗口往后移动
int[] newGas = new int[M]; //新建数组,装净油量(i加油站的含油量 - i+1加油站的耗油量 即 gas[i] - cost[i])
for (int i = 0; i < N; i++) { //存净油量到newGas数组
newGas[i] = gas[i] - cost[i];
newGas[i+N] = gas[i] - cost[i];
}
//计算前缀和,直接用newGas数组
for (int i = 1; i < M; i++) {
newGas[i] = newGas[i] + newGas[i-1];
}
//双端队列
Deque<Integer> dequeMin = new LinkedList<>();
int right = 0; //控制右移
int[] success = new int[N]; //新建数组用来存放可能成哥的加油站
int index = 0; //成功数组下标
//窗口依次右移,计算窗口最小值
for (int i = 0; i < N; i++) {
while(right < N+i){
while(!dequeMin.isEmpty() && newGas[dequeMin.peekLast()] >= newGas[right]){
dequeMin.pollLast();
}
dequeMin.offerLast(right);
right++;
}
if(i > 0){
if(newGas[dequeMin.peekFirst()] - newGas[i-1] >= 0){
success[index++] = i;
}
}else{ //i == 0的情况
if(newGas[dequeMin.peekFirst()] >= 0){
success[index++] = i;
}
}
//L过期问题
if(dequeMin.peekFirst() == i){
dequeMin.pollFirst();
}
}
return success;
}
public static int[] findGasStationTwo(int[] gas, int[] cost){
if(gas == null || cost== null || gas.length != cost.length){
return null;
}
int length = gas.length;
int[] newGas = new int[length << 1];
for (int i = 0; i < length; i++) {
newGas[i] = gas[i] - cost[i];
newGas[i+length] = gas[i] - cost[i] ;
}
int[] arr = new int[length]; //用来记录结果
int index = 0; //记录结果数组下标
for (int i = 0; i < length; i++) {
boolean flag = true; //标记加油站是否满足条件
int count = 0; //记录净油量累加值
for (int j = i; j < length+i; j++) {
if (newGas[j] + count >= 0){
count += newGas[j];
}else{
flag = false; //不满足
break;
}
}
if(flag){
arr[index++] = i;
}
}
return arr;
}
public static int[][] generateRandomArray(int maxSize, int maxValue){
int size = (int)((maxSize + 1) * Math.random());
int[] gas = new int[size];
int[] cost = new int[size];
for (int i = 0; i < size; i++) {
gas[i] = (int) ((maxValue + 1) * Math.random());
cost[i] = (int) ((maxValue + 1) * Math.random());
}
int[][] arr = new int[2][size];
arr[0] = gas;
arr[1] = cost;
return arr;
}
public static boolean ifEqual(int[] resultOne, int[] resultTwo){
if((resultOne == null && resultTwo != null) || (resultOne != null && resultTwo == null) || (resultOne.length != resultTwo.length)){
return false;
}
for (int i = 0; i < resultOne.length; i++) {
if(resultOne[i] != resultTwo[i]){
return false;
}
}
return true;
}
public static void main(String[] args) {
int testTimes = 100000;
int maxSize = 100;
int maxValue = 100;
System.out.println("测试开始:");
for (int i = 0; i < testTimes; i++) {
int[][] array = generateRandomArray(maxSize, maxValue);
int[] resultOne = findGasStationOne(array[0], array[1]);
int[] resultTwo = findGasStationTwo(array[0], array[1]);
if(ifEqual(resultOne, resultTwo)){
System.out.println("真不错!!!Perfect");
}else {
System.out.println("不行,再想想!!");
}
}
}
}