LeetCode/211-220/215. 数组中的第K个最大元素（中等）.md

215. 数组中的第K个最大元素

Question:
给定整数数组 `nums` 和整数 `k`,请返回数组中第 `k` 个最大的元素。

请注意,你需要找的是数组排序后的第 `k` 个最大的元素,而不是第 `k` 个不同的元素。

你必须设计并实现时间复杂度为 $O(n)$ 的算法解决此问题。

示例 1:
```
输入: [3,2,1,5,6,4], k = 2

输出: 5
```
示例 2:
```
输入: [3,2,3,1,2,4,5,5,6], k = 4

输出: 4
```

提示:
* $1 <= k <= nums.length <= 10^5$
* $-10^4 <= nums[i] <= 10^4$

Solution:
### 值域映射 + 树状数组 + 二分

除了直接对数组进行排序,取第 $k$ 位的 $O(n\log{n})$ 做法以外。

对于值域大小 小于 数组长度本身时,我们还能使用「树状数组 + 二分」的 $O(n\log{m})$ 做法,其中 $m$ 为值域大小。

首先值域大小为 $[-10^4, 10^4]$,为了方便,我们为每个 $nums[i]$ 增加大小为 $1e4 + 10$ 的偏移量,将值域映射到 $[10, 2 \times 10^4 + 10]$ 的空间。

将每个增加偏移量后的 $nums[i]$ 存入树状数组,考虑在 $[0, m)$ 范围内进行二分,假设我们真实第 $k$ 大的值为 $t$,那么在以 $t$ 为分割点的数轴上,具有二段性质:

* 在 $[0, t]$ 范围内的数 $cur$ 满足「树状数组中大于等于 $cur$ 的数不低于 $k$ 个」
* 在 $(t, m)$ 范围内的数 $cur$ 不满足「树状数组中大于等于 $cur$ 的数不低于 $k$ 个」

二分出结果后再减去刚开始添加的偏移量即是答案。

Java 代码:
```Java
class Solution {
    int M = 100010, N = 2 * M;
    int[] tr = new int[N];
    int lowbit(int x) {
        return x & -x;
    }
    int query(int x) {
        int ans = 0;
        for (int i = x; i > 0; i -= lowbit(i)) ans += tr[i];
        return ans;
    }
    void add(int x) {
        for (int i = x; i < N; i += lowbit(i)) tr[i]++;
    }
    public int findKthLargest(int[] nums, int k) {
        for (int x : nums) add(x + M);
        int l = 0, r = N - 1;
        while (l < r) {
            int mid = l + r + 1 >> 1;
            if (query(N - 1) - query(mid - 1) >= k) l = mid;
            else r = mid - 1;
        }
        return r - M;
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    int N = 200010, M = 100010, tr[200010];
    int lowbit(int x) {
        return x & -x;
    }
    int query(int x) {
        int ans = 0;
        for (int i = x; i > 0; i -= lowbit(i)) ans += tr[i];
        return ans;
    }
    void add(int x) {
        for (int i = x; i < N; i += lowbit(i)) tr[i]++;
    }
    int findKthLargest(vector<int>& nums, int k) {
        for (int x : nums) add(x + M);
        int l = 0, r = N - 1;
        while (l < r) {
            int mid = l + r + 1 >> 1;
            if (query(N - 1) - query(mid - 1) >= k) l = mid;
            else r = mid - 1;
        }
        return r - M;
    }
};
```
Python 代码:
```Python
class Solution:
    def findKthLargest(self, nums: List[int], k: int) -> int:
        N, M = 200010, 100010
        tr = [0] * N
        def lowbit(x):
            return x & -x
        def query(x):
            ans = 0
            i = x
            while i > 0:
                ans += tr[i]
                i -= lowbit(i)
            return ans
        def add(x):
            i = x
            while i < N:
                tr[i] += 1
                i += lowbit(i)
        for x in nums:
            add(x + M)
        l, r = 0, N - 1
        while l < r:
            mid = l + r + 1 >> 1
            if query(N - 1) - query(mid - 1) >= k: l = mid
            else: r = mid - 1
        return r - M
```
TypeScript 代码:
```TypeScript
function findKthLargest(nums: number[], k: number): number {
    const N = 200010, M = 100010;
    const tr = new Array(N).fill(0);
    const lowbit = function(x: number): number {
        return x & -x;
    };
    const add = function(x: number): void {
        for (let i = x; i < N; i += lowbit(i)) tr[i]++;
    };
    const query = function(x: number): number {
        let ans = 0;
        for (let i = x; i > 0; i -= lowbit(i)) ans += tr[i];
        return ans;
    };
    for (const x of nums) add(x + M);
    let l = 0, r = N - 1;
    while (l < r) {
        const mid = l + r + 1 >> 1;
        if (query(N - 1) - query(mid - 1) >= k) l = mid;
        else r = mid - 1;
    }
    return r - M;
};
```
* 时间复杂度:将所有数字放入树状数组复杂度为 $O(n\log{m})$;二分出答案复杂度为 $O(\log^2{m})$,其中 $m = 2 \times 10^4$ 为值域大小。整体复杂度为 $O(n\log{m})$
* 空间复杂度:$O(m)$

---

### 优先队列(堆)

另外一个容易想到的想法是利用优先队列(堆),由于题目要我们求的是第 $k$ 大的元素,因此我们建立一个小根堆。

根据当前队列元素个数或当前元素与栈顶元素的大小关系进行分情况讨论:

* 当优先队列元素不足 $k$ 个,可将当前元素直接放入队列中;
* 当优先队列元素达到 $k$ 个,并且当前元素大于栈顶元素(栈顶元素必然不是答案),可将当前元素放入队列中。

Java 代码:
```Java
class Solution {
    public int findKthLargest(int[] nums, int k) {
        PriorityQueue<Integer> q = new PriorityQueue<>((a,b)->a-b);
        for (int x : nums) {
            if (q.size() < k || q.peek() < x) q.add(x);
            if (q.size() > k) q.poll();
        }
        return q.peek();
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    int findKthLargest(vector<int>& nums, int k) {
        priority_queue<int, vector<int>, greater<int>> q;
        for (int x : nums) {
            if (q.size() < k || q.top() < x) q.push(x);
            if (q.size() > k) q.pop();
        }
        return q.top();
    }
};
```
Python 代码:
```Python
class Solution:
    def findKthLargest(self, nums: List[int], k: int) -> int:
        q = []
        for x in nums:
            if len(q) < k or q[0] < x:
                heapq.heappush(q, x)
            if len(q) > k:
                heapq.heappop(q)
        return q[0]
```
* 时间复杂度:$O(n\log{k})$
* 空间复杂度:$O(k)$

---

### 快速选择

对于给定数组,求解第 $k$ 大元素,且要求线性复杂度,正解为使用「快速选择」做法。

基本思路与「快速排序」一致,每次敲定一个基准值 `x`,根据当前与 `x` 的大小关系,将范围在 $[l, r]$ 的 $nums[i]$ 划分为到两边。

同时利用,利用题目只要求输出第 $k$ 大的值,而不需要对数组进行整体排序,我们只需要根据划分两边后,第 $k$ 大数会落在哪一边,来决定对哪边进行递归处理即可。

> 快速排序模板为面试向重点内容,需要重要掌握。

Java 代码:
```Java
class Solution {
    int[] nums;
    int qselect(int l, int r, int k) {
        if (l == r) return nums[k];
        int x = nums[l], i = l - 1, j = r + 1;
        while (i < j) {
            do i++; while (nums[i] < x);
            do j--; while (nums[j] > x);
            if (i < j) swap(i, j);
        }
        if (k <= j) return qselect(l, j, k);
        else return qselect(j + 1, r, k);
    }
    void swap(int i, int j) {
        int c = nums[i];
        nums[i] = nums[j];
        nums[j] = c;
    }
    public int findKthLargest(int[] _nums, int k) {
        nums = _nums;
        int n = nums.length;
        return qselect(0, n - 1, n - k);
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    vector<int> nums;
    int qselect(int l, int r, int k) {
        if (l == r) return nums[k];
        int x = nums[l], i = l - 1, j = r + 1;
        while (i < j) {
            do i++; while (nums[i] < x);
            do j--; while (nums[j] > x);
            if (i < j) swap(nums[i], nums[j]);
        }
        if (k <= j) return qselect(l, j, k);
        else return qselect(j + 1, r, k);
    }
    int findKthLargest(vector<int>& _nums, int k) {
        nums = _nums;
        int n = nums.size();
        return qselect(0, n - 1, n - k);
    }
};
```
Python 代码:
```Python
class Solution:
    def findKthLargest(self, nums: List[int], k: int) -> int:
        def qselect(l, r, k):
            if l == r:
                return nums[k]
            x, i, j = nums[l], l - 1, r + 1
            while i < j:
                i += 1
                while nums[i] < x:
                    i += 1
                j -= 1
                while nums[j] > x:
                    j -= 1
                if i < j:
                    nums[i], nums[j] = nums[j], nums[i]
            if k <= j:
                return qselect(l, j, k)
            else:
                return qselect(j + 1, r, k)

        n = len(nums)
        return qselect(0, n - 1, n - k)
```
TypeScript 代码:
```TypeScript
function findKthLargest(nums: number[], k: number): number {
    const qselect = function(l: number, r: number, k: number): number {
        if (l === r) return nums[k];
        const x = nums[l];
        let i = l - 1, j = r + 1;
        while (i < j) {
            i++;
            while (nums[i] < x) i++;
            j--;
            while (nums[j] > x) j--;
            if (i < j) [nums[i], nums[j]] = [nums[j], nums[i]];
        }
        if (k <= j) return qselect(l, j, k);
        else return qselect(j + 1, r, k);
    };
    const n = nums.length;
    return qselect(0, n - 1, n - k);
};
```
* 时间复杂度:期望 $O(n)$
* 空间复杂度:忽略递归带来的额外空间开销,复杂度为 $O(1)$

### 最后

这是我们「刷穿 LeetCode」系列文章的第 `No.215` 篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先把所有不带锁的题目刷完。

在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。

为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:https://github.com/SharingSource/LogicStack-LeetCode 。

在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。


Question:
城市的天际线是从远处观看该城市中所有建筑物形成的轮廓的外部轮廓。

给你所有建筑物的位置和高度,请返回由这些建筑物形成的 天际线 。

每个建筑物的几何信息由数组 `buildings` 表示,其中三元组 `buildings[i] = [lefti, righti, heighti]` 表示:

* `left[i]` 是第 `i` 座建筑物左边缘的 `x` 坐标。
* `right[i]` 是第 `i` 座建筑物右边缘的 `x` 坐标。
* `height[i]` 是第 `i` 座建筑物的高度。

天际线 应该表示为由 “关键点” 组成的列表,格式 `[[x1,y1],[x2,y2],...]`,并按 `x` 坐标 进行 排序 。

关键点是水平线段的左端点。列表中最后一个点是最右侧建筑物的终点,`y` 坐标始终为 $0$ ,仅用于标记天际线的终点。

此外,任何两个相邻建筑物之间的地面都应被视为天际线轮廓的一部分。

注意:输出天际线中不得有连续的相同高度的水平线。例如  `[...[2 3], [4 5], [7 5], [11 5], [12 7]...]` 是不正确的答案;三条高度为 $5$ 的线应该在最终输出中合并为一个:`[...[2 3], [4 5], [12 7], ...]`

示例 1:

```
输入:buildings = [[2,9,10],[3,7,15],[5,12,12],[15,20,10],[19,24,8]]

输出:[[2,10],[3,15],[7,12],[12,0],[15,10],[20,8],[24,0]]

解释:
图 A 显示输入的所有建筑物的位置和高度,
图 B 显示由这些建筑物形成的天际线。图 B 中的红点表示输出列表中的关键点。
```
示例 2:
```
输入:buildings = [[0,2,3],[2,5,3]]

输出:[[0,3],[5,0]]
```

提示:
* $1 <= buildings.length <= 10^4$
* $0 <= lefti < righti <= 2^{31} - 1$
* $1 <= heighti <= 2^{31} - 1$
* `buildings` 按 $left_i$ 非递减排序

Solution:
### 基本分析

这是一题特别的扫描线问题 🤣🤣🤣

既不是求周长,也不是求面积,是求轮廓中的所有的水平线的左端点 🤣🤣🤣

所以这不是一道必须用「线段树」来解决的扫描线问题(因为不需要考虑区间查询问题)。

扫描线的核心在于 **将不规则的形状按照水平或者垂直的方式,划分成若干个规则的矩形。**

---

### 扫描线

对于本题,对应的扫描线分割形状如图:

不难发现,由相邻两个横坐标以及最大高度,可以确定一个矩形。

题目要我们 **输出每个矩形的“上边”的左端点,同时跳过可由前一矩形“上边”延展而来的那些边。**

因此我们需要实时维护一个最大高度,可以使用优先队列(堆)。

实现时,我们可以先记录下 $buildings$ 中所有的左右端点横坐标及高度,并根据端点横坐标进行从小到大排序。

在从前往后遍历处理时(遍历每个矩形),根据当前遍历到的点进行分情况讨论:

* 左端点:因为是左端点,必然存在一条从右延展的边,但不一定是需要被记录的边,因为在同一矩形中,我们只需要记录最上边的边。这时候可以将高度进行入队;

* 右端点:此时意味着之前某一条往右延展的线结束了,这时候需要将高度出队(代表这结束的线不被考虑)。

然后从优先队列中取出当前的最大高度,为了防止当前的线与前一矩形“上边”延展而来的线重合,我们需要使用一个变量 `prev` 记录上一个记录的高度。

代码:
```Java
class Solution {
    public List<List<Integer>> getSkyline(int[][] bs) {
        List<List<Integer>> ans = new ArrayList<>();

        // 预处理所有的点,为了方便排序,对于左端点,令高度为负;对于右端点令高度为正
        List<int[]> ps = new ArrayList<>();
        for (int[] b : bs) {
            int l = b[0], r = b[1], h = b[2];
            ps.add(new int[]{l, -h});
            ps.add(new int[]{r, h});
        }

        // 先按照横坐标进行排序
        // 如果横坐标相同,则按照左端点排序
        // 如果相同的左/右端点,则按照高度进行排序
        Collections.sort(ps, (a, b)->{
            if (a[0] != b[0]) return a[0] - b[0];
            return a[1] - b[1];
        });

        // 大根堆
        PriorityQueue<Integer> q = new PriorityQueue<>((a,b)->b-a);
        int prev = 0;
        q.add(prev);
        for (int[] p : ps) {
            int point = p[0], height = p[1];
            if (height < 0) {
                // 如果是左端点,说明存在一条往右延伸的可记录的边,将高度存入优先队列
                q.add(-height);
            } else {
                // 如果是右端点,说明这条边结束了,将当前高度从队列中移除
                q.remove(height);
            }

            // 取出最高高度,如果当前不与前一矩形“上边”延展而来的那些边重合,则可以被记录
            int cur = q.peek();
            if (cur != prev) {
                List<Integer> list = new ArrayList<>();
                list.add(point);
                list.add(cur);
                ans.add(list);
                prev = cur;
            }
        }
        return ans;
    }
}
```
* 时间复杂度:需要处理的矩阵数量与 $n$ 正比,每个矩阵需要使用优先队列维护高度,其中 `remove` 操作需要先花费 $O(n)$ 复杂度进行查找,然后通过 $O(\log{n})$ 复杂度进行移除,复杂度为 $O(n)$。整体复杂度为 $O(n^2)$
* 空间复杂度:$O(n)$

---

### 答疑

**1. 将左端点的高度存成负数再进行排序是什么意思?**

这里只是为了方便,所以采取了这样的做法,当然也能够多使用一位来代指「左右」。

只要最终可以达到如下的排序规则即可:

1. 先严格按照横坐标进行「从小到大」排序
2. 对于某个横坐标而言,可能会同时出现多个点,应当按照如下规则进行处理:
    1. 优先处理左端点,再处理右端点
    2. 如果同样都是左端点,则按照高度「从大到小」进行处理(将高度增加到优先队列中)
    3. 如果同样都是右端点,则按照高度「从小到大」进行处理(将高度从优先队列中删掉)

代码:
```Java
class Solution {
    public List<List<Integer>> getSkyline(int[][] bs) {
        List<List<Integer>> ans = new ArrayList<>();
        List<int[]> ps = new ArrayList<>();
        for (int[] b : bs) {
            int l = b[0], r = b[1], h = b[2];
            ps.add(new int[]{l, h, -1});
            ps.add(new int[]{r, h, 1});
        }
        /**
         * 先严格按照横坐标进行「从小到大」排序
         * 对于某个横坐标而言,可能会同时出现多个点,应当按照如下规则进行处理:
         * 1. 优先处理左端点,再处理右端点
         * 2. 如果同样都是左端点,则按照高度「从大到小」进行处理(将高度增加到优先队列中)
         * 3. 如果同样都是右端点,则按照高度「从小到大」进行处理(将高度从优先队列中删掉)
         */
        Collections.sort(ps, (a, b)->{
            if (a[0] != b[0]) return a[0] - b[0];
            if (a[2] != b[2]) return a[2] - b[2];
            if (a[2] == -1) {
                return b[1] - a[1];
            } else {
                return a[1] - b[1];
            }
        });
        PriorityQueue<Integer> q = new PriorityQueue<>((a,b)->b-a);
        int prev = 0;
        q.add(prev);
        for (int[] p : ps) {
            int point = p[0], height = p[1], flag = p[2];
            if (flag == -1) {
                q.add(height);
            } else {
                q.remove(height);
            }

            int cur = q.peek();
            if (cur != prev) {
                List<Integer> list = new ArrayList<>();
                list.add(point);
                list.add(cur);
                ans.add(list);
                prev = cur;
            }
        }
        return ans;
    }
}
```

**2. 为什么在处理前,先往「优先队列」添加一个 $0$ ?**

因为题目本身要求我们把一个完整轮廓的「右下角」那个点也取到,所以需要先添加一个 $0$。

也就是下图被圈出来的那些点:

**3. 优先队列的 `remove` 操作成为了瓶颈,如何优化?**

由于优先队列的 `remove` 操作需要先经过 $O(n)$ 的复杂度进行查找,再通过 $O(\log{n})$ 的复杂度进行删除。因此整个 `remove` 操作的复杂度是 $O(n)$ 的,这导致了我们算法整体复杂度为 $O(n^2)$。

优化方式包括:使用基于红黑树的 `TreeMap` 代替优先队列;或是使用「哈希表」记录「执行了删除操作的高度」及「删除次数」,在每次使用前先检查堆顶高度是否已经被标记删除,如果是则进行 `poll` 操作,并更新删除次数,直到遇到一个没被删除的堆顶高度。

代码:
```Java
class Solution {
    public List<List<Integer>> getSkyline(int[][] bs) {
        List<List<Integer>> ans = new ArrayList<>();
        List<int[]> ps = new ArrayList<>();
        for (int[] b : bs) {
            int l = b[0], r = b[1], h = b[2];
            ps.add(new int[]{l, h, -1});
            ps.add(new int[]{r, h, 1});
        }
        /**
         * 先严格按照横坐标进行「从小到大」排序
         * 对于某个横坐标而言,可能会同时出现多个点,应当按照如下规则进行处理:
         * 1. 优先处理左端点,再处理右端点
         * 2. 如果同样都是左端点,则按照高度「从大到小」进行处理(将高度增加到优先队列中)
         * 3. 如果同样都是右端点,则按照高度「从小到大」进行处理(将高度从优先队列中删掉)
         */
        Collections.sort(ps, (a, b)->{
            if (a[0] != b[0]) return a[0] - b[0];
            if (a[2] != b[2]) return a[2] - b[2];
            if (a[2] == -1) {
                return b[1] - a[1];
            } else {
                return a[1] - b[1];
            }
        });
        // 记录进行了删除操作的高度,以及删除次数
        Map<Integer, Integer> map = new HashMap<>();
        PriorityQueue<Integer> q = new PriorityQueue<>((a,b)->b-a);
        int prev = 0;
        q.add(prev);
        for (int[] p : ps) {
            int point = p[0], height = p[1], flag = p[2];
            if (flag == -1) {
                q.add(height);
            } else {
                map.put(height, map.getOrDefault(height, 0) + 1);
            }

            while (!q.isEmpty()) {
                int peek = q.peek();
                if (map.containsKey(peek)) {
                    if (map.get(peek) == 1) map.remove(peek);
                    else map.put(peek, map.get(peek) - 1);
                    q.poll();
                } else {
                    break;
                }
            }

            int cur = q.peek();
            if (cur != prev) {
                List<Integer> list = new ArrayList<>();
                list.add(point);
                list.add(cur);
                ans.add(list);
                prev = cur;
            }
        }
        return ans;
    }
}
```
* 时间复杂度:$O(n\log{n})$
* 空间复杂度:$O(n)$

### 最后

这是我们「刷穿 LeetCode」系列文章的第 `No.218` 篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先把所有不带锁的题目刷完。

在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。

为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:https://github.com/SharingSource/LogicStack-LeetCode 。

在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。


219. 存在重复元素 II

Question:
给你一个整数数组 `nums` 和一个整数 `k` ,判断数组中是否存在两个 不同的索引 `i` 和 `j` ,满足 `nums[i] == nums[j]` 且 `abs(i - j) <= k` 。如果存在,返回 `true` ;否则,返回 `false` 。

示例 1:
```
输入:nums = [1,2,3,1], k = 3

输出:true
```
示例 2:
```
输入:nums = [1,0,1,1], k = 1

输出:true
```
示例 3:
```
输入:nums = [1,2,3,1,2,3], k = 2

输出:false
```

提示:
* $1 <= nums.length <= 10^5$
* $-10^9 <= nums[i] <= 10^9$
* $0 <= k <= 10^5$

Solution:
### 滑动窗口 + 哈希表

整理题意:是否存在长度不超过的 $k + 1$ 窗口,窗口内有相同元素。

我们可以从前往后遍历 $nums$,同时使用 `Set` 记录遍历当前滑窗内出现过的元素。

假设当前遍历的元素为 $nums[i]$:

* 下标小于等于 $k$(起始滑窗长度还不足 $k + 1$):直接往滑窗加数,即将当前元素加入 `Set` 中;
* 下标大于 $k$:将上一滑窗的左端点元素 $nums[i - k - 1]$ 移除,判断当前滑窗的右端点元素 $nums[i]$ 是否存在 `Set` 中,若存在,返回 `True`,否则将当前元素 $nums[i]$ 加入 `Set` 中。

重复上述过程,若整个 $nums$ 处理完后仍未找到,返回 `False`。

**代码(感谢 [@Benhao](/u/himymben/) 同学提供的其他语言版本):**
```Java
class Solution {
    public boolean containsNearbyDuplicate(int[] nums, int k) {
        int n = nums.length;
        Set<Integer> set = new HashSet<>();
        for (int i = 0; i < n; i++) {
            if (i > k) set.remove(nums[i - k - 1]);
            if (set.contains(nums[i])) return true;
            set.add(nums[i]);
        }
        return false;
    }
}
```
-
```Python3
class Solution:
    def containsNearbyDuplicate(self, nums: List[int], k: int) -> bool:
        n = len(nums)
        s = set()
        for i in range(n):
            if i > k:
                s.remove(nums[i - k - 1])
            if nums[i] in s:
                return True
            s.add(nums[i])
        return False
````
-
```Go
func containsNearbyDuplicate(nums []int, k int) bool {
    n := len(nums)
    set := map[int]bool{}
    for i := 0; i < n; i++ {
        if i > k {
            set[nums[i - k - 1]] = false
        }
        if set[nums[i]] {
            return true
        }
        set[nums[i]] = true
    }
    return false
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(k)$

### 最后

这是我们「刷穿 LeetCode」系列文章的第 `No.219` 篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先把所有不带锁的题目刷完。

在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。

为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:https://github.com/SharingSource/LogicStack-LeetCode 。

在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。


Question:
给你一个整数数组 nums 和两个整数 k 和 t 。

请你判断是否存在 两个不同下标 i 和 j,使得 abs(nums[i] - nums[j]) <= t ,同时又满足 abs(i - j) <= k 。

如果存在则返回 true,不存在返回 false。

示例 1:
```
输入:nums = [1,2,3,1], k = 3, t = 0

输出:true
```
示例 2:
```
输入:nums = [1,0,1,1], k = 1, t = 2

输出:true
```
示例 3:
```
输入:nums = [1,5,9,1,5,9], k = 2, t = 3

输出:false
```

提示:
* 0 <= nums.length <= 2 * $10^4$
* -$2^{31}$ <= nums[i] <= $2^{31}$ - 1
* 0 <= k <= $10^4$
* 0 <= t <= $2^{31}$ - 1

Solution:
### 滑动窗口 & 二分

**根据题意,对于任意一个位置 `i`(假设其值为 `u`),我们其实是希望在下标范围为 $[max(0, i - k), i)$ 内找到值范围在 $[u - t, u + t]$ 的数。**

一个朴素的想法是每次遍历到任意位置 `i` 的时候,往后检查 `k` 个元素,但这样做的复杂度是 $O(nk)$ 的,会超时。

显然我们需要优化「检查后面 `k` 个元素」这一过程。

**我们希望使用一个「有序集合」去维护长度为 `k` 的滑动窗口内的数,该数据结构最好支持高效「查询」与「插入/删除」操作:**

* **查询:能够在「有序集合」中应用「二分查找」,快速找到「小于等于 $u$ 的最大值」和「大于等于 `u` 的最小值」(即「有序集合」中的最接近 `u` 的数)。**
* **插入/删除:在往「有序集合」添加或删除元素时,能够在低于线性的复杂度内完成(维持有序特性)。**

或许你会想到近似 $O(1)$ 操作的 `HashMap`,但注意这里我们需要找的是符合 $abs(nums[i], nums[j]) \leqslant t$ 的两个值,`nums[i]` 与 `nums[j]` 并不一定相等,而 `HashMap` 无法很好的支持「范围查询」操作。

我们还会想到「树」结构。

例如 AVL,能够让我们在最坏为 $O(\log{k})$ 的复杂度内取得到最接近 `u` 的值是多少,但本题除了「查询」以外,还涉及频繁的「插入/删除」操作(随着我们遍历 `nums` 的元素,滑动窗口不断右移,我们需要不断的往「有序集合」中删除和添加元素)。

简单采用 AVL 树,会导致每次的插入删除操作都触发 AVL 的平衡调整,一次平衡调整会伴随着若干次的旋转。

而红黑树则很好解决了上述问题:将平衡调整引发的旋转的次数从「若干次」限制到「最多三次」。

**因此,当「查询」动作和「插入/删除」动作频率相当时,更好的选择是使用「红黑树」。**

也就是对应到 Java 中的 `TreeSet` 数据结构(基于红黑树,查找和插入都具有折半的效率)。

其他细节:由于 `nums` 中的数较大,会存在 `int` 溢出问题,我们需要使用 `long` 来存储。

代码:
```Java
class Solution {
    public boolean containsNearbyAlmostDuplicate(int[] nums, int k, int t) {
        int n = nums.length;
        TreeSet<Long> ts = new TreeSet<>();
        for (int i = 0; i < n; i++) {
            Long u = nums[i] * 1L;
            // 从 ts 中找到小于等于 u 的最大值(小于等于 u 的最接近 u 的数)
            Long l = ts.floor(u);
            // 从 ts 中找到大于等于 u 的最小值(大于等于 u 的最接近 u 的数)
            Long r = ts.ceiling(u);
            if(l != null && u - l <= t) return true;
            if(r != null && r - u <= t) return true;
            // 将当前数加到 ts 中,并移除下标范围不在 [max(0, i - k), i) 的数(维持滑动窗口大小为 k)
            ts.add(u);
            if (i >= k) ts.remove(nums[i - k] * 1L);
        }
        return false;
    }
}
```
* 时间复杂度:`TreeSet` 基于红黑树,查找和插入都是 $O(\log{k})$ 复杂度。整体复杂度为 $O(n\log{k})$
* 空间复杂度:$O(k)$

---

### 桶排序

**上述解法无法做到线性的原因是:我们需要在大小为 `k` 的滑动窗口所在的「有序集合」中找到与 `u` 接近的数。**

**如果我们能够将 `k` 个数字分到 $k$ 个桶的话,那么我们就能 $O(1)$ 的复杂度确定是否有 $[u - t, u + t]$ 的数字(检查目标桶是否有元素)。**

具体的做法为:令桶的大小为 $size = t + 1$,根据 `u` 计算所在桶编号:

* 如果已经存在该桶,说明前面已有 $[u - t, u + t]$ 范围的数字,返回 `true`
* 如果不存在该桶,则检查相邻两个桶的元素是有 $[u - t, u + t]$ 范围的数字,如有 返回 `true`
* 建立目标桶,并删除下标范围不在 $[max(0, i - k), i)$ 内的桶

代码:
```Java
class Solution {
    long size;
    public boolean containsNearbyAlmostDuplicate(int[] nums, int k, int t) {
        int n = nums.length;
        Map<Long, Long> map = new HashMap<>();
        size = t + 1L;
        for (int i = 0; i < n; i++) {
            long u = nums[i] * 1L;
            long idx = getIdx(u);
            // 目标桶已存在(桶不为空),说明前面已有 [u - t, u + t] 范围的数字
            if (map.containsKey(idx)) return true;
            // 检查相邻的桶
            long l = idx - 1, r = idx + 1;
            if (map.containsKey(l) && u - map.get(l) <= t) return true;
            if (map.containsKey(r) && map.get(r) - u <= t) return true;
            // 建立目标桶
            map.put(idx, u);
            // 移除下标范围不在 [max(0, i - k), i) 内的桶
            if (i >= k) map.remove(getIdx(nums[i - k] * 1L));
        }
        return false;
    }
    long getIdx(long u) {
        return u >= 0 ? u / size : ((u + 1) / size) - 1;
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(k)$

---

### 【重点】如何理解 `getIdx()` 的逻辑

1. 为什么 `size` 需要对 `t` 进行 `+1` 操作?

**目的是为了确保差值小于等于 `t` 的数能够落到一个桶中。**

举个 🌰,假设 `[0,1,2,3]`,`t = 3`,显然四个数都应该落在同一个桶。

如果不对 `t` 进行 `+1` 操作的话,那么 `[0,1,2]` 和 `[3]` 会被落到不同的桶中,那么为了解决这种错误,我们需要对 `t` 进行 `+1` 作为 `size` 。

这样我们的数轴就能被分割成:

`0 1 2 3 | 4 5 6 7 | 8 9 10 11 | 12 13 14 15 | ...`

**总结一下,令 `size = t + 1` 的本质是因为差值为 `t` 两个数在数轴上相隔距离为 `t + 1`,它们需要被落到同一个桶中。**

**当明确了 `size` 的大小之后,对于正数部分我们则有 `idx = nums[i] / size`。**

2. 如何理解负数部分的逻辑?

由于我们处理正数的时候,处理了数值 `0`,因此我们负数部分是从 `-1` 开始的。

还是我们上述 🌰,此时我们有 `t = 3` 和 `size = t + 1 = 4`。

考虑 `[-4,-3,-2,-1]` 的情况,它们应该落在一个桶中。

如果直接复用 `idx = nums[i] / size` 的话,`[-4]` 和 `[-3,-2,-1]` 会被分到不同的桶中。

**根本原因是我们处理整数的时候,已经分掉了数值 `0`。**

**这时候我们需要先对 `nums[i]` 进行 `+1` 操作(即将负数部分在数轴上进行整体右移),即得到 `(nums[i] + 1) / size`。**

**这样一来负数部分与正数部分一样,可以被正常分割了。**

**但由于 0 号桶已经被使用了,我们还需要在此基础上进行 `-1`,相当于将负数部分的桶下标(`idx`)往左移,即得到 `((nums[i] + 1) / size) - 1`。**

### 最后

这是我们「刷穿 LeetCode」系列文章的第 `No.220` 篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先把所有不带锁的题目刷完。

在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。

为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:https://github.com/SharingSource/LogicStack-LeetCode 。

在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。


LeetCode/2121-2130/2127. 参加会议的最多员工数（困难）.md

2127. 参加会议的最多员工数

Question:
一个公司准备组织一场会议,邀请名单上有 `n` 位员工。

公司准备了一张圆形的桌子,可以坐下任意数目的员工。

员工编号为 $0$ 到 $n - 1$。每位员工都有一位喜欢的员工,每位员工当且仅当他被安排在喜欢员工的旁边,他才会参加会议,每位员工喜欢的员工不会是他自己。

给你一个下标从 $0$ 开始的整数数组 `favorite`,其中 $favorite[i]$ 表示第 $i$ 位员工喜欢的员工。请你返回参加会议的最多员工数目。

示例 1:

```
输入:favorite = [2,2,1,2]

输出:3

解释:
上图展示了公司邀请员工 0,1 和 2 参加会议以及他们在圆桌上的座位。
没办法邀请所有员工参与会议,因为员工 2 没办法同时坐在 0,1 和 3 员工的旁边。
注意,公司也可以邀请员工 1,2 和 3 参加会议。
所以最多参加会议的员工数目为 3 。
```
示例 2:
```
输入:favorite = [1,2,0]

输出:3

解释:
每个员工都至少是另一个员工喜欢的员工。所以公司邀请他们所有人参加会议的前提是所有人都参加了会议。
座位安排同图 1 所示:
- 员工 0 坐在员工 2 和 1 之间。
- 员工 1 坐在员工 0 和 2 之间。
- 员工 2 坐在员工 1 和 0 之间。
参与会议的最多员工数目为 3 。
```
示例 3:

```
输入:favorite = [3,0,1,4,1]

输出:4

解释:
上图展示了公司可以邀请员工 0,1,3 和 4 参加会议以及他们在圆桌上的座位。
员工 2 无法参加,因为他喜欢的员工 0 旁边的座位已经被占领了。
所以公司只能不邀请员工 2 。
参加会议的最多员工数目为 4 。
```

提示:
* $n = favorite.length$
* $2 <= n <= 10^5$
* $0 <= favorite[i] <= n - 1$
* $favorite[i] != i$

Solution:
### 内向基环森林 + 拓扑排序

根据题意,圆形桌上 $x$ 左右两边只要有一位是 $x$ 所喜欢即可。

我们可从 $i$ 向 $favorite[i]$ 添加有向边,从而得到一张包含多个「内向基环树」的图。

内向基环树,是指其满足基环树定义,且内向 ~~bushi~~。

基环树是指其具有 $n$ 个点 $n$ 条边的联通块,而「内向」是指树中任意节点有且只有一条出边,对应的「外向」是指树中任意节点有且只有一条入边。

例如,左图内向,右图外向:

根据题意,**圆桌最多放置一个长度大于 $2$ 的环(内向环,只有一条出边,即只有一个喜欢的人,安插其他非环成员,会破坏留下参加会议的必要条件),但可放置多个长度为 $2$ 的环,且多个环可延伸出最长链(利用左右两侧只需有一个喜欢的人即满足)。**

在「取长度大于 $2$ 的最大环」及「多个长度为 $2$ 的环及其最长链之和」两者中取最大长度即是答案。

Java 代码:
```Java
class Solution {
    public int maximumInvitations(int[] favorite) {
        int n = favorite.length;
        // in 统计每个节点的入度情况, max 统计节最长链
        int[] in = new int[n], max = new int[n];
        for (int x : favorite) in[x]++;
        Deque<Integer> d = new ArrayDeque<>();
        for (int i = 0; i < n; i++) {
            if (in[i] == 0) d.addLast(i);
        }
        // 拓扑排序: 求基环外的最长链
        while (!d.isEmpty()) {
            int cur = d.pollFirst(), ne = favorite[cur];
            max[ne] = Math.max(max[ne], max[cur] + 1);
            if (--in[ne] == 0) d.addLast(ne);
        }
        // 圆桌最多放置一个大于 2 的环(ans1 统计最大值)
        // 圆桌可放置多个等于 2 的环(ans2 累加该长度)
        int ans1 = 0, ans2 = 0;
        for (int i = 0; i < n; i++) {
            if (in[i] == 0) continue;
            int j = favorite[i], cur = 1;
            while (j != i) {
                // 一个环只需被处理一次, 这里将环中其他节点入度置 0, 下次遍历到这些点就会被跳过
                in[j] = 0;
                j = favorite[j];
                cur++;
            }
            if (cur == 2) ans2 += 2 + max[i] + max[favorite[i]];
            else ans1 = Math.max(ans1, cur);
        }
        return Math.max(ans1, ans2);
    }
}
```
Python 代码:
```Python
class Solution:
    def maximumInvitations(self, favorite: List[int]) -> int:
        n = len(favorite)
        # in_degree 统计每个节点的入度情况, max_length 统计节最长链
        in_degree, max_length = [0] * n, [0] * n
        for x in favorite:
            in_degree[x] += 1
        d = deque()
        for i in range(n):
            if in_degree[i] == 0:
                d.append(i)
       # 拓扑排序: 求基环外的最长链
        while d:
            cur = d.popleft()
            ne = favorite[cur]
            max_length[ne] = max(max_length[ne], max_length[cur] + 1)
            in_degree[ne] -= 1
            if in_degree[ne] == 0:
                d.append(ne)
        # 圆桌最多放置一个大于 2 的环(ans1 统计最大值)
        # 圆桌可放置多个等于 2 的环(ans2 累加该长度)
        ans1, ans2 = 0, 0
        for i in range(n):
            if in_degree[i] == 0:
                continue
            j, cur = favorite[i], 1
            while j != i:
                # 一个环只需被处理一次, 这里将环中其他节点入度置 0, 下次遍历到这些点就会被跳过
                in_degree[j] = 0
                j = favorite[j]
                cur += 1
            if cur == 2:
                ans2 += 2 + max_length[i] + max_length[favorite[i]]
            else:
                ans1 = max(ans1, cur)
        return max(ans1, ans2)
```
C++ 代码:
```C++
class Solution {
public:
    int maximumInvitations(vector<int>& favorite) {
        int n = favorite.size();
        // in 统计每个节点的入度情况, max_length 统计节最长链
        vector<int> in(n, 0);
        vector<int> max_length(n, 0);
        for (int x : favorite) in[x]++;
        deque<int> d;
        for (int i = 0; i < n; i++) {
            if (in[i] == 0) d.push_back(i);
        }
        // 拓扑排序: 求基环外的最长链
        while (!d.empty()) {
            int cur = d.front();
            d.pop_front();
            int ne = favorite[cur];
            max_length[ne] = max(max_length[ne], max_length[cur] + 1);
            if (--in[ne] == 0) d.push_back(ne);
        }
        // 圆桌最多放置一个大于 2 的环(ans1 统计最大值)
        // 圆桌可放置多个等于 2 的环(ans2 累加该长度)
        int ans1 = 0, ans2 = 0;
        for (int i = 0; i < n; i++) {
            if (in[i] == 0) continue;
            int j = favorite[i], cur = 1;
            while (j != i) {
                // 一个环只需被处理一次, 这里将环中其他节点入度置 0, 下次遍历到这些点就会被跳过
                in[j] = 0;
                j = favorite[j];
                cur++;
            }
            if (cur == 2) ans2 += 2 + max_length[i] + max_length[favorite[i]];
            else ans1 = max(ans1, cur);
        }
        return max(ans1, ans2);
    }
};
```
TypeScript 代码:
```TypeScript
function maximumInvitations(favorite: number[]): number {
    const n = favorite.length;
    // in_degree 统计每个节点的入度情况, max_length 统计节最长链
    const in_degree = Array(n).fill(0), max_length = Array(n).fill(0);
    for (const x of favorite) in_degree[x]++;
    const d = [];
    for (let i = 0; i < n; i++) {
        if (in_degree[i] === 0) d.push(i);
    }
    // 拓扑排序: 求基环外的最长链
    while (d.length > 0) {
        const cur = d.shift() as number;
        const ne = favorite[cur];
        max_length[ne] = Math.max(max_length[ne], max_length[cur] + 1);
        if (--in_degree[ne] === 0) d.push(ne);
    }
    // 圆桌最多放置一个大于 2 的环(ans1 统计最大值)
    // 圆桌可放置多个等于 2 的环(ans2 累加该长度)
    let ans1 = 0, ans2 = 0;
    for (let i = 0; i < n; i++) {
        if (in_degree[i] === 0) continue;
        let j = favorite[i], cur = 1;
        while (j !== i) {
            // 一个环只需被处理一次, 这里将环中其他节点入度置 0, 下次遍历到这些点就会被跳过
            in_degree[j] = 0;
            j = favorite[j];
            cur++;
        }
        if (cur == 2) ans2 += 2 + max_length[i] + max_length[favorite[i]];
        else ans1 = Math.max(ans1, cur);
    }
    return Math.max(ans1, ans2);
};
```
* 时间复杂度:统计入度的复杂度为 $O(n)$;拓扑排序求最长链复杂度为 $O(n)$;计算答案过程中,每个点最多被访问两次(环内节点),复杂度为 $O(n)$。整体复杂度为 $O(n)$
* 空间复杂度:$O(n)$

### 最后

这是我们「刷穿 LeetCode」系列文章的第 `No.2217` 篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先把所有不带锁的题目刷完。

在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。

为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:https://github.com/SharingSource/LogicStack-LeetCode 。

在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。


LeetCode/2161-2170/2170. 使数组变成交替数组的最少操作数（中等）.md

2170. 使数组变成交替数组的最少操作数

Question:
给你一个下标从 $0$ 开始的数组 $nums$ ,该数组由 $n$ 个正整数组成。

如果满足下述条件,则数组 $nums$ 是一个 交替数组 :

* $nums[i - 2] == nums[i]$ ,其中 $2 <= i <= n - 1$ 。
* $nums[i - 1] != nums[i]$ ,其中 $1 <= i <= n - 1$ 。

在一步 操作 中,你可以选择下标 $i$ 并将 $nums[i]$ 更改 为 任一 正整数。

返回使数组变成交替数组的 最少操作数 。

示例 1:
```
输入:nums = [3,1,3,2,4,3]

输出:3

解释:
使数组变成交替数组的方法之一是将该数组转换为 [3,1,3,1,3,1] 。
在这种情况下,操作数为 3 。
可以证明,操作数少于 3 的情况下,无法使数组变成交替数组。
```
示例 2:
```
输入:nums = [1,2,2,2,2]

输出:2

解释:
使数组变成交替数组的方法之一是将该数组转换为 [1,2,1,2,1].
在这种情况下,操作数为 2 。
注意,数组不能转换成 [2,2,2,2,2] 。因为在这种情况下,nums[0] == nums[1],不满足交替数组的条件。
```

提示:
* $1 <= nums.length <= 10^5$
* $1 <= nums[i] <= 10^5$

Solution:
### 贪心

根据「交替数组」的定义,我们可以将数组根据下标奇偶性分成两个序列,最终目的是:**使用最少的修改次数,使得「两个序列均成为公差为 $0$ 等差数列」,同时「两序列的首项不相等」。**

要用最少次数将一个序列修改为公差为 $0$ 等差数列,等价于修改最少的数字,等价于保留最多的数字,容易想到将序列中的其他「非众数」修改为「众数」(若有多个众数,取任一)。

而单纯对两个序列执行上述逻辑,并不能确保最终得到的是「交替数组」,即未必满足「两序列的首项不相等」的要求。

因此我们可以对 $nums$ 进行扫描,分别统计「偶数下标序列」和「奇数下标序列」的最大值(众数)和次大值(**注意是非严格的次大值,即为「其他众数」或者「出现次数比众数小的数」**),使用 `a` 和 `b` 代指「偶数下标序列」的最大值和次大值,使用 `c` 和 `d` 代指「奇数下标序列」的最大值和次大值。同时使用 `m1` 和 `m2` 分别统计「偶数下标序列」和「奇数下标序列」中某个数的出现次数。

根据两序列的最大值是否冲突进行分情况讨论:

* 若两序列的最大值不冲突($a\neq c$):那么两序列都可以取得最小修改次数(保留最大值),整体的最小修改次数为 $n - m1[a] - m2[c]$ ;
* 若两序列的最大值冲突($a\ = c$):那么仅一序列可以取得最小修改次数(保留最大值),另一序列只能取得“次小”的修改次数(保留次大值),此时整体的最小修改次数为 $n - \max(m1[a] + m2[d], m1[b] + m2[c])$。

代码:
```Java
class Solution {
    static int N = 100010;
    static int[] m1 = new int[N], m2 = new int[N];
    public int minimumOperations(int[] nums) {
        int n = nums.length;
        Arrays.fill(m1, 0);
        Arrays.fill(m2, 0);
        int a = 0, b = 0, c = 0, d = 0;
        for (int i = 0; i < n; i++) {
            int t = nums[i];
            if (i % 2 == 0) {
                m1[t]++;
                if (a == 0 || m1[t] > m1[a]) {
                    b = a; a = t;
                } else if (t != a && (b == 0 || m1[t] > m1[b])) {
                    b = t;
                }
            } else {
                m2[t]++;
                if (c == 0 || m2[t] > m2[c]) {
                    d = c; c = t;
                } else if (t != c && (d == 0 || m2[t] > m2[d])) {
                    d = t;
                }
            }
        }
        if (a != c) return n - m1[a] - m2[c];
        else return n - Math.max(m1[a] + m2[d], m1[b] + m2[c]);
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(C)$

### 最后

这是我们「刷穿 LeetCode」系列文章的第 `No.2170` 篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先把所有不带锁的题目刷完。

在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。

为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:https://github.com/SharingSource/LogicStack-LeetCode 。

在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。


Question:
给你 **二维** 平面上两个 **由直线构成的** 矩形,请你计算并返回两个矩形覆盖的总面积。

每个矩形由其 左下 顶点和 右上 顶点坐标表示:

* 第一个矩形由其左下顶点 (ax1, ay1) 和右上顶点 (ax2, ay2) 定义。
* 第二个矩形由其左下顶点 (bx1, by1) 和右上顶点 (bx2, by2) 定义。

示例 1:

```
输入:ax1 = -3, ay1 = 0, ax2 = 3, ay2 = 4, bx1 = 0, by1 = -1, bx2 = 9, by2 = 2

输出:45
```
示例 2:
```
输入:ax1 = -2, ay1 = -2, ax2 = 2, ay2 = 2, bx1 = -2, by1 = -2, bx2 = 2, by2 = 2

输出:16
```

提示:
* -$10^4 <= ax1, ay1, ax2, ay2, bx1, by1, bx2, by2 <= 10^4$

Solution:
### 容斥原理

首先在给定左下顶点和右上顶点的情况下,计算矩形面积为 $(x2 - x1) * (y2 - y1)$。

因此,起始时我们可以先直接算得给定的两个矩形的面积 $A$ 和 $B$,并进行累加。

剩下的,我们需要求得两矩形的交集面积,利用「容斥原理」,减去交集面积,即是答案。

**求交集矩形面积,可以转换为求两矩形在坐标轴上的重合长度,若两矩形在 $X$ 轴上的重合长度为 $x$,在 $Y$ 轴上的重合长度为 $y$,则有重合面积为 $C = x * y$。同时考虑两矩形在任一坐标轴上没有重合长度,则不存在重合面积,因此需要将重合长度与 $0$ 取 $\max$。**

最终答案为 $A + B - C$ 。

代码;
```Java
class Solution {
    public int computeArea(int ax1, int ay1, int ax2, int ay2, int bx1, int by1, int bx2, int by2) {
        int x = Math.max(0, Math.min(ax2, bx2) - Math.max(ax1, bx1));
        int y = Math.max(0, Math.min(ay2, by2) - Math.max(ay1, by1));
        return (ax2 - ax1) * (ay2 - ay1) + (bx2 - bx1) * (by2 - by1) - (x * y);
    }
}
```
* 时间复杂度:$O(1)$
* 空间复杂度:$O(1)$

### 最后

这是我们「刷穿 LeetCode」系列文章的第 `No.223` 篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先把所有不带锁的题目刷完。

在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。

为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:https://github.com/SharingSource/LogicStack-LeetCode 。

在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。


Question:
给你一个字符串表达式 `s` ,请你实现一个基本计算器来计算并返回它的值。

示例 1:
```
输入:s = "1 + 1"

输出:2
```
示例 2:
```
输入:s = " 2-1 + 2 "

输出:3
```
示例 3:
```
输入:s = "(1+(4+5+2)-3)+(6+8)"

输出:23
```

提示:
* $1 <= s.length <= 3 \times 10^5$
* `s` 由`数字`、`'+'`、`'-'`、`'('`、`')'`、和 `' '` 组成
* `s` 表示一个有效的表达式

Solution:
### 双栈解法

我们可以使用两个栈 `nums` 和 `ops` 。
* `nums` : 存放所有的数字
* `ops` :存放所有的数字以外的操作,`+/-` 也看做是一种操作

然后从前往后做,对遍历到的字符做分情况讨论:
* 空格 : 跳过
* `(` : 直接加入 `ops` 中,等待与之匹配的 `)`
* `)` : 使用现有的 `nums` 和 `ops` 进行计算,直到遇到左边最近的一个左括号为止,计算结果放到 `nums`
* 数字 : 从当前位置开始继续往后取,将整一个连续数字整体取出,加入 `nums`
* `+/-` : 需要将操作放入 `ops` 中。**在放入之前先把栈内可以算的都算掉**,使用现有的 `nums` 和 `ops` 进行计算,直到没有操作或者遇到左括号,计算结果放到 `nums`

一些细节:

* 由于第一个数可能是负数,为了减少边界判断。一个小技巧是先往 `nums` 添加一个 0
* 为防止 () 内出现的首个字符为运算符,将所有的空格去掉,并将 `(-` 替换为 `(0-`,`(+` 替换为 `(0+`(当然也可以不进行这样的预处理,将这个处理逻辑放到循环里去做)

Java 代码:
```Java
class Solution {
    public int calculate(String s) {
        // 存放所有的数字
        Deque<Integer> nums = new ArrayDeque<>();
        // 为了防止第一个数为负数,先往 nums 加个 0
        nums.addLast(0);
        // 将所有的空格去掉
        s = s.replaceAll(" ", "");
        // 存放所有的操作,包括 +/-
        Deque<Character> ops = new ArrayDeque<>();
        int n = s.length();
        char[] cs = s.toCharArray();
        for (int i = 0; i < n; i++) {
            char c = cs[i];
            if (c == '(') {
                ops.addLast(c);
            } else if (c == ')') {
                // 计算到最近一个左括号为止
                while (!ops.isEmpty()) {
                    char op = ops.peekLast();
                    if (op != '(') {
                        calc(nums, ops);
                    } else {
                        ops.pollLast();
                        break;
                    }
                }
            } else {
                if (isNum(c)) {
                    int u = 0, j = i;
                    // 将从 i 位置开始后面的连续数字整体取出,加入 nums
                    while (j < n && isNum(cs[j])) u = u * 10 + (int)(cs[j++] - '0');
                    nums.addLast(u);
                    i = j - 1;
                } else {
                    if (i > 0 && (cs[i - 1] == '(' || cs[i - 1] == '+' || cs[i - 1] == '-')) {
                        nums.addLast(0);
                    }
                    // 有一个新操作要入栈时,先把栈内可以算的都算了
                    while (!ops.isEmpty() && ops.peekLast() != '(') calc(nums, ops);
                    ops.addLast(c);
                }
            }
        }
        while (!ops.isEmpty()) calc(nums, ops);
        return nums.peekLast();
    }
    void calc(Deque<Integer> nums, Deque<Character> ops) {
        if (nums.isEmpty() || nums.size() < 2) return;
        if (ops.isEmpty()) return;
        int b = nums.pollLast(), a = nums.pollLast();
        char op = ops.pollLast();
        nums.addLast(op == '+' ? a + b : a - b);
    }
    boolean isNum(char c) {
        return Character.isDigit(c);
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    void replace(string& s){
        int pos = s.find(" ");
        while (pos != -1) {
            s.replace(pos, 1, "");
            pos = s.find(" ");
        }
    }
    int calculate(string s) {
        // 存放所有的数字
        stack<int> nums;
        // 为了防止第一个数为负数,先往 nums 加个 0
        nums.push(0);
        // 将所有的空格去掉
        replace(s);
        // 存放所有的操作,包括 +/-
        stack<char> ops;
        int n = s.size();
        for(int i = 0; i < n; i++) {
            char c = s[i];
            if(c == '(')
                ops.push(c);
            else if(c == ')') {
                // 计算到最近一个左括号为止
                while(!ops.empty()) {
                    char op = ops.top();
                    if(op != '(')
                        calc(nums, ops);
                    else {
                        ops.pop();
                        break;
                    }
                }
            }
            else {
                if(isdigit(c)) {
                    int cur_num = 0, j = i;
                    // 将从 i 位置开始后面的连续数字整体取出,加入 nums
                    while(j <n && isdigit(s[j]))
                        cur_num = cur_num*10 + (s[j++] - '0');
                    // 注意上面的计算一定要有括号,否则有可能会溢出
                    nums.push(cur_num);
                    i = j - 1;
                }
                else {
                    if (i > 0 && (s[i - 1] == '(' || s[i - 1] == '+' || s[i - 1] == '-')) {
                        nums.push(0);
                    }
                    // 有一个新操作要入栈时,先把栈内可以算的都算了
                    while(!ops.empty() && ops.top() != '(')
                        calc(nums, ops);
                    ops.push(c);
                }
            }
        }
        while(!ops.empty())
            calc(nums, ops);
        return nums.top();
    }
    void calc(stack<int> &nums, stack<char> &ops) {
        if(nums.size() < 2 || ops.empty())
            return;
        int b = nums.top(); nums.pop();
        int a = nums.top(); nums.pop();
        char op = ops.top(); ops.pop();
        nums.push(op == '+' ? a+b : a-b);
    }
};
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n)$

---

### 进阶

1. 如果在此基础上,再考虑 `*` 和 `/`,需要增加什么考虑?如何维护运算符的优先级?
2. 在 $1$ 的基础上,如果考虑支持自定义符号,例如 `a / func(a, b) * (c + d)`,需要做出什么调整?

---

### 补充

1. 对应进阶 1 的补充。

一个支持 `+ - * / ^ %` 的「计算器」,基本逻辑是一样的,使用字典维护一个符号优先级:

```Java
class Solution {
    Map<Character, Integer> map = new HashMap<>(){{
        put('-', 1);
        put('+', 1);
        put('*', 2);
        put('/', 2);
        put('%', 2);
        put('^', 3);
    }};
    public int calculate(String s) {
        s = s.replaceAll(" ", "");
        char[] cs = s.toCharArray();
        int n = s.length();
        Deque<Integer> nums = new ArrayDeque<>();
        nums.addLast(0);
        Deque<Character> ops = new ArrayDeque<>();
        for (int i = 0; i < n; i++) {
            char c = cs[i];
            if (c == '(') {
                ops.addLast(c);
            } else if (c == ')') {
                while (!ops.isEmpty()) {
                    if (ops.peekLast() != '(') {
                        calc(nums, ops);
                    } else {
                        ops.pollLast();
                        break;
                    }
                }
            } else {
                if (isNumber(c)) {
                    int u = 0;
                    int j = i;
                    while (j < n && isNumber(cs[j])) u = u * 10 + (cs[j++] - '0');
                    nums.addLast(u);
                    i = j - 1;
                } else {
                    if (i > 0 && (cs[i - 1] == '(' || cs[i - 1] == '+' || cs[i - 1] == '-')) {
                        nums.addLast(0);
                    }
                    while (!ops.isEmpty() && ops.peekLast() != '(') {
                        char prev = ops.peekLast();
                        if (map.get(prev) >= map.get(c)) {
                            calc(nums, ops);
                        } else {
                            break;
                        }
                    }
                    ops.addLast(c);
                }
            }
        }
        while (!ops.isEmpty() && ops.peekLast() != '(') calc(nums, ops);
        return nums.peekLast();
    }
    void calc(Deque<Integer> nums, Deque<Character> ops) {
        if (nums.isEmpty() || nums.size() < 2) return;
        if (ops.isEmpty()) return;
        int b = nums.pollLast(), a = nums.pollLast();
        char op = ops.pollLast();
        int ans = 0;
        if (op == '+') {
            ans = a + b;
        } else if (op == '-') {
            ans = a - b;
        } else if (op == '*') {
            ans = a * b;
        } else if (op == '/') {
            ans = a / b;
        } else if (op == '^') {
            ans = (int)Math.pow(a, b);
        } else if (op == '%') {
            ans = a % b;
        }
        nums.addLast(ans);
    }
    boolean isNumber(char c) {
        return Character.isDigit(c);
    }
}
```

2. 关于进阶 $2$,其实和进阶 $1$ 一样,重点在于维护优先级。但还有一些编码细节:

对于非单个字符的运算符(例如 函数名`function`),可以在处理前先将所有非单字符的运算符进行替换(将 `function` 替换为 `@#` 等)

然后对特殊运算符做特判,确保遍历过程中识别到特殊运算符之后,往后整体读入(如 `function(a,b)` -> `@(a, b)`,`@(a, b)` 作为整体处理)

### 最后

这是我们「刷穿 LeetCode」系列文章的第 `No.224` 篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先把所有不带锁的题目刷完。

在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。

为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:https://github.com/SharingSource/LogicStack-LeetCode 。

在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。


Question:
给你一个字符串表达式 `s` ,请你实现一个基本计算器来计算并返回它的值。

整数除法仅保留整数部分。

示例 1:
```
输入:s = "3+2*2"

输出:7
```
示例 2:
```
输入:s = " 3/2 "

输出:1
```
示例 3:
```
输入:s = " 3+5 / 2 "

输出:5
```

提示:
* $1 <= s.length <= 3 \times 10^5$
* `s` 由整数和算符 (`'+'`, `'-'`, `'*'`, `'/'`) 组成,中间由一些空格隔开
* `s` 表示一个 有效表达式
* 表达式中的所有整数都是非负整数,且在范围 $[0, 2^{31} - 1]$ 内
* 题目数据保证答案是一个 `32-bit` 整数

Solution:
### 双栈

如果你有看这篇 [224. 基本计算器](https://mp.weixin.qq.com/s?__biz=MzU4NDE3MTEyMA==&mid=2247492602&idx=1&sn=135fd5b530189f13e0395414a6b47893) 的话,今天这道题就是道练习题。

帮你巩固 **双栈解决「通用表达式」问题的通用解法** 。

**事实上,我提供这套解决方案不仅仅能解决只有 `+ - ( )`的 [[224. 基本计算器]](https://mp.weixin.qq.com/s?__biz=MzU4NDE3MTEyMA==&mid=2247492602&idx=1&sn=135fd5b530189f13e0395414a6b47893) 或者 `+ - * /` [227. 基本计算器 II(本题)] 的表达式问题,还能解决 `+ - * / ^ % ( )` 的完全表达式问题。**

甚至支持自定义运算符,只要在运算优先级上进行维护即可。

**对于「表达式计算」这一类问题,你都可以使用这套思路进行解决。我十分建议你加强理解这套处理逻辑。**

对于「任何表达式」而言,我们都使用两个栈 `nums` 和 `ops`:
* `nums` : 存放所有的数字
* `ops` :存放所有的数字以外的操作

然后从前往后做,对遍历到的字符做分情况讨论:
* 空格 : 跳过
* `(` : 直接加入 `ops` 中,等待与之匹配的 `)`
* `)` : 使用现有的 `nums` 和 `ops` 进行计算,直到遇到左边最近的一个左括号为止,计算结果放到 `nums`
* 数字 : 从当前位置开始继续往后取,将整一个连续数字整体取出,加入 `nums`
* `+ - * / ^ %` : 需要将操作放入 `ops` 中。**在放入之前先把栈内可以算的都算掉(只有「栈内运算符」比「当前运算符」优先级高/同等,才进行运算)**,使用现有的 `nums` 和 `ops` 进行计算,直到没有操作或者遇到左括号,计算结果放到 `nums`

我们可以通过 🌰 来理解 **只有「栈内运算符」比「当前运算符」优先级高/同等,才进行运算** 是什么意思:

因为我们是从前往后做的,假设我们当前已经扫描到 `2 + 1` 了(此时栈内的操作为 `+` )。

1. 如果后面出现的 `+ 2` 或者 `- 1` 的话,满足「栈内运算符」比「当前运算符」优先级高/同等,可以将 `2 + 1` 算掉,把结果放到 `nums` 中;
2. 如果后面出现的是 `* 2` 或者 `/ 1` 的话,不满足「栈内运算符」比「当前运算符」优先级高/同等,这时候不能计算 `2 + 1`。

一些细节:

* 由于第一个数可能是负数,为了减少边界判断。一个小技巧是先往 `nums` 添加一个 0
* 为防止 () 内出现的首个字符为运算符,将所有的空格去掉,并将 `(-` 替换为 `(0-`,`(+` 替换为 `(0+`(当然也可以不进行这样的预处理,将这个处理逻辑放到循环里去做)
* 从理论上分析,`nums` 最好存放的是 `long`,而不是 `int`。因为可能存在 `大数 + 大数 + 大数 + ... - 大数 - 大数` 的表达式导致中间结果溢出,最终答案不溢出的情况

代码:

```Java
class Solution {
    // 使用 map 维护一个运算符优先级
    // 这里的优先级划分按照「数学」进行划分即可
    Map<Character, Integer> map = new HashMap<>(){{
        put('-', 1);
        put('+', 1);
        put('*', 2);
        put('/', 2);
        put('%', 2);
        put('^', 3);
    }};
    public int calculate(String s) {
        // 将所有的空格去掉
        s = s.replaceAll(" ", "");
        char[] cs = s.toCharArray();
        int n = s.length();
        // 存放所有的数字
        Deque<Integer> nums = new ArrayDeque<>();
        // 为了防止第一个数为负数,先往 nums 加个 0
        nums.addLast(0);
        // 存放所有「非数字以外」的操作
        Deque<Character> ops = new ArrayDeque<>();
        for (int i = 0; i < n; i++) {
            char c = cs[i];
            if (c == '(') {
                ops.addLast(c);
            } else if (c == ')') {
                // 计算到最近一个左括号为止
                while (!ops.isEmpty()) {
                    if (ops.peekLast() != '(') {
                        calc(nums, ops);
                    } else {
                        ops.pollLast();
                        break;
                    }
                }
            } else {
                if (isNumber(c)) {
                    int u = 0;
                    int j = i;
                    // 将从 i 位置开始后面的连续数字整体取出,加入 nums
                    while (j < n && isNumber(cs[j])) u = u * 10 + (cs[j++] - '0');
                    nums.addLast(u);
                    i = j - 1;
                } else {
                    if (i > 0 && (cs[i - 1] == '(' || cs[i - 1] == '+' || cs[i - 1] == '-')) {
                        nums.addLast(0);
                    }
                    // 有一个新操作要入栈时,先把栈内可以算的都算了
                    // 只有满足「栈内运算符」比「当前运算符」优先级高/同等,才进行运算
                    while (!ops.isEmpty() && ops.peekLast() != '(') {
                        char prev = ops.peekLast();
                        if (map.get(prev) >= map.get(c)) {
                            calc(nums, ops);
                        } else {
                            break;
                        }
                    }
                    ops.addLast(c);
                }
            }
        }
        // 将剩余的计算完
        while (!ops.isEmpty()) calc(nums, ops);
        return nums.peekLast();
    }
    void calc(Deque<Integer> nums, Deque<Character> ops) {
        if (nums.isEmpty() || nums.size() < 2) return;
        if (ops.isEmpty()) return;
        int b = nums.pollLast(), a = nums.pollLast();
        char op = ops.pollLast();
        int ans = 0;
        if (op == '+') ans = a + b;
        else if (op == '-') ans = a - b;
        else if (op == '*') ans = a * b;
        else if (op == '/')  ans = a / b;
        else if (op == '^') ans = (int)Math.pow(a, b);
        else if (op == '%') ans = a % b;
        nums.addLast(ans);
    }
    boolean isNumber(char c) {
        return Character.isDigit(c);
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n)$

---

### 总结

还记得我在 [题解](https://mp.weixin.qq.com/s?__biz=MzU4NDE3MTEyMA==&mid=2247492602&idx=1&sn=135fd5b530189f13e0395414a6b47893) 留的「进阶」内容?

1. 如果 `+ -` 基础上,再考虑 `*` 和 `/`,需要增加什么考虑?如何维护运算符的优先级?

这个进阶问题就对应了 LeetCode 上的两道题:

* [227. 基本计算器 II](https://leetcode-cn.com/problems/basic-calculator-ii/) :本题,包含符号 `+ - * /`
* [772. 基本计算器 III](https://leetcode-cn.com/problems/basic-calculator-iii/) :有锁题,包含符号 `+ - * / ( )`

2. 在「问题1」的基础上,如果考虑支持自定义符号,例如 a / func(a, b) * (c + d),需要做出什么调整?

这个进阶问题,在 LeetCode 上也有类似的题目:

* [770. 基本计算器 IV](https://leetcode-cn.com/problems/basic-calculator-iv/) : 包含自定义函数符号

**综上,使用三叶提供的这套「双栈通用解决方案」,可以解决所有的「表达式计算」问题。因为这套「表达式计算」处理逻辑,本质上模拟了人脑的处理逻辑:根据下一位的运算符优先级决定当前运算符是否可以马上计算。**

### 最后

这是我们「刷穿 LeetCode」系列文章的第 `No.227` 篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先把所有不带锁的题目刷完。

在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。

为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:https://github.com/SharingSource/LogicStack-LeetCode 。

在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。


Question:
给定一个大小为 `n` 的整数数组,找出其中所有出现超过 `⌊ n/3 ⌋` 次的元素。

 示例 1:
```
输入:[3,2,3]

输出:[3]
```
示例 2:
```
输入:nums = [1]

输出:[1]
```
示例 3:
```
输入:[1,1,1,3,3,2,2,2]

输出:[1,2]
```

提示:
* $1 <= nums.length <= 5 * 10^4$
* $-10^9 <= nums[i] <= 10^9$

进阶:尝试设计时间复杂度为 $O(n)$、空间复杂度为 $O(1)$ 的算法解决此问题。

Solution:
### 哈希表计数

一个朴素的做法是使用「哈希表」进行计数,在计数完成后将所有出现次数超过 $n / 3$ 的元素加入答案。

代码:
```Java
class Solution {
    public List<Integer> majorityElement(int[] nums) {
        int n = nums.length;
        Map<Integer, Integer> map = new HashMap<>();
        for (int i : nums) map.put(i, map.getOrDefault(i, 0) + 1);
        List<Integer> ans = new ArrayList<>();
        for (int i : map.keySet()) {
            if (map.get(i) > n / 3) ans.add(i);
        }
        return ans;
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n)$

---

### 摩尔投票

在前置 🧀 [简单题学投票算法](https://mp.weixin.qq.com/s?__biz=MzU4NDE3MTEyMA==&mid=2247488987&idx=1&sn=6087b1909fea1d24f15353515eee8d93&chksm=fd9cbec4caeb37d235ccad8d59724177784036a60525fa5e7b738267e9a84a572b1545528391&token=1288276346&lang=zh_CN#rd) 中,我们使用「摩尔投票」在 $O(1)$ 空间复杂度内找到了出现次数超过一半的元素,即出现次数大于 $n / 2$ 的数。

对于本题,我们需要统计出现次数超过 $n / 3$ 的数。

**我们可以不失一般性的将其拓展为「统计出现次数超过 $n / k$ 的数」。**

**可以证明,出现次数超过 $n / k$ 的数最多只有 $k - 1$ 个。否则必然违背「数总共只有 $n$ 个」或者「当前统计的是出现次数超过 $n / k$ 的数」的前提条件。**

当明确了符合要求的数的数量之后,我们可以使用有限变量来代表这 $k - 1$ 个候选数及其出现次数。

然后使用「摩尔投票」的标准做法,在建立数组时同时 `check` 这 $k - 1$ 个数,假设当前遍历到的元素为 $x$:

* 如果 $x$ 本身是候选者的话,则对其出现次数加一;
* 如果 $x$ 本身不是候选者,检查是否有候选者的出现次数为 $0$:
    * 若有,则让 $x$ 代替其成为候选者,并记录出现次数为 $1$;
    * 若无,则让所有候选者的出现次数减一。

当处理完整个数组后,这 $k - 1$ 个数可能会被填满,但不一定都是符合出现次数超过 $n / k$ 要求的。

需要进行二次遍历,来确定候选者是否符合要求,将符合要求的数加到答案。

**上述做法正确性的关键是:若存在出现次数超过 $n / k$ 的数,最后必然会成为这 $k - 1$ 个候选者之一。**

我们可以通过「反证法」来进行证明:若出现次数超过 $n / k$ 的数 $x$ 最终没有成为候选者。

有两种可能会导致这个结果:

1. 数值 $x$ 从来没成为过候选者:

   如果 $x$ 从来没成为过候选者,那么在遍历 $x$ 的过程中,必然有 $k - 1$ 个候选者被减了超过 $n / k$ 次,假设当前 $x$ 出现次数为 $C$,已知 $C > n / k$,此时总个数为

   $$
    (k - 1) * C + C = C * k
   $$

    再根据 $C > n / k$,可知 $C * k > n$,而我们总共就只有 $n$ 个数,因此该情况恒不成立。

2. 数值 $x$ 成为过候选者,但被逐出替换了:

    同理,被逐出替换,说明发生了对 $x$ 出现次数减一的动作(减到 $0$),每次的减一操作,意味着有其余的 $k - 2$ 个候选者的出现次数也发生了减一动作,加上本身被遍历到的当前数 $num[i]$,共有 $k - 1$ 个数字的和 $x$ 被一同统计。
    因此,根据我们摩尔投票的处理过程,如果 $x$ 成为过候选者,并被逐出替换,那么同样能够推导出我们存在超过 $n$ 个数。

综上,如果存在出现次数超过 $n / k$ 的数,其必然会成为 $k - 1$ 个候选者之一。

代码:
```Java
class Solution {
    public List<Integer> majorityElement(int[] nums) {
        int n = nums.length;
        int a = 0, b = 0;
        int c1 = 0, c2 = 0;
        for (int i : nums) {
            if (c1 != 0 && a == i) c1++;
            else if (c2 != 0 && b == i) c2++;
            else if (c1 == 0 && ++c1 >= 0) a = i;
            else if (c2 == 0 && ++c2 >= 0) b = i;
            else {
                c1--; c2--;
            }
        }
        c1 = 0; c2 = 0;
        for (int i : nums) {
            if (a == i) c1++;
            else if (b == i) c2++;
        }
        List<Integer> ans = new ArrayList<>();
        if (c1 > n / 3) ans.add(a);
        if (c2 > n / 3) ans.add(b);
        return ans;
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(1)$

### 最后

这是我们「刷穿 LeetCode」系列文章的第 `No.229` 篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先把所有不带锁的题目刷完。

在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。

为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:https://github.com/SharingSource/LogicStack-LeetCode 。

在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。


LeetCode/221-230/230. 二叉搜索树中第K小的元素（中等）.md

230. 二叉搜索树中第K小的元素

Question:
给定一个二叉搜索树的根节点 `root`,和一个整数 `k` ,请你设计一个算法查找其中第 `k` 个最小元素(从 $1$ 开始计数)。

示例 1:

```
输入:root = [3,1,4,null,2], k = 1

输出:1
```

示例 2:

```
输入:root = [5,3,6,2,4,null,null,1], k = 3

输出:3
```

提示:
* 树中的节点数为 n 。
* $1 <= k <= n <= 10^4$
* $0 <= Node.val <= 10^4$

Solution:
### 树的遍历 + 排序

朴素的做法是先对二叉树进行一次完整遍历,将所有节点存入列表中,最后对列表排序后返回目标值。

树的遍历可以使用 `DFS` 或 `BFS`。

代码:
```Java
class Solution {
    List<Integer> list = new ArrayList<>();
    public int kthSmallest(TreeNode root, int k) {
        dfs(root);
        Collections.sort(list);
        return list.get(k - 1);
    }
    void dfs(TreeNode root) {
        if (root == null) return ;
        list.add(root.val);
        dfs(root.left);
        dfs(root.right);
    }
}
```
* 时间复杂度:树的遍历时间复杂度为 $O(n)$;排序的复杂度为 $O(n\log{n})$。整体复杂度为 $O(n\log{n})$
* 空间复杂度:$O(n)$

---

### 树的遍历 + 优先队列(堆)

相比于先直接拿到所有节点再排序的解法一,另外一种做法是使用「优先队列(堆)」来做。

由于我们返回的是第 $k$ 小的数,因此我们可以构建一个容量为 $k$ 的大根堆。

根据大根堆的元素个数和当前节点与堆顶元素的关系来分情况讨论:

* 大根堆元素不足 $k$ 个:直接将当前节点值放入大根堆;
* 大根堆元素为 $k$ 个,根据堆顶元素和当前节点值的大小关系进一步分情况讨论:
    * 如果当前节点值元素大于堆顶元素,说明当前节点值不可能在第 $k$ 小的范围内,直接丢弃;
    * 如果当前节点值元素小于堆顶元素,说明当前节点值可能在第 $k$ 小的范围内,先 `poll` 一个再 `add` 进去。

树的遍历可以使用 `DFS` 或 `BFS`。

代码:
```Java
class Solution {
    public int kthSmallest(TreeNode root, int k) {
        PriorityQueue<Integer> q = new PriorityQueue<>((a,b)->b-a);
        Deque<TreeNode> d = new ArrayDeque<>();
        d.addLast(root);
        while (!d.isEmpty()) {
            TreeNode node = d.pollFirst();
            if (q.size() < k) {
                q.add(node.val);
            } else if (q.peek() > node.val) {
                q.poll();
                q.add(node.val);
            }
            if (node.left != null) d.addLast(node.left);
            if (node.right != null) d.addLast(node.right);
        }
        return q.peek();
    }
}
```
* 时间复杂度:树的遍历时间复杂度为 $O(n)$;使用优先队列(堆)复杂度为 $O(n\log{k})$。整体复杂度为 $O(n\log{k})$
* 空间复杂度:空间多少取决于 `d` 和 `q` 使用的容量,`q` 最多不超过 $k$ 个元素,复杂度为 $O(k)$,`d` 最多不超过二叉树的一层,复杂度为 $O(n)$。整体复杂度为 $O(n + k)$

---

### 中序遍历

上述两种节点,都没有利用该树为二叉搜索树的特性。

而我们知道,二叉搜索树的中序遍历是有序的,因此我们只需要对二叉搜索树执行中序遍历,并返回第 $k$ 小的值即可。

不熟悉二叉树的中序遍历的同学,可以看看 [(题解)783. 二叉搜索树节点最小距离](https://leetcode-cn.com/problems/minimum-distance-between-bst-nodes/solution/gong-shui-san-xie-yi-ti-san-jie-shu-de-s-7r17/)。

中序遍历有「迭代」和「递归」两种写法。

代码:
```Java
class Solution {
    public int kthSmallest(TreeNode root, int k) {
        Deque<TreeNode> d = new ArrayDeque<>();
        while (root != null || !d.isEmpty()) {
            while (root != null) {
                d.addLast(root);
                root = root.left;
            }
            root = d.pollLast();
            if (--k == 0) return root.val;
            root = root.right;
        }
        return -1; // never
    }
}
```

```Java
class Solution {
    int k, ans;
    public int kthSmallest(TreeNode root, int _k) {
        k = _k;
        dfs(root);
        return ans;
    }
    void dfs(TreeNode root) {
        if (root == null || k <= 0) return ;
        dfs(root.left);
        if (--k == 0) ans = root.val;
        dfs(root.right);
    }
}
```
* 时间复杂度:由于存在对 $k$ 大小的剪枝,因此整个遍历顺序是从小到大进行遍历,搜索到目标值即结束。复杂度为 $O(k)$
* 空间复杂度:令 $h$ 为树高,复杂度为 $O(h)$

### 最后

这是我们「刷穿 LeetCode」系列文章的第 `No.230` 篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先把所有不带锁的题目刷完。

在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。

为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:https://github.com/SharingSource/LogicStack-LeetCode 。

在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。


LeetCode/2211-2220/2213. 由单个字符重复的最长子字符串（困难）.md

2213. 由单个字符重复的最长子字符串

Question:
给你一个下标从 $0$ 开始的字符串 `s` 。另给你一个下标从 $0$ 开始、长度为 $k$ 的字符串 `queryCharacters`,一个下标从 $0$ 开始、长度也是 $k$ 的整数 下标 数组 `queryIndices`,这两个都用来描述 $k$ 个查询。

第 $i$ 个查询会将 `s` 中位于下标 $queryIndices[i]$ 的字符更新为 $queryCharacters[i]$。

返回一个长度为 $k$ 的数组 `lengths`,其中 $lengths[i]$ 是在执行第 $i$ 个查询之后 `s` 中仅由单个字符重复组成的 最长子字符串的长度 。

示例 1:
```
输入:s = "babacc", queryCharacters = "bcb", queryIndices = [1,3,3]

输出:[3,3,4]

解释:
- 第 1 次查询更新后 s = "bbbacc" 。由单个字符重复组成的最长子字符串是 "bbb" ,长度为 3 。
- 第 2 次查询更新后 s = "bbbccc" 。由单个字符重复组成的最长子字符串是 "bbb" 或 "ccc",长度为 3 。
- 第 3 次查询更新后 s = "bbbbcc" 。由单个字符重复组成的最长子字符串是 "bbbb" ,长度为 4 。
因此,返回 [3,3,4] 。
```
示例 2:
```
输入:s = "abyzz", queryCharacters = "aa", queryIndices = [2,1]

输出:[2,3]

解释:
- 第 1 次查询更新后 s = "abazz" 。由单个字符重复组成的最长子字符串是 "zz" ,长度为 2 。
- 第 2 次查询更新后 s = "aaazz" 。由单个字符重复组成的最长子字符串是 "aaa" ,长度为 3 。
因此,返回 [2,3] 。
```

提示:
* $1 <= s.length <= 10^5$
* `s` 由小写英文字母组成
* $k == queryCharacters.length == queryIndices.length$
* $1 <= k <= 10^5$
* `queryCharacters` 由小写英文字母组成
* $0 <= queryIndices[i] < s.length$

Solution:
### 线段树

这是一道经典的线段树应用题。

根据题意,涉及的操作 “似乎” 是「单点修改」和「区间查询」,那么根据 [(题解) 307. 区域和检索 - 数组可修改](https://leetcode-cn.com/problems/range-sum-query-mutable/solution/guan-yu-ge-lei-qu-jian-he-wen-ti-ru-he-x-41hv/) 的总结,我们应该使用的是「树状数组」吗?

其实并不是(或者说不能直接是),原因在于我们查询的是「修改过后 `s` 中相同字符连续段的最大长度」,而当我们进行所谓的「单点修改」时,会导致原本的连续段被破坏,或者形成新的连续段。也就是此处的修改对于结果而言,并不是单点的。

使用线段树求解,我们唯一需要考虑的是:在 `Node` 中维护些什么信息?

对于线段树的节点信息设计,通常会包含基本的左右端点 `l`、`r` 以及查询目标值 `val` ,然后再考虑维护 `val` 还需要一些什么辅助信息。

对于本题,我们还需要额外维护 `prefix` 和 `suffix`,分别代表「当前区间 $[l, r]$ 内前缀相同字符连续段的最大长度」和「当前区间 $[l, r]$ 内后缀相同字符连续段的最大长度」。

然后考虑每次修改,如何使用子节点信息更新父节点(`pushup` 操作):

* 对于一般性的合并(当前节点的两个子节点的衔接点不是相同字符)而言:

	* `tr[u].prefix = tr[u << 1].prefix`:当前父节点(区间)的前缀最大长度等于左子节点(区间)的前缀最大长度;
	* `tr[u].suffix = tr[u << 1 | 1].suffix`:当前父节点(区间)的后缀最大长度等于右子节点(区间)的后缀最大长度;
	* `tr[u].val = max(left.val, right.val)`:当前父节点(区间)的最大长度为两子节点(区间)的最大长度。
* 对于非一般性的合并(当前节点的两个子节点的衔接点为相同字符):
  * `tr[u].val = max(tr[u].val, left.suffix + right.prefix)`:首先可以确定的是「左区间的后缀」和「右区间的前缀」可以拼接在一起,因此可以使用拼接长度来尝试更新当前节点的最大值;
  * `tr[u].suffix = bLen + left.suffix`:其中 `bLen` 为右节点(区间)的长度。当且仅当右节点(区间)整一段都是相同字符时(即满足 `right.prefix = right.suffix = bLen`),可以使用 `bLen + left.suffix` 来更新 `tr[u].suffix`;
  * `tr[u].prefix = aLen + right.prefix`:其中 `aLen` 为左节点(区间)的长度。当且仅当左节点(区间)整一段都是相同字符时(即满足 `left.prefix = left.prefix = aLen`),可以使用 `aLen + right.prefix` 来更新 `tr[u].prefix `。

代码:
```Java
class Solution {
    class Node {
        int l, r, prefix, suffix, val;
        Node(int _l, int _r) {
            l = _l; r = _r;
            prefix = suffix = val = 1;
        }
    }
    char[] cs;
    Node[] tr;
    void build(int u, int l, int r) {
        tr[u] = new Node(l, r);
        if (l == r) return ;
        int mid = l + r >> 1;
        build(u << 1, l, mid);
        build(u << 1 | 1, mid + 1, r);
    }
    void update(int u, int x, char c) {
        if (tr[u].l == x && tr[u].r == x) {
            cs[x - 1] = c;
            return ;
        }
        int mid = tr[u].l + tr[u].r >> 1;
        if (x <= mid) update(u << 1, x, c);
        else update(u << 1 | 1, x, c);
        pushup(u);
    }
    int query(int u, int l, int r) {
        if (l <= tr[u].l && tr[u].r <= r) return tr[u].val;
        int ans = 0;
        int mid = tr[u].l + tr[u].r >> 1;
        if (l <= mid) ans = query(u << 1, l, r);
        if (r > mid) ans = Math.max(ans, query(u << 1 | 1, l, r));
        return ans;
    }
    void pushup(int u) {
        Node left = tr[u << 1], right = tr[u << 1 | 1];
        int aLen = left.r - left.l + 1, bLen = right.r - right.l + 1;
        char ac = cs[left.r - 1], bc = cs[right.l - 1];
        tr[u].prefix = left.prefix; tr[u].suffix = right.suffix;
        tr[u].val = Math.max(left.val, right.val);
        if (ac == bc) {
            if (left.prefix == aLen) tr[u].prefix = aLen + right.prefix;
            if (right.prefix == bLen) tr[u].suffix = bLen + left.suffix;
            tr[u].val = Math.max(tr[u].val, left.suffix + right.prefix);
        }
    }
    public int[] longestRepeating(String s, String queryCharacters, int[] queryIndices) {
        cs = s.toCharArray();
        int n = cs.length, m = queryCharacters.length();
        tr = new Node[n * 4];
        build(1, 1, n);
        for (int i = 0; i < n; i++) update(1, i + 1, cs[i]);
        int[] ans = new int[m];
        for (int i = 0; i < m; i++) {
            update(1, queryIndices[i] + 1, queryCharacters.charAt(i));
            ans[i] = query(1, 1, n);
        }
        return ans;
    }
}
```
* 时间复杂度:$O(m\log{n})$
* 空间复杂度:$O(n)$

### 最后

这是我们「刷穿 LeetCode」系列文章的第 `No.2213` 篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先把所有不带锁的题目刷完。

在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。

为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:https://github.com/SharingSource/LogicStack-LeetCode 。

在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。


LeetCode/2211-2220/2216. 美化数组的最少删除数（中等）.md

2216. 美化数组的最少删除数

Question:
给你一个下标从 `0` 开始的整数数组 `nums`,如果满足下述条件,则认为数组 `nums` 是一个 美丽数组 :

* `nums.length` 为偶数
* 对所有满足 `i % 2 == 0` 的下标 `i`,`nums[i] != nums[i + 1]` 均成立

注意,空数组同样认为是美丽数组。

你可以从 `nums` 中删除任意数量的元素。当你删除一个元素时,被删除元素右侧的所有元素将会向左移动一个单位以填补空缺,而左侧的元素将会保持 不变 。

返回使 `nums` 变为美丽数组所需删除的最少元素数目。

示例 1:
```
输入:nums = [1,1,2,3,5]

输出:1

解释:可以删除 nums[0] 或 nums[1] ,这样得到的 nums = [1,2,3,5] 是一个美丽数组。可以证明,要想使 nums 变为美丽数组,至少需要删除 1 个元素。
```
示例 2:
```
输入:nums = [1,1,2,2,3,3]

输出:2

解释:可以删除 nums[0] 和 nums[5] ,这样得到的 nums = [1,2,2,3] 是一个美丽数组。可以证明,要想使 nums 变为美丽数组,至少需要删除 2 个元素。
```

提示:
* $1 <= nums.length <= 10^5$
* $0 <= nums[i] <= 10^5$

Solution:
### 模拟

使用变量 `cnt` 代表已删除的元素个数,由于每次删除元素,剩余元素都会往前移动,因此遍历过程中,当前下标为 $i - cnt$。

若当前下标为偶数,且与下一位置元素相同,那么当前元素需被删除,令 `cnt` 自增。

最终数组长度为 $n - cnt$,若长度为奇数,需要再额外删除结尾元素(`cnt` 再加一),否则 `cnt` 即为答案。

Java 代码:

```Java
class Solution {
    public int minDeletion(int[] nums) {
        int n = nums.length, cnt = 0;
        for (int i = 0; i < n; i++) {
            if ((i - cnt) % 2 == 0 && i + 1 < n && nums[i] == nums[i + 1]) cnt++;
        }
        return (n - cnt) % 2 != 0 ? cnt + 1 : cnt;
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    int minDeletion(vector<int>& nums) {
        int n = nums.size(), cnt = 0;
        for (int i = 0; i < n; i++) {
            if ((i - cnt) % 2 == 0 && i + 1 < n && nums[i] == nums[i + 1]) cnt++;
        }
        return (n - cnt) % 2 != 0 ? cnt + 1 : cnt;
    }
};
```
Python 代码:
```Python
class Solution:
    def minDeletion(self, nums: List[int]) -> int:
        n, cnt = len(nums), 0
        for i in range(n):
            if (i - cnt) % 2 == 0 and i + 1 < n and nums[i] == nums[i + 1]:
                cnt += 1
        return cnt + 1 if (n - cnt) % 2 != 0 else cnt
```
TypeScript 代码:
```TypeScript
function minDeletion(nums: number[]): number {
    let n = nums.length, cnt = 0;
    for (let i = 0; i < n; i++) {
        if ((i - cnt) % 2 == 0 && i + 1 < n && nums[i] == nums[i + 1]) cnt++;
    }
    return (n - cnt) % 2 != 0 ? cnt + 1 : cnt;
};
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(1)$

### 最后

这是我们「刷穿 LeetCode」系列文章的第 `No.2216` 篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先把所有不带锁的题目刷完。

在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。

为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:https://github.com/SharingSource/LogicStack-LeetCode 。

在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。


LeetCode/2241-2250/2246. 相邻字符不同的最长路径（困难）.md

2246. 相邻字符不同的最长路径

Question:
给你一棵 树(即一个连通、无向、无环图),根节点是节点 `0`,这棵树由编号从 `0` 到 `n - 1` 的 `n` 个节点组成。

用下标从 `0` 开始、长度为 `n` 的数组 `parent` 来表示这棵树,其中 `parent[i]` 是节点 `i` 的父节点,由于节点 `0` 是根节点,所以 `parent[0] = -1`。

另给你一个字符串 `s`,长度也是 `n`,其中 `s[i]` 表示分配给节点 `i` 的字符。

请你找出路径上任意一对相邻节点都没有分配到相同字符的 最长路径 ,并返回该路径的长度。

示例 1:

```
输入:parent = [-1,0,0,1,1,2], s = "abacbe"

输出:3

解释:任意一对相邻节点字符都不同的最长路径是:0 -> 1 -> 3 。该路径的长度是 3 ,所以返回 3 。
可以证明不存在满足上述条件且比 3 更长的路径。
```

示例 2:

```
输入:parent = [-1,0,0,0], s = "aabc"

输出:3

解释:任意一对相邻节点字符都不同的最长路径是:2 -> 0 -> 3 。该路径的长度为 3 ,所以返回 3 。
```

提示:
* $n = parent.length = s.length$
* $1 <= n <= 10^5$
* 对所有 `i >= 1`,`0 <= parent[i] <= n - 1` 均成立
* $parent[0] = -1$
* `parent` 表示一棵有效的树
* `s` 仅由小写英文字母组成

Solution:
### DFS

起始先用 `parent` 进行建图,随后设计 `DFS` 函数来求解每个节点“往下”的最长路径:将当前节点 `cur` 作为传入参数,返回以节点“往下”的最长路径。

这是一个由子节点最长路径推导父节点最长路径的「自下而上」的推导过程。

> 先来关注该 `DFS`  的功能本身:

假设当前处理到的节点为 `u`,将要访问到的节点为 `j`。递归调用 `DFS` 函数拿到以节点 `j` 为根节点时的“往下”最大路径 `t`,并执行如下处理流程:

* 若节点 `j` 和节点 `u` 对应字符相同,说明将节点 `j` 拼接在节点 `u` 后面并非合法路径,跳过处理
* 否则使用 `t` 来更新以当前节点 `u` 为根时,最大的“往下”子路径 `res`(该值初始值为 `0`)

当处理完节点 `u` 的所有子节点,我们 `res + 1` 即是函数返回值(含义为在合法的最长路径本身拼接节点 `u`)。

> 再来关注 `DFS`  过程中,如何计算问题答案:

在 `DFS` 函数中,我们递归处理了所有节点,而**在真实最长路径在原树中的最高点**,自然也是被处理到的。

这引导我们可以在单次递归,处理当前节点时,使用变量 `l1` 和 `l2` 分别记录当前节点的「最大子路径」和「次大子路径」。

在处理完当前节点后,`1 + l1 + l2` 即是以当前节点作为路径最高点时的最大路径长度(含义为在合法「最大子路径」和「次大子路径」基础上拼接当前节点),用其更新全局变量 `ans`。

代码:
```Java
class Solution {
    int N = 100010, M = N, idx = 0, ans = 1;
    int[] he = new int[N], e = new int[M], ne = new int[M];
    char[] cs;
    void add(int a, int b) {
        e[idx] = b;
        ne[idx] = he[a];
        he[a] = idx++;
    }
    public int longestPath(int[] parent, String s) {
        Arrays.fill(he, -1);
        for (int i = 1; i < parent.length; i++) add(parent[i], i);
        cs = s.toCharArray();
        dfs(0);
        return ans;
    }
    int dfs(int u) {
        int res = 0;
        int l1 = 0, l2 = 0;
        for (int i = he[u]; i != -1; i = ne[i]) {
            int j = e[i];
            int t = dfs(j);
            if (cs[u] == cs[j]) continue;
            if (t > l1) {
                l2 = l1; l1 = t;
            } else if (t > l2) {
                l2 = t;
            }
            res = Math.max(res, t);
            ans = Math.max(ans, 1 + l1 + l2);
        }
        return res + 1;
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n)$

---

### 树形 DP

自然也是能够使用「树形 DP」思路来做。

只不过对于「定根树形 DP」来说,往往一遍 `DFS`  就能实现 $O(n)$ 做法。例如 [124. 二叉树中的最大路径和](https://leetcode.cn/problems/binary-tree-maximum-path-sum/solutions/2451624/gong-shui-san-xie-jian-dan-di-gui-shu-xi-nlci/)。

而「换根树形 DP」则只能通过对“方向”的拆分,用两遍 `DFS`  来进行求解。例如 [310. 最小高度树](https://leetcode.cn/problems/minimum-height-trees/solutions/1397905/by-ac_oier-7xio/) 和 [834. 树中距离之和](https://leetcode.cn/problems/sum-of-distances-in-tree/solutions/2449965/gong-shui-san-xie-shu-xing-dp-chang-gui-1v7ud/)。

在「定根树形 DP」题目中采用「换根树形 DP」做法,无论是从执行流程还是编码来说,都稍显“多余”(毕竟一次 `DFS`  就能以「最佳路径的最高点必然能够被处理」来得证答案的正确性),但在验证大家是否真正掌握「树形 DP」精髓来说,却有极大意义。

代码:

```Java
class Solution {
    int N = 100010, M = N, idx = 0;
    int[] he = new int[N], e = new int[M], ne = new int[M];
    char[] cs;
    int[] f1 = new int[N], f2 = new int[N], g = new int[N], p = new int[N];
    void add(int a, int b) {
        e[idx] = b;
        ne[idx] = he[a];
        he[a] = idx++;
    }
    public int longestPath(int[] parent, String s) {
        Arrays.fill(he, -1);
        for (int i = 1; i < parent.length; i++) add(parent[i], i);
        cs = s.toCharArray();
        dfs1(0);
        dfs2(0);
        int ans = 1;
        int[] temp = new int[3];
        for (int i = 0; i < parent.length; i++) {
            temp[0] = f1[i]; temp[1] = f2[i]; temp[2] = g[i];
            Arrays.sort(temp);
            ans = Math.max(ans, 1 + temp[1] + temp[2]);
        }
        return ans;
    }
    int dfs1(int u) {
        int ans = 0;
        for (int i = he[u]; i != -1; i = ne[i]) {
            int j = e[i];
            int t = dfs1(j);
            if (cs[u] == cs[j]) continue;
            if (t > f1[u]) {
                f2[u] = f1[u]; f1[u] = t;
                p[u] = j;
            } else if (t > f2[u]) {
                f2[u] = t;
            }
            ans = Math.max(ans, t);
        }
        return ans + 1;
    }
    void dfs2(int u) {
        for (int i = he[u]; i != -1; i = ne[i]) {
            int j = e[i];
            if (cs[u] != cs[j]) {
                // 往上再往上
                g[j] = g[u] + 1;
                // 往上再往下(根据 u 的往下最大路径是否由 j 参与决定使用 最大值 还是 次大值)
                if (p[u] != j) g[j] = Math.max(g[j], 1 + f1[u]);
                else g[j] = Math.max(g[j], 1 + f2[u]);
            }
            dfs2(j);
        }
    }
}
```
* 时间复杂度:常数较大的 $O(n)$。相比于一次 `DFS`  的做法来说,额外多了一次 `DFS`,以及构建答案时对定长数组的排序操作
* 空间复杂度:$O(n)$

### 最后

这是我们「刷穿 LeetCode」系列文章的第 `No.2346` 篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先把所有不带锁的题目刷完。

在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。

为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:https://github.com/SharingSource/LogicStack-LeetCode 。

在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。


Question:
给你一个下标从 `0` 开始大小为 `m x n` 的二维整数数组 `grid`,它表示一个网格图。

每个格子为下面 $3$ 个值之一:

* `0` 表示草地。
* `1` 表示着火的格子。
* `2` 表示一座墙,你跟火都不能通过这个格子。

一开始你在最左上角的格子 $(0, 0)$ ,你想要到达最右下角的安全屋格子 $(m - 1, n - 1)$ 。

每一分钟,你可以移动到相邻的草地格子。

每次你移动之后 ,着火的格子会扩散到所有不是墙的相邻格子。

请你返回你在初始位置可以停留的最多分钟数,且停留完这段时间后你还能安全到达安全屋。

如果无法实现,请你返回 $-1$。如果不管你在初始位置停留多久,你总是能到达安全屋,请你返回 $10^9$ 。

注意,如果你到达安全屋后,火马上到了安全屋,这视为你能够安全到达安全屋。

如果两个格子有共同边,那么它们为相邻格子。

示例 1:

```
输入:grid = [[0,2,0,0,0,0,0],[0,0,0,2,2,1,0],[0,2,0,0,1,2,0],[0,0,2,2,2,0,2],[0,0,0,0,0,0,0]]

输出:3

解释:上图展示了你在初始位置停留 3 分钟后的情形。
你仍然可以安全到达安全屋。
停留超过 3 分钟会让你无法安全到达安全屋。
```
示例 2:

```
输入:grid = [[0,0,0,0],[0,1,2,0],[0,2,0,0]]

输出:-1

解释:上图展示了你马上开始朝安全屋移动的情形。
火会蔓延到你可以移动的所有格子,所以无法安全到达安全屋。
所以返回 -1 。
```
示例 3:

```
输入:grid = [[0,0,0],[2,2,0],[1,2,0]]

输出:1000000000

解释:上图展示了初始网格图。
注意,由于火被墙围了起来,所以无论如何你都能安全到达安全屋。
所以返回 109 。
```

提示:
* $m = grid.length$
* $n = grid[i].length$
* $2 <= m, n <= 300$
* $4 <= m \times n <= 2 \times 10^4$
* `grid[i][j]` 是 `0`,`1` 或者 `2`。
* $grid[0][0] = grid[m - 1][n - 1] = 0$

Solution:
### 二分 + BFS

火势蔓延是一个固定的过程,只有人员移动需要决策。

假设人员最晚在 $t$ 秒后出发,仍能到达安全屋,说明人员对逃走路线的访问,要比火势更快。那么人员在更早的时间点($[0, t - 1]$ 秒后)出发,必然仍能按照原定路线到达安全屋(火势对路径的影响不变)。

因此,在以 $t$ 为分割点的(正整数)数轴上,具有二段性,可运用「二分」求分割点。

假设存在某个判定函数 `check`,用于检查人员在 $x$ 秒后出发能否到达安全屋,那么可知:

* 当实际延迟出发的秒数,小于等于 $t$ 秒,必然能安全到达
* 当实际延迟出发的描述,超过 $t$ 秒,必然不能安全到达

在人员移动路线中,“回头路”是没有意义的,因此人员对每个点的访问次数最多为一次。同时,不考虑墙的阻拦,火势也最多在不超过棋盘大小的时间内完全蔓延。

这指导我们最大延迟出发时间不会超过 $n \times m$,可在 $[0, n \times m]$ 值域内进行二分。

接下来,考虑如何实现 `check` 函数,函数入参为延迟出发秒数  $t$,返回值为延迟出发后能否到达安全屋。

首先,对于普通位置,如果火势和人员同时到达,是不允许的,而安全屋 $(n - 1, m - 1)$ 位置的同时到达,是允许的。

因此,我们需要使用两个二维数组 `fg` 和 `pg` 分别记录「火势」和「人员」到达某个位置的最早时间。

1. 创建用于模拟火势蔓延的队列 `fire`,遍历网格,将火源位置进行入队,更新火源位置 $fg[i][j] = 1$,表示火势在第一秒时最早出现在此处;

2. 运用 `BFS`,模拟 $t$ 秒的火势蔓延,火势在这 $t$ 秒内所蔓延到的新位置,均看作为起始火源,即有 $fg[i][j] = 1$。

   若执行完 $t$ 秒后,火势已蔓延到人员起始位置 $(0, 0)$,那么延迟 $t$ 秒出发不可行,直接返回 `False`;

3. 创建用于模拟人员移动的队列 `people`,将起始位置 $(0, 0)$ 进行入队,更新 $pg[0][0] = 1$。

   运用 `BFS`,按照「先火后人」的方式,同步模拟「火势蔓延」和「人员移动」过程。普通位置,只要火势蔓延到,那么人将无法移动到此处;安全屋位置,需要判断是否与火势同一时刻到达。

为了方便,将「火势蔓延」和「人员移动」统一成 `update` 操作,入参包括当前队列 `d`,标识位 `isFire`,以及移动偏移量 `offset`。

在进行 $t$ 秒的火势蔓延时,调用 $t$ 次的 `update(fire, true, 0)`。在火势和人员同步模拟时,分别调用 `update(fire, true, 1)` 和 `update(people, false, 1)`。

使用示例 $1$ 来举个 🌰:

Java 代码:

```Java
class Solution {
    int[][] dirs = new int[][]{{0,1}, {0,-1}, {1,0}, {-1,0}};
    int n, m;
    boolean ok;
    int[][] g, fg, pg;
    public int maximumMinutes(int[][] grid) {
        g = grid;
        n = g.length; m = g[0].length;
        fg = new int[n][m]; pg = new int[n][m];
        if (!check(0)) return -1;
        int l = 0, r = n * m;
        while (l < r) {
            int mid = l + r + 1 >> 1;
            if (check(mid)) l = mid;
            else r = mid - 1;
        }
        return r == m * n ? (int)1e9 : r;
    }
    boolean check(int t) {
        ok = false;
        Deque<int[]> frie = new ArrayDeque<>();
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < m; j++) {
                fg[i][j] = pg[i][j] = 0;
                if (g[i][j] == 1) {
                    fg[i][j] = 1;
                    frie.addLast(new int[]{i, j});
                }
            }
        }
        while(t-- > 0) update(frie, true, 0);  // 先执行 t 秒的火势蔓延
        if (fg[0][0] != 0) return false;
        Deque<int[]> people = new ArrayDeque<>();
        pg[0][0] = 1;
        people.addLast(new int[]{0, 0});
        while (!people.isEmpty()) {
            // 先火后人, 同步进行
            update(frie, true, 1);
            update(people, false, 1);
            if (ok) return true;
        }
        return false;
    }
    void update(Deque<int[]> deque, boolean isFire, int offset) {
        int sz = deque.size();
        while (sz-- > 0) {
            int[] info = deque.pollFirst();
            int x = info[0], y = info[1];
            for (int[] dir : dirs) {
                int nx = x + dir[0], ny = y + dir[1];
                if (nx < 0 || nx >= n || ny < 0 || ny >= m) continue;
                if (g[nx][ny] == 2) continue;
                if (isFire) {
                    if (fg[nx][ny] != 0) continue;
                    fg[nx][ny] = fg[x][y] + offset;
                } else {
                    if (nx == n - 1 && ny == m - 1 && (fg[nx][ny] == 0 || fg[nx][ny] == pg[x][y] + offset)) ok = true;  // 火尚未到达 或 同时到达
                    if (fg[nx][ny] != 0 || pg[nx][ny] != 0) continue;
                    pg[nx][ny] = pg[x][y] + offset;
                }
                deque.addLast(new int[]{nx, ny});
            }
        }
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    vector<vector<int>> dirs = {{0,1}, {0,-1}, {1,0}, {-1,0}};
    int n, m;
    bool ok;
    vector<vector<int>> g, fg, pg;
    int maximumMinutes(vector<vector<int>>& grid) {
        g = grid;
        n = g.size(); m = g[0].size();
        fg = vector<vector<int>>(n, vector<int>(m, 0)), pg = vector<vector<int>>(n, vector<int>(m, 0));
        if (!check(0)) return -1;
        int l = 0, r = n * m;
        while (l < r) {
            int mid = l + r + 1 >> 1;
            if (check(mid)) l = mid;
            else r = mid - 1;
        }
        return r == n * m ? (int)1e9 : r;
    }
    bool check(int t) {
        ok = false;
        deque<vector<int>> frie;
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < m; j++) {
                fg[i][j] = pg[i][j] = 0;
                if (g[i][j] == 1) {
                    fg[i][j] = 1;
                    frie.push_back({i, j});
                }
            }
        }
        while (t-- > 0) update(frie, true, 0);
        if (fg[0][0] != 0) return false;
        deque<vector<int>> people;
        pg[0][0] = 1;
        people.push_back({0, 0});
        while (!people.empty()) {
            update(frie, true, 1);
            update(people, false, 1);
            if (ok) return true;
        }
        return false;
    }
    void update(deque<vector<int>>& deque, bool isFire, int offset) {
        int sz = deque.size();
        while (sz-- > 0) {
            vector<int> info = deque.front();
            deque.pop_front();
            int x = info[0], y = info[1];
            for (vector<int> dir : dirs) {
                int nx = x + dir[0], ny = y + dir[1];
                if (nx < 0 || nx >= n || ny < 0 || ny >= m) continue;
                if (g[nx][ny] == 2) continue;
                if (isFire) {
                    if (fg[nx][ny] != 0) continue;
                    fg[nx][ny] = fg[x][y] + offset;
                } else {
                    if (nx == n - 1 && ny == m - 1 && (fg[nx][ny] == 0 || fg[nx][ny] == pg[x][y] + offset)) ok = true;
                    if (fg[nx][ny] != 0 || pg[nx][ny] != 0) continue;
                    pg[nx][ny] = pg[x][y] + offset;
                }
                deque.push_back({nx, ny});
            }
        }
    }
};
```
Python 代码:
```Python
from collections import deque

class Solution:
    def maximumMinutes(self, grid: List[List[int]]) -> int:
        dirs = [(0,1),(0,-1),(1,0),(-1,0)]
        g = grid
        n, m = len(g), len(g[0])
        fg, pg, ok = None, None, False

        def update(d, isFire, offset):
            nonlocal ok
            for _ in range(len(d)):
                x, y = d.popleft()
                for dx, dy in dirs:
                    nx, ny = x + dx, y + dy
                    if nx < 0 or nx >= n or ny < 0 or ny >= m: continue
                    if g[nx][ny] == 2: continue
                    if isFire:
                        if fg[nx][ny] != 0: continue
                        fg[nx][ny] = fg[x][y] + offset
                    else:
                        if nx == n - 1 and ny == m - 1 and (fg[nx][ny] == 0 or fg[nx][ny] == pg[x][y] + offset): ok = True
                        if fg[nx][ny] != 0 or pg[nx][ny] != 0: continue
                        pg[nx][ny] = pg[x][y] + offset
                    d.append((nx, ny))

        def check(t):
            nonlocal pg, fg, ok
            ok = False
            pg = [[0] * m for _ in range(n)]
            fg = [[g[i][j] == 1 for j in range(m)] for i in range(n)]
            fire = deque([(i, j) for i, j in product(range(n), range(m)) if g[i][j] == 1])
            for _ in range(t): update(fire, True, 0)
            if fg[0][0] != 0: return False
            people = deque()
            pg[0][0] = 1
            people.append((0, 0))
            while people:
                update(fire, True, 1)
                update(people, False, 1)
                if ok: return True
            return False

        if not check(0): return -1
        l, r = 0, n * m
        while l < r:
            mid = l + r + 1 >> 1
            if check(mid): l = mid
            else: r = mid - 1
        return int(1e9) if r == n * m else r
```
TypeScript 代码:
```TypeScript
function maximumMinutes(grid: number[][]): number {
    const dirs = [[0,1],[0,-1],[1,0],[-1,0]];
    const g = grid;
    const n = g.length, m = g[0].length;
    const fg = Array.from({length: n}, () => Array(m).fill(0)), pg = Array.from({length: n}, () => Array(m).fill(0));
    let ok = false;
    const update = function(d: number[][], isFire: boolean, offset: number) {
        let sz = d.length;
        while (sz-- > 0) {
            const info = d.shift();
            const x = info[0], y = info[1];
            for (let di of dirs) {
                const nx = x + di[0], ny = y + di[1];
                if (nx < 0 || nx >= n || ny < 0 || ny >= m) continue;
                if (g[nx][ny] == 2) continue;
                if (isFire) {
                    if (fg[nx][ny] != 0) continue;
                    fg[nx][ny] = fg[x][y] + offset;
                } else {
                    if (nx == n - 1 && ny == m - 1 && (fg[nx][ny] == 0 || fg[nx][ny] == pg[x][y] + offset)) ok = true;
                    if (fg[nx][ny] != 0 || pg[nx][ny] != 0) continue;
                    pg[nx][ny] = pg[x][y] + offset;
                }
                d.push([nx, ny]);
            }
        }
    }
    const check = function(t: number): boolean {
        ok = false
        const fire = new Array()
        for (let i = 0; i < n; i++) {
            for (let j = 0; j < m; j++) {
                fg[i][j] = pg[i][j] = 0;
                if (g[i][j] == 1) {
                    fg[i][j] = 1;
                    fire.push([i, j]);
                }
            }
        }
        while (t-- > 0) update(fire, true, 0);
        if (fg[0][0] != 0) return false;
        const people = new Array();
        pg[0][0] = 1;
        people.push([0, 0]);
        while (people.length != 0) {
            update(fire, true, 1);
            update(people, false, 1);
            if (ok) return true;
        }
        return false;
    }
    if (!check(0)) return -1;
    let l = 0, r = n * m;
    while (l < r) {
        const mid = l + r + 1 >> 1;
        if (check(mid)) l = mid;
        else r = mid - 1;
    }
    return r == n * m ? 1e9 : r;
};
```
* 时间复杂度:在值域 $[0, n \times m]$ 范围内进行二分,二分 `check` 的 `BFS`  实现复杂度为 $O(n \times m)$。整体复杂度为 $O(nm \log{nm})$
* 空间复杂度:$O(n \times m)$

---

### BFS + 分类讨论

经过上述解法,我们发现存在大量重复计算:例如每次唯一确定的“火势蔓延”过程,以及每次根据最新起始火势(由延迟出发时间 $t$ 所决定)进行的“人员移动”过程,都是不必要的,可通过比较双方到达时间来求解。

具体的,还是用 `fg` 和 `pg`,分别预处理出「火势」和「人员」到达每个网格的最早时间。其中火势蔓延唯一确定,而人员的预处理是在不考虑火势的情况下进行。

根据 $f = fg[n-1][m-1]$ 和 $p = pg[n-1][m-1]$ 进行分情况讨论:

* 若 $p = 0$:人与安全屋不连通,返回 $-1$;

* 若 $f = 0$:火与安全屋不连通,同时上述条件不满足($p \neq 0$),即人与安全屋是联通 ,返回 $1e9$;

* 若 $f < p$:火和人都能到达安全屋。即使不考虑人员中途被火影响(人员可能无法按照最佳路线前往安全屋)的情况下,火也比人要更早到达安全屋,返回 $-1$;

* 若 $f \geqslant p$:理想情况下,人比火更早到达安全屋,但存在「人火同时到达」、「人员中途被烧」或「通路被火拦截」等问题,需要进一步分情况讨论:

  不难发现,由于安全屋的位于 $(n - 1, m - 1)$,人员只能从 $(n - 1, m - 2)$ 或 $(n - 2, m - 1)$ 两个位置之一到达安全屋(这两个属于普通位置,不允许人和火同时到达),因此可以将「对特殊位置安全屋」的讨论转为「对普通位置」的讨论:

  * 若 $pg[n - 1][m - 2] \neq 0$,人与该位置联通,且 $f - p + pg[n - 1][m - 2] < fg[n - 1][m - 2]$,人比火更早到达该位置,返回 $f - p$;
  * 若 $pg[n - 2][m - 1] \neq 0$,人与该位置联通,且 $f - p + pg[n - 2][m - 1] < fg[n - 2][m - 1]$,人比火更早到达该位置,返回 $f - p$;
  * 否则,说明延迟 $f - p$ 秒出发,唯二的通路会被火提前拦截,需要早一秒出发,返回 $f - p - 1$;

Java 代码:
```Java
class Solution {
    int[][] dirs = new int[][]{{0,1}, {0,-1}, {1,0}, {-1,0}};
    int[][] g;
    int n, m;
    public int maximumMinutes(int[][] grid) {
        g = grid;
        n = g.length; m = g[0].length;
        int[][] fg = new int[n][m], pg = new int[n][m];
        Deque<int[]> fire = new ArrayDeque<>();
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < m; j++) {
                if (g[i][j] == 1) {
                    fg[i][j] = 1;
                    fire.addLast(new int[]{i, j});
                }
            }
        }
        bfs(fire, fg);
        Deque<int[]> people = new ArrayDeque<>();
        people.addLast(new int[]{0, 0});
        pg[0][0] = 1;
        bfs(people, pg);
        int p = pg[n - 1][m - 1], f = fg[n - 1][m - 1], ans = f - p;
        if (p == 0) return -1;
        if (f == 0) return (int)1e9;
        if (p > f) return -1;
        if (pg[n - 1][m - 2] != 0 && ans + pg[n - 1][m - 2] < fg[n - 1][m - 2]) return ans;
        if (pg[n - 2][m - 1] != 0 && ans + pg[n - 2][m - 1] < fg[n - 2][m - 1]) return ans;
        return ans - 1;
    }
    void bfs(Deque<int[]> d, int[][] time) {
        while (!d.isEmpty()) {
            int[] info = d.pollFirst();
            int x = info[0], y = info[1];
            for (int[] dir : dirs) {
                int nx = x + dir[0], ny = y + dir[1];
                if (nx < 0 || nx >= n || ny < 0 || ny >= m) continue;
                if (g[nx][ny] == 2) continue;
                if (time[nx][ny] != 0) continue;
                time[nx][ny] = time[x][y] + 1;
                d.addLast(new int[]{nx, ny});
            }
        }
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    vector<vector<int>> dirs = {{0,1}, {0,-1}, {1,0}, {-1,0}};
    vector<vector<int>> g;
    int n, m;
    int maximumMinutes(vector<vector<int>>& grid) {
        g = grid;
        n = g.size(); m = g[0].size();
        vector<vector<int>> fg = vector<vector<int>>(n, vector<int>(m, 0)), pg = vector<vector<int>>(n, vector<int>(m, 0));
        deque<pair<int, int>> fire;
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < m; j++) {
                if (g[i][j] == 1) {
                    fg[i][j] = 1;
                    fire.push_back({i, j});
                }
            }
        }
        bfs(fire, fg);
        deque<pair<int, int>> people;
        people.push_back({0, 0});
        pg[0][0] = 1;
        bfs(people, pg);
        int p = pg[n - 1][m - 1], f = fg[n - 1][m - 1], ans = f - p;
        if (p == 0) return -1;
        if (f == 0) return (int)1e9;
        if (p > f) return -1;
        if (pg[n - 1][m - 2] != 0 && ans + pg[n - 1][m - 2] < fg[n - 1][m - 2]) return ans;
        if (pg[n - 2][m - 1] != 0 && ans + pg[n - 2][m - 1] < fg[n - 2][m - 1]) return ans;
        return ans - 1;
    }
    void bfs(deque<pair<int, int>>& d, vector<vector<int>>& time) {
        while (!d.empty()) {
            pair<int, int> info = d.front();
            d.pop_front();
            int x = info.first, y = info.second;
            for (vector<int> dir : dirs) {
                int nx = x + dir[0], ny = y + dir[1];
                if (nx < 0 || nx >= n || ny < 0 || ny >= m) continue;
                if (g[nx][ny] == 2) continue;
                if (time[nx][ny] != 0) continue;
                time[nx][ny] = time[x][y] + 1;
                d.push_back({nx, ny});
            }
        }
    }
};
```
Python 代码:
```Python
from collections import deque

class Solution:
    def maximumMinutes(self, grid: List[List[int]]) -> int:
        dirs = [(0,1), (0,-1), (1,0), (-1,0)]
        g = grid
        n, m = len(g), len(g[0])

        def bfs(d, tn):
            while d:
                x, y = d.popleft()
                for dx, dy in dirs:
                    nx, ny = x + dx, y + dy
                    if nx < 0 or nx >= n or ny < 0 or ny >= m: continue
                    if g[nx][ny] == 2 or tn[nx][ny]: continue
                    tn[nx][ny] = tn[x][y] + 1
                    d.append((nx, ny))

        fg = [[g[i][j] == 1 for j in range(m)] for i in range(n)]
        fire = deque([(i, j) for i, j in product(range(n), range(m)) if g[i][j] == 1])
        bfs(fire, fg)
        pg = [[0] * m for _ in range(n)]
        pg[0][0] = 1
        people = deque([(0, 0)])
        bfs(people, pg)

        p, f = pg[-1][-1], fg[-1][-1]
        ans = f - p
        if p == 0: return -1
        if f == 0: return int(1e9)
        if p > f: return -1
        if pg[-1][-2] != 0 and ans + pg[-1][-2] < fg[-1][-2]: return ans
        if pg[-2][-1] != 0 and ans + pg[-2][-1] < fg[-2][-1]: return ans
        return ans - 1
```
TypeScript 代码:
```TypeScript
function maximumMinutes(grid: number[][]): number {
    const g = grid;
    const n = g.length, m = g[0].length;
    const dirs = [[0, 1], [0, -1], [1, 0], [-1, 0]];
    const bfs = function (d: number[][], time: number[][]): void {
        while (d.length > 0) {
            const info = d.shift() as number[];
            const x = info[0], y = info[1];
            for (const dir of dirs) {
                const nx = x + dir[0], ny = y + dir[1];
                if (nx < 0 || nx >= n || ny < 0 || ny >= m) continue;
                if (g[nx][ny] == 2) continue;
                if (time[nx][ny] != 0) continue;
                time[nx][ny] = time[x][y] + 1;
                d.push([nx, ny]);
            }
        }
    }
    const fg = Array.from({ length: n }, () => Array(m).fill(0));
    const pg = Array.from({ length: n }, () => Array(m).fill(0));
    const fire = [];
    for (let i = 0; i < n; i++) {
        for (let j = 0; j < m; j++) {
            if (g[i][j] == 1) {
                fg[i][j] = 1;
                fire.push([i, j]);
            }
        }
    }
    bfs(fire, fg);
    const people = [];
    people.push([0, 0]);
    pg[0][0] = 1;
    bfs(people, pg);
    const p = pg[n - 1][m - 1], f = fg[n - 1][m - 1], ans = f - p;
    if (p == 0) return -1;
    if (f == 0) return 1e9;
    if (p > f) return -1;
    if (pg[n - 1][m - 2] != 0 && ans + pg[n - 1][m - 2] < fg[n - 1][m - 2]) return ans;
    if (pg[n - 2][m - 1] != 0 && ans + pg[n - 2][m - 1] < fg[n - 2][m - 1]) return ans;
    return ans - 1;
};
```
* 时间复杂度:$O(n \times m)$
* 空间复杂度:$O(n \times m)$

### 最后

这是我们「刷穿 LeetCode」系列文章的第 `No.2258` 篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先把所有不带锁的题目刷完。

在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。

为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:https://github.com/SharingSource/LogicStack-LeetCode 。

在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。


LeetCode/2291-2300/2300. 咒语和药水的成功对数（中等）.md

2300. 咒语和药水的成功对数

Question:
给你两个正整数数组 `spells` 和 `potions`,长度分别为 `n` 和 `m`,其中 `spells[i]` 表示第 `i` 个咒语的能量强度,`potions[j]` 表示第 `j` 瓶药水的能量强度。

同时给你一个整数 `success`。一个咒语和药水的能量强度相乘如果大于等于 `success`,那么它们视为一对成功的组合。

请你返回一个长度为 `n` 的整数数组 `pairs`,其中 `pairs[i]` 是能跟第 `i` 个咒语成功组合的 药水 数目。

示例 1:
```
输入:spells = [5,1,3], potions = [1,2,3,4,5], success = 7

输出:[4,0,3]

解释:
- 第 0 个咒语:5 * [1,2,3,4,5] = [5,10,15,20,25] 。总共 4 个成功组合。
- 第 1 个咒语:1 * [1,2,3,4,5] = [1,2,3,4,5] 。总共 0 个成功组合。
- 第 2 个咒语:3 * [1,2,3,4,5] = [3,6,9,12,15] 。总共 3 个成功组合。
所以返回 [4,0,3] 。
```
示例 2:
```
输入:spells = [3,1,2], potions = [8,5,8], success = 16

输出:[2,0,2]

解释:
- 第 0 个咒语:3 * [8,5,8] = [24,15,24] 。总共 2 个成功组合。
- 第 1 个咒语:1 * [8,5,8] = [8,5,8] 。总共 0 个成功组合。
- 第 2 个咒语:2 * [8,5,8] = [16,10,16] 。总共 2 个成功组合。
所以返回 [2,0,2] 。
```

提示:
* $n = spells.length$
* $m = potions.length$
* $1 <= n, m <= 10^5$
* $1 <= spells[i], potions[i] <= 10^5$
* $1 <= success <= 1010$

Solution:
### 排序 + 二分

为了方便,我们将 `spells` 记为 `a`,将 `potions` 记为 `b`,将 `success` 记为 `t`。

对于每个 $a[i]$,有多少个 $b[j]$ 满足 $a[i] \times b[j] \geqslant t$,等价于问数组 `b` 中值大于等于 $\frac{t}{a[i]}$ 的个数,这容易让我们想到先对数组 `b` 排升序,再通过二分找到满足该条件的最小下标,从该下标到数组结尾,均为满足条件的 $b[j]$。

Java 代码:
```Java
class Solution {
    public int[] successfulPairs(int[] a, int[] b, long t) {
        int n = a.length, m = b.length;
        int[] ans = new int[n];
        Arrays.sort(b);
        for (int i = 0; i < n; i++) {
            double cur = t * 1.0 / a[i];
            int l = 0, r = m - 1;
            while (l < r) {
                int mid = l + r >> 1;
                if (b[mid] >= cur) r = mid;
                else l = mid + 1;
            }
            if (b[r] * 1L * a[i] >= t) ans[i] = m - r;
        }
        return ans;
    }
}
```
Python 代码:
```Python
class Solution:
    def successfulPairs(self, a: List[int], b: List[int], t: int) -> List[int]:
        # n, m = len(a), len(b)
        # b.sort()
        # ans = [0] * n
        # for i in range(n):
        #     cur = t / a[i]
        #     l, r = 0, m - 1
        #     while l < r:
        #         mid = l + r >> 1
        #         if b[mid] >= cur: r = mid
        #         else: l = mid + 1
        #     ans[i] = m - r if b[r] * a[i] >= t else 0
        # return ans
        b.sort()
        return [len(b) - bisect_left(b, t / x) for x in a]
```
C++ 代码:
```C++
class Solution {
public:
    vector<int> successfulPairs(vector<int>& a, vector<int>& b, long long t) {
        // int n = a.size(), m = b.size();
        // vector<int> ans(n);
        // sort(b.begin(), b.end());
        // for (int i = 0; i < n; i++) {
        //     double cur = t * 1.0 / a[i];
        //     int l = 0, r = m - 1;
        //     while (l < r) {
        //         int mid = l + r >> 1;
        //         if (b[mid] >= cur) r = mid;
        //         else l = mid + 1;
        //     }
        //     if (b[r] * 1L * a[i] >= t) ans[i] = m - r;
        // }
        // return ans;
        int n = a.size(), m = b.size();
        vector<int> ans(n);
        sort(b.begin(), b.end());
        for (int i = 0; i < n; i++) ans[i] = b.end() - lower_bound(b.begin(), b.end(), t * 1.0 / a[i]);
        return ans;
    }
};
```
TypeScript 代码:
```TypeScript
function successfulPairs(a: number[], b: number[], t: number): number[] {
    const n = a.length, m = b.length;
    const ans = new Array(n).fill(0);
    b.sort((a,b)=>a-b);
    for (let i = 0; i < n; i++) {
        const cur = t * 1.0 / a[i];
        let l = 0, r = m - 1;
        while (l < r) {
            const mid = l + r >> 1;
            if (b[mid] >= cur) r = mid;
            else l = mid + 1;
        }
        if (b[r] * a[i] >= t) ans[i] = m - r;
    }
    return ans;
};
```
* 时间复杂度:对数组 `b` 排序的复杂度为 $O(m\log{m})$;构建答案时,每次在值域 $[0, m - 1]$ 范围内进行二分,复杂度为 $O(n\log{m})$。整体复杂度为 $O(m\log{m} + n\log{m})$
* 空间复杂度:$O(\log{m} + n)$

### 最后

这是我们「刷穿 LeetCode」系列文章的第 `No.2300` 篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先把所有不带锁的题目刷完。

在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。

为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:https://github.com/SharingSource/LogicStack-LeetCode 。

在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。