Question:
有台奇怪的打印机有以下两个特殊要求:

* 打印机每次只能打印由 同一个字符 组成的序列。
* 每次可以在任意起始和结束位置打印新字符,并且会覆盖掉原来已有的字符。

给你一个字符串 `s`,你的任务是计算这个打印机打印它需要的最少打印次数。

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

输出:2

解释:首先打印 "aaa" 然后打印 "bbb"。
```
示例 2:
```
输入:s = "aba"

输出:2

解释:首先打印 "aaa" 然后在第二个位置打印 "b" 覆盖掉原来的字符 'a'。
```

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

Solution:
### 基本分析

首先,根据题意我们可以分析出一个重要推论:**连续相同的一段字符,必然可以归到同一次打印中,而不会让打印次数变多。注意,这里说的是「归到一次」,而不是说「单独作为一次」。**

怎么理解这句话呢?

举个 🌰,对于诸如 `...bbaaabb...` 的样例数据,其中多个连续的 `a` 必然可以归到同一次打印中,但这一次打印可能只是将 `aaa` 作为整体进行打印;也有可能是 `aaa` 与前面或者后面的 `a` 作为整体被打印(然后中间的 `b` 被后来的打印所覆盖)。但无论是何种情况连续一段的 `aaa` 必然是可以「归到同一次打印」中。

**我们可以不失一般性证明「连续相同的一段字符,必然可以归到同一次打印中,而不会让打印次数变多」这个推理是否正确:**

假设有目标序列 $[...,ai,...,aj,...]$ 其中 $[i, j]$ 连续一段字符相同,假如这一段的打印被最后完成(注意最后完成不代表这一段要保留空白,这一段可以此前被打印多次),除了这一段以外所消耗的打印次数为 $x$,那么根据 $[i, j]$ 不同的打印方案有:

1. 将 $[i, j]$ 单纯划分为多段:总共打印的次数大于 $x + 1$(此方案不会取到打印最小值 $x + 1$,可忽略)
2. 将 $[i, j]$ 归到同一次打印:总共打印的次数等于 $x + 1$
3. 将 $[i, j]$ 结合之前的打印划分为多段,即 $[i, j]$ 一段的两段本身就是「目标字符」,我们本次只需要打印 $[i, j]$ 中间的部分。总共打印的次数等于 $x + 1$

由于同样的地方可以被重复打印,因此我们可以将情况 $3$ 中打印边缘扩展到 $i$ 和 $j$ 处,这样最终打印结果不变,而且总的打印次数没有增加。

到这一步,我们其实已经证明出「连续相同的一段字符,必然可以归到同一次打印中,而不会让打印次数变多」的推论成立了。

但可能会有同学提出疑问:怎么保证 $[i, j]$ 是被最后涂的?怎么保证 $[i, j]$ 不是和其他「不相邻的同样字符」一起打印的?

**答案是不用保证,因为不同状态(打印结果)之间相互独立,而有明确的最小转移成本。即从当前打印结果 `a` 变成打印结果 `b`,是具有明确的最小打印次数的(否则本题无解)。因此我们上述的分析可以看做任意两个中间状态转移的“最后一步”,而且不会整体的结果。**

对应到本题,题目给定的起始状态是空白字符串 `a`,目标状态是入参字符串 `s`。那么真实最优解中,从 `a` 状态到 `s` 状态中间可能会经过任意个中间状态,假设有两个中间状态 `p` 和 `q`,那么我们上述的分析就可以应用到中间状态 `p` 到 `q` 的转移中,可以令得 `p` 到 `q` 转移所花费的转移成本最低(最优),同时这个转移不会影响「`a` 到 `p` 的转移」和「`q` 到 `s` 的转移」,是相互独立的。

因此这个分析可以推广到真实最优转移路径中的任意一步,是一个具有一般性的结论。

上述分析是第一个切入点,第二个切入点是「重复打印会进行覆盖」,这意味着我们其实不需要确保 $[i,j]$ 这一段在目标字符串中完全相同,而只需要 $s[i] = s[j]$ 相同即可,即后续打印不会从边缘上覆盖 $[i,j]$ 区间的原有打印,否则 $[i,j]$ 这一段的打印就能用范围更小的区间所代替。

这样就引导出我们状态转移的关键:状态转移之间只需要确保首位字符相同。

---

### 动态规划

定义 $f[l][r]$ 为将 $[l, r]$ 这一段打印成目标结果所消耗的最小打印次数。

不失一般性考虑 $f[l][r]$ 该如何转移:

* 只染 $l$ 这个位置,此时 $f[l][r] = f[l + 1][r] + 1$
* 不只染 $l$ 这个位置,而是从 $l$ 染到 $k$(需要确保首位相同 $s[l] = s[k]$):$f[l][r] = f[l][k - 1] + f[k + 1][r], l < k <= r$

其中状态转移方程中的情况 $2$ 需要说明一下:由于我们只确保 $s[l] = s[k]$,并不确保 $[l, k]$ 之间的字符相同,根据我们基本分析可知,$s[k]$ 这个点可由打印 $s[l]$ 的时候一同打印,因此本身 $s[k]$ 并不独立消耗打印次数,所以这时候 $[l, k]$ 这一段的最小打印次数应该取 $f[l][k - 1]$,而不是 $f[l][k]$。

最终的 $f[l][r]$ 为上述所有方案中取 $min$。

代码:
```Java
class Solution {
    public int strangePrinter(String s) {
        int n = s.length();
        int[][] f = new int[n + 1][n + 1];
        for (int len = 1; len <= n; len++) {
            for (int l = 0; l + len - 1 < n; l++) {
                int r = l + len - 1;
                f[l][r] = f[l + 1][r] + 1;
                for (int k = l + 1; k <= r; k++) {
                    if (s.charAt(l) == s.charAt(k)) {
                        f[l][r] = Math.min(f[l][r], f[l][k - 1] + f[k + 1][r]);
                    }
                }
            }
        }
        return f[0][n - 1];
    }
}
```
* 时间复杂度:$O(n^3)$
* 空间复杂度:$O(n^2)$

---

### 总结

这道题的原型应该出自 [String painter](http://acm.hdu.edu.cn/showproblem.php?pid=2476)。

如果只是为了把题做出来,难度不算特别大,根据数据范围 $10^2$,可以猜到是 $O(n^3)$ 做法,通常就是区间 DP 的「枚举长度 + 枚举左端点 + 枚举分割点」的三重循环。

但是要搞懂为啥可以这样做,还是挺难,大家感兴趣的话可以好好想想 ~  🤣

### 最后

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

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

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

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


Question:
给你两个整数 `n` 和 `k` ,请你构造一个答案列表 `answer`,该列表应当包含从 `1` 到 `n` 的 `n` 个不同正整数,并同时满足下述条件:

假设该列表是 $answer = [a_1, a_2, a_3, ... , a_n]$ ,那么列表 $[|a_1 - a_2|, |a_2 - a_3|, |a_3 - a_4|, ... , |a_{n-1} - a_n|]$ 中应该有且仅有 k 个不同整数。

返回列表 `answer`。如果存在多种答案,只需返回其中 任意一种 。

示例 1:
```
输入:n = 3, k = 1

输出:[1, 2, 3]

解释:[1, 2, 3] 包含 3 个范围在 1-3 的不同整数,并且 [1, 1] 中有且仅有 1 个不同整数:1
```
示例 2:
```
输入:n = 3, k = 2

输出:[1, 3, 2]

解释:[1, 3, 2] 包含 3 个范围在 1-3 的不同整数,并且 [2, 1] 中有且仅有 2 个不同整数:1 和 2
```

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

Solution:
### 构造

给定范围在 $[1, n]$ 的 $n$ 个数,当「直接升序/降序」排列时,相邻项差值为 $1$,仅一种;而从首位开始按照「升序」间隔排列,次位开始按照「降序」间隔排列(即排列为 `[1, n, 2, n - 1, 3, ...]`)时,相邻差值会从 $n - 1$ 开始递减至 $1$,共 $n - 1$ 种。

那么当我们需要构造 $k$ 种序列时,我们可以先通过「直接升序」的方式构造出若干个 $1$,然后再通过「间隔位分别升降序」的方式构造出从 $k$ 到 $1$ 的差值,共 $k$ 个。

显然,我们需要 $k + 1$ 个数来构造出 $k$ 个差值。因此我们可以先从 $1$ 开始,使用 $n - (k + 1)$ 个数来直接升序(通过方式一构造出若干个 $1$),然后从 $n - k$ 开始间隔升序排列,按照 $n$ 开始间隔降序排列,构造出剩下的序列。

Java 代码:
```Java
class Solution {
    public int[] constructArray(int n, int k) {
        int[] ans = new int[n];
        int t = n - k - 1;
        for (int i = 0; i < t; i++) ans[i] = i + 1;
        for (int i = t, a = n - k, b = n; i < n; ) {
            ans[i++] = a++;
            if (i < n) ans[i++] = b--;
        }
        return ans;
    }
}
```
TypeScript 代码:
```TypeScript
function constructArray(n: number, k: number): number[] {
    const ans = new Array<number>(n).fill(0)
    const t = n - k - 1
    for (let i = 0; i < t; i++) ans[i] = i + 1
    for (let i = t, a = n - k, b = n; i < n; ) {
        ans[i++] = a++
        if (i < n) ans[i++] = b--
    }
    return ans
};
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


668. 乘法表中第k小的数

Question:
几乎每一个人都用 乘法表,但是你能在乘法表中快速找到第 $k$ 小的数字吗?

给定高度 $m$ 、宽度 $n$ 的一张 $m \times n$ 的乘法表,以及正整数 $k$,你需要返回表中第 $k$ 小的数字。

例 1:
```
输入: m = 3, n = 3, k = 5

输出: 3

解释:
乘法表:
1	2	3
2	4	6
3	6	9

第5小的数字是 3 (1, 2, 2, 3, 3).
```
例 2:
```
输入: m = 2, n = 3, k = 6

输出: 6

解释:
乘法表:
1	2	3
2	4	6

第6小的数字是 6 (1, 2, 2, 3, 4, 6).
```
注意:
* $m$ 和 $n$ 的范围在 $[1, 30000]$ 之间。
* $k$ 的范围在 $[1, m \times n]$ 之间。

Solution:
### 二分 + 计数判定

由于 $n$ 和 $m$ 的数据范围都是 $3 \times 10^4$,总数本身就超过了 $10^7$,我们考虑线性复杂度往下的对数复杂度。

题目要求我们求一维展开有序序列中的第 $k$ 小的数,问题本身具有「二段性」:

* 答案右边的每个数均 **满足**「其在一维展开有序序列中左边数的个数大于等于 $k$ 个」
* 答案左边的每个数均 **不满足**「其在一维展开有序序列中左边数的个数大于等于 $k$ 个」

我们考虑如何进行「二分答案」: 假设当前我们二分到的值是 $mid$,对于乘法表中的每行和每列均是单调递增,我们可以通过累加统计 每行/每列 中比 $mid$ 小的数,记作 $a$,累加统计 每行/每列 中等于 $mid$ 的数,记作 $b$,那么 $cnt = a + b$ 即是整个乘法表中小于等于 $mid$ 的数的个数,再通过 $cnt$ 和 $k$ 的大小关系来指导左右指针的变化。

具体的,假设我们通过枚举行来统计 $a$ 和 $b$,当前枚举到的行号为 $i$(行号从 $1$ 开始),该行的最大数为 $i \times m$:

* 若 $i \times m < mid$,整行都是小于 $mid$ 的数,直接在 $a$ 基础上累加 $m$;
* 若 $i \times m >= mid$,根据 $mid$ 是否存在于该行进行分情况讨论:
    * $mid$ 能够被 $i$ 整除,说明 $mid$ 存在于该行,那么比 $mid$ 小的数的个数为 $\frac{mid}{i} - 1$,将其累加到 $a$,同时对 $b$ 进行加一;
    * $mid$ 不能够被 $i$ 整除,说明 $mid$ 不存在于该行,那么比 $mid$ 小的数的个数为 $\frac{mid}{i}$,将其累加到 $a$。

> 一些细节:由于乘法表具有对称性,我们统计时可以对 行和列 中较小的一方进行遍历。

代码:
```Java
class Solution {
    int n, m, k;
    public int findKthNumber(int _m, int _n, int _k) {
        n = Math.min(_m, _n); m = Math.max(_m, _n); k = _k;
        int l = 1, r = m * n;
        while (l < r) {
            int mid = l + r >> 1, cnt = getCnt(mid);
            if (cnt >= k) r = mid;
            else l = mid + 1;
        }
        return r;
    }
    int getCnt(int mid) {
        int a = 0, b = 0;
        for (int i = 1; i <= n; i++) {
            if (i * m < mid) {
                a += m;
            } else {
                if (mid % i == 0 && ++b >= 0) a += mid / i - 1;
                else a += mid / i;
            }
        }
        return a + b;
    }
}
```
* 时间复杂度:$O(\min(n, m) \times \log{nm})$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


669. 修剪二叉搜索树

Question:
给你二叉搜索树的根节点 `root`,同时给定最小边界 `low` 和最大边界 `high`。通过修剪二叉搜索树,使得所有节点的值在 $[low, high]$ 中。修剪树 不应该 改变保留在树中的元素的相对结构 (即,如果没有被移除,原有的父代子代关系都应当保留)。 可以证明,存在 唯一的答案 。

所以结果应当返回修剪好的二叉搜索树的新的根节点。注意,根节点可能会根据给定的边界发生改变。

示例 1:

```
输入:root = [1,0,2], low = 1, high = 2

输出:[1,null,2]
```
示例 2:

```
输入:root = [3,0,4,null,2,null,null,1], low = 1, high = 3

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

提示:
* 树中节点数在范围 $[1, 10^4]$ 内
* $0 <= Node.val <= 10^4$
* 树中每个节点的值都是 唯一 的
* 题目数据保证输入是一棵有效的二叉搜索树
* $0 <= low <= high <= 10^4$

Solution:
### 递归

由于被修剪的是二叉搜索树,因此修剪过程必然能够顺利进行。

容易想到使用原函数作为递归函数:

* 若 `root.val` 小于边界值 `low`,则 `root` 的左子树必然均小于边界值,我们递归处理 `root.right` 即可;
* 若 `root.val` 大于边界值 `high`,则 `root` 的右子树必然均大于边界值,我们递归处理 `root.left` 即可;
* 若 `root.val` 符合要求,则 `root` 可被保留,递归处理其左右节点并重新赋值即可。

Java 代码:
```Java
class Solution {
    public TreeNode trimBST(TreeNode root, int low, int high) {
        if (root == null) return null;
        if (root.val < low) return trimBST(root.right, low, high);
        else if (root.val > high) return trimBST(root.left, low, high);
        root.left = trimBST(root.left, low, high);
        root.right = trimBST(root.right, low, high);
        return root;
    }
}
```
TypeScript 代码:
```TypeScript
function trimBST(root: TreeNode | null, low: number, high: number): TreeNode | null {
    if (root == null) return null
    if (root.val < low) return trimBST(root.right, low, high)
    else if (root.val > high) return trimBST(root.left, low, high)
    root.left = trimBST(root.left, low, high)
    root.right = trimBST(root.right, low, high)
    return root
};
```
* 时间复杂度:$O(n)$
* 空间复杂度:忽略递归带来的额外空间开销,复杂度为 $O(1)$

---

### 迭代

自然能够使用「迭代」进行求解:起始先从给定的 `root` 进行出发,找到第一个满足值符合 $[low, high]$ 范围的节点,该节点为最后要返回的根节点 `ans`。

随后考虑如何修剪 `ans` 的左右节点:当根节点符合 $[low, high]$ 要求时,修剪左右节点过程中仅需考虑一边的边界值即可。即对于 `ans.left` 只需考虑将值小于 `low` 的节点去掉(因为二叉搜索树的特性,`ans` 满足不大于 `high` 要求,则其左节点必然满足);同理 `ans.right` 只需要考虑将大于 `high` 的节点去掉即可。

Java 代码:
```Java
class Solution {
    public TreeNode trimBST(TreeNode root, int low, int high) {
        while (root != null && (root.val < low || root.val > high)) root = root.val < low ? root.right : root.left;
        TreeNode ans = root;
        while (root != null) {
            while (root.left != null && root.left.val < low) root.left = root.left.right;
            root = root.left;
        }
        root = ans;
        while (root != null) {
            while (root.right != null && root.right.val > high) root.right = root.right.left;
            root = root.right;
        }
        return ans;
    }
}
```
TypeScript 代码:
```TypeScript
function trimBST(root: TreeNode | null, low: number, high: number): TreeNode | null {
    while (root != null && (root.val < low || root.val > high)) root = root.val < low ? root.right : root.left
    const ans = root
    while (root != null) {
        while (root.left != null && root.left.val < low) root.left = root.left.right
        root = root.left
    }
    root = ans
    while (root != null) {
        while (root.right != null && root.right.val > high) root.right = root.right.left
        root = root.right
    }
    return ans
};
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


Question:
给定一个非负整数,你至多可以交换一次数字中的任意两位。

返回你能得到的最大值。

示例 1 :
```
输入: 2736

输出: 7236

解释: 交换数字2和数字7。
```
示例 2 :
```
输入: 9973

输出: 9973

解释: 不需要交换。
```
注意:
* 给定数字的范围是 $[0, 10^8]$

Solution:
### 模拟

根据题意,我们应当将大的数放置在高位,而当有数值相同的多个大数时,我们应当选择低位的数字。

因此,我们可以先将 `num` 的每一位处理出来存放到数组 `list` 中,随后预处理一个与 `list` 等长的数组 `idx`,带来代指 `num` 后缀中最大值对应的下标,即当 `idx[i] = j` 含义为在下标为 $[0, i]$ 位中 $num[j]$ 对应的数值最大。

同时由于我们需要遵循「当有数值相同的多个大数时,选择低位的数字」原则,我们应当出现采取严格大于才更新的方式来预处理 `idx`。

最后则是从高位往低位遍历,找到第一个替换的位置进行交换,并重新拼凑回答案。

Java 代码:
```Java
class Solution {
    public int maximumSwap(int num) {
        List<Integer> list = new ArrayList<>();
        while (num != 0) {
            list.add(num % 10); num /= 10;
        }
        int n = list.size(), ans = 0;
        int[] idx = new int[n];
        for (int i = 0, j = 0; i < n; i++) {
            if (list.get(i) > list.get(j)) j = i;
            idx[i] = j;
        }
        for (int i = n - 1; i >= 0; i--) {
            if (list.get(idx[i]) != list.get(i)) {
                int c = list.get(idx[i]);
                list.set(idx[i], list.get(i));
                list.set(i, c);
                break;
            }
        }
        for (int i = n - 1; i >= 0; i--) ans = ans * 10 + list.get(i);
        return ans;
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    int maximumSwap(int num) {
        vector<int> list;
        while (num != 0) {
            list.push_back(num % 10); num /= 10;
        }
        int n = list.size(), ans = 0;
        int* idx = new int[n];
        for (int i = 0, j = 0; i < n; i++) {
            if (list[i] > list[j]) j = i;
            idx[i] = j;
        }
        for (int i = n - 1; i >= 0; i--) {
            if (list[idx[i]] != list[i]) {
                int c = list[idx[i]];
                list[idx[i]] = list[i];
                list[i] = c;
                break;
            }
        }
        for (int i = n - 1; i >= 0; i--) ans = ans * 10 + list[i];
        return ans;
    }
};
```
Python 代码:
```Python
class Solution:
    def maximumSwap(self, num: int) -> int:
        nums = []
        while num != 0:
            nums.append(num % 10); num //= 10
        n, ans = len(nums), 0
        idx = [0] * n
        i, j = 0, 0
        while i < n:
            if nums[i] > nums[j]: j = i
            idx[i] = j
            i += 1
        for i in range(n - 1, -1, -1):
            if nums[idx[i]] != nums[i]:
                c = nums[idx[i]]
                nums[idx[i]] = nums[i]
                nums[i] = c
                break
        for i in range(n - 1, -1, -1):
            ans = ans * 10 + nums[i]
        return ans
```
TypeScript 代码:
```TypeScript
function maximumSwap(num: number): number {
    const list = new Array<number>()
    while (num != 0) {
        list.push(num % 10)
        num = Math.floor(num / 10)
    }
    let n = list.length, ans = 0
    const idx = new Array<number>()
    for (let i = 0, j = 0; i < n; i++) {
        if (list[i] > list[j]) j = i
        idx.push(j)
    }
    for (let i = n - 1; i >= 0; i--) {
        if (list[idx[i]] != list[i]) {
            const c = list[idx[i]]
            list[idx[i]] = list[i]
            list[i] = c
            break
        }
    }
    for (let i = n - 1; i >= 0; i--) ans = ans * 10 + list[i];
    return ans
};
```
* 时间复杂度:$O(\log{num})$
* 空间复杂度:$O(\log{num})$

### 最后

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

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

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

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


671. 二叉树中第二小的节点

Question:
给定一个非空特殊的二叉树,每个节点都是正数,并且每个节点的子节点数量只能为 2 或 0。如果一个节点有两个子节点的话,那么该节点的值等于两个子节点中较小的一个。

更正式地说,root.val = min(root.left.val, root.right.val) 总成立。

给出这样的一个二叉树,你需要输出所有节点中的第二小的值。如果第二小的值不存在的话,输出 -1 。

示例 1:

```
输入:root = [2,2,5,null,null,5,7]

输出:5

解释:最小的值是 2 ,第二小的值是 5 。
```
示例 2:

```
输入:root = [2,2,2]

输出:-1

解释:最小的值是 2, 但是不存在第二小的值。
```
提示:
* 树中节点数目在范围 $[1, 25]$ 内
* $1$ <= Node.val <= $2^{31}$ - 1
* 对于树中每个节点 root.val == min(root.left.val, root.right.val)

Solution:
### 树的遍历

一个朴素的做法是,直接对树进行遍历(广度 & 深度),使用 `HashSet` 进行存储,得到所有去重后的节点大小。

然后找次小值的方式有多种:可以通过排序找次小值,复杂度为 $O(n\log{n})$;也可以使用经典的两个变量 & 一次遍历的方式,找到次小值,复杂度为 $O(n)$。

代码:
```Java
class Solution {
    Set<Integer> set = new HashSet<>();
    public int findSecondMinimumValue(TreeNode root) {
        dfs(root);
        if (set.size() < 2) return -1;
        int first = Integer.MAX_VALUE, second = Integer.MAX_VALUE;
        for (int i : set) {
            if (i <= first) {
                second = first;
                first = i;
            } else if (i <= second) {
                second = i;
            }
        }
        return second;
    }
    void dfs(TreeNode root) {
        if (root == null) return;
        set.add(root.val);
        dfs(root.left);
        dfs(root.right);
    }
}
```
```Java
class Solution {
    Set<Integer> set = new HashSet<>();
    public int findSecondMinimumValue(TreeNode root) {
        bfs(root);
        if (set.size() < 2) return -1;
        int first = Integer.MAX_VALUE, second = Integer.MAX_VALUE;
        for (int i : set) {
            if (i <= first) {
                second = first;
                first = i;
            } else if (i <= second) {
                second = i;
            }
        }
        return second;
    }
    void bfs(TreeNode root) {
        Deque<TreeNode> d = new ArrayDeque<>();
        d.addLast(root);
        while (!d.isEmpty()) {
            TreeNode poll = d.pollFirst();
            set.add(poll.val);
            if (poll.left != null) d.addLast(poll.left);
            if (poll.right != null) d.addLast(poll.right);
        }
    }
}
```
* 时间复杂度:树的搜索复杂度为 $O(n)$,通过线性遍历找次小值,复杂度为 $O(n)$。整体复杂度为 $O(n)$
* 空间复杂度:$O(n)$

---

### 递归

解法一显然没有利用到本题核心条件 :「`root.val = min(root.left.val, root.right.val)`」和「每个子节点数量要么是 `0` 要么是 `2`」。

我们可以设计如下递归函数,含义为 **从 `root` 为根的树进行搜索,找到值比 `cur` 大的最小数**。然后使用全局变量 `ans` 存储答案。
```Java
void dfs(TreeNode root, int cur)
```

那么最终搜索范围为 `dfs(root, root.val)`,这是因为 **性质 `root.val = min(root.left.val, root.right.val)`,即最小值会不断往上传递,最终根节点必然是全局最小值**。

然后再结合「每个子节点数量要么是 `0` 要么是 `2`」,我们可以特判一下 `ans` 是否为第一次赋值,如果给 `ans` 赋了新值或者更新了更小的 `ans`,则不再需要往下搜索了。

代码:
```Java
class Solution {
    int ans = -1;
    public int findSecondMinimumValue(TreeNode root) {
        dfs(root, root.val);
        return ans;
    }
    void dfs(TreeNode root, int cur) {
        if (root == null) return ;
        if (root.val != cur) {
            if (ans == -1) ans = root.val;
            else ans = Math.min(ans, root.val);
            return ;
        }
        dfs(root.left, cur);
        dfs(root.right, cur);
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:忽略递归带来的空间开销。复杂度为 $O(1)$

### 最后

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

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

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

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


Question:
房间中有 `n` 只已经打开的灯泡,编号从 `1` 到 `n` 。

墙上挂着 `4` 个开关 ,这 `4` 个开关各自都具有不同的功能,其中:

* 开关 1 :反转当前所有灯的状态(即开变为关,关变为开)
* 开关 2 :反转编号为偶数的灯的状态(即 `2, 4, ...`)
* 开关 3 :反转编号为奇数的灯的状态(即 `1, 3, ...`)
* 开关 4 :反转编号为 `j = 3k + 1` 的灯的状态,其中 `k = 0, 1, 2, ...`(即 `1, 4, 7, 10, ...`)

你必须 恰好 按压开关 `presses` 次。每次按压,你都需要从 `4` 个开关中选出一个来执行按压操作。

给你两个整数 `n` 和 `presses`,执行完所有按压之后,返回 不同可能状态 的数量。

示例 1:
```
输入:n = 1, presses = 1

输出:2

解释:状态可以是:
- 按压开关 1 ,[关]
- 按压开关 2 ,[开]
```
示例 2:
```
输入:n = 2, presses = 1

输出:3

解释:状态可以是:
- 按压开关 1 ,[关, 关]
- 按压开关 2 ,[开, 关]
- 按压开关 3 ,[关, 开]
```
示例 3:
```
输入:n = 3, presses = 1

输出:4

解释:状态可以是:
- 按压开关 1 ,[关, 关, 关]
- 按压开关 2 ,[关, 开, 关]
- 按压开关 3 ,[开, 关, 开]
- 按压开关 4 ,[关, 开, 开]
```

提示:
* $1 <= n <= 1000$
* $0 <= presses <= 1000$

Solution:
### 分情况讨论

记灯泡数量为 $n$(至少为 $1$),翻转次数为 $k$(至少为 $0$),使用 `1` 代表灯亮,使用 `0` 代表灯灭。

我们根据 $n$ 和 $k$ 的数值分情况讨论:

* 当 $k = 0$ 时,无论 $n$ 为何值,都只有起始(全 `1`)一种状态;
* 当 $k > 0$ 时,根据 $n$ 进一步分情况讨论:
    * 当 $n = 1$ 时,若 $k$ 为满足「$k > 0$」的最小值 $1$ 时,能够取满「`1`/`0`」两种情况,而其余更大 $k$ 值情况能够使用操作无效化(不影响灯的状态);
    * 当 $n = 2$ 时,若 $k = 1$,能够取得「`00`/`10`/`01`」三种状态,当 $k = 2$ 时,能够取满「`11`/`10`/`01`/`00`」四种状态,其余更大 $k$ 可以通过前 $k - 1$ 步归结到任一状态,再通过最后一次的操作 $1$ 归结到任意状态;
    * 当 $n = 3$ 时,若 $k = 1$ 时,对应 $4$ 种操作可取得 $4$ 种方案;当 $k = 2$ 时,可取得 $7$ 种状态;而当 $k = 3$ 时可取满 $2^3 = 8$ 种状态,更大的 $k$ 值可通过同样的方式归结到取满的 $8$ 种状态。
    * 当 $n > 3$ 时,根据四类操作可知,灯泡每 $6$ 组一循环(对应序列 `k + 1`、`2k + 2`、`2k + 1` 和 `3k + 1`),即只需考虑 $n <= 6$ 的情况,而 $n = 4$、$n = 5$ 和 $n = 6$ 时,后引入的灯泡状态均不会产生新的组合(即新引入的灯泡状态由前三个灯泡的状态所唯一确定),因此均可归纳到 $n = 3$ 的情况。

Java 代码:
```Java
class Solution {
    public int flipLights(int n, int k) {
        if (k == 0) return 1;
        if (n == 1) return 2;
        else if (n == 2) return k == 1 ? 3 : 4;
        else return k == 1 ? 4 : k == 2 ? 7 : 8;
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    int flipLights(int n, int k) {
        if (k == 0) return 1;
        if (n == 1) return 2;
        else if (n == 2) return (k == 1) ? 3 : 4;
        else return (k == 1) ? 4 : (k == 2) ? 7 : 8;
    }
};
```
Python 3 代码:
```Python
class Solution:
    def flipLights(self, n: int, k: int) -> int:
        if k == 0:
            return 1
        if n == 1:
            return 2
        elif n == 2:
            return 3 if k == 1 else 4
        else:
            return 4 if k == 1 else 7 if k == 2 else 8
```
TypeScript 代码:
```TypeScript
function flipLights(n: number, k: number): number {
    if (k == 0) return 1
    if (n == 1) return 2
    else if (n == 2) return k == 1 ? 3 : 4;
    else return k == 1 ? 4 : k == 2 ? 7 : 8;
};
```
* 时间复杂度:$O(1)$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


673. 最长递增子序列的个数

Question:
给定一个未排序的整数数组,找到最长递增子序列的个数。

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

输出: 2

解释: 有两个最长递增子序列,分别是 [1, 3, 4, 7] 和[1, 3, 5, 7]。
```
示例 2:
```
输入: [2,2,2,2,2]

输出: 5

解释: 最长递增子序列的长度是1,并且存在5个子序列的长度为1,因此输出5。
注意: 给定的数组长度不超过 2000 并且结果一定是32位有符号整数。
```
提示:
* $1 <= nums.length <= 2000$
* $-10^6 <= nums[i] <= 10^6$

Solution:
### 序列 DP

与朴素的 LIS 问题(问长度)相比,本题问的是最长上升子序列的个数。

我们只需要在朴素 LIS 问题的基础上通过「记录额外信息」来进行求解即可。

在朴素的 LIS 问题中,我们定义 **$f[i]$ 为考虑以 $nums[i]$ 为结尾的最长上升子序列的长度。** 最终答案为所有 $f[0...(n - 1)]$ 中的最大值。

不失一般性地考虑 $f[i]$ 该如何转移:

* 由于每个数都能独自一个成为子序列,因此起始必然有 $f[i] = 1$;
* 枚举区间 $[0, i)$ 的所有数 $nums[j]$,如果满足 $nums[j] < nums[i]$,说明 $nums[i]$ 可以接在 $nums[j]$ 后面形成上升子序列,此时使用 $f[j]$ 更新 $f[i]$,即有 $f[i] = f[j] + 1$。

回到本题,由于我们需要求解的是最长上升子序列的个数,因此需要额外定义 **$g[i]$ 为考虑以 $nums[i]$ 结尾的最长上升子序列的个数。**

结合 $f[i]$ 的转移过程,不失一般性地考虑 $g[i]$ 该如何转移:

* 同理,由于每个数都能独自一个成为子序列,因此起始必然有 $g[i] = 1$;
* 枚举区间 $[0, i)$ 的所有数 $nums[j]$,如果满足 $nums[j] < nums[i]$,说明 $nums[i]$ 可以接在 $nums[j]$ 后面形成上升子序列,这时候对 $f[i]$ 和 $f[j] + 1$ 的大小关系进行分情况讨论:
    * 满足 $f[i] < f[j] + 1$:说明 $f[i]$ 会被 $f[j] + 1$ 直接更新,此时同步直接更新 $g[i] = g[j]$ 即可;
    * 满足 $f[i] = f[j] + 1$:说明找到了一个新的符合条件的前驱,此时将值继续累加到方案数当中,即有 $g[i] += g[j]$。

在转移过程,我们可以同时记录全局最长上升子序列的最大长度 $max$,最终答案为所有满足 $f[i] = max$ 的 $g[i]$ 的累加值。

Java 代码:
```Java
class Solution {
    public int findNumberOfLIS(int[] nums) {
        int n = nums.length;
        int[] f = new int[n], g = new int[n];
        int max = 1;
        for (int i = 0; i < n; i++) {
            f[i] = g[i] = 1;
            for (int j = 0; j < i; j++) {
                if (nums[j] < nums[i]) {
                    if (f[i] < f[j] + 1) {
                        f[i] = f[j] + 1; g[i] = g[j];
                    } else if (f[i] == f[j] + 1) {
                        g[i] += g[j];
                    }
                }
            }
            max = Math.max(max, f[i]);
        }
        int ans = 0;
        for (int i = 0; i < n; i++) {
            if (f[i] == max) ans += g[i];
        }
        return ans;
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    int findNumberOfLIS(vector<int>& nums) {
        int n = nums.size();
        vector<int> f(n), g(n);
        int maxv = 1;
        for (int i = 0; i < n; i++) {
            f[i] = g[i] = 1;
            for (int j = 0; j < i; j++) {
                if (nums[j] < nums[i]) {
                    if (f[i] < f[j] + 1) {
                        f[i] = f[j] + 1;
                        g[i] = g[j];
                    } else if (f[i] == f[j] + 1) {
                        g[i] += g[j];
                    }
                }
            }
            maxv = max(maxv, f[i]);
        }
        int ans = 0;
        for (int i = 0; i < n; i++) {
            if (f[i] == maxv) ans += g[i];
        }
        return ans;
    }
};
```
Python 代码:
```Python
class Solution:
    def findNumberOfLIS(self, nums: List[int]) -> int:
        n = len(nums)
        f, g = [1] * n, [1] * n
        maxv = 1
        for i in range(1, n):
            for j in range(i):
                if nums[j] < nums[i]:
                    if f[i] < f[j] + 1:
                        f[i], g[i] = f[j] + 1, g[j]
                    elif f[i] == f[j] + 1:
                        g[i] += g[j]
            maxv = max(maxv, f[i])
        ans = 0
        for i in range(n):
            if f[i] == maxv: ans += g[i]
        return ans
```
TypeScript 代码:
```TypeScript
function findNumberOfLIS(nums: number[]): number {
    const n = nums.length;
    let f = new Array(n).fill(1), g = new Array(n).fill(1);
    let max = 1;
    for (let i = 1; i < n; i++) {
        for (let j = 0; j < i; j++) {
            if (nums[j] < nums[i]) {
                if (f[i] < f[j] + 1) {
                    f[i] = f[j] + 1;
                    g[i] = g[j];
                } else if (f[i] == f[j] + 1) {
                    g[i] += g[j];
                }
            }
        }
        max = Math.max(max, f[i]);
    }
    let ans = 0;
    for (let i = 0; i < n; i++) {
        if (f[i] == max) ans += g[i];
    }
    return ans;
};
```
* 时间复杂度:$O(n^2)$
* 空间复杂度:$O(n)$

---

### LIS 问题的贪心解 + 树状数组

我们知道,对于朴素的 LIS 问题存在贪心解法,能够在 $O(n\log{n})$ 复杂度内求解 LIS 问题。

在贪心解中,我们会多开一个贪心数组 $q$,用来记录长度为 $len$ 的最长上升子序列的「最小结尾元素」为何值:**$q[len] = x$ 代表长度为 $len$ 的最长上升子序列的最小结尾元素为 $x$。**

**可以证明 $q$ 存在单调性,因此每次确定 $nums[i]$ 可以接在哪个 $nums[j]$ 后面会形成最长上升子序列时,可以通过「二分」来找到满足 $nums[j] < nums[i]$ 的最大下标来实现。**

对于本题,由于我们需要求最长上升子序列的个数,单纯使用一维的贪心数组记录最小结尾元素并不足以。

考虑对其进行扩展,期望能取到「最大长度」的同时,能够知道这个「最大长度」对应多少个子序列数量,同时期望该操作复杂度为 $O(\log{n})$。

我们可以使用「树状数组」维护二元组 $(len, cnt)$ 信息:

1. 因为数据范围较大($-10^6 <= nums[i] <= 10^6$),但数的个数为 $2000$,因此第一步先对 $nums$ 进行离散化操作;
2. 在遍历 $nums$ 时,每次从树状数组中查询值严格小于 $nums[i]$ 离散值(利用 $nums[i]$ 离散化后的值仍为正整数,我们可以直接查询小于等于 $nums[i]$ 离散值 $-1$ 的值)的最大长度,及最大长度对应的数量;
3. 对于流程 $2$ 中查得的 $(len, cnt)$,由于 $nums[i]$ 可以接在其后,因此首先长度加一,同时数量将 $cnt$ 累加到该离散值中。

Java 代码:
```Java
class Solution {
    int n;
    int[][] tr = new int[2010][2];
    int lowbit(int x) {
        return x & -x;
    }
    int[] query(int x) {
        int len = 0, cnt = 0;
        for (int i = x; i > 0; i -= lowbit(i)) {
            if (len == tr[i][0]) {
                cnt += tr[i][1];
            } else if (len < tr[i][0]) {
                len = tr[i][0];
                cnt = tr[i][1];
            }
        }
        return new int[]{len, cnt};
    }
    void add(int x, int[] info) {
        for (int i = x; i <= n; i += lowbit(i)) {
            int len = tr[i][0], cnt = tr[i][1];
            if (len == info[0]) {
                cnt += info[1];
            } else if (len < info[0]) {
                len = info[0];
                cnt = info[1];
            }
            tr[i][0] = len; tr[i][1] = cnt;
        }
    }
    public int findNumberOfLIS(int[] nums) {
        n = nums.length;
        // 离散化
        int[] tmp = nums.clone();
        Arrays.sort(tmp);
        Map<Integer, Integer> map = new HashMap<>();
        for (int i = 0, idx = 1; i < n; i++) {
            if (!map.containsKey(tmp[i])) map.put(tmp[i], idx++);
        }
        // 树状数组维护 (len, cnt) 信息
        for (int i = 0; i < n; i++) {
            int x = map.get(nums[i]);
            int[] info = query(x - 1);
            int len = info[0], cnt = info[1];
            add(x, new int[]{len + 1, Math.max(cnt, 1)});
        }
        int[] ans = query(n);
        return ans[1];
    }
}
```
* 时间复杂度:$O(n\log{n})$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


675. 为高尔夫比赛砍树

Question:
你被请来给一个要举办高尔夫比赛的树林砍树。树林由一个 $m \times n$ 的矩阵表示, 在这个矩阵中:

* $0$ 表示障碍,无法触碰
* $1$ 表示地面,可以行走
* 比 $1$ 大的数 表示有树的单元格,可以行走,数值表示树的高度

每一步,你都可以向上、下、左、右四个方向之一移动一个单位,如果你站的地方有一棵树,那么你可以决定是否要砍倒它。

你需要按照树的高度从低向高砍掉所有的树,每砍过一颗树,该单元格的值变为 $1$(即变为地面)。

你将从 $(0, 0)$ 点开始工作,返回你砍完所有树需要走的最小步数。 如果你无法砍完所有的树,返回 $-1$ 。

可以保证的是,没有两棵树的高度是相同的,并且你至少需要砍倒一棵树。

示例 1:

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

输出:6

解释:沿着上面的路径,你可以用 6 步,按从最矮到最高的顺序砍掉这些树。
```
示例 2:

```
输入:forest = [[1,2,3],[0,0,0],[7,6,5]]

输出:-1

解释:由于中间一行被障碍阻塞,无法访问最下面一行中的树。
```
示例 3:
```
输入:forest = [[2,3,4],[0,0,5],[8,7,6]]

输出:6

解释:可以按与示例 1 相同的路径来砍掉所有的树。
(0,0) 位置的树,可以直接砍去,不用算步数。
```

提示:
* $m == forest.length$
* $n == forest[i].length$
* $1 <= m, n <= 50$
* $0 <= forest[i][j] <= 10^9$

Solution:
### 基本分析

基本题意为:给定一个 $n \times m$ 的矩阵,每次能够在当前位置往「四联通」移动一个单位,其中 $0$ 的位置代表障碍(无法访问),$1$ 的位置代表平地(可直接访问,且无须进行任何决策),其余大于 $1$ 的位置代表有树,经过该位置的时候可以考虑将树砍掉(相应值变为平地 $1$)。

同时题目限定了我们只能按照「从低到高」的顺序进行砍树,并且图中不存在高度相等的两棵树,这意味着 **整个砍树的顺序唯一确定,就是对所有有树的地方进行「高度」排升序,即是完整的砍树路线。**

而另外一个更为重要的性质是:**点与点之间的最短路径,不会随着砍树过程的进行而发生变化(某个树点被砍掉,只会变为平地,不会变为阻碍点,仍可通过)。**

综上,**砍树的路线唯一确定,当我们求出每两个相邻的砍树点最短路径,并进行累加即是答案(整条砍树路径的最少步数)**。

---

### BFS

因此,再结合数据范围只有 $50$,并且点与点之间边权为 $1$(每次移动算一步),我们可以直接进行 `BFS` 进行求解。

先对整张图进行一次遍历,预处理出所有的树点(以三元组 $(height, x, y)$ 的形式进行存储),并对其以 $height$ 排升序,得到唯一确定的砍树路径。

之后就是计算砍树路径中相邻点的最短距离,运用 `BFS` 求解任意两点的最短路径复杂度为 $O(n \times m)$,我们最多有 $n \times m$ 个树点,因此整体复杂度为 $O(n^2 * \times m^2)$。

求解相邻点的最短距离的部分也是整个算法的复杂度上界,数据范围只有 $50$,计算量不超过 $10^7$,可以过。

代码:
```Java
class Solution {
    int N = 50;
    int[][] g = new int[N][N];
    int n, m;
    List<int[]> list = new ArrayList<>();
    public int cutOffTree(List<List<Integer>> forest) {
        n = forest.size(); m = forest.get(0).size();
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < m; j++) {
                g[i][j] = forest.get(i).get(j);
                if (g[i][j] > 1) list.add(new int[]{g[i][j], i, j});
            }
        }
        Collections.sort(list, (a,b)->a[0]-b[0]);
        if (g[0][0] == 0) return -1;
        int x = 0, y = 0, ans = 0;
        for (int[] ne : list) {
            int nx = ne[1], ny = ne[2];
            int d = bfs(x, y, nx, ny);
            if (d == -1) return -1;
            ans += d;
            x = nx; y = ny;
        }
        return ans;
    }
    int[][] dirs = new int[][]{{0,1},{0,-1},{1,0},{-1,0}};
    int bfs(int X, int Y, int P, int Q) {
        if (X == P && Y == Q) return 0;
        boolean[][] vis = new boolean[n][m];
        Deque<int[]> d = new ArrayDeque<>();
        d.addLast(new int[]{X, Y});
        vis[X][Y] = true;
        int ans = 0;
        while (!d.isEmpty()) {
            int size = d.size();
            while (size-- > 0) {
                int[] info = d.pollFirst();
                int x = info[0], y = info[1];
                for (int[] di : dirs) {
                    int nx = x + di[0], ny = y + di[1];
                    if (nx < 0 || nx >= n || ny < 0 || ny >= m) continue;
                    if (g[nx][ny] == 0 || vis[nx][ny]) continue;
                    if (nx == P && ny == Q) return ans + 1;
                    d.addLast(new int[]{nx, ny});
                    vis[nx][ny] = true;
                }
            }
            ans++;
        }
        return -1;
    }
}
```
* 时间复杂度:预处理出所有树点的复杂度为 $O(n \times m)$,对树点进行排序的复杂度为 $O(nm \log{nm})$,最多有 $n \times m$ 个树点,对每两个相邻树点运用 `BFS`  求最短路的复杂度为 $O(n \times m)$,统计完整路径的复杂度为 $O(n^2 \times m^2)$
* 空间复杂度:$O(n \times m)$

---

### AStar 算法

**由于问题的本质是求最短路,同时原问题的边权为 $1$,因此套用其他复杂度比 `BFS` 高的最短路算法,对于本题而言是没有意义,但运用启发式搜索 AStar 算法来优化则是有意义。**

因为在 `BFS` 过程中,我们会无差别往「四联通」方向进行搜索,直到找到「当前树点的下一个目标位置」为止,而实际上,两点之间的最短路径往往与两点之间的相对位置相关。

举个 🌰,当前我们在位置 $S$,我们目标位置是 $T$,而 $T$ 在 $S$ 的右下方,此时我们应当优先搜索方向"往右下方"的路径,当无法从"往右下方"的路径到达 $T$,我们再考虑搜索其他大方向的路径:

如何设计这样带有优先级的搜索顺序,则是 AStar 算法「启发式函数」的设计过程,其本质是对应了对「最小步数」的估算,只有当我们确保「最小步数估算 $\leq $ 实际最小步数」,AStar 算法的正确性才得以保证。

因此我们往往会直接使用「理论最小步数」来作为启发式函数的,对于本题,可直接使用「曼哈顿距离」作为「理论最小步数」。

因此,如果我们是要从源点 $S$ 到汇点 $T$,并且当前位于中途点 $x$ 的话,点 $x$ 的最小步数估算包括两部分:**到点 $x$ 的实际步数 + 从点 $x$ 到点 $T$ 的理论最小步数(曼哈顿距离)**。使用「优先队列」按照「总的最小步数估算」进行出队,即可实现 AStar 算法的搜索顺序。

> **AStar 算法做过很多次了,相关合集可以在 [这里](https://sharingsource.github.io/tags/AStar-%E7%AE%97%E6%B3%95/) 看到。**
另外,网上很多对 AStar 正确性证明不了解的人,会缺少以下 `map.get(nidx) > step + 1` 判断逻辑。
简单来说,启发式函数的设计是针对汇点而言的,**因此 AStar 算法搜索过程只确保对 $T$ 的出入队次序能够对应回到点 $T$ 第 $k$ 短路,而对于其余点的出入队次序到其余点的最短路没有必然的对应关系,因此当某个点的最小步数被更新,我们是要将其进行再次入队的。**

代码:
```Java
class Solution {
    int N = 50;
    int[][] g = new int[N][N];
    int n, m;
    List<int[]> list = new ArrayList<>();
    public int cutOffTree(List<List<Integer>> forest) {
        n = forest.size(); m = forest.get(0).size();
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < m; j++) {
                g[i][j] = forest.get(i).get(j);
                if (g[i][j] > 1) list.add(new int[]{g[i][j], i, j});
            }
        }
        if (g[0][0] == 0) return -1;
        Collections.sort(list, (a,b)->a[0]-b[0]);
        int x = 0, y = 0, ans = 0;
        for (int[] ne : list) {
            int nx = ne[1], ny = ne[2];
            int d = astar(x, y, nx, ny);
            if (d == -1) return -1;
            ans += d;
            x = nx; y = ny;
        }
        return ans;
    }
    int[][] dirs = new int[][]{{0,1},{0,-1},{1,0},{-1,0}};
    int getIdx(int x, int y) {
        return x * m + y;
    }
    int f(int X, int Y, int P, int Q) {
        return Math.abs(X - P) + Math.abs(Y - Q);
    }
    int astar(int X, int Y, int P, int Q) {
        if (X == P && Y == Q) return 0;
        Map<Integer, Integer> map = new HashMap<>();
        PriorityQueue<int[]> q = new PriorityQueue<>((a,b)->a[0]-b[0]);
        q.add(new int[]{f(X, Y, P, Q), X, Y});
        map.put(getIdx(X, Y), 0);
        while (!q.isEmpty()) {
            int[] info = q.poll();
            int x = info[1], y = info[2], step = map.get(getIdx(x, y));
            for (int[] di : dirs) {
                int nx = x + di[0], ny = y + di[1], nidx = getIdx(nx, ny);
                if (nx < 0 || nx >= n || ny < 0 || ny >= m) continue;
                if (g[nx][ny] == 0) continue;
                if (nx == P && ny == Q) return step + 1;
                if (!map.containsKey(nidx) || map.get(nidx) > step + 1) {
                    q.add(new int[]{step + 1 + f(nx, ny, P, Q), nx, ny});
                    map.put(nidx, step + 1);
                }
            }
        }
        return -1;
    }
}
```
* 时间复杂度:启发式搜索分析时空复杂度意义不大
* 空间复杂度:启发式搜索分析时空复杂度意义不大

---

### AStar 算法 + 并查集预处理无解

我们知道,AStar 算法使用到了「优先队列(堆)」来进行启发式搜索,而对于一些最佳路径方向与两点相对位置相反(例如 $T$ 在 $S$ 的右边,但由于存在障碍,最短路径需要先从左边绕一圈才能到 $T$),AStar 反而会因为优先队列(堆)而多一个 $\log$ 的复杂度。

因此一个可行的优化是,我们先提前处理「无解」的情况,常见的做法是在预处理过程中运用「并查集」来维护连通性。

这种对于不影响复杂度上界的预处理相比后续可能出现的大量无效搜索(最终无解)的计算量而言,是有益的。

代码:
```Java
class Solution {
    int N = 50;
    int[][] g = new int[N][N];
    int n, m;
    int[] p = new int[N * N + 10];
    List<int[]> list = new ArrayList<>();
    void union(int a, int b) {
        p[find(a)] = p[find(b)];
    }
    boolean query(int a, int b) {
        return find(a) == find(b);
    }
    int find(int x) {
        if (p[x] != x) p[x] = find(p[x]);
        return p[x];
    }
    int getIdx(int x, int y) {
        return x * m + y;
    }
    public int cutOffTree(List<List<Integer>> forest) {
        n = forest.size(); m = forest.get(0).size();
        // 预处理过程中,同时使用「并查集」维护连通性
        for (int i = 0; i < n * m; i++) p[i] = i;
        int[][] tempDirs = new int[][]{{0,-1},{-1,0}};
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < m; j++) {
                g[i][j] = forest.get(i).get(j);
                if (g[i][j] > 1) list.add(new int[]{g[i][j], i, j});
                if (g[i][j] == 0) continue;
                // 只与左方和上方的区域联通即可确保不重不漏
                for (int[] di : tempDirs) {
                    int nx = i + di[0], ny = j + di[1];
                    if (nx < 0 || nx >= n || ny < 0 || ny >= m) continue;
                    if (g[nx][ny] != 0) union(getIdx(i, j), getIdx(nx, ny));
                }
            }
        }
        // 若不满足所有树点均与 (0,0),提前返回无解
        for (int[] info : list) {
            int x = info[1], y = info[2];
            if (!query(getIdx(0, 0), getIdx(x, y))) return -1;
        }
        Collections.sort(list, (a,b)->a[0]-b[0]);
        int x = 0, y = 0, ans = 0;
        for (int[] ne : list) {
            int nx = ne[1], ny = ne[2];
            int d = astar(x, y, nx, ny);
            if (d == -1) return -1;
            ans += d;
            x = nx; y = ny;
        }
        return ans;
    }
    int f(int X, int Y, int P, int Q) {
        return Math.abs(X - P) + Math.abs(Y - Q);
    }
    int[][] dirs = new int[][]{{0,1},{0,-1},{1,0},{-1,0}};
    int astar(int X, int Y, int P, int Q) {
        if (X == P && Y == Q) return 0;
        Map<Integer, Integer> map = new HashMap<>();
        PriorityQueue<int[]> q = new PriorityQueue<>((a,b)->a[0]-b[0]);
        q.add(new int[]{f(X, Y, P, Q), X, Y});
        map.put(getIdx(X, Y), 0);
        while (!q.isEmpty()) {
            int[] info = q.poll();
            int x = info[1], y = info[2], step = map.get(getIdx(x, y));
            for (int[] di : dirs) {
                int nx = x + di[0], ny = y + di[1], nidx = getIdx(nx, ny);
                if (nx < 0 || nx >= n || ny < 0 || ny >= m) continue;
                if (g[nx][ny] == 0) continue;
                if (nx == P && ny == Q) return step + 1;
                if (!map.containsKey(nidx) || map.get(nidx) > step + 1) {
                    q.add(new int[]{step + 1 + f(nx, ny, P, Q), nx, ny});
                    map.put(nidx, step + 1);
                }
            }
        }
        return -1;
    }
}
```
* 时间复杂度:启发式搜索分析时空复杂度意义不大
* 空间复杂度:启发式搜索分析时空复杂度意义不大

### 最后

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

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

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

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


676. 实现一个魔法字典

Question:
设计一个使用单词列表进行初始化的数据结构,单词列表中的单词 互不相同 。 如果给出一个单词,请判定能否只将这个单词中一个字母换成另一个字母,使得所形成的新单词存在于你构建的字典中。

实现 `MagicDictionary` 类:

* `MagicDictionary()` 初始化对象
* `void buildDict(String[] dictionary)` 使用字符串数组 `dictionary` 设定该数据结构,`dictionary` 中的字符串互不相同
* `bool search(String searchWord)` 给定一个字符串 `searchWord`,判定能否只将字符串中 一个 字母换成另一个字母,使得所形成的新字符串能够与字典中的任一字符串匹配。如果可以,返回 `true`;否则,返回 `false`。

示例:
```
输入
["MagicDictionary", "buildDict", "search", "search", "search", "search"]
[[], [["hello", "leetcode"]], ["hello"], ["hhllo"], ["hell"], ["leetcoded"]]

输出
[null, null, false, true, false, false]

解释
MagicDictionary magicDictionary = new MagicDictionary();
magicDictionary.buildDict(["hello", "leetcode"]);
magicDictionary.search("hello"); // 返回 False
magicDictionary.search("hhllo"); // 将第二个 'h' 替换为 'e' 可以匹配 "hello" ,所以返回 True
magicDictionary.search("hell"); // 返回 False
magicDictionary.search("leetcoded"); // 返回 False
```

提示:
* $1 <= dictionary.length <= 100$
* $1 <= dictionary[i].length <= 100$
* `dictionary[i]` 仅由小写英文字母组成
* `dictionary` 中的所有字符串 互不相同
* $1 <= searchWord.length <= 100$
* `searchWord` 仅由小写英文字母组成
* `buildDict` 仅在 `search` 之前调用一次
* 最多调用 $100$ 次 `search`

Solution:
### Trie + DFS

为了方便,我们令 `dictionary` 为 `ss`,令 `searchWord` 为 `s`。

整体题意:给定字符串 `s`,问能否存在替换掉 `s` 中的某个字符,使得新字符串出现在 `ss` 数组中。

考虑如何使用「字典树/`Trie`」求解该问题:
*  `buildDict` 操作:我们可以将所有的 $ss[i]$ 存入字典树中,方便后续检索;

* `search` 操作:设计递归函数 `boolean query(String s, int idx, int p, int limit)`,其中 `s` 为待检索的字符串,`idx` 为当前处理到字符串 `s` 的哪一位,`p` 为当前搜索到字典树的索引编号(起始有 $p = 0$),`limit` 为当前剩余的替换字符次数,根据题意,`limit` 固定为 $1$,含义为必须替换掉 `s` 的一个字符。
    对于 $s[idx]$ 而言,我们可以枚举新字符串在当前位置是何种字符($C = 26$ 个选择),若当前枚举到的字符与 $s[idx]$ 一致,则不消耗替换次数。
    爆搜过程中替换次数为负数直接剪枝,当爆搜到结尾位置,再检查当前的字典树索引 $p$ 是否为单词结尾节点(对应查询数组 `ss` 中是否存在该字符串),以及剩余的替换次数 `limit` 是否为 $0$。

> **不了解「Trie / 字典树」的同学可以看前置 🧀:[字典树入门](https://mp.weixin.qq.com/s?__biz=MzU4NDE3MTEyMA==&mid=2247488490&idx=1&sn=db2998cb0e5f08684ee1b6009b974089)。里面通过图例展示了字典树基本形态,以及提供了「数组实现」和「TrieNode 实现」两种方式,还有「数组大小估算方式」和「Trie 应用面」介绍**

Java 代码:
```Java
class MagicDictionary {
    int N = 100 * 100, M = 26, idx = 0;
    int[][] tr = new int[N][M];
    boolean[] isEnd = new boolean[N * M];
    void add(String s) {
        int p = 0;
        for (int i = 0; i < s.length(); i++) {
            int u = s.charAt(i) - 'a';
            if (tr[p][u] == 0) tr[p][u] = ++idx;
            p = tr[p][u];
        }
        isEnd[p] = true;
    }
    boolean query(String s, int idx, int p, int limit) {
        if (limit < 0) return false;
        if (idx == s.length()) return isEnd[p] && limit == 0;
        int u = s.charAt(idx) - 'a';
        for (int i = 0; i < 26; i++) {
            if (tr[p][i] == 0) continue;
            if (query(s, idx + 1, tr[p][i], i == u ? limit : limit - 1)) return true;
        }
        return false;
    }
    public void buildDict(String[] ss) {
        for (String s : ss) add(s);
    }
    public boolean search(String s) {
        return query(s, 0, 0, 1);
    }
}
```
TypeScript 代码:
```TypeScript
class MagicDictionary {
    N: number = 100 * 100; M: number = 26; idx: number = 0;
    tr: number[][] = new Array<Array<number>>(this.N)
    isEnd: boolean[] = new Array<boolean>(this.N * this.M).fill(false)
    add(s: string): void {
        let p = 0
        for (let i = 0; i < s.length; i++) {
            const u = s.charCodeAt(i) - 'a'.charCodeAt(0)
            if (this.tr[p] == undefined) this.tr[p] = new Array<number>(this.M).fill(0)
            if (this.tr[p][u] == 0) this.tr[p][u] = ++this.idx
            p = this.tr[p][u]
        }
        this.isEnd[p] = true
    }
    query(s: string, idx: number, p: number, limit: number): boolean {
        if (limit < 0) return false
        if (idx == s.length) return this.isEnd[p] && limit == 0
        const u = s.charCodeAt(idx) - 'a'.charCodeAt(0)
        for (let i = 0; i < 26; i++) {
            if (this.tr[p] == undefined || this.tr[p][i] == 0) continue
            if (this.query(s, idx + 1, this.tr[p][i], i == u ? limit : limit - 1)) return true
        }
        return false
    }
    buildDict(ss: string[]): void {
        for (const s of ss) this.add(s)
    }
    search(s: string): boolean {
        return this.query(s, 0, 0, 1)
    }
}
```
* 时间复杂度:`buildDict` 操作需要将所有字符存入 `Trie`,复杂度为 $\sum_{i = 0}^{n - 1} len(ss[i]])$;`search` 操作在不考虑 `limit` 以及字典树中最多只有 $100$ 具体方案所带来的剪枝效果的话,最坏情况下要搜索所有 $C^L$ 个方案,其中 $C = 26$ 为字符集大小,$L = 100$ 为搜索字符串的最大长度
* 空间复杂度:$O(N \times L \times C)$,其中 $N = 100$ 为存入 `Trie` 的最大方案数,$L = 100$ 为存入字符串的最大长度,$C = 26$ 为字符集大小

---

### 模拟

当然,利用数据范围只有 $100$,直接使用模拟也是可以的。

Java 代码:
```Java
class MagicDictionary {
    String[] ss;
    public void buildDict(String[] _ss) {
        ss = _ss;
    }
    public boolean search(String str) {
        for (String s : ss) {
            int cnt = 0;
            for (int i = 0; s.length() == str.length() && i < s.length() && cnt <= 1; i++) {
                if (s.charAt(i) != str.charAt(i)) cnt++;
            }
            if (cnt == 1) return true;
        }
        return false;
    }
}
```
TypeScript 代码:
```TypeScript
class MagicDictionary {
    ss: string[]
    buildDict(_ss: string[]): void {
        this.ss = _ss
    }
    search(s: string): boolean {
        for (const str of this.ss) {
            let cnt = 0
            for (let i = 0; str.length == s.length && i < s.length && cnt <= 1; i++) {
                if (s.charAt(i) != str.charAt(i)) cnt++
            }
            if (cnt == 1) return true
        }
        return false
    }
}
```
* 时间复杂度:`buildDict` 操作复杂度为 $O(1)$;`search` 操作复杂度为 $O(n \times L)$,其中 $n$ 为数组 `ss` 的长度,$L$ 为查询字符串的长度
* 空间复杂度:$O(n)$

---

### 加餐

1. 前置练习题 [可用 Trie 进阶的模拟题](https://mp.weixin.qq.com/s?__biz=MzU4NDE3MTEyMA==&mid=2247492214&idx=1&sn=40fa070fe3b014873297f7ff740ba60f)

2. 另外一道 [结合 DFS 的 Trie 运用题](https://mp.weixin.qq.com/s?__biz=MzU4NDE3MTEyMA==&mid=2247492188&idx=1&sn=a1436d1ffe2b8200a36c3196ca1c7ed1)

### 最后

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

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

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

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


Question:
实现一个 `MapSum` 类,支持两个方法,`insert` 和 `sum`:

* `MapSum()` 初始化 `MapSum` 对象
* `void insert(String key, int val)` 插入 `key-val` 键值对,字符串表示键 `key` ,整数表示值 `val` 。如果键 `key` 已经存在,那么原来的键值对将被替代成新的键值对。
* `int sum(string prefix)` 返回所有以该前缀 `prefix` 开头的键 `key` 的值的总和。

示例:
```
输入:
["MapSum", "insert", "sum", "insert", "sum"]
[[], ["apple", 3], ["ap"], ["app", 2], ["ap"]]

输出:
[null, null, 3, null, 5]

解释:
MapSum mapSum = new MapSum();
mapSum.insert("apple", 3);
mapSum.sum("ap");           // return 3 (apple = 3)
mapSum.insert("app", 2);
mapSum.sum("ap");           // return 5 (apple + app = 3 + 2 = 5)
```

提示:
* $1 <= key.length, prefix.length <= 50$
* `key` 和 `prefix` 仅由小写英文字母组成
* `1 <= val <= 1000`
* 最多调用 `50` 次 `insert` 和 `sum`

Solution:
### Trie + DFS

从需要实现「存储字符串(映射关系)」并「检索某个字符串前缀的总和」来看,可以知道这是与 $Trie$ 相关的题目,还不了解 $Trie$ 的同学可以先看前置 🧀:[实现 Trie (前缀树)](https://mp.weixin.qq.com/s?__biz=MzU4NDE3MTEyMA==&mid=2247488490&idx=1&sn=db2998cb0e5f08684ee1b6009b974089&chksm=fd9cb8f5caeb31e3f7f67dba981d8d01a24e26c93ead5491edb521c988adc0798d8acb6f9e9d&token=59039721&lang=zh_CN#rd) 。

考虑如何实现两个操作:

* `insert` :在基本的 $Trie$ 插入操作的基础上进行拓展即可。与常规的插入操作的唯一区别为,不能简单记录单词的结束位置,还要存储 $key$ 对应的 $val$ 是多少。具体的我们可以使用 `int` 类型的数组 $hash$ 来代替原有的 `boolean` 类型的数组 $isWord$;

* `sum` : 先对入参 $prefix$ 进行字典树搜索,到达尾部后再使用 `DFS` 搜索后面的所有方案,并累加结果。

代码(`static` 优化代码见 $P2$,避免每个样例都 `new` 大数组):
```Java
class MapSum {
    int[][] tr = new int[2510][26];
    int[] hash = new int[2510];
    int idx;
    public void insert(String key, int val) {
        int p = 0;
        for (int i = 0; i < key.length(); i++) {
            int u = key.charAt(i) - 'a';
            if (tr[p][u] == 0) tr[p][u] = ++idx;
            p = tr[p][u];
        }
        hash[p] = val;
    }
    public int sum(String prefix) {
        int p = 0;
        for (int i = 0; i < prefix.length(); i++) {
            int u = prefix.charAt(i) - 'a';
            if (tr[p][u] == 0) return 0;
            p = tr[p][u];
        }
        return dfs(p);
    }
    int dfs(int p) {
        int ans = hash[p];
        for (int u = 0; u < 26; u++) {
            if (tr[p][u] != 0) ans += dfs(tr[p][u]);
        }
        return ans;
    }
}
```

```Java
class MapSum {
    static int[][] tr = new int[2510][26];
    static int[] hash = new int[2510];
    static int idx;
    public MapSum() {
        for (int i = 0; i <= idx; i++) Arrays.fill(tr[i], 0);
        Arrays.fill(hash, 0);
        idx = 0;
    }
    public void insert(String key, int val) {
        int p = 0;
        for (int i = 0; i < key.length(); i++) {
            int u = key.charAt(i) - 'a';
            if (tr[p][u] == 0) tr[p][u] = ++idx;
            p = tr[p][u];
        }
        hash[p] = val;
    }
    public int sum(String prefix) {
        int p = 0;
        for (int i = 0; i < prefix.length(); i++) {
            int u = prefix.charAt(i) - 'a';
            if (tr[p][u] == 0) return 0;
            p = tr[p][u];
        }
        return dfs(p);
    }
    int dfs(int p) {
        int ans = hash[p];
        for (int u = 0; u < 26; u++) {
            if (tr[p][u] != 0) ans += dfs(tr[p][u]);
        }
        return ans;
    }
}
```
* 时间复杂度:令 $key$ 的最大长度为 $n$,最大调用次数为 $m$,字符集大小为 $C$( 本题 $C$ 固定为 $26$ ),`insert` 操作的复杂度为 $O(n)$;从 `DFS` 的角度分析,`sum` 操作的复杂度为 $O(C^n)$,但事实上,对于本题具有明确的计算量上界,搜索所有的格子的复杂度为 $O(n \times m \times C)$
* 空间复杂度:$O(n \times m \times C)$

---

### Trie 记录前缀字符串总和

为降低 `sum` 操作的复杂度,我们可以在 `insert` 操作中同时记录(累加)每个前缀的总和。

代码(`static` 优化代码见 $P2$,避免每个样例都 `new` 大数组):
```Java
class MapSum {
    int N = 2510;
    int[][] tr = new int[N][26];
    int[] hash = new int[N];
    int idx;
    Map<String, Integer> map = new HashMap<>();
    public void insert(String key, int val) {
        int _val = val;
        if (map.containsKey(key)) val -= map.get(key);
        map.put(key, _val);
        for (int i = 0, p = 0; i < key.length(); i++) {
            int u = key.charAt(i) - 'a';
            if (tr[p][u] == 0) tr[p][u] = ++idx;
            p = tr[p][u];
            hash[p] += val;
        }
    }
    public int sum(String prefix) {
        int p = 0;
        for (int i = 0; i < prefix.length(); i++) {
            int u = prefix.charAt(i) - 'a';
            if (tr[p][u] == 0) return 0;
            p = tr[p][u];
        }
        return hash[p];
    }
}
```

```Java
class MapSum {
    static int N = 2510;
    static int[][] tr = new int[N][26];
    static int[] hash = new int[N];
    static int idx;
    static Map<String, Integer> map = new HashMap<>();
    public MapSum() {
        for (int i = 0; i <= idx; i++) Arrays.fill(tr[i], 0);
        Arrays.fill(hash, 0);
        idx = 0;
        map.clear();
    }
    public void insert(String key, int val) {
        int _val = val;
        if (map.containsKey(key)) val -= map.get(key);
        map.put(key, _val);
        for (int i = 0, p = 0; i < key.length(); i++) {
            int u = key.charAt(i) - 'a';
            if (tr[p][u] == 0) tr[p][u] = ++idx;
            p = tr[p][u];
            hash[p] += val;
        }
    }
    public int sum(String prefix) {
        int p = 0;
        for (int i = 0; i < prefix.length(); i++) {
            int u = prefix.charAt(i) - 'a';
            if (tr[p][u] == 0) return 0;
            p = tr[p][u];
        }
        return hash[p];
    }
}
```
* 时间复杂度:令 $key$ 的最大长度为 $n$,`insert` 操作的复杂度为 $O(n)$;`sum` 操作的复杂度为 $O(n)$
* 空间复杂度:令 $key$ 的最大长度为 $n$,最大调用次数为 $m$,字符集大小为 $C$( 本题 $C$ 固定为 $26$ ),复杂度为 $O(n \times m \times C)$

### 最后

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

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

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

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


678. 有效的括号字符串

Question:
给定一个只包含三种字符的字符串:`(` ,`)` 和 `*`,写一个函数来检验这个字符串是否为有效字符串。

有效字符串具有如下规则:
* 任何左括号 `(` 必须有相应的右括号 `)`。
* 任何右括号 `)` 必须有相应的左括号 `(` 。
* 左括号 `(` 必须在对应的右括号之前 `)`。
* 可以被视为单个右括号 `)` ,或单个左括号 `(` ,或一个空字符串。
* 一个空字符串也被视为有效字符串。

示例 1:

```
输入: "()"

输出: True
```
示例 2:
```
输入: "(*)"

输出: True
```
示例 3:
```
输入: "(*))"

输出: True
```

注意:
* 字符串大小将在 $[1,100]$ 范围内。

Solution:
### 动态规划

**定义 $f[i][j]$ 为考虑前 $i$ 个字符(字符下标从 $1$ 开始),能否与 $j$ 个右括号形成合法括号序列。**

起始时只有 $f[0][0]$ 为 $true$,最终答案为 $f[n][0]$。

不失一般性的考虑 $f[i][j]$ 该如何转移:

* 当前字符为 `(` : 如果 $f[i][j]$ 为 $true$,必然有 $f[i - 1][j - 1]$ 为 $true$,反之亦然。即有 $f[i][j] = f[i - 1][j - 1]$;
* 当前字符为 `)` : 如果 $f[i][j]$ 为 $true$,必然有 $f[i - 1][j + 1]$ 为 $true$,反之亦然。即有 $f[i][j] = f[i - 1][j + 1]$;
* 当前字符为 `*` : 根据 `*` 代指的符号不同,分为三种情况,只有有一种情况为 $true$ 即可。即有 $f[i][j] = f[i - 1][j - 1] ∨ f[i - 1][j + 1] ∨ f[i - 1][j]$。

代码:
```Java
class Solution {
    public boolean checkValidString(String s) {
        int n = s.length();
        boolean[][] f = new boolean[n + 1][n + 1];
        f[0][0] = true;
        for (int i = 1; i <= n; i++) {
            char c = s.charAt(i - 1);
            for (int j = 0; j <= i; j++) {
                if (c == '(') {
                    if (j - 1 >= 0) f[i][j] = f[i - 1][j - 1];
                } else if (c == ')') {
                    if (j + 1 <= i) f[i][j] = f[i - 1][j + 1];
                } else {
                    f[i][j] = f[i - 1][j];
                    if (j - 1 >= 0) f[i][j] |= f[i - 1][j - 1];
                    if (j + 1 <= i) f[i][j] |= f[i - 1][j + 1];
                }
            }
        }
        return f[n][0];
    }
}
```
* 时间复杂度:$O(n^2)$
* 空间复杂度:$O(n^2)$

---

### 模拟

通过解法一,我们进一步发现,对于某个 $f[i][x]$ 而言(即动规数组中的某一行),值为 $true$ 的必然为连续一段。

即 **由于存在可变化的 `*` 符号,因此考虑在考虑前 $i$ 个字符,其能与消耗的左括号的数量具有明确的「上界与下界」。且当前上界与下界的变化,仅取决于「当前为何种字符」,以及「处理上一个字符时上界与下界为多少」。**

但直接记录所能消耗的左括号上限和下限需要处理较多的边界问题。

我们可以使用与[(题解)301. 删除无效的括号](https://leetcode-cn.com/problems/remove-invalid-parentheses/solution/yi-fen-zhong-nei-kan-dong-jiang-gua-hao-aya6k/) 类似的思路:

令左括号的得分为 $1$;右括号的得分为 $-1$。那么对于合法的方案而言,必定满足最终得分为 $0$。

同时由于本题存在 `*`,因此我们需要记录得分的区间区间是多少,而不仅是一个具体的得分。

具体的,使用两个变量 `l` 和 `r` 分别表示「最低得分」和「最高得分」。

根据当前处理到的字符进行分情况讨论:

* 当前字符为 `(` : `l` 和 `r` 同时加一;
* 当前字符为 `)` : `l` 和 `r` 同时减一;
* 当前字符为 `*` : 如果 `*` 代指成 `(` 的话,`l` 和 `r` 都进行加一;如果 `*` 代指成 `)` 的话,`l` 和 `r` 都进行减一;如果 `*` 不变的话,`l` 和 `r` 均不发生变化。因此总的 `l` 的变化为减一,总的 `r` 的变化为加一。

需要注意的是,在匹配过程中如果 `l` 为负数,需要重置为 $0$,因为如果当前序列本身为不合法括号序列的话,增加 `(` 必然还是不合法。同时,当出现 `l > r` 说明上界为负数,即右括号过多,必然为非合法方案,返回 $false$。

代码:
```Java
class Solution {
    public boolean checkValidString(String s) {
        int l = 0, r = 0;
        for (char c : s.toCharArray()) {
            if (c == '(') {
                l++; r++;
            } else if (c == ')') {
                l--; r--;
            } else {
                l--; r++;
            }
            l = Math.max(l, 0);
            if (l > r) return false;
        }
        return l == 0;
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


Question:
你现在是一场采用特殊赛制棒球比赛的记录员。这场比赛由若干回合组成,过去几回合的得分可能会影响以后几回合的得分。

比赛开始时,记录是空白的。你会得到一个记录操作的字符串列表 `ops`,其中 $ops[i]$ 是你需要记录的第 $i$ 项操作,`ops` 遵循下述规则:

1. 整数 `x` - 表示本回合新获得分数 `x`
2. `"+"` - 表示本回合新获得的得分是前两次得分的总和。题目数据保证记录此操作时前面总是存在两个有效的分数。
3. `"D"` - 表示本回合新获得的得分是前一次得分的两倍。题目数据保证记录此操作时前面总是存在一个有效的分数。
4. `"C"` - 表示前一次得分无效,将其从记录中移除。题目数据保证记录此操作时前面总是存在一个有效的分数。

请你返回记录中所有得分的总和。

示例 1:
```
输入:ops = ["5","2","C","D","+"]

输出:30

解释:
"5" - 记录加 5 ,记录现在是 [5]
"2" - 记录加 2 ,记录现在是 [5, 2]
"C" - 使前一次得分的记录无效并将其移除,记录现在是 [5].
"D" - 记录加 2 * 5 = 10 ,记录现在是 [5, 10].
"+" - 记录加 5 + 10 = 15 ,记录现在是 [5, 10, 15].
所有得分的总和 5 + 10 + 15 = 30
```
示例 2:
```
输入:ops = ["5","-2","4","C","D","9","+","+"]

输出:27

解释:
"5" - 记录加 5 ,记录现在是 [5]
"-2" - 记录加 -2 ,记录现在是 [5, -2]
"4" - 记录加 4 ,记录现在是 [5, -2, 4]
"C" - 使前一次得分的记录无效并将其移除,记录现在是 [5, -2]
"D" - 记录加 2 * -2 = -4 ,记录现在是 [5, -2, -4]
"9" - 记录加 9 ,记录现在是 [5, -2, -4, 9]
"+" - 记录加 -4 + 9 = 5 ,记录现在是 [5, -2, -4, 9, 5]
"+" - 记录加 9 + 5 = 14 ,记录现在是 [5, -2, -4, 9, 5, 14]
所有得分的总和 5 + -2 + -4 + 9 + 5 + 14 = 27
```
示例 3:
```
输入:ops = ["1"]

输出:1
```

提示:
* $1 <= ops.length <= 1000$
* $ops[i]$ 为 `"C"`、`"D"`、`"+"`,或者一个表示整数的字符串。整数范围是 $[-3 * 10^4, 3 * 10^4]$
* 对于 `"+"` 操作,题目数据保证记录此操作时前面总是存在两个有效的分数
* 对于 `"C"` 和 `"D"` 操作,题目数据保证记录此操作时前面总是存在一个有效的分数

Solution:
### 模拟

根据题意进行模拟即可。

代码:
```Java
class Solution {
    static int[] nums = new int[1010];
    public int calPoints(String[] ops) {
        int n = ops.length, idx = 0;
        for (int i = 0; i < n; i++, idx++) {
            if (ops[i].equals("+")) nums[idx] = nums[idx - 1] + nums[idx - 2];
            else if (ops[i].equals("D")) nums[idx] = nums[idx - 1] * 2;
            else if (ops[i].equals("C")) idx -= 2;
            else nums[idx] = Integer.parseInt(ops[i]);
        }
        int ans = 0;
        for (int i = 0; i < idx; i++) ans += nums[i];
        return ans;
    }
}
```

```python
nums = [0] * 1010
class Solution:
    def calPoints(self, ops: List[str]) -> int:
        idx = 0
        for x in ops:
            if x == '+':
                nums[idx] = nums[idx - 1] + nums[idx - 2]
            elif x == 'D':
                nums[idx] = nums[idx - 1] * 2
            elif x == 'C':
                idx -= 2
            else:
                nums[idx] = int(x)
            idx += 1
        return sum(nums[:idx])
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


686. 重复叠加字符串匹配

Question:
给定两个字符串 `a` 和 `b`,寻找重复叠加字符串 `a` 的最小次数,使得字符串 `b` 成为叠加后的字符串 `a` 的子串,如果不存在则返回 `-1`。

注意:字符串 `"abc"` 重复叠加 `0` 次是 `""`,重复叠加 `1` 次是 `"abc"`,重复叠加 `2` 次是 `"abcabc"`。

示例 1:
```
输入:a = "abcd", b = "cdabcdab"

输出:3

解释:a 重复叠加三遍后为 "abcdabcdabcd", 此时 b 是其子串。
```
示例 2:
```
输入:a = "a", b = "aa"

输出:2
```
示例 3:
```
输入:a = "a", b = "a"

输出:1
```
示例 4:
```
输入:a = "abc", b = "wxyz"

输出:-1
```

提示:
* $1 <= a.length <= 10^4$
* $1 <= b.length <= 10^4$
* `a` 和 `b` 由小写英文字母组成

Solution:
### 基本分析

首先,可以分析复制次数的「下界」和「上界」为何值:

**对于「下界」的分析是容易的:至少将 `a` 复制长度大于等于 `b` 的长度,才有可能匹配。**

在明确了「下界」后,再分析再经过多少次复制,能够明确得到答案,能够得到明确答案的最小复制次数即是上界。

**由于主串是由 `a` 复制多次而来,并且是从主串中找到子串 `b`,因此可以明确子串的起始位置,不会超过 `a` 的长度。**

即**长度越过 `a` 长度的起始匹配位置,必然在此前已经被匹配过了。**

由此,我们可知复制次数「上界」最多为「下界 + $1$」。

令 `a` 的长度为 $n$,`b` 的长度为 $m$,下界次数为 $c1$,上界次数为 $c2 = c1 + 1$。

因此我们可以对 `a` 复制 $c2$ 次,得到主串后匹配 `b`,如果匹配成功后的结束位置不超过了 $n * c1$,说明复制 $c1$ 即可,返回 $c1$,超过则返回 $c2$;匹配不成功则返回 $-1$。

---

### 卡常

这是我最开始的 AC 版本。

虽然这是道挺显然的子串匹配问题,但是昨晚比平时晚睡了一个多小时,早上起来精神状态不是很好,身体的每个细胞都在拒绝写 KMP 🤣

就动了歪脑筋写了个「卡常」做法。

通过该做法再次印证了 LC 的评测机制十分奇葩:居然不是对每个用例单独计时,也不是算总的用例用时,而是既算单用例耗时,又算总用时??

导致我直接 `TLE` 了 $6$ 次才通过(从 $700$ 试到了 $100$),其中有 $4$ 次 `TLE` 是显示通过了所有样例,但仍然 `TLE`,我不理解为什么要设置这样迷惑的机制。

回到该做法本身,首先对 `a` 进行复制确保长度大于等于 `b`,然后在一定时间内,不断的「复制 - 检查」,如果在规定时间内能够找到则返回复制次数,否则返回 `-1`。

代码:
```Java
import java.time.Clock;
class Solution {
    public int repeatedStringMatch(String a, String b) {
        StringBuilder sb = new StringBuilder();
        int ans = 0;
        while (sb.length() < b.length() && ++ans > 0) sb.append(a);
        Clock clock = Clock.systemDefaultZone();
        long start = clock.millis();
        while (clock.millis() - start < 100) {
            if (sb.indexOf(b) != -1) return ans;
            sb.append(a);
            ans++;
        }
        return -1;
    }
}
```
* 时间复杂度:$O(C)$
* 空间复杂度:$O(C)$

---

### 上下界性质

通过「基本分析」后,我们发现「上下界」具有准确的大小关系,其实不需要用到「卡常」做法。

只需要进行「上界」次复制后,尝试匹配,根据匹配结果返回答案即可。

代码:
```Java
class Solution {
    public int repeatedStringMatch(String a, String b) {
        StringBuilder sb = new StringBuilder();
        int ans = 0;
        while (sb.length() < b.length() && ++ans > 0) sb.append(a);
        sb.append(a);
        int idx = sb.indexOf(b);
        if (idx == -1) return -1;
        return idx + b.length() > a.length() * ans ? ans + 1 : ans;
    }
}
```
* 时间复杂度:需要 $\left \lceil \frac{m}{n} \right \rceil + 1$ 次拷贝 和 一次子串匹配。复杂度为 $O(n * (\left \lceil \frac{m}{n} \right \rceil + 1))$
* 空间复杂度:$O(n * (\left \lceil \frac{m}{n} \right \rceil + 1))$

---

### KMP

其中 `indexOf` 部分可以通过 KMP/字符串哈希 实现,不熟悉 KMP 的同学,可以查看 [一文详解 KMP 算法](https://mp.weixin.qq.com/s?__biz=MzU4NDE3MTEyMA==&mid=2247486317&idx=1&sn=9c2ff2fa5db427133cce9c875064e7a4&chksm=fd9ca072caeb29642bf1f5c151e4d5aaff4dc10ba408b23222ea1672cfc41204a584fede5c05&token=1782709324&lang=zh_CN#rd),里面通过大量配图讲解了 KMP 的匹配过程与提供了实用模板。

使用 KMP 代替 `indexOf` 可以有效利用主串是由多个 `a` 复制而来的性质。

代码:
```Java
class Solution {
    public int repeatedStringMatch(String a, String b) {
        StringBuilder sb = new StringBuilder();
        int ans = 0;
        while (sb.length() < b.length() && ++ans > 0) sb.append(a);
        sb.append(a);
        int idx = strStr(sb.toString(), b);
        if (idx == -1) return -1;
        return idx + b.length() > a.length() * ans ? ans + 1 : ans;
    }

    int strStr(String ss, String pp) {
        if (pp.isEmpty()) return 0;

        // 分别读取原串和匹配串的长度
        int n = ss.length(), m = pp.length();
        // 原串和匹配串前面都加空格,使其下标从 1 开始
        ss = " " + ss;
        pp = " " + pp;

        char[] s = ss.toCharArray();
        char[] p = pp.toCharArray();

        // 构建 next 数组,数组长度为匹配串的长度(next 数组是和匹配串相关的)
        int[] next = new int[m + 1];
        // 构造过程 i = 2,j = 0 开始,i 小于等于匹配串长度 【构造 i 从 2 开始】
        for (int i = 2, j = 0; i <= m; i++) {
            // 匹配不成功的话,j = next(j)
            while (j > 0 && p[i] != p[j + 1]) j = next[j];
            // 匹配成功的话,先让 j++
            if (p[i] == p[j + 1]) j++;
            // 更新 next[i],结束本次循环,i++
            next[i] = j;
        }

        // 匹配过程,i = 1,j = 0 开始,i 小于等于原串长度 【匹配 i 从 1 开始】
        for (int i = 1, j = 0; i <= n; i++) {
            // 匹配不成功 j = next(j)
            while (j > 0 && s[i] != p[j + 1]) j = next[j];
            // 匹配成功的话,先让 j++,结束本次循环后 i++
            if (s[i] == p[j + 1]) j++;
            // 整一段匹配成功,直接返回下标
            if (j == m) return i - m;
        }
        return -1;
    }
}
```
* 时间复杂度:需要 $\left \lceil \frac{m}{n} \right \rceil + 1$ 次拷贝 和 一次子串匹配。复杂度为 $O(n * (\left \lceil \frac{m}{n} \right \rceil + 1))$
* 空间复杂度:$O(n * (\left \lceil \frac{m}{n} \right \rceil + 1))$

---

### 字符串哈希

结合「基本分析」,我们知道这本质是一个子串匹配问题,我们可以使用「字符串哈希」来解决。

令 `a` 的长度为 $n$,`b` 的长度为 $m$。

仍然是先将 `a` 复制「上界」次,得到主串 `ss`,目的是从 `ss` 中检测是否存在子串为 `b`。

在字符串哈希中,为了方便,我们将 `ss` 和 `b` 进行拼接,设拼接后长度为 $len$,那么 `b` 串的哈希值为 $[len - m + 1, len]$ 部分(下标从 $1$ 开始),记为 $target$。

然后在 $[1, n]$ 范围内枚举起点,尝试找长度为 $m$ 的哈希值与 $target$ 相同的哈希值。

代码:
```Java
class Solution {
    public int repeatedStringMatch(String a, String b) {
        StringBuilder sb = new StringBuilder();
        int ans = 0;
        while (sb.length() < b.length() && ++ans > 0) sb.append(a);
        sb.append(a);
        int idx = strHash(sb.toString(), b);
        if (idx == -1) return -1;
        return idx + b.length() > a.length() * ans ? ans + 1 : ans;
    }
    int strHash(String ss, String b) {
        int P = 131;
        int n = ss.length(), m = b.length();
        String str = ss + b;
        int len = str.length();
        int[] h = new int[len + 10], p = new int[len + 10];
        h[0] = 0; p[0] = 1;
        for (int i = 0; i < len; i++) {
            p[i + 1] = p[i] * P;
            h[i + 1] = h[i] * P + str.charAt(i);
        }
        int r = len, l = r - m + 1;
        int target = h[r] - h[l - 1] * p[r - l + 1]; // b 的哈希值
        for (int i = 1; i <= n; i++) {
            int j = i + m - 1;
            int cur = h[j] - h[i - 1] * p[j - i + 1]; // 子串哈希值
            if (cur == target) return i - 1;
        }
        return -1;
    }
}
```
* 时间复杂度:需要 $\left \lceil \frac{m}{n} \right \rceil + 1$ 次拷贝 和 一次子串匹配。复杂度为 $O(n * (\left \lceil \frac{m}{n} \right \rceil + 1))$
* 空间复杂度:$O(n * (\left \lceil \frac{m}{n} \right \rceil + 1))$

### 最后

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

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

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

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


Question:
给定一个二叉树的 `root`,返回 最长的路径的长度 ,这个路径中的 每个节点具有相同值 。 这条路径可以经过也可以不经过根节点。

两个节点之间的路径长度 由它们之间的边数表示。

示例 1:

```
输入:root = [5,4,5,1,1,5]

输出:2
```
示例 2:

```
输入:root = [1,4,5,4,4,5]

输出:2
```

提示:
* 树的节点数的范围是 $[0, 10^4] $
* $-1000 <= Node.val <= 1000$
* 树的深度将不超过 `1000`

Solution:
### 递归

设计递归函数 `int dfs(TreeNode root)`,含义为传入根节点 `root`,返回以该节点为起点,往下走同值路径所能经过的最大路径长度(即不能同时往左右节点走),同时使用全局变量 `max` 记录答案路径所能经过最大路径长度。

在递归函数内部,先通过递归 `root` 的左右子节点,拿到以 `root.left` 和 `root.right` 为起点的最大路径长度 `l` 和 `r`,然后根据当前节点值和左右子节点值的相等关系来更新 `ans`,同时用 `cur` 维护「以当前节点 `root` 为目标路径中深度最小(位置最高)节点时」所经过的最大路径长度。

Java 代码:
```Java
class Solution {
    int max = 0;
    public int longestUnivaluePath(TreeNode root) {
        dfs(root);
        return max;
    }
    int dfs(TreeNode root) {
        if (root == null) return 0;
        int ans = 0, cur = 0, l = dfs(root.left), r = dfs(root.right);
        if (root.left != null && root.left.val == root.val) {
            ans = l + 1; cur += l + 1;
        }
        if (root.right != null && root.right.val == root.val) {
            ans = Math.max(ans, r + 1); cur += r + 1;
        }
        max = Math.max(max, cur);
        return ans;
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    int maxv = 0;
    int dfs(TreeNode* root) {
        if (root == nullptr) return 0;
        int ans = 0, cur = 0;
        int l = dfs(root->left), r = dfs(root->right);
        if (root->left != nullptr && root->left->val == root->val) {
            ans = l + 1; cur += l + 1;
        }
        if (root->right != nullptr && root->right->val == root->val) {
            ans = max(ans, r + 1); cur += r + 1;
        }
        maxv = max(maxv, cur);
        return ans;
    }
    int longestUnivaluePath(TreeNode* root) {
        dfs(root);
        return maxv;
    }
};
```
Python 代码:
```Python
class Solution:
    def longestUnivaluePath(self, root: Optional[TreeNode]) -> int:
        maxv = 0
        def dfs(root):
            nonlocal maxv
            if not root: return 0
            ans, cur = 0, 0
            l, r = dfs(root.left), dfs(root.right)
            if root.left and root.left.val == root.val:
                ans = l + 1
                cur += l + 1
            if root.right and root.right.val == root.val:
                ans = max(ans, r + 1)
                cur += r + 1
            maxv = max(maxv, cur)
            return ans
        dfs(root)
        return maxv
```
TypeScript 代码:
```TypeScript
let max = 0;
function longestUnivaluePath(root: TreeNode | null): number {
    max = 0
    dfs(root)
    return max
};
function dfs(root: TreeNode | null): number {
    if (root == null) return 0
    let ans = 0, cur = 0, l = dfs(root.left), r = dfs(root.right)
    if (root.left != null && root.left.val == root.val) {
        ans = l + 1; cur += l + 1
    }
    if (root.right != null && root.right.val == root.val) {
        ans = Math.max(ans, r + 1); cur += r + 1
    }
    max = Math.max(max, cur)
    return ans
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:忽略递归带来的额外空间开销,复杂度为 $O(1)$

### 最后

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

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

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

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


688. 骑士在棋盘上的概率

Question:
在一个 $n \times n$ 的国际象棋棋盘上,一个骑士从单元格 $(row, column)$ 开始,并尝试进行 $k$ 次移动。行和列是 从 $0$ 开始 的,所以左上单元格是 $(0,0)$ ,右下单元格是 $(n - 1, n - 1)$ 。

象棋骑士有 $8$ 种可能的走法,如下图所示。每次移动在基本方向上是两个单元格,然后在正交方向上是一个单元格。

每次骑士要移动时,它都会随机从 $8$ 种可能的移动中选择一种(即使棋子会离开棋盘),然后移动到那里。

骑士继续移动,直到它走了 $k$ 步或离开了棋盘。

返回 骑士在棋盘停止移动后仍留在棋盘上的概率 。

示例 1:
```
输入: n = 3, k = 2, row = 0, column = 0

输出: 0.0625

解释: 有两步(到(1,2),(2,1))可以让骑士留在棋盘上。
在每一个位置上,也有两种移动可以让骑士留在棋盘上。
骑士留在棋盘上的总概率是0.0625。
```
示例 2:
```
输入: n = 1, k = 0, row = 0, column = 0

输出: 1.00000
```

提示:
* $1 <= n <= 25$
* $0 <= k <= 100$
* $0 <= row, column <= n$

Solution:
### 线性 DP

**定义 $f[i][j][p]$ 为从位置 $(i, j)$ 出发,使用步数不超过 $p$ 步,最后仍在棋盘内的概率。**

不失一般性考虑 $f[i][j][p]$ 该如何转移,根据题意,移动规则为「八连通」,对下一步的落点 $(nx, ny)$ 进行分情况讨论即可:

* 由于计算的是仍在棋盘内的概率,因此对于 $(nx, ny)$ 在棋盘外的情况,无须考虑;
* 若下一步的落点 $(nx, ny)$ 在棋盘内,其剩余可用步数为 $p - 1$,则最后仍在棋盘的概率为 $f[nx][ny][p - 1]$,则落点 $(nx, ny)$ 对 $f[i][j][p]$ 的贡献为 $f[nx][ny][p - 1] \times \frac{1}{8}$,其中 $\frac{1}{8}$ 为事件「**从 $(i, j)$ 走到 $(nx, ny)$**」的概率(八连通移动等概率发生),该事件与「**到达 $(nx, ny)$ 后进行后续移动并留在棋盘**」为相互独立事件。

最终的 $f[i][j][p]$ 为「八连通」落点的概率之和,即有:

$$
f[i][j][p] = \sum {f[nx][ny][p - 1] \times \frac{1}{8}}
$$

代码:
```Java
class Solution {
    int[][] dirs = new int[][]{{-1,-2},{-1,2},{1,-2},{1,2},{-2,1},{-2,-1},{2,1},{2,-1}};
    public double knightProbability(int n, int k, int row, int column) {
        double[][][] f = new double[n][n][k + 1];
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < n; j++) {
                f[i][j][0] = 1;
            }
        }
        for (int p = 1; p <= k; p++) {
            for (int i = 0; i < n; i++) {
                for (int j = 0; j < n; j++) {
                    for (int[] d : dirs) {
                        int nx = i + d[0], ny = j + d[1];
                        if (nx < 0 || nx >= n || ny < 0 || ny >= n) continue;
                        f[i][j][p] += f[nx][ny][p - 1] / 8;
                    }
                }
            }
        }
        return f[row][column][k];
    }
}
```
* 时间复杂度:令某个位置可联通的格子数量 $C = 8$,复杂度为 $O(n^2 \times k \times C)$
* 空间复杂度:$O(n^2 \times k)$

### 最后

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

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

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

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


LeetCode/681-690/689. 三个无重叠子数组的最大和（困难）.md

689. 三个无重叠子数组的最大和

Question:
给你一个整数数组 `nums` 和一个整数 `k` ,找出三个长度为 `k` 、互不重叠、且 `3 * k` 项的和最大的子数组,并返回这三个子数组。

以下标的数组形式返回结果,数组中的每一项分别指示每个子数组的起始位置(下标从 $0$ 开始)。如果有多个结果,返回字典序最小的一个。

示例 1:
```
输入:nums = [1,2,1,2,6,7,5,1], k = 2

输出:[0,3,5]

解释:子数组 [1, 2], [2, 6], [7, 5] 对应的起始下标为 [0, 3, 5]。
也可以取 [2, 1], 但是结果 [1, 3, 5] 在字典序上更大。
```
示例 2:
```
输入:nums = [1,2,1,2,1,2,1,2,1], k = 2

输出:[0,2,4]
```

提示:
* $1 <= nums.length <= 2 * 10^4$
* $1 <= nums[i] < 2^{16}$
* $1 <= k <= floor(nums.length / 3)$

Solution:
### 前缀和 + 序列 DP

若不考虑输出方案,仅是求「三个无重叠子数组的最大和」的最大值。

只需要使用动态规划求解即可:**定义 $f[i][j]$ 为考虑前 $i$ 个数,凑成无重叠子数组数量为 $j$ 个时的最大值。**

最终答案为 $f[n - 1][3]$。

不失一般性的考虑 $f[i][j]$ 该如何计算(以最优方案是否包含 $nums[i]$ 进行讨论):

*   最优方案中包含 $num[i]$:由于这 $j$ 个无重叠,因此前面的 $j - 1$ 个子数组不能覆盖 $[i - k + 1, i]$。即只能在 $[0, i - k]$ 中选 $j - 1$ 个子数组。此时有:

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

> 其中求解 $\sum_{idx = i - k + 1}^{i} nums[idx]$ 部分可以使用「前缀和」优化

*   最优方案不包含 $num[i]$:当明确了 $nums[i]$ 对最优方案无贡献,此时问题等价于考虑前 $i - 1$ 个数,凑成 $j$ 个不重叠子数组的最大值。此时有:

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

最终 $f[i][j]$ 为上述两种方案中的最大值。

然后考虑「如何回溯出字典序最小的具体方案」,常规的回溯具体方案的做法是,从最终答案 $f[n - 1][3]$ 开始往回追溯。

利用 $f[n - 1][3]$ 仅由两个可能的节点($f[n - 2][3]$ 和 $f[n - 1 - k][2]$)转移而来,通过判断 $f[n - 1][3]$ 等于 $f[n - 2][3]$ 还是 $f[n - 1 - k][2] + \sum_{idx = n - k }^{n - 1} nums[idx]$ 来决定回溯点为何值。

但该做法只能确保回溯出字典序最大的方案是正确(当两个可能的前驱节点都能转移到 $f[i][j]$ 时优先采纳靠后的位置),而我们需要回溯出字典序最小的方案。

在上述解法的基础上,有两种「求解字典序最小具体方案」的做法:

1.  将正序 DP 调整为反序 DP。修改状态定义为 $f[i][j]$ 为考虑 $[i, n - 1]$ 中进行选择,凑出无重叠子数组数量为 $j$ 个时的最大值(最终答案为 $f[0][3]$)。转移过程分析同理,然后从下标 $idx = 0$ 开始进行回溯,优先采纳 $idx$ 小的方案即可;

2.  仍然采取正序 DP 的做法,但对原数组进行翻转,从而将回溯「字典序最大」转换为「字典序最小」具体方案。

> 一些细节:为了避免对边界的处理,我们令动规数组 $f$ 和前缀和数组 $sun$ 的下标从 $1$ 开始。

代码($P1$ 为反序 DP 做法,$P2$ 为翻转数组做法):

```Java
class Solution {
    public int[] maxSumOfThreeSubarrays(int[] nums, int k) {
        int n = nums.length;
        long[] sum = new long[n + 1];
        for (int i = 1; i <= n; i++) sum[i] = sum[i - 1] + nums[i - 1];
        long[][] f = new long[n + 10][4];
        for (int i = n - k + 1; i >= 1; i--) {
            for (int j = 1; j < 4; j++) {
                f[i][j] = Math.max(f[i + 1][j], f[i + k][j - 1] + sum[i + k - 1] - sum[i - 1]);
            }
        }
        int[] ans = new int[3];
        int i = 1, j = 3, idx = 0;
        while (j > 0) {
            if (f[i + 1][j] > f[i + k][j - 1] + sum[i + k - 1] - sum[i - 1]) {
                i++;
            } else {
                ans[idx++] = i - 1;
                i += k; j--;
            }
        }
        return ans;
    }
}
```

```Java
class Solution {
    public int[] maxSumOfThreeSubarrays(int[] nums, int k) {
        int n = nums.length;
        reverse(nums);
        long[] sum = new long[n + 1];
        for (int i = 1; i <= n; i++) sum[i] = sum[i - 1] + nums[i - 1];
        long[][] f = new long[n + 10][4];
        for (int i = k; i <= n; i++) {
            for (int j = 1; j < 4; j++) {
                f[i][j] = Math.max(f[i - 1][j], f[i - k][j - 1] + sum[i] - sum[i - k]);
            }
        }
        int[] ans = new int[3];
        int i = n, j = 3, idx = 0;
        while (j > 0) {
            if (f[i - 1][j] > f[i - k][j - 1] + sum[i] - sum[i - k]) {
                i--;
            } else {
                ans[idx++] = n - i;
                i -= k; j--;
            }
        }
        return ans;
    }
    void reverse(int[] nums) {
        int n = nums.length;
        int l = 0, r = n - 1;
        while (l < r) {
            int c = nums[l];
            nums[l++] = nums[r];
            nums[r--] = c;
        }
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


Question:
给定一个保存员工信息的数据结构,它包含了员工 唯一的 id ,重要度 和 直系下属的 id 。

比如,员工 1 是员工 2 的领导,员工 2 是员工 3 的领导。他们相应的重要度为 15 , 10 , 5 。那么员工 1 的数据结构是 [1, 15, [2]] ,员工 2的 数据结构是 [2, 10, [3]] ,员工 3 的数据结构是 [3, 5, []] 。注意虽然员工 3 也是员工 1 的一个下属,但是由于 并不是直系 下属,因此没有体现在员工 1 的数据结构中。

现在输入一个公司的所有员工信息,以及单个员工 id ,返回这个员工和他所有下属的重要度之和。

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

输出:11

解释:
员工 1 自身的重要度是 5 ,他有两个直系下属 2 和 3 ,而且 2 和 3 的重要度均为 3 。因此员工 1 的总重要度是 5 + 3 + 3 = 11 。
```

提示:
* 一个员工最多有一个 直系 领导,但是可以有多个 直系 下属
* 员工数量不超过 2000 。

Solution:
### 递归 / DFS

一个直观的做法是,写一个递归函数来统计某个员工的总和。

统计自身的 $importance$ 值和直系下属的 $importance$ 值。同时如果某个下属还有下属的话,则递归这个过程。

代码:
```Java
class Solution {
    Map<Integer, Employee> map = new HashMap<>();
    public int getImportance(List<Employee> es, int id) {
        int n = es.size();
        for (int i = 0; i < n; i++) map.put(es.get(i).id, es.get(i));
        return getVal(id);
    }
    int getVal(int id) {
        Employee master = map.get(id);
        int ans = master.importance;
        for (int oid : master.subordinates) {
            Employee other = map.get(oid);
            ans += other.importance;
            for (int sub : other.subordinates) ans += getVal(sub);
        }
        return ans;
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n)$

---

### 迭代 / BFS

另外一个做法是使用「队列」来存储所有将要计算的 $Employee$ 对象,每次弹出时进行统计,并将其「下属」添加到队列尾部。

代码:
```Java
class Solution {
    public int getImportance(List<Employee> es, int id) {
        int n = es.size();
        Map<Integer, Employee> map = new HashMap<>();
        for (int i = 0; i < n; i++) map.put(es.get(i).id, es.get(i));
        int ans = 0;
        Deque<Employee> d = new ArrayDeque<>();
        d.addLast(map.get(id));
        while (!d.isEmpty()) {
            Employee poll = d.pollFirst();
            ans += poll.importance;
            for (int oid : poll.subordinates) {
                d.addLast(map.get(oid));
            }
        }
        return ans;
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


Question:
我们有 $n$ 种不同的贴纸。每个贴纸上都有一个小写的英文单词。

您想要拼写出给定的字符串 `target` ,方法是从收集的贴纸中切割单个字母并重新排列它们。如果你愿意,你可以多次使用每个贴纸,每个贴纸的数量是无限的。

返回你需要拼出 `target` 的最小贴纸数量。如果任务不可能,则返回 $-1$ 。

注意:在所有的测试用例中,所有的单词都是从 $1000$ 个最常见的美国英语单词中随机选择的,并且 `target` 被选择为两个随机单词的连接。

示例 1:
```
输入: stickers = ["with","example","science"], target = "thehat"

输出:3

解释:
我们可以使用 2 个 "with" 贴纸,和 1 个 "example" 贴纸。
把贴纸上的字母剪下来并重新排列后,就可以形成目标 “thehat“ 了。
此外,这是形成目标字符串所需的最小贴纸数量。
```
示例 2:
```
输入:stickers = ["notice","possible"], target = "basicbasic"

输出:-1

解释:我们不能通过剪切给定贴纸的字母来形成目标“basicbasic”。
```

提示:
* $n == stickers.length$
* $1 <= n <= 50$
* $1 <= stickers[i].length <= 10$
* $1 <= target <= 15$
* `stickers[i]` 和 `target` 由小写英文单词组成

Solution:
### 动态规划(记忆化搜索)

为了方便,我们记 $ss = stickers$,$t = target$,其中 $t$ 的长度为 $n$。

我们使用一个 $state$(一个 `int` 类型变量)来代表当前 $t$ 的凑成情况:若 $t[i]$ 已被凑成,则在 $state$ 中低 $i$ 位为 $1$,否则为 $0$。

起始时有 `state = 0`,最终若能凑成 $t$,则有 `state = (1 << n) - 1`。

由于每个 $ss[i]$ 可以被使用多次,因此对于一个特定的 $state$ 而言,其转换为最终的 `(1 << n) - 1` 的最小步数固定,因此我们可以使用「记忆化搜索」来避免对相同的 $state$ 进行重复搜索。

而在单步的搜索过程中,我们枚举每个 $ss[i]$ 来更新 $state$,假设使用某个 $ss[i]$ 得到的新状态为 $nstate$,则所有的 `dfs(nstate) + 1` 的最小值即是 $f[state]$。

代码:
```Java
class Solution {
    int N = 20, M = 1 << 20, INF = 50;
    int[] f = new int[M];
    String[] ss;
    String t;
    int dfs(int state) {
        int n = t.length();
        if (state == ((1 << n) - 1)) return 0;
        if (f[state] != -1) return f[state];
        int ans = INF;
        for (String s : ss) {
            int nstate = state;
            out:for (char c : s.toCharArray()) {
                for (int i = 0; i < n; i++) {
                    if (t.charAt(i) == c && ((nstate >> i) & 1) == 0) {
                        nstate |= (1 << i);
                        continue out;
                    }
                }
            }
            if (nstate != state) ans = Math.min(ans, dfs(nstate) + 1);
        }
        return f[state] = ans;
    }
    public int minStickers(String[] stickers, String target) {
        ss = stickers; t = target;
        Arrays.fill(f, -1);
        int ans = dfs(0);
        return ans == INF ? -1 : ans;
    }
}
```
* 时间复杂度:令 $n$ 和 $m$ 分别代表字符串 `t` 的长度和数组 `ss` 的长度。共有 $2^n$ 个状态,单次状态的计算复杂度为 $$O(\sum_{i = 0}^{m - 1}ss[i].length \times n)$$。整体复杂度为 $$O(2^n \times \sum_{i = 0}^{m - 1}ss[i].length \times n)$$
* 空间复杂度:$O(2^n)$

---

### 动态规划

定义 $f[state]$ 为当前 $t$ 的凑成情况为 $state$ 时,使用的最少贴纸数量。

对应的我们有 $f[0] = 0$,代表当 $t$ 的任何一位都不被凑成时,所需要的最小贴纸数量为 $0$。

每次我们尝试使用有效的状态 $s$($f[s]$ 不为 `INF`  为有效状态)来更新新状态 $ns$,状态转移过程与解法一类似,每次尝试使用任意的 $ss[i]$ 来得到新的 $ns$。

代码:
```Java
class Solution {
    static int N = 15, INF = 20;
    static int[] f = new int[1 << N];
    public int minStickers(String[] ss, String t) {
        int n = ss.length, m = t.length(), mask = 1 << m;
        Arrays.fill(f, INF);
        f[0] = 0;
        for (int s = 0; s < mask; s++) {
            if (f[s] == INF) continue;
            for (String str : ss) {
                int ns = s, len = str.length();
                for (int i = 0; i < len; i++) {
                    int c = str.charAt(i) - 'a';
                    for (int j = 0; j < m; j++) {
                        if (t.charAt(j) - 'a' == c && (((ns >> j) & 1) == 0)) {
                            ns |= (1 << j);
                            break;
                        }
                    }
                }
                f[ns] = Math.min(f[ns], f[s] + 1);
            }
        }
        return f[mask - 1] == INF ? -1 : f[mask - 1];
    }
}
```
* 时间复杂度:令 $n$ 和 $m$ 分别代表字符串 `t` 的长度和数组 `ss` 的长度。共有 $2^n$ 个状态,单次状态的计算复杂度为 $$O(\sum_{i = 0}^{m - 1}ss[i].length \times n)$$。整体复杂度为 $$O(2^n \times \sum_{i = 0}^{m - 1}ss[i].length \times n)$$
* 空间复杂度:$O(2^n)$

---

### 预处理优化

在解法一和解法二的状态转移过程中,我们每次都尝试枚举所有的 $ss[i]$ 来将 $s$ 更新为 $ns$。

实际上,可以有效填充 $t$ 中尚未被占用字符的 $ss[i]$ 可能只是少数,因此我们可以先预处理每个 $ss[i]$ 到底能够提供那些字符。

在将状态 $s$ 更新为 $ns$ 时,我们只枚举那些有效的 $ss[i]$。

代码:
```Java
class Solution {
    static int N = 15, INF = 20;
    static int[] f = new int[1 << N];
    public int minStickers(String[] ss, String t) {
        int n = ss.length, m = t.length(), mask = 1 << m;
        Map<Integer, List<Integer>> map = new HashMap<>();
        for (int i = 0; i < n; i++) {
            String str = ss[i];
            for (char c : str.toCharArray()) {
                int d = c - 'a';
                List<Integer> list = map.getOrDefault(d, new ArrayList<>());
                if (list.size() == 0 || list.get(list.size() - 1) != i) list.add(i);
                map.put(d, list);
            }
        }
        Arrays.fill(f, INF);
        f[0] = 0;
        for (int s = 0; s < mask; s++) {
            if (f[s] == INF) continue;
            int loc = -1;
            for (int i = 0; i < m && loc == -1; i++) {
                if (((s >> i) & 1) == 0) loc = i;
            }
            if (loc == -1) continue;
            List<Integer> list = map.getOrDefault(t.charAt(loc) - 'a', new ArrayList<>());
            for (int i = 0; i < list.size(); i++) {
                String str = ss[list.get(i)];
                int ns = s, len = str.length();
                for (int j = 0; j < len; j++) {
                    char c = str.charAt(j);
                    for (int k = 0; k < m; k++) {
                        if (t.charAt(k) == c && (((ns >> k) & 1) == 0)) {
                            ns |= (1 << k);
                            break;
                        }
                    }
                }
                f[ns] = Math.min(f[ns], f[s] + 1);
            }
        }
        return f[mask - 1] == INF ? -1 : f[mask - 1];
    }
}
```
* 时间复杂度:令 $n$ 和 $m$ 分别代表字符串 `t` 的长度和数组 `ss` 的长度。共有 $2^n$ 个状态,单次状态的计算复杂度为 $$O(\sum_{i = 0}^{m - 1}ss[i].length \times n)$$。整体复杂度为 $$O(2^n \times \sum_{i = 0}^{m - 1}ss[i].length \times n)$$
* 空间复杂度:$O(2^n)$

### 最后

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

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

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

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


692. 前K个高频单词

Question:
给一非空的单词列表,返回前 `k` 个出现次数最多的单词。

返回的答案应该按单词出现频率由高到低排序。

如果不同的单词有相同出现频率,按字母顺序排序。

示例 1:
```
输入: ["i", "love", "leetcode", "i", "love", "coding"], k = 2

输出: ["i", "love"]

解析: "i" 和 "love" 为出现次数最多的两个单词,均为2次。
    注意,按字母顺序 "i" 在 "love" 之前。
```

示例 2:
```
输入: ["the", "day", "is", "sunny", "the", "the", "the", "sunny", "is", "is"], k = 4

输出: ["the", "is", "sunny", "day"]

解析: "the", "is", "sunny" 和 "day" 是出现次数最多的四个单词,
    出现次数依次为 4, 3, 2 和 1 次。
```

注意:
* 假定 `k` 总为有效值,`1 ≤ k ≤ 集合元素数`。
* 输入的单词均由小写字母组成。

扩展练习:
* 尝试以 $O(n\log{k})$ 时间复杂度和 $O(n)$ 空间复杂度解决。

Solution:
### 哈希表 & 优先队列(堆)

这道题是在「优先队列(堆)」裸题的基础上增加了字典序大小的比较。

相应的,我们不能只根据「词频大小」构建小根堆来获取前 $k$ 个元素,还需要结合字典序大小来做。

具体的,我们可以使用「哈希表」&「优先队列」进行求解:

1. 使用「哈希表」来统计所有的词频
2. 构建大小为 $k$ 按照「词频升序 + (词频相同)字典序倒序」的优先队列:
    * 如果词频不相等,根据词频进行升序构建,确保堆顶元素是堆中词频最小的元素
    * 如果词频相等,根据字典序大小进行倒序构建,结合 $2.1$ 可以确保堆顶元素是堆中「词频最小 & 字典序最大」的元素
3. 对所有元素进行遍历,尝试入堆:
    * 堆内元素不足 $k$ 个:直接入堆
    * 词频大于堆顶元素:堆顶元素不可能是前 $k$ 大的元素。将堆顶元素弹出,并将当前元素添加到堆中
    * 词频小于堆顶元素;当前元素不可能是前 $k$ 大的元素,直接丢弃。
    * 词频等于堆顶元素:根据当前元素与堆顶元素的字典序大小决定(如果字典序大小比堆顶元素要小则入堆)
4. 输出堆内元素,并翻转

代码:
```Java
class Solution {
    public List<String> topKFrequent(String[] ws, int k) {
        Map<String, Integer> map = new HashMap<>();
        for (String w : ws) map.put(w, map.getOrDefault(w, 0) + 1);
        PriorityQueue<Object[]> q = new PriorityQueue<>(k, (a, b)->{
            // 如果词频不同,根据词频升序
            int c1 = (Integer)a[0], c2 = (Integer)b[0];
            if (c1 != c2) return c1 - c2;
            // 如果词频相同,根据字典序倒序
            String s1 = (String)a[1], s2 = (String)b[1];
            return s2.compareTo(s1);
        });
        for (String s : map.keySet()) {
            int cnt = map.get(s);
            if (q.size() < k) { // 不足 k 个,直接入堆
                q.add(new Object[]{cnt, s});
            } else {
                Object[] peek = q.peek();
                if (cnt > (Integer)peek[0]) { // 词频比堆顶元素大,弹出堆顶元素,入堆
                    q.poll();
                    q.add(new Object[]{cnt, s});
                } else if (cnt == (Integer)peek[0]) { // 词频与堆顶元素相同
                    String top = (String)peek[1];
                    if (s.compareTo(top) < 0) { // 且字典序大小比堆顶元素小,弹出堆顶元素,入堆
                        q.poll();
                        q.add(new Object[]{cnt, s});
                    }
                }
            }
        }
        List<String> ans = new ArrayList<>();
        while (!q.isEmpty()) ans.add((String)q.poll()[1]);
        Collections.reverse(ans);
        return ans;
    }
}
```
* 时间复杂度:使用哈希表统计词频,复杂度为 $O(n)$;使用最多 $n$ 个元素维护一个大小为 $k$ 的堆,复杂度为 $O(n\log{k})$;输出答案复杂度为 $O(k)$(同时 $k \leq n$)。整体复杂度为 $O(n\log{k})$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


693. 交替位二进制数

Question:
给定一个正整数,检查它的二进制表示是否总是 $0$、$1$ 交替出现:换句话说,就是二进制表示中相邻两位的数字永不相同。

示例 1:
```
输入:n = 5

输出:true

解释:5 的二进制表示是:101
```
示例 2:
```
输入:n = 7

输出:false

解释:7 的二进制表示是:111.
```
示例 3:
```
输入:n = 11

输出:false

解释:11 的二进制表示是:1011.
```

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

Solution:
### 遍历

根据题意,对 $n$  的每一位进行遍历检查。

代码:
```Java
class Solution {
    public boolean hasAlternatingBits(int n) {
        int cur = -1;
        while (n != 0) {
            int u = n & 1;
            if ((cur ^ u) == 0) return false;
            cur = u; n >>= 1;
        }
        return true;
    }
}
```
* 时间复杂度:$O(\log{n})$
* 空间复杂度:$O(1)$

---

### 位运算

另外一种更为巧妙的方式是利用交替位二进制数性质。

当给定值 $n$ 为交替位二进制数时,将 $n$ 右移一位得到的值 $m$ 仍为交替位二进制数,且与原数 $n$ 错开一位,两者异或能够得到形如 $0000...1111$ 的结果 $x$,此时对 $x$ 执行加法(进位操作)能够得到形如 $0000...10000$ 的结果,将该结果与 $x$ 执行按位与后能够得到全 $0$ 结果。

代码:
```Java
class Solution {
    public boolean hasAlternatingBits(int n) {
        int x = n ^ (n >> 1);
        return (x & (x + 1)) == 0;
    }
}
```
* 时间复杂度:$O(1)$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


Question:
给定一个非空且只包含非负数的整数数组 nums,数组的度的定义是指数组里任一元素出现频数的最大值。

你的任务是在 nums 中找到与 nums 拥有相同大小的度的最短连续子数组,返回其长度。

示例 1:
```
输入:[1, 2, 2, 3, 1]
输出:2
解释:
输入数组的度是2,因为元素1和2的出现频数最大,均为2.
连续子数组里面拥有相同度的有如下所示:
[1, 2, 2, 3, 1], [1, 2, 2, 3], [2, 2, 3, 1], [1, 2, 2], [2, 2, 3], [2, 2]
最短连续子数组[2, 2]的长度为2,所以返回2.
```
示例 2:
```
输入:[1,2,2,3,1,4,2]
输出:6
```

提示:
* nums.length 在1到 50,000 区间范围内。
* nums[i] 是一个在 0 到 49,999 范围内的整数。

Solution:
### 数组计数

由于已知值的范围是 `[0, 49999]`。

我们可以使用数组 `cnt` 来统计每个值出现的次数,数组 `first` 和 `last` 记录每个值「首次出现」和「最后出现」的下标。

同时统计出最大词频为 `max`。

然后再遍历一次数组,对于那些满足词频为 `max` 的数值进行长度计算。

```Java
class Solution {
    int N = 50009;
    public int findShortestSubArray(int[] nums) {
        int n = nums.length;
        int[] cnt = new int[N];
        int[] first = new int[N], last = new int[N];
        Arrays.fill(first, -1);
        int max = 0;
        for (int i = 0; i < n; i++) {
            int t = nums[i];
            max = Math.max(max, ++cnt[t]);
            if (first[t] == -1) first[t] = i;
            last[t] = i;
        }
        int ans = Integer.MAX_VALUE;
        for (int i = 0; i < n; i++) {
            int t = nums[i];
            if (cnt[t] == max) ans = Math.min(ans, last[t] - first[t] + 1);
        }
        return ans;
    }
}
```
* 时间复杂度:对数组进行常数次扫描。复杂度为 $O(n)$
* 空间复杂度:$O(n)$

***

### 哈希表计数

同样的,除了使用静态数组,我们还可以使用哈希表进行计数。

```java
class Solution {
    public int findShortestSubArray(int[] nums) {
        int n = nums.length;
        Map<Integer, Integer> cnt = new HashMap<>();
        Map<Integer, Integer> first = new HashMap<>(), last = new HashMap<>();
        int max = 0;
        for (int i = 0; i < n; i++) {
            int t = nums[i];
            cnt.put(t, cnt.getOrDefault(t, 0) + 1);
            max = Math.max(max, cnt.get(t));
            if (!first.containsKey(t)) first.put(t, i);
            last.put(t, i);
        }
        int ans = Integer.MAX_VALUE;
        for (int i = 0; i < n; i++) {
            int t = nums[i];
            if (cnt.get(t) == max) {
                ans = Math.min(ans, last.get(t) - first.get(t) + 1);
            }
        }
        return ans;
    }
}
```
* 时间复杂度:对数组进行常数次扫描。复杂度为 $O(n)$
* 空间复杂度:$O(n)$

***

### 总结

我们知道 `哈希表 = 哈希函数 + 数组`,由于哈希函数计算需要消耗时间(Java 中首先涉及自动装箱/拆箱,之后还要取对象的 hashCode 进行右移异或,最后才计算哈希桶的下标),以及处理哈希冲突的开销。

其效率必然比不上使用我们静态数组进行计数。

因此我建议,对于那些 **数值范围确定且不太大($10^6$ 以内都可以使用,本题数量级在 $10^4$)** 的计算场景,使用数组进行计数,而不是使用哈希表。

### 最后

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

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

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

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


698. 划分为k个相等的子集

Question:
给定一个整数数组  `nums` 和一个正整数 `k`,找出是否有可能把这个数组分成 `k` 个非空子集,其总和都相等。

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

输出: True

说明: 有可能将其分成 4 个子集(5),(1,4),(2,3),(2,3)等于总和。
```
示例 2:
```
输入: nums = [1,2,3,4], k = 3

输出: false
```

提示:
* $1 <= k <= len(nums) <= 16$
* $0 < nums[i] < 10000$
* 每个元素的频率在 $[1,4]$ 范围内

Solution:
### 搜索 + 剪枝(贪心策略)

将 $n$ 个数均分成 $k$ 份,且 $n$ 与 $k$ 数据范围为 $16$,容易想到搜索剪枝。

先对一些显然的无解情况进行分析:记 $tot = \sum_{i = 0}^{n - 1}nums[i]$,**若 $tot$ 不为 $k$ 的倍数,必然是无法实现均分,直接返回 `false`(可行性剪枝)**。同时可知单个集合的总和为 $t = \frac{tot}{k}$。

设计搜索函数为 `boolean dfs(int idx, int cur, int cnt, boolean[] vis)`,各项参数含义如下:
* `cur` 为当前集合的元素和(初始值为 $0$);
* `cnt` 是已凑成多少个总和为 $t$ 的集合(初始值为 $0$,当 $cnt = k$ 时,我们搜索到了合法方案,返回 `true`,否则对 `cnt` 进行加一操作,并将 `cur` 置零,搜索下个集合);
* `vis` 用于记录哪些 $nums[i]$ 已被使用;
* `idx` 是搜索关键,其含义为搜索空间的分割点。即对于当前正在搜索的集合,我们不会每次都扫描整个 `nums` 来找添加到该集合的下一个元素,而是能够明确下一个元素必然在 $idx$ 的左边或右边。
具体的,我们知道若最终存在合法方案,必然每个 $nums[i]$ 都均棣属于某个具体集合。我们考虑搜索某个集合的组成元素时,按照「从大到小」的方式进行搜索(起始先对 `nums` 进行排序),这样能够**确保若上一个被加入该集合的元素为 $nums[i]$,则下一个被添加的元素 $nums[j]$ 必然位于 $nums[i]$ 的左边,即从下标 $i - 1$ 开始往前搜索(顺序性剪枝)**;
同时,也正是我们按照「从大到小」的方式进行搜索,**确保了当前集合的搜索,无须对已搜索到的集合进行调整**。
也就是说我们搜索的第一个集合是所有 $nums[i]$ 中的最大值所在的那个集合;二次搜索是所有 $nums[i]$ 减去第一个集合后剩余元素中最大值所在的集合 ...
这引导我们,**如果当前集合如果连第一个值都无法搜到(即剩余元素的最大值不能作为当前集合的元素),必然无解(可行性剪枝)**。

这样的「搜索 + 剪枝」的解法本质是利用了「贪心」来做策略:**我们每个回合的搜索总是在搜索「剩余未使用元素的最大值」所在的那个集合,并且按照「优先使用大数值」的原则来构造。**

可证明该做法的正确性:由于搜索的是「剩余未使用元素的最大值」所在的那个集合,因此剩余未使用元素必然在集合内,若被搜索到的其余元素参与集合构造导致有解变无解(即需要将其余元素进行替换才能确保有解),根据我们「从大到小」搜索下一个元素原则,替换过程必然不会使集合元素个数变少,即总是会拿不少于 $K$ 个的元素来替换当前集合的 $K$ 个元素(总和相同),从而可推断该替换并非必须。

Java 代码:
```Java
class Solution {
    int[] nums;
    int n, t, k;
    public boolean canPartitionKSubsets(int[] _nums, int _k) {
        nums = _nums; k = _k;
        int tot = 0;
        for (int x : nums) tot += x;
        if (tot % k != 0) return false; // 可行性剪枝
        Arrays.sort(nums);
        n = nums.length; t = tot / k;
        return dfs(n - 1, 0, 0, new boolean[n]);
    }
    boolean dfs(int idx, int cur, int cnt, boolean[] vis) {
        if (cnt == k) return true;
        if (cur == t) return dfs(n - 1, 0, cnt + 1, vis);
        for (int i = idx; i >= 0; i--) {  // 顺序性剪枝
            if (vis[i] || cur + nums[i] > t) continue;  // 可行性剪枝
            vis[i] = true;
            if (dfs(i - 1, cur + nums[i], cnt, vis)) return true;
            vis[i] = false;
            if (cur == 0) return false; // 可行性剪枝
        }
        return false;
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    vector<int> nums;
    int n, t, k;
    bool dfs(int idx, int cur, int cnt, vector<bool>& vis) {
        if (cnt == k) return true;
        if (cur == t) return dfs(n - 1, 0, cnt + 1, vis);
        for (int i = idx; i >= 0; i--) {
            if (vis[i] || cur + nums[i] > t) continue;
            vis[i] = true;
            if (dfs(i - 1, cur + nums[i], cnt, vis)) return true;
            vis[i] = false;
            if (cur == 0) return false;
        }
        return false;
    }
    bool canPartitionKSubsets(vector<int>& _nums, int _k) {
        nums = _nums; k = _k;
        int tot = 0;
        for (int x : nums) tot += x;
        if (tot % k != 0) return false;
        sort(nums.begin(), nums.end());
        n = nums.size(); t = tot / k;
        vector<bool> vis(n, false);
        return dfs(n - 1, 0, 0, vis);
    }
};
```
Python 代码:
```Python
class Solution:
    def canPartitionKSubsets(self, _nums, _k):
        nums, k = _nums, _k
        tot = sum(nums)
        if tot % k != 0:
            return False
        nums.sort()
        n, t = len(nums), tot // k
        def dfs(idx, cur, cnt, vis):
            if cnt == k:
                return True
            if cur == t:
                return dfs(n - 1, 0, cnt + 1, vis)
            for i in range(idx, -1, -1):
                if vis[i] or cur + nums[i] > t:
                    continue
                vis[i] = True
                if dfs(i - 1, cur + nums[i], cnt, vis):
                    return True
                vis[i] = False
                if cur == 0:
                    return False
            return False
        return dfs(n - 1, 0, 0, [False] * n)
```
TypeScript 代码:
```TypeScript
let nums: number[];
let n: number, t: number, k: number;
function canPartitionKSubsets(_nums: number[], _k: number): boolean {
    nums = _nums; k = _k;
    let tot = 0
    for (let x of nums) tot += x
    if (tot % k != 0) return false
    nums.sort((a,b)=>a-b)
    n = nums.length; t = tot / k
    return dfs(n - 1, 0, 0, new Array<boolean>(n).fill(false))
};
function dfs(idx: number, cur: number, cnt: number, vis: boolean[]): boolean {
    if (cnt == k) return true
    if (cur == t) return dfs(n - 1, 0, cnt + 1, vis)
    for (let i = idx; i >= 0; i--) {
        if (vis[i] || cur + nums[i] > t) continue
        vis[i] = true
        if (dfs(idx - 1, cur + nums[i], cnt, vis)) return true
        vis[i] = false
        if (cur == 0) return false
    }
    return false
}
```
* 时间复杂度:爆搜剪枝不分析时空复杂度
* 空间复杂度:爆搜剪枝不分析时空复杂度

---

### 模拟退火

数据范围为 $16$ 自然也是很好的调参运用题。

**因为将 $n$ 个数划分为 $k$ 份,等效于用 $n$ 个数构造出一个「特定排列」,然后对「特定排列」进行固定模式的构造逻辑,就能实现「答案」与「目标排列」的对应关系。**

基于此,我们可以使用「模拟退火」进行求解。

单次迭代的基本流程:

1. 随机选择两个下标,计算「交换下标元素前对应序列的得分」&「交换下标元素后对应序列的得分」
2. 如果温度下降(交换后的序列更优),进入下一次迭代
3. 如果温度上升(交换前的序列更优),以「一定的概率」恢复现场(再交换回来)

我们可以制定如下规则来衡量当前排列与合法排列的差异程度:将当前的 `nums` 从前往后划分成总和不超过 $t$ 的 $k$ 份,并将 $k$ 份与 $t$ 的差值之和,作为对当前排列的差异程度评级 `diff`,当出现 `diff = 0` 说明找到了合法排列,可结束搜索。

该计算规则可确保排列变化的连续性能有效体现在 `diff` 数值上。

Java 代码(`2024/06/01` 可通过):
```Java
class Solution {
    int[] nums;
    int n, tval, k;
    Random random = new Random(20220920);
    double hi = 1e9, lo = 1e-4, fa = 0.95;
    int N = 600;
    boolean ans;
    int calc() {
        int diff = tval * k;
        for (int i = 0, j = 0; i < n && j < k; j++) {
            int cur = 0;
            while (i < n && cur + nums[i] <= tval) cur += nums[i++];
            diff -= cur;
        }
        if (diff == 0) ans = true;
        return diff;
    }
    void sa() {
        shuffle(nums);
        for (double t = hi; t > lo && !ans; t *= fa) {
            int a = random.nextInt(n), b = random.nextInt(n);
            if (a == b) continue;
            int prev = calc();
            swap(nums, a, b);
            int cur = calc();
            int diff = cur - prev;
            if (Math.log(diff / t) > random.nextDouble()) swap(nums, a, b);
        }
    }
    public boolean canPartitionKSubsets(int[] _nums, int _k) {
        nums = _nums; k = _k;
        int tot = 0;
        for (int x : nums) tot += x;
        if (tot % k != 0) return false;
        n = nums.length; tval = tot / k;
        while (!ans && N-- > 0) sa();
        return ans;
    }
    void shuffle(int[] nums) {
        for (int i = n; i > 0; i--) swap(nums, random.nextInt(i), i - 1);
    }
    void swap(int[] nums, int a, int b) {
        int c = nums[a];
        nums[a] = nums[b];
        nums[b] = c;
    }
}
```
TypeScript 代码(`2024/06/01` 可通过):
```TypeScript
let nums: number[];
let n: number, tval: number, k: number
let ans: boolean
let hi: number, lo: number, fa: number, N: number
function getRandom(max: number): number {
    return Math.floor(Math.random() * (max + 1))
}
function calc(): number {
    let diff = tval * k
    for (let i = 0, j = 0; i < n && j < k; j++) {
        let cur = 0
        while (i < n && cur + nums[i] <= tval) cur += nums[i++]
        diff -= cur
    }
    if (diff == 0) ans = true
    return diff
}
function sa(): void {
    shuffle(nums)
    for (let t = hi; t > lo && !ans; t *= fa) {
        const a = getRandom(n - 1), b = getRandom(n - 1)
        if (a == b) continue
        const prev = calc()
        swap(nums, a, b)
        const cur = calc()
        const diff = cur - prev
        if (Math.log(diff / t) > Math.random()) swap(nums, a, b)
    }
}
function canPartitionKSubsets(_nums: number[], _k: number): boolean {
    nums = _nums; k = _k
    let tot = 0
    for (let x of nums) tot += x
    if (tot % k != 0) return false
    n = nums.length; tval = tot / k
    hi = 1e7; lo = 1e-4; fa = 0.977; N = 420;
    ans = false
    while (!ans && N-- > 0) sa()
    return ans
}
function shuffle(nums: number[]): void {
    for (let i = n; i > 0; i--) swap(nums, getRandom(i), i - 1)
}
function swap(nums: number[], a: number, b: number): void {
    const c = nums[a]
    nums[a] = nums[b]
    nums[b] = c
}
```
* 时间复杂度:启发式搜索不分析时空复杂度
* 空间复杂度:启发式搜索不分析时空复杂度

### 最后

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

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

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

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