0
点赞
收藏
分享

微信扫一扫

窗口问题之加油站良好出发点问题

题目:

在一条环路上有 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("不行,再想想!!");
            }
        }
    }
}

举报

相关推荐

0 条评论