字节人都会的 3D接雨水 😂

6,687 阅读5分钟

最近逛脉脉,刷到了一个热题,3D 接雨水,正好咱们之前看过 2D 的,这次就来看看这个 3D 的。

image.png

407. 接雨水 II

image.png

因为做过 2D 的,所以我顺着之前的思路有点想法:

  1. 想法一: 2D 时,可以聚焦于先算出当前这个柱子能装多少水,等于左边最高柱子和右边最高柱子中较低的那一个,那顺着这个思路,我想到了 3D 就是左边最高柱子和右边最高柱子、上边最高柱子和下边最高柱子这 4 个中较低的那一个。然而我写出思路提交时,连样例都没有通过,后面思考发现这种想法不对,因为 3D 时想要接到雨水需要周围的柱子都围住了才行,只是这四个是不够的。
  2. 想法二:放弃想法一之后,我又想到了 dfs 染色。对于 3D 的柱子,可以通过 Z 轴拆分维度,比如当 Z 等于 1 时,计算能接到的雨水,比如当 Z 等于 2 时,计算能接到的雨水,后面以此类推就能算出全部的雨水了,那 Z 等于 1 时怎么计算有多少雨水呢?可以用 dfs 染色,当有雨水时,这些雨水一定是不和外界联通的,即是被一圈墙围起来的,所以 dfs 跑一遍就能算出来了,但是这个方法超时了,因为时间复杂度是 2*10^4*200*2008*10^8

最终实在没想法了,就看了题解。思路是:把这个看成木桶接水,就像是短板理论:

image.png

最外层一圈一定接不到水,也就是可以看成木桶的边缘,先把木桶的边缘都放进优先队列中,这时取出最小的柱子,向这个柱子的 4 个方向扩散,扩散出的柱子有两种情况,如果比当前柱子大,那么不能接水,如果比当前柱子小,那么接到的水就是当前柱子高度减去扩散柱子的高度(为什么?因为当前柱子一定是边缘最小的了,所以它一定能接到水),后面在把扩散的柱子加入到优先队列中,已经比较完的柱子就移出队列,这样就又形成了新的桶的边缘,并且水桶面积也在不断缩小(当然要记录走过的柱子,防止重复走),最终就会计算完成。

说实话,这种想法很难想出来,所以这题就是多复习,多背诵吧 😂

最终代码如下:(我是用 JavaScript 写的)

function compare(a, b) {
    return a[2] - b[2]
}

function trapRainWater(heightMap) {
    let queue = new MyPriorityQueue(compare)

    let m = heightMap.length
    let n = heightMap[0].length

    let vis = []
    let res = 0
    for (let i = 0; i < m; i++) {
        vis[i] = []
        for (let j = 0; j < n; j++) {
            if (i === 0 || i === m - 1 || j === 0 || j === n - 1) {
                queue.enqueue([i, j, heightMap[i][j]])
                vis[i][j] = 1
            }
        }
    }

    while (!queue.isEmpty()) {
        let d = [-1, 0, 1, 0, -1]
        let ele = queue.dequeue()
        for (let j = 0; j < 4; j++) {
            let tx = ele[0] + d[j]
            let ty = ele[1] + d[j + 1]
            if (0 <= tx && tx < m && 0 <= ty && ty < n && vis[tx][ty] !== 1) {
                if (heightMap[tx][ty] < ele[2]) {
                    res += ele[2] - heightMap[tx][ty]
                }
                vis[tx][ty] = 1
                queue.enqueue([tx, ty, Math.max(heightMap[tx][ty], ele[2])])
            }
        }
    }
    return res
};

LeetCode 的 JS/TS 语言支持很拉胯,虽然说了能用三方库实现优先队列,但由于三方库没有更新导致很难用,会报错,所以我就自己去复制了三方库中最新的代码,用于实现优先队列,大家也直接复制我的工具代码即可:(通过这个工具代码发现了 datastructures-js 仓库,这里实现了 JavaScript 版的类似于 Java 的数据结构,刷题时很有用)

class Heap {
    /**
     * @param {function} compare
     * @param {array} [_values]
     * @param {number|string|object} [_leaf]
     */
    constructor(compare, _values, _leaf) {
        if (typeof compare !== 'function') {
            throw new Error('Heap constructor expects a compare function');
        }
        this._compare = compare;
        this._nodes = Array.isArray(_values) ? _values : [];
        this._leaf = _leaf || null;
    }

    /**
     * Converts the heap to a cloned array without sorting.
     * @public
     * @returns {Array}
     */
    toArray() {
        return Array.from(this._nodes);
    }

    /**
     * Checks if a parent has a left child
     * @private
     */
    _hasLeftChild(parentIndex) {
        const leftChildIndex = (parentIndex * 2) + 1;
        return leftChildIndex < this.size();
    }

    /**
     * Checks if a parent has a right child
     * @private
     */
    _hasRightChild(parentIndex) {
        const rightChildIndex = (parentIndex * 2) + 2;
        return rightChildIndex < this.size();
    }

    /**
     * Compares two nodes
     * @private
     */
    _compareAt(i, j) {
        return this._compare(this._nodes[i], this._nodes[j]);
    }

    /**
     * Swaps two nodes in the heap
     * @private
     */
    _swap(i, j) {
        const temp = this._nodes[i];
        this._nodes[i] = this._nodes[j];
        this._nodes[j] = temp;
    }

    /**
     * Checks if parent and child should be swapped
     * @private
     */
    _shouldSwap(parentIndex, childIndex) {
        if (parentIndex < 0 || parentIndex >= this.size()) {
            return false;
        }

        if (childIndex < 0 || childIndex >= this.size()) {
            return false;
        }

        return this._compareAt(parentIndex, childIndex) > 0;
    }

    /**
     * Compares children of a parent
     * @private
     */
    _compareChildrenOf(parentIndex) {
        if (!this._hasLeftChild(parentIndex) && !this._hasRightChild(parentIndex)) {
            return -1;
        }

        const leftChildIndex = (parentIndex * 2) + 1;
        const rightChildIndex = (parentIndex * 2) + 2;

        if (!this._hasLeftChild(parentIndex)) {
            return rightChildIndex;
        }

        if (!this._hasRightChild(parentIndex)) {
            return leftChildIndex;
        }

        const compare = this._compareAt(leftChildIndex, rightChildIndex);
        return compare > 0 ? rightChildIndex : leftChildIndex;
    }

    /**
     * Compares two children before a position
     * @private
     */
    _compareChildrenBefore(index, leftChildIndex, rightChildIndex) {
        const compare = this._compareAt(rightChildIndex, leftChildIndex);

        if (compare <= 0 && rightChildIndex < index) {
            return rightChildIndex;
        }

        return leftChildIndex;
    }

    /**
     * Recursively bubbles up a node if it's in a wrong position
     * @private
     */
    _heapifyUp(startIndex) {
        let childIndex = startIndex;
        let parentIndex = Math.floor((childIndex - 1) / 2);

        while (this._shouldSwap(parentIndex, childIndex)) {
            this._swap(parentIndex, childIndex);
            childIndex = parentIndex;
            parentIndex = Math.floor((childIndex - 1) / 2);
        }
    }

    /**
     * Recursively bubbles down a node if it's in a wrong position
     * @private
     */
    _heapifyDown(startIndex) {
        let parentIndex = startIndex;
        let childIndex = this._compareChildrenOf(parentIndex);

        while (this._shouldSwap(parentIndex, childIndex)) {
            this._swap(parentIndex, childIndex);
            parentIndex = childIndex;
            childIndex = this._compareChildrenOf(parentIndex);
        }
    }

    /**
     * Recursively bubbles down a node before a given index
     * @private
     */
    _heapifyDownUntil(index) {
        let parentIndex = 0;
        let leftChildIndex = 1;
        let rightChildIndex = 2;
        let childIndex;

        while (leftChildIndex < index) {
            childIndex = this._compareChildrenBefore(
                index,
                leftChildIndex,
                rightChildIndex
            );

            if (this._shouldSwap(parentIndex, childIndex)) {
                this._swap(parentIndex, childIndex);
            }

            parentIndex = childIndex;
            leftChildIndex = (parentIndex * 2) + 1;
            rightChildIndex = (parentIndex * 2) + 2;
        }
    }

    /**
     * Inserts a new value into the heap
     * @public
     * @param {number|string|object} value
     * @returns {Heap}
     */
    insert(value) {
        this._nodes.push(value);
        this._heapifyUp(this.size() - 1);
        if (this._leaf === null || this._compare(value, this._leaf) > 0) {
            this._leaf = value;
        }
        return this;
    }

    /**
     * Inserts a new value into the heap
     * @public
     * @param {number|string|object} value
     * @returns {Heap}
     */
    push(value) {
        return this.insert(value);
    }

    /**
     * Removes and returns the root node in the heap
     * @public
     * @returns {number|string|object}
     */
    extractRoot() {
        if (this.isEmpty()) {
            return null;
        }

        const root = this.root();
        this._nodes[0] = this._nodes[this.size() - 1];
        this._nodes.pop();
        this._heapifyDown(0);

        if (root === this._leaf) {
            this._leaf = this.root();
        }

        return root;
    }

    /**
     * Removes and returns the root node in the heap
     * @public
     * @returns {number|string|object}
     */
    pop() {
        return this.extractRoot();
    }

    /**
     * Applies heap sort and return the values sorted by priority
     * @public
     * @returns {array}
     */
    sort() {
        for (let i = this.size() - 1; i > 0; i -= 1) {
            this._swap(0, i);
            this._heapifyDownUntil(i);
        }
        return this._nodes;
    }

    /**
     * Fixes node positions in the heap
     * @public
     * @returns {Heap}
     */
    fix() {
        // fix node positions
        for (let i = Math.floor(this.size() / 2) - 1; i >= 0; i -= 1) {
            this._heapifyDown(i);
        }

        // fix leaf value
        for (let i = Math.floor(this.size() / 2); i < this.size(); i += 1) {
            const value = this._nodes[i];
            if (this._leaf === null || this._compare(value, this._leaf) > 0) {
                this._leaf = value;
            }
        }

        return this;
    }

    /**
     * Verifies that all heap nodes are in the right position
     * @public
     * @returns {boolean}
     */
    isValid() {
        const isValidRecursive = (parentIndex) => {
            let isValidLeft = true;
            let isValidRight = true;

            if (this._hasLeftChild(parentIndex)) {
                const leftChildIndex = (parentIndex * 2) + 1;
                if (this._compareAt(parentIndex, leftChildIndex) > 0) {
                    return false;
                }
                isValidLeft = isValidRecursive(leftChildIndex);
            }

            if (this._hasRightChild(parentIndex)) {
                const rightChildIndex = (parentIndex * 2) + 2;
                if (this._compareAt(parentIndex, rightChildIndex) > 0) {
                    return false;
                }
                isValidRight = isValidRecursive(rightChildIndex);
            }

            return isValidLeft && isValidRight;
        };

        return isValidRecursive(0);
    }

    /**
     * Returns a shallow copy of the heap
     * @public
     * @returns {Heap}
     */
    clone() {
        return new Heap(this._compare, this._nodes.slice(), this._leaf);
    }

    /**
     * Returns the root node in the heap
     * @public
     * @returns {number|string|object}
     */
    root() {
        if (this.isEmpty()) {
            return null;
        }

        return this._nodes[0];
    }

    /**
     * Returns the root node in the heap
     * @public
     * @returns {number|string|object}
     */
    top() {
        return this.root();
    }

    /**
     * Returns a leaf node in the heap
     * @public
     * @returns {number|string|object}
     */
    leaf() {
        return this._leaf;
    }

    /**
     * Returns the number of nodes in the heap
     * @public
     * @returns {number}
     */
    size() {
        return this._nodes.length;
    }

    /**
     * Checks if the heap is empty
     * @public
     * @returns {boolean}
     */
    isEmpty() {
        return this.size() === 0;
    }

    /**
     * Clears the heap
     * @public
     */
    clear() {
        this._nodes = [];
        this._leaf = null;
    }

    /**
     * Implements an iterable on the heap
     * @public
     */
    [Symbol.iterator]( "Symbol.iterator") {
        let size = this.size();
        return {
            next: () => {
                size -= 1;
                return {
                    value: this.pop(),
                    done: size === -1
                };
            }
        };
    }

    /**
     * Builds a heap from a array of values
     * @public
     * @static
     * @param {array} values
     * @param {function} compare
     * @returns {Heap}
     */
    static heapify(values, compare) {
        if (!Array.isArray(values)) {
            throw new Error('Heap.heapify expects an array of values');
        }

        if (typeof compare !== 'function') {
            throw new Error('Heap.heapify expects a compare function');
        }

        return new Heap(compare, values).fix();
    }

    /**
     * Checks if a list of values is a valid heap
     * @public
     * @static
     * @param {array} values
     * @param {function} compare
     * @returns {boolean}
     */
    static isHeapified(values, compare) {
        return new Heap(compare, values).isValid();
    }
}

class MyPriorityQueue {
    /**
     * Creates a priority queue
     * @params {function} compare
     */
    constructor(compare, _values) {
        if (typeof compare !== 'function') {
            throw new Error('MyPriorityQueue constructor expects a compare function');
        }
        this._heap = new Heap(compare, _values);
        if (_values) {
            this._heap.fix();
        }
    }

    /**
     * Returns an element with highest priority in the queue
     * @public
     * @returns {number|string|object}
     */
    front() {
        return this._heap.root();
    }

    /**
     * Returns an element with lowest priority in the queue
     * @public
     * @returns {number|string|object}
     */
    back() {
        return this._heap.leaf();
    }

    /**
     * Adds a value to the queue
     * @public
     * @param {number|string|object} value
     * @returns {MyPriorityQueue}
     */
    enqueue(value) {
        return this._heap.insert(value);
    }

    /**
     * Adds a value to the queue
     * @public
     * @param {number|string|object} value
     * @returns {MyPriorityQueue}
     */
    push(value) {
        return this.enqueue(value);
    }

    /**
     * Removes and returns an element with highest priority in the queue
     * @public
     * @returns {number|string|object}
     */
    dequeue() {
        return this._heap.extractRoot();
    }

    /**
     * Removes and returns an element with highest priority in the queue
     * @public
     * @returns {number|string|object}
     */
    pop() {
        return this.dequeue();
    }

    /**
     * Returns the number of elements in the queue
     * @public
     * @returns {number}
     */
    size() {
        return this._heap.size();
    }

    /**
     * Checks if the queue is empty
     * @public
     * @returns {boolean}
     */
    isEmpty() {
        return this._heap.isEmpty();
    }

    /**
     * Clears the queue
     * @public
     */
    clear() {
        this._heap.clear();
    }

    /**
     * Returns a sorted list of elements from highest to lowest priority
     * @public
     * @returns {array}
     */
    toArray() {
        return this._heap.clone().sort().reverse();
    }

    /**
     * Implements an iterable on the priority queue
     * @public
     */
    [Symbol.iterator]( "Symbol.iterator") {
        let size = this.size();
        return {
            next: () => {
                size -= 1;
                return {
                    value: this.pop(),
                    done: size === -1
                };
            }
        };
    }

    /**
     * Creates a priority queue from an existing array
     * @public
     * @static
     * @returns {MyPriorityQueue}
     */
    static fromArray(values, compare) {
        return new MyPriorityQueue(compare, values);
    }
}