933. 最近的请求次数

Question:
写一个 `RecentCounter` 类来计算特定时间范围内最近的请求。

请你实现 `RecentCounter` 类:
* `RecentCounter()` 初始化计数器,请求数为 $0$ 。
* `int ping(int t)` 在时间 $t$ 添加一个新请求,其中 $t$ 表示以毫秒为单位的某个时间,并返回过去 $3000$ 毫秒内发生的所有请求数(包括新请求)。确切地说,返回在 $[t-3000, t]$ 内发生的请求数。

保证 每次对 `ping` 的调用都使用比之前更大的 $t$ 值。

示例 1:
```
输入:
["RecentCounter", "ping", "ping", "ping", "ping"]
[[], [1], [100], [3001], [3002]]

输出:
[null, 1, 2, 3, 3]

解释:
RecentCounter recentCounter = new RecentCounter();
recentCounter.ping(1);     // requests = [1],范围是 [-2999,1],返回 1
recentCounter.ping(100);   // requests = [1, 100],范围是 [-2900,100],返回 2
recentCounter.ping(3001);  // requests = [1, 100, 3001],范围是 [1,3001],返回 3
recentCounter.ping(3002);  // requests = [1, 100, 3001, 3002],范围是 [2,3002],返回 3
```

提示:
* $1 <= t <= 10^9$
* 保证每次对 `ping` 调用所使用的 $t$ 值都 严格递增
* 至多调用 `ping` 方法 $10^4$ 次

Solution:
> 以下内容(支持任意的 $[l, r]$ 查询)基于我漏看了 $t$ 递增这一条件。由于 $t$ 递增,因此往前距离大于 $3000$ 的记录无须保留,从而简化了问题,可以使用队列直接做,今天打字很多了,不补充了。

### 基本分析

根据题意,题目涉及「单点修改」和「区间查询」,根据 [区间求和问题](https://leetcode-cn.com/problems/range-sum-query-mutable/solution/guan-yu-ge-lei-qu-jian-he-wen-ti-ru-he-x-41hv/) 的总结,可以使用「树状数组」和「线段树」进行求解。

但是留意到 $t$ 的数据范围为 $1e9$,虽然调用次数是 $1e4$,但由于本题是「强制在线」问题,因此我们无法利用「离散化」手段来解决 `MLE`  问题,而要使用「线段树(动态开点)」来解决。

---

### 线段树(动态开点)

动态开点的优势在于,不需要事前构造空树,而是在插入操作 `update` 和查询操作 `query` 时根据访问需要进行「开点」操作。由于我们不保证查询和插入都是连续的,因此对于父节点 $u$ 而言,我们不能通过 `u << 1` 和 `u << 1  | 1` 的固定方式进行访问,而要将节点 $tr[u]$ 的左右节点所在 `tr` 数组的下标进行存储,分别记为 `ls` 和 `rs` 属性。对于 $tr[u].ls = 0$ 和 $tr[u].rs = 0$ 则是代表子节点尚未被创建,当需要访问到它们,而又尚未创建的时候,则将其进行创建。

线段树的插入和查询都是 $\log{n}$ 的(如果涉及区间修改,则由懒标记来确保 $\log{n}$ 复杂度),因此我们在单次操作的时候,最多会创建数量级为 $\log{n}$ 的点,因此空间复杂度为 $O(m\log{n})$,而不是 $O(4 * n)$,而开点数的预估需不能仅仅根据 $\log{n}$ 来进行,还要对具体常数进行分析,才能得到准确的点数上界。

动态开点相比于原始的线段树实现,本质仍是使用「满二叉树」的形式进行存储,只不过是按需创建区间,如果我们是按照连续段进行查询或插入,最坏情况下仍然会占到 $4 * n$ 的空间,因此盲猜 $\log{n}$ 的常数在 $4$ 左右,保守一点可以直接估算到 $6$,因此我们可以估算点数为 $6 * m * \log{n}$,其中 $n = 1e9$ 和 $m = 1e4$ 分别代表值域大小和查询次数。

当然一个比较实用的估点方式可以「尽可能的多开点数」,利用题目给定的空间上界和我们创建的自定义类(结构体)的大小,尽可能的多开( `Java` 的 $128M$ 可以开到 $5 * 10^6$ 以上)。

代码:
```Java
class RecentCounter {
    class Node {
        // ls 和 rs 分别代表当前节点(区间)的左右子节点在 tr 的下标
        // val 代表在当前节点(区间)所包含的数的个数
        int ls, rs, val;
    }
    int N = (int)1e9, M = 800010, idx = 1;
    Node[] tr = new Node[M];
    void update(int u, int lc, int rc, int x, int v) {
        if (lc == x && rc == x) {
            tr[u].val += (rc - lc + 1) * v;
            return ;
        }
        lazyCreate(u);
        int mid = lc + rc >> 1;
        if (x <= mid) update(tr[u].ls, lc, mid, x, v);
        else update(tr[u].rs, mid + 1, rc, x, v);
        pushup(u);
    }
    int query(int u, int lc, int rc, int l, int r) {
        if (l <= lc && rc <= r) return tr[u].val;
        lazyCreate(u);
        int mid = lc + rc >> 1, ans = 0;
        if (l <= mid) ans = query(tr[u].ls, lc, mid, l, r);
        if (r > mid) ans += query(tr[u].rs, mid + 1, rc, l, r);
        return ans;
    }
    void lazyCreate(int u) {
        if (tr[u] == null) tr[u] = new Node();
        if (tr[u].ls == 0) {
            tr[u].ls = ++idx;
            tr[tr[u].ls] = new Node();
        }
        if (tr[u].rs == 0) {
            tr[u].rs = ++idx;
            tr[tr[u].rs] = new Node();
        }
    }
    void pushup(int u) {
        tr[u].val = tr[tr[u].ls].val + tr[tr[u].rs].val;
    }
    public int ping(int t) {
        update(1, 1, N, t, 1);
        return query(1, 1, N, Math.max(0, t - 3000), t);
    }
}
```
* 时间复杂度:令 `ping` 的调用次数为 $m$,值域大小为 $n$,线段树的插入和查询复杂度均为 $O(\log{n})$
* 空间复杂度:$O(m * \log{n})$

---

### 分块

另外一个稍有遗憾的算法是「分块」。

通常来说,值域范围为 $n = 1e9$,我们可以定义块大小为 $\sqrt{n}$,这样时空复杂度都是 $O(\sqrt{n})$。

回到本题,我们定义一个 `region` 数组,用于存储每个块所包含的数的个数。

对于在位置 $x$ 插入一个值 $v$ 而言,我们可以计算位置 $x$ 所在的块编号 $idx = \left \lfloor \frac{x}{m} \right \rfloor$,然后对块大小进行累加操作($region[idx] += x$)。同时由于在查询 $[t - 3000, t]$ 时不总是覆盖完整的块(也就是我们可能需要询问某一段的总个数,而这一段仅是块内的部分区间),因此我们需要额外记录位置 $x$ 当前所包含的总数是多少,本题值域大小达到了 $1e9$,因此不能使用静态数组来做,只能使用哈希表来实现动态记录。

我们可以分析单个 `ping` 操作的计算量/复杂度:
* `update` 操作:$O(1)$;
* `query` 操作:不限制区域大小的话,计算量为 $1e5 * C$($C$ 是一个小于 $4$ 的常数),由于存在 $[t - 3000, t]$ 的限制,相当于这部分操作全部变成块内了,计算量固定为 $3000$。

于是有通过了所有样例,居然还 `TLE` 的结果???(这是啥测评机制

但经过分析,我们知道瓶颈在于块内操作过多,我们可以利用 $[t - 3000, t]$ 来优化我们的分块算法:直接定义块大小为 $300$,然后倒推出需要多个块空间,分块算法就可以过了(~~特别说明:通过时间为 `2022-05-06`,未来可能还会有无聊的人来卡分块。~~ 我不信,卡不掉 🤣

代码:
```Java
class RecentCounter {
    static int m = 300, n = (int)(1e9 / m) + 10;
    static Map<Integer, Integer> map = new HashMap<>();
    static int[] region = new int[n];
    int getIdx(int x) {
        return x / m;
    }
    void update(int x, int v) {
        map.put(x, map.getOrDefault(x, 0) + v);
        region[getIdx(x)] += v;
    }
    int query(int l, int r) {
        int ans = 0;
        if (getIdx(l) == getIdx(r)) {
            for (int k = l; k <= r; k++) ans += map.getOrDefault(k, 0);
        } else {
            int i = l, j = r;
            while (getIdx(i) == getIdx(l)) ans += map.getOrDefault(i++, 0);
            while (getIdx(j) == getIdx(r)) ans += map.getOrDefault(j--, 0);
            for (int k = getIdx(i); k <= getIdx(j); k++) ans += region[k];
        }
        return ans;
    }
    public RecentCounter() {
        map.clear();
        Arrays.fill(region, 0);
    }
    public int ping(int t) {
        update(t, 1);
        return query(Math.max(0, t - 3000), t);
    }
}
```
* 时间复杂度:通常情况下:调用次数为 $m$,值域大小为 $n$,复杂度为 $O(\sqrt{n})$;本题:$O(C)$,其中 $C = 300$ 为块大小
* 空间复杂度:通常情况下:$O(m +\sqrt{n})$;本题:$O(m + M)$,其中 $M$ 为分块数组大小

### 最后

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

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

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

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


Question:
给你一个大小为 `n x n` 的二元矩阵 `g`,其中 `1` 表示陆地,`0` 表示水域。

岛是由四面相连的 `1` 形成的一个最大组,即不会与非组内的任何其他 `1` 相连,`g` 中恰好存在两座岛。

你可以将任意数量的 `0` 变为 `1` ,以使两座岛连接起来,变成一座岛。

返回必须翻转的 `0` 的最小数目。

示例 1:
```
输入:g = [[0,1],[1,0]]

输出:1
```
示例 2:
```
输入:g = [[0,1,0],[0,0,0],[0,0,1]]

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

输出:1
```

提示:
* $n = g.length = g[i].length$
* $2 <= n <= 100$
* `g[i][j]` 为 `0` 或 `1`
* `g` 中恰有两个岛

Solution:
### 并查集 + 双向 BFS

**使用「并查集」将两个岛标记出来,然后将两个岛的点分别入队,再运用「双向 BFS」来找最短通路。**

对于所有满足 $g[i][j] = 1$ 的节点与其四联通的方向,值同为 $1$ 的节点进行并查集连通性维护。

随后建立两个队列 `d1` 和 `d2` 分别存储两个岛的节点(以二元组 $(x, y)$ 的方式出入队),并使用两个哈希表 `m1` 和 `m2` 来记录从两岛屿出发到达该节点所消耗的步数(以节点的一维编号为 `key`,以消耗步数为 `value`)。

最后是使用「双向 BFS」来求解使两岛屿联通的最小通路:每次从队列中较少的进行拓展,只有尚未被处理过的节点(没有被当前哈希表所记录)才进行入队并更新消耗步数,当拓展节点在另外一个队列对应的哈希表表中出现过,说明找到了最短通路。

Java 代码:
```Java
class Solution {
    static int N = 10010;
    static int[] p = new int[N];
    static int[][] dirs = new int[][]{{1,0},{-1,0},{0,1},{0,-1}};
    int n;
    int getIdx(int x, int y) {
        return x * n + y;
    }
    int find(int x) {
        if (p[x] != x) p[x] = find(p[x]);
        return p[x];
    }
    void union(int x, int y) {
        p[find(x)] = p[find(y)];
    }
    public int shortestBridge(int[][] g) {
        n = g.length;
        for (int i = 0; i <= n * n; i++) p[i] = i;
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < n; j++) {
                if (g[i][j] == 0) continue;
                for (int[] di : dirs) {
                    int x = i + di[0], y = j + di[1];
                    if (x < 0 || x >= n || y < 0 || y >= n) continue;
                    if (g[x][y] == 0) continue;
                    union(getIdx(i, j), getIdx(x, y));
                }
            }
        }
        int a = -1, b = -1;
        Deque<int[]> d1 = new ArrayDeque<>(), d2 = new ArrayDeque<>();
        Map<Integer, Integer> m1 = new HashMap<>(), m2 = new HashMap<>();
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < n; j++) {
                if (g[i][j] == 0) continue;
                int idx = getIdx(i, j), root = find(idx);
                if (a == -1) a = root;
                else if (a != root && b == -1) b = root;
                if (root == a) {
                    d1.addLast(new int[]{i, j});
                    m1.put(idx, 0);
                } else if (root == b) {
                    d2.addLast(new int[]{i, j});
                    m2.put(idx, 0);
                }
            }
        }
        while (!d1.isEmpty() && !d2.isEmpty()) {
            int t = -1;
            if (d1.size() < d2.size()) t = update(d1, m1, m2);
            else t = update(d2, m2, m1);
            if (t != -1) return t - 1;
        }
        return -1; // never
    }
    int update(Deque<int[]> d, Map<Integer, Integer> m1, Map<Integer, Integer> m2) {
        int sz = d.size();
        while (sz-- > 0) {
            int[] info = d.pollFirst();
            int x = info[0], y = info[1], idx = getIdx(x, y), step = m1.get(idx);
            for (int[] di : dirs) {
                int nx = x + di[0], ny = y + di[1], nidx = getIdx(nx, ny);
                if (nx < 0 || nx >= n || ny < 0 || ny >= n) continue;
                if (m1.containsKey(nidx)) continue;
                if (m2.containsKey(nidx)) return step + 1 + m2.get(nidx);
                d.addLast(new int[]{nx, ny});
                m1.put(nidx, step + 1);
            }
        }
        return -1;
    }
}
```
Python 代码:
```Python
import queue

class Solution:
    def shortestBridge(self, g: List[List[int]]) -> int:
        def getIdx(x, y):
            return x * n + y

        def find(x):
            if p[x] != x:
                p[x] = find(p[x])
            return p[x]

        def union(x, y):
            p[find(x)] = p[find(y)]

        def update(d, cur, other):
            sz = d.qsize()
            while sz != 0:
                x, y = d.get()
                idx, step = getIdx(x, y), cur.get(getIdx(x, y))
                for di in dirs:
                    nx, ny = x + di[0], y + di[1]
                    nidx = getIdx(nx, ny)
                    if nx < 0 or nx >= n or ny < 0 or ny >= n:
                        continue
                    if nidx in cur:
                        continue
                    if nidx in other:
                        return step + 1 + other.get(nidx)
                    d.put([nx, ny])
                    cur[nidx] = step + 1
                sz -= 1
            return -1

        n = len(g)
        p = [i for i in range(n * n + 10)]
        dirs = [[1, 0], [-1, 0], [0, 1], [0, -1]]
        for i in range(n):
            for j in range(n):
                if g[i][j] == 0:
                    continue
                for di in dirs:
                    x, y = i + di[0], j + di[1]
                    if x < 0 or x >= n or y < 0 or y >= n:
                        continue
                    if g[x][y] == 0:
                        continue
                    union(getIdx(i, j), getIdx(x, y))
        a, b = -1, -1
        d1, d2 = queue.Queue(), queue.Queue()
        m1, m2 = {}, {}
        for i in range(n):
            for j in range(n):
                if g[i][j] == 0:
                    continue
                idx, root = getIdx(i, j), find(getIdx(i, j))
                if a == -1:
                    a = root
                elif a != root and b == -1:
                    b = root
                if a == root:
                    d1.put([i, j])
                    m1[idx] = 0
                elif b == root:
                    d2.put([i, j])
                    m2[idx] = 0
        while not d1.empty() and not d2.empty():
            t = -1
            if d1.qsize() < d2.qsize():
                t = update(d1, m1, m2)
            else:
                t = update(d2, m2, m1)
            if t != -1:
                return t - 1
        return -1
```
TypeScript 代码:
```TypeScript
let n: number
const p = new Array<number>(10010).fill(0)
const dirs = [[0,1],[0,-1],[1,0],[-1,0]]
function shortestBridge(g: number[][]): number {
    function getIdx(x: number, y: number): number {
        return x * n + y
    }
    function find(x: number): number {
        if (p[x] != x) p[x] = find(p[x])
        return p[x]
    }
    function union(x: number, y: number): void {
        p[find(x)] = p[find(y)]
    }
    function update(d: Array<Array<number>>, m1: Map<number, number>, m2: Map<number, number>): number {
        let sz = d.length
        while (sz-- > 0) {
            const info = d.shift()
            const x = info[0], y = info[1], idx = getIdx(x, y), step = m1.get(idx)
            for (const di of dirs) {
                const nx = x + di[0], ny = y + di[1], nidx = getIdx(nx, ny)
                if (nx < 0 || nx >= n || ny < 0 || ny >= n) continue
                if (m1.has(nidx)) continue
                if (m2.has(nidx)) return step + 1 + m2.get(nidx)
                d.push([nx, ny])
                m1.set(nidx, step + 1)
            }
        }
        return -1
    }
    n = g.length
    for (let i = 0; i < n * n; i++) p[i] = i
    for (let i = 0; i < n; i++) {
        for (let j = 0; j < n; j++) {
            if (g[i][j] == 0) continue
            for (const di of dirs) {
                const x = i + di[0], y = j + di[1]
                if (x < 0 || x >= n || y < 0 || y >= n) continue
                if (g[x][y] == 0) continue
                union(getIdx(i, j), getIdx(x, y))
            }
        }
    }
    let a = -1, b = -1
    const d1 = new Array<number[]>(), d2 = new Array<number[]>()
    const m1 = new Map<number, number>(), m2 = new Map<number, number>()
    for (let i = 0; i < n; i++) {
        for (let j = 0; j < n; j++) {
            if (g[i][j] == 0) continue
            const idx = getIdx(i, j), root = find(idx)
            if (a == -1) a = root
            else if (a != root && b == -1) b = root
            if (a == root) {
                d1.push([i, j])
                m1.set(idx, 0)
            } else if (b == root) {
                d2.push([i, j])
                m2.set(idx, 0)
            }
        }
    }
    while (d1.length != 0 && d2.length != 0) {
        let t = -1
        if (d1.length < d2.length) t = update(d1, m1, m2)
        else t = update(d2, m2, m1)
        if (t != -1) return t - 1
    }
    return -1
}
```
* 时间复杂度:$O(n^2)$
* 空间复杂度:$O(n^2)$

### 最后

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

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

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

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


937. 重新排列日志文件

Question:
给你一个日志数组 logs。每条日志都是以空格分隔的字串,其第一个字为字母与数字混合的 标识符 。

有两种不同类型的日志:

* 字母日志:除标识符之外,所有字均由小写字母组成
* 数字日志:除标识符之外,所有字均由数字组成

请按下述规则将日志重新排序:

* 所有 字母日志 都排在 数字日志 之前。
* 字母日志 在内容不同时,忽略标识符后,按内容字母顺序排序;在内容相同时,按标识符排序。
* 数字日志 应该保留原来的相对顺序。

返回日志的最终顺序。

示例 1:
```
输入:logs = ["dig1 8 1 5 1","let1 art can","dig2 3 6","let2 own kit dig","let3 art zero"]

输出:["let1 art can","let3 art zero","let2 own kit dig","dig1 8 1 5 1","dig2 3 6"]

解释:
字母日志的内容都不同,所以顺序为 "art can", "art zero", "own kit dig" 。
数字日志保留原来的相对顺序 "dig1 8 1 5 1", "dig2 3 6" 。
```
示例 2:
```
输入:logs = ["a1 9 2 3 1","g1 act car","zo4 4 7","ab1 off key dog","a8 act zoo"]

输出:["g1 act car","a8 act zoo","ab1 off key dog","a1 9 2 3 1","zo4 4 7"]
```

提示:
* $1 <= logs.length <= 100$
* $3 <= logs[i].length <= 100$
* $logs[i]$ 中,字与字之间都用 单个 空格分隔
* 题目数据保证 $logs[i]$ 都有一个标识符,并且在标识符之后至少存在一个字

Solution:
### 自定义类 + 排序

根据排序规则,我们需要对每个 $str[i]$ 进行裁剪处理,从而得知每个 $str[i]$ 是属于「字母日志」还是「数字日志」,以及得到 `sign` 部分和 `content` 部分。

在排序过程中,每个 $str[i]$ 会被访问多次,为了让每个 $str[i]$ 只进行一次这样的预处理工作,我们可以自定义类,将这部分工作放到类的实例化去做。

最后是简单将 $str[i]$ 转存成 `Log` 实例,自定义排序,用排序结果构造答案的基本逻辑。

代码:
```Java
class Solution {
    class Log {
        int type, idx;
        String ori, sign, content;
        Log(String s, int _idx) {
            idx = _idx;
            int n = s.length(), i = 0;
            while (i < n && s.charAt(i) != ' ') i++;
            sign = s.substring(0, i);
            content = s.substring(i + 1);
            ori = s;
            type = Character.isDigit(content.charAt(0)) ? 1 : 0;
        }
    }
    public String[] reorderLogFiles(String[] logs) {
        int n = logs.length;
        List<Log> list = new ArrayList<>();
        for (int i = 0; i < n; i++) list.add(new Log(logs[i], i));
        Collections.sort(list, (a, b)->{
            if (a.type != b.type) return a.type - b.type;
            if (a.type == 1) return a.idx - b.idx;
            return !a.content.equals(b.content) ? a.content.compareTo(b.content) : a.sign.compareTo(b.sign);
        });
        String[] ans = new String[n];
        for (int i = 0; i < n; i++) ans[i] = list.get(i).ori;
        return ans;
    }
}
```
* 时间复杂度:将所有的 $str[i]$ 转存成 `Log` 实例,复杂度为 $O(n)$;排序复杂度为 $O(n\log{n})$;构造答案复杂度为 $O(n)$。整体复杂度为 $O(n\log{n})$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


938. 二叉搜索树的范围和

Question:
给定二叉搜索树的根结点 root,返回值位于范围 [low, high] 之间的所有结点的值的和。

示例 1:

```
输入:root = [10,5,15,3,7,null,18], low = 7, high = 15

输出:32
```
示例 2:

```
输入:root = [10,5,15,3,7,13,18,1,null,6], low = 6, high = 10

输出:23
```

提示:
* 树中节点数目在范围 [1, 2 * $10^4$] 内
* 1 <= Node.val <= $10^5$
* 1 <= low <= high <= $10^5$
* 所有 Node.val 互不相同

Solution:
### 基本思路

这又是众多「二叉搜索树遍历」题目中的一道。

**二叉搜索树的中序遍历是有序的。**

只要对其进行「中序遍历」即可得到有序列表,在遍历过程中判断节点值是否符合要求,对于符合要求的节点值进行累加即可。

二叉搜索树的「中序遍历」有「迭代」和「递归」两种形式。**由于给定了值范围 $[low, high]$,因此可以在遍历过程中做一些剪枝操作,但并不影响时空复杂度。**

---

### 递归

递归写法十分简单,属于树的遍历中最简单的实现方式。

代码:
```Java
class Solution {
    int low, high;
    int ans;
    public int rangeSumBST(TreeNode root, int _low, int _high) {
        low = _low; high = _high;
        dfs(root);
        return ans;
    }
    void dfs(TreeNode root) {
        if (root == null) return;
        dfs(root.left);
        if (low <= root.val && root.val <= high) ans += root.val;
        dfs(root.right);
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n)$

---

### 迭代

迭代其实就是使用「栈」来模拟递归过程,也属于树的遍历中的常见实现形式。

一般简单的面试中如果问到树的遍历,面试官都不会对「递归」解法感到满意,因此掌握「迭代/非递归」写法同样重要。

代码:
```Java
class Solution {
    public int rangeSumBST(TreeNode root, int low, int high) {
        int ans = 0;
        Deque<TreeNode> d = new ArrayDeque<>();
        while (root != null || !d.isEmpty()) {
            while (root != null) {
                d.addLast(root);
                root = root.left;
            }
            root = d.pollLast();
            if (low <= root.val && root.val <= high) {
                ans += root.val;
            }
            root = root.right;
        }
        return ans;
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


940. 不同的子序列 II

Question:
给定一个字符串 `s`,计算 `s` 的 不同非空子序列 的个数。

因为结果可能很大,所以返回答案需要对 $10^9 + 7$ 取余 。

字符串的 子序列 是经由原字符串删除一些(也可能不删除)字符但不改变剩余字符相对位置的一个新字符串。

例如,`"ace"` 是 `"abcde"` 的一个子序列,但 `"aec"` 不是。

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

输出:7

解释:7 个不同的子序列分别是 "a", "b", "c", "ab", "ac", "bc", 以及 "abc"。
```
示例 2:
```
输入:s = "aba"

输出:6

解释:6 个不同的子序列分别是 "a", "b", "ab", "ba", "aa" 以及 "aba"。
```
示例 3:
```
输入:s = "aaa"

输出:3

解释:3 个不同的子序列分别是 "a", "aa" 以及 "aaa"。
```

提示:
* $1 <= s.length <= 2000$
* `s` 仅由小写英文字母组成

Solution:
### 序列 DP

为了方便,我们令 `s` 下标从 $1$ 开始,定义 $f[i][j]$ 为考虑前 $i$ 个字符,且结尾字符为 $j$ 的不同子序列的个数,其中 $j$ 的范围为 $[0, 25]$ 代指小写字符 `a-z`。

我们有显而易见的初始化条件 $f[0][X] = 0$,最终答案为 $\sum_{i = 0}^{25}f[n][i]$。

不失一般性考虑 $f[i][j]$ 该如何转移,根据 $s[i]$ 是否为 $j$ 进行分情况讨论:

* $s[i] \neq j$ : 由于状态定义限定了结尾字符必须是 $j$,因而 $s[i]$ 必然不会用到,此时有:

$$
f[i][j] = f[i - 1][j]
$$

* $s[i] = j$ : 此时 $s[i]$ 可作为结尾元素,同时由于我们统计的是「不同」的子序列个数,因而「以 $j$ 结尾的子序列方案数」与「以 $s[i]$ 结尾的子序列方案数」完全等价。
    对于以 $s[i]$ 作为子序列结尾字符的方案数,容易想到其方案数等于「$s[i]$ 单独作为子序列」+「$s[i]$ 拼接在其余子序列后面形成新子序列」,即有:

$$
f[i][j] = 1 + \sum_{k = 0}^{25} f[i - 1][k]
$$

Java 代码:
```Java
class Solution {
    int MOD = (int)1e9+7;
    public int distinctSubseqII(String s) {
        int n = s.length(), ans = 0;
        int[][] f = new int[n + 1][26];
        for (int i = 1; i <= n; i++) {
            int c = s.charAt(i - 1) - 'a';
            for (int j = 0; j < 26; j++) {
                if (c != j) {
                    f[i][j] = f[i - 1][j];
                } else {
                    int cur = 1;
                    for (int k = 0; k < 26; k++) cur = (cur + f[i - 1][k]) % MOD;
                    f[i][j] = cur;
                }
            }
        }
        for (int i = 0; i < 26; i++) ans = (ans + f[n][i]) % MOD;
        return ans;
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    int distinctSubseqII(string s) {
        int n = s.length(), MOD = 1e9 + 7;
        vector<vector<int>> f(n + 1, vector<int>(26, 0));
        for (int i = 1; i <= n; ++i) {
            int c = s[i - 1] - 'a';
            for (int j = 0; j < 26; ++j) {
                if (c != j) {
                    f[i][j] = f[i - 1][j];
                } else {
                    int cur = 1;
                    for (int k = 0; k < 26; ++k) cur = (cur + f[i - 1][k]) % MOD;
                    f[i][j] = cur;
                }
            }
        }
        int ans = 0;
        for (int i = 0; i < 26; ++i) ans = (ans + f[n][i]) % MOD;
        return ans;
    }
};
```
Python 代码:
```Python
class Solution:
    def distinctSubseqII(self, s: str) -> int:
        n, MOD = len(s), 1e9+7
        f = [[0] * 26 for _ in range(n + 1)]
        for i in range(1, n + 1):
            c = ord(s[i - 1]) - ord('a')
            for j in range(26):
                f[i][j] = f[i - 1][j] if c != j else (1 + sum(f[i - 1])) % MOD
        return int(sum(f[n]) % MOD)
```
TypeScript 代码:
```TypeScript
function distinctSubseqII(s: string): number {
    const MOD = 1e9+7
    let n = s.length, ans = 0
    const f = new Array<Array<number>>(n + 1)
    for (let i = 0; i <= n; i++) f[i] = new Array<number>(26).fill(0)
    for (let i = 1; i <= n; i++) {
        const c = s.charCodeAt(i - 1) - 'a'.charCodeAt(0)
        for (let j = 0; j < 26; j++) {
            if (c != j) {
                f[i][j] = f[i - 1][j]
            } else {
                let cur = 1
                for (let k = 0; k < 26; k++) cur = (cur + f[i - 1][k]) % MOD
                f[i][j] = cur
            }
        }
    }
    for (let i = 0; i < 26; i++) ans = (ans + f[n][i]) % MOD
    return ans
}
```
* 时间复杂度:$O(n \times C^2)$,其中 $C = 26$ 为字符集大小
* 空间复杂度:$O(n \times C)$

---

### 转移优化

根据转移的依赖关系,实现上,我们并不需要真正记录每一个 $f[i][X]$,而可以直接记录一个总的不同子序列方案数 `ans`。

这可以避免每次计算新状态时,都累加前一个 $f[i - 1][X]$ 的值,有效减低时空复杂度。

Java 代码:
```Java
class Solution {
    int MOD = (int)1e9+7;
    public int distinctSubseqII(String s) {
        int n = s.length(), ans = 0;
        int[] f = new int[26];
        for (int i = 0; i < n; i++) {
            int c = s.charAt(i) - 'a', prev = f[c];
            f[c] = (ans + 1) % MOD;
            ans = (ans + f[c]) % MOD;
            ans = (ans - prev + MOD) % MOD;
        }
        return ans;
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    int distinctSubseqII(string s) {
        int n = s.length(), ans = 0, MOD = 1e9 + 7;
        vector<int> f(26, 0);
        for (int i = 0; i < n; ++i) {
            int c = s[i] - 'a', prev = f[c];
            f[c] = (ans + 1) % MOD;
            ans = (ans + f[c]) % MOD;
            ans = (ans - prev + MOD) % MOD;
        }
        return ans;
    }
};
```
Python 代码:
```Python
class Solution:
    def distinctSubseqII(self, s: str) -> int:
        n, MOD, ans = len(s), 1e9+7, 0
        f = [0] * 26
        for i in range(n):
            c = ord(s[i]) - ord('a')
            prev = f[c]
            f[c] = (ans + 1) % MOD
            ans = (ans + f[c] - prev) % MOD
        return int(ans)
```
TypeScript 代码:
```TypeScript
function distinctSubseqII(s: string): number {
    const MOD = 1e9+7
    let n = s.length, ans = 0
    const f = new Array<number>(26).fill(0)
    for (let i = 0; i < n; i++) {
        const c = s.charCodeAt(i) - 'a'.charCodeAt(0), prev = f[c]
        f[c] = (ans + 1) % MOD
        ans = (ans + f[c]) % MOD
        ans = (ans - prev + MOD) % MOD
    }
    return ans
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(C)$

### 最后

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

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

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

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


942. 增减字符串匹配

Question:
由范围 $[0,n]$ 内所有整数组成的 $n + 1$ 个整数的排列序列可以表示为长度为 $n$ 的字符串 `s` ,其中:

* 如果 `perm[i] < perm[i + 1]` ,那么 `s[i] == 'I'`
* 如果 `perm[i] > perm[i + 1]` ,那么 `s[i] == 'D'`

给定一个字符串 `s` ,重构排列 `perm` 并返回它。如果有多个有效排列 `perm`,则返回其中 任何一个 。

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

输出:[0,4,1,3,2]
```
示例 2:
```
输入:s = "III"

输出:[0,1,2,3]
```
示例 3:
```
输入:s = "DDI"

输出:[3,2,0,1]
```

提示:
* $1 <= s.length <= 10^5$
* `s` 只包含字符 `"I"` 或 `"D"`

Solution:
### 构造

根据题意,我们需要设想一种「构造」方式,使得利用 `s` 创建序列的过程能够顺利进行。

直觉上容易猜想到当 $s[i] = I$ 时,使用当前最小值进行构造,当 $s[i] = D$ 时使用当前最大值进行构造。

使用「归纳法」来证明该做法的可行性(可用数的范围为 $[0, n]$,构造答案为 $ans$ 数组):

1. 对于边界情况:起始最小值为 $0$,最大值为 $n$:

    * 若有 $s[0] = I$,使用 $0$ 进行构造(即有 $ans[0] = 0$),可用数范围变为 $[1, n]$,后续再利用 $s[1]$ 进行构造 $ans[1]$ 时,可以确保始终满足 $ans[1] > ans[0]$,即上一步构造的正确性与下一步的决策无关;

    * 若有 $s[1] = D$,使用 $n$ 进行构造(即有 $ans[0] = n$),可用数范围变为 $[0, n - 1]$,后续再利用 $s[1]$ 进行构造 $ans[1]$ 时,可以确保始终满足 $ans[1] < ans[0]$,即上一步构造的正确性与下一步的决策无关;

2. 原问题的非边界情况:与边界情况等同,可用数仍是连续段,且当前的决策无须考虑额外的条件(上一步的构造的正确性已有保证)。

此外,我们证明了该构造方法必然能够利用顺利构造出合法序列。

该做法成立的本质为:**始终确保可用数是连续段,每次选择位于边界的数进行构造,可以直接确保当前构造回合的正确性,从而让边界情况的归纳推理可以运用到每个构造回合。**

代码:
```Java
class Solution {
    public int[] diStringMatch(String s) {
        int n = s.length(), l = 0, r = n, idx = 0;
        int[] ans = new int[n + 1];
        for (int i = 0; i < n; i++) {
            if (s.charAt(i) == 'I') ans[idx++] = l++;
            else ans[idx++] = r--;
        }
        ans[idx] = l;
        return ans;
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


Question:
给定一个字符串数组 `words`,找到以 `words` 中每个字符串作为子字符串的最短字符串。如果有多个有效最短字符串满足题目条件,返回其中 任意一个 即可。

我们可以假设 `words` 中没有字符串是 `words` 中另一个字符串的子字符串。

示例 1:
```
输入:words = ["alex","loves","leetcode"]

输出:"alexlovesleetcode"

解释:"alex","loves","leetcode" 的所有排列都会被接受。
```
示例 2:
```
输入:words = ["catg","ctaagt","gcta","ttca","atgcatc"]

输出:"gctaagttcatgcatc"
```

提示:
* $1 <= words.length <= 12$
* $1 <= words[i].length <= 20$
* `words[i]` 由小写英文字母组成
* `words` 中的所有字符串互不相同

Solution:
### 状压 DP

为了方便,将 `words` 记为 `ws`。

预处理二维数组 `g` 来表示字符串 `ws[i]` 和 `ws[j]` 的重叠部分的长度:若 `g[i][j] = len` 代表字符串 `ws[i]` 长度为 `len`  的后缀与字符串 `ws[j]` 长度为 `len` 的前缀相同。

另外用一个二进制数 `status` 来代表当前超级串 `ans` 对 `ws` 的使用(覆盖)情况:**若 `status` 的第 `i` 位为 `1` 代表字符串 `ws[i]` 已被使用(即 `ws[i]` 已是 `ans` 的子串),若 `status` 的第 `i` 位为 `0` 代表 `ws[i]` 未被使用。**

我们知道,当所有的 $g[i][j] = 0$ 时,代表所有拼接方式长度均为 $\sum_{i = 0}^{n - 1}ws[i].length$,即不能通过产生重叠部分来缩短超级串的长度。

因此,**最小化超级串 `ans` 的长度等价于最大化重叠部分的长度**。

定义 $f[s][i]$ 代表当前状态为 `s` 且当前最后一个使用到的字符串为 `ws[i]` (当前超级串 `ans` 的结尾部分为 `ws[i]`)时的最大重合长度。

最终超级串的长度为所有字符串的总长度 $\sum_{i = 0}^{n - 1}ws[i].length$ 减去最大重合长度 $\max(f[2^n - 1][i])$。

不失一般性考虑 $f[s][i]$ 可用于更新哪些状态,我们可枚举接在字符串 `ws[i]` 后面的字符串 `ws[j]` 为何值:

1. 由于每个字符串只能使用一次,转移需要满足 `s` 的第 `i` 位为 $1$,`s` 的第 `j` 位为 $0$ 的前提条件,含义为 `ws[i]` 已被使用,而 `ws[j]` 未被使用
2. 满足前提条件 $1$,代表 `ws[j]` 可接在 `ws[i]` 后面,此时有状态转移方程:
	$$
	    f[s | (1 << j)][j] = f[s][i] + g[i][j]
  $$

接下来,考虑如何构建具体方案。

**使用二维数组 $p[s][i]$ 记录每个状态是由哪个前驱转移而来**:若有 $p[s][i] = j$,代表取得最大重叠长度过程中,字符串 `ws[j]` 接在 `ws[i]` 后面。

我们从后往前对 `ans` 进行构造,若 `ans = ws[0] + ws[1] + ... + ws[k - 1] + ws[k]`,我们是先找 `ws[k]`,再通过 `ws[k]` 找 `ws[k - 1]`,直到将整个 `ans` 构建出来。

构造过程中使用变量解释如下:

* `ans` 为具体的超级串
* `status` 代表当前还有哪些字符串待拼接到,初始化为 $2^n - 1$,代表还没有任何字符串拼接到 `ans` 中
* `idx` 代表当前处理到的字符串下标,初始化通过遍历所有的 $f[2^n - 1][i]$ 找到合适的 $i$ 作为 `idx`
* `last` 代表前一个处理到字符串下标,初始化为 `-1`

一些细节:当 `last` 不为初始值 `-1` 时,需要跳过 `ws[idx]` 与 `ws[last]` 的重复部分进行拼接。

Java 代码:
```Java
class Solution {
    public String shortestSuperstring(String[] ws) {
        int n = ws.length, mask = 1 << n;
        int[][] g = new int[n][n];
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < n; j++) {
                String a = ws[i], b = ws[j];
                int l1 = a.length(), l2 = b.length(), len = Math.min(l1, l2);
                for (int k = len; k >= 1; k--) {
                    if (a.substring(l1 - k).equals(b.substring(0, k))) {
                        g[i][j] = k;
                        break;
                    }
                }
            }
        }
        int[][] f = new int[mask][n], p = new int[mask][n];
        for (int s = 0; s < mask; s++) {
            for (int i = 0; i < n; i++) {
                if (((s >> i) & 1) == 0) continue;
                for (int j = 0; j < n; j++) {
                    if (((s >> j) & 1) == 1) continue;
                    if (f[s | (1 << j)][j] <= f[s][i] + g[i][j]) {
                        f[s | (1 << j)][j] = f[s][i] + g[i][j];
                        p[s | (1 << j)][j] = i;
                    }
                }
            }
        }
        int max = f[mask - 1][0], idx = 0, last = -1, status = mask - 1;
        for (int i = 1; i < n; i++) {
            if (max < f[mask - 1][i]) {
                max = f[mask - 1][i];
                idx = i;
            }
        }
        String ans = "";
        while (status != 0) {
            if (last == -1) ans = ws[idx];
            else ans = ws[idx].substring(0, ws[idx].length() - g[idx][last]) + ans;
            last = idx;
            idx = p[status][idx];
            status ^= (1 << last);
        }
        return ans;
    }
}
```
Python 代码:
```Python
class Solution:
    def shortestSuperstring(self, ws: List[str]) -> str:
        n, mask = len(ws), 1 << len(ws)
        g = [[0 for _ in range(n)] for _ in range(n)]
        for i in range(n):
            for j in range(n):
                a, b = ws[i], ws[j]
                l1, l2 = len(a), len(b)
                length = min(l1, l2)
                for k in range(length, 0, -1):
                    if a[l1 - k:] == b[:k]:
                        g[i][j] = k
                        break
        f = [[0 for _ in range(n)] for _ in range(mask)]
        p = [[0 for _ in range(n)] for _ in range(mask)]
        for s in range(mask):
            for i in range(n):
                if (s >> i) & 1 == 0:
                    continue
                for j in range(n):
                    if (s >> j) & 1 == 1:
                        continue
                    if f[s | (1 << j)][j] <= f[s][i] + g[i][j]:
                        f[s | (1 << j)][j] = f[s][i] + g[i][j]
                        p[s | (1 << j)][j] = i

        max_val = f[mask - 1][0]
        idx, last, status = 0, -1, mask - 1
        for i in range(1, n):
            if max_val < f[mask - 1][i]:
                max_val = f[mask - 1][i]
                idx = i
        ans = ""
        while status != 0:
            if last == -1:
                ans = ws[idx]
            else:
                ans = ws[idx][:len(ws[idx]) - g[idx][last]] + ans
            last = idx
            idx = p[status][idx]
            status ^= 1 << last
        return ans
```
C++ 代码:
```C++
class Solution {
public:
    string shortestSuperstring(vector<string>& ws) {
 int n = ws.size(), mask = 1 << n;
        vector<vector<int>> g(n, vector<int>(n, 0));
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < n; j++) {
                string a = ws[i], b = ws[j];
                int l1 = a.length(), l2 = b.length(), len = min(l1, l2);
                for (int k = len; k >= 1; k--) {
                    if (a.substr(l1 - k) == b.substr(0, k)) {
                        g[i][j] = k;
                        break;
                    }
                }
            }
        }
        vector<vector<int>> f(mask, vector<int>(n, 0)), p(mask, vector<int>(n, 0));
        for (int s = 0; s < mask; s++) {
            for (int i = 0; i < n; i++) {
                if (((s >> i) & 1) == 0) continue;
                for (int j = 0; j < n; j++) {
                    if (((s >> j) & 1) == 1) continue;
                    if (f[s | (1 << j)][j] <= f[s][i] + g[i][j]) {
                        f[s | (1 << j)][j] = f[s][i] + g[i][j];
                        p[s | (1 << j)][j] = i;
                    }
                }
            }
        }
        int max = f[mask - 1][0], idx = 0, last = -1, status = mask - 1;
        for (int i = 1; i < n; i++) {
            if (max < f[mask - 1][i]) {
                max = f[mask - 1][i];
                idx = i;
            }
        }
        string ans = "";
        while (status != 0) {
            if (last == -1) ans = ws[idx];
            else ans = ws[idx].substr(0, ws[idx].length() - g[idx][last]) + ans;
            last = idx;
            idx = p[status][idx];
            status ^= (1 << last);
        }
        return ans;
    }
};
```
TypeScript 代码:
```TypeScript
function shortestSuperstring(ws: string[]): string {
    const n = ws.length, mask = 1 << n;
    const g = new Array(n).fill(0).map(() => new Array(n).fill(0));
    for (let i = 0; i < n; i++) {
        for (let j = 0; j < n; j++) {
            const a = ws[i], b = ws[j];
            const l1 = a.length, l2 = b.length;
            const len = Math.min(l1, l2);
            for (let k = len; k >= 1; k--) {
                if (a.substring(l1 - k) === b.substring(0, k)) {
                    g[i][j] = k;
                    break;
                }
            }
        }
    }
    const f = new Array(mask).fill(0).map(() => new Array(n).fill(0));
    const p = new Array(mask).fill(0).map(() => new Array(n).fill(0));
    for (let s = 0; s < mask; s++) {
        for (let i = 0; i < n; i++) {
            if (((s >> i) & 1) === 0) continue;
            for (let j = 0; j < n; j++) {
                if (((s >> j) & 1) === 1) continue;
                if (f[s | (1 << j)][j] <= f[s][i] + g[i][j]) {
                    f[s | (1 << j)][j] = f[s][i] + g[i][j];
                    p[s | (1 << j)][j] = i;
                }
            }
        }
    }
    let max = f[mask - 1][0], idx = 0, last = -1, status = mask - 1;
    for (let i = 1; i < n; i++) {
        if (max < f[mask - 1][i]) {
            max = f[mask - 1][i];
            idx = i;
        }
    }
    let ans = "";
    while (status != 0) {
        if (last === -1) ans = ws[idx];
        else ans = ws[idx].substring(0, ws[idx].length - g[idx][last]) + ans;
        last = idx;
        idx = p[status][idx];
        status ^= (1 << last);
    }
    return ans;
}
```
* 时间复杂度:将字符串 $ws[i]$ 的最大长度记为 $C = 20$,预处理复杂度为 $O(n^2 \times C)$;状态数量为 $2^n$,`DP` 复杂度为 $O(2^n \times n^2)$。构造答案复杂度为 $O(n)$。整体复杂度为 $O(2^n\times n^2)$
* 空间复杂度:$O(2^n \times n)$

### 最后

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

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

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

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


Question:
给你由 $n$ 个小写字母字符串组成的数组 `strs`,其中每个字符串长度相等。

这些字符串可以每个一行,排成一个网格。例如,`strs = ["abc", "bce", "cae"]` 可以排列为:
```
abc
bce
cae
```
你需要找出并删除 不是按字典序升序排列的 列。在上面的例子(下标从 $0$ 开始)中,列 $0$(`'a', 'b', 'c'`)和列 $2$(`'c', 'e', 'e'`)都是按升序排列的,而列 $1$(`'b', 'c', 'a'`)不是,所以要删除列 $1$ 。

返回你需要删除的列数。

示例 1:
```
输入:strs = ["cba","daf","ghi"]

输出:1

解释:网格示意如下:
  cba
  daf
  ghi
列 0 和列 2 按升序排列,但列 1 不是,所以只需要删除列 1 。
```
示例 2:
```
输入:strs = ["a","b"]

输出:0

解释:网格示意如下:
  a
  b
只有列 0 这一列,且已经按升序排列,所以不用删除任何列。
```
示例 3:
```
输入:strs = ["zyx","wvu","tsr"]

输出:3

解释:网格示意如下:
  zyx
  wvu
  tsr
所有 3 列都是非升序排列的,所以都要删除。
```

提示:
* $n == strs.length$
* $1 <= n <= 100$
* $1 <= strs[i].length <= 1000$
* `strs[i]` 由小写英文字母组成

Solution:
### 模拟

根据题意进行模拟即可。

代码:
```Java
class Solution {
    public int minDeletionSize(String[] strs) {
        int n = strs.length, m = strs[0].length(), ans = 0;
        out:for (int i = 0; i < m; i++) {
            for (int j = 0, cur = -1; j < n; j++) {
                int t = (int) strs[j].charAt(i);
                if (t < cur && ++ans >= 0) continue out;
                cur = t;
            }
        }
        return ans;
    }
}
```
* 时间复杂度:$O(n \times m)$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


Question:
给定 `pushed` 和 `popped` 两个序列,每个序列中的 值都不重复,只有当它们可能是在最初空栈上进行的推入 `push` 和弹出 `pop` 操作序列的结果时,返回 `true`;否则,返回 `false`。

示例 1:
```
输入:pushed = [1,2,3,4,5], popped = [4,5,3,2,1]

输出:true

解释:我们可以按以下顺序执行:
push(1), push(2), push(3), push(4), pop() -> 4,
push(5), pop() -> 5, pop() -> 3, pop() -> 2, pop() -> 1
```
示例 2:
```
输入:pushed = [1,2,3,4,5], popped = [4,3,5,1,2]

输出:false

解释:1 不能在 2 之前弹出。
```

提示:
* $1 <= pushed.length <= 1000$
* $0 <= pushed[i] <= 1000$
* `pushed` 的所有元素 互不相同
* $popped.length = pushed.length$
* `popped` 是 `pushed` 的一个排列

Solution:
### 栈运用模拟

根据题意,利用元素各不相同,我们使用一个栈来处理 `pushed` 数组,每次将 $pushed[i]$ 放入栈中,然后比较当前栈顶元素是否与待弹出元素相同(使用变量 `j` 来代指当前待弹出元素下标),若相等则弹栈并进行 `j` 自增,当所有的元素处理完后,栈为空说明栈序列合法。

Java 代码:
```Java
class Solution {
    public boolean validateStackSequences(int[] pushed, int[] popped) {
        Deque<Integer> d = new ArrayDeque<>();
        for (int i = 0, j = 0; i < pushed.length; i++) {
            d.addLast(pushed[i]);
            while (!d.isEmpty() && d.peekLast() == popped[j] && ++j >= 0) d.pollLast();
        }
        return d.isEmpty();
    }
}
```
Typescript 代码:
```Typescript
function validateStackSequences(pushed: number[], popped: number[]): boolean {
    let n = pushed.length, he = 0, ta = 0
    const stk: number[] = new Array<number>(n).fill(0)
    for (let i = 0, j = 0; i < n; i++) {
        stk[ta++] = pushed[i]
        while (he < ta && stk[ta - 1] == popped[j] && ++j >= 0) ta--
    }
    return he == ta
};
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n)$

---

### 双指针

我们也可以直接利用 `pushed` 充当栈,使用变量 `idx` 代指栈顶下标,变量 `j` 指向 `popped` 中待弹出的元素。

该做法好处无须额外空间,坏处是会修改入参数组。

Java 代码:
```Java
class Solution {
    public boolean validateStackSequences(int[] pushed, int[] popped) {
        int n = pushed.length, idx = 0;
        for (int i = 0, j = 0; i < n; i++) {
            pushed[idx++] = pushed[i];
            while (idx > 0 && pushed[idx - 1] == popped[j] && ++j >= 0) idx--;
        }
        return idx == 0;
    }
}
```
TypeScript 代码:
```TypeScript
function validateStackSequences(pushed: number[], popped: number[]): boolean {
    let n = pushed.length, idx = 0
    for (let i = 0, j = 0; i < n; i++) {
        pushed[idx++] = pushed[i]
        while (idx > 0 && pushed[idx - 1] == popped[j] && ++j >= 0) idx--
    }
    return idx == 0
};
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(1)$

---

### 加餐

**加餐一道同类型题目 : [常规栈运用题](https://mp.weixin.qq.com/s?__biz=MzU4NDE3MTEyMA==&mid=2247493276&idx=1&sn=b4b1ff8e6b1794f4f2e4327399755b0c)🎉🎉**

### 最后

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

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

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

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


950. 按递增顺序显示卡牌

Question:
牌组中的每张卡牌都对应有一个唯一的整数。你可以按你想要的顺序对这套卡片进行排序。

最初,这些卡牌在牌组里是正面朝下的(即,未显示状态)。

现在,重复执行以下步骤,直到显示所有卡牌为止:

1. 从牌组顶部抽一张牌,显示它,然后将其从牌组中移出。
2. 如果牌组中仍有牌,则将下一张处于牌组顶部的牌放在牌组的底部。
3. 如果仍有未显示的牌,那么返回步骤 1。否则,停止行动。

返回能以递增顺序显示卡牌的牌组顺序。

答案中的第一张牌被认为处于牌堆顶部。

示例:
```
输入:[17,13,11,2,3,5,7]

输出:[2,13,3,11,5,17,7]

解释:
我们得到的牌组顺序为 [17,13,11,2,3,5,7](这个顺序不重要),然后将其重新排序。
重新排序后,牌组以 [2,13,3,11,5,17,7] 开始,其中 2 位于牌组的顶部。
我们显示 2,然后将 13 移到底部。牌组现在是 [3,11,5,17,7,13]。
我们显示 3,并将 11 移到底部。牌组现在是 [5,17,7,13,11]。
我们显示 5,然后将 17 移到底部。牌组现在是 [7,13,11,17]。
我们显示 7,并将 13 移到底部。牌组现在是 [11,17,13]。
我们显示 11,然后将 17 移到底部。牌组现在是 [13,17]。
我们展示 13,然后将 17 移到底部。牌组现在是 [17]。
我们显示 17。
由于所有卡片都是按递增顺序排列显示的,所以答案是正确的。
```

提示:
* $1 <= A.length <= 1000$
* $1 <= A[i] <= 10^6$
* 对于所有的 `i != j`,`A[i] != A[j]`

Solution:
### 模拟

根据题意,我们可以先使用双端队列对 `deck` 进行一次模拟,并用哈希表记下每个元素 $deck[i]$ 的显示顺序(利用 $deck[i]$ 元素各不相同,可直接用 $deck[i]$ 作为 `key`)。

随后考虑如何通过哈希表来构建答案数组 `ans`。

假设原数组中的 $deck[i]$ 为首次显示的卡牌,那么 $ans[i]$ 应该放置 `deck` 中最小的元素,同理若 $deck[j]$ 若最后显示的卡牌,则 $ans[j]$ 应放置 `deck` 中的最大元素。

为了方便找 `deck` 中第 $k$ 大元素,可对 `deck` 进行拷贝并排序。

Java 代码:
```Java
class Solution {
    public int[] deckRevealedIncreasing(int[] deck) {
        int n = deck.length, idx = 0;
        Map<Integer, Integer> map = new HashMap<>();
        Deque<Integer> d = new ArrayDeque<>();
        for (int x : deck) d.addLast(x);
        while (!d.isEmpty()) {
            map.put(d.pollFirst(), idx++);
            if (!d.isEmpty()) d.addLast(d.pollFirst());
        }
        int[] ans = new int[n], temp = deck.clone();
        Arrays.sort(temp);
        for (int i = 0; i < n; i++) ans[i] = temp[map.get(deck[i])];
        return ans;
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    vector<int> deckRevealedIncreasing(vector<int>& deck) {
        int n = deck.size(), idx = 0;
        map<int, int> map;
        deque<int> d;
        for (int x : deck) d.push_back(x);
        while (!d.empty()) {
            map[d.front()] = idx++;
            d.pop_front();
            if (!d.empty()) {
                d.push_back(d.front());
                d.pop_front();
            }
        }
        vector<int> ans(n);
        vector<int> temp = deck;
        sort(temp.begin(), temp.end());
        for (int i = 0; i < n; i++) ans[i] = temp[map[deck[i]]];
        return ans;
    }
};
```
Python3 代码:
```Python
class Solution:
    def deckRevealedIncreasing(self, deck: List[int]) -> List[int]:
        n, idx = len(deck), 0
        map = {}
        d = deque(deck)
        while d:
            map[d.popleft()] = idx
            idx += 1
            if d:
                d.append(d.popleft())
        temp = sorted(deck)
        return [temp[map[deck[i]]] for i in range(n)]
```
TypeScript 代码:
```TypeScript
function deckRevealedIncreasing(deck: number[]): number[] {
    let n = deck.length, idx = 0;
    const map = {};
    const d = [...deck];
    while (d.length > 0) {
        map[d.shift()!] = idx++;
        if (d.length > 0) d.push(d.shift()!);
    }
    const ans = new Array(n);
    const temp = [...deck].sort((a, b) => a - b);
    for (let i = 0; i < n; i++) ans[i] = temp[map[deck[i]]];
    return ans;
};
```
* 时间复杂度:使用队列模拟一次操作的复杂度为 $O(n)$(每个元素只有一次出入队机会);对原数组进行复制并排序的复杂度 $O(n\log{n})$;构建答案复杂度为 $O(n)$。整体复杂度为 $O(n\log{n})$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


LeetCode/951-960/952. 按公因数计算最大组件大小（困难）.md

952. 按公因数计算最大组件大小

Question:
给定一个由不同正整数的组成的非空数组 `nums`,考虑下面的图:

* 有 `nums.length` 个节点,按从 `nums[0]` 到 `nums[nums.length - 1]` 标记;
* 只有当 `nums[i]` 和 `nums[j]` 共用一个大于 $1$ 的公因数时,`nums[i]` 和 `nums[j]`之间才有一条边。

返回 图中最大连通组件的大小 。

示例 1:

```
输入:nums = [4,6,15,35]

输出:4
```
示例 2:

```
输入:nums = [20,50,9,63]

输出:2
```
示例 3:

```
输入:nums = [2,3,6,7,4,12,21,39]

输出:8
```

提示:
* $1 <= nums.length <= 2 \times 10^4$
* $1 <= nums[i] <= 10^5$
* `nums` 中所有值都 不同

Solution:
### 枚举质因数 + 并查集

先考虑如何使用 `nums` 进行建图,`nums` 大小为 $n = 2 \times 10^4$,枚举所有点对并通过判断两数之间是否存在边的做法复杂度为 $O(n^2\sqrt{M})$(其中 $M = 1e5$ 为 $nums[i]$ 的最大值),无须考虑。

而不通过「枚举点 + 求公约数」的建图方式,可以对 $nums[i]$ 进行质因数分解(复杂度为 $O(\sqrt{nums[i]})$),假设其分解出来的质因数集合为 $S$,我们可以建立从 $S_{k}$ 到 $nums[i]$ 的映射关系,若 $nums[i]$ 与 $nums[j]$ 存在边,则 $nums[i]$ 和 $nums[j]$ 至少会被同一个质因数所映射。

维护连通块数量可以使用「并查集」来做,维护映射关系可以使用「哈希表」来做。

维护映射关系时,使用质因数为 `key`,下标值 $i$ 为 `value`(我们使用下标 $i$ 作为点编号,而不是使用 $nums[i]$ ,是利用$nums[i]$ 各不相同,从而将并查集数组大小从 $1e5$ 收窄到 $2 \times 10^4$)。

同时在使用「并查集」维护连通块时,同步维护每个连通块大小 `sz` 以及当前最大的连通块大小 `ans`。

Java 代码:
```Java
class Solution {
    static int N = 20010;
    static int[] p = new int[N], sz = new int[N];
    int ans = 1;
    int find(int x) {
        if (p[x] != x) p[x] = find(p[x]);
        return p[x];
    }
    void union(int a, int b) {
        if (find(a) == find(b)) return ;
        sz[find(a)] += sz[find(b)];
        p[find(b)] = p[find(a)];
        ans = Math.max(ans, sz[find(a)]);
    }
    public int largestComponentSize(int[] nums) {
        int n = nums.length;
        Map<Integer, List<Integer>> map = new HashMap<>();
        for (int i = 0; i < n; i++) {
            int cur = nums[i];
            for (int j = 2; j * j <= cur; j++) {
                if (cur % j == 0) add(map, j, i);
                while (cur % j == 0) cur /= j;
            }
            if (cur > 1) add(map, cur, i);
        }
        for (int i = 0; i <= n; i++) {
            p[i] = i; sz[i] = 1;
        }
        for (int key : map.keySet()) {
            List<Integer> list = map.get(key);
            for (int i = 1; i < list.size(); i++) union(list.get(0), list.get(i));
        }
        return ans;
    }
    void add(Map<Integer, List<Integer>> map, int key, int val) {
        List<Integer> list = map.getOrDefault(key, new ArrayList<>());
        list.add(val);
        map.put(key, list);
    }
}
```
TypeScript 代码:
```TypeScript
const N = 20010
const p: number[] = new Array<number>(N), sz = new Array<number>(N)
let ans = 0
function find(x: number): number {
    if (p[x] != x) p[x] = find(p[x])
    return p[x]
}
function union(a: number, b: number): void {
    if (find(a) == find(b)) return
    sz[find(a)] += sz[find(b)]
    p[find(b)] = p[find(a)]
    ans = Math.max(ans, sz[find(a)])
}
function largestComponentSize(nums: number[]): number {
    const n = nums.length
    const map: Map<number, Array<number>> = new Map<number, Array<number>>()
    for (let i = 0; i < n; i++) {
        let cur = nums[i]
        for (let j = 2; j * j <= cur; j++) {
            if (cur % j == 0) add(map, j, i)
            while (cur % j == 0) cur /= j
        }
        if (cur > 1) add(map, cur, i)
    }
    for (let i = 0; i < n; i++) {
        p[i] = i; sz[i] = 1
    }
    ans = 1
    for (const key of map.keys()) {
        const list = map.get(key)
        for (let i = 1; i < list.length; i++) union(list[0], list[i])
    }
    return ans
};
function add(map: Map<number, Array<number>>, key: number, val: number): void {
    let list = map.get(key)
    if (list == null) list = new Array<number>()
    list.push(val)
    map.set(key, list)
}
```
* 时间复杂度:$O(n\sqrt{M})$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


953. 验证外星语词典

Question:
某种外星语也使用英文小写字母,但可能顺序 `order` 不同。字母表的顺序(`order`)是一些小写字母的排列。

给定一组用外星语书写的单词 `words`,以及其字母表的顺序 `order`,只有当给定的单词在这种外星语中按字典序排列时,返回 `true`;否则,返回 `false`。

示例 1:
```
输入:words = ["hello","leetcode"], order = "hlabcdefgijkmnopqrstuvwxyz"

输出:true

解释:在该语言的字母表中,'h' 位于 'l' 之前,所以单词序列是按字典序排列的。
```
示例 2:
```
输入:words = ["word","world","row"], order = "worldabcefghijkmnpqstuvxyz"

输出:false

解释:在该语言的字母表中,'d' 位于 'l' 之后,那么 words[0] > words[1],因此单词序列不是按字典序排列的。
```
示例 3:
```
输入:words = ["apple","app"], order = "abcdefghijklmnopqrstuvwxyz"

输出:false

解释:当前三个字符 "app" 匹配时,第二个字符串相对短一些,然后根据词典编纂规则 "apple" > "app",因为 'l' > '∅',其中 '∅' 是空白字符,定义为比任何其他字符都小(更多信息)。
```

提示:
* $1 <= words.length <= 100$
* $1 <= words[i].length <= 20$
* $order.length == 26$
* 在 `words[i]` 和 `order` 中的所有字符都是英文小写字母。

Solution:
### 自定义排序

为了快速判断某两个字符在字典序的前后关系,先使用一个大小与字符集相等的数组对 `order` 进行转存。

然后对 `words` 进行拷贝复制得到 `clone`,并执行自定义排序,最后根据排序前后顺序是否相等来返回答案。

代码:
```Java
class Solution {
    public boolean isAlienSorted(String[] words, String order) {
        int[] ord = new int[26];
        for (int i = 0; i < 26; i++) ord[order.charAt(i) - 'a'] = i;
        String[] clone = words.clone();
        Arrays.sort(clone, (a, b)->{
            int n = a.length(), m = b.length();
            int i = 0, j = 0;
            while (i < n && j < m) {
                int c1 = a.charAt(i) - 'a', c2 = b.charAt(j) - 'a';
                if (c1 != c2) return ord[c1] - ord[c2];
                i++; j++;
            }
            if (i < n) return 1;
            if (j < m) return -1;
            return 0;
        });
        int n = words.length;
        for (int i = 0; i < n; i++) {
            if (!clone[i].equals(words[i])) return false;
        }
        return true;
    }
}
```
* 时间复杂度:$O(n\log{n})$
* 空间复杂度:$O(\log{n})$

---

### 模拟

更近一步,我们无须对整个数组进行排序,只需要将「自定义排序」中的规则抽出来,判断相邻两个字符串是满足字典序排序即可。

代码:
```Java
class Solution {
    int[] ord = new int[26];
    int check(String a, String b) {
        int n = a.length(), m = b.length();
        int i = 0, j = 0;
        while (i < n && j < m) {
            int c1 = a.charAt(i) - 'a', c2 = b.charAt(j) - 'a';
            if (c1 != c2) return ord[c1] - ord[c2];
            i++; j++;
        }
        if (i < n) return 1;
        if (j < m) return -1;
        return 0;
    }
    public boolean isAlienSorted(String[] words, String order) {
        for (int i = 0; i < 26; i++) ord[order.charAt(i) - 'a'] = i;
        int n = words.length;
        for (int i = 1; i < n; i++) {
            if (check(words[i - 1], words[i]) > 0) return false;
        }
        return true;
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(C)$

### 最后

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

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

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

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


Question:
给定一个长度为偶数的整数数组 `arr`,只有对 `arr` 进行重组后可以满足 “对于每个$ 0 <= i < len(arr) / 2$,都有 $arr[2 \times i + 1] = 2 \times arr[2 \times i]$” 时,返回 `true`;否则,返回 `false`。

示例 1:
```
输入:arr = [3,1,3,6]

输出:false
```
示例 2:
```
输入:arr = [2,1,2,6]

输出:false
```
示例 3:
```
输入:arr = [4,-2,2,-4]

输出:true

解释:可以用 [-2,-4] 和 [2,4] 这两组组成 [-2,-4,2,4] 或是 [2,4,-2,-4]
```

提示:
* $0 <= arr.length <= 3 \times 10^4$
* `arr.length` 是偶数
* $-10^5 <= arr[i] <= 10^5$

Solution:
### 逐个构造 + 优先队列

整理一下题意:是否能对 `arr` 进行重组,使得每一个奇数位置的值均是前一个位置的值的两倍,即凑成 $\frac{n}{2}$ 组形如 $(x, 2 \times x)$ 的数对。

对于一个任意的有理数而言,对其乘 $2$ 仅会改变数值的大小,而不会改变其方向(正负性质)。

因此如果我们每次都拿最接近 $0$ 的值作为起点,整个构造过程就是唯一确定的。

具体的,我们可以借助优先队列(堆)来实现,构造一个以与 $0$ 值距离作为基准的小根堆。每次从堆中取出元素 $x$,根据当前元素 $x$ 是否被「预定」过进行分情况讨论:

* 当前值 $x$ 没有被预定过,说明 $x$ 必然是数对中的「绝对值」的较小值,此时给 $x$ 并预定一个 $x \times 2$,即对 $x \times 2$ 的预定次数加一;
* 当前值 $x$ 已经被预定过,说明 $x$ 和此前的某个数 $\frac{x}{2}$ 组成过数对,对 $x$ 的预定次数减一。

当且仅当构成过程结束后,所有数的「预定」次数为 $0$ 时,`arr` 可以凑成 $\frac{n}{2}$ 组形如 $(x, 2 \times x)$ 的数对。

> 一些细节:由于 $arr[i]$ 的数值范围为 $[-10^5, 10^5]$,同时存在乘 $2$ 操作,因此我们需要对计算结果进行 $2 \times 10^5$ 的偏移操作,确保其为正数。

代码:
```Java
class Solution {
    static int N = 100010, M = N * 2;
    static int[] cnts = new int[M * 2];
    public boolean canReorderDoubled(int[] arr) {
        Arrays.fill(cnts, 0);
        PriorityQueue<Integer> q = new PriorityQueue<>((a,b)->Math.abs(a)-Math.abs(b));
        for (int i : arr) q.add(i);
        while (!q.isEmpty()) {
            int x = q.poll(), t = x * 2;
            if (cnts[x + M] != 0 && --cnts[x + M] >= 0) continue;
            cnts[t + M]++;
        }
        for (int i = 0; i < M * 2; i++) {
            if (cnts[i] != 0) return false;
        }
        return true;
    }
}
```
* 时间复杂度:令 $n$ 为 `arr` 长度,$m$ 为 $arr[i]$ 的值域范围,起始将所有数值放入优先队列(堆)的复杂度为 $O(n\log{n})$,从优先队列(堆)中取出并构造复杂度为 $O(n\log{n})$,检查构造是否成功复杂度为 $O(m)$,整体复杂度为 $O(n\log{n} + m)$
* 空间复杂度:$O(n + m)$

---

### 成组构造 + 排序 + 预处理剪枝

上述解法中,我们不可避免的对 `arr` 进行一遍完成的尝试构造,并且在尝试构造结束后再进行一次性的合法性检查。

事实上,如果 `arr` 能够凑成 $\frac{n}{2}$ 组形如 $(x, 2 \times x)$ 的数对,并且对于某个 $x$ 可能会出现多次,我们可以统计 $arr[i]$ 的数量,并根据绝对值大小进行排序,进行成组构造:$cnts[x]$ 个 $x$ 消耗 $cnts[x]$ 个 $2 \times x$。

同时由于我们提前预处理了每个 $arr[i]$ 的出现次数,我们可以提前知道是否有 $cnts[x]$ 个 $2 \times x$ 和 $x$ 成组,从而可以边构造边检查合法性。

 代码:
```Java
class Solution {
    static int N = 100010, M = N * 2;
    static int[] cnts = new int[M * 2];
    public boolean canReorderDoubled(int[] arr) {
        Arrays.fill(cnts, 0);
        List<Integer> list = new ArrayList<>();
        for (int i : arr) {
            if (++cnts[i + M] == 1) list.add(i);
        }
        Collections.sort(list, (a,b)->Math.abs(a)-Math.abs(b));
        for (int i : list) {
            if (cnts[i * 2 + M] < cnts[i + M]) return false;
            cnts[i * 2 + M] -= cnts[i + M];
        }
        return true;
    }
}
```
* 时间复杂度:统计 $arr[i]$ 的出现次数以及对 $arr[i]$ 去重的复杂度为 $O(n)$,对去重数组进行排序的复杂度为 $O(n\log{n})$,验证是否合法复杂度为 $O(n)$。整体复杂度为 $O(n\log{n})$
* 空间复杂度:$O(n + m)$

---

### 成组构造 + 拓扑排序

对于上述两种解法,要么利用「优先队列」要么利用「排序」,目的都是为了找到数对中的「绝对值较小」的那一位,然后开始往后构造。

**事实上,我们可以利用任意 $x$ 只可能与 $\frac{x}{2}$ 或者 $2 \times x$ 组成数对来进行建图,通过对图跑拓扑序来验证是否能够凑成 $\frac{n}{2}$ 组形如 $(x, 2 \times x)$ 的数对。**

> 不了解「拓扑排序」的同学可以看前置 🧀:[图论拓扑排序入门](https%3A//mp.weixin.qq.com/s?__biz%3DMzU4NDE3MTEyMA%3D%3D%26mid%3D2247489706%26idx%3D1%26sn%3D771cd807f39d1ca545640c0ef7e5baec),里面通过图解演示了何为拓扑序,以及通过「反证法」证明了为何有向无环图能够能够进行拓扑排序。

特别的,我们需要特殊处理 $arr[i] = 0$ 的情况,由于 $0$ 只能与本身组成数对,为了避免自环,我们需要跳过 $arr[i] = 0$ 的点,同时特判 $arr[i] = 0$ 的出现数量为奇数时,返回无解。

和解法二一样,先对 $arr[i]$ 进行数量统计以及去重预处理(跳过 $0$),然后对去重数组 `list` 中出现的数值 $x$ 进行分情况讨论:

* $x$ 为奇数,由于 $\frac{x}{2}$ 不为整数,因此 $x$ 只能作为数对中绝对值较小的那个(即 $x$ 入度为 $0$),加入队列;
* $x$ 为偶数,首先令 $x$ 的入度 $in[x] = cnts[\frac{x}{2}]$,代表有 $cnts[\frac{x}{2}]$ 个 $\frac{x}{2}$ 与其对应。当 $in[x] = 0$ 时,说明没有 $\frac{x}{2}$ 与其成对,此时 $x$ 只能作为数对中绝对值较小的那个(即 $x$ 入度为 $0$),加入队列。

跑一遍拓扑排序,假设当前出队值为 $t$,此时需要消耗掉 $cnts[t]$ 个 $t \times 2$ 与其形成数对(即 $cnts[t \times 2] -= cnts[t]$ ),同时 $t \times 2$ 的入度也要更新(即 $in[t \times 2] -= cnts[t]$ ),若 $in[t \times 2] = 0$ 且此时 $cnts[t \times 2] > 0$,将 $t \times 2$ 进行入队。同时由于我们明确减少了 $t \times 2$ 的数量,因此需要同步更新 $t \times 4$ 的入度,同理,当 $t \times 4$ 的入度 $in[t \times 4] = 0$,同时 $cnts[t \times 4] > 0$ 时,需要将 $t \times 4$ 进行入队。

代码:
```Java
class Solution {
    static int N = 100010, M = 2 * N;
    static int[] cnts = new int[M * 2], in = new int[M * 2];
    public boolean canReorderDoubled(int[] arr) {
        Arrays.fill(cnts, 0);
        Arrays.fill(in, 0);
        List<Integer> list = new ArrayList<>();
        for (int i : arr) {
            if (++cnts[i + M] == 1 && i != 0) list.add(i);
        }
        if (cnts[M] % 2 != 0) return false;
        Deque<Integer> d = new ArrayDeque<>();
        for (int i : list) {
            if (i % 2 == 0) {
                in[i + M] = cnts[i / 2 + M];
                if (in[i + M] == 0) d.addLast(i);
            } else {
                d.addLast(i);
            }
        }
        while (!d.isEmpty()) {
            int t = d.pollFirst();
            if (cnts[t * 2 + M] < cnts[t + M]) return false;
            cnts[t * 2 + M] -= cnts[t + M];
            in[t * 2 + M] -= cnts[t + M];
            if (in[t * 2 + M] == 0 && cnts[t * 2 + M] != 0) d.addLast(t * 2);
            in[t * 4 + M] -= cnts[t + M];
            if (in[t * 4 + M] == 0 && cnts[t * 4 + M] != 0) d.addLast(t * 4);
        }
        return true;
    }
}
```
* 时间复杂度:统计数量和入度的复杂度为 $O(n)$;跑拓扑序验证的复杂度为 $O(n)$。整体复杂度为 $O(n)$
* 空间复杂度:$O(n + m)$

### 最后

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

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

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

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


LeetCode/961-970/961. 在长度 2N 的数组中找出重复 N 次的元素（简单）.md

961. 在长度 2N 的数组中找出重复 N 次的元素

Question:
给你一个整数数组 `nums` ,该数组具有以下属性:

* $nums.length == 2 \times n$
* `nums` 包含 $n + 1$ 个 不同的 元素
* `nums` 中恰有一个元素重复 $n$ 次

找出并返回重复了 $n$ 次的那个元素。

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

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

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

输出:5
```

提示:
* $2 <= n <= 50004
* $nums.length == 2 \times n$
* $0 <= nums[i] <= 10^4$
* `nums` 由 $n + 1$ 个 不同的 元素组成,且其中一个元素恰好重复 $n$ 次

Solution:
### 计数模拟

根据题目给定的三个条件可推断出:数组中仅有一个元素出现多次,其余元素均出现一次。

又利用数据范围为 $10^4$,我们可以使用数组充当哈希表进行计数,当出现一个 $nums[i]$ 重复出现即是答案。

代码:
```Java
class Solution {
    int[] cnts = new int[10010];
    public int repeatedNTimes(int[] nums) {
        for (int x : nums) {
            if (++cnts[x] > 1) return x;
        }
        return -1;
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(C)$

---

### 构造共性

假设重复出现的数字是 $x$,数字 $x$ 重复了 $n$ 次,要将这 $n$ 个相同的 $x$ 间隔开,需要 $n - 1$ 个额外数字,而实际上我们除 $x$ 以外还有 $n$ 个额外数字(数字总数为 $n + 1$ 个),因此在我们所能构造出的所有排列方式中,**最多** 使相邻 $x$ 之间间隔了 $2$ 个额外数字。

对于每个 $nums[i]$ 而言,我们检查往前的三个位置(若有),如果有重复相等情况,说明当前的 $nums[i]$ 即是答案。

代码:
```Java
class Solution {
    public int repeatedNTimes(int[] nums) {
        int n = nums.length;
        for (int i = 0; i < n; i++) {
            int t = nums[i];
            if (i - 1 >= 0 && nums[i - 1] == t) return t;
            if (i - 2 >= 0 && nums[i - 2] == t) return t;
            if (i - 3 >= 0 && nums[i - 3] == t) return t;
        }
        return -1;
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


Question:
如果二叉树每个节点都具有相同的值,那么该二叉树就是单值二叉树。

只有给定的树是单值二叉树时,才返回 `true`;否则返回 `false`。

示例 1:

```
输入:[1,1,1,1,1,null,1]

输出:true
```
示例 2:

```
输入:[2,2,2,5,2]

输出:false
```

提示:
* 给定树的节点数范围是 $[1, 100]¥。
* 每个节点的值都是整数,范围为 $[0, 99]$ 。

Solution:
### 递归

根据题意进行模拟即可。

代码:
```Java
class Solution {
    int val = -1;
    public boolean isUnivalTree(TreeNode root) {
        if (val == -1) val = root.val;
        if (root == null) return true;
        if (root.val != val) return false;
        return isUnivalTree(root.left) && isUnivalTree(root.right);
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:忽略递归带来的额外空间消耗,复杂度为 $O(1)$

---

### 迭代

也可以使用迭代的方式进行层序遍历。

代码:
```Java
class Solution {
    public boolean isUnivalTree(TreeNode root) {
        int val = root.val;
        Deque<TreeNode> d = new ArrayDeque<>();
        d.addLast(root);
        while (!d.isEmpty()) {
            TreeNode poll = d.pollFirst();
            if (poll.val != val) return false;
            if (poll.left != null) d.addLast(poll.left);
            if (poll.right != null) d.addLast(poll.right);
        }
        return true;
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


Question:
给你一个整数数组 `arr`,请使用 **煎饼翻转** 完成对数组的排序。

一次煎饼翻转的执行过程如下:

* 选择一个整数 `k` ,$1 <= k <= arr.length$
* 反转子数组 `arr[0...k-1]`(下标从 $0$ 开始)

例如,`arr = [3,2,1,4]`,选择 `k = 3` 进行一次煎饼翻转,反转子数组 `[3,2,1]` ,得到 `arr = [1,2,3,4]` 。

以数组形式返回能使 `arr` 有序的煎饼翻转操作所对应的 `k` 值序列。任何将数组排序且翻转次数在 `10 * arr.length` 范围内的有效答案都将被判断为正确。

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

输出:[4,2,4,3]

解释:
我们执行 4 次煎饼翻转,k 值分别为 4,2,4,和 3。
初始状态 arr = [3, 2, 4, 1]
第一次翻转后(k = 4):arr = [1, 4, 2, 3]
第二次翻转后(k = 2):arr = [4, 1, 2, 3]
第三次翻转后(k = 4):arr = [3, 2, 1, 4]
第四次翻转后(k = 3):arr = [1, 2, 3, 4],此时已完成排序。
```
示例 2:
```
输入:[1,2,3]

输出:[]

解释:
输入已经排序,因此不需要翻转任何内容。
请注意,其他可能的答案,如 [3,3] ,也将被判断为正确。
```

提示:
* $1 <= arr.length <= 100$
* $1 <= arr[i] <= arr.length$
* `arr` 中的所有整数互不相同(即,arr 是从 1 到 arr.length 整数的一个排列)

Solution:
### 冒泡排序

由于每次我们都对「某段前缀」进行整体翻转,并且规定了翻转次数在一定范围内的方案均为合法翻转方案,同时 $arr$ 又是 $1$ 到 $n$ 的排列。

我们可以很自然想到「冒泡排序」:**每次确定未排序部分最右端的元素(最大值)。**

具体的,假设下标 $[k + 1, n - 1]$ 部分已有序,如果我们希望当前值 $t$ 出现在某个位置 $k$ 上,可以进行的操作为:

* 如果当前值 $t$ 已在 $k$ 上,无须进行操作;
* 如果当前值不在 $k$ 上,根据当前值是否在数组头部(下标为 $0$)进行分情况讨论:
    * 当前值在数组头部(下标为 $0$),直接将 $[0, k]$ 部分进行翻转(将 $k + 1$ 加入答案中),即可将当前值 $t$ 放到位置 $k$ 上;
    * 当前值不在数组头部(下标为 $0$),而是在 $idx$ 位置上,需要先将 $[0, idx]$ 部分进行翻转(将 $idx + 1$ 加入答案中),这样使得当前值 $t$ 出现数组头部(下标为 $0$),然后再将 $[0, k]$ 部分进行翻转(将 $k + 1$ 加入答案中),即可将当前值 $t$ 放到位置 $k$ 上。

而翻转某个前缀的操作可使用「双指针」实现,复杂度为 $O(n)$。

代码:
```Java
class Solution {
    public List<Integer> pancakeSort(int[] arr) {
        int n = arr.length;
        int[] idxs = new int[n + 10];
        for (int i = 0; i < n; i++) idxs[arr[i]] = i;
        List<Integer> ans = new ArrayList<>();
        for (int i = n; i >= 1; i--) {
            int idx = idxs[i];
            if (idx == i - 1) continue;
            if (idx != 0) {
                ans.add(idx + 1);
                reverse(arr, 0, idx, idxs);
            }
            ans.add(i);
            reverse(arr, 0, i - 1, idxs);
        }
        return ans;
    }
    void reverse(int[] arr, int i, int j, int[] idxs) {
        while (i < j) {
            idxs[arr[i]] = j; idxs[arr[j]] = i;
            int c = arr[i];
            arr[i++] = arr[j];
            arr[j--] = c;
        }
    }
}
```
* 时间复杂度:需要对 $n$ 个元素进行排序,每个元素最多触发两次翻转,每次翻转的复杂度为 $O(n)$。整体复杂度为 $O(n^2)$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


978. 最长湍流子数组

Question:
当 `A` 的子数组 $A[i], A[i+1], ..., A[j]$ 满足下列条件时,我们称其为湍流子数组:

* 若 $i <= k < j$,当 $k$ 为奇数时,$A[k] > A[k+1]$,且当 $k$ 为偶数时,$A[k] < A[k+1]$;
* 若 $i <= k < j$,当 $k$ 为偶数时,$A[k] > A[k+1]$ ,且当 $k$ 为奇数时,$A[k] < A[k+1]$。

也就是说,如果比较符号在子数组中的每个相邻元素对之间翻转,则该子数组是湍流子数组。

返回 `A` 的最大湍流子数组的长度。

示例 1:
```
输入:[9,4,2,10,7,8,8,1,9]
输出:5
解释:(A[1] > A[2] < A[3] > A[4] < A[5])
```
示例 2:
```
输入:[4,8,12,16]
输出:2
```
示例 3:
```
输入:[100]
输出:1
```

提示:
* $1 <= A.length <= 40000$
* $0 <= A[i] <= 10^9$

Solution:
### 基本思路

本题其实是要我们求最长一段呈 `↗ ↘ ↗ ↘` 或者 `↘ ↗ ↘ ↗` 形状的数组长度。

看一眼数据范围,有 $40000$,那么枚举起点和终点,然后对划分出来的子数组检查是否为「湍流子数组」的朴素解法就不能过了。

朴素解法的复杂度为 $O(n^3)$ ,直接放弃朴素解法。

复杂度往下优化,其实就 $O(n)$ 的 DP 解法了。

---

### 动态规划

至于 DP 如何分析,通过我们会先考虑一维 DP 能否求解,不行再考虑二维 DP。

对于本题,**由于每个位置而言,能否「接着」上一个位置**形成「湍流」,取决于上一位置是由什么形状而来。

举个例子,对于样例 `[3,4,2]`,从 4 -> 2 已经确定是 `↘` 状态,那么对于 2 这个位置能否「接着」4 形成「湍流」,要求 4 必须是由 `↗` 而来。

**因此我们还需要记录某一位是如何来的(`↗` 还是 `↘`),需要使二维 DP 来求解 ~**

我们定义 $f[i][j]$ 代表以位置 $i$ 为结尾,而结尾状态为 $j$ 的最长湍流子数组长度(0:上升状态 / 1:下降状态)

> PS. 这里的状态定义我是猜的,这其实是个技巧。通常我们做 DP 题,都是先猜一个定义,然后看看这个定义是否能分析出状态转移方程帮助我们「不重不漏」的枚举所有的方案。一般我是直接根据答案来猜定义,这里是求最长子数组长度,所以我猜一个 f(i,j) 代表最长湍流子数组长度

不失一般性考虑 $f[i][j]$ 该如何求解,我们知道位置 $i$ 是如何来是唯一确定的(取决于 $arr[i]$ 和 $arr[i - 1]$ 的大小关系),而只有三种可能性:

* $arr[i - 1] < arr[i]$:该点是由上升而来,能够「接着」的条件是 $i - 1$ 是由下降而来。则有:$f[i][0] = f[i - 1][1] + 1$
* $arr[i - 1] > arr[i]$:改点是由下降而来,能够「接着」的条件是 $i - 1$ 是由上升而来。则有:$f[i][1] = f[i - 1][0] + 1$
* $arr[i - 1] = arr[i]$:不考虑,不符合「湍流」的定义

代码:
```Java
class Solution {
    public int maxTurbulenceSize(int[] arr) {
        int n = arr.length, ans = 1;
        int[][] f = new int[n][2];
        f[0][0] = f[0][1] = 1;
        for (int i = 1; i < n; i++) {
            f[i][0] = f[i][1] = 1;
            if (arr[i] > arr[i - 1]) f[i][0] = f[i - 1][1] + 1;
            else if (arr[i] < arr[i - 1]) f[i][1] = f[i - 1][0] + 1;
            ans = Math.max(ans, Math.max(f[i][0], f[i][1]));
        }
        return ans;
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n)$

---

### 空间优化:奇偶滚动

我们发现对于 $f[i][j]$ 状态的更新只依赖于 $f[i - 1][j]$ 的状态。

因此我们可以使用「奇偶滚动」方式来将第一维从 $n$ 优化到 $2$。

修改的方式也十分机械,只需要改为「奇偶滚动」的维度直接修改成 $2$ ,然后该维度的所有访问方式增加 `%2` 或者 `&1` 即可:

代码:
```Java
class Solution {
    public int maxTurbulenceSize(int[] arr) {
        int n = arr.length, ans = 1;
        int[][] f = new int[2][2];
        f[0][0] = f[0][1] = 1;
        for (int i = 1; i < n; i++) {
            f[i % 2][0] = f[i % 2][1] = 1;
            if (arr[i] > arr[i - 1]) f[i % 2][0] = f[(i - 1) % 2][1] + 1;
            else if (arr[i] < arr[i - 1]) f[i % 2][1] = f[(i - 1) % 2][0] + 1;
            ans = Math.max(ans, Math.max(f[i % 2][0], f[i % 2][1]));
        }
        return ans;
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:使用固定 `2 * 2` 的数组空间。复杂度为 $O(1)$

---

### 空间优化:维度消除

既然只需要记录上一行状态,能否直接将行的维度消除呢?

答案是可以的,当我们要转移第 $i$ 行的时候,$f[i]$ 装的就已经是 $i - 1$ 行的结果。

这也是著名「背包问题」的一维通用优手段。

但相比于「奇偶滚动」的空间优化,这种优化手段只是常数级别的优化(空间复杂度与「奇偶滚动」相同),而且优化通常涉及代码改动。

代码:
```Java
class Solution {
    public int maxTurbulenceSize(int[] arr) {
        int n = arr.length, ans = 1;
        int[] f = new int[2];
        f[0] = f[1] = 1;
        for (int i = 1; i < n; i++) {
            int a = f[0], b = f[1];
            f[0] = arr[i - 1] < arr[i] ? b + 1 : 1;
            f[1] = arr[i - 1] > arr[i] ? a + 1 : 1;
            ans = Math.max(ans, Math.max(f[0], f[1]));
        }
        return ans;
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


981. 基于时间的键值存储

Question:
创建一个基于时间的键值存储类 `TimeMap`,它支持下面两个操作:

1. set(string key, string value, int timestamp)
	* 存储键 key、值 value,以及给定的时间戳 timestamp。
2. get(string key, int timestamp)
	* 返回先前调用 set(key, value, timestamp_prev) 所存储的值,其中 timestamp_prev <= timestamp。
	* 如果有多个这样的值,则返回对应最大的  timestamp_prev 的那个值。
	* 如果没有值,则返回空字符串("")。

示例 1:
```
输入:inputs = ["TimeMap","set","get","get","set","get","get"], inputs = [[],["foo","bar",1],["foo",1],["foo",3],["foo","bar2",4],["foo",4],["foo",5]]

输出:[null,null,"bar","bar",null,"bar2","bar2"]

解释:
TimeMap kv;
kv.set("foo", "bar", 1); // 存储键 "foo" 和值 "bar" 以及时间戳 timestamp = 1
kv.get("foo", 1);  // 输出 "bar"
kv.get("foo", 3); // 输出 "bar" 因为在时间戳 3 和时间戳 2 处没有对应 "foo" 的值,所以唯一的值位于时间戳 1 处(即 "bar")
kv.set("foo", "bar2", 4);
kv.get("foo", 4); // 输出 "bar2"
kv.get("foo", 5); // 输出 "bar2"
```
示例 2:
```
输入:inputs = ["TimeMap","set","set","get","get","get","get","get"], inputs = [[],["love","high",10],["love","low",20],["love",5],["love",10],["love",15],["love",20],["love",25]]

输出:[null,null,null,"","high","high","low","low"]
```

提示:
* 所有的键/值字符串都是小写的。
* 所有的键/值字符串长度都在 [1, 100] 范围内。
* 所有 TimeMap.set 操作中的时间戳 timestamps 都是严格递增的。
* 1 <= timestamp <= $10^7$
* TimeMap.set 和 TimeMap.get 函数在每个测试用例中将(组合)调用总计 `120000` 次。

Solution:
### 哈希表套数组

由于 `timestamp` 是严格递增,且没有删除 KV 的操作。

**我们可以使用哈希表套数组的方式进行实现,从而达到均摊 $O(1)$ 的插入操作和 $O(\log{n})$ 的查询操作。**

具体的,为了方便理解,我们可以先建一个 `Node` 类,类中包含键值对和时间戳信息。

然后使用一个全局哈希表 `map` 记录某个 `key` 对应了哪些 `Node`。其中多个 `Node` 是以动态数组的形式进行「以 `timestamp` 升序」存储:

* `set` 操作:以 $O(1)$ 的复杂度找到某个 `key` 对应的数组,利用 `timestamp` 严格递增的特性,以 $O(1)$ 复杂度将新 `Node` 加入当前数组尾部;
* `get` 操作:以 $O(1)$ 的复杂度找到某个 `key` 对应的数组,利用 `timestamp` 严格递增的特性,通过二分以 $O(\log{n})$ 复杂度找到可能符合条件的 `Node`。

代码:
```Java
class TimeMap {
    class Node {
        String k, v;
        int t;
        Node (String _k, String _v, int _t) {
            k = _k; v = _v; t = _t;
        }
    }

    Map<String, List<Node>> map = new HashMap<>();
    public void set(String k, String v, int t) {
        List<Node> list = map.getOrDefault(k, new ArrayList<>());
        list.add(new Node(k, v, t));
        map.put(k, list);
    }

    public String get(String k, int t) {
        List<Node> list = map.getOrDefault(k, new ArrayList<>());
        if (list.isEmpty()) return "";
        int n = list.size();
        int l = 0, r = n - 1;
        while (l < r) {
            int mid = l + r + 1 >> 1;
            if (list.get(mid).t <= t) {
                l = mid;
            } else {
                r = mid - 1;
            }
        }
        return list.get(r).t <= t ? list.get(r).v : "";
    }
}
```
* 时间复杂度:`set` 操作的复杂度为 $O(1)$;`get` 操作的复杂度为 $O(\log{n})$
* 空间复杂度:$O(n)$

---

### 哈希表套树

如果增加 `del` 操作呢?我们需要做出何种调整?

考虑在原题的基础上,增加一个 `String del(String k, int t)` 的功能:将严格等于键和时间戳的 KV 对删掉。

**由于存在删除 KV 的动作,我们需要将实现从「哈希表套数组」改成「哈希表套树」,这里直接使用基于红黑树实现的 `TreeMap` 即可。**

同时为了验证删除逻辑的正确性,我们在 `get` 动作发生前,先产生一次随机性的删除,删除后又重新插入。

代码:
```Java
class TimeMap {
    class Node {
        String k, v;
        int t;
        Node (String _k, String _v, int _t) {
            k = _k; v = _v; t = _t;
        }
    }

    Map<String, TreeMap<Integer, Node>> map = new HashMap<>();
    public void set(String k, String v, int t) {
        update(k, t);
        TreeMap<Integer, Node> ts = map.getOrDefault(k, new TreeMap<Integer, Node>());
        ts.put(t, new Node(k, v, t));
        map.put(k, ts);
    }

    Node _get(String k, int t) {
        TreeMap<Integer, Node> ts = map.get(k);
        if (ts == null) return null;
        Map.Entry<Integer, Node> entry = ts.floorEntry(t);
        if (entry == null) return null;
        Node node = entry.getValue();
        return node;
    }

    public String get(String k, int t) {
        randomDel();
        Node node = _get(k, t);
        return node != null && node.t <= t ? node.v : "";
    }

    public String del(String k, int t) {
        TreeMap<Integer, Node> ts = map.get(k);
        if (ts == null) return null;
        Map.Entry<Integer, Node> entry = ts.floorEntry(t);
        if (entry == null) return null;
        Node node = entry.getValue();
        if (node != null && node.t == t) {
            ts.remove(t);
            return node.v;
        }
        return "";
    }

    List<String> allInfo = new ArrayList<>();
    Random random = new Random();
    // 保存所有的 kt 信息
    void update(String k, int t) {
        String nk = k + "_" + t;
        allInfo.add(nk);
    }
    // 随机删除,再重新插入,验证代码正确性
    void randomDel() {
        int idx = random.nextInt(allInfo.size());
        String[] ss = allInfo.get(idx).split("_");
        String k = ss[0];
        int t = Integer.parseInt(ss[1]);
        Node node = _get(k, t);
        del(node.k, node.t);
        set(node.k, node.v, node.t);
    }
}
```
* 时间复杂度:`set` 操作的复杂度为 $O(\log{n})$;`get` 操作会完成随机删除/重新插入/查询的动作,复杂度均为为 $O(\log{n})$,整个 `get` 的复杂度仍是 $O(\log{n})$(只是常数变大了)
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


985. 查询后的偶数和

Question:
给出一个整数数组 `A` 和一个查询数组 `queries`。

对于第 `i` 次查询,有 `val = queries[i][0]`, `index = queries[i][1]`,我们会把 `val` 加到 `A[index]` 上。然后,第 `i` 次查询的答案是 `A` 中偶数值的和。

(此处给定的 `index = queries[i][1]` 是从 `0` 开始的索引,每次查询都会永久修改数组 `A`。)

返回所有查询的答案。你的答案应当以数组 `answer` 给出,`answer[i]` 为第 `i` 次查询的答案。

示例:
```
输入:A = [1,2,3,4], queries = [[1,0],[-3,1],[-4,0],[2,3]]

输出:[8,6,2,4]

解释:
开始时,数组为 [1,2,3,4]。
将 1 加到 A[0] 上之后,数组为 [2,2,3,4],偶数值之和为 2 + 2 + 4 = 8。
将 -3 加到 A[1] 上之后,数组为 [2,-1,3,4],偶数值之和为 2 + 4 = 6。
将 -4 加到 A[0] 上之后,数组为 [-2,-1,3,4],偶数值之和为 -2 + 4 = 2。
将 2 加到 A[3] 上之后,数组为 [-2,-1,3,6],偶数值之和为 -2 + 6 = 4。
```

提示:
* $1 <= A.length <= 10000$
* $-10000 <= A[i] <= 10000$
* $1 <= queries.length <= 10000$
* $-10000 <= queries[i][0] <= 10000$
* $0 <= queries[i][1] < A.length$

Solution:
### 模拟

为了方便,将 `queries` 记为 `qs`。

由于每次修改都是对 `A` 的永久修改,因此我们使用一个变量 `t` 来记录当前所有偶数和即可。

具体的,我们先统计原数组所有偶数和为 `t`,从前往后处理每个 $qs[i]$,若修改前的原值 $nums[qs[i][1]]$ 为偶数,我们需要将其从 `t` 中减去,再进行实际修改 `nums[qs[i][1]] += qs[i][0]`,若修改后值为偶数,则将其累加到 `t` 上。

Java 代码:
```Java
class Solution {
    public int[] sumEvenAfterQueries(int[] nums, int[][] qs) {
        int m = qs.length, t = 0;
        int[] ans = new int[m];
        for (int x : nums) t += x % 2 == 0 ? x : 0;
        for (int i = 0; i < m; i++) {
            int val = qs[i][0], idx = qs[i][1];
            if (nums[idx] % 2 == 0) t -= nums[idx];
            nums[idx] += val;
            if (nums[idx] % 2 == 0) t += nums[idx];
            ans[i] = t;
        }
        return ans;
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    vector<int> sumEvenAfterQueries(vector<int>& nums, vector<vector<int>>& qs) {
        int m = qs.size(), t = 0;
        std::vector<int> ans(m);
        for (int x : nums) t += x % 2 == 0 ? x : 0;
        for (int i = 0; i < m; i++) {
            int val = qs[i][0], idx = qs[i][1];
            if (nums[idx] % 2 == 0) t -= nums[idx];
            nums[idx] += val;
            if (nums[idx] % 2 == 0) t += nums[idx];
            ans[i] = t;
        }
        return ans;
    }
};
```
Python3 代码:
```Python3
class Solution:
    def sumEvenAfterQueries(self, nums: List[int], qs: List[List[int]]) -> List[int]:
        m, t = 0, sum(x for x in nums if x % 2 == 0)
        ans = []
        for val, idx in qs:
            if nums[idx] % 2 == 0:
                t -= nums[idx]
            nums[idx] += val
            if nums[idx] % 2 == 0:
                t += nums[idx]
            ans.append(t)
        return ans
```
Go 代码:
```Go
func sumEvenAfterQueries(nums []int, qs [][]int) []int {
    m, t := len(qs), 0
    ans := make([]int, m)
    for _, x := range nums {
        if x % 2 == 0 {
            t += x
        }
    }
    for i := 0; i < m; i++ {
        val, idx := qs[i][0], qs[i][1]
        if nums[idx] % 2 == 0 {
            t -= nums[idx]
        }
        nums[idx] += val
        if nums[idx] % 2 == 0 {
            t += nums[idx]
        }
        ans[i] = t
    }
    return ans
}
```
TypeScript 代码:
```TypeScript
function sumEvenAfterQueries(nums: number[], qs: number[][]): number[] {
    let m = qs.length, t = 0
    for (const x of nums) t += x % 2 == 0 ? x : 0
    const ans = new Array(m)
    for (let i = 0; i < m; i++) {
        const val = qs[i][0], idx = qs[i][1]
        if (nums[idx] % 2 == 0) t -= nums[idx]
        nums[idx] += val
        if (nums[idx] % 2 == 0) t += nums[idx]
        ans[i] = t;
    }
    return ans
};
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


987. 二叉树的垂序遍历

Question:
给你二叉树的根结点 `root` ,请你设计算法计算二叉树的 垂序遍历 序列。

对位于 $(row, col)$ 的每个结点而言,其左右子结点分别位于 $(row + 1, col - 1)$ 和 $(row + 1, col + 1)$ 。树的根结点位于 $(0, 0)$ 。

二叉树的 垂序遍历 从最左边的列开始直到最右边的列结束,按列索引每一列上的所有结点,形成一个按出现位置从上到下排序的有序列表。如果同行同列上有多个结点,则按结点的值从小到大进行排序。

返回二叉树的 垂序遍历 序列。

示例 1:

```
输入:root = [3,9,20,null,null,15,7]

输出:[[9],[3,15],[20],[7]]

解释:
列 -1 :只有结点 9 在此列中。
列  0 :只有结点 3 和 15 在此列中,按从上到下顺序。
列  1 :只有结点 20 在此列中。
列  2 :只有结点 7 在此列中。
```
示例 2:

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

输出:[[4],[2],[1,5,6],[3],[7]]

解释:
列 -2 :只有结点 4 在此列中。
列 -1 :只有结点 2 在此列中。
列  0 :结点 1 、5 和 6 都在此列中。
          1 在上面,所以它出现在前面。
          5 和 6 位置都是 (2, 0) ,所以按值从小到大排序,5 在 6 的前面。
列  1 :只有结点 3 在此列中。
列  2 :只有结点 7 在此列中。
```
示例 3:

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

输出:[[4],[2],[1,5,6],[3],[7]]

解释:
这个示例实际上与示例 2 完全相同,只是结点 5 和 6 在树中的位置发生了交换。
因为 5 和 6 的位置仍然相同,所以答案保持不变,仍然按值从小到大排序。
```

提示:
* 树中结点数目总数在范围 $[1, 10]$

Solution:
### DFS + 哈希表 + 排序

根据题意,我们需要按照优先级「**“列号从小到大”,对于同列节点,“行号从小到大”,对于同列同行元素,“节点值从小到大”**」进行答案构造。

因此我们可以对树进行遍历,遍历过程中记下这些信息 $(col, row, val)$,然后根据规则进行排序,并构造答案。

我们可以先使用「哈希表」进行存储,最后再进行一次性的排序。

代码:
```Java
class Solution {
    Map<TreeNode, int[]> map = new HashMap<>(); // col, row, val
    public List<List<Integer>> verticalTraversal(TreeNode root) {
        map.put(root, new int[]{0, 0, root.val});
        dfs(root);
        List<int[]> list = new ArrayList<>(map.values());
        Collections.sort(list, (a, b)->{
            if (a[0] != b[0]) return a[0] - b[0];
            if (a[1] != b[1]) return a[1] - b[1];
            return a[2] - b[2];
        });
        int n = list.size();
        List<List<Integer>> ans = new ArrayList<>();
        for (int i = 0; i < n; ) {
            int j = i;
            List<Integer> tmp = new ArrayList<>();
            while (j < n && list.get(j)[0] == list.get(i)[0]) tmp.add(list.get(j++)[2]);
            ans.add(tmp);
            i = j;
        }
        return ans;
    }
    void dfs(TreeNode root) {
        if (root == null) return ;
        int[] info = map.get(root);
        int col = info[0], row = info[1], val = info[2];
        if (root.left != null) {
            map.put(root.left, new int[]{col - 1, row + 1, root.left.val});
            dfs(root.left);
        }
        if (root.right != null) {
            map.put(root.right, new int[]{col + 1, row + 1, root.right.val});
            dfs(root.right);
        }
    }
}
```
* 时间复杂度:令总节点数量为 $n$,填充哈希表时进行树的遍历,复杂度为 $O(n)$;构造答案时需要进行排序,复杂度为 $O(n\log{n})$。整体复杂度为 $O(n\log{n})$
* 空间复杂度:$O(n)$

---

### DFS + 优先队列(堆)

显然,最终要让所有节点的相应信息有序,可以使用「优先队列(堆)」边存储边维护有序性。

代码:
```Java
class Solution {
    PriorityQueue<int[]> q = new PriorityQueue<>((a, b)->{ // col, row, val
        if (a[0] != b[0]) return a[0] - b[0];
        if (a[1] != b[1]) return a[1] - b[1];
        return a[2] - b[2];
    });
    public List<List<Integer>> verticalTraversal(TreeNode root) {
        int[] info = new int[]{0, 0, root.val};
        q.add(info);
        dfs(root, info);
        List<List<Integer>> ans = new ArrayList<>();
        while (!q.isEmpty()) {
            List<Integer> tmp = new ArrayList<>();
            int[] poll = q.peek();
            while (!q.isEmpty() && q.peek()[0] == poll[0]) tmp.add(q.poll()[2]);
            ans.add(tmp);
        }
        return ans;
    }
    void dfs(TreeNode root, int[] fa) {
        if (root.left != null) {
            int[] linfo = new int[]{fa[0] - 1, fa[1] + 1, root.left.val};
            q.add(linfo);
            dfs(root.left, linfo);
        }
        if (root.right != null) {
            int[] rinfo = new int[]{fa[0] + 1, fa[1] + 1, root.right.val};
            q.add(rinfo);
            dfs(root.right, rinfo);
        }
    }
}
```
* 时间复杂度:令总节点数量为 $n$,将节点信息存入优先队列(堆)复杂度为 $O(n\log{n})$;构造答案复杂度为 $O(n\log{n})$。整体复杂度为 $O(n\log{n})$
* 空间复杂度:$O(n)$

---

### DFS + 哈希嵌套 + 排序

当然,如果想锻炼一下自己的代码能力,不使用三元组 $(col, row, val)$ 进行存储,而是使用哈希表嵌套,也是可以的。

用三个「哈希表」来记录相关信息:

1. 使用 `node2row` 和 `node2col` 分别用来记录「**节点到行**」&「**节点到列**」的映射关系,并实现 `dfs1` 对树进行遍历,目的是为了记录下相关的映射关系;

2. 使用 `col2row2nodes` 记录「**从列到行,从行到节点集**」的映射关系,具体的存储格式为 `{col : {row : [node1, node2, ... ]}}`,实现 `dfs2` 再次进行树的遍历,配合之前 `node2row` 和 `node2col`信息,填充 `col2row2nodes` 的映射关系;

3. 按照题意,按「列号从小到大」,对于同列节点,按照「行号从小到大」,对于同列同行元素,按照「节点值从小到大」的规则,使用 `col2row2nodes` + 排序 构造答案。

> 注意:本解法可以只进行一次树的遍历,分两步主要是不想 `dfs` 操作过于复杂,加大读者的阅读难度,于是在拆开不影响复杂度上界的情况,选择了分两步。

代码:
```Java
class Solution {
    Map<TreeNode, Integer> node2col = new HashMap<>(), node2row = new HashMap<>();
    Map<Integer, Map<Integer, List<Integer>>> col2row2nodes = new HashMap<>();
    public List<List<Integer>> verticalTraversal(TreeNode root) {
        List<List<Integer>> ans = new ArrayList<>();
        node2col.put(root, 0);
        node2row.put(root, 0);
        dfs1(root);
        dfs2(root);
        List<Integer> cols = new ArrayList<>(col2row2nodes.keySet());
        Collections.sort(cols);
        for (int col : cols) {
            Map<Integer, List<Integer>> row2nodes = col2row2nodes.get(col);
            List<Integer> rows = new ArrayList<>(row2nodes.keySet());
            Collections.sort(rows);
            List<Integer> cur = new ArrayList<>();
            for (int row : rows) {
                List<Integer> nodes = row2nodes.get(row);
                Collections.sort(nodes);
                cur.addAll(nodes);
            }
            ans.add(cur);
        }
        return ans;
    }
    // 树的遍历,根据「节点到列」&「节点到行」的映射关系,构造出「从列到行,从行到节点集」的映射关系
    void dfs2(TreeNode root) {
        if (root == null) return ;
        int col = node2col.get(root), row = node2row.get(root);
        Map<Integer, List<Integer>> row2nodes = col2row2nodes.getOrDefault(col, new HashMap<>());
        List<Integer> nodes = row2nodes.getOrDefault(row, new ArrayList<>());
        nodes.add(root.val);
        row2nodes.put(row, nodes);
        col2row2nodes.put(col, row2nodes);
        dfs2(root.left);
        dfs2(root.right);
    }
    // 树的遍历,记录下「节点到列」&「节点到行」的映射关系
    void dfs1(TreeNode root) {
        if (root == null) return ;
        if (root.left != null) {
            int col = node2col.get(root);
            node2col.put(root.left, col - 1);
            int row = node2row.get(root);
            node2row.put(root.left, row + 1);
            dfs1(root.left);
        }
        if (root.right != null) {
            int col = node2col.get(root);
            node2col.put(root.right, col + 1);
            int row = node2row.get(root);
            node2row.put(root.right, row + 1);
            dfs1(root.right);
        }
    }
}
```
* 时间复杂度:令总的节点数量为 $n$,填充几个哈希表的复杂度为 $O(n)$;构造答案时需要对行号、列号和节点值进行排序,总的复杂度上界为 $O(n\log{n})$。整体复杂度为 $O(n\log{n})$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


LeetCode/991-1000/992. K 个不同整数的子数组（困难）.md

992. K 个不同整数的子数组

Question:
给定一个正整数数组 $A$,如果 $A$ 的某个子数组中不同整数的个数恰好为 $K$,则称 $A$ 的这个连续、不一定不同的子数组为好子数组。

例如,$[1,2,3,1,2]$ 中有 $3$ 个不同的整数:$1$,$2$,以及 $3$。

返回 $A$ 中好子数组的数目。

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

输出:7

解释:恰好由 2 个不同整数组成的子数组:[1,2], [2,1], [1,2], [2,3], [1,2,1], [2,1,2], [1,2,1,2]
```
示例 2:
```
输入:A = [1,2,1,3,4], K = 3

输出:3

解释:恰好由 3 个不同整数组成的子数组:[1,2,1,3], [2,1,3], [1,3,4]
```

提示:
* $1 <= A.length <= 20000$
* $1 <= A[i] <= A.length$
* $1 <= K <= A.length$

Solution:
### 滑动窗口

对原数组每个 $nums[i]$ 而言:

1. 找到其左边「最远」满足出现 $k$ 个不同字符的下标,记为 $p$ ,这时候形成的区间为 $[p, i]$
2. 找到其左边「最远」满足出现 $k - 1$ 个不同字符的下标,记为 $j$ ,这时候形成的区间为 $[j, i]$
3. **那么对于 $j - p$ 其实就是代表以 $nums[i]$ 为右边界(必须包含 $num[i]$),不同字符数量「恰好」为 $k$ 的子数组数量**

我们使用 $lower$ 数组存起每个位置的 $p$;使用 $upper$ 数组存起每个位置的 $j$。

累积每个位置的 $upper[i] - lower[i]$ 就是答案。

计算 $lower$ 数组 和 $upper$ 数组的过程可以使用双指针。

代码:
```Java
class Solution {
    public int subarraysWithKDistinct(int[] nums, int k) {
        int n = nums.length;
        int[] lower = new int[n], upper = new int[n];
        find(nums, lower, k);
        find(nums, upper, k - 1);
        int ans = 0;
        for (int i = 0; i < n; i++) ans += upper[i] - lower[i];
        return ans;
    }
    void find(int[] nums, int[] arr, int k) {
        int n = nums.length;
        int[] cnt = new int[20010];
        for (int i = 0, j = 0, sum = 0; j < n; j++) {
            if (++cnt[nums[j]] == 1) sum++;
            while (sum > k) {
                if (--cnt[nums[i++]] == 0) sum--;
            }
            if (sum == k) arr[j] = i;
        }
    }
}
```
* 时间复杂度:对数组进行常数次扫描。复杂度为 $O(n)$。
* 空间复杂度:$O(n)$

---

### 其他

这里的 $lower$ 和 $upper$ 其实可以优化掉,但也只是常数级别的优化,空间复杂度仍为 $O(n)$。

推荐大家打印一下 $lower$ 和 $upper$ 来看看,加深对 **「 $upper[i] - lower[i]$ 代表了考虑 $nums[i]$ 为右边界,不同字符数量「恰好」为 $k$ 的子数组数量 」** 这句话的理解。

### 最后

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

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

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

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


993. 二叉树的堂兄弟节点

Question:
在二叉树中,根节点位于深度 $0$ 处,每个深度为 $k$ 的节点的子节点位于深度 $k+1$ 处。

如果二叉树的两个节点深度相同,但 父节点不同 ,则它们是一对堂兄弟节点。

我们给出了具有唯一值的二叉树的根节点 $root$ ,以及树中两个不同节点的值 $x$ 和 $y$ 。

只有与值 $x$ 和 $y$ 对应的节点是堂兄弟节点时,才返回 $true$ 。否则,返回 $false$。

示例 1:

```
输入:root = [1,2,3,4], x = 4, y = 3

输出:false
```
示例 2:

```
输入:root = [1,2,3,null,4,null,5], x = 5, y = 4

输出:true
```
示例 3:

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

输出:false
```

提示:
* 二叉树的节点数介于 $2$ 到 $100$ 之间。
* 每个节点的值都是唯一的、范围为 $1$ 到 $100$ 的整数。

Solution:
### DFS

显然,我们希望得到某个节点的「父节点」&「所在深度」,不难设计出如下「DFS 函数签名」:

```Java
/**
 * 查找 t 的「父节点值」&「所在深度」
 * @param root 当前搜索到的节点
 * @param fa root 的父节点
 * @param depth 当前深度
 * @param t 搜索目标值
 * @return [fa.val, depth]
 */
int[] dfs(TreeNode root, TreeNode fa, int depth, int t);
```
之后按照遍历的逻辑处理即可。

需要注意的时,我们需要区分出「搜索不到」和「搜索对象为 $root$(没有 $fa$ 父节点)」两种情况。

我们约定使用 $-1$ 代指没有找到目标值 $t$,使用 $0$ 代表找到了目标值 $t$,但其不存在父节点。

代码:
```Java
class Solution {
    public boolean isCousins(TreeNode root, int x, int y) {
        int[] xi = dfs(root, null, 0, x);
        int[] yi = dfs(root, null, 0, y);
        return xi[1] == yi[1] && xi[0] != yi[0];
    }
    int[] dfs(TreeNode root, TreeNode fa, int depth, int t) {
        if (root == null) return new int[]{-1, -1}; // 使用 -1 代表为搜索不到 t
        if (root.val == t) {
            return new int[]{fa != null ? fa.val : 1, depth}; // 使用 1 代表搜索值 t 为 root
        }
        int[] l = dfs(root.left, root, depth + 1, t);
        if (l[0] != -1) return l;
        return dfs(root.right, root, depth + 1, t);
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:忽略递归开销为 $O(1)$,否则为 $O(n)$

---

### BFS

能使用 `DFS`,自然也能使用 `BFS`,两者大同小异。

代码:
```Java
class Solution {
    public boolean isCousins(TreeNode root, int x, int y) {
        int[] xi = bfs(root, x);
        int[] yi = bfs(root, y);
        return xi[1] == yi[1] && xi[0] != yi[0];
    }
    int[] bfs(TreeNode root, int t) {
        Deque<Object[]> d = new ArrayDeque<>(); // 存储值为 [cur, fa, depth]
        d.addLast(new Object[]{root, null, 0});
        while (!d.isEmpty()) {
            int size = d.size();
            while (size-- > 0) {
                Object[] poll = d.pollFirst();
                TreeNode cur = (TreeNode)poll[0], fa = (TreeNode)poll[1];
                int depth = (Integer)poll[2];

                if (cur.val == t) return new int[]{fa != null ? fa.val : 0, depth};
                if (cur.left != null) d.addLast(new Object[]{cur.left, cur, depth + 1});
                if (cur.right != null) d.addLast(new Object[]{cur.right, cur, depth + 1});
            }
        }
        return new int[]{-1, -1};
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


LeetCode/991-1000/995. K 连续位的最小翻转次数（困难）.md

995. K 连续位的最小翻转次数

Question:
在仅包含 0 和 1 的数组 A 中,一次 K 位翻转包括选择一个长度为 K 的(连续)子数组,同时将子数组中的每个 0 更改为 1,而每个 1 更改为 0。

返回所需的 K 位翻转的最小次数,以便数组没有值为 0 的元素。如果不可能,返回 -1。

示例 1:
```
输入:A = [0,1,0], K = 1
输出:2
解释:先翻转 A[0],然后翻转 A[2]。
```
示例 2:
```
输入:A = [1,1,0], K = 2
输出:-1
解释:无论我们怎样翻转大小为 2 的子数组,我们都不能使数组变为 [1,1,1]。
```
示例 3:
```
输入:A = [0,0,0,1,0,1,1,0], K = 3
输出:3
解释:
翻转 A[0],A[1],A[2]: A变成 [1,1,1,1,0,1,1,0]
翻转 A[4],A[5],A[6]: A变成 [1,1,1,1,1,0,0,0]
翻转 A[5],A[6],A[7]: A变成 [1,1,1,1,1,1,1,1]
```

提示:
* 1 <= A.length <= 30000
* 1 <= K <= A.length

Solution:
### 贪心解法

目标是将数组的每一位都变为 1 ,因此对于每一位 0 都需要翻转。

我们可以从前往后处理,遇到 0 则对后面的 `k` 位进行翻转。

这样我们的算法复杂度是 $O(nk)$ 的,数据范围是 3w(数量级为 $10^4$),极限数据下单秒的运算量在 $10^8$ 以上,会有超时风险。

```java []
class Solution {
    public int minKBitFlips(int[] nums, int k) {
        int n = nums.length;
        int ans = 0;
        for (int i = 0; i < n; i++) {
            if (nums[i] == 0) {
                if (i + k > n) return -1;
                for (int j = i; j < i + k; j++) nums[j] ^= 1;
                ans++;
            }
        }
        return ans;
    }
}
```
* 时间复杂度:$O(nk)$
* 空间复杂度:$O(1)$

***

### 补充
评论有同学提出了一些有价值的疑问,我觉得挺有代表性的,因此补充到题解:

1. **为什么这样的解法是「贪心解法」,而不是「暴力解法」?**

首先「暴力解法」必然是**对所有可能出现的翻转方案进行枚举**,然后检查每一个方案得到的结果是否符合全是 1 的要求。

这样的解法,才是暴力解法,它的本质是通过「穷举」找答案。复杂度是指数级别的。

而我们的「朴素贪心解法」只是执行了众多翻转方案中的一种。

举个 🌰,对于 `nums = [0,0,1,1]` 并且 `k = 2` 的数据:

暴力解法应该是「枚举」以下三种方案:

1. 只翻转以第一个 0 开头的子数组(长度固定为 2)
2. 只翻转以第二个 0 开头的子数组(长度固定为 2)
3. 同时翻转第一个 0 开头和第二个 0 开头的子数组(长度固定为 2,只不过这时候第一个 0 被翻转了一次,第二个 0 被翻转了两次)

然后对三种方案得到的最终解进行检查,找出符合结果全是 1 的方案。这种通过「穷举」方案检查合法性的解法才是「暴力」解法。

2. **为什么我采用了与「朴素贪心」解法相似的做法,超时了?**

结果测试 C++、Python 超时,只有 Java 能过。

同样是 97 号样例数据,提交给 LeetCode 执行。Java 运行 200 ms 以内,而 C++ 运行 600 ms。

***

### 贪心 + 差分解法

由于我们总是对连续的一段进行「相同」的操作,同时只有「奇数」次数的翻转才会真正改变当前位置上的值。

自然而然,我们会想到使用数组 `arr` 来记录每一位的翻转次数。

同时我们又不希望是通过「遍历记 `arr` 的 `k` 位进行 +1」来完成统计。

因此可以使用差分数组来进行优化:当需要对某一段 `[l,r]` 进行 +1 的时候,只需要 `arr[l]++` 和 `arr[r + 1]--` 即可。

```java
class Solution {
    public int minKBitFlips(int[] nums, int k) {
        int n = nums.length;
        int ans = 0;
        int[] arr = new int[n + 1];
        for (int i = 0, cnt = 0; i < n; i++) {
            cnt += arr[i];
            if ((nums[i] + cnt) % 2 == 0) {
                if (i + k > n) return -1;
                arr[i + 1]++;
                arr[i + k]--;
                ans++;
            }
        }
        return ans;
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n)$

***

### 证明

为什么「一遇到 0 就马上进行翻转」这样的做法得到的是最优解?

这道题的贪心证明思路和 [765. 情侣牵手](https://leetcode-cn.com/problems/couples-holding-hands/solution/liang-chong-100-de-jie-fa-bing-cha-ji-ta-26a6/) 是一样的。

本质上是在证明**当我在处理第 `k` 个位置的 0 的时候,前面 `k - 1` 个位置不存在 0,接下来要如何进行操作,可使得总的翻转次数最小。**

如果你上次真正理解了我的证明过程的话,那么你会很容易就能证明出本题的贪心思路。

所以这次将这个证明过程留给大家思考 ~

***

### 为什么要「证明」或「理解证明」?

证明的意义在于,**你知道为什么这样做是对的**。

带来的好处是:

1. 一道「贪心」题目能搞清楚证明,那么同类的「贪心」题目你就都会做了。**否则就会停留在“我知道这道题可以这样贪心,别的题我不确定是否也能这样做”**。

2. 在「面试」阶段,**你可以很清晰讲解你的思路**。让面试官从你的「思维方式」上喜欢上你

***

### 更多与证明/分析相关的题解:

[561. 数组拆分 I](https://leetcode-cn.com/problems/array-partition-i/) : [反证法证明贪心算法的正确性](https://leetcode-cn.com/problems/array-partition-i/solution/jue-dui-neng-kan-dong-de-zheng-ming-fan-f7trz/)

[765. 情侣牵手](https://leetcode-cn.com/problems/couples-holding-hands/) : [为什么交换任意一个都是对的?:两种 100% 的解法:并查集 & 贪心](https://leetcode-cn.com/problems/couples-holding-hands/solution/liang-chong-100-de-jie-fa-bing-cha-ji-ta-26a6/)

[1579. 保证图可完全遍历](https://leetcode-cn.com/problems/remove-max-number-of-edges-to-keep-graph-fully-traversable/) : [为什么先处理公共边是对的?含贪心证明 + 数组模板 ~](https://leetcode-cn.com/problems/remove-max-number-of-edges-to-keep-graph-fully-traversable/solution/tan-xin-bing-cha-ji-shu-zu-shi-xian-jian-w7ko/)

[1631. 最小体力消耗路径](https://leetcode-cn.com/problems/path-with-minimum-effort/) : [反证法证明思路的合法性](https://leetcode-cn.com/problems/path-with-minimum-effort/solution/fan-zheng-fa-zheng-ming-si-lu-de-he-fa-x-ohby/)

[11. 盛最多水的容器](https://leetcode-cn.com/problems/container-with-most-water/) : [双指针+贪心解法【含证明】](https://leetcode-cn.com/problems/container-with-most-water/solution/shua-chuan-lc-shuang-zhi-zhen-tan-xin-ji-52gf/)

### 最后

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

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

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

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


997. 找到小镇的法官

Question:
在一个小镇里,按从 $1$ 到 $n$ 为 $n$ 个人进行编号。传言称,这些人中有一个是小镇上的秘密法官。

如果小镇的法官真的存在,那么:

* 小镇的法官不相信任何人。
* 每个人(除了小镇法官外)都信任小镇的法官。
* 只有一个人同时满足条件 $1$ 和条件 $24 。

给定数组 $trust$,该数组由信任对 $trust[i] = [a, b]$ 组成,表示编号为 $a$ 的人信任编号为 $b$ 的人。

如果小镇存在秘密法官并且可以确定他的身份,请返回该法官的编号。否则,返回 $-1$。

示例 1:
```
输入:n = 2, trust = [[1,2]]

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

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

输出:-1
```
示例 4:
```
输入:n = 3, trust = [[1,2],[2,3]]

输出:-1
```
示例 5:
```
输入:n = 4, trust = [[1,3],[1,4],[2,3],[2,4],[4,3]]

输出:3
```

提示:
* $1 <= n <= 1000$
* $0 <= trust.length <= 10^4$
* $trust[i].length == 2$
* $trust[i]$ 互不相同
* $trust[i][0] != trust[i][1]$
* $1 <= trust[i][0], trust[i][1] <= n$

Solution:
### 模拟

今天起晚了 🤣

令 $m$ 为 `trust` 数组长度,对于每个 $trust[i] = (a, b)$ 而言,看作是从 $a$ 指向 $b$ 的有向边。

遍历 `trust`,统计每个节点的「入度」和「出度」:若存在 $a -> b$,则 $a$ 节点「出度」加一,$b$ 节点「入度」加一。

最后遍历所有点,若存在「入度」数量为 $n - 1$,且「出度」数量为 $0$ 的节点即是法官。

代码:
```Java
class Solution {
    public int findJudge(int n, int[][] trust) {
        int[] in = new int[n + 1], out = new int[n + 1];
        for (int[] t : trust) {
            int a = t[0], b = t[1];
            in[b]++; out[a]++;
        }
        for (int i = 1; i <= n; i++) {
            if (in[i] == n - 1 && out[i] == 0) return i;
        }
        return -1;
    }
}
```
* 时间复杂度:$O(m + n)$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


Question:
最大树定义:一棵树,并满足「其中每个节点的值都大于其子树中的任何其他值」。

给你最大树的根节点 `root` 和一个整数 `val` 。

给定的树是利用 `Construct(a)` 例程从列表 `a`(`root = Construct(a)`)递归地构建的:

* 如果 `a` 为空,返回 `null` 。
* 否则,令 `a[i]` 作为 `a` 的最大元素。创建一个值为 `a[i]` 的根节点 `root` 。
* `root` 的左子树将被构建为 `Construct([a[0], a[1], ..., a[i - 1]])` 。
* `root` 的右子树将被构建为 `Construct([a[i + 1], a[i + 2], ..., a[a.length - 1]])` 。
* 返回 `root` 。

请注意,题目没有直接给出 `a` ,只是给出一个根节点 `root = Construct(a)` 。

假设 `b` 是 `a` 的副本,并在末尾附加值 `val`。题目数据保证 `b` 中的值互不相同。

返回 `Construct(b)`。

示例 1:

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

输出:[5,4,null,1,3,null,null,2]

解释:a = [1,4,2,3], b = [1,4,2,3,5]
```
示例 2:

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

输出:[5,2,4,null,1,null,3]

解释:a = [2,1,5,4], b = [2,1,5,4,3]
```
示例 3:

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

输出:[5,2,4,null,1,3]

解释:a = [2,1,5,3], b = [2,1,5,3,4]
```

提示:
* 树中节点数目在范围 $[1, 100]$ 内
* $1 <= Node.val <= 100$
* 树中的所有值 互不相同
* $1 <= val <= 100$

Solution:
### 模拟

题意不是很好理解,先稍微解释一下吧。

大概意思是最大树 `root` 是根据特定的规则构造出来的,即给定的 `root` 其实对应一个具体的 `nums`,题目要求是将 `val` 追加到 `nums` 的尾部,然后再对得到的 `nums` 运用相同规则的构造,返回重新构造的最大树头结点。

根据构造规则,若有下标 $i < j$,则 $nums[i]$ 必然在 $nums[j]$ 水平线的左边,而 `val` 又是追加在原有 `nums` 的结尾。因此其最终位置分如下两种情况:

* `val` 为新 `nums` 中的最大值,同时 `val` 又是追加在原有 `nums` 的结尾,此时将原有的 `root` 挂在 `val` 对应节点的左子树即可,新树的根节点为 `val` 对应节点;
* 否则,我们只需要不断在 `root` 的右子树中找目标位置(反证法可以知,`val` 必不可能出现在任一非右位置,否则可推断出在 `val` 右边仍有元素,这与 `val` 位于 `nums` 的结尾位置冲突)。假设目标位置的父节点为 `prev`,目标位置的原节点为 `cur`,根据构造规则可知 `prev.right = node` 且 `node.left = cur`,新树的根节点不变。

Java 代码:
```Java
class Solution {
    public TreeNode insertIntoMaxTree(TreeNode root, int val) {
        TreeNode node = new TreeNode(val);
        TreeNode prev = null, cur = root;
        while (cur != null && cur.val > val) {
            prev = cur; cur = cur.right;
        }
        if (prev == null) {
            node.left = cur;
            return node;
        } else {
            prev.right = node;
            node.left = cur;
            return root;
        }
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    TreeNode* insertIntoMaxTree(TreeNode* root, int val) {
        TreeNode* node = new TreeNode(val);
        TreeNode* prev = nullptr;
        TreeNode* cur = root;
        while (cur != nullptr && cur->val > val) {
            prev = cur; cur = cur->right;
        }
        if (prev == nullptr) {
            node->left = cur;
            return node;
        } else {
            prev->right = node;
            node->left = cur;
            return root;
        }
    }
};
```
Python 代码:
```Python
class Solution:
    def insertIntoMaxTree(self, root: Optional[TreeNode], val: int) -> Optional[TreeNode]:
        node = TreeNode(val)
        prev, cur = None, root
        while cur is not None and cur.val > val:
            prev = cur
            cur = cur.right
        if prev is None:
            node.left = cur
            return node
        else:
            prev.right = node
            node.left = cur
            return root
```
Typescript 代码:
```Typescript
function insertIntoMaxTree(root: TreeNode | null, val: number): TreeNode | null {
    const node = new TreeNode(val)
    let prev = null, cur = root
    while (cur != null && cur.val > val) {
        prev = cur; cur = cur.right
    }
    if (prev == null) {
        node.left = root
        return node
    } else {
        prev.right = node
        node.left = cur
        return root
    }
};
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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