Question:
给你一个 $n \times n$ 矩阵 `grid` ,矩阵由若干 $0$ 和 $1$ 组成。请你用四叉树表示该矩阵 `grid` 。

你需要返回能表示矩阵的 四叉树 的根结点。

注意,当 `isLeaf` 为 `False` 时,你可以把 `True` 或者 `False` 赋值给节点,两种值都会被判题机制 接受 。

四叉树数据结构中,每个内部节点只有四个子节点。此外,每个节点都有两个属性:

* `val`:储存叶子结点所代表的区域的值。$1$ 对应 `True`,$0$ 对应 `False`;
* `isLeaf`: 当这个节点是一个叶子结点时为 `True`,如果它有 $4$ 个子节点则为 `False` 。
```Java
class Node {
    public boolean val;
    public boolean isLeaf;
    public Node topLeft;
    public Node topRight;
    public Node bottomLeft;
    public Node bottomRight;
}
```
我们可以按以下步骤为二维区域构建四叉树:

1. 如果当前网格的值相同(即,全为 $0$ 或者全为 $1$),将 `isLeaf` 设为 `True` ,将 `val` 设为网格相应的值,并将四个子节点都设为 `Null` 然后停止。
2. 如果当前网格的值不同,将 `isLeaf` 设为 `False`, 将 `val` 设为任意值,然后如下图所示,将当前网格划分为四个子网格。
3. 使用适当的子网格递归每个子节点。

四叉树格式:

输出为使用层序遍历后四叉树的序列化形式,其中 `null` 表示路径终止符,其下面不存在节点。

它与二叉树的序列化非常相似。唯一的区别是节点以列表形式表示 $[isLeaf, val]$ 。

如果 `isLeaf` 或者 `val` 的值为 `True` ,则表示它在列表 $[isLeaf, val]$ 中的值为 $1$ ;如果 `isLeaf` 或者 `val` 的值为 `False` ,则表示值为 $0$ 。

示例 1:

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

输出:[[0,1],[1,0],[1,1],[1,1],[1,0]]

解释:请注意,在下面四叉树的图示中,0 表示 false,1 表示 True 。
```

示例 2:

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

输出:[[0,1],[1,1],[0,1],[1,1],[1,0],null,null,null,null,[1,0],[1,0],[1,1],[1,1]]

解释:网格中的所有值都不相同。我们将网格划分为四个子网格。
topLeft,bottomLeft 和 bottomRight 均具有相同的值。
topRight 具有不同的值,因此我们将其再分为 4 个子网格,这样每个子网格都具有相同的值。
```

示例 3:

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

输出:[[1,1]]
```
示例 4:
```
输入:grid = [[0]]

输出:[[1,0]]
```
示例 5:
```
输入:grid = [[1,1,0,0],[1,1,0,0],[0,0,1,1],[0,0,1,1]]

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

提示:
* $n == grid.length == grid[i].length$
* $n == 2^x$ 其中 $0 <= x <= 6$

Solution:
### 递归

假定我们存在函数 `Node dfs(int a, int b, int c, int d)`,其能够返回「以 $(a, b)$ 为左上角,$(c, d)$ 为右下角」所代表的矩阵的根节点。

那么最终答案为 `dfs(0, 0, n-1, n-1)`,不失一般性考虑「以 $(a, b)$ 为左上角,$(c, d)$ 为右下角」时如何计算:

* 判断该矩阵是否为全 $0$ 或全 $1$:
    * 如果是则直接创建根节点(该节点四个子节点属性均为空)并进行返回;
    * 如果不是则创建根节点,递归创建四个子节点并进行赋值,利用左上角 $(a,b)$ 和右下角 $(c, d)$ 可算的横纵坐标的长度为 $c - a + 1$ 和 $d - b + 1$,从而计算出将当前矩阵四等分所得到的子矩阵的左上角和右下角坐标。

由于矩阵大小最多为 $2^6 = 64$ ,因此判断某个子矩阵是否为全 $0$ 或全 $1$ 的操作用「前缀和」或者是「暴力」来做都可以。

代码:
```Java
class Solution {
    int[][] g;
    public Node construct(int[][] grid) {
        g = grid;
        return dfs(0, 0, g.length - 1, g.length - 1);
    }
    Node dfs(int a, int b, int c, int d) {
        boolean ok = true;
        int t = g[a][b];
        for (int i = a; i <= c && ok; i++) {
            for (int j = b; j <= d && ok; j++) {
                if (g[i][j] != t) ok = false;
            }
        }
        if (ok) return new Node(t == 1, true);
        Node root = new Node(t == 1, false);
        int dx = c - a + 1, dy = d - b + 1;
        root.topLeft = dfs(a, b, a + dx / 2 - 1, b + dy / 2 - 1);
        root.topRight = dfs(a, b + dy / 2, a + dx / 2 - 1, d);
        root.bottomLeft = dfs(a + dx / 2, b, c, b + dy / 2 - 1);
        root.bottomRight = dfs(a + dx / 2, b + dy / 2, c, d);
        return root;
    }
}
```
* 时间复杂度:递归的复杂度分析要根据主定理,假设矩阵大小为 $n \times n$,根据主定理 $T(n) = aT(\frac{n}{b}) + f(n)$,单次递归最多会产生 $4$ 个子问题(由大矩阵递归 $4$ 个小矩阵),因此问题递归子问题数量 $a = 4$,而子问题规模缩减系数 $b$ 为原本的一半(子矩阵的大小为 $\frac{n}{2} \times \frac{n}{2}$),剩余的 $f(n)$ 为判断全 $0$ 和 全 $1$ 的时间开销,不考虑标识位 $ok$ 带来的剪枝效果,每次判断全 $0$ 或全 $1$ 的复杂度与当前问题规模相等,即 $f(n) = O(n^2)$,但整个大小为 $n \times n$ 矩阵每次进行长宽减半的子矩阵拆分,最多会被拆分为 $\log{n}$ 次,因此这部分总的计算量为 $\log{n} \times n^2$ 。整体复杂度为 $O(n^2 + \log{n} \times n^2)$
* 空间复杂度:忽略递归带来的额外空间开销,复杂度为 $O(1)$

---

### 递归(前缀和优化)

使用前缀和优化「判断全 $0$ 和全 $1$」的操作:对矩阵 `grid` 求前缀和数组 `sum`,对于一个「以左上角为 $(a, b)$,右下角为 $(c, d)$ 」的子矩阵而言,其所包含的格子总数为 $tot = (c - a + 1) * (d - b + 1)$ 个,当且仅当矩阵和为 $0$ 或 $tot$ 时,矩阵全 $0$ 或 $1$。

代码:
```Java
class Solution {
    static int[][] sum = new int[70][70];
    int[][] g;
    public Node construct(int[][] grid) {
        g = grid;
        int n = grid.length;
        for (int i = 1; i <= n; i++) {
            for (int j = 1; j <= n; j++) {
                sum[i][j] = sum[i - 1][j] + sum[i][j - 1] - sum[i - 1][j - 1] + g[i - 1][j - 1];
            }
        }
        return dfs(0, 0, n - 1, n - 1);
    }
    Node dfs(int a, int b, int c, int d) {
        int cur = sum[c + 1][d + 1] - sum[a][d + 1] - sum[c + 1][b] + sum[a][b];
        int dx = c - a + 1, dy = d - b + 1, tot = dx * dy;
        if (cur == 0 || cur == tot) return new Node(g[a][b] == 1, true);
        Node root = new Node(g[a][b] == 1, false);
        root.topLeft = dfs(a, b, a + dx / 2 - 1, b + dy / 2 - 1);
        root.topRight = dfs(a, b + dy / 2, a + dx / 2 - 1, d);
        root.bottomLeft = dfs(a + dx / 2, b, c, b + dy / 2 - 1);
        root.bottomRight = dfs(a + dx / 2, b + dy / 2, c, d);
        return root;
    }
}
```
* 时间复杂度:分析同理,但判断全 $0$ 和全 $1$ 的复杂度下降为 $O(1)$,整体复杂度为 $O(n^2 + \log{n})$
* 空间复杂度:忽略递归带来的额外空间开销,复杂度为 $O(n^2)$

### 最后

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

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

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

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


429. N 叉树的层序遍历

Question:
给定一个 `N` 叉树,返回其节点值的层序遍历。(即从左到右,逐层遍历)。

树的序列化输入是用层序遍历,每组子节点都由 `null` 值分隔(参见示例)。

示例 1:

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

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

```
输入:root = [1,null,2,3,4,5,null,null,6,7,null,8,null,9,10,null,null,11,null,12,null,13,null,null,14]

输出:[[1],[2,3,4,5],[6,7,8,9,10],[11,12,13],[14]]
```

提示:
* 树的高度不会超过 `1000`
* 树的节点总数在 $[0, 10^4]$ 之间

Solution:
### BFS

根据题意进行 `BFS` 即可。

由于我们需要以「层」为单位构建答案,因此在单次 `BFS` 过程中也按层进行。

代码:
```Java
class Solution {
    public List<List<Integer>> levelOrder(Node root) {
        List<List<Integer>> ans = new ArrayList<>();
        Deque<Node> d = new ArrayDeque<>();
        if (root != null) d.addLast(root);
        while (!d.isEmpty()) {
            int size = d.size();
            List<Integer> list = new ArrayList<>();
            while (size-- > 0) {
                Node t = d.pollFirst();
                for (Node node : t.children) d.addLast(node);
                list.add(t.val);
            }
            ans.add(list);
        }
        return ans;
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


430. 扁平化多级双向链表

Question:
多级双向链表中,除了指向下一个节点和前一个节点指针之外,它还有一个子链表指针,可能指向单独的双向链表。这些子列表也可能会有一个或多个自己的子项,依此类推,生成多级数据结构,如下面的示例所示。

给你位于列表第一级的头节点,请你扁平化列表,使所有结点出现在单级双链表中。

示例 1:
```
输入:head = [1,2,3,4,5,6,null,null,null,7,8,9,10,null,null,11,12]

输出:[1,2,3,7,8,11,12,9,10,4,5,6]

解释:
```
输入的多级列表如下图所示:

扁平化后的链表如下图:

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

输出:[1,3,2]

解释:
输入的多级列表如下图所示:

  1---2---NULL
  |
  3---NULL
```

示例 3:
```
输入:head = []

输出:[]
```

Solution:
### 递归

一道常规链表模拟题。

利用 `flatten` 函数本身的含义(将链表头为 $head$ 的链表进行扁平化,并将扁平化后的头结点进行返回),我们可以很容易写出递归版本。

为防止空节点等边界问题,起始时建立一个哨兵节点 $dummy$ 指向 $head$,然后利用 $head$ 指针从前往后处理链表:

* 当前节点 $head$ 没有 $child$ 节点:直接让指针后即可,即 $head = head.next$;
* 当前节点 $head$ 有 $child$ 节点:将 $head.child$ 传入 `flatten` 函数递归处理,拿到普遍化后的头结点 $chead$,然后将 $head$ 和 $chead$ 建立“相邻”关系(注意要先存起来原本的 $tmp = head.next$ 以及将 $head.child$ 置空),然后继续往后处理,直到扁平化的 $chead$ 链表的尾部,将其与 $tmp$ 建立“相邻”关系。

重复上述过程,直到整条链表被处理完。

代码:
```Java
class Solution {
    public Node flatten(Node head) {
        Node dummy = new Node(0);
        dummy.next = head;
        while (head != null) {
            if (head.child == null) {
                head = head.next;
            } else {
                Node tmp = head.next;
                Node chead = flatten(head.child);
                head.next = chead;
                chead.prev = head;
                head.child = null;
                while (head.next != null) head = head.next;
                head.next = tmp;
                if (tmp != null) tmp.prev = head;
                head = tmp;
            }
        }
        return dummy.next;
    }
}
```
* 时间复杂度:最坏情况下,每个节点会被访问 $h$ 次($h$ 为递归深度,最坏情况下 $h = n$)。整体复杂度为 $O(n^2)$
* 空间复杂度:最坏情况下所有节点都分布在 `child` 中,此时递归深度为 $n$。复杂度为 $O(n)$

---

### 递归(优化)

在上述解法中,由于我们直接使用 `flatten` 作为递归函数,导致递归处理 $head.child$ 后不得不再进行遍历来找当前层的“尾结点”,这导致算法复杂度为 $O(n^2)$。

一个可行的优化是,额外设计一个递归函数 `dfs` 用于返回扁平化后的链表“尾结点”,从而确保我们找尾结点的动作不会在每层发生。

代码:
```Java
class Solution {
    public Node flatten(Node head) {
        dfs(head);
        return head;
    }
    Node dfs(Node head) {
        Node last = head;
        while (head != null) {
            if (head.child == null) {
                last = head;
                head = head.next;
            } else {
                Node tmp = head.next;
                Node childLast = dfs(head.child);
                head.next = head.child;
                head.child.prev = head;
                head.child = null;
                if (childLast != null) childLast.next = tmp;
                if (tmp != null) tmp.prev = childLast;
                last = head;
                head = childLast;
            }
        }
        return last;
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:最坏情况下所有节点都分布在 `child` 中,此时递归深度为 $n$。复杂度为 $O(n)$

---

### 迭代

自然也能够使用迭代进行求解。

与「递归」不同的是,「迭代」是以“段”为单位进行扁平化,而「递归」是以深度(方向)进行扁平化,这就导致了两种方式对每个扁平节点的处理顺序不同。

已样例 $1$ 为 🌰。

递归的处理节点(新的 $next$ 指针的构建)顺序为:

迭代的处理节点(新的 $next$ 指针的构建)顺序为:

但由于链表本身不存在环,「迭代」的构建顺序发生调整,仍然可以确保每个节点被访问的次数为常数次。

代码:
```Java
class Solution {
    public Node flatten(Node head) {
        Node dummy = new Node(0);
        dummy.next = head;
        for (; head != null; ) {
            if (head.child == null) {
                head = head.next;
            } else {
                Node tmp = head.next;
                Node child = head.child;
                head.next = child;
                child.prev = head;
                head.child = null;
                Node last = head;
                while (last.next != null) last = last.next;
                last.next = tmp;
                if (tmp != null) tmp.prev = last;
                head = head.next;
            }
        }
        return dummy.next;
    }
}
```
* 时间复杂度:可以发现,迭代写法的扁平化过程并不与遍历方向保持一致(以段为单位进行扁平化,而非像递归那样总是往遍历方向进行扁平化),但每个节点被访问的次数仍为常数次。复杂度为 $O(n)$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


LeetCode/431-440/432. 全 O(1) 的数据结构（困难）.md

Question:
请你设计一个用于存储字符串计数的数据结构,并能够返回计数最小和最大的字符串。

实现 `AllOne` 类:

* `AllOne()` 初始化数据结构的对象。
* `inc(String key)` 字符串 `key` 的计数增加 $1$ 。如果数据结构中尚不存在 `key` ,那么插入计数为 $1$ 的 `key` 。
* `dec(String key)` 字符串 `key` 的计数减少 $1$ 。如果 `key` 的计数在减少后为 $0$ ,那么需要将这个 `key` 从数据结构中删除。测试用例保证:在减少计数前,`key` 存在于数据结构中。
* `getMaxKey()` 返回任意一个计数最大的字符串。如果没有元素存在,返回一个空字符串 `""` 。
* `getMinKey()` 返回任意一个计数最小的字符串。如果没有元素存在,返回一个空字符串 `""` 。

示例:
```
输入
["AllOne", "inc", "inc", "getMaxKey", "getMinKey", "inc", "getMaxKey", "getMinKey"]
[[], ["hello"], ["hello"], [], [], ["leet"], [], []]

输出
[null, null, null, "hello", "hello", null, "hello", "leet"]

解释
AllOne allOne = new AllOne();
allOne.inc("hello");
allOne.inc("hello");
allOne.getMaxKey(); // 返回 "hello"
allOne.getMinKey(); // 返回 "hello"
allOne.inc("leet");
allOne.getMaxKey(); // 返回 "hello"
allOne.getMinKey(); // 返回 "leet"
```

提示:
* $1 <= key.length <= 10$
* `key` 由小写英文字母组成
* 测试用例保证:在每次调用 `dec` 时,数据结构中总存在 `key`
* 最多调用 `inc`、`dec`、`getMaxKey` 和 `getMinKey` 方法 $5 * 10^4$ 次

Solution:
### 双向链表 + 哈希表

题目要求我们支持 $O(1)$ 的查询和修改,其中查询只需返回任意一个计数次数「最多」和「最少」的元素即可(如果有)。

虽然插入的字符串长度不超过 $10$(该数据范围的含义为字符串的哈希计算消耗可看作常数),但单纯的使用「哈希表」仅能做到 $O(1)$ 的计数,无法做到 $O(1)$ 查询。

**我们可以采取和 [实现一个 LRUCache](https://mp.weixin.qq.com/s?__biz=MzU4NDE3MTEyMA==&mid=2247486820&idx=1&sn=2055864e10848bce55afc4e2feda79a7&chksm=fd9ca67bcaeb2f6d2038706e32cafffd701d3d6b71b60c52a568ed6dc0e268d93f8a78498105&token=146288031&lang=zh_CN#rd) & [实现一个 LFUCache](https://mp.weixin.qq.com/s?__biz=MzU4NDE3MTEyMA==&mid=2247486856&idx=1&sn=59b2ed57e4a75eac0e63fc0cf08bed5d&chksm=fd9ca697caeb2f81f83cdc741375f9130c3fa40463edb4ae98d702354ca106378fdfe23735ad&token=146288031&lang=zh_CN#rd) 类似的思路,通过自定义节点并手写双链表来实现。**

定义一个节点类 `Node`,除了包含用于实现双向链表的 `left` 和 `right` 以外,还包含一个数值类型的变量 `cnt`, 用于记录该节点存储的是计数次数为多少的元素,以及一个 `Set` 类型的容器,用于支持 $O(1)$ 插入和删除元素,记作 `set`。

同时为了快速知道某个字符串属于哪个 `Node`,我们还需要开一个「哈希表」进行定位(以字符串为哈希表的键,字符串所在 `Node` 作为值),当定位到字符串对应的 `Node` 之后则可以利用双向链表的 $O(1)$ 增加/修改/删除。

在双向链表中,起始只有两个哨兵节点 `hh` 和 `tt` ,当进行若干 `inc/dec` 操作后的基本形态为:

对应几个操作:

`inc/dec` 操作:当对一个字符串 `key` 进行「增加计数」或「减少计数」时,先在哈希表中看 `key` 是否存在:

* 若存在:根据其所属的 `Node` 的计数 `cnt` 为多少,并结合当前是「增加计数」还是「减少计数」来决定是找 `Node` 的「右节点」还是「左节点」,同时检查相邻节点的计数值 `cnt` 是否为目标值,对应要检查数值是 $cnt + 1$ 和 $cnt - 1$:
  * 若相邻节点的 `cnt` 为目标值:即目标节点存在,将 `key` 从原 `Node` 的 `set` 集合中移除,并添加到目标节点的集合中,更新哈希表;
  * 若相邻节点的 `cnt` 不是目标值:则需要创建相应的目标节点,并构建双向链表关系,把 `key` 存入新创建的目标节点,更新哈希表。
* 若不存在(只能是 `inc` 操作):查找是否存在 $cnt = 1$ 的节点(也就是检查 `hh.right` 节点的计数值):
  * 如果存在 $cnt = 1$ 的目标节点:将 `key` 添加到目标节点的 `set` 集合中,更新哈希表;
  * 若不存在 $cnt = 1$ 的目标节点:创建相应的节点,并构建双向关系,并构建双向链表关系,把 `key` 存入新创建的目标节点,更新哈希表。

`getMaxKey/getMinKey` 操作:分别从 `tt.left` 和 `hh.right` 中尝试查找,如果存在非哨兵节点,则从节点的 `set` 集合中取任意元素进行返回,否则返回空串。

最后,为了确保 `getMaxKey/getMinKey` 操作能够严格 $O(1)$,我们在进行 `inc/dec` 操作时我们需要对一些 `set` 容量为 $0$ 的节点进行释放,即解除其所在双向链表的关系。

代码:
```Java
class AllOne {

    class Node {
        int cnt;
        Set<String> set = new HashSet<>();
        Node left, right;
        Node(int _cnt) {
            cnt = _cnt;
        }
    }

    Node hh, tt;
    Map<String, Node> map = new HashMap<>();
    public AllOne() {
        hh = new Node(-1000); tt = new Node(-1000);
        hh.right = tt; tt.left = hh;
    }

    void clear(Node node) {
        if (node.set.size() == 0) {
            node.left.right = node.right;
            node.right.left = node.left;
        }
    }

    public void inc(String key) {
        if (map.containsKey(key)) {
            Node node = map.get(key);
            node.set.remove(key);
            int cnt = node.cnt;
            Node next = null;
            if (node.right.cnt == cnt + 1) {
                next = node.right;
            } else {
                next = new Node(cnt + 1);
                next.right = node.right;
                next.left = node;
                node.right.left = next;
                node.right = next;
            }
            next.set.add(key);
            map.put(key, next);
            clear(node);
        } else {
            Node node = null;
            if (hh.right.cnt == 1) {
                node = hh.right;
            } else {
                node = new Node(1);
                node.right = hh.right;
                node.left = hh;
                hh.right.left = node;
                hh.right = node;
            }
            node.set.add(key);
            map.put(key, node);
        }
    }

    public void dec(String key) {
        Node node = map.get(key);
        node.set.remove(key);
        int cnt = node.cnt;
        if (cnt == 1) {
            map.remove(key);
        } else {
            Node prev = null;
            if (node.left.cnt == cnt - 1) {
                prev = node.left;
            } else {
                prev = new Node(cnt - 1);
                prev.right = node;
                prev.left = node.left;
                node.left.right = prev;
                node.left = prev;
            }
            prev.set.add(key);
            map.put(key, prev);
        }
        clear(node);
    }

    public String getMaxKey() {
        Node node = tt.left;
        for (String str : node.set) return str;
        return "";
    }

    public String getMinKey() {
        Node node = hh.right;
        for (String str : node.set) return str;
        return "";
    }
}
```
* 时间复杂度:$O(1)$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


Question:
基因序列可以表示为一条由 $8$ 个字符组成的字符串,其中每个字符都是 `'A'`、`'C'`、`'G'` 和 `'T'` 之一。

假设我们需要调查从基因序列 `start` 变为 `end` 所发生的基因变化。一次基因变化就意味着这个基因序列中的一个字符发生了变化。

* 例如,`"AACCGGTT" --> "AACCGGTA"` 就是一次基因变化。

另有一个基因库 `bank` 记录了所有有效的基因变化,只有基因库中的基因才是有效的基因序列。

给你两个基因序列 `start` 和 `end` ,以及一个基因库 `bank` ,请你找出并返回能够使 `start` 变化为 `end` 所需的最少变化次数。如果无法完成此基因变化,返回 $-1$ 。

注意:起始基因序列 `start` 默认是有效的,但是它并不一定会出现在基因库中。

示例 1:
```
输入:start = "AACCGGTT", end = "AACCGGTA", bank = ["AACCGGTA"]

输出:1
```
示例 2:
```
输入:start = "AACCGGTT", end = "AAACGGTA", bank = ["AACCGGTA","AACCGCTA","AAACGGTA"]

输出:2
```
示例 3:
```
输入:start = "AAAAACCC", end = "AACCCCCC", bank = ["AAAACCCC","AAACCCCC","AACCCCCC"]

输出:3
```
提示:
* $start.length == 8$
* $end.length == 8$
* $0 <= bank.length <= 10$
* $bank[i].length == 8$
* `start`、`end` 和 `bank[i]` 仅由字符 `['A', 'C', 'G', 'T']` 组成

Solution:
### BFS

为了方便,我们令 $S = start$、 $T = end$,将每个基因序列视为「状态」。

容易想到使用 `BFS`  进行求解,并使用「哈希表」记录到达某个状态所消耗的步数(同时为了快速判断某个状态是否合法,我们使用 `Set` 结构对 $bank[i]$ 进行转存)。

起始将 `S` 加入队列,并更新到达 `S` 所使用的步数为 $0$,然后进行常规的 `BFS` 过程:每次取出队头元素,尝试替换当前状态的某一位,来得到新的状态(限定新状态必须合法,即必须出现在 `Set` 中),如果新状态合法并且没有在记录步数的哈希表中出现过,则将新状态入队并更新得到新状态所用步数,否则丢弃新状态。

重复上述过程直到找到 `T`(返回具体步数) 或者队列为空(返回 $-1$)。

代码:
```Java
class Solution {
    static char[] items = new char[]{'A', 'C', 'G', 'T'};
    public int minMutation(String S, String T, String[] bank) {
        Set<String> set = new HashSet<>();
        for (String s : bank) set.add(s);
        Deque<String> d = new ArrayDeque<>();
        Map<String, Integer> map = new HashMap<>();
        d.addLast(S);
        map.put(S, 0);
        while (!d.isEmpty()) {
            int size = d.size();
            while (size-- > 0) {
                String s = d.pollFirst();
                char[] cs = s.toCharArray();
                int step = map.get(s);
                for (int i = 0; i < 8; i++) {
                    for (char c : items) {
                        if (cs[i] == c) continue;
                        char[] clone = cs.clone();
                        clone[i] = c;
                        String sub = String.valueOf(clone);
                        if (!set.contains(sub)) continue;
                        if (map.containsKey(sub)) continue;
                        if (sub.equals(T)) return step + 1;
                        map.put(sub, step + 1);
                        d.addLast(sub);
                    }
                }
            }
        }
        return -1;
    }
}
```
* 时间复杂度:令 $n$ 为 `bank` 的数组长度(合法状态数),将 `bank` 存入 `Set` 结构复杂度为 $O(n)$,每个状态经过一步操作最多拓展出 $C = 32$ 个新基因(共有 $8$ 个位置,每个位置有 $4$ 个选择),`BFS`  过程复杂度为 $O(C \times n)$。整体复杂度为  $O(C \times n)$
* 空间复杂度:$O(n)$

---

### 双向 BFS

同理,我们可以使用「双向 `BFS`」进行求解。

双向 `BFS` 与常规 `BFS`  相比,能够有效解决「搜索空间爆炸」的问题:

对双向 `BFS`  不熟悉的同学可以看前置🧀:[(题解) 127. 单词接龙](https://leetcode-cn.com/problems/word-ladder/solution/gong-shui-san-xie-ru-he-shi-yong-shuang-magjd/)。

代码:
```Java
class Solution {
    static char[] items = new char[]{'A', 'C', 'G', 'T'};
    Set<String> set = new HashSet<>();
    public int minMutation(String S, String T, String[] bank) {
        set.add(S);
        for (String s : bank) set.add(s);
        if (!set.contains(T)) return -1;
        Deque<String> d1 = new ArrayDeque<>(), d2 = new ArrayDeque<>();
        d1.addLast(S); d2.addLast(T);
        Map<String, Integer> m1 = new HashMap<>(), m2 = new HashMap<>();
        m1.put(S, 0); m2.put(T, 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;
        }
        return -1;
    }
    int update(Deque<String> d, Map<String, Integer> cur, Map<String, Integer> other) {
        int m = d.size();
        while (m-- > 0) {
            String s = d.pollFirst();
            char[] cs = s.toCharArray();
            int step = cur.get(s);
            for (int i = 0; i < 8; i++) {
                for (char c : items) {
                    if (cs[i] == c) continue;
                    char[] clone = cs.clone();
                    clone[i] = c;
                    String sub = String.valueOf(clone);
                    if (!set.contains(sub) || cur.containsKey(sub)) continue;
                    if (other.containsKey(sub)) return other.get(sub) + step + 1;
                    d.addLast(sub);
                    cur.put(sub, step + 1);
                }
            }
        }
        return -1;
    }
}
```
* 时间复杂度:令 $n$ 为 `bank` 的数组长度(合法状态数),将 `bank` 存入 `Set` 结构复杂度为 $O(n)$,每个状态经过一步操作最多拓展出 $C = 32$ 个新基因(共有 $8$ 个位置,每个位置有 $4$ 个选择),`BFS`  过程复杂度为 $O(C \times n)$。整体复杂度为  $O(C \times n)$
* 空间复杂度:$O(n)$

---

### AStar 算法

若不考虑 `bank` 的限制,对于一个特定状态而言,我们可以任意选择一位替换为 $4$ 类字符之一,因此对于任意状态 $x$ 而言,其与目标状态 $T$ 的「理论最小转换步数」为两者对应位置不同字符的数量,而由于存在 `bank` 限制,实际最小步数必然满足「大于等于」该理论最小转换步数。

基于此,我们可以计算当前状态到目标状态的「理论最小转换步数」作为启发式函数,进行启发式搜索。

具体的,我们使用优先队列(堆)维护所有的状态,每次优先「启发值 = 理论最小转换步数」的状态进行优先出队拓展。

对「AStar 算法」不了解的同学可以看前置 🧀:[发挥 A* 算法最大价值的关键点](https://mp.weixin.qq.com/s?__biz=MzU4NDE3MTEyMA==&mid=2247489588&idx=1&sn=479e4c0627247ab7e20af7909f2a8b64)。

代码:
```Java
class Solution {
    class Node {
        String s;
        int val;
        Node(String _s) {
            s = _s;
            for (int i = 0; i < 8; i++) {
                if (s.charAt(i) != T.charAt(i)) val++;
            }
        }
    }
    static char[] items = new char[]{'A', 'C', 'G', 'T'};
    String S, T;
    public int minMutation(String start, String end, String[] bank) {
        Set<String> set = new HashSet<>();
        for (String s : bank) set.add(s);
        S = start; T = end;
        PriorityQueue<Node> q = new PriorityQueue<>((a,b)->a.val-b.val);
        Map<String, Integer> map = new HashMap<>();
        q.add(new Node(S));
        map.put(S, 0);
        while (!q.isEmpty()) {
            Node node = q.poll();
            char[] cs = node.s.toCharArray();
            int step = map.get(node.s);
            for (int i = 0; i < 8; i++) {
                for (char c : items) {
                    if (cs[i] == c) continue;
                    char[] clone = cs.clone();
                    clone[i] = c;
                    String sub = String.valueOf(clone);
                    if (!set.contains(sub)) continue;
                    if (sub.equals(T)) return step + 1;
                    if (!map.containsKey(sub) || map.get(sub) > step + 1) {
                        map.put(sub, step + 1);
                        q.add(new Node(sub));
                    }
                }
            }
        }
        return -1;
    }
}
```
* 时间复杂度:启发式搜索分析时空复杂度意义不大
* 空间复杂度:启发式搜索分析时空复杂度意义不大

---

### 建图 + DFS

**由 `S` 和 $bank[i]$ 组成合法点集,且点集中任意两点之间存在无向边的充要条件是:点 $u$ 和点 $v$ 所代表的字符中,仅有一个位置字符不同。**

因此我们可以将所有的点存入 `list` 中,假设 `list` 长度为 $n$。同时为了方便,我们人为确保 `S` 出现在头部(点编号为 $1$),`T` 出现在尾部(点编号为 $n$)。

遍历 `list` 进行建图(对于两字符串中仅有一位置不同的点进行连边操作),然后跑一遍从 $1$ 到 $n$ 的 `DFS`。

由于图中可能有环或无解,因此必须「设定一个最大搜索深度」并增加「最优解剪枝」,确保搜索过程结束。

最大搜索深度的设定可以利用反证法:如果 `S` 能够到达 `T`,那么最优路径中必然不存在环(否则可以把环去掉,得到一条更短的路径),即最优路径所经过的点的数量必然不超过 $n$。

代码:
```Java
class Solution {
    int N = 15, M = 15 * 15 * 2 + 50, idx = 0, loc = 1;
    int[] he = new int[N], e = new int[M], ne = new int[M];
    int n, ans;
    void add(int a, int b) {
        e[idx] = b;
        ne[idx] = he[a];
        he[a] = idx++;
    }
    void dfs(int u, int fa, int depth) {
        if (depth >= ans) return ; // 最优解剪枝
        if (u == n) {
            ans = depth;
            return ;
        }
        for (int i = he[u]; i != -1; i = ne[i]) {
            int j = e[i];
            if (j == fa) continue;
            dfs(j, u, depth + 1);
        }
    }
    public int minMutation(String S, String T, String[] bank) {
        List<String> list = new ArrayList<>();
        list.add(S);
        boolean ok = false;
        for (String s : bank) {
            if (s.equals(S)) continue;
            if (s.equals(T)) {
                ok = true;
                continue;
            }
            list.add(s);
        }
        if (!ok) return -1;
        list.add(T);
        n = list.size();
        ans = n;
        Arrays.fill(he, -1);
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < n; j++) {
                if (i == j) continue;
                int cnt = 0;
                for (int k = 0; k < 8 && cnt <= 1; k++) {
                    if (list.get(i).charAt(k) != list.get(j).charAt(k)) cnt++;
                }
                if (cnt == 1) {
                    add(i + 1, j + 1); add(j + 1, i + 1);
                }
            }
        }
        dfs(1, -1, 0);
        return ans == n ? -1 : ans;
    }
}
```
* 时间复杂度:令 `bank` 的长度为 $n$(即点集的数量级为 $n$),预处理出 `list` 的复杂度为 $O(n)$;建图操作的复杂度为 $O(C \times n^2)$,其中 $C = 8$ 基因序列长度;`DFS` 过程由于设定了最大搜索深度,复杂度为 $O(n^2)$。整体复杂度为 $O(C \times n^2)$
* 空间复杂度:最坏情况下为完全图,复杂度为 $O(n^2)$

### 最后

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

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

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

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


434. 字符串中的单词数

Question:
统计字符串中的单词个数,这里的单词指的是连续的不是空格的字符。

请注意,你可以假定字符串里不包括任何不可打印的字符。

示例:
```
输入: "Hello, my name is John"

输出: 5

解释: 这里的单词是指连续的不是空格的字符,所以 "Hello," 算作 1 个单词。
```

Solution:
### 模拟

题目对于「单词」的定义为「连续的不是空格的字符」。

因此,我们可以从前往后处理字符串 `s` 并进行计数,对于是空格的字符进行跳过(不计数),而对于非空格字符,则在遍历完一个完整单词(连续一段)后进行一次计数。

代码:
```Java
class Solution {
    public int countSegments(String s) {
        int n = s.length();
        int ans = 0;
        for (int i = 0; i < n; ) {
            if (s.charAt(i) == ' ' && i++ >= 0) continue;
            while (i < n && s.charAt(i) != ' ') i++;
            ans++;
        }
        return ans;
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


Question:
给你一个区间数组 `intervals` ,其中 $intervals[i] = [start_i, end_i]$,且每个 $start_i$ 都 不同 。

区间 $i$ 的 右侧区间 可以记作区间 $j$ ,并满足 $start_j >= end_i$ ,且 $start_j$ 最小化 。

返回一个由每个区间 $i$ 的 右侧区间 的最小起始位置组成的数组。如果某个区间 $i$ 不存在对应的 右侧区间 ,则下标 $i$ 处的值设为 $-1$ 。

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

输出:[-1]

解释:集合中只有一个区间,所以输出-1。
```
示例 2:
```
输入:intervals = [[3,4],[2,3],[1,2]]

输出:[-1,0,1]

解释:对于 [3,4] ,没有满足条件的“右侧”区间。
对于 [2,3] ,区间[3,4]具有最小的“右”起点;
对于 [1,2] ,区间[2,3]具有最小的“右”起点。
```
示例 3:
```
输入:intervals = [[1,4],[2,3],[3,4]]

输出:[-1,2,-1]

解释:对于区间 [1,4] 和 [3,4] ,没有满足条件的“右侧”区间。
对于 [2,3] ,区间 [3,4] 有最小的“右”起点。
```

提示:
* $1 <= intervals.length <= 2 \times 10^4$
* $intervals[i].length == 2$
* $-10^6 <= start_i <= end_i <= 10^6$
* 每个间隔的起点都 不相同

Solution:
### 排序 + 二分

为了方便,我们称 $intervals$ 为 $its$。

对于每个 $its[i]$ 而言,我们需要在所有满足「$its[j][0] \geqslant its[i][1]$」中找到 $its[j][0]$ 值最小的下标 $j$,并将其记为 $ans[i]$。

对于一个特定的 $its[i]$ 而言,其右端点固定,并且我们只关心目标位置的左端点。

因此我们可以构造一个记录区间左端点的数组 $clone$,并将其进行排序,同时为了记录每个左端点来自于原序列中的那个下标,还需要额外记录原序列下标,即以 $(start, idx)$ 二元组的形式进行转存,并根据 $start$ 排序。

然后从前往后处理每个 $its[i]$,运用「二分」在 $clone$ 中找到第一个满足左端点 $start$ 大于等于 $its[i][1]$ 的成员 $clone[j]$,将其 $clone[j][1]$ 即是 $its[i]$ 的最右区间。

代码:
```Java
class Solution {
    public int[] findRightInterval(int[][] its) {
        int n = its.length;
        int[][] clone = new int[n][2];
        for (int i = 0; i < n; i++) clone[i] = new int[]{its[i][0], i};
        Arrays.sort(clone, (a,b)->a[0]-b[0]);
        int[] ans = new int[n];
        for (int i = 0; i < n; i++) {
            int l = 0, r = n - 1;
            while (l < r) {
                int mid = l + r >> 1;
                if (clone[mid][0] >= its[i][1]) r = mid;
                else l = mid + 1;
            }
            ans[i] = clone[r][0] >= its[i][1] ? clone[r][1] : -1;
        }
        return ans;
    }
}
```
* 时间复杂度:排序复杂度为 $O(n\log{n})$;对于每个 $its[i]$ 找到最右区间需要进行二分,复杂度为 $O(n\log{n})$。整体复杂度为 $O(n\log{n})$
* 空间复杂度:$O(n)$

---

### 双指针(莫队思想)

更进一步,在解法一中我们并没有对求解询问的顺序进行调整,这导致了我们不得不每次都在整个左端点序列中进行二分。

朴素处理询问的方式,需要每次对整个序列进行扫描,复杂度为 $O(n^2)$。

实际上,如果我们按照「右端点从小到大」的顺序处理询问,其每个询问对应的「最右区间的左端点」也具有单调特性。

因此,我们可以运用莫队思想:**通过调整询问的处理顺序,来减少扫描目标位置的指针移动次数。将其从「必然进行 $n^2$ 次移动」优化为「最多不超过 $n$ 次移动」,从而将 构造答案 的复杂度从 $O(n^2)$ 优化为 $O(n)$。**

最后,由于每个 $its[i]$ 只关心目标位置的「左端点」,因此我们无须对某一段进行分块,而直接使用双指针实现即可。

代码:
```Java
class Solution {
    public int[] findRightInterval(int[][] its) {
        int n = its.length;
        int[][] ss = new int[n][2], es = new int[n][2];
        for (int i = 0; i < n; i++) {
            ss[i] = new int[]{its[i][0], i};
            es[i] = new int[]{its[i][1], i};
        }
        Arrays.sort(ss, (a,b)->a[0]-b[0]);
        Arrays.sort(es, (a,b)->a[0]-b[0]);
        int[] ans = new int[n];
        for (int i = 0, j = 0; i < n; i++) {
            int[] cur = es[i];
            int loc = cur[0], idx = cur[1];
            while (j < n && ss[j][0] < loc) j++;
            ans[idx] = j == n ? -1 : ss[j][1];
        }
        return ans;
    }
}
```
* 时间复杂度:排序复杂度为 $O(n\log{n})$;双指针构造答案的复杂度为 $O(n)$。整体复杂度为 $O(n\log{n})$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


Question:
给定一个二叉树的根节点 `root` ,和一个整数 `targetSum`,求该二叉树里节点值之和等于 `targetSum` 的 路径 的数目。

路径 不需要从根节点开始,也不需要在叶子节点结束,但是路径方向必须是向下的(只能从父节点到子节点)。

示例 1:

```
输入:root = [10,5,-3,3,2,null,11,3,-2,null,1], targetSum = 8

输出:3

解释:和等于 8 的路径有 3 条,如图所示。
```
示例 2:
```
输入:root = [5,4,8,11,null,13,4,7,2,null,null,5,1], targetSum = 22

输出:3
```

提示:
* 二叉树的节点个数的范围是 [0,1000]
* $-10^9 <= Node.val <= 10^9$
* $-1000 <= targetSum <= 1000$

Solution:
### 树的遍历 + DFS

一个朴素的做法是搜索以每个节点为根的(往下的)所有路径,并对路径总和为 $targetSum$ 的路径进行累加统计。

使用 `dfs1` 来搜索所有节点,复杂度为 $O(n)$;在 `dfs1` 中对于每个当前节点,使用 `dfs2` 搜索以其为根的所有(往下的)路径,同时累加路径总和为 $targetSum$ 的所有路径,复杂度为 $O(n)$。

整体复杂度为 $O(n^2)$,数据范围为 $10^3$,可以过。

代码:
```Java
class Solution {
    int ans, t;
    public int pathSum(TreeNode root, int _t) {
        t = _t;
        dfs1(root);
        return ans;
    }
    void dfs1(TreeNode root) {
        if (root == null) return;
        dfs2(root, root.val);
        dfs1(root.left);
        dfs1(root.right);
    }
    void dfs2(TreeNode root, long val) {
        if (val == t) ans++;
        if (root.left != null) dfs2(root.left, val + root.left.val);
        if (root.right != null) dfs2(root.right, val + root.right.val);
    }
}
```
* 时间复杂度:$O(n^2)$
* 空间复杂度:忽略递归带来的额外空间开销,复杂度为 $O(1)$

---

### 树的遍历 + 前缀和

在「解法一」中,我们统计的是以每个节点为根的(往下的)所有路径,也就是说统计的是以每个节点为「路径开头」的所有合法路径。

**本题的一个优化切入点为「路径只能往下」,因此如果我们转换一下,统计以每个节点为「路径结尾」的合法数量的话,配合原本就是「从上往下」进行的数的遍历(最完整的路径必然是从原始根节点到当前节点的唯一路径),相当于只需要在完整路径中找到有多少个节点到当前节点的路径总和为 $targetSum$。**

**于是这个树上问题彻底转换一维问题:求解从原始起点(根节点)到当前节点 $b$ 的路径中,有多少节点 $a$ 满足 $sum[a...b] = targetSum$,由于从原始起点(根节点)到当前节点的路径唯一,因此这其实是一个「一维前缀和」问题。**

具体的,我们可以在进行树的遍历时,记录下从原始根节点 $root$ 到当前节点 $cur$ 路径中,从 $root$ 到任意中间节点 $x$ 的路径总和,配合哈希表,快速找到满足以 $cur$ 为「路径结尾」的、使得路径总和为 $targetSum$ 的目标「路径起点」有多少个。

> 一些细节:由于我们只能统计往下的路径,但是树的遍历会同时搜索两个方向的子树。因此我们应当在搜索完以某个节点为根的左右子树之后,应当回溯地将路径总和从哈希表中删除,防止统计到跨越两个方向的路径。

代码:
```Java
class Solution {
    Map<Long, Integer> map = new HashMap<>();
    int ans, t;
    public int pathSum(TreeNode root, int _t) {
        if (root == null) return 0;
        t = _t;
        map.put(0L, 1);
        dfs(root, root.val);
        return ans;
    }
    void dfs(TreeNode root, long val) {
        if (map.containsKey(val - t)) ans += map.get(val - t);
        map.put(val, map.getOrDefault(val, 0) + 1);
        if (root.left != null) dfs(root.left, val + root.left.val);
        if (root.right != null) dfs(root.right, val + root.right.val);
        map.put(val, map.getOrDefault(val, 0) - 1);
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


LeetCode/431-440/438. 找到字符串中所有字母异位词（中等）.md

438. 找到字符串中所有字母异位词

Question:
给定两个字符串 `s` 和 `p`,找到 `s` 中所有 `p` 的 异位词 的子串,返回这些子串的起始索引。不考虑答案输出的顺序。

**异位词** 指由相同字母重排列形成的字符串(包括相同的字符串)。

示例 1:
```
输入: s = "cbaebabacd", p = "abc"

输出: [0,6]

解释:
起始索引等于 0 的子串是 "cba", 它是 "abc" 的异位词。
起始索引等于 6 的子串是 "bac", 它是 "abc" 的异位词。
```
 示例 2:
```
输入: s = "abab", p = "ab"

输出: [0,1,2]

解释:
起始索引等于 0 的子串是 "ab", 它是 "ab" 的异位词。
起始索引等于 1 的子串是 "ba", 它是 "ab" 的异位词。
起始索引等于 2 的子串是 "ab", 它是 "ab" 的异位词。
```

提示:
* $1 <= s.length, p.length <= 3 * 10^4$
* `s` 和 `p` 仅包含小写字母

Solution:
### 双指针(滑动窗口)

这是一道使用双指针实现滑动窗口的裸题。

具体的,我们可以先创建一个大小为 $26$ 的数组 $c2$ 来统计字符串 `p` 的词频,另外一个同等大小的数组 $c1$ 用来统计「滑动窗口」内的 `s` 的子串词频。

当两个数组所统计词频相等,说明找到了一个异位组,将窗口的左端点加入答案。

代码:
```Java
class Solution {
    public List<Integer> findAnagrams(String s, String p) {
        List<Integer> ans = new ArrayList<>();
        int n = s.length(), m = p.length();
        int[] c1 = new int[26], c2 = new int[26];
        for (int i = 0; i < m; i++) c2[p.charAt(i) - 'a']++;
        for (int l = 0, r = 0; r < n; r++) {
            c1[s.charAt(r) - 'a']++;
            if (r - l + 1 > m) c1[s.charAt(l++) - 'a']--;
            if (check(c1, c2)) ans.add(l);
        }
        return ans;
    }
    boolean check(int[] c1, int[] c2) {
        for (int i = 0; i < 26; i++) {
            if (c1[i] != c2[i]) return false;
        }
        return true;
    }
}
```
* 时间复杂度:令 `s` 和 `p` 的长度分别为 $n$ 和 $m$,$C = 26$ 为字符集大小。统计 `p` 词频(构建 $c2$ 数组)的复杂度为 $O(m)$;使用双指针检查 `s` 串的复杂度为 $O(C * n)$。整体复杂度为 $O(C*n + m)$
* 空间复杂度:$O(C)$

---

### 优化 check

解法一中每次对滑动窗口的检查都不可避免需要检查两个词频数组,复杂度为 $O(C)$。

事实上,我们只关心两个数组是否完全一致,因而我们能够只维护一个词频数组 $cnt$ 来实现。

起始处理 `p` 串时,只对 $cnt$ 进行词频字符自增操作。当处理 `s` 的滑动窗口子串时,尝试对 $cnt$ 中的词频进行「抵消/恢复」操作:
* 当滑动窗口的右端点右移时(增加字符),对 $cnt$ 执行右端点字符的「抵消」操作;
* 当滑动窗口的左端点右移时(减少字符),对 $cnt$ 执行左端点字符的「恢复」操作。

同时,使用变量 $a$ 统计 `p` 中不同字符的数量,使用变量 $b$ 统计滑动窗口(子串)内有多少个字符词频与 $p$ 相等。

当滑动窗口移动( 执行「抵消/恢复」)时,如果「抵消」后该字符词频为 $0$,说明本次右端点右移,多产生了一位词频相同的字符;如果「恢复」后该字符词频数量为 $1$,说明少了一个为词频相同的字符。当且仅当 $a = b$ 时,我们找到了一个新的异位组。

代码:
```Java
class Solution {
    public List<Integer> findAnagrams(String s, String p) {
        List<Integer> ans = new ArrayList<>();
        int n = s.length(), m = p.length();
        int[] cnt = new int[26];
        for (int i = 0; i < m; i++) cnt[p.charAt(i) - 'a']++;
        int a = 0;
        for (int i = 0; i < 26; i++) if (cnt[i] != 0) a++;
        for (int l = 0, r = 0, b = 0; r < n; r++) {
            // 往窗口增加字符,进行词频的抵消操作,如果抵消后词频为 0,说明有一个新的字符词频与 p 完全相等
            if (--cnt[s.charAt(r) - 'a'] == 0) b++;
            // 若窗口长度超过规定,将窗口左端点右移,执行词频恢复操作,如果恢复后词频为 1(恢复前为 0),说明少了一个词频与 p 完全性相等的字符
            if (r - l + 1 > m && ++cnt[s.charAt(l++) - 'a'] == 1) b--;
            if (b == a) ans.add(l);
        }
        return ans;
    }
}
```
* 时间复杂度:令 `s` 和 `p` 的长度分别为 $n$ 和 $m$,$C = 26$ 为字符集大小。构造 $cnt$ 的复杂度为 $O(m)$,统计 $p$ 中不同的字符数量为 $O(C)$,对 `s` 进行滑动窗口扫描得出答案的复杂度为 $O(n)$。整体复杂度为 $O(m + C + n)$
* 空间复杂度:$O(C)$

### 最后

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

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

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

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


440. 字典序的第K小数字

Question:
给定整数 $n$ 和 $k$,返回  $[1, n]$ 中字典序第 $k$ 小的数字。

示例 1:
```
输入: n = 13, k = 2

输出: 10

解释: 字典序的排列是 [1, 10, 11, 12, 13, 2, 3, 4, 5, 6, 7, 8, 9],所以第二小的数字是 10。
```
示例 2:
```
输入: n = 1, k = 1

输出: 1
```

提示:
* $1 <= k <= n <= 10^9$

Solution:
### 计数模拟

寻找字典序第 $k$ 小的数。

我们可以将该过程分两步操作 :「确定前缀」和「从以某个前缀开始找目标值」。

假定我们存在某个函数 `int getCnt(int x, int limit)`,该函数实现了统计范围 $[1, limit]$ 内以 $x$ 为前缀的数的个数。

有了该函数之后,我们可以从最小的前缀 $1$ 开始枚举,假设当前枚举到前缀 $x$,根据 $cnt = getCnt(x, n)$ 与 $k$ 的大小关系进行分情况讨论:

* $cnt < k$:说明所有以 $x$ 为前缀的数组均可跳过,此时让 $x$ 自增,$k$ 减去 $cnt$。含义为从下一个「数值比 $x$ 大」的前缀中找目标值;
* $cnt \geqslant k$:说明目标值前缀必然为 $x$,此时我们需要在以 $x$ 为前缀的前提下找目标值。此时让 $x$ 乘 $10$,$k$ 减 $1$(代表跳过了 $x$ 本身)。含义为从下一个「字典序比 $x$ 大」的前缀中找目标值。

当 $k = 1$ 时,当前前缀 $x$ 即是答案(含义为以 $x$ 为前缀的所有数中,最小的数,也就是 $x$ 本身)。

然后重点看看 `int getCnt(int x, int limit)` 函数如何实现。

为了方便,记 $x$ 的位数为 $n$,$limit$ 位数为 $m$。

根据 `getCnt` 的函数定义,在范围 $[1, limit]$ 内,以 $x$ 为前缀的数值数量等于下面所有情况的数量之和:

* 位数为 $n$ 的数:仅有 $x$ 本身,共 $1$ 个;
* 位数为 $n + 1 < m$ 的数,有 `x0` 到 `x9`,共 $10$ 个;
* 位数为 $n + 2 < m$ 的数,有 `x00` 到 `x99`,共 $100$ 个;
* ...
* 位数为 $m$ 的数,此时根据「$limit$ 长度与 $x$ 等同的前缀 $u$」和「$x$」的大小关系,进一步分情况讨论(举个 🌰,当 $limit = 12456$,$x$ 为 $123$ 时,$u = 124$,两者位数相差 $k = 2$ 位):
    * $u < x$:此时所有位数为 $m$ 的数均大于 $limit$,合法个数为 $0$;
    * $u == x$:此时所有位数为 $m$ 的数中部分满足 $limit$ 限制,合法个数为 $limit - x * 10^k + 1$ 个(只有 $[x0...0, limit]$ 为合法数);
    * $u > x$:此时所有位数为 $m$ 的数均小于 $limit$,合法个数为 $10^k$。

Java 代码:
```Java
class Solution {
    public int findKthNumber(int n, int k) {
        int ans = 1;
        while (k > 1) {
            int cnt = getCnt(ans, n);
            if (cnt < k) {
                k -= cnt; ans++;
            } else {
                k--; ans *= 10;
            }
        }
        return ans;
    }
    int getCnt(int x, int limit) {
        String a = String.valueOf(x), b = String.valueOf(limit);
        int n = a.length(), m = b.length(), k = m - n;
        int ans = 0, u = Integer.parseInt(b.substring(0, n));
        for (int i = 0; i < k; i++) ans += Math.pow(10, i);
        if (u > x) ans += Math.pow(10, k);
        else if (u == x) ans += limit - x * Math.pow(10, k) + 1;
        return ans;
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    int findKthNumber(int n, int k) {
        int ans = 1;
        while (k > 1) {
            int cnt = getCnt(ans, n);
            if (cnt < k) {
                k -= cnt; ans++;
            } else {
                k--; ans *= 10;
            }
        }
        return ans;
    }
    int getCnt(int x, int limit) {
        string a = to_string(x), b = to_string(limit);
        int n = a.length(), m = b.length(), k = m - n;
        int ans = 0, u = stoi(b.substr(0, n));
        for (int i = 0; i < k; i++) ans += pow(10, i);
        if (u > x) ans += pow(10, k);
        else if (u == x) ans += limit - x * pow(10, k) + 1;
        return ans;
    }
};
```
Python 代码:
```Python
class Solution:
    def findKthNumber(self, n: int, k: int) -> int:
        def get_cnt(x, limit):
            a, b = str(x), str(limit)
            n, m = len(a), len(b)
            k = m - n
            ans = 0
            u = int(b[:n])
            for i in range(k):
                ans += int(pow(10, i))
            if u > x:
                ans += int(pow(10, k))
            elif u == x:
                ans += limit - x * int(pow(10, k)) + 1
            return ans

        ans = 1
        while k > 1:
            cnt = get_cnt(ans, n)
            if cnt < k:
                k, ans = k - cnt, ans + 1
            else:
                k, ans = k - 1, ans * 10
        return ans
```
TypeScript 代码:
```TypeScript
function findKthNumber(n: number, k: number): number {
    const getCnt = function(x: number, limit: number): number {
        const a = String(x), b = String(limit);
        const n = a.length, m = b.length, k = m - n;
        let ans = 0;
        let u = parseInt(b.substring(0, n));
        for (let i = 0; i < k; i++) ans += Math.pow(10, i);
        if (u > x) ans += Math.pow(10, k);
        else if (u == x) ans += limit - x * Math.pow(10, k) + 1;
        return ans;
    };
    let ans = 1;
    while (k > 1) {
        let cnt = getCnt(ans, n);
        if (cnt < k) {
            k -= cnt; ans++;
        } else {
            k--; ans *= 10;
        }
    }
    return ans;
};
```
* 时间复杂度:枚举前缀以及 `getCnt` 操作均与位数相关,复杂度均为 $O(\log{n})$。整体复杂度为 $O(\log{^2}{n})$
* 空间复杂度:忽略子串生成复杂度为 $O(1)$,否则为 $O(\log{^2}{n})$

### 最后

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

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

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

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


Question:
你总共有 `n` 枚硬币,并计划将它们按阶梯状排列。

对于一个由 `k` 行组成的阶梯,其第 `i` 行必须正好有 i 枚硬币。阶梯的最后一行**可能**是不完整的。

给你一个数字 `n` ,计算并返回可形成**完整阶梯行**的总行数。

示例 1:

```
输入:n = 5

输出:2

解释:因为第三行不完整,所以返回 2 。
```
示例 2:

```
输入:n = 8

输出:3

解释:因为第四行不完整,所以返回 3 。
```

提示:
* $1 <= n <= 2^{31} - 1$

Solution:
### 数学

假设 $n$ 个硬币最多凑出 $x$ 行,根据等差数列求和公式 $S_n = n * a_1 + \frac{n * (n - 1)}{2} * d$(首项和公差均为 $1$),可得 $n$ 和 $x$ 的关系:

$$
\frac{x * (x + 1)}{2} <= n
$$

问题为求满足上式前提下,$x$ 的最大值为多少。

根据 [求解一元二次方程](https://baike.baidu.com/item/%E4%B8%80%E5%85%83%E4%BA%8C%E6%AC%A1%E6%96%B9%E7%A8%8B) 求根公式,可得:

$$
x <= \left \lfloor \frac{- 1 + \sqrt{1 + 8 * n}}{2} \right \rfloor
$$

代码:
```Java
class Solution {
    public int arrangeCoins(int n) {
        return (int)((Math.sqrt(1 + 8.0 * n) - 1) / 2);
    }
}
```
* 时间复杂度:$O(1)$
* 空间复杂度:$O(1)$

---

### 二分

对于解方程不熟悉的同学,也可以使用「二分」来做。

对于 $n$ 个硬币而言,最多凑成的行数不会超过 $n$ 行,当且仅当 $n = 1$ 的时候可以取到。

假设最大能够凑成的行数为 $x$,那么以 $x$ 为分割点的数轴上具有二段性,使用 $n$ 个硬币如果能够凑成 $x$ 行,那么必然也能够凑成少于等于 $x$ 行的阶梯硬币,必然不能凑成大于 $x$ 行的阶梯硬币。

对于 $x$ 行阶梯硬币所需要的硬币数量,还是可以利用等差数列求和公式直接算出。

代码:
```Java
class Solution {
    public int arrangeCoins(int n) {
        long l = 1, r = n;
        while (l < r) {
            long mid = l + r + 1 >> 1;
            if (mid * (mid + 1) / 2 <= n) l = mid;
            else r = mid - 1;
        }
        return (int)r;
    }
}
```
* 时间复杂度:$O(\log{n})$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


442. 数组中重复的数据

Question:
给你一个长度为 $n$ 的整数数组 `nums`,其中 `nums` 的所有整数都在范围 $[1, n]$ 内,且每个整数出现 一次 或 两次 。请你找出所有出现 两次 的整数,并以数组形式返回。

你必须设计并实现一个时间复杂度为 $O(n)$ 且仅使用常量额外空间的算法解决此问题。

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

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

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

输出:[]
```

提示:
* $n == nums.length$
* $1 <= n <= 10^5$
* $1 <= nums[i] <= n$
* `nums` 中的每个元素出现 一次 或 两次

Solution:
### 原地哈希

给定数组长度为 $n$,且所有数范围在 $[1, n]$,找出出现次数超过一次的所有数字。

利用值域与数字下标空间大小的等同关系,我们可以构造一种对应“关系”,使得每个数出现在它应该出现的位置:对于值为 $k$ 的数字,我们将其应该出现在的位置定为 $k - 1$。

基于此,我们从前往后遍历 $nums$,并尝试将当前处理到的 $nums[i]$ 放到目标位置 $nums[i] - 1$ 处。如果一个数在尝试移动到它应该出现的位置时,发现 $nums[nums[i] - 1] = nums[i] (nums[i] - 1 \neq i)$,则说明该数字出现了超过一次。此时我们将 $nums[i]$ 加入答案,由于此时没有发生交换,而 $nums[i]$ 占用的仍是其他数字的目标位置,为了防止 $nums[i]$ 与其他数字发生交换后,再次被检验并重复加入答案,我们将 $nums[i]$ 置为负数,并在遍历过程中跳过负数。

代码:
```Java
class Solution {
    public List<Integer> findDuplicates(int[] nums) {
        List<Integer> ans = new ArrayList<>();
        int n = nums.length;
        for (int i = 0; i < n; i++) {
            int t = nums[i];
            if (t < 0 || t - 1 == i) continue;
            if (nums[t - 1] == t) {
                ans.add(t);
                nums[i] *= -1;
            } else {
                int c = nums[t - 1];
                nums[t - 1] = t;
                nums[i--] = c;
            }
        }
        return ans;
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


Question:
给你一个字符数组 chars ,请使用下述算法压缩:

从一个空字符串 s 开始。对于 chars 中的每组 连续重复字符 :

* 如果这一组长度为 1 ,则将字符追加到 s 中。
* 否则,需要向 s 追加字符,后跟这一组的长度。

压缩后得到的字符串 s 不应该直接返回 ,需要转储到字符数组 chars 中。需要注意的是,如果组长度为 10 或 10 以上,则在 chars 数组中会被拆分为多个字符。

请在** 修改完输入数组后**,返回该数组的新长度。

你必须设计并实现一个只使用常量额外空间的算法来解决此问题。

示例 1:
```
输入:chars = ["a","a","b","b","c","c","c"]

输出:返回 6 ,输入数组的前 6 个字符应该是:["a","2","b","2","c","3"]

解释:
"aa" 被 "a2" 替代。"bb" 被 "b2" 替代。"ccc" 被 "c3" 替代。
```
示例 2:
```
输入:chars = ["a"]

输出:返回 1 ,输入数组的前 1 个字符应该是:["a"]

解释:
没有任何字符串被替代。
```
示例 3:
```
输入:chars = ["a","b","b","b","b","b","b","b","b","b","b","b","b"]

输出:返回 4 ,输入数组的前 4 个字符应该是:["a","b","1","2"]。

解释:
由于字符 "a" 不重复,所以不会被压缩。"bbbbbbbbbbbb" 被 “b12” 替代。
注意每个数字在数组中都有它自己的位置。
```

提示:
* 1 <= chars.length <= 2000
* chars[i] 可以是小写英文字母、大写英文字母、数字或符号

Solution:
### 双指针

令输入数组 `cs` 长度为 $n$。

使用两个指针 `i` 和 `j` 分别指向「当前处理到的位置」和「答案待插入的位置」:

1. `i` 指针一直往后处理,每次找到字符相同的连续一段 $[i, idx)$,令长度为 $cnt$;
2. 将当前字符插入到答案,并让 `j` 指针后移:`cs[j++] = cs[i]`;
3. 检查长度 $cnt$ 是否大于 $1$,如果大于 $1$,需要将数字拆分存储。由于简单的实现中,我们只能从个位开始处理 $cnt$,因此需要使用 `start` 和 `end` 记录下存储数字的部分,再处理完 $cnt$ 后,将 $[start, end)$ 部分进行翻转,并更新 `j` 指针;
4. 更新 `i` 为 `idx`,代表循环处理下一字符。

代码:
```Java
class Solution {
    public int compress(char[] cs) {
        int n = cs.length;
        int i = 0, j = 0;
        while (i < n) {
            int idx = i;
            while (idx < n && cs[idx] == cs[i]) idx++;
            int cnt = idx - i;
            cs[j++] = cs[i];
            if (cnt > 1) {
                int start = j, end = start;
                while (cnt != 0) {
                    cs[end++] = (char)((cnt % 10) + '0');
                    cnt /= 10;
                }
                reverse(cs, start, end - 1);
                j = end;
            }
            i = idx;
        }
        return j;
    }
    void reverse(char[] cs, int start, int end) {
        while (start < end) {
            char t = cs[start];
            cs[start] = cs[end];
            cs[end] = t;
            start++; end--;
        }
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


LeetCode/441-450/446. 等差数列划分 II - 子序列（困难）.md

446. 等差数列划分 II - 子序列

Question:
给你一个整数数组 nums ,返回 nums 中所有 等差子序列 的数目。

如果一个序列中 至少有三个元素 ,并且任意两个相邻元素之差相同,则称该序列为等差序列。

* 例如,[1, 3, 5, 7, 9]、[7, 7, 7, 7] 和 [3, -1, -5, -9] 都是等差序列。
* 再例如,[1, 1, 2, 5, 7] 不是等差序列。

数组中的子序列是从数组中删除一些元素(也可能不删除)得到的一个序列。

* 例如,[2,5,10] 是 [1,2,1,2,4,1,5,10] 的一个子序列。

题目数据保证答案是一个 32-bit 整数。

示例 1:
```
输入:nums = [2,4,6,8,10]

输出:7

解释:所有的等差子序列为:
[2,4,6]
[4,6,8]
[6,8,10]
[2,4,6,8]
[4,6,8,10]
[2,4,6,8,10]
[2,6,10]
```
示例 2:
```
输入:nums = [7,7,7,7,7]

输出:16

解释:数组中的任意子序列都是等差子序列。
```

提示:
* 1  <= nums.length <= 1000
* -$2^{31}$ <= nums[i] <= $2^{31}$ - 1

Solution:
### 基本分析

从题目描述来看,我们可以确定这是一个「序列 DP」问题,通常「序列 DP」需要 $O(n^2)$ 的时间复杂度,而某些具有特殊性质的「序列 DP」问题,例如 LIS 问题,能够配合贪心思路 + 二分做到 $O(n\log{n})$ 复杂度。再看一眼数据范围为 $10^3$,基本可以确定这是一道复杂度为 $O(n^2)$ 的「序列 DP」问题。

---

### 动态规划 + 容斥原理

**既然分析出是序列 DP 问题,我们可以先猜想一个基本的状态定义,看是否能够「不重不漏」的将状态通过转移计算出来。如果不行,我们再考虑引入更多的维度来进行求解。**

先从最朴素的猜想出发,定义 $f[i]$ 为考虑下标不超过 $i$ 的所有数,并且以 $nums[i]$ 为结尾的等差序列的个数。

不失一般性的 $f[i]$ 该如何转移,不难发现我们需要枚举 $[0, i - 1]$ 范围内的所有数,假设当前我们枚举到 $[0, i - 1]$ 中的位置 $j$,我们可以直接算出两个位置的差值 $d = nums[i] - nums[j]$,但我们不知道 $f[j]$ 存储的子序列数量是差值为多少的。

同时,根据题目我们要求的是所有的等差序列的个数,而不是求差值为某个具体值 $x$ 的等差序列的个数。换句话说,我们需要记录下所有差值的子序列个数,并求和才是答案。

**因此我们的 $f[i]$ 不能是一个数,而应该是一个「集合」,该集合记录下了所有以 $nums[i]$ 为结尾,差值为所有情况的子序列的个数。**

我们可以设置 $f[i] = g$,其中 $g$ 为一个「集合」数据结构,我们期望在 $O(1)$ 的复杂度内查的某个差值 $d$ 的子序列个数是多少。

**这样 $f[i][j]$ 就代表了以 $nums[i]$ 为结尾,并且差值为 $j$ 的子序列个数是多少。**

当我们多引入一维进行这样的状态定义后,我们再分析一下能否「不重不漏」的通过转移计算出所有的动规值。

不失一般性的考虑 $f[i][j]$ 该如何转移,显然序列 DP 问题我们还是要枚举区间 $[0, i - 1]$ 的所有数。

**和其他的「序列 DP」问题一样,枚举当前位置前面的所有位置的目的,是为了找到当前位置的数,能够接在哪一个位置的后面,形成序列。**

**对于本题,枚举区间 $[0, i - 1]$ 的所有数的含义是:枚举以 $nums[i]$ 为子序列结尾时,它的前一个值是什么,也就是 $nums[i]$ 接在哪个数的后面,形成等差子序列。**

这样必然是可以「不重不漏」的处理到所有以 $nums[i]$ 为子序列结尾的情况的。

至于具体的状态转移方程,我们令差值 $d = nums[i] - nums[j]$,显然有(先不考虑长度至少为 $3$ 的限制):

$$
f[i][d] = \sum_{j = 0}^{i - 1} (f[j][d] + 1)
$$

含义为:**在原本以 $nums[j]$ 为结尾的,且差值为 $d$ 的子序列的基础上接上 $nums[i]$,再加上新的子序列 $(nums[j], nums[i])$,共 $f[j][d] + 1$ 个子序列。**

**最后对所有的哈希表的「值」对进行累加计数,就是以任意位置为结尾,长度大于 $1$ 的等差子序列的数量 $ans$。**

这时候再看一眼数据范围 $-2^{31} <= nums[i] <= 2^{31}-1$,如果从数据范围出发,使用「数组」充当集合的话,我们需要将数组开得很大,必然会爆内存。

但同时有 $1  <= nums.length <= 1000$,也就是说「最小差值」和「最大差值」之间可能相差很大,但是差值的数量是有限的,不会超过 $n^2$ 个。

为了不引入复杂的「离散化」操作,我们可以直接使用「哈希表」来充当「集合」。

每一个 $f[i]$ 为一个哈希表,哈希表的以 `{d:cnt}` 的形式进行存储,`d` 为子序列差值,`cnt` 为子序列数量。

虽然相比使用数组,哈希表常数更大,但是经过上述分析,我们的复杂度为 $O(n^2)$,计算量为 $10^6$,距离计算量上界 $10^7$ 还保有一段距离,因此直接使用哈希表十分安全。

到这里,我们解决了不考虑「长度为至少为 $3$」限制的原问题。

那么需要考虑「长度为至少为 $3$」限制怎么办?

**显然,我们计算的 $ans$ 为统计所有的「长度大于 $1$」的等差子序列数量,由于长度必然为正整数,也就是统计的是「长度大于等于 $2$」的等差子序列的数量。**

**因此,如果我们能够求出长度为 $2$ 的子序列的个数的话,从 $ans$ 中减去,得到的就是「长度为至少为 $3$」子序列的数量。**

长度为 $2$ 的等差子序列,由于没有第三个数的差值限制,因此任意的数对 $(j, i)$ 都是一个合法的长度为 $2$ 的等差子序列。

而求长度为 $n$ 的数组的所有数对,其实就是求 **首项为 $0$,末项为 $n - 1$,公差为 $1$,长度为 $n$ 的等差数列之和**,直接使用「等差数列求和」公式求解即可。

代码:
```Java
class Solution {
    public int numberOfArithmeticSlices(int[] nums) {
        int n = nums.length;
        // 每个 f[i] 均为哈希表,哈希表键值对为 {d : cnt}
        // d : 子序列差值
        // cnt : 以 nums[i] 为结尾,且差值为 d 的子序列数量
        List<Map<Long, Integer>> f = new ArrayList<>();
        for (int i = 0; i < n; i++) {
            Map<Long, Integer> cur = new HashMap<>();
            for (int j = 0; j < i; j++) {
                Long d = nums[i] * 1L - nums[j];
                Map<Long, Integer> prev = f.get(j);
                int cnt = cur.getOrDefault(d, 0);
                cnt += prev.getOrDefault(d, 0);
                cnt ++;
                cur.put(d, cnt);
            }
            f.add(cur);
        }
        int ans = 0;
        for (int i = 0; i < n; i++) {
            Map<Long, Integer> cur = f.get(i);
            for (Long key : cur.keySet()) ans += cur.get(key);
        }
        int a1 = 0, an = n - 1;
        int cnt = (a1 + an) * n / 2;
        return ans - cnt;
    }
}
```
* 时间复杂度:DP 过程的复杂度为 $O(n^2)$,遍历所有的哈希表的复杂度上界不会超过 $O(n^2)$。整体复杂度为 $O(n^2)$
* 空间复杂度:所有哈希表存储的复杂度上界不会超过 $O(n^2)$

### 最后

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

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

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

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


Question:
给定平面上 `n` 对 互不相同 的点 `points`,其中 `points[i] = [xi, yi]` 。回旋镖 是由点 `(i, j, k)` 表示的元组 ,其中 `i` 和 `j` 之间的距离和 `i` 和 `k` 之间的距离相等(需要考虑元组的顺序)。

返回平面上所有回旋镖的数量。

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

输出:2

解释:两个回旋镖为 [[1,0],[0,0],[2,0]] 和 [[1,0],[2,0],[0,0]]
```
示例 2:
```
输入:points = [[1,1],[2,2],[3,3]]

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

输出:0
```

提示:
* $n == points.length$
* $1 <= n <= 500$
* $points[i].length == 2$
* -$10^4 <= x_i, y_i <= 10^4$
* 所有点都 互不相同

Solution:
### 哈希表

数据范围为 $500$,三层循环的朴素做法显然会 TLE。

对于每个回旋镖三元组而言,本质上我们在统计给定 $i$ 的情况下,与 $i$ 距离相等的 $(j, k)$ 组合个数为多少。

我们可以使用哈希表进行预处理,在统计以 $i$ 为三元组第一位的回旋镖个数前,先计算出 $i$ 和其余点的距离,并以 `{ 距离 : 个数 }` 的形式进行存储,然后分别对所有的距离进行累加计数。

在计算距离时为了避免使用 `sqrt`,我们直接使用 $x^2 + y^2$ 来代指两点间的距离。

代码:
```Java
class Solution {
    public int numberOfBoomerangs(int[][] points) {
        int n = points.length;
        int ans = 0;
        for (int i = 0; i < n; i++) {
            Map<Integer, Integer> map = new HashMap<>();
            for (int j = 0; j < n; j++) {
                if (i == j) continue;
                int x = points[i][0] - points[j][0], y = points[i][1] - points[j][1];
                int dist = x * x + y * y;
                map.put(dist, map.getOrDefault(dist, 0) + 1);
            }
            for (int dist : map.keySet()) {
                int cnt = map.get(dist);
                ans += cnt * (cnt - 1);
            }
        }
        return ans;
    }
}
```
* 时间复杂度:$O(n^2)$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


LeetCode/441-450/448. 找到所有数组中消失的数字（简单）.md

448. 找到所有数组中消失的数字

Question:
给定一个范围在  $1 ≤ a[i] ≤ n$ ( $n$ = 数组大小 ) 的 整型数组,数组中的元素一些出现了两次,另一些只出现一次。

找到所有在 $[1, n]$ 范围之间没有出现在数组中的数字。

您能在不使用额外空间且时间复杂度为 $O(n)$ 的情况下完成这个任务吗? 你可以假定返回的数组不算在额外空间内。

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

输出:
[5,6]
```

Solution:
### 桶排序(原地哈希)

题目规定了 $1 ≤ a[i] ≤ n$,因此我们可以使用「桶排序」的思路,将每个数放在其应该出现的位置上。

基本思路为:

按照桶排序思路进行预处理:保证 $1$ 出现在 $nums[0]$ 的位置上,$2$ 出现在 $nums[1]$ 的位置上,...,$n$ 出现在 $nums[n - 1]$ 的位置上。不在 $[1, n]$ 范围内的数不用动。

例如样例中 $[4,3,2,7,8,2,3,1]$ 将会被预处理成 $[1,2,3,4,3,2,7,8]$。

遍历 $nums$,将不符合 `nums[i] != i + 1` 的数字加入结果集 ~

代码:
```Java
class Solution {
    public List<Integer> findDisappearedNumbers(int[] nums) {
        int n = nums.length;
        for (int i = 0; i < n; i++) {
            while (nums[i] != i + 1 && nums[nums[i] - 1] != nums[i]) swap(nums, i, nums[i] - 1);
        }
        List<Integer> ans = new ArrayList<>();
        for (int i = 0; i < n; i++) {
            if (nums[i] != i + 1) ans.add(i + 1);
        }
        return ans;
    }
    void swap(int[] nums, int a, int b) {
        int c = nums[a];
        nums[a] = nums[b];
        nums[b] = c;
    }
}
```
* 时间复杂度:每个数字最多挪动一次。复杂度为 $O(n)$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


LeetCode/441-450/449. 序列化和反序列化二叉搜索树（中等）.md

449. 序列化和反序列化二叉搜索树

Question:
序列化是将数据结构或对象转换为一系列位的过程,以便它可以存储在文件或内存缓冲区中,或通过网络连接链路传输,以便稍后在同一个或另一个计算机环境中重建。

设计一个算法来序列化和反序列化 二叉搜索树 。 对序列化/反序列化算法的工作方式没有限制。 您只需确保二叉搜索树可以序列化为字符串,并且可以将该字符串反序列化为最初的二叉搜索树。

编码的字符串应尽可能紧凑。

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

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

输出:[]
```

提示:
* 树中节点数范围是 $[0, 10^4]$
* $0 <= Node.val <= 10^4$
* 题目数据 保证 输入的树是一棵二叉搜索树。

Solution:
### BST 特性(前序遍历)

实现上,我们可以忽略「BST」这一条件,使用「BFS」或者「直接充当满二叉树」来序列化和反序列化。

但由于点的数量是 $1e4$,最坏情况下是当 BST 成链时,会有较大的空间浪费。

因此,一种较为紧凑的序列化/反序列化的方式是利用「前序遍历 + BST 特性」:

* 序列化:对 BST 进行「前序遍历」,并跳过空节点,节点值通过 `,` 进行分割,假设最终序列化出来的字符串是 `s`。
    之所以使用「前序遍历」是为了方便反序列化:首先对于某个子树而言,其必然是连续存储,也就是必然能够使用 $s[l,r]$ 所表示处理,同时首位元素必然是该子树的头结点;

* 反序列化:将 `s` 根据分隔符 `,` 进行分割,假设分割后数组 `ss` 长度为 $n$,那么 $ss[0, n - 1]$ 代表完整的子树,我们可以利用「二叉树」特性递归构建,设计递归函数 `TreeNode dfs2(int l, int r, Sring[] ss)`,其含义为利用 $ss[l, r]$ 连续段构造二叉树,并返回头结点:
    1. $ss[l]$ 为头结点,其值为 $t$,在 $[l, r]$ 范围内找到第一个比 $t$ 大的位置 $j$:
    2. $ss[l]$ 的左子树的所有值均比 $t$ 小,且在 `s` 中连续存储,我们可以递归处理 $[l + 1, j - 1]$ 构建左子树;
    3. $ss[l]$ 的右子树的所有值均比 $t$ 大,且在 `s` 中连续存储,我们可以递归处理 $[j, r]$ 构建右子树。

代码:
```Java
public class Codec {
    public String serialize(TreeNode root) {
        if (root == null) return null;
        List<String> list = new ArrayList<>();
        dfs1(root, list);
        int n = list.size();
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < n; i++) {
            sb.append(list.get(i));
            if (i != n - 1) sb.append(",");
        }
        return sb.toString();
    }
    void dfs1(TreeNode root, List<String> list) {
        if (root == null) return ;
        list.add(String.valueOf(root.val));
        dfs1(root.left, list);
        dfs1(root.right, list);
    }
    public TreeNode deserialize(String s) {
        if (s == null) return null;
        String[] ss = s.split(",");
        return dfs2(0, ss.length - 1, ss);
    }
    TreeNode dfs2(int l, int r, String[] ss) {
        if (l > r) return null;
        int j = l + 1, t = Integer.parseInt(ss[l]);
        TreeNode ans = new TreeNode(t);
        while (j <= r && Integer.parseInt(ss[j]) <= t) j++;
        ans.left = dfs2(l + 1, j - 1, ss);
        ans.right = dfs2(j, r, ss);
        return ans;
    }
}
```
* 时间复杂度:令节点数量为 $n$,序列化的复杂度为 $O(n)$;反序列时由于存在「找第一个比头结点值大的位置」操作,每个节点可能被扫描多次,扫描次数与当前节点所在的深度相关,最坏情况下为一条往左下方的链,复杂度为 $O(n^2)$
* 空间复杂度:$O(n)$

---

### 二分优化

在解法一中的「反序列操作」操作的瓶颈在于需要「找第一个比头结点值大的位置」。

假设连续段 $s[l, r]$ 代表某棵子树的话,由于我们是采用「前序遍历」的方式生成 `s`,因此头结点必然是 $s[l]$,而对于头结点的左右子树,必然是连续两段(先左再右)的形式存储在 $[l + 1, r]$ 中,同时由于该子树是 BST,因此这连续两段必然满足「前一段(左子树)小于 $t$」和「后一段(右子树)大于 $t$」。

即具有「二段性」,因此「找第一个比头结点值大的位置」可用「二分」实现。

代码:
```Java
public class Codec {
    public String serialize(TreeNode root) {
        if (root == null) return null;
        List<String> list = new ArrayList<>();
        dfs1(root, list);
        int n = list.size();
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < n; i++) {
            sb.append(list.get(i));
            if (i != n - 1) sb.append(",");
        }
        return sb.toString();
    }
    void dfs1(TreeNode root, List<String> list) {
        if (root == null) return ;
        list.add(String.valueOf(root.val));
        dfs1(root.left, list);
        dfs1(root.right, list);
    }
    public TreeNode deserialize(String s) {
        if (s == null) return null;
        String[] ss = s.split(",");
        return dfs2(0, ss.length - 1, ss);
    }
    TreeNode dfs2(int l, int r, String[] ss) {
        if (l > r) return null;
        int ll = l + 1, rr = r, t = Integer.parseInt(ss[l]);
        while (ll < rr) {
            int mid = ll + rr >> 1;
            if (Integer.parseInt(ss[mid]) > t) rr = mid;
            else ll = mid + 1;
        }
        if (Integer.parseInt(ss[rr]) <= t) rr++;
        TreeNode ans = new TreeNode(t);
        ans.left = dfs2(l + 1, rr - 1, ss);
        ans.right = dfs2(rr, r, ss);
        return ans;
    }
}
```
* 时间复杂度:令节点数量为 $n$,序列化的复杂度为 $O(n)$;反序列时由于存在「找第一个比头结点值大的位置」操作,最坏情况下为一条往左下方的链,该操作采用「二分」,复杂度为 $O(\log{n})$,整体复杂度为 $O(n\log{n})$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


LeetCode/441-450/450. 删除二叉搜索树中的节点（中等）.md

450. 删除二叉搜索树中的节点

Question:
给定一个二叉搜索树的根节点 `root` 和一个值 `key`,删除二叉搜索树中的 `key` 对应的节点,并保证二叉搜索树的性质不变。返回二叉搜索树(有可能被更新)的根节点的引用。

一般来说,删除节点可分为两个步骤:

* 首先找到需要删除的节点;
* 如果找到了,删除它。

示例 1:

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

输出:[5,4,6,2,null,null,7]

解释:给定需要删除的节点值是 3,所以我们首先找到 3 这个节点,然后删除它。
一个正确的答案是 [5,4,6,2,null,null,7], 如下图所示。
```

```
另一个正确答案是 [5,2,6,null,4,null,7]。
```

示例 2:
```
输入: root = [5,3,6,2,4,null,7], key = 0

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

解释: 二叉树不包含值为 0 的节点
```
示例 3:
```
输入: root = [], key = 0

输出: []
```

提示:
* 节点数的范围 $[0, 10^4]$
* $-10^5 <= Node.val <= 10^5$
* 节点值唯一
* root 是合法的二叉搜索树
* $-10^5 <= key <= 10^5$

进阶: 要求算法时间复杂度为 $O(h)$,$h$ 为树的高度。

Solution:
### 递归

利用题目本身的函数签名的含义,也就是「**在以 `root` 为根的子树中,删除值为 `key` 的节点,并返回删除节点后的树的根节点**」,我们可以用「递归」来做。

起始先对边界情况进行处理,当 `root` 为空(可能起始传入的 `root` 为空,也可能是递归过程中没有找到值为 `key` 的节点时,导致的 `root` 为空),我们无须进行任何删除,直接返回 `null` 即可。

根据当前 `root.val` 与 `key` 的大小关系,进行分情况讨论:

1. 若有 $root.val < key$,说明待删除的节点必然不是当前节点,以及不在当前节点的左子树中,我们将删除动作「递归」到当前节点的右子树,并将删除(可能进行)之后的新的右子树根节点,重新赋值给 `root.right`,即有 `root.right = deleteNode(root.right, key)`;
2. 若有 $root.val > key$,说明待删除的节点必然不是当前节点,以及不在当前节点的右子树,我们将删除节点「递归」到当前节点的左子树,并将删除(可能进行)之后的新的左子树根节点,重新赋值给 `root.left`,即有 `root.left = deleteNode(root.left, key)`;
3. 若有 $root.val = key$,此时找到了待删除的节点,我们根据左右子树的情况,进行进一步分情况讨论:
    * 若左/右子树为空,我们直接返回右/左子树节点即可(含义为直接将右/左子树节点搬到当前节点的位置)如图所示:

    * 若左右子树均不为空,我们有两种选择:
        * 从「当前节点的左子树」中选择「值最大」的节点替代 `root` 的位置,确保替代后仍满足 `BST` 特性;
        * 从「当前节点的右子树」中选择「值最小」的节点替代 `root` 的位置,确保替代后仍满足 `BST` 特性;

        我们以「从当前节点的左子树中选择值最大的节点」为例子,我们通过树的遍历,找到其位于「最右边」的节点,记为 $t$($t$ 作为最右节点,必然有 `t.right = null`),利用原本的 `root` 也是合法 `BST`,原本的 `root.right` 子树的所有及节点,必然满足大于 `t.val`,我们可以直接将 `root.right` 接在 `t.right` 上,并返回我们重接后的根节点,也就是 `root.left`。

        而「从当前节点的右子树中选择值最小的节点」,同理(代码见 $P2$)。

代码:
```Java
class Solution {
    public TreeNode deleteNode(TreeNode root, int key) {
        if (root == null) return null;
        if (root.val == key) {
            if (root.left == null) return root.right;
            if (root.right == null) return root.left;
            TreeNode t = root.left;
            while (t.right != null) t = t.right;
            t.right = root.right;
            return root.left;
        } else if (root.val < key) root.right = deleteNode(root.right, key);
        else root.left = deleteNode(root.left, key);
        return root;
    }
}
```

-

```Java
class Solution {
    public TreeNode deleteNode(TreeNode root, int key) {
        if (root == null) return null;
        if (root.val == key) {
            if (root.left == null) return root.right;
            if (root.right == null) return root.left;
            TreeNode t = root.right;
            while (t.left != null) t = t.left;
            t.left = root.left;
            return root.right;
        } else if (root.val < key) root.right = deleteNode(root.right, key);
        else root.left = deleteNode(root.left, key);
        return root;
    }
}
```
* 时间复杂度:$O(h)$,其中 $h$ 为树的深度
* 空间复杂度:忽略递归带来的额外空间消耗,复杂度为 $O(1)$

### 最后

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

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

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

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


451. 根据字符出现频率排序

Question:
给定一个字符串,请将字符串里的字符按照出现的频率降序排列。

示例 1:
```
输入:
"tree"

输出:
"eert"

解释:
'e'出现两次,'r'和't'都只出现一次。
因此'e'必须出现在'r'和't'之前。此外,"eetr"也是一个有效的答案。
```
示例 2:
```
输入:
"cccaaa"

输出:
"cccaaa"

解释:
'c'和'a'都出现三次。此外,"aaaccc"也是有效的答案。
注意"cacaca"是不正确的,因为相同的字母必须放在一起。
```
示例 3:
```
输入:
"Aabb"

输出:

"bbAa"

解释:
此外,"bbaA"也是一个有效的答案,但"Aabb"是不正确的。
注意'A'和'a'被认为是两种不同的字符。
```

提示:
* $1 <= s.length <= 5 \times 10^5$
* `s` 由大小写英文字母和数字组成

Solution:
### 数据结构 + 模拟

这是一道考察数据结构运用的模拟题。

具体做法如下:
1. 先使用「哈希表」对词频进行统计;
2. 遍历统计好词频的哈希表,将每个键值对以 `{字符,词频}` 的形式存储到「优先队列(堆)」中。并规定「优先队列(堆)」排序逻辑为:
    * 如果 `词频` 不同,则按照 `词频` 倒序;
    * 如果 `词频` 相同,则根据 `字符字典序` 升序(由于本题采用 Special Judge 机制,这个排序策略随意调整也可以。但通常为了确保排序逻辑满足「全序关系」,这个地方可以写正写反,但理论上不能不写,否则不能确保每次排序结果相同);
3. 从「优先队列(堆)」依次弹出,构造答案。

代码:
```Java
class Solution {
    public String frequencySort(String s) {
        char[] cs = s.toCharArray();
        Map<Character, Integer> map = new HashMap<>();
        for (char c : cs) map.put(c, map.getOrDefault(c, 0) + 1);
        PriorityQueue<int[]> q = new PriorityQueue<>((a,b)->{
            return a[1] != b[1] ? b[1] - a[1] : a[0] - b[0];
        });
        for (char c : map.keySet()) q.add(new int[]{c, map.get(c)});
        StringBuilder sb = new StringBuilder();
        while (!q.isEmpty()) {
            int[] poll = q.poll();
            int c = poll[0], k = poll[1];
            while (k-- > 0) sb.append((char)(c));
        }
        return sb.toString();
    }
}
```
* 时间复杂度:令字符集的大小为 $C$。使用「哈希表」统计词频的复杂度为 $O(n)$;最坏情况下字符集中的所有字符都有出现,最多有 $C$ 个节点要添加到「优先队列(堆)」中,复杂度为 $O(C\log{C})$;构造答案需要从「优先队列(堆)」中取出元素并拼接,复杂度为 $O(n)$。整体复杂度为 $O(\max(n, C\log{C}))$
* 空间复杂度:$O(n)$

---

### 数组实现 + 模拟

基本思路不变,将上述过程所用到的数据结构使用数组替代。

具体的,利用 ASCII 字符集共 $128$ 位,预先建立一个大小为 $128$ 的数组,利用「桶排序」的思路替代「哈希表」和「优先队列(堆)」的作用。

代码:
```Java
class Solution {
    public String frequencySort(String s) {
        int[][] cnts = new int[128][2];
        char[] cs = s.toCharArray();
        for (int i = 0; i < 128; i++) cnts[i][0] = i;
        for (char c : cs) cnts[c][1]++;
        Arrays.sort(cnts, (a, b)->{
            return a[1] != b[1] ? b[1] - a[1] : a[0] - b[0];
        });
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < 128; i++) {
            char c = (char)cnts[i][0];
            int k = cnts[i][1];
            while (k-- > 0) sb.append(c);
        }
        return sb.toString();
    }
}
```
* 时间复杂度:令字符集的大小为 $C$,复杂度为 $O(\max(n, C\log{C}))$
* 空间复杂度:$O(n + C + \log{C})$

### 最后

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

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

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

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


LeetCode/451-460/453. 最小操作次数使数组元素相等（简单）.md

453. 最小操作次数使数组元素相等

Question:
给你一个长度为 `n` 的整数数组,每次操作将会使 `n - 1` 个元素增加 `1` 。

返回让数组所有元素相等的最小操作次数。

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

输出:3

解释:
只需要3次操作(注意每次操作会增加两个元素的值):
[1,2,3]  =>  [2,3,3]  =>  [3,4,3]  =>  [4,4,4]
```
示例 2:
```
输入:nums = [1,1,1]

输出:0
```

提示:
* n == nums.length
* $1 <= nums.length <= 10^5$
* $-10^9 <= nums[i] <= 10^9$
* 答案保证符合 32-bit 整数

Solution:
### 数学

为了方便,令原数组 $num$ 的总和为 $sum$,最小值为 $min$,最大值为 $max$,长度为 $n$,真实最小操作次数为 $ans$。

由于每次只能将 $n - 1$ 个元素整体加一,因此在最终的相等状态,整体元素的大小值 $t$ 满足关系 $t \geqslant max$。

我们考虑是否必然可以取到关系式中的等号?

答案是不一定,当且仅当 $num$ 本身有 $n - 1$ 个元素与 $max$ 差值相等,才能取得关系式中的等号。

同时我们知道,$ans$ 与 $t$ 存在一一对应关系:

$$
ans = \frac{t * n - sum}{n - 1}
$$

要取得最小的 $ans$,其实等价于取得最小的 $t$,但仅靠 $t \geqslant max$ 关系,我们无法直接求得 $ans$。

事实上,我们可以通过值变化来进行分析,凭直觉我们会觉得:**在配平整个数组的过程中,当前数组中的最小值会参与到自增过程中。**

我们通过「反证法」来证明该猜想的正确性。

假设在配平数组的过程,某次自增操作中,「当前最小值」没有参与到自增当中,那么此时自增的对象是除「当前最小值」以外的其余元素,这时候「当前最小值」与其他元素的差值将会增加 $1$,此时如果将操作换成「包含当前最小值自增」的话,我们是可以将最终值 $t$ 减一的,如果最终值 $t$ 变小的话,那么 $ans$ 也会变小,结果会更好。

**因此,如果我们某次自增操作中没有包含「当前最小值」对应的元素的话,我们可以通过调整 $t$ 的大小(减一),来将操作调整为「包含当前最小值进行自增」,同时结果会变好。**

到这里就结束了吗?

还没有,因为到这里我们还不能直接与原始最小值 $min$ 结合起来。

我们还需要证明 **原始的相对最小值 $min$ 在匹配过程中,可以一直保持自身是当前数组中的「相对最小值」**。

这可以通过「归纳法」来证明:

**如果在每次自增操作中,都包含「当前最小值」,那么意味着原始最小值与其他元素的「相对大小」关系不会发生改变(因为原始最小值会一直作为「相对最小值」参与到每一次自增当中)得证成立。**

至此,我们可以得到 $t$ 和 $min$ 的关系式:

$$
t = min + ans
$$

代入之前我们得到的关系式可得:

$$
ans = \frac{(min + ans) * n - sum}{n - 1}
$$

变形整理后可得:

$$
ans = sum - min * n
$$

代码:
```Java
class Solution {
    public int minMoves(int[] nums) {
        int n = nums.length;
        long min = nums[0], sum = 0;
        for (int i : nums) {
            min = Math.min(min, i);
            sum += i;
        }
        return (int)(sum - min * n);
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


Question:
给你一个整数数组 `nums`,数组中共有 `n` 个整数。`132` 模式的子序列 由三个整数 `nums[i]`、`nums[j]` 和 `nums[k]` 组成,并同时满足:$i < j < k$ 和 $nums[i] < nums[k] < nums[j]$ 。

如果 `nums` 中存在 `132` 模式的子序列 ,返回 `true`;否则,返回 `false`。

进阶:很容易想到时间复杂度为 $O(n^2)$ 的解决方案,你可以设计一个时间复杂度为 $O(n \log{n})$ 或$ O(n)$ 的解决方案吗?

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

输出:false

解释:序列中不存在 132 模式的子序列。
```
示例 2:
```
输入:nums = [3,1,4,2]

输出:true

解释:序列中有 1 个 132 模式的子序列: [1, 4, 2] 。
```
示例 3:
```
输入:nums = [-1,3,2,0]

输出:true

解释:序列中有 3 个 132 模式的的子序列:[-1, 3, 2]、[-1, 3, 0] 和 [-1, 2, 0] 。
```

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

Solution:
### 基本思路

朴素的做法是分别对三个数进行枚举,这样的做法是 $O(n^3)$ 的,数据范围是 $10^4$,稳稳超时。

事实上,这样的数据范围甚至不足以我们枚举其中两个数,然后优化找第三个数的 $O(n^2)$ 做法。

**这时候根据数据范围会联想到树状数组,使用树状数组的复杂度是 $O(n\log{n})$ 的,可以过。但是代码量会较多一点,还需要理解离散化等前置知识**。题解也不太好写。

因此,我们可以从 132 的大小特性去分析,如果在确定一个数之后,如何快速找到另外两个数(我们使用 `ijk` 来代指 132 结构):

1. 枚举 `i`:由于 `i` 是 132 结构中最小的数,那么相当于我们要从 i 后面,找到一个对数 `(j,k)`,使得 `(j,k)` 都满足比 `i` 大,同时 `j` 和 `k` 之间存在 `j > k` 的关系。由于我们的遍历是单向的,因此我们可以将问题转化为找 `k`,首先 `k` 需要比 `i` 大,同时在 `[i, k]` 之间存在比 `k` 大的数即可。

2. 枚举 `j`:由于 `j` 是 132 结构里最大的数,因此我们需要在 `j` 的右边中比 `j` 小的「最大」的数,在 `j` 的左边找比 `j` 小的「最小」的数。这很容易联想到单调栈,但是朴素的单调栈是帮助我们找到左边或者右边「最近」的数,无法直接满足我们「最大」和「最小」的要求,需要引入额外逻辑。

3. 枚举 `k`:由于 `k` 是 132 结构中的中间值,这里的分析逻辑和「枚举 i」类似,因为遍历是单向的,我们需要找到 `k` 左边的 `i`,同时确保 `[i,k]` 之间存在比 `i` 和 `k` 大的数字。

以上三种分析方法都是可行的,但「枚举 i」的做法是最简单的。

**因为如果存在 `(j,k)` 满足要求的话,我们只需要找到一个最大的满足条件的 `k`,通过与 `i` 的比较即可。**

也许你还不理解是什么意思。没关系,我们一边证明一边说。

---

### 过程 & 证明

先说处理过程吧,我们从后往前做,维护一个「单调递减」的栈,同时使用 `k` 记录所有出栈元素的最大值(`k` 代表满足 132 结构中的 2)。

那么当我们遍历到 `i`,只要满足发现满足 `nums[i] < k`,说明我们找到了符合条件的 `i j k`。

举个🌰,对于样例数据 `[3, 1, 4, 2]`,我们知道满足 132 结构的子序列是 `[1, 4, 2]`,其处理逻辑是(遍历从后往前):

1. 枚举到 2:栈内元素为 [2],`k` = INF
2. 枚举到 4:不满足「单调递减」,2 出栈更新 `k`,4 入栈。栈内元素为 [4],`k` = 2
3. 枚举到 1:满足 `nums[i] < k`,说明对于 `i` 而言,后面有一个比其大的元素(满足 `i < k` 的条件),同时这个 `k` 的来源又是因为维护「单调递减」而弹出导致被更新的(满足 `i` 和 `k` 之间,有比 `k` 要大的元素)。因此我们找到了满足 132 结构的组合。

**这样做的本质是:我们通过维护「单调递减」来确保已经找到了有效的 `(j,k)`。换句话说如果 `k` 有值的话,那么必然是因为有 `j > k`,导致的有值。也就是 132 结构中,我们找到了 32,剩下的 `i` (也就是 132 结构中的 1)则是通过遍历过程中与 `k` 的比较来找到。这样做的复杂度是 $O(n)$ 的,比树状数组还要快。**

从过程上分析,是没有问题的。

搞清楚了处理过程,证明也变得十分简单。

我们不失一般性的考虑任意数组 `nums`,假如真实存在 `ijk` 符合 132 的结构(这里的 `ijk` 特指所有满足 132 结构要求的组合中 `k` 最大的那个组合)。

由于我们的比较逻辑只针对 `i` 和 `k`,而 `i` 是从后往前的处理的,必然会被遍历到;漏掉 `ijk` 的情况只能是:在遍历到 `i` 的时候,我们没有将 `k` 更新到变量中:

1. 这时候变量的值要比真实情况下的 `k` 要小,说明 `k` 还在栈中,而遍历位置已经到达了 `i`,说明 `j` 和 `k` 同时在栈中,与「单调递减」的性质冲突。
2. 这时候变量的值要比真实情况下的 `k` 要大,说明在 `k` 出栈之后,有比 `k` 更大的数值出栈了(同时必然有比变量更大的值在栈中),这时候要么与我们假设 `ijk` 是 `k` 最大的组合冲突;要么与我们遍历到的位置为 `i` 冲突。

**综上,由于「单调递减」的性质,我们至少能找到「遍历过程中」所有符合条件的 `ijk` 中 `k` 最大的那个组合。**

Java 代码:
```Java
class Solution {
    public boolean find132pattern(int[] nums) {
        int n = nums.length;
        Deque<Integer> d = new ArrayDeque<>();
        int k = Integer.MIN_VALUE;
        for (int i = n - 1; i >= 0; i--) {
            if (nums[i] < k) return true;
            while (!d.isEmpty() && d.peekLast() < nums[i]) {
                // 事实上,k 的变化也具有单调性,直接使用 k = pollLast() 也是可以的
                k = Math.max(k, d.pollLast());
            }
            d.addLast(nums[i]);
        }
        return false;
    }
}
```
Python3 代码:
```Python3
class Solution:
    def find132pattern(self, nums: List[int]) -> bool:
        stack = []
        k = -(10 ** 9 + 7)
        for i in range(len(nums) - 1,-1,-1):
            if nums[i] < k:
                return True
            while stack and stack[-1] < nums[i]:
                k = max(k,stack.pop())
            stack.append(nums[i])
        return False
```
C++ 代码:
```C++
class Solution {
public:
    bool find132pattern(vector<int>& nums) {
        stack<int> st;
        int n = nums.size(), k = INT_MIN;
        for(int i = n - 1; i >= 0; i--){
            if(nums[i] < k) return true;
            while(!st.empty() and st.top() < nums[i]) {
                k = max(k,st.top()); st.pop();
            }
            st.push(nums[i]);
        }
        return false;
    }
};
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


457. 环形数组是否存在循环

Question:
存在一个不含 $0$ 的 环形 数组 $nums$ ,每个 $nums[i]$ 都表示位于下标 $i$ 的角色应该向前或向后移动的下标个数:

* 如果 $nums[i]$ 是正数,向前 移动 $nums[i]$ 步
* 如果 $nums[i]$ 是负数,向后 移动 $nums[i]$ 步

因为数组是**环形**的,所以可以假设从最后一个元素向前移动一步会到达第一个元素,而第一个元素向后移动一步会到达最后一个元素。

数组中的 循环 由长度为 $k$ 的下标序列 $seq$ :
* 遵循上述移动规则将导致重复下标序列 `seq[0] -> seq[1] -> ... -> seq[k - 1] -> seq[0] -> ...`
* 所有 $nums[seq[j]]$ 应当不是 全正 就是 全负
* $k > 1$

如果 $nums$ 中存在循环,返回 $true$ ;否则,返回 $false$。

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

输出:true

解释:存在循环,按下标 0 -> 2 -> 3 -> 0 。循环长度为 3 。
```
示例 2:
```
输入:nums = [-1,2]

输出:false

解释:按下标 1 -> 1 -> 1 ... 的运动无法构成循环,因为循环的长度为 1 。根据定义,循环的长度必须大于 1 。
```
示例 3:
```
输入:nums = [-2,1,-1,-2,-2]

输出:false

解释:按下标 1 -> 2 -> 1 -> ... 的运动无法构成循环,因为 nums[1] 是正数,而 nums[2] 是负数。
所有 nums[seq[j]] 应当不是全正就是全负。
```

提示:
* 1 <= nums.length <= 5000
* -1000 <= nums[i] <= 1000
* nums[i] != 0

进阶:你能设计一个时间复杂度为 O(n) 且额外空间复杂度为 O(1) 的算法吗?

Solution:
### 模拟

根据题意,我们可以从每个下标 $i$ 进行出发检查,如果以某个下标 $i$ 为出发点发现了「循环」,返回 `True`,否则返回 `False`。

唯一需要注意的细节是,当我们处理到的下标为 $cur$,计算下一个跳转点 $next = cur + nums[cur]$ 时,对于越过数组的情况进行处理:

1. 如果 $next$ 为负数:在 $next$ 的基础上增加 $n * \left \lceil next / n \right \rceil$,将其映射回正值;

2. 如果 $next$ 为正数:将 $next$ 模数组长度 $n$,确保不会越界。

整理一下,我们可以统一写成 `next = ((cur + nums[cur]) % n + n ) % n`。

在 `check` 内部,当以下任一条件出现,则可以结束检查(令 $k$ 为记录过程中扫描过的下标数量):

1. 如果在检查过程中,找到了与起点相同的下标,且 $k > 1$,说明存在符合条件的「循环」,返回 `True`;

2. 如果检查过程中扫描的数量 $k$ 超过了数组长度 $n$,那么根据「[鸽笼原理](https://baike.baidu.com/item/%E6%8A%BD%E5%B1%89%E5%8E%9F%E7%90%86/233776)」,必然有数被重复处理了,同时条件一并不符合,因此再处理下去,也不会到达与起点相同的下标,返回 `False`;

3. 处理过程中发现不全是正数或者负数,返回 `False`。

代码:
```Java
class Solution {
    int n;
    int[] nums;
    public boolean circularArrayLoop(int[] _nums) {
        nums = _nums;
        n = nums.length;
        for (int i = 0; i < n; i++) {
            if (check(i)) return true;
        }
        return false;
    }
    boolean check(int start) {
        int cur = start;
        boolean flag = nums[start] > 0;
        int k = 1;
        while (true) {
            if (k > n) return false;
            int next = ((cur + nums[cur]) % n + n ) % n;
            if (flag && nums[next] < 0) return false;
            if (!flag && nums[next] > 0) return false;
            if (next == start) return k > 1;
            cur = next;
            k++;
        }
    }
}
```
* 时间复杂度:$O(n^2)$
* 空间复杂度:$O(1)$

---

### 图的遍历标记(使用新数组标记)

**这是一种补充做法,更多的作为「解法一」和「解法三」之间的过渡,建议在充分理解本解法之后,再学习解法三。**

从「解法一」我们发现,我们会对很多重复的路径进行重复检查。

假如从位置 $a$ 到位置 $d$ 存在一条无环通路 $a-b-c-d$,根据「解法一」我们会在对 $a$ 进行通路是否有环的检查之后,再对 $b$ 、$c$ 和 $d$ 进行路径是否有环的检查。

事实上,由于每个点只有一个出度(某个位置能跳到的下一个位置是唯一确定的),因此我们可以使用 `vis` 数组记录那些下标被检查过了,从而避免相同的路径被重复检查。

**同时,我们可以扩充 `vis` 数组的功能,使其不仅仅能用于判断某个位置是否被检查过,还能记录下某个位置是在哪一轮被检查过。具体的,我们令 $vis[i] = idx$ 代表位置 $i$ 在第 $idx$ 轮被标记。**

如此一来,当我们检查某个位置 $start$ 的通路时,如果遇到一个跳点 $next$,发现 $vis[next]$ 不为 $0$(代表被被记过),可通过将 $vis[next]$ 与当前轮次编号做对比,来得知该位置是否在本轮被标记。

代码:
```Java
class Solution {
    public boolean circularArrayLoop(int[] nums) {
        int n = nums.length;
        // 使用 vis 数组对每个下标进行标记
        // 如果下标为 i 的位置在第 idx 轮被标记,则有 vis[i] = idx
        int[] vis = new int[n];
        for (int start = 0, idx = 1; start < n; start++, idx++) {
            if (vis[start] != 0) continue;
            int cur = start;
            boolean flag = nums[cur] > 0;
            while (true) {
                int next = ((cur + nums[cur]) % n + n) % n;
                if (next == cur) break;
                if (vis[next] != 0) {
                    // 如果 next 点已经被标记过,并且不是在本轮被标记,那么往后的通路必然都被标记,且无环,跳出
                    if (vis[next] != idx) break;
                    // 如果 next 点已被标记,并且是本来被标记,说明找到了环
                    else return true;
                }
                if (flag && nums[next] < 0) break;
                if (!flag && nums[next] > 0) break;
                vis[next] = idx;
                cur = next;
            }
        }
        return false;
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n)$

---

### 图的遍历标记(使用原数组标记)

**根据题意,我们将每个下标看做“点”,「当前点」和「当前点所能到达的下一个点」看作“边”。**

**从而将问题转换为经典的「图论寻环」问题,同时又因为每个点出度固定为 $1$,并且规定「循环」必然是「同向」才合法,因此如果我们在遍历过程中发现存在反向,就停止检查。**

另外,为实现 $O(1)$ 的空间,我们需要在原数组上进行标记,我们设立一个足够大的数 `OFFSET`,对于由下标 $i$ 发起的寻环操作,我们将扫描的数标记为 `OFFSET + i`。如果在扫描完由 $i$ 发起的寻环后,没法发现自环,说明找到了「循环」,输出 `True`。

代码:
```Java
class Solution {
    int OFFSET = 100010;
    public boolean circularArrayLoop(int[] nums) {
        int n = nums.length;
        for (int i = 0; i < n; i++) {
            if (nums[i] >= OFFSET) continue;
            int cur = i, tag = OFFSET + i, last = -1;
            boolean flag = nums[cur] > 0;
            while (true) {
                int next = ((cur + nums[cur]) % n + n ) % n;
                last = nums[cur];
                nums[cur] = tag;
                cur = next;
                if (cur == i) break;
                if (nums[cur] >= OFFSET) break;
                if (flag && nums[cur] < 0) break;
                if (!flag && nums[cur] > 0) break;
            }
            if (last % n != 0 && nums[cur] == tag) return true;
        }
        return false;
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


Question:
有 `buckets` 桶液体,其中 正好 有一桶含有毒药,其余装的都是水。它们从外观看起来都一样。

为了弄清楚哪只水桶含有毒药,你可以喂一些猪喝,通过观察猪是否会死进行判断。不幸的是,你只有 `minutesToTest` 分钟时间来确定哪桶液体是有毒的。

喂猪的规则如下:

1. 选择若干活猪进行喂养
2. 可以允许小猪同时饮用任意数量的桶中的水,并且该过程不需要时间。
3. 小猪喝完水后,必须有 `minutesToDie` 分钟的冷却时间。在这段时间里,你只能观察,而不允许继续喂猪。
4. 过了 `minutesToDie` 分钟后,所有喝到毒药的猪都会死去,其他所有猪都会活下来。
5. 重复这一过程,直到时间用完。

给你桶的数目 `buckets` ,`minutesToDie` 和 `minutesToTest` ,返回在规定时间内判断哪个桶有毒所需的 最小 猪数。

示例 1:
```
输入:buckets = 1000, minutesToDie = 15, minutesToTest = 60

输出:5
```
示例 2:
```
输入:buckets = 4, minutesToDie = 15, minutesToTest = 15

输出:2
```
示例 3:
```
输入:buckets = 4, minutesToDie = 15, minutesToTest = 30

输出:2
```

提示:
* $1 <= buckets <= 1000$
* $1 <= minutesToDie <= minutesToTest <= 100$

Solution:
### 数学

我们用实验对象来代指题干的小动物。同时为了方便,我们使用 $n$ 代指有多少桶水,$d$ 为实验对象的反应时间,$t$ 为测试总时间。

根据题意,最大测试次数为 $k = \left \lfloor \frac{t}{d} \right \rfloor$。

我们可以先考虑 $k = 1$ 的情况,最简单的情况是,我们使用与水同等数量的实验对象数量来进行测试。

此时哪个实验对象有反应,则可以推断出哪一桶水有问题。

但这样的测试方式,每个实验动物承载的信息量是很低的,每个实验对象仅承载了某一桶水是否有问题。

**为减少实验对象数量,我们需要增大每个实验对象承载的信息量(让每个实验对象同时测试多桶水),然后从最终所有实验对象的状态(是否有所反应)来反推哪一桶水有问题。**

用最小单位表示最大信息量,这引导我们使用「进制表示」相关方式。由于我们只有 $1$ 次测试机会,因此我们可以使用二进制的方式进行测试。

当 $k = 1$,使用二进制的方式测试哪桶水有问题,我们至少需要 $m$ 个实验对象(其中 $m$ 为 $n$ 的二进制表示的长度),然后让编号为 $x$($0 <= x < m$)的实验对象喝掉二进制表示中第 $x$ 位为 $1$ 的水。

最终这 $m$ 个实验对象会对应一个结果序列:如果编号 $x_1$ 的实验对象没有反应,说明有问题的水的二进制表示中第 $x_1$ 位为 $0$,如果编号为 $x_2$ 的实验对象有反应,则说明有问题的水的二进制表示中第 $x_2$ 为 $1$。即根据最终每个实验对象的状态,我们可以完整地反推回有问题的水的编号是多少。

当 $k > 1$ 时,相当于在原问题基础上,多考虑一层「轮数」维度,即不仅考虑某个实验对象是否有所反应,还需要考虑是在哪一轮有所反应。

我们还是使用「进制表示」的方式来最大化每个单位所能承载的最大信息量。

具体的,我们先使用 $k + 1$ 进制对所有水进行编号,此时每桶水都有唯一的进制表示编码。然后我们考虑「什么时候」将水喂给「哪个实验对象」。

其中一种可行的测试方式是:设定需要的实验对象数量 $m$ 为 $k + 1$ 进制数的长度,若某桶水的 $k + 1$ 进制中的第 $x$ 位为 $i$($0 <= i <= k$),则代表将该水在第 $i$ 轮喂给编号为 $x$ 的实验对象。

同理,利用最终的结果矩阵,我们可以反推回是哪一桶水是有问题的。

**上述做法,只是阐述了我们存在这样的可行解,需要证明这样的做法是最优解。**

利用 [香农熵](https://baike.baidu.com/item/香农熵),我们可以计算明确熵值,公式为:

$$
H(X) = - \sum_{x}^{} P(x) \log_2[P(x)]
$$

其中 $P(x)$ 代表随机事件 $x$ 的发生概率。

对于本题,记随机事件 $A$ 为 $n$ 桶水中哪一个桶有问题,概率为 $\frac{1}{n}$。

记随机事件 $B$ 为在测试轮数为 $k$ 时,所有实验对象的最终状态,每个实验对象的状态共有 $k + 1$ 种,即共有 $C = (k + 1)^m$ 种最终结果,可近似看做等概率 $\frac{1}{C}$。

我们需要求得在满足 $H(A) <= H(B)$ 前提下的最小 $m$ 值。

代入公式可得:

$$
-(\log_2{\frac{1}{n}}) <= - \sum_{result = 0}^{(k + 1)^m} \frac{1}{(k + 1)^m} \log_2{\frac{1}{(k + 1)^m}} = m \log_2(k + 1)
$$

移项化简得:

$$
\frac{\log_2{n}}{\log_2{(k + 1)}} <= m
$$

代码:
```Java
class Solution {
    public int poorPigs(int n, int d, int t) {
        int k = t / d;
        return (int) Math.ceil(Math.log(n) / Math.log(k + 1));
    }
}
```
* 时间复杂度:$O(1)$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


Question:
请你为 **最不经常使用(LFU)** 缓存算法设计并实现数据结构。

实现 `LFUCache` 类:

* `LFUCache(int capacity)` - 用数据结构的容量 `capacity` 初始化对象
* `int get(int key)` - 如果键存在于缓存中,则获取键的值,否则返回 -1。
* `void put(int key, int value)` - 如果键已存在,则变更其值;如果键不存在,请插入键值对。当缓存达到其容量时,则应该在插入新项之前,使最不经常使用的项无效。在此问题中,当存在平局(即两个或更多个键具有相同使用频率)时,应该去除 **最近最久未使用** 的键。

注意「项的使用次数」就是自插入该项以来对其调用 `get` 和 `put` 函数的次数之和。使用次数会在对应项被移除后置为 0 。

为了确定最不常使用的键,可以为缓存中的每个键维护一个 使用计数器 。使用计数最小的键是最久未使用的键。

当一个键首次插入到缓存中时,它的使用计数器被设置为 1 (由于 `put` 操作)。对缓存中的键执行 `get` 或 `put` 操作,使用计数器的值将会递增。

示例:
```
输入:
["LFUCache", "put", "put", "get", "put", "get", "get", "put", "get", "get", "get"]
[[2], [1, 1], [2, 2], [1], [3, 3], [2], [3], [4, 4], [1], [3], [4]]
输出:
[null, null, null, 1, null, -1, 3, null, -1, 3, 4]

解释:
// cnt(x) = 键 x 的使用计数
// cache=[] 将显示最后一次使用的顺序(最左边的元素是最近的)
LFUCache lFUCache = new LFUCache(2);
lFUCache.put(1, 1);   // cache=[1,_], cnt(1)=1
lFUCache.put(2, 2);   // cache=[2,1], cnt(2)=1, cnt(1)=1
lFUCache.get(1);      // 返回 1
                      // cache=[1,2], cnt(2)=1, cnt(1)=2
lFUCache.put(3, 3);   // 去除键 2 ,因为 cnt(2)=1 ,使用计数最小
                      // cache=[3,1], cnt(3)=1, cnt(1)=2
lFUCache.get(2);      // 返回 -1(未找到)
lFUCache.get(3);      // 返回 3
                      // cache=[3,1], cnt(3)=2, cnt(1)=2
lFUCache.put(4, 4);   // 去除键 1 ,1 和 3 的 cnt 相同,但 1 最久未使用
                      // cache=[4,3], cnt(4)=1, cnt(3)=2
lFUCache.get(1);      // 返回 -1(未找到)
lFUCache.get(3);      // 返回 3
                      // cache=[3,4], cnt(4)=1, cnt(3)=3
lFUCache.get(4);      // 返回 4
                      // cache=[3,4], cnt(4)=2, cnt(3)=3
```

提示:
* 0 <= capacity, key, value <= $10^4$
* 最多调用 $10^5$ 次 `get` 和 `put` 方法

进阶:你可以为这两种操作设计时间复杂度为 $O(1)$ 的实现吗?

Solution:
### 基本分析

前两天我们刚讲过 [146. LRU 缓存机制](https://mp.weixin.qq.com/s?__biz=MzU4NDE3MTEyMA==&mid=2247486820&idx=1&sn=2055864e10848bce55afc4e2feda79a7&chksm=fd9ca67bcaeb2f6d2038706e32cafffd701d3d6b71b60c52a568ed6dc0e268d93f8a78498105&token=672687058&lang=zh_CN#rd) ,简单理解 LRU 就是「移除最久不被使用的元素」。

因此对于 LRU 我们只需要在使用「哈希表」的同时,维护一个「双向链表」即可:

* 每次发生 `get` 或 `put` 的时候就将元素存放双向链表头部
* 当需要移除元素时,则从双向链表尾部开始移除

LFU 简单理解则是指「移除使用次数最少的元素」,如果存在多个使用次数最小的元素,则移除「最近不被使用的那个」(LRU 规则)。同样的 `get` 和 `put` 都算作一次使用。

因此,我们需要记录下每个元素的使用次数,并且在 $O(1)$ 的复杂度内「修改某个元素的使用次数」和「找到使用次数最小的元素」。

---

### 桶排序 + 双向链表

**我们可以使用「桶排序」的思路,搭配「双向链表」实现 $O(1)$ 操作。**

**在 `LFUCache` 中,我们维护一个由 `Bucket` 作为节点的双向链表,每个 `Bucket` 都有一个 `idx` 编号,代表当前桶存放的是「使用了多少次」的键值对**(`idx = 1` 的桶存放使用一次的键值对;`idx = 2` 的桶存放的是使用两次的键值对 ... )。

同时 `LFUCache` 持有一个「哈希表」,用来记录哪些 `key` 在哪个桶内。

**在 `Bucket` 内部则是维护了一条以 `Item` 作为节点的双向链表,`Item` 是用作存放真实键值对的。**

同样的,`Bucket` 也持有一个「哈希表」,用来记录 `key` 与 `Item` 的映射关系。

因此 `LFUCache` 其实是一个「链表套链表」的数据结构:

对应到 `LFUCache` 的几种操作:

* `get` :先通过 `LFUCache` 持有的哈希表进行查找,如果不存在返回 $-1$,如果存在找到键值对所在的桶 `cur`:
    * 调用对应的 `cur` 的 `remove` 操作,得到键值对对应的 `item`(移除代表当前键值对使用次数加一了,不会在存在于原来的桶中)。
    * 将 `item` 放到 `idx` 为 $cur.idx + 1$ 的桶 `target` 中(代表代表当前键值对使用次数加一,应该放到新的目标桶中)。
    * 如果目标桶 `target` 不存在,则创建;如果原来桶 `cur` 移除键值对后为空,则销毁。
    * 更新 `LFUCache` 中哈希表的信息。
* `put` : 先通过 `LFUCache` 持有的哈希表进行查找:
    * 如果存在:找到键值对所在的桶 `cur`,调用 `cur` 的 `put` 操作,更新键值对,然后调用 `LFUCache` 的 `get` 操作实现使用次数加一。
    * 如果不存在:先检查容量是否达到数量:
        * 容量达到数量的话需要调用「编号最小的桶」的 `clear` 操作,在 `clear` 操作内部,会从 `item` 双向链表的尾部开始移除元素。完成后再执行插入操作。
    * 插入操作:将键值对添加到 $idx = 1$ 的桶中(代表当前键值对使用次数为 $1$),如果桶不存在则创建。

代码:
```Java
class LFUCache {

    class Item {
        Item l, r;
        int k, v;
        public Item(int _k, int _v) {
            k = _k;
            v = _v;
        }
    }

    class Bucket {
        Bucket l, r;
        int idx;
        Item head, tail;
        Map<Integer, Item> map = new HashMap<>();
        public Bucket(int _idx) {
            idx = _idx;
            head = new Item(-1, -1);
            tail = new Item(-1, -1);
            head.r = tail;
            tail.l = head;
        }
        void put(int key, int value) {
            Item item = null;
            if (map.containsKey(key)) {
                item = map.get(key);
                // 更新值
                item.v = value;
                // 在原来的双向链表位置中移除
                item.l.r = item.r;
                item.r.l = item.l;
            } else {
                item = new Item(key, value);
                // 添加到哈希表中
                map.put(key, item);
            }
            // 增加到双向链表头部
            item.r = head.r;
            item.l = head;
            head.r.l = item;
            head.r = item;
        }
        Item remove(int key) {
            if (map.containsKey(key)) {
                Item item = map.get(key);
                // 从双向链表中移除
                item.l.r = item.r;
                item.r.l = item.l;
                // 从哈希表中移除
                map.remove(key);
                return item;
            }
            return null; // never
        }
        Item clear() {
            // 从双向链表尾部找到待删除的节点
            Item item = tail.l;
            item.l.r = item.r;
            item.r.l = item.l;
            // 从哈希表中移除
            map.remove(item.k);
            return item;
        }
        boolean isEmpty() {
            return map.size() == 0;
        }
    }

    Map<Integer, Bucket> map = new HashMap<>();
    Bucket head, tail;
    int n;
    int cnt;
    public LFUCache(int capacity) {
        n = capacity;
        cnt = 0;
        head = new Bucket(-1);
        tail = new Bucket(-1);
        head.r = tail;
        tail.l = head;
    }

    public int get(int key) {
        if (map.containsKey(key)) {
            Bucket cur = map.get(key);

            Bucket target = null;
            if (cur.r.idx != cur.idx + 1) {
                // 目标桶空缺
                target = new Bucket(cur.idx + 1);
                target.r = cur.r;
                target.l = cur;
                cur.r.l = target;
                cur.r = target;
            } else {
                target = cur.r;
            }

            // 将当前键值对从当前桶移除,并加入新的桶
            Item remove = cur.remove(key);
            target.put(remove.k, remove.v);
            // 更新当前键值对所在桶信息
            map.put(key, target);

            // 如果在移除掉当前键值对后,当前桶为空,则将当前桶删除(确保空间是 O(n) 的)
            // 也确保调用编号最小的桶的 clear 方法,能够有效移除掉一个元素
            deleteIfEmpty(cur);

            return remove.v;
        }
        return -1;
    }

    public void put(int key, int value) {
        if (n == 0) return;
        if (map.containsKey(key)) {
            // 元素已存在,修改一下值
            Bucket cur = map.get(key);
            cur.put(key, value);
            // 调用一下 get 实现「使用次数」+ 1
            get(key);
        } else {
            // 容器已满,需要先删除元素
            if (cnt == n) {
                // 从第一个桶(编号最小、使用次数最小)中进行清除
                Bucket cur = head.r;
                Item clear = cur.clear();
                map.remove(clear.k);
                cnt--;

                // 如果在移除掉键值对后,当前桶为空,则将当前桶删除(确保空间是 O(n) 的)
                // 也确保调用编号最小的桶的 clear 方法,能够有效移除掉一个元素
                deleteIfEmpty(cur);
            }

            // 需要将当前键值对增加到 1 号桶
            Bucket first = null;

            // 如果 1 号桶不存在则创建
            if (head.r.idx != 1) {
                first = new Bucket(1);
                first.r = head.r;
                first.l = head;
                head.r.l = first;
                head.r = first;
            } else {
                first = head.r;
            }

            // 将键值对添加到 1 号桶
            first.put(key, value);
            // 更新键值对所在桶信息
            map.put(key, first);
            // 计数器加一
            cnt++;
        }
    }

    void deleteIfEmpty(Bucket cur) {
        if (cur.isEmpty()) {
            cur.l.r = cur.r;
            cur.r.l = cur.l;
            cur = null; // help GC
        }
    }
}
```
* 时间复杂度:各操作均为 $O(1)$
* 时间复杂度:$O(n)$

### 最后

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

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

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

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


Question:
两个整数之间的汉明距离指的是这两个数字对应二进制位不同的位置的数目。

给出两个整数 x 和 y,计算它们之间的汉明距离。

注意:
0 ≤ x, y < 231.

示例:
```
输入: x = 1, y = 4

输出: 2

解释:
1   (0 0 0 1)
4   (0 1 0 0)
       ↑   ↑

上面的箭头指出了对应二进制位不同的位置。
```

Solution:
### 逐位比较

本身不改变 $x$ 和 $y$,每次取不同的偏移位进行比较,不同则加一。

循环固定取满 $32$ 。

代码:
```Java
class Solution {
    public int hammingDistance(int x, int y) {
        int ans = 0;
        for (int i = 0; i < 32; i++) {
            int a = (x >> i) & 1 , b = (y >> i) & 1;
            ans += a != b ? 1 : 0;
        }
        return ans;
    }
}
```
* 时间复杂度:$O(C)$,$C$ 固定为 $32$
* 空间复杂度:$O(1)$

---

### 右移统计

每次都统计当前 $x$ 和 $y$ 的最后一位,统计完则将 $x$ 和 $y$ 右移一位。

当 $x$ 和 $y$ 的最高一位 $1$ 都被统计过之后,循环结束。

代码:
```Java
class Solution {
    public int hammingDistance(int x, int y) {
        int ans = 0;
        while ((x | y) != 0) {
            int a = x & 1, b = y & 1;
            ans += a ^ b;
            x >>= 1; y >>= 1;
        }
        return ans;
    }
}
```
* 时间复杂度:$O(C)$,$C$ 最多为 $32$
* 空间复杂度:$O(1)$

---

### lowbit

熟悉树状数组的同学都知道,`lowbit` 可以快速求得 $x$ 二进制表示中最低位 $1$ 表示的值。

因此我们可以先将 $x$ 和 $y$ 进行异或,再统计异或结果中 $1$ 的个数。

代码:
```Java
class Solution {
    int lowbit(int x) {
        return x & -x;
    }
    public int hammingDistance(int x, int y) {
        int ans = 0;
        for (int i = x ^ y; i > 0; i -= lowbit(i)) ans++;
        return ans;
    }
}
```
* 时间复杂度:$O(C)$,$C$ 最多为 $32$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


LeetCode/461-470/462. 最少移动次数使数组元素相等 II（中等）.md

462. 最少移动次数使数组元素相等 II

Question:
给你一个长度为 $n$ 的整数数组 $nums$,返回使所有数组元素相等需要的最少移动数。

在一步操作中,你可以使数组中的一个元素加 $1$ 或者减 $1$ 。

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

输出:2

解释:
只需要两步操作(每步操作指南使一个元素加 1 或减 1):
[1,2,3]  =>  [2,2,3]  =>  [2,2,2]
```
示例 2:
```
输入:nums = [1,10,2,9]

输出:16
```

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

Solution:
### 数学

假定所有的 $nums[i]$ 均位于数轴上的 $nums[i]$ 的位置,题目要求我们在数轴上找出一个点 $t$,使得所有 $nums[i]$ 到 $t$ 的距离之和最小。

**首先,容易证明 $t$ 不可能位于最小的 $nums[i]$ 的左侧,也不可能位于最大的 $nums[i]$ 的右侧,否则我们「至少」能够将目标点调整为 最小的 $nums[i]$ 或 最大的 $nums[i]$ 来得到更小的距离总和。**

> 其实由上述这一点进行推广,已经可以证明最优点必然是在中间点($nums$ 数量为奇数时)或者中间两点形成的闭区间中的任意点($nums$ 数量为偶数时)。
但为了证明更加直观,我们仍从「反证法」的角度进行证明。

我们根据每个 $nums[i]$ 位于 $t$ 的左侧还是右侧进行划分:假设位于 $t$ 左侧的 $nums[i]$ 对答案的贡献为 $A$,位于 $t$ 右侧的 $nums[i]$ 对答案的贡献为 $B$,最终目的是为了让 $A + B$ 最小。

我们猜想当 $t$ 取中位数时,$A + B$ 取得最小值,并通过「反证法」进行证明:

* 假设真实最优解 $t'$ 位于中位数 $t$ 的 左侧:假设调整距离为 $d$,导致变化的点数为 $x$,则有左边总和为 $A - xd$,右边总和为 $B + (n - x)d$,总和为 $A + B - 2xd + nd$,如果要使得结果更好,需要满足 $nd - 2xd < 0$,即满足 $x > \frac{n}{2}$,这与我们本身 $t$ 为中位数,即左右两边数的个数均为 $\frac{n}{2}$ 冲突(特别地,当 $nums$ 为偶数时,且目标点位于中间两点中的任一点时,左右数的个数并非为 $\frac{n}{2}$,但距离总和情况与 $t$ 位于两点间的其余点的情况一致);

* 假设真实最优解 $t'$ 位于中位数 $t$ 的 右侧:同理。

代码:
```Java
class Solution {
    public int minMoves2(int[] nums) {
        Arrays.sort(nums);
        int n = nums.length, t = nums[(n - 1) / 2], ans = 0;
        for (int i : nums) ans += Math.abs(t - i);
        return ans;
    }
}
```
* 时间复杂度:$O(n\log{n})$
* 空间复杂度:$O(\log{n})$

### 最后

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

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

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

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


Question:
在 "100 game" 这个游戏中,两名玩家轮流选择从 $1$ 到 $10$ 的任意整数,累计整数和,先使得累计整数和 达到或超过  $100$ 的玩家,即为胜者。

如果我们将游戏规则改为 “玩家 不能 重复使用整数” 呢?

例如,两个玩家可以轮流从公共整数池中抽取从 $1$ 到 $15$ 的整数(不放回),直到累计整数和 >= $100$。

给定两个整数 `maxChoosableInteger` (整数池中可选择的最大数)和 `desiredTotal`(累计和),若先出手的玩家是否能稳赢则返回 `true` ,否则返回 `false` 。假设两位玩家游戏时都表现 最佳 。

示例 1:
```
输入:maxChoosableInteger = 10, desiredTotal = 11

输出:false

解释:
无论第一个玩家选择哪个整数,他都会失败。
第一个玩家可以选择从 1 到 10 的整数。
如果第一个玩家选择 1,那么第二个玩家只能选择从 2 到 10 的整数。
第二个玩家可以通过选择整数 10(那么累积和为 11 >= desiredTotal),从而取得胜利.
同样地,第一个玩家选择任意其他整数,第二个玩家都会赢。
```
示例 2:
```
输入:maxChoosableInteger = 10, desiredTotal = 0

输出:true
```
示例 3:
```
输入:maxChoosableInteger = 10, desiredTotal = 1

输出:true
```

提示:
* $1 <= maxChoosableInteger <= 20$
* $0 <= desiredTotal <= 300$

Solution:
### 二维博弈论 DP(TLE)

这是一道博弈论 DP 的题,为了方便,我们使用 $n$ 来表示 $maxChoosableInteger$,使用 $t$ 来表示 $desiredTotal$。

由于 $n$ 数据范围为 $20$,且每个数只能选一次,我们可以使用一个二进制数 $state$ 来表示 $[1, n]$ 范围内的被选择的数的情况:二进制表示中 $1$ 的位置代表数已被选择,否则代表尚未选择。

首先朴素二维状态表示相对容易想到:**定义 $f[statue][k]$ 为当前已被选择的数为 $state$,轮数为 $k$ 时,「原始回合的先手」能否获胜($1$ 代表能,$-1$ 代表不能),其中 $k$ 从 $0$ 开始,通过 $k$ 的奇偶性可知是原始回合的先手还是后手。**

设计递归函数来实现「记忆化搜索」,函数 `int dfs(int state, int tot, int k)` 表示当前状态为 $state$,$tot$ 对应累计和,$k$ 代表轮数,最终答案通过判断 `dfs(0, 0, 0)` 是否为 $1$ 来得知。

**转移过程中,如果发现当前回合的决策,能够直接使得累积和超过 $t$,说明当前回合玩家获胜;或者如果当前决策能够导致下一回合的玩家失败的话,当前回合玩家也获胜,否则当前玩家失败。**

代码:
```Java
class Solution {
    int n, t;
    int[][] f = new int[1 << 20][2];
    // 1 true / -1 false
    int dfs(int state, int tot, int k) {
        if (state == ((1 << n) - 1) && tot < t) return -1;
        if (f[state][k % 2] != 0) return f[state][k % 2];
        int hope = k % 2 == 0 ? 1 : -1;
        for (int i = 0; i < n; i++) {
            if (((state >> i) & 1) == 1) continue;
            if (tot + i + 1 >= t) return f[state][k % 2] = hope;
            if (dfs(state | (1 << i), tot + i + 1, k + 1) == hope) return f[state][k % 2] = hope;
        }
        return f[state][k % 2] = -hope;
    }
    public boolean canIWin(int _n, int _t) {
        n = _n; t = _t;
        if (t == 0) return true;
        return dfs(0, 0, 0) == 1;
    }
}
```
* 时间复杂度:共有 $2^{n} \times 2$ 个状态,每个状态转移需要 $O(n)$ 复杂度,整体复杂度为 $O(2^{n + 1} \times n)$
* 空间复杂度:$O(2^{n + 1})$

---

### 优化状态表示

进一步发现,若能优化轮数维度,可以有效减少一半的计算量,我们调整状态定义为:**定义 $f[state]$ 为当前状态为 $state$,「当前先手」能否获胜($1$ 代表能,$-1$ 代表不能)。**

同时调整递归函数为 $int dfs(int state, int tot)$,最终答案通过判断 `dfs(0, 0)` 是否为 $1$ 来得知。

注意这里调整的重点在于:将记录「原始回合的先后手发起 和 原始回合的先后手获胜情况」调整为「当前回合发起 和 当前回合获胜情况」。

代码:
```Java
class Solution {
    int n, t;
    int[] f = new int[1 << 20];
    // 1 true / -1 false
    int dfs(int state, int tot) {
        if (f[state] != 0) return f[state];
        for (int i = 0; i < n; i++) {
            if (((state >> i) & 1) == 1) continue;
            if (tot + i + 1 >= t) return f[state] = 1;
            if (dfs(state | (1 << i), tot + i + 1) == -1) return f[state] = 1;
        }
        return f[state] = -1;
    }
    public boolean canIWin(int _n, int _t) {
        n = _n; t = _t;
        if (n * (n + 1) / 2 < t) return false;
        if (t == 0) return true;
        return dfs(0, 0) == 1;
    }
}
```
* 时间复杂度:共有 $2^{n}$ 个状态,每个状态转移需要 $O(n)$ 复杂度,整体复杂度为 $O(2^{n} \times n)$
* 空间复杂度:$O(2^{n})$

### 最后

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

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

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

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


LeetCode/461-470/467. 环绕字符串中唯一的子字符串（中等）.md

467. 环绕字符串中唯一的子字符串

Question:
把字符串 `s` 看作是 `“abcdefghijklmnopqrstuvwxyz”` 的无限环绕字符串,所以 `s` 看起来是这样的:

`"...zabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcd...."` .

现在给定另一个字符串 `p` 。返回 `s` 中 唯一 的 `p` 的 非空子串 的数量 。

示例 1:
```
输入: p = "a"

输出: 1

解释: 字符串 s 中只有一个"a"子字符。
```
示例 2:
```
输入: p = "cac"

输出: 2

解释: 字符串 s 中的字符串“cac”只有两个子串“a”、“c”。.
```
示例 3:
```
输入: p = "zab"

输出: 6

解释: 在字符串 s 中有六个子串“z”、“a”、“b”、“za”、“ab”、“zab”。
```

提示:
* $1 <= p.length <= 10^5$
* `p` 由小写英文字母构成

Solution:
### 线性 DP + 树状数组 + 同字符最大长度计数

> ~~早上起来没睡醒~~ 老了,脑袋不行了,第一反应是用「线性 DP + 树状数组」来做,估了一下时间复杂度没问题就写了。
该做法有一点点思维难度,因此可能不是这道中等题的标准解法。

**定义 $f[i]$ 为以 $s[i]$ 为结尾的最大有效子串的长度。**

从该状态定义容易得到如下的状态转移方程:

* 存在 $s[i - 1]$ 并且 $s[i]$ 能够接在 $s[i - 1]$ 后面(除了 $s[i]$ 为 $s[i - 1]$ 的下一字母以外,还特别包括 $s[i - 1] = z$ 同时 $s[i] = a$ 的情况),则我们有 $f[i] = f[i - 1] + 1$;
* 不存在 $s[i - 1]$ 或者 $s[i]$ 不能接在 $s[i - 1]$ 后面,则有 $f[i] = 1$,含义为 $s[i]$ 只能自身组成子串。

**与此同时,我们知道当结尾元素固定,子串长度固定,对应子串唯一确定。**

当不考虑子串重复问题时,若 $f[i] = k$,则以 $s[i]$ 为结尾的有效子串数量为 $k$ 个(对应以 $s[i]$ 为结尾,长度范围为 $[1, k]$ 的子数组)。

但实际上,我们不能统计相同的子串,因此我们需要考虑该如何去重。

不失一般性地,假设我们当前处理到字符串 `p` 中的第 $i$ 位,以 $s[i]$ 为结尾的最大子串长度为 $f[i]$:

* 此前如果 **出现过** 以 $s[i]$ 为结尾,长度「大于等于」$f[i]$ 的子串的话,那么以 $s[i]$ 为结尾长度为 $f[i]$ 的子串必然已被统计,需要跳过。因此我们可以使用一个长度为 $26$ 的数组 `max`,记录以每个字符 $s[i]$ 结尾的,出现过的最大子串长度为多少(当 `max[s[i]] >= f[i]` 时,跳过计数);
* 此前如果 **出现过** 以 $s[i]$ 为结尾,长度「小于」$f[i]$ 的子串的话,我们也不能直接统计累加 $f[i]$ 到答案上,这会导致那些以 $s[i]$ 为结尾,长度小于 $f[i]$ 的子串被重复计数,此时我们 **需要知道在以 $s[i]$ 为结尾,长度为 $[1, f[i]]$ 范围内还有多少个子串尚未被统计**,这可以使用「树状数组」解决:在 $[1, f[i]]$ 中总个数为 $a = f[i]$,使用树状数组维护在 $[1, f[i]]$ 中已被统计的数的个数 $b$,那么 $cnt = a - b$ 即是本次可增加的计数,计数完成后我们还需要在树状数组中的 $f[i]$ 位置增加 $cnt$,确保下次查询相同字符结尾长度不小于 $f[i]$ 的已覆盖子串数量时不会出错。

至此,我们通过「树状数组」+「记录同字符最大长度」的方式来分别解决「长度比 $f[i]$ 小」和「长度比 $f[i]$ 大」的重复子串统计问题。

代码:
```Java
class Solution {
    int N = 100010;
    int[][] trs = new int[26][N];
    int[] f = new int[N], max = new int[26];
    int n, ans;
    int lowbit(int x) {
        return x & -x;
    }
    void add(int[] tr, int x, int v) {
        for (int i = x; i <= n + 1; i += lowbit(i)) tr[i] += v;
    }
    int query(int[] tr, int x) {
        int ans = 0;
        for (int i = x; i > 0; i -= lowbit(i)) ans += tr[i];
        return ans;
    }
    public int findSubstringInWraproundString(String _p) {
        char[] cs = _p.toCharArray();
        n = cs.length;
        for (int i = 0; i < n; i++) {
            int c = cs[i] - 'a';
            if (i == 0) {
                f[i] = 1;
            } else {
                int p = cs[i - 1] - 'a';
                if ((c == 0 && p == 25) || p + 1 == c) f[i] = f[i - 1] + 1;
                else f[i] = 1;
            }
            if (max[c] >= f[i]) continue;
            int cnt = f[i] - query(trs[c], f[i]);
            if (cnt == 0) continue;
            ans += cnt;
            add(trs[c], f[i], cnt);
            max[c] = f[i];
        }
        return ans;
    }
}
```
* 时间复杂度:$O(n\log{n})$
* 空间复杂度:$O(C \times n)$,其中 $C = 26$ 为字符串 `p` 的字符集大小

---

### 线性 DP

对于相同的结尾字符 $c$ 而言,如果在整个动规过程中的最大长度为 $len$,那么以 $c$ 为结尾字符对答案的贡献为 $len$。

基于此,我们只需保留解法一中的 `max` 数组即可,同时利用 $f[i]$ 只依赖于 $f[i - 1]$ 进行更新,因此动规数组也可以使用一个变量来代替。

代码:
```Java
class Solution {
    public int findSubstringInWraproundString(String _p) {
        char[] cs = _p.toCharArray();
        int n = cs.length, ans = 0;
        int[] max = new int[26];
        max[cs[0] - 'a']++;
        for (int i = 1, j = 1; i < n; i++) {
            int c = cs[i] - 'a', p = cs[i - 1] - 'a';
            if ((p == 25 && c == 0) || p + 1 == c) j++;
            else j = 1;
            max[c] = Math.max(max[c], j);
        }
        for (int i = 0; i < 26; i++) ans += max[i];
        return ans;
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(C)$,其中 $C = 26$ 为字符串 `p` 的字符集大小

### 最后

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

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

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

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