普通视图

发现新文章,点击刷新页面。
昨天 — 2026年1月14日LeetCode 每日一题题解

[Python3/Java/C++/Go/TypeScript] 一题一解:扫描线(清晰题解)

作者 lcbin
2026年1月14日 07:48

方法一:扫描线

本题可以使用扫描线算法来计算所有正方形的总面积。

我们将每个正方形的上下边界作为扫描线的事件点,按 $y$ 坐标从小到大排序。对于每个事件点,我们使用线段树来维护当前扫描线下方被覆盖的 $x$ 轴区间长度,从而计算出当前扫描线与上一个扫描线之间的面积增量。

具体步骤如下:

  1. 预处理事件点:对于每个正方形,计算其上下边界的 $y$ 坐标,并将其作为事件点加入事件列表中。每个事件点包含 $y$ 坐标、左边界 $x_1$、右边界 $x_2$ 以及一个标志(表示是上边界还是下边界)。
  2. 排序事件点:将所有事件点按 $y$ 坐标从小到大排序。
  3. 构建线段树:使用离散化后的 $x$ 坐标构建线段树,用于维护当前被覆盖的 $x$ 轴区间长度。
  4. 扫描事件点:遍历排序后的事件点列表,对于每个事件点:
    • 计算当前事件点与上一个事件点之间的面积增量,并累加到总面积中。
    • 根据当前事件点的类型(上边界或下边界),更新线段树,增加或减少对应的 $x$ 轴区间覆盖计数。
  5. 计算目标面积:总面积的一半即为目标面积。
  6. 再次扫描事件点:再次遍历事件点列表,计算累计面积,当累计面积达到目标面积时,计算并返回对应的 $y$ 坐标。

###python

class Node:
    __slots__ = ("l", "r", "cnt", "length")

    def __init__(self):
        self.l = self.r = 0
        self.cnt = self.length = 0


class SegmentTree:
    def __init__(self, nums):
        n = len(nums) - 1
        self.nums = nums
        self.tr = [Node() for _ in range(n << 2)]
        self.build(1, 0, n - 1)

    def build(self, u, l, r):
        self.tr[u].l, self.tr[u].r = l, r
        if l != r:
            mid = (l + r) >> 1
            self.build(u << 1, l, mid)
            self.build(u << 1 | 1, mid + 1, r)

    def modify(self, u, l, r, k):
        if self.tr[u].l >= l and self.tr[u].r <= r:
            self.tr[u].cnt += k
        else:
            mid = (self.tr[u].l + self.tr[u].r) >> 1
            if l <= mid:
                self.modify(u << 1, l, r, k)
            if r > mid:
                self.modify(u << 1 | 1, l, r, k)
        self.pushup(u)

    def pushup(self, u):
        if self.tr[u].cnt:
            self.tr[u].length = self.nums[self.tr[u].r + 1] - self.nums[self.tr[u].l]
        elif self.tr[u].l == self.tr[u].r:
            self.tr[u].length = 0
        else:
            self.tr[u].length = self.tr[u << 1].length + self.tr[u << 1 | 1].length

    @property
    def length(self):
        return self.tr[1].length


class Solution:
    def separateSquares(self, squares: List[List[int]]) -> float:
        xs = set()
        segs = []
        for x1, y1, l in squares:
            x2, y2 = x1 + l, y1 + l
            xs.update([x1, x2])
            segs.append((y1, x1, x2, 1))
            segs.append((y2, x1, x2, -1))
        segs.sort()
        st = sorted(xs)
        tree = SegmentTree(st)
        d = {x: i for i, x in enumerate(st)}
        area = 0
        y0 = 0
        for y, x1, x2, k in segs:
            area += (y - y0) * tree.length
            tree.modify(1, d[x1], d[x2] - 1, k)
            y0 = y

        target = area / 2
        area = 0
        y0 = 0
        for y, x1, x2, k in segs:
            t = (y - y0) * tree.length
            if area + t >= target:
                return y0 + (target - area) / tree.length
            area += t
            tree.modify(1, d[x1], d[x2] - 1, k)
            y0 = y
        return 0

###java

class Node {
    int l, r, cnt, length;
}

class SegmentTree {
    private Node[] tr;
    private int[] nums;

    public SegmentTree(int[] nums) {
        this.nums = nums;
        int n = nums.length - 1;
        tr = new Node[n << 2];
        for (int i = 0; i < tr.length; ++i) {
            tr[i] = new Node();
        }
        build(1, 0, n - 1);
    }

    private void build(int u, int l, int r) {
        tr[u].l = l;
        tr[u].r = r;
        if (l != r) {
            int mid = (l + r) >> 1;
            build(u << 1, l, mid);
            build(u << 1 | 1, mid + 1, r);
        }
    }

    public void modify(int u, int l, int r, int k) {
        if (tr[u].l >= l && tr[u].r <= r) {
            tr[u].cnt += k;
        } else {
            int mid = (tr[u].l + tr[u].r) >> 1;
            if (l <= mid) {
                modify(u << 1, l, r, k);
            }
            if (r > mid) {
                modify(u << 1 | 1, l, r, k);
            }
        }
        pushup(u);
    }

    private void pushup(int u) {
        if (tr[u].cnt > 0) {
            tr[u].length = nums[tr[u].r + 1] - nums[tr[u].l];
        } else if (tr[u].l == tr[u].r) {
            tr[u].length = 0;
        } else {
            tr[u].length = tr[u << 1].length + tr[u << 1 | 1].length;
        }
    }

    public int query() {
        return tr[1].length;
    }
}

class Solution {
    public double separateSquares(int[][] squares) {
        Set<Integer> xs = new HashSet<>();
        List<int[]> segs = new ArrayList<>();
        for (int[] sq : squares) {
            int x1 = sq[0], y1 = sq[1], l = sq[2];
            int x2 = x1 + l, y2 = y1 + l;
            xs.add(x1);
            xs.add(x2);
            segs.add(new int[] {y1, x1, x2, 1});
            segs.add(new int[] {y2, x1, x2, -1});
        }
        segs.sort(Comparator.comparingInt(a -> a[0]));
        int[] st = new int[xs.size()];
        int i = 0;
        for (int x : xs) {
            st[i++] = x;
        }
        Arrays.sort(st);
        SegmentTree tree = new SegmentTree(st);
        Map<Integer, Integer> d = new HashMap<>(st.length);
        for (i = 0; i < st.length; i++) {
            d.put(st[i], i);
        }
        double area = 0.0;
        int y0 = 0;
        for (int[] s : segs) {
            int y = s[0], x1 = s[1], x2 = s[2], k = s[3];
            area += (double) (y - y0) * tree.query();
            tree.modify(1, d.get(x1), d.get(x2) - 1, k);
            y0 = y;
        }
        double target = area / 2.0;
        area = 0.0;
        y0 = 0;
        for (int[] s : segs) {
            int y = s[0], x1 = s[1], x2 = s[2], k = s[3];
            double t = (double) (y - y0) * tree.query();
            if (area + t >= target) {
                return y0 + (target - area) / tree.query();
            }
            area += t;
            tree.modify(1, d.get(x1), d.get(x2) - 1, k);
            y0 = y;
        }
        return 0.0;
    }
}

###cpp

struct Node {
    int l = 0, r = 0, cnt = 0;
    int length = 0;
};

class SegmentTree {
private:
    vector<Node> tr;
    vector<int> nums;

    void build(int u, int l, int r) {
        tr[u].l = l;
        tr[u].r = r;
        if (l != r) {
            int mid = (l + r) >> 1;
            build(u << 1, l, mid);
            build(u << 1 | 1, mid + 1, r);
        }
    }

    void pushup(int u) {
        if (tr[u].cnt > 0) {
            tr[u].length = nums[tr[u].r + 1] - nums[tr[u].l];
        } else if (tr[u].l == tr[u].r) {
            tr[u].length = 0;
        } else {
            tr[u].length = tr[u << 1].length + tr[u << 1 | 1].length;
        }
    }

public:
    SegmentTree(const vector<int>& nums)
        : nums(nums) {
        int n = (int) nums.size() - 1;
        tr.assign(n << 2, Node());
        build(1, 0, n - 1);
    }

    void modify(int u, int l, int r, int k) {
        if (tr[u].l >= l && tr[u].r <= r) {
            tr[u].cnt += k;
        } else {
            int mid = (tr[u].l + tr[u].r) >> 1;
            if (l <= mid) modify(u << 1, l, r, k);
            if (r > mid) modify(u << 1 | 1, l, r, k);
        }
        pushup(u);
    }

    int query() const {
        return tr[1].length;
    }
};

class Solution {
public:
    double separateSquares(vector<vector<int>>& squares) {
        set<int> xs;
        vector<array<int, 4>> segs;

        for (auto& sq : squares) {
            int x1 = sq[0], y1 = sq[1], l = sq[2];
            int x2 = x1 + l, y2 = y1 + l;
            xs.insert(x1);
            xs.insert(x2);
            segs.push_back({y1, x1, x2, 1});
            segs.push_back({y2, x1, x2, -1});
        }

        sort(segs.begin(), segs.end(), [](const auto& a, const auto& b) {
            return a[0] < b[0];
        });

        vector<int> st;
        st.reserve(xs.size());
        for (int x : xs) st.push_back(x);

        SegmentTree tree(st);

        unordered_map<int, int> d;
        d.reserve(st.size() * 2);
        for (int i = 0; i < (int) st.size(); i++) d[st[i]] = i;

        double area = 0.0;
        int y0 = 0;
        for (auto& s : segs) {
            int y = s[0], x1 = s[1], x2 = s[2], k = s[3];
            area += (double) (y - y0) * tree.query();
            tree.modify(1, d[x1], d[x2] - 1, k);
            y0 = y;
        }

        double target = area / 2.0;
        area = 0.0;
        y0 = 0;
        for (auto& s : segs) {
            int y = s[0], x1 = s[1], x2 = s[2], k = s[3];
            double t = (double) (y - y0) * tree.query();
            if (area + t >= target) {
                return y0 + (target - area) / tree.query();
            }
            area += t;
            tree.modify(1, d[x1], d[x2] - 1, k);
            y0 = y;
        }

        return 0.0;
    }
};

###go

type Node struct {
l, r   int
cnt    int
length int
}

type SegmentTree struct {
tr   []Node
nums []int
}

func NewSegmentTree(nums []int) *SegmentTree {
n := len(nums) - 1
tr := make([]Node, n<<2)
t := &SegmentTree{tr: tr, nums: nums}
t.build(1, 0, n-1)
return t
}

func (t *SegmentTree) build(u, l, r int) {
t.tr[u].l = l
t.tr[u].r = r
if l != r {
mid := (l + r) >> 1
t.build(u<<1, l, mid)
t.build(u<<1|1, mid+1, r)
}
}

func (t *SegmentTree) modify(u, l, r, k int) {
if l > r {
return
}
if t.tr[u].l >= l && t.tr[u].r <= r {
t.tr[u].cnt += k
} else {
mid := (t.tr[u].l + t.tr[u].r) >> 1
if l <= mid {
t.modify(u<<1, l, r, k)
}
if r > mid {
t.modify(u<<1|1, l, r, k)
}
}
t.pushup(u)
}

func (t *SegmentTree) pushup(u int) {
if t.tr[u].cnt > 0 {
t.tr[u].length = t.nums[t.tr[u].r+1] - t.nums[t.tr[u].l]
} else if t.tr[u].l == t.tr[u].r {
t.tr[u].length = 0
} else {
t.tr[u].length = t.tr[u<<1].length + t.tr[u<<1|1].length
}
}

func (t *SegmentTree) query() int {
return t.tr[1].length
}

func separateSquares(squares [][]int) float64 {
pos := make(map[int]bool)
xs := make([]int, 0)
segs := make([][]int, 0, len(squares)*2)
for _, sq := range squares {
x1, y1, l := sq[0], sq[1], sq[2]
x2, y2 := x1+l, y1+l
if !pos[x1] {
pos[x1] = true
xs = append(xs, x1)
}
if !pos[x2] {
pos[x2] = true
xs = append(xs, x2)
}
segs = append(segs, []int{y1, x1, x2, 1})
segs = append(segs, []int{y2, x1, x2, -1})
}
sort.Slice(segs, func(i, j int) bool { return segs[i][0] < segs[j][0] })
sort.Ints(xs)
tree := NewSegmentTree(xs)
d := make(map[int]int, len(xs))
for i, x := range xs {
d[x] = i
}
area := 0.0
y0 := 0
for _, s := range segs {
y, x1, x2, k := s[0], s[1], s[2], s[3]
area += float64(y-y0) * float64(tree.query())
tree.modify(1, d[x1], d[x2]-1, k)
y0 = y
}
target := area / 2.0
area = 0.0
y0 = 0
for _, s := range segs {
y, x1, x2, k := s[0], s[1], s[2], s[3]
curLen := tree.query()
t := float64(y-y0) * float64(curLen)
if area+t >= target {
return float64(y0) + (target-area)/float64(curLen)
}
area += t
tree.modify(1, d[x1], d[x2]-1, k)
y0 = y
}
return 0.0
}

###ts

class Node {
    l = 0;
    r = 0;
    cnt = 0;
    length = 0;
}

class SegmentTree {
    private tr: Node[];
    private nums: number[];
    constructor(nums: number[]) {
        this.nums = nums;
        const n = nums.length - 1;
        this.tr = Array.from({ length: n << 2 }, () => new Node());
        this.build(1, 0, n - 1);
    }

    private build(u: number, l: number, r: number): void {
        this.tr[u].l = l;
        this.tr[u].r = r;
        if (l !== r) {
            const mid = (l + r) >> 1;
            this.build(u << 1, l, mid);
            this.build((u << 1) | 1, mid + 1, r);
        }
    }

    modify(u: number, l: number, r: number, k: number): void {
        if (l > r) return;
        if (this.tr[u].l >= l && this.tr[u].r <= r) {
            this.tr[u].cnt += k;
        } else {
            const mid = (this.tr[u].l + this.tr[u].r) >> 1;
            if (l <= mid) this.modify(u << 1, l, r, k);
            if (r > mid) this.modify((u << 1) | 1, l, r, k);
        }
        this.pushup(u);
    }

    private pushup(u: number): void {
        if (this.tr[u].cnt > 0) {
            this.tr[u].length = this.nums[this.tr[u].r + 1] - this.nums[this.tr[u].l];
        } else if (this.tr[u].l === this.tr[u].r) {
            this.tr[u].length = 0;
        } else {
            this.tr[u].length = this.tr[u << 1].length + this.tr[(u << 1) | 1].length;
        }
    }

    query(): number {
        return this.tr[1].length;
    }
}

function separateSquares(squares: number[][]): number {
    const xsSet = new Set<number>();
    const segs: number[][] = [];
    for (const [x1, y1, l] of squares) {
        const [x2, y2] = [x1 + l, y1 + l];
        xsSet.add(x1);
        xsSet.add(x2);
        segs.push([y1, x1, x2, 1]);
        segs.push([y2, x1, x2, -1]);
    }
    segs.sort((a, b) => a[0] - b[0]);
    const xs = Array.from(xsSet);
    xs.sort((a, b) => a - b);
    const tree = new SegmentTree(xs);
    const d = new Map<number, number>();
    for (let i = 0; i < xs.length; i++) {
        d.set(xs[i], i);
    }
    let area = 0.0;
    let y0 = 0;
    for (const [y, x1, x2, k] of segs) {
        area += (y - y0) * tree.query();
        tree.modify(1, d.get(x1)!, d.get(x2)! - 1, k);
        y0 = y;
    }
    const target = area / 2.0;
    area = 0.0;
    y0 = 0;
    for (const [y, x1, x2, k] of segs) {
        const curLen = tree.query();
        const t = (y - y0) * curLen;
        if (area + t >= target) {
            return y0 + (target - area) / curLen;
        }
        area += t;
        tree.modify(1, d.get(x1)!, d.get(x2)! - 1, k);
        y0 = y;
    }
    return 0.0;
}

时间复杂度 $O(n \times \log n)$,空间复杂度 $O(n)$。其中 $n$ 是正方形的数量。


有任何问题,欢迎评论区交流,欢迎评论区提供其它解题思路(代码),也可以点个赞支持一下作者哈 😄~

每日一题-分割正方形 II🔴

2026年1月14日 00:00

给你一个二维整数数组 squares ,其中 squares[i] = [xi, yi, li] 表示一个与 x 轴平行的正方形的左下角坐标和正方形的边长。

找到一个最小的 y 坐标,它对应一条水平线,该线需要满足它以上正方形的总面积 等于 该线以下正方形的总面积。

答案如果与实际答案的误差在 10-5 以内,将视为正确答案。

注意:正方形 可能会 重叠。重叠区域只 统计一次 

 

示例 1:

输入: squares = [[0,0,1],[2,2,1]]

输出: 1.00000

解释:

任何在 y = 1y = 2 之间的水平线都会有 1 平方单位的面积在其上方,1 平方单位的面积在其下方。最小的 y 坐标是 1。

示例 2:

输入: squares = [[0,0,2],[1,1,1]]

输出: 1.00000

解释:

由于蓝色正方形和红色正方形有重叠区域且重叠区域只统计一次。所以直线 y = 1 将正方形分割成两部分且面积相等。

 

提示:

  • 1 <= squares.length <= 5 * 104
  • squares[i] = [xi, yi, li]
  • squares[i].length == 3
  • 0 <= xi, yi <= 109
  • 1 <= li <= 109
  • 所有正方形的总面积不超过 1015

Lazy 线段树 + 扫描线(Python/Java/C++/Go)

作者 endlesscheng
2025年2月17日 17:23

前置题目850. 矩形面积 II我的题解

首先用 850 题的扫描线方法,求出所有正方形的面积并 $\textit{totArea}$。

然后再次扫描,设扫描线下方的面积和为 $\textit{area}$,那么扫描线上方的面积和为 $\textit{totArea} - \textit{area}$。

题目要求

$$
\textit{area} = \textit{totArea} - \textit{area}
$$

$$
\textit{area}\cdot 2 = \textit{totArea}
$$

设当前扫描线的纵坐标为 $y$,下一个需要经过的正方形上/下边界的纵坐标为 $y'$,被至少一个正方形覆盖的底边长之和为 $\textit{sumLen}$,那么新的面积和为

$$
\textit{area} + \textit{sumLen} \cdot (y'-y)
$$

如果发现

$$
(\textit{area} + \textit{sumLen} \cdot (y'-y))\cdot 2 \ge \textit{totArea}
$$

取等号,解得

$$
y' = y + \dfrac{\textit{totalArea}/2 - \textit{area}}{\textit{sumL}} = y + \dfrac{\textit{totalArea} - \textit{area}\cdot 2}{\textit{sumL}\cdot 2}
$$

即为答案。

编程技巧:把第一次扫描过程中的关键数据 $\textit{area}$ 和 $\textit{sumLen}$ 记录到一个数组中,然后遍历数组(或者二分),这样可以避免跑两遍线段树(空间换时间)。

###py

class Node:
    __slots__ = 'l', 'r', 'min_cover_len', 'min_cover', 'todo'

    def __init__(self):
        self.l = 0
        self.r = 0
        self.min_cover_len = 0  # 区间内被矩形覆盖次数最少的底边长之和
        self.min_cover = 0      # 区间内被矩形覆盖的最小次数
        self.todo = 0           # 子树内的所有节点的 min_cover 需要增加的量,注意这可以是负数


class SegmentTree:
    def __init__(self, xs: List[int]):
        n = len(xs) - 1  # xs.size() 个横坐标有 xs.size()-1 个差值
        self.seg = [Node() for _ in range(2 << (n - 1).bit_length())]
        self.build(xs, 1, 0, n - 1)

    def get_uncovered_length(self) -> int:
        return 0 if self.seg[1].min_cover else self.seg[1].min_cover_len

    # 根据左右儿子的信息,更新当前节点的信息
    def maintain(self, o: int) -> None:
        lo = self.seg[o * 2]
        ro = self.seg[o * 2 + 1]
        mn = min(lo.min_cover, ro.min_cover)
        self.seg[o].min_cover = mn
        # 只统计等于 min_cover 的底边长之和
        self.seg[o].min_cover_len = (lo.min_cover_len if lo.min_cover == mn else 0) + \
                                    (ro.min_cover_len if ro.min_cover == mn else 0)

    # 仅更新节点信息,不下传懒标记 todo
    def do(self, o: int, v: int) -> None:
        self.seg[o].min_cover += v
        self.seg[o].todo += v

    # 下传懒标记 todo
    def spread(self, o: int) -> None:
        v = self.seg[o].todo
        if v:
            self.do(o * 2, v)
            self.do(o * 2 + 1, v)
            self.seg[o].todo = 0

    # 建树
    def build(self, xs: List[int], o: int, l: int, r: int) -> None:
        self.seg[o].l = l
        self.seg[o].r = r
        if l == r:
            self.seg[o].min_cover_len = xs[l + 1] - xs[l]
            return
        m = (l + r) // 2
        self.build(xs, o * 2, l, m)
        self.build(xs, o * 2 + 1, m + 1, r)
        self.maintain(o)

    # 区间更新
    def update(self, o: int, l: int, r: int, v: int) -> None:
        if l <= self.seg[o].l and self.seg[o].r <= r:
            self.do(o, v)
            return
        self.spread(o)
        m = (self.seg[o].l + self.seg[o].r) // 2
        if l <= m:
            self.update(o * 2, l, r, v)
        if m < r:
            self.update(o * 2 + 1, l, r, v)
        self.maintain(o)


# 代码逻辑同 850 题,增加一个 records 数组记录关键数据
class Solution:
    def separateSquares(self, squares: List[List[int]]) -> float:
        xs = []
        events = []
        for lx, y, l in squares:
            rx = lx + l
            xs.append(lx)
            xs.append(rx)
            events.append((y, lx, rx, 1))
            events.append((y + l, lx, rx, -1))

        # 排序,方便离散化
        xs = sorted(set(xs))

        # 初始化线段树
        t = SegmentTree(xs)

        # 模拟扫描线从下往上移动
        events.sort(key=lambda e: e[0])
        records = []
        tot_area = 0
        for (y, lx, rx, delta), e2 in pairwise(events):
            l = bisect_left(xs, lx)  # 离散化
            r = bisect_left(xs, rx) - 1  # r 对应着 xs[r] 与 xs[r+1]=rx 的差值
            t.update(1, l, r, delta)  # 更新被 [lx, rx] 覆盖的次数
            sum_len = xs[-1] - xs[0] - t.get_uncovered_length()  # 减去没被矩形覆盖的长度
            records.append((tot_area, sum_len))  # 记录关键数据
            tot_area += sum_len * (e2[0] - y)  # 新增面积 = 被至少一个矩形覆盖的底边长之和 * 矩形高度

        # 二分找最后一个 < tot_area / 2 的面积
        i = bisect_left(records, tot_area, key=lambda r: r[0] * 2) - 1
        area, sum_len = records[i]
        return events[i][0] + (tot_area - area * 2) / (sum_len * 2)

###java

class SegmentTree {
    private final int n;
    private final int[] minCoverLen; // 区间内被矩形覆盖次数最少的底边长之和
    private final int[] minCover;    // 区间内被矩形覆盖的最小次数
    private final int[] todo;        // 子树内的所有节点的 minCover 需要增加的量,注意这可以是负数

    public SegmentTree(int[] xs) {
        n = xs.length - 1; // xs.length 个横坐标有 xs.length-1 个差值
        int size = 2 << (32 - Integer.numberOfLeadingZeros(n - 1));
        minCoverLen = new int[size];
        minCover = new int[size];
        todo = new int[size];
        build(xs, 1, 0, n - 1);
    }

    public void update(int l, int r, int v) {
        update(1, 0, n - 1, l, r, v);
    }

    public int getUncoveredLength() {
        return minCover[1] == 0 ? minCoverLen[1] : 0;
    }

    // 根据左右儿子的信息,更新当前节点的信息
    private void maintain(int o) {
        int mn = Math.min(minCover[o * 2], minCover[o * 2 + 1]);
        minCover[o] = mn;
        // 只统计等于 mn 的底边长之和
        minCoverLen[o] = (minCover[o * 2] == mn ? minCoverLen[o * 2] : 0) +
                (minCover[o * 2 + 1] == mn ? minCoverLen[o * 2 + 1] : 0);
    }

    // 仅更新节点信息,不下传懒标记 todo
    private void do_(int o, int v) {
        minCover[o] += v;
        todo[o] += v;
    }

    // 下传懒标记 todo
    private void spread(int o) {
        if (todo[o] != 0) {
            do_(o * 2, todo[o]);
            do_(o * 2 + 1, todo[o]);
            todo[o] = 0;
        }
    }

    // 建树
    private void build(int[] xs, int o, int l, int r) {
        if (l == r) {
            minCoverLen[o] = xs[l + 1] - xs[l];
            return;
        }
        int m = (l + r) / 2;
        build(xs, o * 2, l, m);
        build(xs, o * 2 + 1, m + 1, r);
        maintain(o);
    }

    // 区间更新
    private void update(int o, int l, int r, int ql, int qr, int v) {
        if (ql <= l && r <= qr) {
            do_(o, v);
            return;
        }
        spread(o);
        int m = (l + r) / 2;
        if (ql <= m) {
            update(o * 2, l, m, ql, qr, v);
        }
        if (m < qr) {
            update(o * 2 + 1, m + 1, r, ql, qr, v);
        }
        maintain(o);
    }
}

// 代码逻辑同 850 题,增加一个 records 数组记录关键数据
class Solution {
    private record Event(int y, int lx, int rx, int delta) {
    }

    private record Record(long area, int sumLen) {
    }

    public double separateSquares(int[][] squares) {
        int n = squares.length * 2;
        int[] xs = new int[n];
        Event[] events = new Event[n];
        n = 0;
        for (int[] sq : squares) {
            int lx = sq[0];
            int y = sq[1];
            int l = sq[2];
            int rx = lx + l;
            xs[n] = lx;
            xs[n + 1] = rx;
            events[n++] = new Event(y, lx, rx, 1);
            events[n++] = new Event(y + l, lx, rx, -1);
        }

        // 排序,方便离散化
        Arrays.sort(xs);

        // 初始化线段树
        SegmentTree t = new SegmentTree(xs);

        // 模拟扫描线从下往上移动
        Arrays.sort(events, (a, b) -> a.y - b.y);
        Record records[] = new Record[n - 1];
        long totArea = 0;
        for (int i = 0; i < n - 1; i++) {
            Event e = events[i];
            int l = Arrays.binarySearch(xs, e.lx); // 离散化
            int r = Arrays.binarySearch(xs, e.rx) - 1; // r 对应着 xs[r] 与 xs[r+1]=rx 的差值
            t.update(l, r, e.delta); // 更新被 [lx, rx] 覆盖的次数
            int sumLen = xs[n - 1] - xs[0] - t.getUncoveredLength(); // 减去没被矩形覆盖的长度
            records[i] = new Record(totArea, sumLen);
            totArea += (long) sumLen * (events[i + 1].y - e.y); // 新增面积 = 被至少一个矩形覆盖的底边长之和 * 矩形高度
        }

        // 找最后一个 < totArea / 2 的面积
        int i = 0;
        while (i < n - 1 && records[i].area * 2 < totArea) {
            i++;
        }
        i--;
        return events[i].y + (totArea - records[i].area * 2) / (records[i].sumLen * 2.0);
    }
}

###cpp

class SegmentTree {
public:
    SegmentTree(vector<int>& xs) {
        unsigned n = xs.size() - 1; // xs.size() 个横坐标有 xs.size()-1 个差值
        seg.resize(2 << bit_width(n - 1));
        build(xs, 1, 0, n - 1);
    }

    void update(int l, int r, int v) {
        update(1, l, r, v);
    }

    int get_uncovered_length() {
        return seg[1].min_cover ? 0 : seg[1].min_cover_len;
    }

private:
    struct Node {
        int l, r;
        int min_cover_len = 0; // 区间内被矩形覆盖次数最少的底边长之和
        int min_cover = 0;     // 区间内被矩形覆盖的最小次数
        int todo = 0;          // 子树内的所有节点的 min_cover 需要增加的量,注意这可以是负数
    };

    vector<Node> seg;

    // 根据左右儿子的信息,更新当前节点的信息
    void maintain(int o) {
        Node& lo = seg[o * 2];
        Node& ro = seg[o * 2 + 1];
        int mn = min(lo.min_cover, ro.min_cover);
        seg[o].min_cover = mn;
        // 只统计等于 min_cover 的底边长之和
        seg[o].min_cover_len = (lo.min_cover == mn ? lo.min_cover_len : 0) +
                               (ro.min_cover == mn ? ro.min_cover_len : 0);
    }

    // 仅更新节点信息,不下传懒标记 todo
    void do_(int o, int v) {
        seg[o].min_cover += v;
        seg[o].todo += v;
    }

    // 下传懒标记 todo
    void spread(int o) {
        int& v = seg[o].todo;
        if (v != 0) {
            do_(o * 2, v);
            do_(o * 2 + 1, v);
            v = 0;
        }
    }

    // 建树
    void build(vector<int>& xs, int o, int l, int r) {
        seg[o].l = l;
        seg[o].r = r;
        if (l == r) {
            seg[o].min_cover_len = xs[l + 1] - xs[l];
            return;
        }
        int m = (l + r) / 2;
        build(xs, o * 2, l, m);
        build(xs, o * 2 + 1, m + 1, r);
        maintain(o);
    }

    // 区间更新
    void update(int o, int l, int r, int v) {
        if (l <= seg[o].l && seg[o].r <= r) {
            do_(o, v);
            return;
        }
        spread(o);
        int m = (seg[o].l + seg[o].r) / 2;
        if (l <= m) {
            update(o * 2, l, r, v);
        }
        if (m < r) {
            update(o * 2 + 1, l, r, v);
        }
        maintain(o);
    }
};

// 代码逻辑同 850 题,增加一个 records 数组记录关键数据
class Solution {
public:
    double separateSquares(vector<vector<int>>& squares) {
        vector<int> xs;
        struct Event { int y, lx, rx, delta; };
        vector<Event> events;
        for (auto& sq : squares) {
            int lx = sq[0], y = sq[1], l = sq[2];
            int rx = lx + l;
            xs.push_back(lx);
            xs.push_back(rx);
            events.emplace_back(y, lx, rx, 1);
            events.emplace_back(y + l, lx, rx, -1);
        }

        // 排序去重,方便离散化
        ranges::sort(xs);
        xs.erase(ranges::unique(xs).begin(), xs.end());

        // 初始化线段树
        SegmentTree t(xs);

        // 模拟扫描线从下往上移动
        ranges::sort(events, {}, &Event::y);
        vector<pair<long long, int>> records(events.size() - 1);
        long long tot_area = 0;
        for (int i = 0; i + 1 < events.size(); i++) {
            auto& [y, lx, rx, delta] = events[i];
            int l = ranges::lower_bound(xs, lx) - xs.begin(); // 离散化
            int r = ranges::lower_bound(xs, rx) - xs.begin() - 1; // r 对应着 xs[r] 与 xs[r+1]=rx 的差值
            t.update(l, r, delta); // 更新被 [lx, rx] 覆盖的次数
            int sum_len = xs.back() - xs[0] - t.get_uncovered_length(); // 减去没被矩形覆盖的长度
            records[i] = {tot_area, sum_len};
            tot_area += 1LL * sum_len * (events[i + 1].y - y); // 新增面积 = 被至少一个矩形覆盖的底边长之和 * 矩形高度
        }

        // 二分找最后一个 < tot_area / 2 的面积
        int i = ranges::lower_bound(records, tot_area, {}, [](auto& p) { return p.first * 2; }) - records.begin() - 1;
        auto [area, sum_len] = records[i];
        return events[i].y + (tot_area - area * 2) / (sum_len * 2.0);
    }
};

###go

// 线段树每个节点维护一段横坐标区间 [lx, rx]
type seg []struct {
    l, r        int
    minCoverLen int // 区间内被矩形覆盖次数最少的底边长之和
    minCover    int // 区间内被矩形覆盖的最小次数
    todo        int // 子树内的所有节点的 minCover 需要增加的量,注意这可以是负数
}

// 根据左右儿子的信息,更新当前节点的信息
func (t seg) maintain(o int) {
    lo, ro := &t[o<<1], &t[o<<1|1]
    mn := min(lo.minCover, ro.minCover)
    t[o].minCover = mn
    t[o].minCoverLen = 0
    if lo.minCover == mn { // 只统计等于 minCover 的底边长之和
        t[o].minCoverLen = lo.minCoverLen
    }
    if ro.minCover == mn {
        t[o].minCoverLen += ro.minCoverLen
    }
}

// 仅更新节点信息,不下传懒标记 todo
func (t seg) do(o, v int) {
    t[o].minCover += v
    t[o].todo += v
}

// 下传懒标记 todo
func (t seg) spread(o int) {
    v := t[o].todo
    if v != 0 {
        t.do(o<<1, v)
        t.do(o<<1|1, v)
        t[o].todo = 0
    }
}

// 建树
func (t seg) build(xs []int, o, l, r int) {
    t[o].l, t[o].r = l, r
    t[o].todo = 0
    if l == r {
        t[o].minCover = 0
        t[o].minCoverLen = xs[l+1] - xs[l]
        return
    }
    m := (l + r) >> 1
    t.build(xs, o<<1, l, m)
    t.build(xs, o<<1|1, m+1, r)
    t.maintain(o)
}

// 区间更新
func (t seg) update(o, l, r, v int) {
    if l <= t[o].l && t[o].r <= r {
        t.do(o, v)
        return
    }
    t.spread(o)
    m := (t[o].l + t[o].r) >> 1
    if l <= m {
        t.update(o<<1, l, r, v)
    }
    if m < r {
        t.update(o<<1|1, l, r, v)
    }
    t.maintain(o)
}

// 代码逻辑同 850 题,增加一个 records 数组记录关键数据
func separateSquares(squares [][]int) float64 {
    m := len(squares) * 2
    xs := make([]int, 0, m)
    type event struct{ y, lx, rx, delta int }
    events := make([]event, 0, m)
    for _, sq := range squares {
        lx, y, l := sq[0], sq[1], sq[2]
        rx := lx + l
        xs = append(xs, lx, rx)
        events = append(events, event{y, lx, rx, 1}, event{y + l, lx, rx, -1})
    }

    // 排序去重,方便离散化
    slices.Sort(xs)
    xs = slices.Compact(xs)

    // 初始化线段树
    n := len(xs) - 1 // len(xs) 个横坐标有 len(xs)-1 个差值
    t := make(seg, 2<<bits.Len(uint(n-1)))
    t.build(xs, 1, 0, n-1)

    // 模拟扫描线从下往上移动
    slices.SortFunc(events, func(a, b event) int { return a.y - b.y })
    type pair struct{ area, sumLen int }
    records := make([]pair, m-1)
    totArea := 0
    for i, e := range events[:m-1] {
        l := sort.SearchInts(xs, e.lx)
        r := sort.SearchInts(xs, e.rx) - 1 // 注意 r 对应着 xs[r] 与 xs[r+1]=e.rx 的差值
        t.update(1, l, r, e.delta)         // 更新被 [e.lx, e.rx] 覆盖的次数
        sumLen := xs[len(xs)-1] - xs[0]    // 总的底边长度
        if t[1].minCover == 0 {            // 需要去掉没被矩形覆盖的长度
            sumLen -= t[1].minCoverLen
        }
        records[i] = pair{totArea, sumLen} // 记录关键数据
        totArea += sumLen * (events[i+1].y - e.y) // 新增面积 = 被至少一个矩形覆盖的底边长之和 * 矩形高度
    }

    // 二分找最后一个 < totArea / 2 的面积
    i := sort.Search(m-1, func(i int) bool { return records[i].area*2 >= totArea }) - 1
    return float64(events[i].y) + float64(totArea-records[i].area*2)/float64(records[i].sumLen*2)
}

复杂度分析

  • 时间复杂度:$\mathcal{O}(n\log n)$,其中 $n$ 是 $\textit{squares}$ 的长度。
  • 空间复杂度:$\mathcal{O}(n)$。

更多相似题目,见下面数据结构题单中的「§8.4 Lazy 线段树」。

分类题单

如何科学刷题?

  1. 滑动窗口与双指针(定长/不定长/单序列/双序列/三指针/分组循环)
  2. 二分算法(二分答案/最小化最大值/最大化最小值/第K小)
  3. 单调栈(基础/矩形面积/贡献法/最小字典序)
  4. 网格图(DFS/BFS/综合应用)
  5. 位运算(基础/性质/拆位/试填/恒等式/思维)
  6. 图论算法(DFS/BFS/拓扑排序/最短路/最小生成树/二分图/基环树/欧拉路径)
  7. 动态规划(入门/背包/状态机/划分/区间/状压/数位/数据结构优化/树形/博弈/概率期望)
  8. 【本题相关】常用数据结构(前缀和/差分/栈/队列/堆/字典树/并查集/树状数组/线段树)
  9. 数学算法(数论/组合/概率期望/博弈/计算几何/随机算法)
  10. 贪心与思维(基本贪心策略/反悔/区间/字典序/数学/思维/脑筋急转弯/构造)
  11. 链表、二叉树与回溯(前后指针/快慢指针/DFS/BFS/直径/LCA/一般树)
  12. 字符串(KMP/Z函数/Manacher/字符串哈希/AC自动机/后缀数组/子序列自动机)

我的题解精选(已分类)

扫描线 & 线段树,详解矩形面积并

作者 tsreaper
2025年2月16日 14:01

解法:扫描线 & 线段树

我们先把所有正方形并集的面积求出来。这是非常经典的扫描线模板题:矩形面积并。

graph.png

我们以上图的三个矩形为例,讲述如何求它们面积的并。我们对每个矩形的上边界和下边界都画一条水平线,可以看出,相邻水平线之间,矩形的横截长度都是一样的。因此,相邻水平线之间矩形面积的并,就等于横截长度乘以水平线的高度差。问题转为如何求矩形的横截长度。

我们来看相邻的水平线之间,矩形的横截长度发生了什么变化。

  • 在 $y = 1$ 时,横截长度只是区间 $[1, 6]$ 的长度,即 $5$。所以 $y = 1$ 到 $y = 3$ 之间的面积是 $(3 - 1) \times 5 = 10$。
  • 到了 $y = 3$ 时,由于新矩形的加入,区间数量增加了一个,变成 $[1, 6] \cup [9, 14]$ 的长度,即 $10$。所以 $y = 3$ 到 $y = 4$ 之间的面积是 $(4 - 3) \times 10 = 10$。
  • 到了 $y = 4$ 时,由于新矩形的加入,区间数量又增加了一个,变成 $[1, 6] \cup [4, 11] \cup [9, 14] = [1, 14]$ 的长度,即 $13$。所以 $y = 4$ 到 $y = 7$ 之间的面积是 $(7 - 4) \times 13 = 39$。
  • 到了 $y = 7$ 时,由于一个矩形的退出,区间的数量减少了一个,变成 $[1, 6] \cup [9, 14]$ 的长度,即 $10$。所以 $y = 7$ 到 $y = 8$ 之间的面积是 $(8 - 7) \times 10 = 10$。
  • 到了 $y = 8$ 时,由于一个矩形的退出,区间的数量又减少了一个,变成 $[1, 6]$ 的长度,即 $5$。所以 $y = 8$ 到 $y = 10$ 之间的面积是 $(10 - 8) \times 5 = 10$。

所以矩形面积并为 $10 + 10 + 39 + 10 + 10 = 79$。

从上面的例子可以看出,横截长度的变化,其实就是维护一个区间的集合。每次我要加入或删除一个区间,然后求区间并集的长度。

$n$ 个区间的端点只有 $2n$ 种,比如上面的例子中,$3$ 个区间的端点只有 $x = 1, 4, 6, 9, 11, 14$ 这 $6$ 种。因此我们可以把相邻端点看成一个小区间,这样任何的区间就只会由若干个不相交(不计端点相交,因为对长度不影响)的小区间构成。比如上面的例子中,我们有 $5$ 个小区间 $a_1 = [1, 4], a_2 = [4, 6], a_3 = [6, 9], a_4 = [9, 11], a_5 = [11, 14]$。那么 $[1, 6] = a_1 \cup a_2$,$[4, 11] = a_2 \cup a_3 \cup a_4$,$[9, 14] = a_4 \cup a_5$。

因为我们只对区间的长度有兴趣,所以我们直接把每个区间转为长度,例如 $a_1 = 3, a_2 = 2, \cdots$。这样我们就成功把区间问题转为了序列问题:给定 $(2n - 1)$ 个数 $a_1, a_2, \cdots, a_{2n - 1}$,其中第 $i$ 个数被覆盖了 $b_i$ 次,一开始 $b_i = 0$。每次操作会选择一个区间 $[l, r]$,让 $l \le i \le r$ 的 $b_i$ 都增加或减少 $1$。每次操作后,求 $\sum\limits_{b_i \ne 0} a_i$。

区间修改 + 区间查询,大家可能会很快想到用线段树来维护。可是对于一个节点,满足 $b_i \ne 0$ 的 $a_i$ 之和很难维护。比如一个节点代表了下标 $[1, 4]$,目前有 $b_1 = b_2 = b_4 = 1$,$b_3 = 2$。如果我对该节点进行了 $-1$ 操作,我怎么知道只有 $b_1$,$b_2$ 和 $b_4$ 变成了 $0$,而 $b_3$ 还不是 $0$ 呢?

我们发现,直接维护的困难在于:一次操作对于同一节点内的每个元素可能有不同的影响。所以我们要换一种维护方式,只维护能将所有元素一视同仁的值。

对于线段树的一个节点,我们维护:$b_i$ 的最小值 $x$,以及满足 $b_i = x$ 的 $a_i$ 之和 $s$。为什么这样维护可以呢?因为对节点进行 $+v$ 操作,只会让 $x$ 增加 $v$,而不影响 $s$ 的值。那 $\sum\limits_{b_i \ne 0} a_i$ 怎么求呢?设节点对应的元素总和为 $t$,那么当 $b_i = 0$ 时,答案就是 $t - s$,否则答案就是 $t$。

因为操作涉及到了区间修改 + 区间查询,我们可以用懒标记下推线段树完成操作的维护。当然,这种线段树的维护方式不是最高效的,事实上还可以利用每次添加区间的操作一定会被撤回的性质,写出一种不需要懒标记下推,非常特殊的线段树,见 leetcode 850. 矩形面积 II 的官方题解。这里只介绍懒标记下推线段树的维护方法,是因为懒标记下推是最为常用,最为“标准”的线段树,比较好理解。

这样我们就成功维护了横截长度。最后的问题是找到最下面的水平线,使得水平线下方的面积并减去水平线上方的面积并大等于 $0$。

假设我们枚举到水平线 $y = t_1$ 和 $y = t_2$ 之间时,横截长度为 $l$,且到了 $y = t_2$ 之后,这个差值 $d \ge 0$。此时如果水平线往下移 $\Delta t$,这个差值会减少 $2l\Delta t$,因此答案就是 $t - \frac{d}{2l}$。

复杂度 $\mathcal{O}(n\log n)$。

参考代码(c++)

class Solution {
public:
    double separateSquares(vector<vector<int>>& squares) {
        int n = squares.size(), m = 0;
        map<int, int> mp;
        for (auto &sq : squares) mp[sq[0]] = mp[sq[0] + sq[2]] = 1;
        for (auto &p : mp) p.second = m++;
        int A[m];
        for (auto &p : mp) A[p.second] = p.first;

        struct Node {
            // mn:当前节点的最小覆盖数
            // len:满足覆盖数 = 最小覆盖数的 A[i] 之和
            // lazy:加法的懒标记
            int mn, len, lazy;

            // 对节点的覆盖数整个增加 qv,只影响 mn,不影响 len
            void add(int qv) {
                mn += qv;
                lazy += qv;
            }
        } tree[m * 4 + 5];

        // 线段树两个子节点合并
        auto merge = [&](Node &nl, Node &nr) {
            int mn = min(nl.mn, nr.mn);
            return Node {
                mn,
                (nl.mn == mn ? nl.len : 0) + (nr.mn == mn ? nr.len : 0),
                0
            };
        };

        // 线段树模板开始

        // 建树
        auto build = [&](this auto &&self, int id, int l, int r) -> void {
            if (l == r) tree[id] = { 0, A[r] - A[r - 1], 0 };
            else {
                int nxt = id << 1, mid = (l + r) >> 1;
                self(nxt, l, mid); self(nxt | 1, mid + 1, r);
                tree[id] = merge(tree[nxt], tree[nxt | 1]);
            }
        };

        // 懒标记下推
        auto down = [&](int id) {
            if (tree[id].lazy == 0) return;
            int nxt = id << 1;
            tree[nxt].add(tree[id].lazy);
            tree[nxt | 1].add(tree[id].lazy);
            tree[id].lazy = 0;
        };

        // 区间加减覆盖次数
        auto modify = [&](this auto &&self, int id, int l, int r, int ql, int qr, int qv) -> void {
            if (ql <= l && r <= qr) tree[id].add(qv);
            else {
                down(id);
                int nxt = id << 1, mid = (l + r) >> 1;
                if (ql <= mid) self(nxt, l, mid, ql, qr, qv);
                if (qr > mid) self(nxt | 1, mid + 1, r, ql, qr, qv);
                tree[id] = merge(tree[nxt], tree[nxt | 1]);
            }
        };

        // 线段树模板结束

        // 把正方形的上下边界取出来
        vector<array<int, 4>> vec;
        for (auto &sq : squares) {
            vec.push_back({sq[1], mp[sq[0]] + 1, mp[sq[0] + sq[2]], 1});
            vec.push_back({sq[1] + sq[2], mp[sq[0]] + 1, mp[sq[0] + sq[2]], -1});
        }
        sort(vec.begin(), vec.end());

        // 求总的面积并
        long long tot = 0;
        build(1, 1, m - 1);
        for (int i = 0; i + 1 < vec.size(); i++) {
            // 考虑水平线 y = vec[i][0] 和 y = vec[i + 1][0] 之间的情况
            modify(1, 1, m - 1, vec[i][1], vec[i][2], vec[i][3]);
            // 求横截长度
            int len = A[m - 1] - A[0];
            // 如果最小覆盖数是 0,那么扣掉相应的长度
            if (tree[1].mn == 0) len -= tree[1].len;
            // 面积 = 横截长度 * 高度差
            tot += 1LL * len * (vec[i + 1][0] - vec[i][0]);
        }

        long long now = 0;
        build(1, 1, m - 1);
        for (int i = 0; i + 1 < vec.size(); i++) {
            modify(1, 1, m - 1, vec[i][1], vec[i][2], vec[i][3]);
            int len = A[m - 1] - A[0];
            if (tree[1].mn == 0) len -= tree[1].len;
            now += 1LL * len * (vec[i + 1][0] - vec[i][0]);
            // delta 非负了,套公式
            long long det = now - (tot - now);
            if (det >= 0) return vec[i + 1][0] - 0.5 * det / len;
        };
        return -1;
    }
};

两次扫描线解法(跑两次850题)

作者 vclip
2025年2月16日 13:08

这里属于会 850. 矩形面积 II 题就能秒。

如果不动 850 题的代码,直接调用 850 题的话,可以采用二分 $y$ 坐标,然后截断正方形,去除在当前 $y$ 值以上的部分,再用 850 题的方法求面积判断是否达到总面积一半解决。

但实际上也可以两次扫描线解决:先用 850 题的方法求出总面积 $S$,然后用垂直 $y$ 轴的扫描线扫,扫到在 $y_1$ 以下的正方形面积 $S_1$ 小于一半,在 $y_2$ 以下的正方形面积 $S_2$ 大于等于一半时,答案即为 $y_1+\dfrac{S/2-S_1}{w}$,其中 $w$ 为在 $y_1$ 和 $y_2$ 之间的正方形截面长度(即 850 题中线段树维护的长度)。

代码

###C++

class Solution {
public:
    struct Edge {
        int y;
        bool state : 1;
        int x0 : 31, x1;
        auto operator<=>(Edge other) const {
            return y <=> other.y;
        }
    };

    struct Node {
        int cnt, len, sum;
    };

    double separateSquares(const vector<vector<int>>& squares) {
        const int n = squares.size();
        vector<int> ord;
        vector<Edge> edges;
        for (const auto& e : squares) {
            ord.push_back(e[0]);
            ord.push_back(e[0] + e[2]);
            edges.push_back({e[1], true, e[0], e[0] + e[2]});
            edges.push_back({e[1] + e[2], false, e[0], e[0] + e[2]});
        }
        ranges::sort(ord);
        ord.erase(unique(ord.begin(), ord.end()), ord.end());
        const int c = ord.size() - 1;
        for (auto& e : edges) {
            e.x0 = ranges::lower_bound(ord, e.x0) - ord.begin();
            e.x1 = ranges::lower_bound(ord, e.x1) - ord.begin();
        }
        sort(edges.begin(), edges.end());
        const int h = c > 1 ? 32 - __builtin_clz(c - 1) : 0;
        const int m = 1 << h, M = m + c;
        vector<Node> tree(M);
        for (int i = 0;i < c;++i)
            tree[m + i] = {0, ord[i + 1] - ord[i], 0};
        for (int i = 1;i <= h;++i)
            for (int j = 0;j < (c >> i);++j)
                tree[(m >> i) + j] = {0, ord[(j + 1) << i] - ord[j << i], 0};
        const auto update_leaf = [&] (int p, int d) {
            tree[p].sum = (tree[p].cnt += d) > 0 ? tree[p].len : 0;
        };
        const auto update_branch = [&] (int p, int d) {
            tree[p].sum = (tree[p].cnt += d) > 0 ? tree[p].len : tree[2 * p].sum + tree[2 * p + 1].sum;
        };
        const auto update = [&] (int l, int r, int d) {
            const int L = l += m, R = r += m;
            if (l & 1) update_leaf(l++, d);
            if (r & 1) update_leaf(--r, d);
            for (l >>= 1, r >>= 1;l < r;l >>= 1, r >>= 1) {
                if (l & 1) update_branch(l++, d);
                if (r & 1) update_branch(--r, d);
            }
            for (int i = L >> 1, j = M >> 1;i < j;i >>= 1, j >>= 1)
                update_branch(i, 0);
            for (int i = R >> 1, j = M >> 1;i < j;i >>= 1, j >>= 1)
                update_branch(i, 0);
        };
        const auto query = [&] {
            int ans = 0;
            for (int i = m, j = M;i < j;i >>= 1, j >>= 1) {
                if (i & 1) ans += tree[i++].sum;
                if (j & 1) ans += tree[--j].sum;
            }
            return ans;
        };
        long long total = 0;
        {
            int pre = 0;
            for (const auto [x, state, y0, y1] : edges) {
                total += 1ll * query() * (x - pre);
                update(y0, y1, 2 * state - 1);
                pre = x;
            }
        }
        int pre = 0;
        long long sum = 0;
        for (const auto [x, state, y0, y1] : edges) {
            const int w = query();
            const long long nsum = sum + 1ll * w * (x - pre);
            if (2 * nsum >= total) return pre + (total - 2 * sum) / (2.0 * w);
            update(y0, y1, 2 * state - 1);
            sum = nsum;
            pre = x;
        }
        return -1;
    }
};
昨天以前LeetCode 每日一题题解

分割正方形 I

2026年1月8日 12:04

方法一:二分查找

思路与算法

设数组 $\textit{squares}$ 的长度为 $n$,数组中的每个元素为 $(x_i,y_i,l_i)$,此时所有正方形的面积之和则为:

$$
\textit{totalArea} = \sum_{i=0}^{n-1} l_i^2
$$

根据题目要求,我们需要找到一个分割线 $y$,使得所有在 $y$ 以上的正方形的面积之和等于所有在 $y$ 以下的正方形的面积之和。设 $y$ 以下的正方形面积为 $\textit{area}_y$,此时需要满足:

$$
\textit{area}_y \cdot 2= \textit{totalArea}
$$

随着分割线 $y$ 的增大,$\textit{area}_y$ 单调不减,因此可以使用二分查找。具体地,我们可以二分查找找到最小的 $y$ 的值,使得在 $y$ 以下的正方形的面积满足:

$$
\textit{area}_y \cdot 2 \ge \textit{totalArea}
$$

假设给定的 $y$ 的值,如果给定正方形 $(x_i,y_i,l_i)$,满足 $y_i < y$,那么这个正方形在 $y$ 以下,否则在 $y$ 以上。此时该正方形在 $y$ 以下的面积则为:

$$
\textit{area} = l_i \cdot \min(y - y_i, l_i)
$$

我们可以根据这个性质来计算在 $y$ 以下的所有正方形的面积之和:

$$
\textit{area}y = \sum{i=0}^{n-1} l_i \cdot \max(0,\min(y - y_i, l_i))
$$

由于计算面积时存在精度问题,题目要求与实际答案的误差在 $10^{-5}$ 以内。我们需要在二分查找时使用 $10^{-5}$ 作为精度,即可以使用上限与下限的差距不超过 $\text{10}^{-5}$ 作为二分查找的终止条件:

$$
\textit{hi} - \textit{lo} \le 10^{-5}
$$

我们通过二分查找找到最小的 $y$ 值即为答案。

细节

我们可以分析二分查找的次数上限,设初始二分区间长度为 $L$,每二分一次,二分区间长度减半。要至少减半到 $10^{-5}$ 才能满足题目的误差要求。设循环次数为 $k$,我们有:

$$
\dfrac{L}{2^k} \le 10^{-5}
$$

解得:

$$
k \ge \log_2 (L \cdot 10^5)
$$

在本题的数据范围下,$0 \le L \le 10^9$,此时 $k \ge \log_2 (L \cdot 10^5) \ge \log_2 (10^{14}) = 14 \log_2 (10) \approx 46.506993328423076$。二分查找的次数上限为 $47$ 次。

代码

###C++

class Solution {
public:
    double separateSquares(vector<vector<int>>& squares) {
        double max_y = 0, total_area = 0;
        for (auto& sq : squares) {
            int y = sq[1], l = sq[2];
            total_area += (double)l * l;
            max_y = max(max_y, (double)(y + l));
        }
        
        auto check = [&](double limit_y) -> bool {
            double area = 0;
            for (auto& sq : squares) {
                int y = sq[1], l = sq[2];
                if (y < limit_y) {
                    area += l * min(limit_y - y, (double)l);
                }
            }
            return area >= total_area / 2;
        };
        
        double lo = 0, hi = max_y;
        double eps = 1e-5;
        while (abs(hi - lo) > eps) {
            double mid = (hi + lo) / 2;
            if (check(mid)) {
                hi = mid;
            } else {
                lo = mid;
            }
        }
        return hi;
    }
};

###Java

class Solution {
    public double separateSquares(int[][] squares) {
        double max_y = 0, total_area = 0;
        for (int[] sq : squares) {
            int y = sq[1], l = sq[2];
            total_area += (double)l * l;
            max_y = Math.max(max_y, (double)(y + l));
        }
        
        double lo = 0, hi = max_y;
        double eps = 1e-5;
        while (Math.abs(hi - lo) > eps) {
            double mid = (hi + lo) / 2;
            if (check(mid, squares, total_area)) {
                hi = mid;
            } else {
                lo = mid;
            }
        }

        return hi;
    }

    private Boolean check(double limit_y, int[][] squares, double total_area) {
        double area = 0;
        for (int[] sq : squares) {
            int y = sq[1], l = sq[2];
            if (y < limit_y) {
                area += (double)l * Math.min(limit_y - y, (double)l);
            }
        }
        return area >= total_area / 2;
    }
}

###C#

public class Solution {
    public double SeparateSquares(int[][] squares) {
        double max_y = 0, total_area = 0;
        foreach (int[] sq in squares) {
            int y = sq[1], l = sq[2];
            total_area += (double)l * l;
            max_y = Math.Max(max_y, (double)(y + l));
        }
        
        double lo = 0, hi = max_y;
        double eps = 1e-5;
        while (Math.Abs(hi - lo) > eps) {
            double mid = (hi + lo) / 2;
            if (Check(mid, squares, total_area)) {
                hi = mid;
            } else {
                lo = mid;
            }
        }

        return hi;
    }

    private bool Check(double limit_y, int[][] squares, double total_area) {
        double area = 0;
        foreach (int[] sq in squares) {
            int y = sq[1], l = sq[2];
            if (y < limit_y) {
                area += (double)l * Math.Min(limit_y - y, (double)l);
            }
        }
        return area >= total_area / 2;
    }
}

###Go

func separateSquares(squares [][]int) float64 {
    max_y, total_area := 0.0, 0.0
    for _, sq := range squares {
        y, l := sq[1], sq[2]
        total_area += float64(l * l)
        if float64(y + l) > max_y {
            max_y = float64(y + l)
        }
    }
    
    check := func(limit_y float64) bool {
        area := 0.0
        for _, sq := range squares {
            y, l := sq[1], sq[2]
            if float64(y) < limit_y {
                overlap := math.Min(limit_y-float64(y), float64(l))
                area += float64(l) * overlap
            }
        }
        
        return area >= total_area / 2.0
    }
    
    lo, hi := 0.0, max_y
    eps := 1e-5
    for math.Abs(hi-lo) > eps {
        mid := (hi + lo) / 2.0
        if check(mid) {
            hi = mid
        } else {
            lo = mid
        }
    }
    return hi
}

###Python

class Solution:
    def separateSquares(self, squares: List[List[int]]) -> float:
        max_y, total_area = 0, 0
        for x, y, l in squares:
            total_area += l ** 2
            max_y = max(max_y, y + l)
        
        def check(limit_y):
            area = 0
            for x, y, l in squares:
                if y < limit_y:
                    area += l * min(limit_y - y, l)
            return area >= total_area / 2
        
        lo, hi = 0, max_y
        eps = 1e-5
        while abs(hi - lo) > eps:
            mid = (hi + lo) / 2
            if check(mid):
                hi = mid
            else:
                lo = mid

        return hi

###C

bool check(double limit_y, int** squares, int squaresSize, double total_area) {
    double area = 0.0;

    for (int i = 0; i < squaresSize; i++) {
        int y = squares[i][1];
        int l = squares[i][2];
        if (y < limit_y) {
            area += (double)l * fmin(l, limit_y - y);
        }
    }

    return area >= total_area / 2.0;
}

double separateSquares(int** squares, int squaresSize, int* squaresColSize) {
    double max_y = 0.0, total_area = 0.0;
    for (int i = 0; i < squaresSize; i++) {
        int y = squares[i][1];
        int l = squares[i][2];
        total_area += (double)l * l;
        if (y + l > max_y) {
            max_y = y + l;
        }
    }
    
    double lo = 0.0, hi = max_y;
    double eps = 1e-5;
    while (fabs(hi - lo) > eps) {
        double mid = (hi + lo) / 2.0;
        if (check(mid, squares, squaresSize, total_area)) {
            hi = mid;
        } else {
            lo = mid;
        }
    }
    return hi;
}

###JavaScript

var separateSquares = function(squares) {
    let max_y = 0, total_area = 0;
    for (const [x, y, l] of squares) {
        total_area += l * l;
        max_y = Math.max(max_y, y + l);
    }
    
    const check = (limit_y) => {
        let area = 0;
        for (const [x, y, l] of squares) {
            if (y < limit_y) {
                area += l * Math.min(limit_y - y, l);
            }
        }
        return area >= total_area / 2;
    };
    
    let lo = 0, hi = max_y;
    const eps = 1e-5;
    while (Math.abs(hi - lo) > eps) {
        const mid = (hi + lo) / 2;
        if (check(mid)) {
            hi = mid;
        } else {
            lo = mid;
        }
    }
    return hi;
};

###TypeScript

function separateSquares(squares: number[][]): number {
    let max_y = 0, total_area = 0;
    for (const [x, y, l] of squares) {
        total_area += l * l;
        max_y = Math.max(max_y, y + l);
    }
    
    const check = (limit_y: number): boolean => {
        let area = 0;
        for (const [x, y, l] of squares) {
            if (y < limit_y) {
                area += l * Math.min(limit_y - y, l);
            }
        }
        return area >= total_area / 2;
    };
    
    let lo = 0, hi = max_y;
    const eps = 1e-5;
    while (Math.abs(hi - lo) > eps) {
        const mid = (hi + lo) / 2;
        if (check(mid)) {
            hi = mid;
        } else {
            lo = mid;
        }
    }
    return hi;
}

###Rust

impl Solution {
    pub fn separate_squares(squares: Vec<Vec<i32>>) -> f64 {
        let mut max_y: f64 = 0.0;
        let mut total_area: f64 = 0.0;
        
        for sq in &squares {
            let l = sq[2] as f64;
            total_area += l * l;
            max_y = max_y.max((sq[1] + sq[2]) as f64);
        }
        
        let mut lo = 0.0;
        let mut hi = max_y;
        let eps = 1e-5;
        while (hi - lo).abs() > eps {
            let mid = (hi + lo) / 2.0;
            if Self::check(mid, &squares, total_area) {
                hi = mid;
            } else {
                lo = mid;
            }
        }
        
        hi
    }
    
    fn check(limit_y: f64, squares: &Vec<Vec<i32>>, total_area: f64) -> bool {
        let mut area = 0.0;
        
        for sq in squares {
            let y = sq[1] as f64;
            let l = sq[2] as f64;
            if y < limit_y {
                let overlap = (limit_y - y).min(l);
                area += l * overlap;
            }
        }
        
        area >= total_area / 2.0
    }
}

复杂度分析

  • 时间复杂度:$O(n \log (LU))$,其中 $n$ 是数组 $\textit{squares}$ 的长度,设数组中的每个元素为 $(x_i,y_i,l_i)$,此时 $U = \max(y_i + l_i)$,$L = 10^5$。二分查找每次校验的时间复杂度度为 $O(n)$,二分查找的次数为 $O(\log (LU))$,因此总时间复杂度为 $O(n \log (LU))$。

  • 空间复杂度:$O(1)$。

方法二:扫描线

思路与算法

我们可以参考「扫描线」的解法。首先可以计算出所有正方形的总面积 $\textit{totalArea}$,接着我们从下往上进行扫描,设扫描线 $y = y^{'}$ 下方的覆盖的面积和为 $\textit{area}$,那么扫描线上方的面积和为 $\textit{totalArea}−\textit{area}$。

题目要求 $y = y^{'}$ 下面的面积与上方的面积相等,即:

$$
\textit{area} = \textit{totalArea}− \textit{area}
$$

即:

$$
\textit{area} = \dfrac{\textit{totalArea}}{2}
$$

设当前经过正方形上/下边界的扫描线为 $y = y^{'}$,此时扫面线以下的覆盖面积为 $\textit{area}$;向上移动时下一个需要经过的正方形上/下边界的扫描线为 $y = y^{''}$,此时被正方形覆盖的底边长之和为 $\textit{width}$,则此时在扫面线 $y = y^{''}$ 以下覆盖的面积之和为:

$$
\textit{area} + \textit{width} \cdot (y^{''} - y^{'})
$$

此时当满足:

$$
\textit{area} < \dfrac{\textit{totalArea}}{2} \
\textit{area} + \textit{width} \cdot (y^{''} - y^{'}) \ge \dfrac{\textit{totalArea}}{2}
$$

时,则可以知道目标值 $y$ 一定处于区间 $[y^{'},y^{''}]$。
由于两个扫面线之间的被覆盖区域中所有的矩形的高度相同,扫面线在区间 $[y^{'},y^{''}]$ 移动长度为 $\Delta$ 时,此时被覆盖区域的面积变化即为 $\Delta \cdot \textit{width}$,此时被覆盖的面积只需增加 $\dfrac{\textit{totalArea}}{2} - \textit{area}$,即可满足上下面积相等,此时我们可以直接求出目标值 $y$ 即为:

$$
y = y^{'} + \dfrac{\dfrac{\textit{totalArea}}{2} - \textit{area}}{\textit{width}} = y^{'} + \dfrac{\textit{totalArea} - 2\cdot \textit{area}}{2\cdot\textit{width}}
$$

我们依次遍历每个正方形上/下边界的扫面线,找到目标值返回即可。

代码

###C++

class Solution {
public:
    double separateSquares(vector<vector<int>>& squares) {
        long long total_area = 0;
        vector<tuple<int, int, int>> events; 
        for (const auto& sq : squares) {
            int y = sq[1], l = sq[2];
            total_area += (long long)l * l;
            events.emplace_back(y, l, 1);
            events.emplace_back(y + l, l, -1);
        }
        // 按照 y 坐标进行排序
        sort(events.begin(), events.end(), [](const auto& a, const auto& b) {
            return get<0>(a) < get<0>(b);
        });
        
        double covered_width = 0;  // 当前扫描线下所有底边之和
        double curr_area = 0;       // 当前累计面积
        double prev_height = 0;     // 前一个扫描线的高度
        for (const auto &[y, l, delta] : events) {
            int diff = y - prev_height;
            // 两条扫面线之间新增的面积
            double area = covered_width * diff;
            // 如果加上这部分面积超过总面积的一半
            if (2LL * (curr_area + area) >= total_area) {
                return prev_height + (total_area - 2.0 * curr_area) / (2.0 * covered_width);
            }
            // 更新宽度:开始事件加宽度,结束事件减宽度
            covered_width += delta * l;
            curr_area += area;
            prev_height = y;
        }
        
        return 0.0;
    }
};

###Java

class Solution {
    public double separateSquares(int[][] squares) {
        long totalArea = 0;
        List<int[]> events = new ArrayList<>();
        
        for (int[] sq : squares) {
            int y = sq[1], l = sq[2];
            totalArea += (long) l * l;
            events.add(new int[]{y, l, 1});
            events.add(new int[]{y + l, l, -1});
        }
        
        // 按y坐标排序
        events.sort((a, b) -> Integer.compare(a[0], b[0]));
        double coveredWidth = 0;  // 当前扫描线下所有底边之和
        double currArea = 0;      // 当前累计面积
        double prevHeight = 0;    // 前一个扫描线的高度
        
        for (int[] event : events) {
            int y = event[0];
            int l = event[1];
            int delta = event[2];
            
            int diff = y - (int) prevHeight;
            // 两条扫描线之间新增的面积
            double area = coveredWidth * diff;
            // 如果加上这部分面积超过总面积的一半
            if (2L * (currArea + area) >= totalArea) {
                return prevHeight + (totalArea - 2.0 * currArea) / (2.0 * coveredWidth);
            }
            // 更新宽度:开始事件加宽度,结束事件减宽度
            coveredWidth += delta * l;
            currArea += area;
            prevHeight = y;
        }
        
        return 0.0;
    }
}

###C#

public class Solution {
    public double SeparateSquares(int[][] squares) {
        long totalArea = 0;
        List<int[]> events = new List<int[]>();
        
        foreach (var sq in squares) {
            int y = sq[1], l = sq[2];
            totalArea += (long)l * l;
            events.Add(new int[] { y, l, 1 });
            events.Add(new int[] { y + l, l, -1 });
        }
        
        // 按y坐标排序
        events.Sort((a, b) => a[0].CompareTo(b[0]));
        
        double coveredWidth = 0;  // 当前扫描线下所有底边之和
        double currArea = 0;      // 当前累计面积
        double prevHeight = 0;    // 前一个扫描线的高度
        
        foreach (var eventItem in events) {
            int y = eventItem[0];
            int l = eventItem[1];
            int delta = eventItem[2];
            
            int diff = y - (int)prevHeight;
            // 两条扫描线之间新增的面积
            double area = coveredWidth * diff;
            // 如果加上这部分面积超过总面积的一半
            if (2L * (currArea + area) >= totalArea) {
                return prevHeight + (totalArea - 2.0 * currArea) / (2.0 * coveredWidth);
            }
            // 更新宽度:开始事件加宽度,结束事件减宽度
            coveredWidth += delta * l;
            currArea += area;
            prevHeight = y;
        }
        
        return 0.0;
    }
}

###Go

func separateSquares(squares [][]int) float64 {
    var totalArea int64 = 0
    type Event struct {
        y     int
        l     int
        delta int
    }
    events := make([]Event, 0, len(squares)*2)
    
    for _, sq := range squares {
        y, l := sq[1], sq[2]
        totalArea += int64(l) * int64(l)
        events = append(events, Event{y, l, 1})
        events = append(events, Event{y + l, l, -1})
    }
    
    // 按y坐标排序
    sort.Slice(events, func(i, j int) bool {
        return events[i].y < events[j].y
    })
    
    coveredWidth := 0.0  // 当前扫描线下所有底边之和
    currArea := 0.0      // 当前累计面积
    prevHeight := 0.0    // 前一个扫描线的高度
    
    for _, event := range events {
        y, l, delta := event.y, event.l, event.delta
        diff := float64(y) - prevHeight
        // 两条扫描线之间新增的面积
        area := coveredWidth * diff
        // 如果加上这部分面积超过总面积的一半
        if 2.0*(currArea+area) >= float64(totalArea) {
            return prevHeight + (float64(totalArea) - 2.0*currArea) / (2.0 * coveredWidth)
        }
        // 更新宽度:开始事件加宽度,结束事件减宽度
        coveredWidth += float64(delta * l)
        currArea += area
        prevHeight = float64(y)
    }
    
    return 0.0
}

###Python

class Solution:
    def separateSquares(self, squares: List[List[int]]) -> float:
        total_area = 0
        events = []
        
        for sq in squares:
            y, l = sq[1], sq[2]
            total_area += l * l
            events.append((y, l, 1))
            events.append((y + l, l, -1))
        
        # 按y坐标排序
        events.sort(key=lambda x: x[0])
        
        covered_width = 0.0  # 当前扫描线下所有底边之和
        curr_area = 0.0      # 当前累计面积
        prev_height = 0.0    # 前一个扫描线的高度
        
        for y, l, delta in events:
            diff = y - prev_height
            # 两条扫描线之间新增的面积
            area = covered_width * diff
            # 如果加上这部分面积超过总面积的一半
            if 2 * (curr_area + area) >= total_area:
                return prev_height + (total_area - 2 * curr_area) / (2 * covered_width)
            # 更新宽度:开始事件加宽度,结束事件减宽度
            covered_width += delta * l
            curr_area += area
            prev_height = y
        
        return 0.0

###C

typedef struct {
    int y;
    int l;
    int delta;
} Event;

int compareEvents(const void* a, const void* b) {
    Event* e1 = (Event*)a;
    Event* e2 = (Event*)b;
    return e1->y - e2->y;
}

double separateSquares(int** squares, int squaresSize, int* squaresColSize) {
    long long totalArea = 0;
    Event* events = malloc(2 * squaresSize * sizeof(Event));
    int eventCount = 0;
    
    for (int i = 0; i < squaresSize; i++) {
        int y = squares[i][1];
        int l = squares[i][2];
        totalArea += (long long)l * l;
        events[eventCount++] = (Event){y, l, 1};
        events[eventCount++] = (Event){y + l, l, -1};
    }
    
    // 按y坐标排序
    qsort(events, eventCount, sizeof(Event), compareEvents);
    
    double coveredWidth = 0.0;  // 当前扫描线下所有底边之和
    double currArea = 0.0;      // 当前累计面积
    double prevHeight = 0.0;    // 前一个扫描线的高度
    
    for (int i = 0; i < eventCount; i++) {
        int y = events[i].y;
        int l = events[i].l;
        int delta = events[i].delta;
        
        int diff = y - (int)prevHeight;
        // 两条扫描线之间新增的面积
        double area = coveredWidth * diff;
        // 如果加上这部分面积超过总面积的一半
        if (2LL * (currArea + area) >= totalArea) {
            double result = prevHeight + (totalArea - 2.0 * currArea) / (2.0 * coveredWidth);
            free(events);
            return result;
        }
        // 更新宽度:开始事件加宽度,结束事件减宽度
        coveredWidth += delta * l;
        currArea += area;
        prevHeight = y;
    }
    
    free(events);
    return 0.0;
}

###JavaScript

var separateSquares = function(squares) {
    let totalArea = 0n;
    const events = [];
    
    for (const sq of squares) {
        const y = sq[1], l = sq[2];
        totalArea += BigInt(l) * BigInt(l);
        events.push([y, l, 1]);
        events.push([y + l, l, -1]);
    }
    
    // 按y坐标排序
    events.sort((a, b) => a[0] - b[0]);
    
    let coveredWidth = 0;  // 当前扫描线下所有底边之和
    let currArea = 0;      // 当前累计面积
    let prevHeight = 0;    // 前一个扫描线的高度
    
    for (const [y, l, delta] of events) {
        const diff = y - prevHeight;
        // 两条扫描线之间新增的面积
        const area = coveredWidth * diff;
        // 如果加上这部分面积超过总面积的一半
        if (2n * BigInt(Math.ceil(currArea + area)) >= totalArea) {
            return prevHeight + (Number(totalArea) - 2.0 * currArea) / (2.0 * coveredWidth);
        }
        // 更新宽度:开始事件加宽度,结束事件减宽度
        coveredWidth += delta * l;
        currArea += area;
        prevHeight = y;
    }
    
    return 0.0;
};

###TypeScript

function separateSquares(squares: number[][]): number {
    let totalArea: bigint = 0n;
    const events: [number, number, number][] = [];
    
    for (const sq of squares) {
        const y = sq[1], l = sq[2];
        totalArea += BigInt(l) * BigInt(l);
        events.push([y, l, 1]);
        events.push([y + l, l, -1]);
    }
    
    // 按y坐标排序
    events.sort((a, b) => a[0] - b[0]);
    
    let coveredWidth: number = 0;  // 当前扫描线下所有底边之和
    let currArea: number = 0;      // 当前累计面积
    let prevHeight: number = 0;    // 前一个扫描线的高度
    
    for (const [y, l, delta] of events) {
        const diff: number = y - prevHeight;
        // 两条扫描线之间新增的面积
        const area: number = coveredWidth * diff;
        // 如果加上这部分面积超过总面积的一半
        if (2n * BigInt(Math.ceil(currArea + area)) >= totalArea) {
            return prevHeight + (Number(totalArea) - 2.0 * currArea) / (2.0 * coveredWidth);
        }
        // 更新宽度:开始事件加宽度,结束事件减宽度
        coveredWidth += delta * l;
        currArea += area;
        prevHeight = y;
    }
    
    return 0.0;
}

###Rust

impl Solution {
    pub fn separate_squares(squares: Vec<Vec<i32>>) -> f64 {
        let mut total_area: i64 = 0;
        let mut events: Vec<(i32, i32, i32)> = Vec::new();
        
        for sq in &squares {
            let y = sq[1];
            let l = sq[2];
            total_area += l as i64 * l as i64;
            events.push((y, l, 1));
            events.push((y + l, l, -1));
        }
        
        // 按y坐标排序
        events.sort_by_key(|&(y, _, _)| y);
        
        let mut covered_width: f64 = 0.0;  // 当前扫描线下所有底边之和
        let mut curr_area: f64 = 0.0;      // 当前累计面积
        let mut prev_height: f64 = 0.0;    // 前一个扫描线的高度
        
        for (y, l, delta) in events {
            let diff = y as f64 - prev_height;
            // 两条扫描线之间新增的面积
            let area = covered_width * diff;
            // 如果加上这部分面积超过总面积的一半
            if 2.0 * (curr_area + area) >= total_area as f64 {
                return prev_height + (total_area as f64 - 2.0 * curr_area) / (2.0 * covered_width);
            }
            // 更新宽度:开始事件加宽度,结束事件减宽度
            covered_width += (delta * l) as f64;
            curr_area += area;
            prev_height = y as f64;
        }
        
        0.0
    }
}

复杂度分析

  • 时间复杂度:$O(n \log n)$,其中 $n$ 是数组 $\textit{squares}$ 的长度。排序需要的时间复杂度为 $O(n \log n)$。

  • 空间复杂度:$O(n)$,其中 $n$ 是数组 $\textit{squares}$ 的长度。存储扫面线高度,需要的空间为 $O(n)$。

每日一题-分割正方形 I🟡

2026年1月13日 00:00

给你一个二维整数数组 squares ,其中 squares[i] = [xi, yi, li] 表示一个与 x 轴平行的正方形的左下角坐标和正方形的边长。

找到一个最小的 y 坐标,它对应一条水平线,该线需要满足它以上正方形的总面积 等于 该线以下正方形的总面积。

答案如果与实际答案的误差在 10-5 以内,将视为正确答案。

注意:正方形 可能会 重叠。重叠区域应该被 多次计数 

 

示例 1:

输入: squares = [[0,0,1],[2,2,1]]

输出: 1.00000

解释:

任何在 y = 1y = 2 之间的水平线都会有 1 平方单位的面积在其上方,1 平方单位的面积在其下方。最小的 y 坐标是 1。

示例 2:

输入: squares = [[0,0,2],[1,1,1]]

输出: 1.16667

解释:

面积如下:

  • 线下的面积:7/6 * 2 (红色) + 1/6 (蓝色) = 15/6 = 2.5
  • 线上的面积:5/6 * 2 (红色) + 5/6 (蓝色) = 15/6 = 2.5

由于线以上和线以下的面积相等,输出为 7/6 = 1.16667

 

提示:

  • 1 <= squares.length <= 5 * 104
  • squares[i] = [xi, yi, li]
  • squares[i].length == 3
  • 0 <= xi, yi <= 109
  • 1 <= li <= 109
  • 所有正方形的总面积不超过 1012

二分

作者 tsreaper
2025年2月16日 14:03

解法:二分

二分水平线的坐标,找到最小的坐标,使得水平线上面的面积和小等于水平线下面的面积和即可。

因为坐标最大值是 $2 \times 10^9$,而精度要求是 $10^{-5}$,我们的二分次数应该提供至少 $10^{-14}$ 的精度。直接二分 $100$ 次,$2^{-100}$ 远小于 $10^{-14}$。

复杂度 $\mathcal{O}(tn)$,其中 $t = 100$ 是二分次数。

参考代码(c++)

class Solution {
public:
    double separateSquares(vector<vector<int>>& squares) {
        int mx = 0;
        for (auto &sq : squares) mx = max(mx, sq[1] + sq[2]);

        auto check = [&](double lim) {
            // a:水平线下面的面积和
            // b:水平线上面的面积和
            long double a = 0, b = 0;
            for (auto &sq : squares) {
                // 正方形完全位于水平线下面
                if (sq[1] + sq[2] <= lim) a += 1LL * sq[2] * sq[2];
                // 正方形完全位于水平线上面
                else if (sq[1] >= lim) b += 1LL * sq[2] * sq[2];
                // 正方形被水平线分成两半
                else {
                    double t = (lim - sq[1]) * sq[2];
                    a += t;
                    b += 1LL * sq[2] * sq[2] - t;
                }
            }
            return a >= b;
        };

        // 二分 100 次
        double head = 0, tail = mx;
        for (int i = 0; i < 100; i++) {
            double mid = (head + tail) / 2;
            if (check(mid)) tail = mid;
            else head = mid;
        }
        return (head + tail) / 2;
    }
};

三种方法:浮点二分 / 整数二分 / 差分(Python/Java/C++/Go)

作者 endlesscheng
2025年2月16日 08:43

方法一:浮点二分

所有正方形的面积之和为

$$
\textit{totalArea} = \sum_{i=0}^{n-1} l_i^2
$$

设在水平线 $Y=y$ 下方的面积之和为 $\textit{area}_y$,那么水平线上方的面积之和为 $\textit{totalArea}-\textit{area}_y$。

题目要求

$$
\textit{area}_y = \textit{totalArea}-\textit{area}_y
$$

$$
\textit{area}_y\cdot 2 = \textit{totalArea}
$$

我们可以二分最小的 $y$,满足

$$
\textit{area}_y\cdot 2 \ge \textit{totalArea}
$$

$\textit{area}_y$ 怎么算?

枚举正方形 $(x_i,y_i,l_i)$,如果水平线在正方形底边上面,即 $y_i < y$,那么这个正方形在水平线下方的面积为

$$
l_i\cdot\min(y-y_i, l_i)
$$

否则在水平线下方的面积为 $0$。

总的来说就是

$$
l_i\cdot\min(\max(y-y_i,0), l_i)
$$

在水平线下方的总面积为

$$
\textit{area}y = \sum{i=0}^{n-1} l_i\cdot\min(\max(y-y_i,0), l_i)
$$

细节

二分的左边界为 $0$,右边界为 $\max(y_i+l_i)$。这里无需讨论开闭区间,因为我们算的是小数。

循环条件怎么写?

推荐的写法是固定一个循环次数,因为浮点数有舍入误差,可能算出的 $\textit{mid}$ 和 $\textit{left}$ 相等,此时 $\textit{left}=\textit{mid}$ 不会更新 $\textit{left}$,导致死循环。

:本题由于值域小,也可以在 $\textit{left}$ 和 $\textit{right}$ 相距小于 $10^{-5}$ 时结束循环。但这种做法无法用于值域较大的场景,所以不推荐。

循环多少次?

设初始二分区间长度为 $L$,每二分一次,二分区间长度减半。要至少减半到 $10^{-5}$ 才能满足题目的误差要求。设循环次数为 $k$,我们有

$$
\dfrac{L}{2^k} \le 10^{-5}
$$

解得

$$
k\ge \log_2 (L\cdot 10^5)
$$

在本题的数据范围下,可以取 $k=48$(或者 $47$,取决于代码实现方式)。

本题视频讲解,欢迎点赞关注~

###py

class Solution:
    def separateSquares(self, squares: List[List[int]]) -> float:
        M = 100_000
        total_area = sum(l * l for _, _, l in squares)

        def check(y: float) -> bool:
            area = 0
            for _, yi, l in squares:
                if yi < y:
                    area += l * min(y - yi, l)
            return area >= total_area / 2

        left = 0
        right = max_y = max(y + l for _, y, l in squares)
        for _ in range((max_y * M).bit_length()):
            mid = (left + right) / 2
            if check(mid):
                right = mid
            else:
                left = mid
        return (left + right) / 2  # 区间中点误差小

###java

class Solution {
    public double separateSquares(int[][] squares) {
        long totArea = 0;
        int maxY = 0;
        for (int[] sq : squares) {
            int l = sq[2];
            totArea += (long) l * l;
            maxY = Math.max(maxY, sq[1] + l);
        }

        double left = 0;
        double right = maxY;
        for (int i = 0; i < 47; i++) {
            double mid = (left + right) / 2;
            if (check(squares, mid, totArea)) {
                right = mid;
            } else {
                left = mid;
            }
        }
        return (left + right) / 2; // 区间中点误差小
    }

    private boolean check(int[][] squares, double y, long totArea) {
        double area = 0;
        for (int[] sq : squares) {
            double yi = sq[1];
            if (yi < y) {
                double l = sq[2];
                area += l * Math.min(y - yi, l);
            }
        }
        return area >= totArea / 2.0;
    }
}

###cpp

class Solution {
public:
    double separateSquares(vector<vector<int>>& squares) {
        long long tot_area = 0;
        int max_y = 0;
        for (auto& sq : squares) {
            int l = sq[2];
            tot_area += 1LL * l * l;
            max_y = max(max_y, sq[1] + l);
        }

        auto check = [&](double y) -> bool {
            double area = 0;
            for (auto& sq : squares) {
                double yi = sq[1];
                if (yi < y) {
                    double l = sq[2];
                    area += l * min(y - yi, l);
                }
            }
            return area >= tot_area / 2.0;
        };

        double left = 0, right = max_y;
        for (int i = 0; i < 47; i++) {
            double mid = (left + right) / 2;
            (check(mid) ? right : left) = mid;
        }
        return (left + right) / 2; // 区间中点误差小
    }
};

###go

func separateSquares(squares [][]int) float64 {
totArea := 0
maxY := 0
for _, sq := range squares {
l := sq[2]
totArea += l * l
maxY = max(maxY, sq[1]+l)
}

check := func(y float64) bool {
area := 0.
for _, sq := range squares {
yi := float64(sq[1])
if yi < y {
l := float64(sq[2])
area += l * min(y-yi, l)
}
}
return area >= float64(totArea)/2
}

left, right := 0., float64(maxY)
for range bits.Len(uint(maxY * 1e5)) {
mid := (left + right) / 2
if check(mid) {
right = mid
} else {
left = mid
}
}
return (left + right) / 2 // 区间中点误差小
}

复杂度分析

  • 时间复杂度:$\mathcal{O}(n\log (U/\epsilon))$,其中 $n$ 是 $\textit{squares}$ 的长度,$\epsilon=10^{-5}$,$U=\max(y_i+l_i)$。
  • 空间复杂度:$\mathcal{O}(1)$。

方法二:整数二分(写法一)

1)

方法一的 $y$ 是个小数。

记 $M = \epsilon^{-1} = 10^5$,改为二分整数 $y\cdot M$,最后把二分结果再除以 $M$,即为答案。

在使用整数计算的前提下,这可以保证返回结果与正确答案的绝对误差在 $1/M=10^{-5}$ 以内。

2)

下面代码采用开区间二分,这仅仅是二分的一种写法,使用闭区间或者半闭半开区间都是可以的。

  • 开区间左端点初始值:$0$。无法满足要求。
  • 开区间右端点初始值:$\max(y_i+l_i) \cdot M$。一定满足要求。

3)

能否全程使用整数计算?(只在返回的时候计算浮点数)

设当前二分的整数值为 $\textit{multiY}$,那么水平线下方的面积为

$$
\begin{aligned}
& l_i\cdot\min\left(\max\left(\dfrac{\textit{multiY}}{M}-y_i,0\right), l_i\right) \
={} & \dfrac{l_i\cdot\min(\max(\textit{multiY}-y_i\cdot M,0), l_i \cdot M)}{M} \
\end{aligned}
$$

所以有

$$
\textit{area}y = \dfrac{1}{M} \sum{i=0}^{n-1} l_i\cdot\min(\max(\textit{multiY}-y_i\cdot M,0),l_i \cdot M)
$$

判定条件

$$
\textit{area}_y\cdot 2 \ge \textit{totalArea}
$$

可以改为

$$
2 \sum_{i=0}^{n-1} l_i\cdot\min(\max(\textit{multiY}-y_i\cdot M,0), l_i\cdot M)\ge \textit{totalArea}\cdot M
$$

这样就可以全程使用整数计算了,只在最终返回时用到了浮点数。

###py

class Solution:
    def separateSquares(self, squares: List[List[int]]) -> float:
        M = 100_000
        total_area = sum(l * l for _, _, l in squares)

        def check(multi_y: int) -> bool:
            area = 0
            for _, y, l in squares:
                if y * M < multi_y:
                    area += l * min(multi_y - y * M, l * M)
            return area * 2 >= total_area * M

        max_y = max(y + l for _, y, l in squares)
        return bisect_left(range(max_y * M), True, key=check) / M

###java

class Solution {
    private static final int M = 100_000;

    public double separateSquares(int[][] squares) {
        long totArea = 0;
        int maxY = 0;
        for (int[] sq : squares) {
            int l = sq[2];
            totArea += (long) l * l;
            maxY = Math.max(maxY, sq[1] + l);
        }

        long left = 0;
        long right = (long) maxY * M;
        while (left + 1 < right) {
            long mid = (left + right) >>> 1;
            if (check(squares, mid, totArea)) {
                right = mid;
            } else {
                left = mid;
            }
        }
        return (double) right / M;
    }

    private boolean check(int[][] squares, long multiY, double totArea) {
        long area = 0;
        for (int[] sq : squares) {
            long y = sq[1];
            if (y * M < multiY) {
                long l = sq[2];
                area += l * Math.min(multiY - y * M, l * M);
            }
        }
        return area * 2 >= totArea * M;
    }
}

###cpp

class Solution {
public:
    double separateSquares(vector<vector<int>>& squares) {
        long long tot_area = 0;
        int max_y = 0;
        for (auto& sq : squares) {
            int l = sq[2];
            tot_area += 1LL * l * l;
            max_y = max(max_y, sq[1] + l);
        }

        const int M = 100'000;
        auto check = [&](long long multi_y) -> bool {
            long long area = 0;
            for (auto& sq : squares) {
                long long y = sq[1];
                if (y * M < multi_y) {
                    long long l = sq[2];
                    area += l * min(multi_y - y * M, l * M);
                }
            }
            return area * 2 >= tot_area * M;
        };

        long long left = 0, right = 1LL * max_y * M;
        while (left + 1 < right) {
            long long mid = left + (right - left) / 2;
            (check(mid) ? right : left) = mid;
        }
        return 1.0 * right / M;
    }
};

###go

func separateSquares(squares [][]int) float64 {
totArea := 0
maxY := 0
for _, sq := range squares {
l := sq[2]
totArea += l * l
maxY = max(maxY, sq[1]+l)
}

const m = 100_000
multiY := sort.Search(maxY*m, func(multiY int) bool {
area := 0
for _, sq := range squares {
y, l := sq[1], sq[2]
if y*m < multiY {
area += l * min(multiY-y*m, l*m)
}
}
return area*2 >= totArea*m
})
return float64(multiY) / m
}

复杂度分析

  • 时间复杂度:$\mathcal{O}(n\log (U/\epsilon))$,其中 $n$ 是 $\textit{squares}$ 的长度,$\epsilon=10^{-5}$,$U=\max(y_i+l_i)$。
  • 空间复杂度:$\mathcal{O}(1)$。

方法二:整数二分(写法二)

改成在 $0$ 到 $\max(y_i+l_i)$ 中二分最小的整数 $y$,满足

$$
\textit{area}_y\cdot 2 \ge \textit{totalArea}
$$

那么答案就在整数 $y-1$ 到整数 $y$ 之间。

由于输入都是整数,所以从 $y-1$ 到 $y$,在水平线下方的面积和是线性增加的,我们可以直接把答案解出来。

由于从 $y-1$ 到 $y$,矩形的底边长之和不变,所以用矩形面积的增量,除以矩形的高 $y-(y-1)=1$,就是矩形的底边长之和

$$
\textit{sumL} = \textit{area}y - \textit{area}{y-1}
$$

设答案为 $y'$,那么

$$
\textit{area}_{y'} = \textit{area}_y - (y-y')\cdot \textit{sumL}
$$

题目要求

$$
\textit{area}_{y'} \cdot 2 = \textit{totalArea}
$$

解得

$$
y' = y - \dfrac{\textit{area}_y - \textit{totalArea}/2}{\textit{sumL}} = y - \dfrac{\textit{area}_y\cdot 2 - \textit{totalArea}}{\textit{sumL}\cdot 2}
$$

###py

class Solution:
    def separateSquares(self, squares: List[List[int]]) -> float:
        def calc_area(y: int) -> int:
            area = 0
            for _, yi, l in squares:
                if yi < y:
                    area += l * min(y - yi, l)
            return area

        tot_area = sum(l * l for _, _, l in squares)
        max_y = max(y + l for _, y, l in squares)
        y = bisect_left(range(max_y), tot_area, key=lambda y: calc_area(y) * 2)

        area_y = calc_area(y)
        sum_l = area_y - calc_area(y - 1)
        return y - (area_y * 2 - tot_area) / (sum_l * 2)  # 这样写误差更小

###java

class Solution {
    public double separateSquares(int[][] squares) {
        long totArea = 0;
        int maxY = 0;
        for (int[] sq : squares) {
            int l = sq[2];
            totArea += (long) l * l;
            maxY = Math.max(maxY, sq[1] + l);
        }

        int left = 0;
        int right = maxY;
        while (left + 1 < right) {
            int mid = (left + right) >>> 1;
            if (calcArea(squares, mid) * 2 >= totArea) {
                right = mid;
            } else {
                left = mid;
            }
        }
        int y = right;

        long areaY = calcArea(squares, y);
        long sumL = areaY - calcArea(squares, y - 1);
        return y - (areaY * 2 - totArea) / (sumL * 2.0); // 这样写误差更小
    }

    private long calcArea(int[][] squares, int y) {
        long area = 0;
        for (int[] sq : squares) {
            int yi = sq[1];
            if (yi < y) {
                int l = sq[2];
                area += (long) l * Math.min(y - yi, l);
            }
        }
        return area;
    }
}

###cpp

class Solution {
public:
    double separateSquares(vector<vector<int>>& squares) {
        long long tot_area = 0;
        int max_y = 0;
        for (auto& sq : squares) {
            int l = sq[2];
            tot_area += 1LL * l * l;
            max_y = max(max_y, sq[1] + l);
        }

        auto calc_area = [&](int y) {
            long long area = 0;
            for (auto& sq : squares) {
                int yi = sq[1];
                if (yi < y) {
                    int l = sq[2];
                    area += 1LL * l * min(y - yi, l);
                }
            }
            return area;
        };

        int left = 0, right = max_y;
        while (left + 1 < right) {
            int mid = left + (right - left) / 2;
            (calc_area(mid) * 2 >= tot_area ? right : left) = mid;
        }
        int y = right;

        long long area_y = calc_area(y);
        long long sum_l = area_y - calc_area(y - 1);
        return y - (area_y * 2 - tot_area) / (sum_l * 2.0); // 这样写误差更小
    }
};

###go

func separateSquares(squares [][]int) float64 {
totArea := 0
maxY := 0
for _, sq := range squares {
l := sq[2]
totArea += l * l
maxY = max(maxY, sq[1]+l)
}

calcArea := func(y int) (area int) {
for _, sq := range squares {
yi := sq[1]
if yi < y {
l := sq[2]
area += l * min(y-yi, l)
}
}
return
}
y := sort.Search(maxY, func(y int) bool { return calcArea(y)*2 >= totArea })

areaY := calcArea(y)
sumL := areaY - calcArea(y-1)
return float64(y) - float64(areaY*2-totArea)/float64(sumL*2) // 这样写误差更小
}

复杂度分析

  • 时间复杂度:$\mathcal{O}(n\log U)$,其中 $n$ 是 $\textit{squares}$ 的长度,$U=\max(y_i+l_i)$。
  • 空间复杂度:$\mathcal{O}(1)$。

方法三:差分+扫描线

lc3453.png

想象有一根水平扫描线在从下往上扫描,对于示例 2,这根扫描线依次扫过 $y=0,1,2$:

  • 从 $y=0$ 到 $y=1$,面积的增加量可以视作一个底边长为 $2$,高为 $1$ 的矩形的面积,即 $2\cdot 1 = 2$。
  • 从 $y=1$ 到 $y=2$,面积的增加量可以视作一个底边长为 $2+1=3$(重叠的要累加),高为 $1$ 的矩形的面积,即 $3\cdot 1 = 3$。

扫描的过程中,维护面积之和 $\textit{area}$,底边长之和 $\textit{sumL}$。

设当前 $y$ 与下一个 $y'$ 之差为 $y'-y$,则新增面积为

$$
\textit{sumL}\cdot (y'-y)
$$

如果发现

$$
\textit{area} \cdot 2 \ge \textit{totalArea}
$$

那么可以直接算出答案,计算公式和上面「方法二:整数二分(写法二)」是一样的。

$\textit{sumL}$ 可以用差分维护,请看 差分原理讲解

###py

class Solution:
    def separateSquares(self, squares: List[List[int]]) -> float:
        tot_area = 0
        diff = defaultdict(int)
        for _, y, l in squares:
            tot_area += l * l
            diff[y] += l
            diff[y + l] -= l

        area = sum_l = 0
        for y, y2 in pairwise(sorted(diff)):
            sum_l += diff[y]  # 矩形底边长度之和
            area += sum_l * (y2 - y)  # 底边长 * 高 = 新增面积
            if area * 2 >= tot_area:
                return y2 - (area * 2 - tot_area) / (sum_l * 2)

###java

class Solution {
    public double separateSquares(int[][] squares) {
        long totArea = 0;
        TreeMap<Integer, Long> diff = new TreeMap<>();
        for (int[] sq : squares) {
            int y = sq[1];
            long l = sq[2];
            totArea += l * l;
            diff.merge(y, l, Long::sum);
            diff.merge(y + (int) l, -l, Long::sum);
        }

        long area = 0;
        long sumL = 0;
        int preY = 0; // 不好计算下一个 y,改成维护上一个 y
        for (var e : diff.entrySet()) {
            int y = e.getKey();
            area += sumL * (y - preY); // 底边长 * 高 = 新增面积
            if (area * 2 >= totArea) {
                return y - (area * 2 - totArea) / (sumL * 2.0);
            }
            preY = y;
            sumL += e.getValue(); // 矩形底边长度之和
        }
        return -1;
    }
}

###cpp

class Solution {
public:
    double separateSquares(vector<vector<int>>& squares) {
        long long tot_area = 0;
        map<int, long long> diff;
        for (auto& sq : squares) {
            int y = sq[1], l = sq[2];
            tot_area += 1LL * l * l;
            diff[y] += l;
            diff[y + l] -= l;
        }

        long long area = 0, sum_l = 0;
        for (auto it = diff.begin();;) {
            auto [y, sl] = *it;
            int y2 = (++it)->first;
            sum_l += sl; // 矩形底边长度之和
            area += sum_l * (y2 - y); // 底边长 * 高 = 新增面积
            if (area * 2 >= tot_area) {
                return y2 - (area * 2 - tot_area) / (sum_l * 2.0);
            }
        }
    }
};

###go

func separateSquares(squares [][]int) float64 {
totArea := 0
diff := map[int]int{}
for _, sq := range squares {
y, l := sq[1], sq[2]
totArea += l * l
diff[y] += l
diff[y+l] -= l
}

ys := slices.Sorted(maps.Keys(diff))
area, sumL := 0, 0
for i := 0; ; i++ {
sumL += diff[ys[i]] // 矩形底边长度之和
area += sumL * (ys[i+1] - ys[i]) // 底边长 * 高 = 新增面积
if area*2 >= totArea {
return float64(ys[i+1]) - float64(area*2-totArea)/float64(sumL*2)
}
}
}

复杂度分析

  • 时间复杂度:$\mathcal{O}(n\log n)$,其中 $n$ 是 $\textit{squares}$ 的长度。瓶颈在排序/维护有序集合上。
  • 空间复杂度:$\mathcal{O}(n)$。

专题训练

见下面数据结构题单的「§2.1 一维差分」。

分类题单

如何科学刷题?

  1. 滑动窗口与双指针(定长/不定长/单序列/双序列/三指针/分组循环)
  2. 二分算法(二分答案/最小化最大值/最大化最小值/第K小)
  3. 单调栈(基础/矩形面积/贡献法/最小字典序)
  4. 网格图(DFS/BFS/综合应用)
  5. 位运算(基础/性质/拆位/试填/恒等式/思维)
  6. 图论算法(DFS/BFS/拓扑排序/基环树/最短路/最小生成树/网络流)
  7. 动态规划(入门/背包/划分/状态机/区间/状压/数位/数据结构优化/树形/博弈/概率期望)
  8. 常用数据结构(前缀和/差分/栈/队列/堆/字典树/并查集/树状数组/线段树)
  9. 数学算法(数论/组合/概率期望/博弈/计算几何/随机算法)
  10. 贪心与思维(基本贪心策略/反悔/区间/字典序/数学/思维/脑筋急转弯/构造)
  11. 链表、树与回溯(前后指针/快慢指针/DFS/BFS/直径/LCA)
  12. 字符串(KMP/Z函数/Manacher/字符串哈希/AC自动机/后缀数组/子序列自动机)

我的题解精选(已分类)

欢迎关注 B站@灵茶山艾府

每日一题-访问所有点的最小时间🟢

2026年1月12日 00:00

平面上有 n 个点,点的位置用整数坐标表示 points[i] = [xi, yi] 。请你计算访问所有这些点需要的 最小时间(以秒为单位)。

你需要按照下面的规则在平面上移动:

  • 每一秒内,你可以:
    • 沿水平方向移动一个单位长度,或者
    • 沿竖直方向移动一个单位长度,或者
    • 跨过对角线移动 sqrt(2) 个单位长度(可以看作在一秒内向水平和竖直方向各移动一个单位长度)。
  • 必须按照数组中出现的顺序来访问这些点。
  • 在访问某个点时,可以经过该点后面出现的点,但经过的那些点不算作有效访问。

 

示例 1:

输入:points = [[1,1],[3,4],[-1,0]]
输出:7
解释:一条最佳的访问路径是: [1,1] -> [2,2] -> [3,3] -> [3,4] -> [2,3] -> [1,2] -> [0,1] -> [-1,0]   
从 [1,1] 到 [3,4] 需要 3 秒 
从 [3,4] 到 [-1,0] 需要 4 秒
一共需要 7 秒

示例 2:

输入:points = [[3,2],[-2,2]]
输出:5

 

提示:

  • points.length == n
  • 1 <= n <= 100
  • points[i].length == 2
  • -1000 <= points[i][0], points[i][1] <= 1000

切比雪夫距离(Python/Java/C++/C/Go/JS/Rust)

作者 endlesscheng
2025年12月31日 13:57

假设我们要从点 $(x_1,y_1)$ 移动到点 $(x_2,y_2)$。

水平距离为 $d_x = |x_1-x_2|$。

垂直距离为 $d_y = |y_1-y_2|$。

如果 $d_x$ 和 $d_y$ 都大于 $0$,那么:

  • 使用对角线移动,一秒就能把 $d_x$ 和 $d_y$ 都减少 $1$。
  • 另外两种移动,一秒只能把 $d_x$ 和 $d_y$ 的其中一个减少一。

所以当 $d_x$ 和 $d_y$ 都大于 $0$ 时,使用对角线移动是最优的。

  • 如果 $d_x > d_y$,先沿着对角线移动 $d_y$ 秒,再水平移动 $d_x - d_y$ 秒,一共移动 $d_x$ 秒。
  • 如果 $d_x \le d_y$,先沿着对角线移动 $d_x$ 秒,再垂直移动 $d_y - d_x$ 秒,一共移动 $d_y$ 秒。

所以从点 $(x_1,y_1)$ 移动到点 $(x_2,y_2)$ 至少要花

$$
\max(d_x,d_y) = \max(|x_1-x_2|,|y_1-y_2|)
$$

秒。上式也是两点的切比雪夫距离。

由于题目要求「必须按照数组中出现的顺序来访问这些点」,我们遍历 $\textit{points}$ 中的相邻点对,计算上式,累加即为答案。

###py

class Solution:
    def minTimeToVisitAllPoints(self, points: List[List[int]]) -> int:
        ans = 0
        for (x1, y1), (x2, y2) in pairwise(points):
            ans += max(abs(x1 - x2), abs(y1 - y2))
        return ans

###py

class Solution:
    def minTimeToVisitAllPoints(self, points: List[List[int]]) -> int:
        return sum(max(abs(x1 - x2), abs(y1 - y2)) for (x1, y1), (x2, y2) in pairwise(points))

###java

class Solution {
    public int minTimeToVisitAllPoints(int[][] points) {
        int ans = 0;
        for (int i = 1; i < points.length; i++) {
            int[] p = points[i - 1];
            int[] q = points[i];
            ans += Math.max(Math.abs(p[0] - q[0]), Math.abs(p[1] - q[1]));
        }
        return ans;
    }
}

###cpp

class Solution {
public:
    int minTimeToVisitAllPoints(vector<vector<int>>& points) {
        int ans = 0;
        for (int i = 1; i < points.size(); i++) {
            auto& p = points[i - 1];
            auto& q = points[i];
            ans += max(abs(p[0] - q[0]), abs(p[1] - q[1]));
        }
        return ans;
    }
};

###c

#define MAX(a, b) ((b) > (a) ? (b) : (a))

int minTimeToVisitAllPoints(int** points, int pointsSize, int* pointsColSize) {
    int ans = 0;
    for (int i = 1; i < pointsSize; i++) {
        int* p = points[i - 1];
        int* q = points[i];
        ans += MAX(abs(p[0] - q[0]), abs(p[1] - q[1]));
    }
    return ans;
}

###go

func minTimeToVisitAllPoints(points [][]int) (ans int) {
for i := 1; i < len(points); i++ {
p := points[i-1]
q := points[i]
ans += max(abs(p[0]-q[0]), abs(p[1]-q[1]))
}
return
}

func abs(x int) int {
if x < 0 {
return -x
}
return x
}

###js

var minTimeToVisitAllPoints = function(points) {
    let ans = 0;
    for (let i = 1; i < points.length; i++) {
        const [x1, y1] = points[i - 1];
        const [x2, y2] = points[i];
        ans += Math.max(Math.abs(x1 - x2), Math.abs(y1 - y2));
    }
    return ans;
};

###rust

impl Solution {
    pub fn min_time_to_visit_all_points(points: Vec<Vec<i32>>) -> i32 {
        points.windows(2)
            .map(|w| (w[0][0] - w[1][0]).abs().max((w[0][1] - w[1][1]).abs()))
            .sum()
    }
}

复杂度分析

  • 时间复杂度:$\mathcal{O}(n)$,其中 $n$ 是 $\textit{points}$ 的长度。
  • 空间复杂度:$\mathcal{O}(1)$。

分类题单

如何科学刷题?

  1. 滑动窗口与双指针(定长/不定长/单序列/双序列/三指针/分组循环)
  2. 二分算法(二分答案/最小化最大值/最大化最小值/第K小)
  3. 单调栈(基础/矩形面积/贡献法/最小字典序)
  4. 网格图(DFS/BFS/综合应用)
  5. 位运算(基础/性质/拆位/试填/恒等式/思维)
  6. 图论算法(DFS/BFS/拓扑排序/基环树/最短路/最小生成树/网络流)
  7. 动态规划(入门/背包/划分/状态机/区间/状压/数位/数据结构优化/树形/博弈/概率期望)
  8. 常用数据结构(前缀和/差分/栈/队列/堆/字典树/并查集/树状数组/线段树)
  9. 数学算法(数论/组合/概率期望/博弈/计算几何/随机算法)
  10. 贪心与思维(基本贪心策略/反悔/区间/字典序/数学/思维/脑筋急转弯/构造)
  11. 链表、树与回溯(前后指针/快慢指针/DFS/BFS/直径/LCA)
  12. 字符串(KMP/Z函数/Manacher/字符串哈希/AC自动机/后缀数组/子序列自动机)

我的题解精选(已分类)

访问所有点的最小时间

2020年2月19日 11:34

方法一:切比雪夫距离

对于平面上的两个点 x = (x0, x1)y = (y0, y1),设它们横坐标距离之差为 dx = |x0 - y0|,纵坐标距离之差为 dy = |x1 - y1|,对于以下三种情况,我们可以分别计算出从 x 移动到 y 的最少次数:

  • dx < dy:沿对角线移动 dx 次,再竖直移动 dy - dx 次,总计 dx + (dy - dx) = dy 次;

  • dx == dy:沿对角线移动 dx 次;

  • dx > dy:沿对角线移动 dy 次,再水平移动 dx - dy 次,总计 dy + (dx - dy) = dx 次。

可以发现,对于任意一种情况,从 x 移动到 y 的最少次数为 dxdy 中的较大值 max(dx, dy),这也被称作 xy 之间的 切比雪夫距离

由于题目要求,需要按照数组中出现的顺序来访问这些点。因此我们遍历整个数组,对于数组中的相邻两个点,计算出它们的切比雪夫距离,所有的距离之和即为答案。

###C++

class Solution {
public:
    int minTimeToVisitAllPoints(vector<vector<int>>& points) {
        int x0 = points[0][0], x1 = points[0][1];
        int ans = 0;
        for (int i = 1; i < points.size(); ++i) {
            int y0 = points[i][0], y1 = points[i][1];
            ans += max(abs(x0 - y0), abs(x1 - y1));
            x0 = y0;
            x1 = y1;
        }
        return ans;
    }
};

###Python

class Solution:
    def minTimeToVisitAllPoints(self, points: List[List[int]]) -> int:
        x0, x1 = points[0]
        ans = 0
        for i in range(1, len(points)):
            y0, y1 = points[i]
            ans += max(abs(x0 - y0), abs(x1 - y1))
            x0, x1 = points[i]
        return ans

###Java

class Solution {
    public int minTimeToVisitAllPoints(int[][] points) {
        int x0 = points[0][0], y0 = points[0][1];
        int ans = 0;
        for (int i = 1; i < points.length; ++i) {
            int x1 = points[i][0], y1 = points[i][1];
            ans += Math.max(Math.abs(x0 - x1), Math.abs(y0 - y1));
            x0 = x1;
            y0 = y1;
        }
        return ans;
    }
}

###C#

public class Solution {
    public int MinTimeToVisitAllPoints(int[][] points) {
        int x0 = points[0][0], y0 = points[0][1];
        int ans = 0;
        for (int i = 1; i < points.Length; ++i) {
            int x1 = points[i][0], y1 = points[i][1];
            ans += Math.Max(Math.Abs(x0 - x1), Math.Abs(y0 - y1));
            x0 = x1;
            y0 = y1;
        }
        return ans;
    }
}

###Go

func minTimeToVisitAllPoints(points [][]int) int {
    x0, y0 := points[0][0], points[0][1]
    ans := 0
    for i := 1; i < len(points); i++ {
        x1, y1 := points[i][0], points[i][1]
        dx := abs(x0 - x1)
        dy := abs(y0 - y1)
        if dx > dy {
            ans += dx
        } else {
            ans += dy
        }
        x0, y0 = x1, y1
    }
    return ans
}

func abs(x int) int {
    if x < 0 {
        return -x
    }
    return x
}

###C

#include <stdlib.h>

int minTimeToVisitAllPoints(int** points, int pointsSize, int* pointsColSize) {
    int x0 = points[0][0], y0 = points[0][1];
    int ans = 0;
    for (int i = 1; i < pointsSize; ++i) {
        int x1 = points[i][0], y1 = points[i][1];
        int dx = abs(x0 - x1);
        int dy = abs(y0 - y1);
        ans += (dx > dy) ? dx : dy;
        x0 = x1;
        y0 = y1;
    }
    return ans;
}

###JavaScript

var minTimeToVisitAllPoints = function(points) {
    let x0 = points[0][0], y0 = points[0][1];
    let ans = 0;
    for (let i = 1; i < points.length; ++i) {
        let x1 = points[i][0], y1 = points[i][1];
        ans += Math.max(Math.abs(x0 - x1), Math.abs(y0 - y1));
        x0 = x1;
        y0 = y1;
    }
    return ans;
};

###TypeScript

function minTimeToVisitAllPoints(points: number[][]): number {
    let x0 = points[0][0], y0 = points[0][1];
    let ans = 0;
    for (let i = 1; i < points.length; ++i) {
        let x1 = points[i][0], y1 = points[i][1];
        ans += Math.max(Math.abs(x0 - x1), Math.abs(y0 - y1));
        x0 = x1;
        y0 = y1;
    }
    return ans;
}

###Rust

impl Solution {
    pub fn min_time_to_visit_all_points(points: Vec<Vec<i32>>) -> i32 {
        let mut x0 = points[0][0];
        let mut y0 = points[0][1];
        let mut ans = 0;
        
        for i in 1..points.len() {
            let x1 = points[i][0];
            let y1 = points[i][1];
            ans += (x0 - x1).abs().max((y0 - y1).abs());
            x0 = x1;
            y0 = y1;
        }
        
        ans
    }
}

复杂度分析

  • 时间复杂度:$O(N)$,其中 $N$ 是数组的长度。

  • 空间复杂度:$O(1)$。

C++:暴力法

作者 yfnupup
2019年11月24日 12:31

题解:两点之间的距离就是直角三角形直角边的较大值

image.png

代码如下:

###cpp

class Solution {
public:
    int minTimeToVisitAllPoints(vector<vector<int>>& points) {
        int step=0;
        for(int i=0;i<points.size()-1;++i)
        {
            int x=abs(points[i][0]-points[i+1][0]);
            int y=abs(points[i][1]-points[i+1][1]);
            step+=max(x,y);
        }
        return step;
    }  
};

每日一题-最大矩形🔴

2026年1月11日 00:00

给定一个仅包含 01 、大小为 rows x cols 的二维二进制矩阵,找出只包含 1 的最大矩形,并返回其面积。

 

示例 1:

输入:matrix = [["1","0","1","0","0"],["1","0","1","1","1"],["1","1","1","1","1"],["1","0","0","1","0"]]
输出:6
解释:最大矩形如上图所示。

示例 2:

输入:matrix = [["0"]]
输出:0

示例 3:

输入:matrix = [["1"]]
输出:1

 

提示:

  • rows == matrix.length
  • cols == matrix[0].length
  • 1 <= rows, cols <= 200
  • matrix[i][j]'0''1'

直接调用 84 题代码解决(Python/Java/C++/C/Go/JS/Rust)

作者 endlesscheng
2025年6月19日 19:14

回顾一下,这是 84. 柱状图中最大的矩形 的图:

lc84.jpg{:width=400}

对于本题,设 $\textit{matrix}$ 有 $m$ 行,我们可以枚举矩形的底边,做 $m$ 次 84 题。

lc85.png{:width=300}

  • 以第一行为底的柱子高度为 $[1,0,1,0,0]$,最大矩形面积为 $1$。
  • 以第二行为底的柱子高度为 $[2,0,2,1,1]$,最大矩形面积为 $3$。
  • 以第三行为底的柱子高度为 $[3,1,3,2,2]$,最大矩形面积为 $6$。
  • 以第四行为底的柱子高度为 $[4,0,0,3,0]$,最大矩形面积为 $4$。
  • 答案为 $\max(1,3,6,4) = 6$。

由于我们枚举的是矩形的底边,如果 $\textit{matrix}[i][j]=0$,那么没有柱子,高度等于 $0$。否则,在上一行柱子的基础上,把柱子高度增加 $1$。形象地说,就是在柱子下面垫一块石头,把柱子抬高。

###py

class Solution:
    # 84. 柱状图中最大的矩形
    def largestRectangleArea(self, heights: List[int]) -> int:
        st = [-1]  # 在栈中只有一个数的时候,栈顶的「下面那个数」是 -1,对应 left[i] = -1 的情况
        ans = 0
        for right, h in enumerate(heights):
            while len(st) > 1 and heights[st[-1]] >= h:
                i = st.pop()  # 矩形的高(的下标)
                left = st[-1]  # 栈顶下面那个数就是 left
                ans = max(ans, heights[i] * (right - left - 1))
            st.append(right)
        return ans

    def maximalRectangle(self, matrix: List[List[str]]) -> int:
        n = len(matrix[0])
        heights = [0] * (n + 1)  # 末尾多一个 0,理由见我 84 题题解
        ans = 0
        for row in matrix:
            # 计算底边为 row 的柱子高度
            for j, c in enumerate(row):
                if c == '0':
                    heights[j] = 0  # 柱子高度为 0
                else:
                    heights[j] += 1  # 柱子高度加一
            ans = max(ans, self.largestRectangleArea(heights))  # 调用 84 题代码
        return ans

###java

class Solution {
    int maximalRectangle(char[][] matrix) {
        int n = matrix[0].length;
        int[] heights = new int[n + 1]; // 末尾多一个 0,理由见我 84 题题解
        int ans = 0;
        for (char[] row : matrix) {
            // 计算底边为 row 的柱子高度
            for (int j = 0; j < n; j++) {
                if (row[j] == '0') {
                    heights[j] = 0; // 柱子高度为 0
                } else {
                    heights[j]++; // 柱子高度加一
                }
            }
            ans = Math.max(ans, largestRectangleArea(heights)); // 调用 84 题代码
        }
        return ans;
    }

    // 84. 柱状图中最大的矩形
    private int largestRectangleArea(int[] heights) {
        int n = heights.length;
        int[] st = new int[n]; // 用数组模拟栈
        int top = -1; // 栈顶下标
        st[++top] = -1; // 在栈中只有一个数的时候,栈顶的「下面那个数」是 -1,对应 left[i] = -1 的情况
        int ans = 0;
        for (int right = 0; right < n; right++) {
            int h = heights[right];
            while (top > 0 && heights[st[top]] >= h) {
                int i = st[top--]; // 矩形的高(的下标)
                int left = st[top]; // 栈顶下面那个数就是 left
                ans = Math.max(ans, heights[i] * (right - left - 1));
            }
            st[++top] = right;
        }
        return ans;
    }
}

###cpp

class Solution {
    // 84. 柱状图中最大的矩形
    int largestRectangleArea(vector<int>& heights) {
        stack<int> st;
        st.push(-1); // 在栈中只有一个数的时候,栈顶的「下面那个数」是 -1,对应 left[i] = -1 的情况
        int ans = 0;
        for (int right = 0; right < heights.size(); right++) {
            int h = heights[right];
            while (st.size() > 1 && heights[st.top()] >= h) {
                int i = st.top(); // 矩形的高(的下标)
                st.pop();
                int left = st.top(); // 栈顶下面那个数就是 left
                ans = max(ans, heights[i] * (right - left - 1));
            }
            st.push(right);
        }
        return ans;
    }

public:
    int maximalRectangle(vector<vector<char>>& matrix) {
        int n = matrix[0].size();
        vector<int> heights(n + 1); // 末尾多一个 0,理由见我 84 题题解
        int ans = 0;
        for (auto& row : matrix) {
            // 计算底边为 row 的柱子高度
            for (int j = 0; j < n; j++) {
                if (row[j] == '0') {
                    heights[j] = 0; // 柱子高度为 0
                } else {
                    heights[j]++; // 柱子高度加一
                }
            }
            ans = max(ans, largestRectangleArea(heights)); // 调用 84 题代码
        }
        return ans;
    }
};

###c

#define MAX(a, b) ((b) > (a) ? (b) : (a))

// 84. 柱状图中最大的矩形
int largestRectangleArea(int* heights, int n) {
    int* stack = malloc(n * sizeof(int));
    int top = -1; // 栈顶下标
    stack[++top] = -1; // 在栈中只有一个数的时候,栈顶的「下面那个数」是 -1,对应 left[i] = -1 的情况
    int ans = 0;
    for (int right = 0; right < n; right++) {
        int h = heights[right];
        while (top > 0 && heights[stack[top]] >= h) {
            int i = stack[top--]; // 矩形的高(的下标)
            int left = stack[top]; // 栈顶下面那个数就是 left
            ans = MAX(ans, heights[i] * (right - left - 1));
        }
        stack[++top] = right;
    }
    free(stack);
    return ans;
}

int maximalRectangle(char** matrix, int matrixSize, int* matrixColSize) {
    int n = matrixColSize[0];
    int* heights = calloc(n + 1, sizeof(int)); // 末尾多一个 0,理由见我 84 题题解
    int ans = 0;
    for (int i = 0; i < matrixSize; i++) {
        char* row = matrix[i];
        // 计算底边为 row 的柱子高度
        for (int j = 0; j < n; j++) {
            if (row[j] == '0') {
                heights[j] = 0; // 柱子高度为 0
            } else {
                heights[j]++; // 柱子高度加一
            }
        }
        ans = MAX(ans, largestRectangleArea(heights, n + 1)); // 调用 84 题代码,注意传入的是 n+1
    }
    free(heights);
    return ans;
}

###go

// 84. 柱状图中最大的矩形
func largestRectangleArea(heights []int) (ans int) {
    st := []int{-1} // 在栈中只有一个数的时候,栈顶的「下面那个数」是 -1,对应 left[i] = -1 的情况
    for right, h := range heights {
        for len(st) > 1 && heights[st[len(st)-1]] >= h {
            i := st[len(st)-1] // 矩形的高(的下标)
            st = st[:len(st)-1]
            left := st[len(st)-1] // 栈顶下面那个数就是 left
            ans = max(ans, heights[i]*(right-left-1))
        }
        st = append(st, right)
    }
    return
}

func maximalRectangle(matrix [][]byte) (ans int) {
    heights := make([]int, len(matrix[0])+1) // 末尾多一个 0,理由见我 84 题题解
    for _, row := range matrix {
        // 计算底边为 row 的柱子高度
        for j, c := range row {
            if c == '0' {
                heights[j] = 0 // 柱子高度为 0
            } else {
                heights[j]++ // 柱子高度加一
            }
        }
        ans = max(ans, largestRectangleArea(heights)) // 调用 84 题代码
    }
    return
}

###js

// 84. 柱状图中最大的矩形
var largestRectangleArea = function(heights) {
    const st = [-1]; // 在栈中只有一个数的时候,栈顶的「下面那个数」是 -1,对应 left[i] = -1 的情况
    let ans = 0;
    for (let right = 0; right < heights.length; right++) {
        const h = heights[right]
        while (st.length > 1 && heights[st[st.length - 1]] >= h) {
            const i = st.pop(); // 矩形的高(的下标)
            const left = st[st.length - 1]; // 栈顶下面那个数就是 left
            ans = Math.max(ans, heights[i] * (right - left - 1));
        }
        st.push(right);
    }
    return ans;
};

var maximalRectangle = function(matrix) {
    const n = matrix[0].length;
    let heights = Array(n + 1).fill(0); // 末尾多一个 0,理由见我 84 题题解
    let ans = 0;
    for (const row of matrix) {
        // 计算底边为 row 的柱子高度
        for (let j = 0; j < n; j++) {
            if (row[j] === '0') {
                heights[j] = 0; // 柱子高度为 0
            } else {
                heights[j]++; // 柱子高度加一
            }
        }
        ans = Math.max(ans, largestRectangleArea(heights)); // 调用 84 题代码
    }
    return ans;
};

###rust

impl Solution {
    // 84. 柱状图中最大的矩形
    fn largest_rectangle_area(heights: &[i32]) -> i32 {
        let mut st = vec![-1]; // 在栈中只有一个数的时候,栈顶的「下面那个数」是 -1,对应 left[i] = -1 的情况
        let mut ans = 0;
        for (right, &h) in heights.iter().enumerate() {
            let right = right as i32;
            while st.len() > 1 && heights[*st.last().unwrap() as usize] >= h {
                let i = st.pop().unwrap() as usize; // 矩形的高(的下标)
                let left = *st.last().unwrap(); // 栈顶下面那个数就是 left
                ans = ans.max(heights[i] * (right - left - 1));
            }
            st.push(right);
        }
        ans
    }

    pub fn maximal_rectangle(matrix: Vec<Vec<char>>) -> i32 {
        let n = matrix[0].len();
        let mut heights = vec![0; n + 1]; // 末尾多一个 0,理由见我 84 题题解
        let mut ans = 0;
        for row in matrix {
            // 计算底边为 row 的柱子高度
            for (j, c) in row.into_iter().enumerate() {
                if c == '0' {
                    heights[j] = 0; // 柱子高度为 0
                } else {
                    heights[j] += 1; // 柱子高度加一
                }
            }
            ans = ans.max(Self::largest_rectangle_area(&heights)); // 调用 84 题代码
        }
        ans
    }
}

复杂度分析

  • 时间复杂度:$\mathcal{O}(mn)$,其中 $m$ 和 $n$ 分别为 $\textit{matrix}$ 的行数和列数。做 $m$ 次 84 题,每次 $\mathcal{O}(n)$。
  • 空间复杂度:$\mathcal{O}(n)$。

相似题目

见下面单调栈题单的「二、矩形」。

分类题单

如何科学刷题?

  1. 滑动窗口与双指针(定长/不定长/单序列/双序列/三指针/分组循环)
  2. 二分算法(二分答案/最小化最大值/最大化最小值/第K小)
  3. 单调栈(基础/矩形面积/贡献法/最小字典序)
  4. 网格图(DFS/BFS/综合应用)
  5. 位运算(基础/性质/拆位/试填/恒等式/思维)
  6. 图论算法(DFS/BFS/拓扑排序/基环树/最短路/最小生成树/网络流)
  7. 动态规划(入门/背包/划分/状态机/区间/状压/数位/数据结构优化/树形/博弈/概率期望)
  8. 常用数据结构(前缀和/差分/栈/队列/堆/字典树/并查集/树状数组/线段树)
  9. 数学算法(数论/组合/概率期望/博弈/计算几何/随机算法)
  10. 贪心与思维(基本贪心策略/反悔/区间/字典序/数学/思维/脑筋急转弯/构造)
  11. 链表、二叉树与回溯(前后指针/快慢指针/DFS/BFS/直径/LCA/一般树)
  12. 字符串(KMP/Z函数/Manacher/字符串哈希/AC自动机/后缀数组/子序列自动机)

我的题解精选(已分类)

欢迎关注 B站@灵茶山艾府

最大矩形

2020年12月25日 23:43

方法一: 使用柱状图的优化暴力方法

思路与算法

最原始地,我们可以列举每个可能的矩形。我们枚举矩形所有可能的左上角坐标和右下角坐标,并检查该矩形是否符合要求。然而该方法的时间复杂度过高,不能通过所有的测试用例,因此我们必须寻找其他方法。

我们首先计算出矩阵的每个元素的左边连续 $1$ 的数量,使用二维数组 $\textit{left}$ 记录,其中 $\textit{left}[i][j]$ 为矩阵第 $i$ 行第 $j$ 列元素的左边连续 $1$ 的数量。

<ppt1,ppt2,ppt3,ppt4,ppt5,ppt6,ppt7,ppt8,ppt9,ppt10>

随后,对于矩阵中任意一个点,我们枚举以该点为右下角的全 $1$ 矩形。

具体而言,当考察以 $\textit{matrix}[i][j]$ 为右下角的矩形时,我们枚举满足 $0 \le k \le i$ 的所有可能的 $k$,此时矩阵的最大宽度就为

$$
\textit{left}[i][j], \textit{left}[i-1][j], \ldots, \textit{left}[k][j]
$$

的最小值。

下图有助于理解。给定每个点的最大宽度,可计算出底端黄色方块的最大矩形面积。

<fig1,fig2,fig3,fig4,fig5,fig6,fig7>

对每个点重复这一过程,就可以得到全局的最大矩形。

我们预计算最大宽度的方法事实上将输入转化成了一系列的柱状图,我们针对每个柱状图计算最大面积。

fig2
{:align="center"}

于是,上述方法本质上是「84. 柱状图中最大的矩形」题中优化暴力算法的复用。

代码

###C++

class Solution {
public:
    int maximalRectangle(vector<vector<char>>& matrix) {
        int m = matrix.size();
        if (m == 0) {
            return 0;
        }
        int n = matrix[0].size();
        vector<vector<int>> left(m, vector<int>(n, 0));

        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                if (matrix[i][j] == '1') {
                    left[i][j] = (j == 0 ? 0: left[i][j - 1]) + 1;
                }
            }
        }

        int ret = 0;
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                if (matrix[i][j] == '0') {
                    continue;
                }
                int width = left[i][j];
                int area = width;
                for (int k = i - 1; k >= 0; k--) {
                    width = min(width, left[k][j]);
                    area = max(area, (i - k + 1) * width);
                }
                ret = max(ret, area);
            }
        }
        return ret;
    }
};

###Java

class Solution {
    public int maximalRectangle(char[][] matrix) {
        int m = matrix.length;
        if (m == 0) {
            return 0;
        }
        int n = matrix[0].length;
        int[][] left = new int[m][n];

        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                if (matrix[i][j] == '1') {
                    left[i][j] = (j == 0 ? 0 : left[i][j - 1]) + 1;
                }
            }
        }

        int ret = 0;
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                if (matrix[i][j] == '0') {
                    continue;
                }
                int width = left[i][j];
                int area = width;
                for (int k = i - 1; k >= 0; k--) {
                    width = Math.min(width, left[k][j]);
                    area = Math.max(area, (i - k + 1) * width);
                }
                ret = Math.max(ret, area);
            }
        }
        return ret;
    }
}

###JavaScript

var maximalRectangle = function(matrix) {
    const m = matrix.length;
    if (m === 0) {
        return 0;
    }
    const n = matrix[0].length;
    const left = new Array(m).fill(0).map(() => new Array(n).fill(0));

    for (let i = 0; i < m; i++) {
        for (let j = 0; j < n; j++) {
            if (matrix[i][j] === '1') {
                left[i][j] = (j === 0 ? 0 : left[i][j - 1]) + 1;
            }
        }
    }

    let ret = 0;
    for (let i = 0; i < m; i++) {
        for (let j = 0; j < n; j++) {
            if (matrix[i][j] === '0') {
                continue;
            }
            let width = left[i][j];
            let area = width;
            for (let k = i - 1; k >= 0; k--) {
                width = Math.min(width, left[k][j]);
                area = Math.max(area, (i - k + 1) * width);
            }
            ret = Math.max(ret, area);
        }
    }
    return ret;
};

###go

func maximalRectangle(matrix [][]byte) (ans int) {
    if len(matrix) == 0 {
        return
    }
    m, n := len(matrix), len(matrix[0])
    left := make([][]int, m)
    for i, row := range matrix {
        left[i] = make([]int, n)
        for j, v := range row {
            if v == '0' {
                continue
            }
            if j == 0 {
                left[i][j] = 1
            } else {
                left[i][j] = left[i][j-1] + 1
            }
        }
    }
    for i, row := range matrix {
        for j, v := range row {
            if v == '0' {
                continue
            }
            width := left[i][j]
            area := width
            for k := i - 1; k >= 0; k-- {
                width = min(width, left[k][j])
                area = max(area, (i-k+1)*width)
            }
            ans = max(ans, area)
        }
    }
    return
}

func min(a, b int) int {
    if a < b {
        return a
    }
    return b
}

func max(a, b int) int {
    if a > b {
        return a
    }
    return b
}

###C

int maximalRectangle(char** matrix, int matrixSize, int* matrixColSize) {
    int m = matrixSize;
    if (m == 0) {
        return 0;
    }
    int n = matrixColSize[0];
    int left[m][n];
    memset(left, 0, sizeof(left));
    for (int i = 0; i < m; i++) {
        for (int j = 0; j < n; j++) {
            if (matrix[i][j] == '1') {
                left[i][j] = (j == 0 ? 0 : left[i][j - 1]) + 1;
            }
        }
    }

    int ret = 0;
    for (int i = 0; i < m; i++) {
        for (int j = 0; j < n; j++) {
            if (matrix[i][j] == '0') {
                continue;
            }
            int width = left[i][j];
            int area = width;
            for (int k = i - 1; k >= 0; k--) {
                width = fmin(width, left[k][j]);
                area = fmax(area, (i - k + 1) * width);
            }
            ret = fmax(ret, area);
        }
    }
    return ret;
}

###C#

public class Solution {
    public int MaximalRectangle(char[][] matrix) {
        int m = matrix.Length;
        if (m == 0) return 0;
        int n = matrix[0].Length;
        int[][] left = new int[m][];
        for (int i = 0; i < m; i++) {
            left[i] = new int[n];
            for (int j = 0; j < n; j++) {
                if (matrix[i][j] == '1') {
                    left[i][j] = (j == 0 ? 0 : left[i][j - 1]) + 1;
                }
            }
        }

        int ret = 0;
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                if (matrix[i][j] == '0') {
                    continue;
                }
                int width = left[i][j];
                int area = width;
                for (int k = i - 1; k >= 0; k--) {
                    width = Math.Min(width, left[k][j]);
                    area = Math.Max(area, (i - k + 1) * width);
                }
                ret = Math.Max(ret, area);
            }
        }
        return ret;
    }
}

###Python

class Solution:
    def maximalRectangle(self, matrix: list[list[str]]) -> int:
        if not matrix:
            return 0
        m, n = len(matrix), len(matrix[0])
        left = [[0] * n for _ in range(m)]

        for i in range(m):
            for j in range(n):
                if matrix[i][j] == '1':
                    left[i][j] = (0 if j == 0 else left[i][j - 1]) + 1

        ret = 0
        for i in range(m):
            for j in range(n):
                if matrix[i][j] == '0':
                    continue
                width = left[i][j]
                area = width
                for k in range(i - 1, -1, -1):
                    width = min(width, left[k][j])
                    area = max(area, (i - k + 1) * width)
                ret = max(ret, area)
        return ret

###TypeScript

function maximalRectangle(matrix: string[][]): number {
    const m = matrix.length;
    if (m === 0) {
        return 0;
    }
    const n = matrix[0].length;
    const left: number[][] = Array.from({ length: m }, () => Array(n).fill(0));

    for (let i = 0; i < m; i++) {
        for (let j = 0; j < n; j++) {
            if (matrix[i][j] === '1') {
                left[i][j] = (j === 0 ? 0 : left[i][j - 1]) + 1;
            }
        }
    }

    let ret = 0;
    for (let i = 0; i < m; i++) {
        for (let j = 0; j < n; j++) {
            if (matrix[i][j] === '0') {
                continue;
            }
            let width = left[i][j];
            let area = width;
            for (let k = i - 1; k >= 0; k--) {
                width = Math.min(width, left[k][j]);
                area = Math.max(area, (i - k + 1) * width);
            }
            ret = Math.max(ret, area);
        }
    }
    return ret;
}

###Rust

impl Solution {
    pub fn maximal_rectangle(matrix: Vec<Vec<char>>) -> i32 {
        let m = matrix.len();
        if m == 0 {
            return 0;
        }
        let n = matrix[0].len();
        let mut left = vec![vec![0; n]; m];

        for i in 0..m {
            for j in 0..n {
                if matrix[i][j] == '1' {
                    left[i][j] = if j == 0 { 0 } else { left[i][j - 1] } + 1;
                }
            }
        }

        let mut ret = 0;
        for i in 0..m {
            for j in 0..n {
                if matrix[i][j] == '0' {
                    continue;
                }
                let mut width = left[i][j];
                let mut area = width;
                for k in (0..i).rev() {
                    width = width.min(left[k][j]);
                    area = area.max((i - k + 1) * width);
                }
                ret = ret.max(area);
            }
        }
        ret as i32
    }
}

复杂度分析

  • 时间复杂度:$O(m^2n)$,其中 $m$ 和 $n$ 分别是矩阵的行数和列数。计算 $\textit{left}$ 矩阵需要 $O(mn)$ 的时间。随后对于矩阵的每个点,需要 $O(m)$ 的时间枚举高度。故总的时间复杂度为 $O(mn) + O(mn) \cdot O(m) = O(m^2n)$。

  • 空间复杂度:$O(mn)$,其中 $m$ 和 $n$ 分别是矩阵的行数和列数。我们分配了一个与给定矩阵等大的数组,用于存储每个元素的左边连续 $1$ 的数量。

方法二:单调栈

思路与算法

在方法一中,我们讨论了将输入拆分成一系列的柱状图。为了计算矩形的最大面积,我们只需要计算每个柱状图中的最大面积,并找到全局最大值。

我们可以使用「84. 柱状图中最大的矩形的官方题解」中的单调栈的做法,将其应用在我们生成的柱状图中。

代码

###C++

class Solution {
public:
    int maximalRectangle(vector<vector<char>>& matrix) {
        int m = matrix.size();
        if (m == 0) {
            return 0;
        }
        int n = matrix[0].size();
        vector<vector<int>> left(m, vector<int>(n, 0));

        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                if (matrix[i][j] == '1') {
                    left[i][j] = (j == 0 ? 0: left[i][j - 1]) + 1;
                }
            }
        }

        int ret = 0;
        for (int j = 0; j < n; j++) { // 对于每一列,使用基于柱状图的方法
            vector<int> up(m, 0), down(m, 0);

            stack<int> stk;
            for (int i = 0; i < m; i++) {
                while (!stk.empty() && left[stk.top()][j] >= left[i][j]) {
                    stk.pop();
                }
                up[i] = stk.empty() ? -1 : stk.top();
                stk.push(i);
            }
            stk = stack<int>();
            for (int i = m - 1; i >= 0; i--) {
                while (!stk.empty() && left[stk.top()][j] >= left[i][j]) {
                    stk.pop();
                }
                down[i] = stk.empty() ? m : stk.top();
                stk.push(i);
            }

            for (int i = 0; i < m; i++) {
                int height = down[i] - up[i] - 1;
                int area = height * left[i][j];
                ret = max(ret, area);
            }
        }
        return ret;
    }
};

###Java

class Solution {
    public int maximalRectangle(char[][] matrix) {
        int m = matrix.length;
        if (m == 0) {
            return 0;
        }
        int n = matrix[0].length;
        int[][] left = new int[m][n];

        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                if (matrix[i][j] == '1') {
                    left[i][j] = (j == 0 ? 0 : left[i][j - 1]) + 1;
                }
            }
        }

        int ret = 0;
        for (int j = 0; j < n; j++) { // 对于每一列,使用基于柱状图的方法
            int[] up = new int[m];
            int[] down = new int[m];

            Deque<Integer> stack = new ArrayDeque<Integer>();
            for (int i = 0; i < m; i++) {
                while (!stack.isEmpty() && left[stack.peek()][j] >= left[i][j]) {
                    stack.pop();
                }
                up[i] = stack.isEmpty() ? -1 : stack.peek();
                stack.push(i);
            }
            stack.clear();
            for (int i = m - 1; i >= 0; i--) {
                while (!stack.isEmpty() && left[stack.peek()][j] >= left[i][j]) {
                    stack.pop();
                }
                down[i] = stack.isEmpty() ? m : stack.peek();
                stack.push(i);
            }

            for (int i = 0; i < m; i++) {
                int height = down[i] - up[i] - 1;
                int area = height * left[i][j];
                ret = Math.max(ret, area);
            }
        }
        return ret;
    }
}

###JavaScript

var maximalRectangle = function(matrix) {
    const m = matrix.length;
    if (m === 0) {
        return 0;
    }
    const n = matrix[0].length;
    const left = new Array(m).fill(0).map(() => new Array(n).fill(0));

    for (let i = 0; i < m; i++) {
        for (let j = 0; j < n; j++) {
            if (matrix[i][j] === '1') {
                left[i][j] = (j === 0 ? 0 : left[i][j - 1]) + 1;
            }
        }
    }

    let ret = 0;
    for (let j = 0; j < n; j++) { // 对于每一列,使用基于柱状图的方法
        const up = new Array(m).fill(0);
        const down = new Array(m).fill(0);

        let stack = new Array();
        for (let i = 0; i < m; i++) {
            while (stack.length && left[stack[stack.length - 1]][j] >= left[i][j]) {
                stack.pop();
            }
            up[i] = stack.length === 0 ? -1 : stack[stack.length - 1];
            stack.push(i);
        }
        stack = [];
        for (let i = m - 1; i >= 0; i--) {
            while (stack.length && left[stack[stack.length - 1]][j] >= left[i][j]) {
                stack.pop();
            }
            down[i] = stack.length === 0 ? m : stack[stack.length - 1];
            stack.push(i);
        }

        for (let i = 0; i < m; i++) {
            const height = down[i] - up[i] - 1;
            const area = height * left[i][j];
            ret = Math.max(ret, area);
        }
    }
    return ret;
};

###go

func maximalRectangle(matrix [][]byte) (ans int) {
    if len(matrix) == 0 {
        return
    }
    m, n := len(matrix), len(matrix[0])
    left := make([][]int, m)
    for i, row := range matrix {
        left[i] = make([]int, n)
        for j, v := range row {
            if v == '0' {
                continue
            }
            if j == 0 {
                left[i][j] = 1
            } else {
                left[i][j] = left[i][j-1] + 1
            }
        }
    }
    for j := 0; j < n; j++ { // 对于每一列,使用基于柱状图的方法
        up := make([]int, m)
        down := make([]int, m)
        stk := []int{}
        for i, l := range left {
            for len(stk) > 0 && left[stk[len(stk)-1]][j] >= l[j] {
                stk = stk[:len(stk)-1]
            }
            up[i] = -1
            if len(stk) > 0 {
                up[i] = stk[len(stk)-1]
            }
            stk = append(stk, i)
        }
        stk = nil
        for i := m - 1; i >= 0; i-- {
            for len(stk) > 0 && left[stk[len(stk)-1]][j] >= left[i][j] {
                stk = stk[:len(stk)-1]
            }
            down[i] = m
            if len(stk) > 0 {
                down[i] = stk[len(stk)-1]
            }
            stk = append(stk, i)
        }
        for i, l := range left {
            height := down[i] - up[i] - 1
            area := height * l[j]
            ans = max(ans, area)
        }
    }
    return
}

func max(a, b int) int {
    if a > b {
        return a
    }
    return b
}

###C

int maximalRectangle(char** matrix, int matrixSize, int* matrixColSize) {
    int m = matrixSize;
    if (m == 0) {
        return 0;
    }
    int n = matrixColSize[0];
    int left[m][n];
    memset(left, 0, sizeof(left));
    for (int i = 0; i < m; i++) {
        for (int j = 0; j < n; j++) {
            if (matrix[i][j] == '1') {
                left[i][j] = (j == 0 ? 0 : left[i][j - 1]) + 1;
            }
        }
    }

    int ret = 0;
    for (int j = 0; j < n; j++) {  // 对于每一列,使用基于柱状图的方法
        int up[m], down[m];
        memset(up, 0, sizeof(up));
        memset(down, 0, sizeof(down));
        int stk[m], top = 0;
        for (int i = 0; i < m; i++) {
            while (top > 0 && left[stk[top - 1]][j] >= left[i][j]) {
                top--;
            }
            up[i] = top == 0 ? -1 : stk[top - 1];
            stk[top++] = i;
        }
        top = 0;
        for (int i = m - 1; i >= 0; i--) {
            while (top > 0 && left[stk[top - 1]][j] >= left[i][j]) {
                top--;
            }
            down[i] = top == 0 ? m : stk[top - 1];
            stk[top++] = i;
        }

        for (int i = 0; i < m; i++) {
            int height = down[i] - up[i] - 1;
            int area = height * left[i][j];
            ret = fmax(ret, area);
        }
    }
    return ret;
}

###C#

public class Solution {
    public int MaximalRectangle(char[][] matrix) {
        int m = matrix.Length;
        if (m == 0) return 0;
        int n = matrix[0].Length;
        int[][] left = new int[m][];
        for (int i = 0; i < m; i++) {
            left[i] = new int[n];
            for (int j = 0; j < n; j++) {
                if (matrix[i][j] == '1') {
                    left[i][j] = (j == 0 ? 0 : left[i][j - 1]) + 1;
                }
            }
        }

        int ret = 0;
        for (int j = 0; j < n; j++) {
            int[] up = new int[m];
            int[] down = new int[m];
            Stack<int> stk = new Stack<int>();

            for (int i = 0; i < m; i++) {
                while (stk.Count > 0 && left[stk.Peek()][j] >= left[i][j])
                    stk.Pop();
                up[i] = stk.Count == 0 ? -1 : stk.Peek();
                stk.Push(i);
            }

            stk.Clear();
            for (int i = m - 1; i >= 0; i--) {
                while (stk.Count > 0 && left[stk.Peek()][j] >= left[i][j])
                    stk.Pop();
                down[i] = stk.Count == 0 ? m : stk.Peek();
                stk.Push(i);
            }

            for (int i = 0; i < m; i++) {
                int height = down[i] - up[i] - 1;
                ret = Math.Max(ret, height * left[i][j]);
            }
        }
        return ret;
    }
}

###Python

class Solution:
    def maximalRectangle(self, matrix: List[List[str]]) -> int:
        if not matrix:
            return 0
        m, n = len(matrix), len(matrix[0])
        left = [[0] * n for _ in range(m)]

        for i in range(m):
            for j in range(n):
                if matrix[i][j] == '1':
                    left[i][j] = (0 if j == 0 else left[i][j - 1]) + 1

        ret = 0
        for j in range(n):
            up = [0] * m
            down = [0] * m
            stk = []

            for i in range(m):
                while stk and left[stk[-1]][j] >= left[i][j]:
                    stk.pop()
                up[i] = stk[-1] if stk else -1
                stk.append(i)

            stk = []
            for i in range(m - 1, -1, -1):
                while stk and left[stk[-1]][j] >= left[i][j]:
                    stk.pop()
                down[i] = stk[-1] if stk else m
                stk.append(i)

            for i in range(m):
                height = down[i] - up[i] - 1
                ret = max(ret, height * left[i][j])

        return ret

###TypeScript

function maximalRectangle(matrix: string[][]): number {
    const m = matrix.length;
    if (m === 0) {
        return 0;
    }
    const n = matrix[0].length;
    const left: number[][] = Array.from({ length: m }, () => Array(n).fill(0));

    for (let i = 0; i < m; i++) {
        for (let j = 0; j < n; j++) {
            if (matrix[i][j] === '1') {
                left[i][j] = (j === 0 ? 0 : left[i][j - 1]) + 1;
            }
        }
    }

    let ret = 0;
    for (let j = 0; j < n; j++) {
        const up: number[] = new Array(m);
        const down: number[] = new Array(m);
        const stk: number[] = [];

        for (let i = 0; i < m; i++) {
            while (stk.length && left[stk[stk.length - 1]][j] >= left[i][j]) {
                stk.pop();
            }
            up[i] = stk.length ? stk[stk.length - 1] : -1;
            stk.push(i);
        }

        stk.length = 0;
        for (let i = m - 1; i >= 0; i--) {
            while (stk.length && left[stk[stk.length - 1]][j] >= left[i][j]) {
                stk.pop();
            }
            down[i] = stk.length ? stk[stk.length - 1] : m;
            stk.push(i);
        }

        for (let i = 0; i < m; i++) {
            const height = down[i] - up[i] - 1;
            ret = Math.max(ret, height * left[i][j]);
        }
    }

    return ret;
}

###Rust

impl Solution {
    pub fn maximal_rectangle(matrix: Vec<Vec<char>>) -> i32 {
        let m = matrix.len();
        if m == 0 {
            return 0;
        }
        let n = matrix[0].len();
        let mut left = vec![vec![0; n]; m];

        for i in 0..m {
            for j in 0..n {
                if matrix[i][j] == '1' {
                    left[i][j] = if j == 0 { 0 } else { left[i][j - 1] } + 1;
                }
            }
        }

        let mut ret = 0;
        for j in 0..n {
            let mut up = vec![0; m];
            let mut down = vec![0; m];
            let mut stk: Vec<usize> = Vec::new();

            for i in 0..m {
                while let Some(&top) = stk.last() {
                    if left[top][j] >= left[i][j] {
                        stk.pop();
                    } else {
                        break;
                    }
                }
                up[i] = stk.last().map(|&x| x as i32).unwrap_or(-1);
                stk.push(i);
            }

            stk.clear();
            for i in (0..m).rev() {
                while let Some(&top) = stk.last() {
                    if left[top][j] >= left[i][j] {
                        stk.pop();
                    } else {
                        break;
                    }
                }
                down[i] = stk.last().copied().unwrap_or(m);
                stk.push(i);
            }

            for i in 0..m {
                let height = down[i] - up[i] as usize - 1;
                ret = ret.max(height * left[i][j]);
            }
        }

        ret as i32
    }
}

读者可以自行比对上面的代码与此前第 84 题的代码的相似之处。

复杂度分析

  • 时间复杂度:$O(mn)$,其中 $m$ 和 $n$ 分别是矩阵的行数和列数。计算 $\textit{left}$ 矩阵需要 $O(mn)$ 的时间;对每一列应用柱状图算法需要 $O(m)$ 的时间,一共需要 $O(mn)$ 的时间。

  • 空间复杂度:$O(mn)$,其中 $m$ 和 $n$ 分别是矩阵的行数和列数。我们分配了一个与给定矩阵等大的数组,用于存储每个元素的左边连续 $1$ 的数量。

详细通俗的思路分析,多解法

作者 windliang
2019年6月18日 18:36

题目描述(困难难度)

image.png

给一个只有 0 和 1 的矩阵,输出一个最大的矩形的面积,这个矩形里边只含有 1。

解法一 暴力破解

遍历每个点,求以这个点为矩阵右下角的所有矩阵面积。如下图的两个例子,橙色是当前遍历的点,然后虚线框圈出的矩阵是其中一个矩阵。

image.png

怎么找出这样的矩阵呢?如下图,如果我们知道了以这个点结尾的连续 1 的个数的话,问题就变得简单了。

image.png

  1. 首先求出高度是 1 的矩形面积,也就是它自身的数,如图中橙色的 4,面积就是 4。

  2. 然后向上扩展一行,高度增加一,选出当前列最小的数字,作为矩阵的宽,求出面积,对应上图的矩形框。

  3. 然后继续向上扩展,重复步骤 2。

按照上边的方法,遍历所有的点,求出所有的矩阵就可以了。

以橙色的点为右下角,高度为 1。

image.png

高度为 2。

image.png

高度为 3。

image.png

###java

public int maximalRectangle(char[][] matrix) {
    if (matrix.length == 0) {
        return 0;
    }
    //保存以当前数字结尾的连续 1 的个数
    int[][] width = new int[matrix.length][matrix[0].length];
    int maxArea = 0;
    //遍历每一行
    for (int row = 0; row < matrix.length; row++) {
        for (int col = 0; col < matrix[0].length; col++) {
            //更新 width
            if (matrix[row][col] == '1') {
                if (col == 0) {
                    width[row][col] = 1;
                } else {
                    width[row][col] = width[row][col - 1] + 1;
                }
            } else {
                width[row][col] = 0;
            }
            //记录所有行中最小的数
            int minWidth = width[row][col];
            //向上扩展行
            for (int up_row = row; up_row >= 0; up_row--) {
                int height = row - up_row + 1;
                //找最小的数作为矩阵的宽
                minWidth = Math.min(minWidth, width[up_row][col]);
                //更新面积
                maxArea = Math.max(maxArea, height * minWidth);
            }
        }
    }
    return maxArea;
}

时间复杂度:O(m²n)。

空间复杂度:O(mn)。

解法二

接下来的解法,会让这道题变得异常简单。还记得84 题吗?求一个直方图矩形的最大面积。

image.png

大家可以先做84 题,然后回来考虑这道题。

再想一下这个题,看下边的橙色的部分,这完全就是上一道题呀!

image.png

算法有了,就是求出每一层的 heights[] 然后传给上一题的函数就可以了。

利用上一题的栈解法。

###java

public int maximalRectangle(char[][] matrix) {
    if (matrix.length == 0) {
        return 0;
    }
    int[] heights = new int[matrix[0].length];
    int maxArea = 0;
    for (int row = 0; row < matrix.length; row++) {
        //遍历每一列,更新高度
        for (int col = 0; col < matrix[0].length; col++) {
            if (matrix[row][col] == '1') {
                heights[col] += 1;
            } else {
                heights[col] = 0;
            }
        }
        //调用上一题的解法,更新函数
        maxArea = Math.max(maxArea, largestRectangleArea(heights));
    }
    return maxArea;
}

public int largestRectangleArea(int[] heights) {
    int maxArea = 0;
    Stack<Integer> stack = new Stack<>();
    int p = 0;
    while (p < heights.length) {
        //栈空入栈
        if (stack.isEmpty()) {
            stack.push(p);
            p++;
        } else {
            int top = stack.peek();
            //当前高度大于栈顶,入栈
            if (heights[p] >= heights[top]) {
                stack.push(p);
                p++;
            } else {
                //保存栈顶高度
                int height = heights[stack.pop()];
                //左边第一个小于当前柱子的下标
                int leftLessMin = stack.isEmpty() ? -1 : stack.peek();
                //右边第一个小于当前柱子的下标
                int RightLessMin = p;
                //计算面积
                int area = (RightLessMin - leftLessMin - 1) * height;
                maxArea = Math.max(area, maxArea);
            }
        }
    }
    while (!stack.isEmpty()) {
        //保存栈顶高度
        int height = heights[stack.pop()];
        //左边第一个小于当前柱子的下标
        int leftLessMin = stack.isEmpty() ? -1 : stack.peek();
        //右边没有小于当前高度的柱子,所以赋值为数组的长度便于计算
        int RightLessMin = heights.length;
        int area = (RightLessMin - leftLessMin - 1) * height;
        maxArea = Math.max(area, maxArea);
    }
    return maxArea;
}

时间复杂度:O(mn)。

空间复杂度:O(n)。

利用上一题的解法四。

###java

public int maximalRectangle(char[][] matrix) {
    if (matrix.length == 0) {
        return 0;
    }
    int[] heights = new int[matrix[0].length];
    int maxArea = 0;
    for (int row = 0; row < matrix.length; row++) {
        //遍历每一列,更新高度
        for (int col = 0; col < matrix[0].length; col++) {
            if (matrix[row][col] == '1') {
                heights[col] += 1;
            } else {
                heights[col] = 0;
            }
        }
        //调用上一题的解法,更新函数
        maxArea = Math.max(maxArea, largestRectangleArea(heights));
    }
    return maxArea;
}

public int largestRectangleArea(int[] heights) {
    if (heights.length == 0) {
        return 0;
    }
    int[] leftLessMin = new int[heights.length];
    leftLessMin[0] = -1;
    for (int i = 1; i < heights.length; i++) {
        int l = i - 1;
        while (l >= 0 && heights[l] >= heights[i]) {
            l = leftLessMin[l];
        }
        leftLessMin[i] = l;
    }

    int[] rightLessMin = new int[heights.length];
    rightLessMin[heights.length - 1] = heights.length;
    for (int i = heights.length - 2; i >= 0; i--) {
        int r = i + 1;
        while (r <= heights.length - 1 && heights[r] >= heights[i]) {
            r = rightLessMin[r];
        }
        rightLessMin[i] = r;
    }
    int maxArea = 0;
    for (int i = 0; i < heights.length; i++) {
        int area = (rightLessMin[i] - leftLessMin[i] - 1) * heights[i];
        maxArea = Math.max(area, maxArea);
    }
    return maxArea;
}

时间复杂度:O(mn)。

空间复杂度:O(n)。

解法三

解法二中套用的栈的解法,我们其实可以不用调用函数,而是把栈糅合到原来求 heights 中。因为栈的话并不是一次性需要所有的高度,所以可以求出一个高度,然后就操作栈。

###java

public int maximalRectangle(char[][] matrix) {
    if (matrix.length == 0) {
        return 0;
    }
    int[] heights = new int[matrix[0].length + 1]; //小技巧后边讲
    int maxArea = 0;
    for (int row = 0; row < matrix.length; row++) {
        Stack<Integer> stack = new Stack<Integer>();
        heights[matrix[0].length] = 0;
        //每求一个高度就进行栈的操作
        for (int col = 0; col <= matrix[0].length; col++) {
            if (col < matrix[0].length) { //多申请了 1 个元素,所以要判断
                if (matrix[row][col] == '1') {
                    heights[col] += 1;
                } else {
                    heights[col] = 0;
                }
            }
            if (stack.isEmpty() || heights[col] >= heights[stack.peek()]) {
                stack.push(col);
            } else {
                //每次要判断新的栈顶是否高于当前元素
                while (!stack.isEmpty() && heights[col] < heights[stack.peek()]) {
                    int height = heights[stack.pop()];
                    int leftLessMin = stack.isEmpty() ? -1 : stack.peek();
                    int RightLessMin = col;
                    int area = (RightLessMin - leftLessMin - 1) * height;
                    maxArea = Math.max(area, maxArea);
                }
                stack.push(col);
            }
        }

    }
    return maxArea;
}

时间复杂度:O(mn)。

空间复杂度:O(n)。

里边有一个小技巧,84 题的栈解法中,我们用了两个 while 循环,第二个 while 循环用来解决遍历完元素栈不空的情况。其实,我们注意到两个 while 循环的逻辑完全一样的。所以我们可以通过一些操作,使得遍历结束后,依旧进第一个 while 循环,从而剩下了第 2 个 while 循环,代码看起来会更简洁。

那就是 heights 多申请一个元素,赋值为 0。这样最后一次遍历的时候,栈顶肯定会大于当前元素,所以就进入了第一个 while 循环。

解法四 动态规划

解法二中,用了 84 题的两个解法,解法三中我们把栈糅合进了原算法,那么另一种可以一样的思路吗?不行!因为栈不要求所有的高度,可以边更新,边处理。而另一种,是利用两个数组, leftLessMin [ ] 和 rightLessMin [ ]。而这两个数组的更新,是需要所有高度的。

解法二中,我们更新一次 heights,就利用之前的算法,求一遍 leftLessMin [ ] 和 rightLessMin [ ],然后更新面积。而其实,我们求 leftLessMin [ ] 和 rightLessMin [ ] 可以利用之前的 leftLessMin [ ] 和 rightLessMin [ ] 来更新本次的。

我们回想一下 leftLessMin [ ] 和 rightLessMin [ ] 的含义, leftLessMin [ i ] 代表左边第一个比当前柱子矮的下标,如下图橙色柱子时当前遍历的柱子。rightLessMin [ ] 时右边第一个。

image.png

left 和 right 是对称关系,下边只考虑 left 的求法。

如下图,如果当前新增的层全部是 1,当然这时最完美的情况,那么 leftLessMin [ ] 根本不需要改变。

image.png

然而事实是残酷的,一定会有 0 的出现。

image.png

我们考虑最后一个柱子的更新。上一层的 leftLessMin = 1,也就是蓝色 0 的位置是第一个比它低的柱子。但是在当前层,由于中间出现了 0。所以不再是之前的 leftLessMin ,而是和上次出现 0 的位置进行比较(因为 0 一定比当前柱子小),谁的下标大,更接近当前柱子,就选择谁。上图中出现 0 的位置是 2,之前的 leftLessMin 是 1,选一个较大的,那就是 2 了。

###java

public int maximalRectangle4(char[][] matrix) {
    if (matrix.length == 0) {
        return 0;
    }
    int maxArea = 0;
    int cols = matrix[0].length;
    int[] leftLessMin = new int[cols];
    int[] rightLessMin = new int[cols];
    Arrays.fill(leftLessMin, -1); //初始化为 -1,也就是最左边
    Arrays.fill(rightLessMin, cols); //初始化为 cols,也就是最右边
    int[] heights = new int[cols];
    for (int row = 0; row < matrix.length; row++) {
        //更新所有高度
        for (int col = 0; col < cols; col++) {
            if (matrix[row][col] == '1') {
                heights[col] += 1;
            } else {
                heights[col] = 0;
            }
        }
//更新所有leftLessMin
        int boundary = -1; //记录上次出现 0 的位置
        for (int col = 0; col < cols; col++) {
            if (matrix[row][col] == '1') {
                //和上次出现 0 的位置比较
                leftLessMin[col] = Math.max(leftLessMin[col], boundary);
            } else {
                //当前是 0 代表当前高度是 0,所以初始化为 -1,防止对下次循环的影响
                leftLessMin[col] = -1; 
                //更新 0 的位置
                boundary = col;
            }
        }
        //右边同理
        boundary = cols;
        for (int col = cols - 1; col >= 0; col--) {
            if (matrix[row][col] == '1') {
                rightLessMin[col] = Math.min(rightLessMin[col], boundary);
            } else {
                rightLessMin[col] = cols;
                boundary = col;
            }
        }

        //更新所有面积
        for (int col = cols - 1; col >= 0; col--) {
            int area = (rightLessMin[col] - leftLessMin[col] - 1) * heights[col];
            maxArea = Math.max(area, maxArea);
        }

    }
    return maxArea;

}

时间复杂度:O(mn)。

空间复杂度:O(n)。

有时候,如果可以把题抽象到已解决的问题当中去,可以大大的简化问题,很酷!

❌
❌