LeetCode 接雨水

111 阅读2分钟

题目链接

image.png

仔细读题,按照题目说的,最直接的想法:接到雨水的话应该需要 2 个边界,左边界和右边界,那么用 i 循环左边界,j 找右边界,雨水个数就是左边界和右边界之间的间隔 * min(左边界,右边界) - 左边界到右边界之间的黑色柱子,你这么一听就应该能想到时间复杂度挺高的,但我们本着先用简单正确的思路解决问题,如果提交超时了那么在思考如何优化。所以这里先不计较时间复杂度,那么可以写出如下代码:

function trap(height: number[]): number {
    const len=height.length
    // 计算(i,j)的雨水个数,即矩形面积减去黑色柱子之和
    const getRange=(i,j,base)=>{
        let sum=0
        for(let k=i+1;k<j;k++) sum+=height[k]
        return base*(j-i-1)-sum
    }
    let res=0
    for(let i=0;i<len;i++){
        // 当前第i个柱子是否最大
        let isCurrentMax=true
        let rightMax=0,rightMaxIdx=-1
        for(let j=i+1;j<len;j++){
            // 如果右边有比当前柱子大的,那么矩形的高度是当前柱子的高度,矩形的宽度就是 j-i-1
            if(height[j]>=height[i]){
                res+=getRange(i,j,height[i])
                // 如果找到答案,应该忽略中间,防止重复计算
                i=j-1
                isCurrentMax=false
                // 如果找到了,那么就应该退出循环
                break
            }
            // 维护右边最大值和坐标
            if(rightMax<height[j]){
                rightMax=height[j]
                rightMaxIdx=j
            }
        }
        // 如果右边没有比当前即i柱子大的,那么当前i柱子就是最大的
        // 所以矩形的高应该是右边最大的柱子
        if(isCurrentMax&&rightMaxIdx!==-1){
            res+=getRange(i,rightMaxIdx,height[rightMaxIdx])
            // 如果找到答案,应该忽略中间,防止重复计算
            i=rightMaxIdx-1
        }
    }
    return res
};

这应该是一个比较直接的解法,我想着错误答案应该是不会的,但应该会超时,然而意想不到的是居然通过了 😂

那么我们来看下时间复杂度,最差情况应该是 O(n^3),非常高,按理来说题目数据量1 <= n <= 2 * 10^4,这个数据量 O(n^2) 的算法应该都会超时的,更何况 O(n^3)了,但能到最差情况应该也很少。

当然作为高级的程序员,你应该去继续优化算法。

getRange 可用前缀和优化,降到 O(1)。右边的最值也可预先处理,降到降到 O(1)。但是需要解决个问题,就是找到当前元素间隔最近的比他大的元素的下标,这个我想了半天,只想到了 O(n^2)的解决方案,没有想到更快的了。但是看了题解后,发现这里可以用单调栈来解决,单调栈能在 O(n)复杂度内找到离他最近的比他大的或小的值。

class Stack{
    list:number[][]
    constructor(){
        this.list=[]
    }
    push(val){
        this.list.push(val)
    }
    pop(){
        return this.list.pop()
    }
    top(){
        return this.list[this.list.length-1]
    }
    isEmpty(){
        return !this.list.length
    }
    clear(){
        this.list.length=0
    }
}

function trap(height: number[]): number {
    const len=height.length
    const prefixSum:number[]=[]
    let preSum=0
    height.forEach(i=>{
        preSum+=i
        prefixSum.push(preSum)
    })
    // 计算(i,j)的雨水个数,即矩形面积减去黑色柱子之和
    const getRange=(i:number,j:number,base:number)=>{
        return base*(j-i-1)-(prefixSum[j-1]-prefixSum[i])
    }
    let rNextMax:number[][]=[]
    let sc:Stack=new Stack()
    for(let i=len-1;i>=0;i--){
        while(!sc.isEmpty()&&sc.top()[0]<=height[i]){
            sc.pop()
        }
        if(sc.isEmpty()){
            rNextMax[i]=[-1,-1]
        }else {
            rNextMax[i]=sc.top()
        }
        sc.push([height[i],i])
    }

    let rRangeMax:number[][]=[]
    let t=0,tidx=len-1
    for(let i=len-1;i>=0;i--){
        if(t<height[i]){
            t=height[i]
            tidx=i
        }
        rRangeMax[i]=[t,tidx]
    }

    let res =0
    for(let i=0;i<len;i++){
        if(rNextMax[i][1]>i){
            res+=getRange(i,rNextMax[i][1],height[i])
            i=rNextMax[i][1]-1
        }else {
            if(i+1===len) continue
            res+=getRange(i,rRangeMax[i+1][1],rRangeMax[i+1][0])
            i=rRangeMax[i+1][1]-1
        }
    }
    return res
};

(说实话,这种方法要比后面的方法麻烦的多,但是毕竟是自己想出来的,比较偏爱)

还可以用另一种思路,比较难想,但简单,也不需要使用单调栈数据结构,可以背下来:只看局部,对于第 i 个位置能装多少水?取决于min(左边最大值,右边最大值),最值可以预先处理,所以时间复杂度是 O(n)

function trap(height: number[]): number {
    let n=height.length
    let t=0
    let rMax:number[]=[]
    for(let i=n-1;i>=0;i--){
        if(height[i]>t){
            t=height[i]
        }
        rMax[i]=t
    }
    t=0
    let res=0
    for(let i=0;i<n;i++){
        if(height[i]>t){
            t=height[i]
        }
        res+=Math.min(t,rMax[i])-height[i]
    }
    return res
};