Cancel

0850 - Rectangle Area II

Other

·

June 11, 2023

题干

问题描述

破题

要求矩形覆盖的面积,在做了天际线问题和完美矩形问题后,这个问题的思路就看得非常清楚了,只需要沿着某一轴扫描,计算在另一轴上覆盖的长度,计算的时机是遇到每个矩形的入边或者出边的(重复的不算),这样拆分成一个个小矩形,就得到了覆盖的面积和。

于是问题的关键,不妨以 $x$ 轴为扫描方向,就在于如何动态地计算 $y$ 轴上的覆盖长度。

由于本题的限制相当宽松,建筑的数量最多只到 $200$ ,因此可以允许其他非最优解的多种复杂度的方法。

解①:

在 $y$ 轴,对每个矩形竖直的边,$[y_1, y_2]$ ,把边的开始位置 $y_1$,标记 $1$ ,它的结束位置标记 $-1$ ,把这些标记按照 $y$ 轴上的坐标进行排序,对这些标记进行累加,最后的和一定是 $0$ ,它的变化过程一定是从 $0$ 到正值再到 $0$ ,每一次归 $0$ 就意味着一条线段的结束,把这些分离的线段累加起来,这就得到了此时的 $y$ 轴上的线段和。

由于每次扫描都需要排序并重新装填 $y$ 坐标,因此总的时间复杂度为 $O(n\cdot (n\text{log}n + n)) = O(n^2\text{log}n)$ 。

这有可以优化的地方,如果提前把 $y$ 轴离散化,就可以直接做坐标-排名的映射,就不需要每次都排序 $y$ 坐标,于是就只剩下装填所有的 $y$ 坐标的成本,时间复杂度就可以降低为 $O(n^2)$ 。

解②:

更直接地,把 $x$ 轴坐标也离散化,与离散化地 $y$ 轴坐标构成二维数组,每个矩形(的 $4$ 个顶点)在这个二维数组上映射,然后扫描这个二维数组计算面积。

时间复杂度是离散化+矩形映射+统计: $O(n\text{log}n + n + n^2)$ = $O(n^2)$

解③:

上述地算法之所以不够优,关键在于每扫描到一个位置,就要用至少 $O(n)$ 的时间复杂度装填 $y$ 坐标,而拿到这道题目后,第一眼可以看出,我们应该可以用前面的分段树或者树状数组来在 $O(\text{log}n)$ 的时间内装填 $y$ 坐标。

这看起来非常简单,但是,但是,在开始写的时候,才发现这道题并不如想象中那么简单,它有别于天际线那道题的关键是建筑都是连续的,不存在从半空中开始涨起,而矩形没有这个性质,它在 $y$ 轴上的排列可能是不连续的。

线段的取消

这样的话似乎只能在 y 轴空间上存储整个线段,但是如何在矩形结束的时候准确撤销它所属的线段呢? 但是分段树可以按照区间1 额外存储该区间边的数量,这样在新的边加入时,更新的时候把它覆盖的区间的边的数量增 $1$ ,取消边的时候就把区间的边的数量减 $1$ ,在二分区间向下更新的时候,如果某个区间的边数不为零,那么这个区间的值就是它代表的整个线段的长度,否则,就要由子区间的值求和得到。

这样分段树全区间的值,就是 $y$ 轴上线段的总长度。

区间的表示

以为解决了边的添加和取消就完事大吉了吗?,不,还有很大的问题:如果分段树上每个点对应某个坐标,就像习惯得那样,这就面临坐标划分与区间划分二者不一致的问题,对于用坐标表示的边来说,坐标是可重叠的,而分段树的区间划分两端是不可重叠的。举例说,对于 [0, 2] 的边的划分,按照坐标来说就是 $[0, 1]$ 和 $[1, 2]$ ,而按照区间划分则为 $[0,1]$ 和 $[2, 2]$ 。

这实在是不好解决,因为分段树上 $[a, b]$ 的区间不能表示为坐标上 $[a, b]$ 的边,必须寻找一个新的与分段树区间划分一致地表示。于是我们可以考虑坐标可以重叠,但是区间不会重叠,可以定义标号 $i$ 表示坐标从 $[i, i+1]$ 的区间,这样分段树上的 $[a, b]$ 就不再表示坐标 $[a, b]$ 的边,而是标号 $a$~$b$ ,一共 $b-a+1$ 个单位区间的和,这样问题就解决了!

取模与数据类型

最后由于计算结果过大,需要对结果取模,其中模数给得是 $10^9+7$ ,是个质数,这有什么说法吗?

质数取模得可以想到的是中国剩余定理,如果一个一元线性同余2方程的模数两两互质,那么这个方程是有解的而且可以构造出来,但它显然和我们这里没什么关系,事实上可以取任何一个大小符合条件的数,至于质不质地也没什么关系。

至于取模运算本身倒是(对我们有用的)运算性质:

\[\begin{array}{l} &a \cdot b\pmod{N} &\equiv (a\pmod{N} \cdot b\pmod{N})\pmod{N}\\ &(a+b) \pmod{N} &\equiv (a\pmod{N} + b\pmod{N})\pmod{N} \end{array}{}\]

另外需要吐槽得是本题 LeetCode 习惯性地给出的 i32 类型就不适当,它在例题上,在对结果取模前就数值溢出了,你必须手动把它提到更大的整数类型上。

统一排序

由于在每个不同坐标的矩形的进入或离开时都要计算面积,因此像之前把入边和出边分开保存排序的做法3会让代码显得很冗余吗,于是直接把 $x$ 坐标、入边出边的类型标记和 $y$ 轴两坐标,构成一个 $4$ 元组,统一排序。

Rust 实现4

源代码

分段树

struct SegmentTree {
    /// (coverd edges number, range_sum)
    data: Vec<(usize, usize)>,
    y_end: usize,
    y_data: Vec<i32>,
}


/// DFS 型
impl SegmentTree {
    fn new(y_data: Vec<i32>, y_end: usize) -> Self {
        SegmentTree {
            // numbers of interval units
            data: vec![(0, 0); (y_end - 1) * 2],
            y_end,
            y_data,
        }
    }

    fn update(&mut self, l: usize, r: usize, mark: bool) {
        self.update_(l, r, 0, self.y_end - 2, 0, mark)
    }

    fn update_(&mut self, l: usize, r: usize, tl: usize, tr: usize, i: usize, mark: bool) {
        if l > r {
            return;
        }

        let mid = (tl + tr) / 2;
        let sub_len = (mid - tl) + 1;

        let i_lf = i + 1;
        let i_rh = i + 2 * sub_len as usize;

        if l == tl && r == tr {
            if mark {
                self.data[i].0 = self.data[i].0 + 1;
            }
            else {
                self.data[i].0 = self.data[i].0 - 1;
            }
        } else {
            self.update_(l, min(mid, r), tl, mid, i_lf, mark);
            self.update_(max(mid + 1, l), r, mid + 1, tr, i_rh, mark);
        }

        if self.data[i].0 > 0 {
            self.data[i].1 = (self.y_data[tr + 1] - self.y_data[tl]) as usize;
        }
        else if tl != tr {
            self.data[i].1 = self.data[i_lf].1 + self.data[i_rh].1;
        }
        else {
            self.data[i].1 = 0;
        }
    }

    fn sum(&self) -> i32 {
        self.data[0].1 as i32
    }
}

其中 y_end 指得是原坐标的排名,加一后的值,表示总共排名的数量或者总的不同 $y$ 坐标的数量。内部构成的区间数应该比总的不同 $y$ 坐标数少一。

完整过程

use std::{
    cmp::{max, min},
    collections::{BTreeSet, HashMap},
};

const M: u64 = 1_000_000_000 + 7;

pub fn solve(rectangles: Vec<Vec<i32>>) -> i32 {
    let (y_data, y_map) = build_y_discrezation(&rectangles);

    let lines = build_x_lines(rectangles, &y_map);

    let mut tree = SegmentTree::new(y_data, y_map.len());

    let mut acc = 0;
    let mut x0 = lines[0].0;

    for (x, ty, y1, y2) in lines {

        if x != x0 {
            let h = tree.sum();

            acc += ((x - x0) as u64 * h as u64) % M;

            x0 = x;
        }

        tree.update(y1, y2 - 1, ty);

    }

    (acc as u64 % M) as i32
}

fn build_y_discrezation(rectangles: &Vec<Vec<i32>>) -> (Vec<i32>, HashMap<i32, usize>) {
    let mut set = BTreeSet::new();

    for rec in rectangles {
        set.insert(rec[1]);
        set.insert(rec[3]);
    }

    (
        set.iter().cloned().collect(),
        set.into_iter()
            .enumerate()
            .map(|(i, x)| (x, i as usize))
            .collect(),
    )
}

fn build_x_lines(
    rectangles: Vec<Vec<i32>>,
    y_map: &HashMap<i32, usize>,
) -> Vec<(i32, bool, usize, usize)> {
    let mut lines = Vec::with_capacity(rectangles.len() * 2);

    for rec in rectangles {
        let x1 = rec[0];
        let x2 = rec[2];
        let y1 = *y_map.get(&rec[1]).unwrap();
        let y2 = *y_map.get(&rec[3]).unwrap();

        lines.push((x1, true, y1, y2));  // 0  true for enter
        lines.push((x2, false, y1, y2)); // 1  false for exit
    }

    lines.sort_unstable();

    lines
}

注解

  1. 用树状数组好像也不好解决这个问题 ↩

  2. 同余就是余数相同的意思 ↩

  3. 为了理论上减少排序的负担 ↩

  4. 就像前面吐槽用 Python 主要是因为它足够慢,足够缺少优化,这个 Rust 实现运行基本在 0-2 ms,内存 2.x MB,很难说有测量比较的意义 ↩

© minghu6

·

theme

Simplex theme logo

by golas