twoPointers-左右扫TrapWater系列

TrapWater系列的题,思路是对每个given位置,要求从左右扫到这个位置的“最大/小值”,或者“累积计算结果”,等等,总之是一定要从边上撸过来……于是把类似思路的题总结了一下。

题目简介
11. Container With Most Water42的铺垫
42. Trapping Rain Water求左/右到当前点的最高柱子
238. Product of Array Except Self求左/右到当前点的累积乘积
926. Flip String to Monotone Increasing求左/右到当前点所需的flip数
84. Largest Rectangle in Histogram求histogram左/右比当前点矮的第一个
85. Maximal Rectangle求每一层的histogram的左右边界

11. Container With Most Water

在这里插入图片描述

Input: [1,8,6,2,5,4,8,3,7]
Output: 49 求最多能盛水的体积

这个题并不是左右扫两遍的,放在这里是为了42做铺垫。

11核心的思想就是:从两边往中间挪板,宽度已经减小了,就寄希望于水面高度可以增加了。原来的水面高度是由短的那个决定的,挪高的那个板,比现在水面高则没啥用,比现在水面还低那更没用了呵呵。所以,只有挪短的那个板,才有机会水体积变大。

class Solution {
    public int maxArea(int[] height) {
        int maxArea = Integer.MIN_VALUE;
        int left = 0, right = height.length-1;
        while (left < right) {
            int area = (right - left) * Math.min(height[left], height[right]);
            maxArea = Math.max(area, maxArea);
            if (height[left] < height[right]) {//挪短的那个板,才有机会水体积变大
                left++;
            } else {
                right--;
            }
        }
        return maxArea;
    }
}

42. Trapping Rain Water

在这里插入图片描述

Input: [0,1,0,2,1,0,1,3,2,1,2,1]
Output: 6

这个题比11难在哪里呢?

  • 11没有“池底”的概念,于是只需要确定左右挡板的高度,就可以唯一确定水的容积,整个水的形状是长方形。
  • 42有“池底”的概念,每个位置“池底”高度不一样,就要每个位置单独计算水柱的高度。整个水的形状是不规则图形。

左右各扫一遍的方法,就是针对“每个位置单独计算”的情况设计的:

  1. go right: 维护一个从边到当前位置的最大值
  2. go left: 维护一个从边到当前位置的最大值

然后取两者较小值,然后减去当前池底的高度,就是当前位置的水柱体积。

class Solution {
    public int trap(int[] height) {
        int water = 0;
        int len = height.length;
        int lIdx = 0, rIdx = len -1;
        int lMax = Integer.MIN_VALUE, rMax = Integer.MIN_VALUE;
        int[] lMaxArr = new int[len];
        int[] rMaxArr = new int[len];
        //go right
        for (int i = 0; i < len; i++) {
            lMax = Math.max(lMax, height[i]);
            lMaxArr[i] = lMax;
        }
        //go left
        for (int i = len-1; i >= 0; i--) {
            rMax = Math.max(rMax, height[i]);
            rMaxArr[i] = rMax;
        }
        //find min on each position
        for (int i = 0; i < height.length; i++) {
            water += (Math.min(lMaxArr[i], rMaxArr[i]) - height[i]);
        }
        return water;
    }
}

有个trick可以改进:不需要memory,且只撸一遍。
和11. Container With Most Water 思路类似,木桶理论,挪短的那一边,才有可能得到更大的水体积。

想想原来我们为什么需要左右各扫一遍,用memory来记录“从开始到此位置”的最大值呢?因为对于a given position,我们需要知道从两边分别到此处的最大值,然而通常如果只扫一遍的话,只能知道从某一边到此处的最大值。

那为什么此处我们可以免掉memory呢?是因为这个题设定的特殊点:就是左右挡板从两端往中间挪,在挪到某个位置的时候(不管是从左边挪过来的还是右边挪过来的),就可以确定最终的“短板”了,因为至少”从左边扫过来的最大值“和”从右边扫过来的最大值“两者至少已经求出一个了,而最终的水柱高度是由两者中的短的决定,所以当下这个值就可以算作为当前位置最终的水柱高度了。

class Solution {
    public int trap(int[] height) {
        int left = 0, right = height.length-1;
        int leftMax = 0, rightMax = 0;
        int water = 0;
        while (left <= right) {
            leftMax = Math.max(leftMax, height[left]);
            rightMax = Math.max(rightMax, height[right]);
            if (leftMax < rightMax) {
                water += leftMax - height[left];
                left++;
            } else {
                water += rightMax - height[right];
                right--;
            }
        }
        return water;
    }
}

238. Product of Array Except Self

Input: [1,2,3,4] 求除了它本身以外的所有数乘积。
Output: [24,12,8,6] ,O(n)时间且不能用除法。

这个题并不是盛水的问题,写在这里是因为238也是“左右各扫描一遍”来解决的问题。

这个题直觉想:对于a given number,就把除了它之外的数全乘起来就好,但这种方法时间O(n^2),太复杂且有太多重复计算。题目要求只能用O(n)时间。

怎么减少重复计算呢?一个straight-forward的改进是:用累积乘积。然而本题不能用除法,如果只撸一遍就不能避免用除法,于是,嗯,可以左右各来一遍。这里就发现了这个238和前面11,42盛水问题的联系了:
“对于a given position,我们需要知道从两边分别到此处的累积结果,然而通常如果只扫一遍的话,只能知道从某一边到此处的累积结果,于是需要左右各一遍。”

另外,凡是对时间要求苛刻的问题,都要想1)空间换时间;2)多撸几遍。这种数组上操作,且要求O(n)时间的,就可以考虑“多撸几遍”。

左右各扫一遍:

  1. go right: 求从左开始的累积“乘积”
  2. go left: 求从右开始的累积“乘积”
    注意在go left的时候,每个位置上的从左开始的累积“乘积”已经算好了,于是不需要再开一个数组存从右开始的累积“乘积”,直接两者乘好,存入结果数组。
class Solution {
    public int[] productExceptSelf(int[] nums) {
        int len = nums.length;
        int[] ret = new int[len];
        ret[0] = 1;
        // go right
        for (int i = 1; i < len; i++) {
            ret[i] = ret[i-1] * nums[i-1];
        }
        // go left
        int rightAccum = 1;
        for (int i = len-1; i >= 0; i--) {
            ret[i] *= rightAccum;
            rightAccum *= nums[i];
        }
        return ret;
    }
}

926. Flip String to Monotone Increasing

Input: “00110”,Output: 1 求最少的flip使得整个数列单调增(非减)
Explanation: We flip the last digit to get 00111.

(这个题和238类似,也是需要累积结果。)
直觉:要求一个切分点,左边全(打算)改成0,右边全(打算)改成1。从左往右扫,对a given点,要求:1)它左边有几个1需要改成0,这个可以通过从左撸过来的时候顺便算(累积结果);2)它右边有几个0需要改成1,这个从左边撸过来就没法得知了。于是我们想到左右各扫一遍。

  • f0[i]定义为:如果从左到第i位置全是0,需要几个flip。(go right)
  • f1[i]定义为:如果从右到第i位置全是1,需要几个flip。(go left)

f0[i]和f1[i]是“加法原理”的关系(算这件事需要两步,1.算从左边到i的flip数,2.算从右边到i的flip数,两者是要加起来的),f0[i]+f1[i]作为“如果当前位置作为切分点”的flip数,对所有切分点的flip数们,维护一个最小值,就是最终结果啦。

这种“需要扫两遍”的,都没法合并成一遍(要不然为啥专门扫两遍)。这个题的代码可以把左右的两遍合并在一起做,其实只是表面合并,f0和f1两个DP数组用不同的下标i = 1, j = S.length() - 1来记录。

class Solution {
    public int minFlipsMonoIncr(String S) {
        int[] f0 = new int[S.length() + 1];
        int[] f1 = new int[S.length() + 1];
        for (int i = 1, j = S.length() - 1; j >= 0; i++, j--) {
            f0[i] += f0[i - 1] + (S.charAt(i - 1) == '0' ? 0 : 1);//go right
            f1[j] += f1[j + 1] + (S.charAt(j) == '1' ? 0 : 1);//go left
        }
        int res = Integer.MAX_VALUE;
        for (int i = 0; i <= S.length(); ++i) {
            res = Math.min(res, f0[i] + f1[i]);
        }
        return res;
    }
}

84. Largest Rectangle in Histogram

在这里插入图片描述

Input: [2,1,5,6,2,3] 求其中长方形的最大面积
Output: 10

求最大面积,肯定是维护一个全局的maxArea,我们的任务就是尝试各种长宽搭配,使得不要错过那种最大的搭配。

  • 前面盛水那两个11,42,就是不断挪左右挡板,每对确定的左右挡板位置,求这个搭配的盛水量。
  • 这个题84,可以在每个柱子处把长方形高度利用到最高,然后宽度尽量往两边拓展,看这种情况下能得到多大的长方形。

于是捏,对于a given position,它不关心左右比它高的柱子,因为不是瓶颈。它最关心的是左右第一个比它矮的柱子firstLowerLeft/firstLowerRight,就是当前情况能拓展的边界了。

而如果在每个位置都向两边拓展,则时间O(n^2),而且有很多重复计算。用memory记录已经算出来的结果firstLowerLeft[p],可以减少重复计算。

比如第五个柱子高2,往左拓展的时候,到6的时候,firstLowerLeft[3]告诉你,比我低的下标是2(高度5),不管这个5是不是比原来那个高2的柱子高,都是高2的柱子值得考虑的可能比它矮的柱子candidate,不会错过“第一个比它矮的”。而且若高2的和高1的这俩柱子之间有很多很高的柱子,这样就可以跳过很多queries。

一个小细节,探索边界的时候,跳出while-loop之后,会停留在“比当前柱子矮”的那个下标上,而求长方形面积的时候,是要往中间收一个的。

class Solution {
    public int largestRectangleArea(int[] heights) {
        if (heights == null || heights.length == 0) {return 0;}
        int[] firstLowerLeft = new int[heights.length]; //index
        int[] firstLowerRight = new int[heights.length];
        //scan from left to right
        firstLowerLeft[0] = -1; //init
        for (int i = 1; i < heights.length; i++) {
            int p = i - 1;
            while (p >= 0 && heights[p] >= heights[i]) {
                p = firstLowerLeft[p];
            }
            firstLowerLeft[i] = p;
        }
        //scan from right to left
        firstLowerRight[heights.length - 1] = heights.length; //init
        for (int i = heights.length - 2; i >= 0; i--) {
            int p = i + 1;
            while (p < heights.length && heights[p] >= heights[i]) {
                p = firstLowerRight[p];
            }
            firstLowerRight[i] = p;
        }
        //calculate max area
        int maxArea = 0;
        for (int i = 0; i < heights.length; i++) {
            maxArea = Math.max(maxArea, heights[i] * ((firstLowerRight[i]-1) - (firstLowerLeft[i]+1) + 1));
        }
        return maxArea;
    }
}

85. Maximal Rectangle

Input:
[
[“1”,“0”,“1”,“0”,“0”],
[“1”,“0”,“1”,“1”,“1”],
[“1”,“1”,“1”,“1”,“1”],
[“1”,“0”,“0”,“1”,“0”]
]
Output: 6 求最大长方形面积(由1表示)

这个和84.Largest Rectangle in Histogram很像,只不过那个84是有一个整齐的底边,这个是底端不对齐的。怎么办呢?一层一层搞。这样每一层都当作Histogram的底层,并向上构造当前行的Histogram。

构造好当前行的Histogram之后,和前面几个题思路类似,也需要求左右边界的过程,但不太一样:

  • 84.histogram: 类似于DP的思路,从i-1开始查询下标,直到遇到比i矮的。
  • 85.matrix:
    curLeft定义为:只考虑当前行,的左边界下标。
    leftBoundary定义为:考虑到当前行为止形成的histogram,当前列能向左拓展的左边界下标(受上一层制约,当然也受下一侧制约只不过还没生成)。
  1. 如果当前值是1,则继承上一层的leftBoundary,并和当前层curLeft求更靠右(求max)的leftBoundary值。
  2. 如果当前值是0,则恢复默认值leftBoundary=0,更新当前行的左边界curLeft=j+1(说明j往左都不用考虑了)

举个🌰:(来自morrischen2008)
0 0 0 1 0 0 0
0 0 1 1 1 0 0
0 1 1 1 1 1 0

第几层Histogram左右边界
00 0 0 1 0 0 0l: 0 0 0 3 0 0 0
r: 7 7 7 4 7 7 7
10 0 0 1 0 0 0
0 0 1 1 1 0 0
l: 0 0 2 3 2 0 0
r: 7 7 5 4 5 7 7
20 0 0 1 0 0 0
0 0 1 1 1 0 0
0 1 1 1 1 1 0
l: 0 1 2 3 2 1 0
r: 7 6 5 4 5 6 7
class Solution {
    public int maximalRectangle(char[][] matrix) {
        if (matrix.length == 0) {return 0;}
        int m = matrix.length;
        int n = matrix[0].length;
        int[] heights = new int[n];
        int[] leftBoundary = new int[n];
        int[] rightBoundary = new int[n];
        Arrays.fill(leftBoundary, 0);
        Arrays.fill(rightBoundary, n);
        int maxArea = 0;
        for (int i = 0; i < m; i++) {//处理第i层(累积前i层)
            int curLeft = 0;
            int curRight = n;
            //计算当前层的Histogram高度(from either side)
            for (int j = 0; j < n; j++) {
                if (matrix[i][j] == '1') {
                    heights[j]++;
                } else {
                    heights[j] = 0;
                }
            }
            //go right:更新左边界
            for (int j = 0; j < n; j++) {
                if (matrix[i][j] == '1') {
                    leftBoundary[j] = Math.max(leftBoundary[j], curLeft);
                } else {
                    leftBoundary[j] = 0;
                    curLeft = j + 1;
                }
            }
            //go left:更新右边界
            for (int j = n-1; j >= 0; j--) {
                if (matrix[i][j] == '1') {
                    rightBoundary[j] = Math.min(rightBoundary[j], curRight);
                } else {
                    rightBoundary[j] = n;
                    curRight = j;
                }
            }
            //计算面积(from either size)
            for (int j = 0; j < n; j++) {
                maxArea = Math.max(maxArea, ((rightBoundary[j]-leftBoundary[j]) * heights[j]));
            }
        }
        return maxArea;
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值