LeetCode/1211-1220/1220. 统计元音字母序列的数目（困难）.md

1220. 统计元音字母序列的数目

Question:
给你一个整数 `n`,请你帮忙统计一下我们可以按下述规则形成多少个长度为 `n` 的字符串:

字符串中的每个字符都应当是小写元音字母('a', 'e', 'i', 'o', 'u')
* 每个元音 `'a'` 后面都只能跟着 `'e'`
* 每个元音 `'e'` 后面只能跟着 `'a'` 或者是 `'i'`
* 每个元音 `'i'` 后面 不能 再跟着另一个 `'i'`
* 每个元音 `'o'` 后面只能跟着 `'i'` 或者是 `'u'`
* 每个元音 `'u'` 后面只能跟着 `'a'`

由于答案可能会很大,所以请你返回 模 $10^9 + 7$ 之后的结果。

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

输出:5

解释:所有可能的字符串分别是:"a", "e", "i" , "o" 和 "u"。
```
示例 2:
```
输入:n = 2

输出:10

解释:所有可能的字符串分别是:"ae", "ea", "ei", "ia", "ie", "io", "iu", "oi", "ou" 和 "ua"。
```
示例 3:
```
输入:n = 5

输出:68
```

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

Solution:
### 线性 DP

**定义 $f[i][j]$ 为考虑长度为 $i + 1$ 的字符串,且结尾元素为 $j$ 的方案数(其中 $j$ 代表数组 `['a', 'e', 'i', 'o', 'u']` 下标)。**

不失一般性考虑 $f[i][j]$ 该如何计算。

**我们可以从题意给定的规则进行出发,从 $f[i]$ 出发往前更新 $f[i + 1]$,也可以直接利用对称性进行反向分析。**

为了方便大家理解,还是将常规的「从 $f[i]$ 出发往前更新 $f[i + 1]$」作为主要分析方法吧。

根据条件可以容易写出转移方程:

* 每个元音 `'a'` 后面都只能跟着 `'e'` :$f[i + 1][1] += f[i][0]$;
* 每个元音 `'e'` 后面只能跟着 `'a'` 或者是 `'i'` :$f[i + 1][0] += f[i][1]$、$f[i + 1][2] += f[i][1]$;
* 每个元音 `'i'` 后面 不能 再跟着另一个 `'i'` :$f[i + 1][j] += f[i][2], (j 不能为 2)$;
* 每个元音 `'o'` 后面只能跟着 `'i'` 或者是 `'u'` :$f[i + 1][2] += f[i][3]$、$f[i + 1][4] += f[i][3]$;
* 每个元音 `'u'` 后面只能跟着 `'a'` :$f[i + 1][0] += f[i][4]$。

**代码(利用对称性统计方案数代码见 $P2$,感谢 [@Benhao](/u/himymben/) 和 [@5cm/s 🌸](/u/megurine/) 提供的、我根本看不懂的、试图把困难题也做成「全鱼宴」题解的代码 🤣 ):**
```Java
class Solution {
    int MOD = (int)1e9+7;
    public int countVowelPermutation(int n) {
        long[][] f = new long[n][5];
        Arrays.fill(f[0], 1);
        for (int i = 0; i < n - 1; i++) {
            // 每个元音 'a' 后面都只能跟着 'e'
            f[i + 1][1] += f[i][0];
            // 每个元音 'e' 后面只能跟着 'a' 或者是 'i'
            f[i + 1][0] += f[i][1];
            f[i + 1][2] += f[i][1];
            // 每个元音 'i' 后面 不能 再跟着另一个 'i'
            f[i + 1][0] += f[i][2];
            f[i + 1][1] += f[i][2];
            f[i + 1][3] += f[i][2];
            f[i + 1][4] += f[i][2];
            // 每个元音 'o' 后面只能跟着 'i' 或者是 'u'
            f[i + 1][2] += f[i][3];
            f[i + 1][4] += f[i][3];
            // 每个元音 'u' 后面只能跟着 'a'
            f[i + 1][0] += f[i][4];
            for (int j = 0; j < 5; j++) f[i + 1][j] %= MOD;
        }
        long ans = 0;
        for (int i = 0; i < 5; i++) ans += f[n - 1][i];
        return (int)(ans % MOD);
    }
}
```
-
```Java
class Solution {
    int MOD = (int)1e9+7;
    public int countVowelPermutation(int n) {
        long[][] f = new long[n][5];
        Arrays.fill(f[0], 1);
        for (int i = 1; i < n; i++) {
            f[i][0] = f[i - 1][1];
            f[i][1] = f[i - 1][0] + f[i - 1][2];
            f[i][2] = f[i - 1][0] + f[i - 1][1] + f[i - 1][3] + f[i - 1][4];
            f[i][3] = f[i - 1][2] + f[i - 1][4];
            f[i][4] = f[i - 1][0];
            for (int j = 0; j < 5; j++) f[i][j] %= MOD;
        }
        long ans = 0;
        for (int i = 0; i < 5; i++) ans += f[n - 1][i];
        return (int)(ans % MOD);
    }
}
```
-

```Python
MOD = int(1e9) + 7
class Solution:
    def countVowelPermutation(self, n: int) -> int:
        f = [[1] * 5] + [[0] * 5 for _ in range(n - 1)]
        for i in range(1, n):
            f[i][0] = f[i-1][1]
            f[i][1] = (f[i-1][0] + f[i-1][2]) % MOD
            f[i][2] = ((((f[i-1][0] + f[i-1][1]) % MOD + f[i-1][3]) % MOD) + f[i-1][4]) % MOD
            f[i][3] = (f[i-1][2] + f[i-1][4]) % MOD
            f[i][4] = f[i-1][0]
        return (((((f[-1][0] + f[-1][1]) % MOD + f[-1][2]) % MOD + f[-1][3]) % MOD) + f[-1][4]) % MOD
```
-

```Go
const MOD int = 1e9 + 7
func countVowelPermutation(n int) int {
    f := make([][]int, n)
    f[0] = []int{1, 1, 1, 1, 1}
    for i := 1; i < n; i++ {
        f[i] = make([]int, 5)
        f[i][0] = f[i-1][1]
        f[i][1] = (f[i-1][0] + f[i-1][2]) % MOD
        f[i][2] = (((f[i-1][0] + f[i-1][1]) % MOD + f[i-1][3]) % MOD + f[i-1][4]) % MOD
        f[i][3] = (f[i-1][2] + f[i-1][4]) % MOD
        f[i][4] = f[i-1][0]
    }
    return ((((f[n-1][0] + f[n-1][1]) % MOD + f[n-1][2]) % MOD + f[n-1][3]) % MOD + f[n-1][4]) % MOD
}
```
-
```Elixir
defmodule Solution do
  @spec count_vowel_permutation(n :: integer) :: integer
  def count_vowel_permutation(n) do
    solve(n, 1, 1, 1, 1, 1)
  end
  def solve(1, a, e, i, o, u) do
    rem((a + e + i + o + u), 1000000007)
  end
  def solve(n, a, e, i, o, u) do
    solve(n - 1, add(e, i, u), add(a, i), add(e, o), i, add(i, o))
  end
  def add(n1, n2) do
    add(n1, n2, 0)
  end
  def add(n1, n2, n3) do
    rem(n1 + n2 + n3, 1000000007)
  end
end
```
-
```Erlang
-spec count_vowel_permutation(N :: integer()) -> integer().
count_vowel_permutation(N) ->
  solve(N, 1, 1, 1, 1, 1).

solve(1, A, E, I, O, U) ->
  (A + E + I + O + U) rem 1000000007;
solve(N, A, E, I, O, U) ->
  solve(N - 1, add(E, I, U), add(A, I), add(E, O), I, add(I, O)).
add(N1, N2) ->
  add(N1, N2, 0).
add(N1, N2, N3) ->
  (N1 + N2 + N3) rem 1000000007.
```
-
```Racket
(define/contract (count-vowel-permutation n)
  (-> exact-integer? exact-integer?)
    (define MOD 1000000007)
    (define (ad nums [tot 0])
      (cond
        [(empty? nums) (remainder tot MOD)]
        [else (ad (cdr nums) (+ tot (car nums)))]
      )
    )
    (let loop([n n] [a 1] [e 1] [i 1] [o 1] [u 1])
      (cond
        [(= n 1) (ad (list a e i o u))]
        [else (loop (- n 1) (ad (list e i u)) (ad (list a i)) (ad (list e o)) i (ad (list i o)))]
      )
    )
    )
```
-
```JS
const MOD = 10 ** 9 + 7;
const add = (...nums) => nums.reduce((a, b) => a + b) % MOD
var countVowelPermutation = function(n,a=1,e=1,i=1,o=1,u=1) {
    return n === 1
        ? add(a, e, i, o, u)
        : countVowelPermutation(n - 1, add(e, i, u), add(a, i), add(e, o), i, add(i, o))
};
```
-
```JS
const MOD = 10 ** 9 + 7;
const add = (...nums) => nums.reduce((a, b) => a + b) % MOD
var countVowelPermutation = function(n) {
    let [a, e, i, o, u] = [1, 1, 1, 1, 1]
    while(--n)
        ([a, e, i, o, u] = [add(e, i, u), add(a + i), add(e + o), i, add(i, o)])
    return add(a, e, i, o, u)
};
```
* 时间复杂度:令 $C$ 为字符集大小,本题 $C$ 固定为 $5$。整体复杂度为 $O(n * C)$
* 空间复杂度:$O(n * C)$

---

### 矩阵快速幂

**通常考虑递推能否使用「矩阵快速幂」来加速,主要依据为该递推过程是否为存在「结合律」线性递推过程。**

如果是存在「结合律」的线性数列递推,诸如「斐波那契数列」等,都能使用「矩阵快速幂」来加速数列递推。

对「矩阵快速幂」不了解的同学,可以先看「矩阵快速幂の三部曲」(学过三部曲的同学做这道题应该是降维打击):

1. [简单题学「矩阵快速幂」](https://mp.weixin.qq.com/s?__biz=MzU4NDE3MTEyMA==&mid=2247488198&idx=1&sn=8272ca6b0ef6530413da4a270abb68bc&chksm=fd9cb9d9caeb30cf6c2defab0f5204adc158969d64418916e306f6bf50ae0c38518d4e4ba146&token=684485087&lang=zh_CN#rd)
2. [简单题学「矩阵快速幂」II](https://mp.weixin.qq.com/s?__biz=MzU4NDE3MTEyMA==&mid=2247488373&idx=1&sn=2845f7d6966658f1bb82e1dfb086dfad&chksm=fd9cb86acaeb317c3cba3aea9e63ae728749b74482494448151643bedb783d6597d7c640774b&token=684485087&lang=zh_CN#rd)
3. [矩阵快速幂的运用 :从「状态机 DP」到「矩阵快速幂」](https://mp.weixin.qq.com/s?__biz=MzU4NDE3MTEyMA==&mid=2247488607&idx=1&sn=060d3e6c34344e05218f81878651fe02&chksm=fd9cbf40caeb3656f9308ea0190fac700801838356e63196c3b1b3459a5f01c646e7e9abe2fd&token=684485087&lang=zh_CN#rd)

我们最终需要求的是 $\sum_{i = 0}^{4} f[n - 1][i]$,将需要求得的部分列成列向量:

$$
ans = \begin{bmatrix}
f[n - 1][0]\\
f[n - 1][1]\\
f[n - 1][2]\\
f[n - 1][3]\\
f[n - 1][4]
\end{bmatrix}
$$

同时我们有起始的矩阵(每个元音字母独立成为一个字符):

$$
ori =
\begin{bmatrix}
f[0][0]\\
f[0][1]\\
f[0][2]\\
f[0][3]\\
f[0][4]
\end{bmatrix} = \begin{bmatrix}
1\\
1\\
1\\
1\\
1
\end{bmatrix}
$$

我们知道 $f[i][X]$ 依赖于 $f[i - 1][X]$,同时在「解法一」中明确了各个 $f[i][j]$ 与 $f[i - 1][X]$ 的关系。

根据「矩阵乘法」,不难发现:

$$
\begin{bmatrix}
f[i][0]\\
f[i][1]\\
f[i][2]\\
f[i][3]\\
f[i][4]
\end{bmatrix} =
\begin{bmatrix}
0& 1& 0& 0& 0\\
1& 0& 1& 0& 0\\
1& 1& 0& 1& 1\\
0& 0& 1& 0& 1\\
1& 0& 0& 0& 0
\end{bmatrix}
*
\begin{bmatrix}
f[i - 1][0]\\
f[i - 1][1]\\
f[i - 1][2]\\
f[i - 1][3]\\
f[i - 1][4]
\end{bmatrix}
$$

我们令:
$$
mat = \begin{bmatrix}
0& 1& 0& 0& 0\\
1& 0& 1& 0& 0\\
1& 1& 0& 1& 1\\
0& 0& 1& 0& 1\\
1& 0& 0& 0& 0
\end{bmatrix}
$$

根据递推关系,可得:

$$
ans = mat * mat * ... * mat * ori
$$

再根据矩阵乘法具有「结合律」,最终可得:

$$
ans = mat^{n - 1} * ori
$$

**代码(感谢 [@Benhao](/u/himymben/) 同学提供的「行向量」写法):**
```Java
class Solution {
    int MOD = (int)1e9+7;
    long[][] mul(long[][] a, long[][] b) {
        int r = a.length, c = b[0].length, z = b.length;
        long[][] ans = new long[r][c];
        for (int i = 0; i < r; i++) {
            for (int j = 0; j < c; j++) {
                for (int k = 0; k < z; k++) {
                    ans[i][j] += a[i][k] * b[k][j];
                    ans[i][j] %= MOD;
                }
            }
        }
        return ans;
    }
    public int countVowelPermutation(int n) {
        long[][] ans = new long[][]{
            {1}, {1}, {1}, {1}, {1}
        };
        long[][] mat = new long[][]{
            {0, 1, 0, 0, 0},
            {1, 0, 1, 0, 0},
            {1, 1, 0, 1, 1},
            {0, 0, 1, 0, 1},
            {1, 0, 0, 0, 0},
        };
        int x = n - 1;
        while (x != 0) {
            if ((x & 1) != 0) ans = mul(mat, ans);
            mat = mul(mat, mat);
            x >>= 1;
        }
        long sum = 0;
        for (int i = 0; i < 5; i++) sum += ans[i][0];
        return (int)(sum % MOD);
    }
}
```
-
```python
import numpy as np
MOD = 10 ** 9 + 7
dtype = np.dtype('uint64')
class Solution:
    def countVowelPermutation(self, n: int) -> int:
        ans, mat = np.ones(5, dtype=dtype), np.array([[0, 1, 1, 0, 1], [1, 0, 1, 0, 0], [0, 1, 0, 1, 0], [0, 0, 1, 0, 0], [0, 0, 1, 1, 0]],dtype=dtype)
        n -= 1
        while n:
            if n & 1:
                ans = ans @ mat % MOD
            mat = mat @ mat % MOD
            n >>= 1
        return int(ans.sum()) % MOD
```
-
```Go
const MOD int = 1e9 + 7

func countVowelPermutation(n int) (res int) {
    mul := func(X, Y [][]int) [][]int{
        res := make([][]int, len(X))
        for i := 0;i < len(res);i++{res[i] = make([]int, len(Y[0]))}
        for i := 0;i < len(res);i++{
            for j := 0;j < len(res[0]);j++{
                for k := 0;k < len(Y);k++{
                    res[i][j] = (res[i][j] + X[i][k] * Y[k][j] % MOD) % MOD
                }
            }
        }
        return res
    }

    ans, mat := [][]int{{1, 1, 1, 1, 1}}, [][]int{{0, 1, 1, 0, 1}, {1, 0, 1, 0, 0}, {0, 1, 0, 1, 0}, {0, 0, 1, 0, 0}, {0, 0, 1, 1, 0}}
    n--
    for n > 0 {
        if(n & 1 != 0){
            tmp := mul(ans, mat)
            ans = tmp
        }
        tmpMat := mul(mat, mat)
        mat = tmpMat
        n >>= 1
    }
    for _, row := range ans {
        for _, cell := range row{
            res = (res + cell) % MOD
        }
    }
    return
}
```
* 时间复杂度:共需要执行 $\log{n}$ 次矩阵操作,运算量最大的矩阵操作为 $mat * mat$,复杂度为 $C^3$。整体复杂度为 $O(\log{n} * C^3)$
* 空间复杂度:复杂度上界为 $mat$ 大小,复杂度为 $O(C^2)$

### 最后

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

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

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

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


LeetCode/1221-1230/1129. 颜色交替的最短路径（中等）.md

1129. 颜色交替的最短路径

Question:
在一个有向图中,节点分别标记为 `0, 1, ..., n-1`。

图中每条边为红色或者蓝色,且存在自环或平行边。

`red_edges` 中的每一个 `[i, j]` 对表示从节点 `i` 到节点 `j` 的红色有向边。

类似地,`blue_edges` 中的每一个 `[i, j]` 对表示从节点 `i` 到节点 `j` 的蓝色有向边。

返回长度为 `n` 的数组 `answer`,其中 `answer[X]` 是从节点 `0` 到节点 `X` 的红色边和蓝色边交替出现的最短路径的长度。

如果不存在这样的路径,那么 `answer[x] = -1`。

示例 1:
```
输入:n = 3, red_edges = [[0,1],[1,2]], blue_edges = []

输出:[0,1,-1]
```
示例 2:
```
输入:n = 3, red_edges = [[0,1]], blue_edges = [[2,1]]

输出:[0,1,-1]
```
示例 3:
```
输入:n = 3, red_edges = [[1,0]], blue_edges = [[2,1]]

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

输出:[0,1,2]
```
示例 5:
```
输入:n = 3, red_edges = [[0,1],[0,2]], blue_edges = [[1,0]]

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

提示:
* `1 <= n <= 100`
* `red_edges.length <= 400`
* `blue_edges.length <= 400`
* `red_edges[i].length = blue_edges[i].length = 2`
* `0 <= red_edges[i][j], blue_edges[i][j] < n`

Solution:
### 朴素 BFS

为了方便,将 `redEdges` 记为 `rs`,将 `blueEdges` 记为 `bs`。

使用两数组进行建图,利用点数和边数关系(稀疏图),使用「邻接表」方式进行建图。

将所有红色有向边权值记为 `1`,将所有蓝色有向边权值记为 `-1`。注意这里的权重仅表示该边的颜色,并非代表经过该边的真实成本。

也正是经过所有边的成本均相同,对于原问题「从 `0` 号节点进行出发,求到所有点的颜色交替的最短路径」,我们容易想到使用 `BFS`  进行求解。

`BFS`  过程中将三元组 $(point, last, step)$ 进行入队:

* `point` 代表当前所在点编号
* `last` 代表到达 `point` 所使用的边的颜色,默认为 `0` 代表不需要经过任何边到达起点
* `step` 代表到达 `point` 所使用的步数

利用数据范围不大(点数为 $100$,边数为 $800$),可以直接对每个点进行一次独立的 `BFS`  来求最短路。由于图存在重边和自环,因此在求解最短路的过程需要对边进行去重。

Java 代码:
```Java
class Solution {
    static int N = 110, M = 810, idx = 0;
    static int[] he = new int[N], e = new int[M], ne = new int[M], w = new int[M];
    void add(int a, int b, int c) {
        e[idx] = b;
        w[idx] = c;
        ne[idx] = he[a];
        he[a] = idx++;
    }
    public int[] shortestAlternatingPaths(int n, int[][] rs, int[][] bs) {
        idx = 0;
        Arrays.fill(he, -1);
        for (int[] e : rs) add(e[0], e[1], 1);
        for (int[] e : bs) add(e[0], e[1], -1);
        int[] ans = new int[n];
        boolean[] vis = new boolean[rs.length + bs.length];
        out:for (int k = 1; k < n; k++) {
            ans[k] = -1;
            Arrays.fill(vis, false);
            Deque<int[]> d = new ArrayDeque<>();
            d.addLast(new int[]{0, 0, 0}); // point, last, step
            while (!d.isEmpty()) {
                int[] info = d.pollFirst();
                int p = info[0], last = info[1], step = info[2];
                for (int i = he[p]; i != -1; i = ne[i]) {
                    int j = e[i], c = w[i];
                    if (vis[i]) continue;
                    if (c + last == 0 || last == 0) {
                        if (j == k) {
                            ans[k] = step + 1;
                            continue out;
                        } else {
                            d.addLast(new int[]{j, c, step + 1});
                            vis[i] = true;
                        }
                    }
                }
            }
        }
        return ans;
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    int he[110], e[810], ne[810], w[810], idx = 0;
    void add(int a, int b, int c) {
        e[idx] = b;
        w[idx] = c;
        ne[idx] = he[a];
        he[a] = idx++;
    }
    vector<int> shortestAlternatingPaths(int n, vector<vector<int>>& rs, vector<vector<int>>& bs) {
        idx = 0;
        fill(he, he + n, -1);
        for (auto& e : rs) add(e[0], e[1], 1);
        for (auto& e : bs) add(e[0], e[1], -1);
        vector<int> ans(n);
        vector<bool> vis(rs.size() + bs.size());
        for (int k = 1; k < n; k++) {
            ans[k] = -1;
            fill(vis.begin(), vis.end(), false);
            queue<vector<int>> q;
            q.push({ 0, 0, 0 }); // point, last, step
            bool out = false;
            while (!q.empty()) {
                if (out) break;
                vector<int> info = q.front(); q.pop();
                int p = info[0], last = info[1], step = info[2];
                for (int i = he[p]; i != -1; i = ne[i]) {
                    int j = e[i], c = w[i];
                    if (vis[i]) continue;
                    if (c + last == 0 || last == 0) {
                        if (j == k) {
                            ans[k] = step + 1;
                            out = true;
                            break;
                        } else {
                            q.push({ j, c, step + 1 });
                            vis[i] = true;
                        }
                    }
                }
            }
        }
        return ans;
    }
};
```
* 时间复杂度:将点数记为 `n` ,边数记为 `m`。建图复杂度为 $O(m)$;构造答案复杂度为 $O(n \times (n + m))$。整体复杂度为 $O(n \times (n + m))$
* 空间复杂度:$O(n + m)$

---

### 优化

实际上,我们并没有对每个点进行独立 `BFS`  的必要。

为了获取所有从节点 `0` 出发的最短路,可直接从节点 `0` 进行出发(对边进行去重),所有能够从节点 `0` 沿交替路径到达的节点必然都会被访问到,且节点首次被访问到时必然是最短路径。

因此我们只需要初始化所有的 `ans[i] = -1`,并从节点 `0` 开始进行一次 `BFS`  即可。

Java 代码:
```Java
class Solution {
    static int N = 110, M = 810, idx = 0;
    static int[] he = new int[N], e = new int[M], ne = new int[M], w = new int[M];
    void add(int a, int b, int c) {
        e[idx] = b;
        w[idx] = c;
        ne[idx] = he[a];
        he[a] = idx++;
    }
    public int[] shortestAlternatingPaths(int n, int[][] rs, int[][] bs) {
        idx = 0;
        Arrays.fill(he, -1);
        for (int[] e : rs) add(e[0], e[1], 1);
        for (int[] e : bs) add(e[0], e[1], -1);
        int[] ans = new int[n];
        Arrays.fill(ans, -1);
        ans[0] = 0;
        boolean[] vis = new boolean[rs.length + bs.length];
        Arrays.fill(vis, false);
        Deque<int[]> d = new ArrayDeque<>();
        d.addLast(new int[]{0, 0, 0}); // point, last, step
        while (!d.isEmpty()) {
            int[] info = d.pollFirst();
            int p = info[0], last = info[1], step = info[2];
            for (int i = he[p]; i != -1; i = ne[i]) {
                if (vis[i]) continue;
                int j = e[i], c = w[i];
                if (c + last == 0 || last == 0) {
                    ans[j] = ans[j] == -1 ? step + 1 : Math.min(ans[j], step + 1);
                    d.addLast(new int[]{j, c, step + 1});
                    vis[i] = true;
                }
            }
        }
        return ans;
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    int he[110], e[810], ne[810], w[810], idx = 0;
    void add(int a, int b, int c) {
        e[idx] = b;
        w[idx] = c;
        ne[idx] = he[a];
        he[a] = idx++;
    }
    vector<int> shortestAlternatingPaths(int n, vector<vector<int>>& rs, vector<vector<int>>& bs) {
        idx = 0;
        fill(he, he + n, -1);
        for (auto& e : rs) add(e[0], e[1], 1);
        for (auto& e : bs) add(e[0], e[1], -1);
        vector<int> ans(n, -1);
        ans[0] = 0;
        vector<bool> vis(rs.size() + bs.size(), false);
        deque<vector<int>> d;
        d.push_back({0, 0, 0}); // point, last, step
        while (!d.empty()) {
            vector<int> info = d.front();
            d.pop_front();
            int p = info[0], last = info[1], step = info[2];
            for (int i = he[p]; i != -1; i = ne[i]) {
                if (vis[i]) continue;
                int j = e[i], c = w[i];
                if (c + last == 0 || last == 0) {
                    ans[j] = ans[j] == -1 ? step + 1 : min(ans[j], step + 1);
                    d.push_back({j, c, step + 1});
                    vis[i] = true;
                }
            }
        }
        return ans;
    }
};
```
* 时间复杂度:将点数记为 `n` ,边数记为 `m`。建图复杂度为 $O(m)$;构造答案复杂度为 $O(n + m)$。整体复杂度为 $O(n + m)$
* 空间复杂度:$O(n + m)$

### 最后

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

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

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

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


1221. 分割平衡字符串

Question:
在一个 平衡字符串 中,'L' 和 'R' 字符的数量是相同的。

给你一个平衡字符串 s,请你将它分割成尽可能多的平衡字符串。

注意:分割得到的每个字符串都必须是平衡字符串。

返回可以通过分割得到的平衡字符串的 最大数量 。

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

输出:4

解释:s 可以分割为 "RL"、"RRLL"、"RL"、"RL" ,每个子字符串中都包含相同数量的 'L' 和 'R' 。
```
示例 2:
```
输入:s = "RLLLLRRRLR"

输出:3

解释:s 可以分割为 "RL"、"LLLRRR"、"LR" ,每个子字符串中都包含相同数量的 'L' 和 'R' 。
```
示例 3:
```
输入:s = "LLLLRRRR"

输出:1

解释:s 只能保持原样 "LLLLRRRR".
```
示例 4:
```
输入:s = "RLRRRLLRLL"

输出:2

解释:s 可以分割为 "RL"、"RRRLLRLL" ,每个子字符串中都包含相同数量的 'L' 和 'R' 。
```

提示:
* 1 <= s.length <= 1000
* s[i] = 'L' 或 'R'
* s 是一个 平衡 字符串

Solution:
### 基本分析

题目确保了 `s` 为一个平衡字符串,即必然能分割成若干个 `LR` 子串。

一个合法的 `LR` 子串满足 `L` 字符和 `R` 字符数量相等,常规检查一个字符串是否为合格的 `LR` 子串可以使用 $O(n)$ 的遍历方式,可以使用记录前缀信息的数据结构,而对于成对结构的元素统计,更好的方式是转换为数学判定,使用 `1` 来代指 `L` 得分,使用 `-1` 来代指 `R` 得分。

那么一个子串为合格 `LR` 子串的充要条件为 **整个 `LR` 子串的总得分为 $0$**。

这种方式最早应该在 [(题解) 301. 删除无效的括号](https://leetcode-cn.com/problems/remove-invalid-parentheses/solution/yi-fen-zhong-nei-kan-dong-jiang-gua-hao-aya6k/) 详细讲过,可延伸到任意的成对结构元素统计题目里去。

---

### 贪心

回到本题,题目要求分割的 `LR` 子串尽可能多,直观上应该是尽可能让每个分割串尽可能短。

**我们使用「归纳法」来证明该猜想的正确性。**

首先题目数据保证给定的 `s` 本身是合法的 `LR` 子串,假设从 $[0...a]$ 可以从 `s` 中分割出 **长度最小** 的 `LR` 子串,而从 $[0...b]$ 能够分割出 **长度更大** 的 `LR` 子串(即 `a <= b` )。

**我们来证明起始时(第一次分割)「将从 `b` 分割点将 `s` 断开」调整为「从 `a` 分割点将 `s` 断开」结果不会变差:**

1. 从 `b` 点首次分割调整为从 `a` 点首次分割,两种分割形式分割点两端仍为合法 `LR` 子串,因此不会从“有解”变成“无解”;

2. 从 `b` 分割后的剩余部分长度小于从 `a` 分割后的剩余部分,同时由 `b` 分割后的剩余部分会被由 `a` 分割后的剩余部分所严格覆盖,因此「对 `a` 分割的剩余部分再分割所得的子串数量」**至少** 与「从 `b` 点分割的剩余部分再分割所得的子串数量」相等(不会变少)。

**至此,我们证明了对于首次分割,将任意合格分割点调整为最小分割点,结果不会变得更差(当 `a < b` 时还会更好)。**

**同时,由于首次分割后的剩余部分仍为合格的 `LR` 子串,因此归纳分析所依赖的结构没有发生改变,可以将上述的推理分析推广到每一个决策的回合(新边界)中。**

**至此,我们证明了只要每一次都从最小分割点进行分割,就可以得到最优解。**

代码:
```Java
class Solution {
    public int balancedStringSplit(String s) {
        char[] cs = s.toCharArray();
        int n = cs.length;
        int ans = 0;
        for (int i = 0; i < n; ) {
            int j = i + 1, score = cs[i] == 'L' ? 1 : -1;
            while (j < n && score != 0) score += cs[j++] == 'L' ? 1 : -1;
            i = j;
            ans++;
        }
        return ans;
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:调用 `toCharArray` 会拷贝新数组进行返回(为遵循 `String` 的不可变原则),因此使用 `toCharArray` 复杂度为 $O(n)$,使用 `charAt` 复杂度为 $O(1)$

### 最后

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

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

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

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


LeetCode/1221-1230/1222. 可以攻击国王的皇后（中等）.md

1222. 可以攻击国王的皇后

Question:
在一个 `8x8` 的棋盘上,放置着若干「黑皇后」和一个「白国王」。

给定一个由整数坐标组成的数组 `queens`,表示黑皇后的位置;以及一对坐标 `king`,表示白国王的位置,返回所有可以攻击国王的皇后的坐标(任意顺序)。

示例 1:

```
输入:queens = [[0,1],[1,0],[4,0],[0,4],[3,3],[2,4]], king = [0,0]

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

解释:
[0,1] 的皇后可以攻击到国王,因为他们在同一行上。
[1,0] 的皇后可以攻击到国王,因为他们在同一列上。
[3,3] 的皇后可以攻击到国王,因为他们在同一条对角线上。
[0,4] 的皇后无法攻击到国王,因为她被位于 [0,1] 的皇后挡住了。
[4,0] 的皇后无法攻击到国王,因为她被位于 [1,0] 的皇后挡住了。
[2,4] 的皇后无法攻击到国王,因为她和国王不在同一行/列/对角线上。
```
示例 2:

```
输入:queens = [[0,0],[1,1],[2,2],[3,4],[3,5],[4,4],[4,5]], king = [3,3]

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

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

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

提示:
* $1 <= queens.length <= 63$
* $queens[i].length = 2$
* $0 <= queens[i][j] < 8$
* $king.length = 2$
* $0 <= king[0], king[1] < 8$
* 一个棋盘格上最多只能放置一枚棋子。

Solution:
### 模拟

创建一个二维数组 `dirs` 来表示八个方向的坐标偏移,同时使用变量 `step` 来表示从国王位置出发的步数。

在每个方向 `i` 上,通过 `step` 步可以到达的坐标为 $(x + dirs[i][0] \times step, y + dirs[i][1] \times step)$。

模拟过程中,我们可使用布尔数组 `checked` 来记录已处理的方向,当我们遇到皇后或越过棋盘时,可标记该方向已处理。

Java 代码:
```Java
class Solution {
    int[][] dirs = new int[][]{{1,0},{0,1},{0,-1},{-1,0},{1,1},{-1,-1},{1,-1},{-1,1}};
    public List<List<Integer>> queensAttacktheKing(int[][] qs, int[] k) {
        boolean[][] vis = new boolean[10][10];
        for (int[] q : qs) vis[q[0]][q[1]] = true;
        int x = k[0], y = k[1], step = 1;
        List<List<Integer>> ans = new ArrayList<>();
        boolean[] checked = new boolean[8];
        while (step <= 8) {
            for (int i = 0; i < 8; i++) {
                if (checked[i]) continue;
                int nx = x + dirs[i][0] * step, ny = y + dirs[i][1] * step;
                if (nx < 0 || nx >= 8 || ny < 0 || ny >= 8) {
                    checked[i] = true;
                } else {
                    if (!vis[nx][ny]) continue;
                    List<Integer> list = new ArrayList<>();
                    list.add(nx); list.add(ny);
                    ans.add(list);
                    checked[i] = true;
                }
            }
            step++;
        }
        return ans;
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    vector<vector<int>> queensAttacktheKing(vector<vector<int>>& queens, vector<int>& king) {
        vector<vector<int>> dirs {{1,0},{0,1},{0,-1},{-1,0},{1,1},{-1,-1},{1,-1},{-1,1}};
        vector<vector<int>> ans;
        vector<vector<bool>> vis(8, vector<bool>(8, false));
        for (auto& queen : queens) vis[queen[0]][queen[1]] = true;
        int x = king[0], y = king[1], step = 1;
        vector<bool> checked(8, false);
        while (step <= 8) {
            for (int i = 0; i < 8; i++) {
                if (checked[i]) continue;
                int nx = x + step * dirs[i][0];
                int ny = y + step * dirs[i][1];
                if (nx < 0 || nx >= 8 || ny < 0 || ny >= 8) {
                    checked[i] = true;
                } else {
                    if (!vis[nx][ny]) continue;
                    ans.push_back({nx, ny});
                    checked[i] = true;
                }
            }
            step++;
        }
        return ans;
    }
};
```
Python 代码:
```Python
class Solution:
    def queensAttacktheKing(self, queens: List[List[int]], king: List[int]) -> List[List[int]]:
        dirs = [[1,0],[0,1],[0,-1],[-1,0],[1,1],[-1,-1],[1,-1],[-1,1]]
        vis = [[False] * 8 for _ in range(8)]
        for q in queens:
            vis[q[0]][q[1]] = True
        x, y = king
        ans = []
        checked = [False] * 8
        step = 1
        while step <= 8:
            for i in range(8):
                if checked[i]: continue
                nx, ny = x + dirs[i][0] * step, y + dirs[i][1] * step
                if nx < 0 or nx >= 8 or ny < 0 or ny >= 8:
                    checked[i] = True
                else:
                    if not vis[nx][ny]: continue
                    ans.append([nx, ny])
                    checked[i] = True
            step += 1
        return ans
```
TypeScript 代码:
```TypeScript
function queensAttacktheKing(queens: number[][], king: number[]): number[][] {
    let dirs = [[1,0],[0,1],[0,-1],[-1,0],[1,1],[-1,-1],[1,-1],[-1,1]];
    let vis: boolean[][] = Array.from({length:8},()=>Array(8).fill(false));
    for (let q of queens) vis[q[0]][q[1]] = true;
    let x = king[0], y = king[1], step = 1;
    let ans: number[][] = [];
    let checked: boolean[] = Array(8).fill(false);
    while (step <= 8) {
        for (let i = 0; i < 8; i++) {
            if (checked[i]) continue;
            let nx = x + dirs[i][0] * step, ny = y + dirs[i][1] * step;
            if (nx < 0 || nx >= 8 || ny < 0 || ny >= 8) {
                checked[i] = true;
            } else {
                if (!vis[nx][ny]) continue;
                ans.push([nx, ny]);
                checked[i] = true;
            }
        }
        step++;
    }
    return ans;
};
```
* 时间复杂度:$O(C)$,其中 $C = 8 \times 8$ 为棋盘总大小
* 空间复杂度:$O(C)$,其中 $C = 8 \times 8$ 为棋盘总大小

---

### BFS

上述模拟过程也可以使用 `BFS`  方式进行。

相比于直接模拟,`BFS`  无须使用额外变量记录处理过的方向以及当前位置与国王的距离。

Java 代码:
```Java
class Solution {
    int[][] dirs = new int[][]{{1,0},{0,1},{0,-1},{-1,0},{1,1},{-1,-1},{1,-1},{-1,1}};
    public List<List<Integer>> queensAttacktheKing(int[][] qs, int[] k) {
        boolean[][] vis = new boolean[10][10];
        for (int[] q : qs) vis[q[0]][q[1]] = true;
        Deque<int[]> d = new ArrayDeque<>();
        for (int i = 0; i < 8; i++) {
            int x = k[0] + dirs[i][0], y = k[1] + dirs[i][1];
            if (x < 0 || x >= 8 || y < 0 || y >= 8) continue;
            d.addLast(new int[]{x, y, i});
        }
        List<List<Integer>> ans = new ArrayList<>();
        while (!d.isEmpty()) {
            int[] info = d.pollFirst();
            int x = info[0], y = info[1], p = info[2];
            if (vis[x][y]) {
                List<Integer> list = new ArrayList<>();
                list.add(x); list.add(y);
                ans.add(list);
            } else {
                int nx = x + dirs[p][0], ny = y + dirs[p][1];
                if (nx < 0 || nx >= 8 || ny < 0 || ny >= 8) continue;
                d.addLast(new int[]{nx, ny, p});
            }
        }
        return ans;
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    vector<vector<int>> queensAttacktheKing(vector<vector<int>>& queens, vector<int>& king) {
        vector<vector<int>> dirs = {{1,0},{0,1},{0,-1},{-1,0},{1,1},{-1,-1},{1,-1},{-1,1}};
        vector<vector<bool>> vis(8, vector<bool>(8, false));
        for (vector<int>& queen : queens) vis[queen[0]][queen[1]] = true;
        deque<vector<int>> d;
        for (int i = 0; i < 8; i++) {
            int x = king[0] + dirs[i][0], y = king[1] + dirs[i][1];
            if (x < 0 || x >= 8 || y < 0 || y >= 8) continue;
            d.push_back({x, y, i});
        }
        vector<vector<int>> res;
        while (!d.empty()) {
            vector<int> cur = d.front();
            d.pop_front();
            if (vis[cur[0]][cur[1]]) {
                res.push_back({cur[0], cur[1]});
            } else {
                int nx = cur[0] + dirs[cur[2]][0], ny = cur[1] + dirs[cur[2]][1];
                if (nx < 0 || nx >= 8 || ny < 0 || ny >= 8) continue;
                d.push_back({nx, ny, cur[2]});
            }
        }
        return res;
    }
};
```
Python 代码:
```Python
class Solution:
    def queensAttacktheKing(self, queens, king):
        dirs = [[1,0],[0,1],[0,-1],[-1,0],[1,1],[-1,-1],[1,-1],[-1,1]]
        vis = [[False]*8 for _ in range(8)]
        for q in queens: vis[q[0]][q[1]] = True
        d = deque()
        for i in range(8):
            x = king[0] + dirs[i][0]
            y = king[1] + dirs[i][1]
            if x < 0 or x >= 8 or y < 0 or y >= 8: continue
            d.append([x, y, i])
        ans = []
        while d:
            cur = d.popleft()
            if vis[cur[0]][cur[1]]:
                ans.append([cur[0], cur[1]])
            else:
                x = cur[0] + dirs[cur[2]][0]
                y = cur[1] + dirs[cur[2]][1]
                if x < 0 or x >= 8 or y < 0 or y >= 8: continue
                d.append([x, y, cur[2]])
        return ans
```
TypeScript 代码:
```TypeScript
function queensAttacktheKing(queens: number[][], king: number[]): number[][] {
    let dirs = [[1,0],[0,1],[0,-1],[-1,0],[1,1],[-1,-1],[1,-1],[-1,1]];
    let vis: boolean[][] = Array(8).fill(false).map(() => Array(8).fill(false));
    queens.forEach(q => vis[q[0]][q[1]] = true);
    let d: number[][] = [];
    for (let i = 0; i < 8; i++) {
        let x = king[0] + dirs[i][0], y = king[1] + dirs[i][1];
        if (x < 0 || x >= 8 || y < 0 || y >= 8) continue;
        d.push([x, y, i]);
    }
    let res = [];
    while (d.length !== 0) {
        let cur = d.shift();
        if (vis[cur[0]][cur[1]]) {
            res.push([cur[0], cur[1]]);
        } else {
            let x = cur[0] + dirs[cur[2]][0], y = cur[1] + dirs[cur[2]][1];
            if (x < 0 || x >= 8 || y < 0 || y >= 8) continue;
            d.push([x, y, cur[2]]);
        }
    }
    return res;
};
```
* 时间复杂度:$O(C)$,其中 $C = 4 \times 8$ 为四条线的总步数
* 空间复杂度:$O(C)$,其中 $C = 8 \times 8$ 为棋盘总大小

### 最后

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

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

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

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


1224. 最大相等频率

Question:
给你一个正整数数组 `nums`,请你帮忙从该数组中找出能满足下面要求的最长前缀,并返回该前缀的长度:

* 从前缀中 恰好删除一个 元素后,剩下每个数字的出现次数都相同。

如果删除这个元素后没有剩余元素存在,仍可认为每个数字都具有相同的出现次数(也就是 $0$ 次)。

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

输出:7

解释:对于长度为 7 的子数组 [2,2,1,1,5,3,3],如果我们从中删去 nums[4] = 5,就可以得到 [2,2,1,1,3,3],里面每个数字都出现了两次。
```
示例 2:
```
输入:nums = [1,1,1,2,2,2,3,3,3,4,4,4,5]

输出:13
```

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

Solution:
### 模拟(计数 + 分情况讨论)

我们使用 `cnt` 数组记录每个数的出现次数(即 `cnt[i] = x` 含义为数值 $i$ 的出现次数为 $x$ 次),以及使用 `sum` 来记录出现次数为某个值的数有多少个(即 `sum[i] = x` 含义为出现次数为 $i$ 的数值共有 $x$ 个),同时使用 `max` 来记录最大出现次数 $cnt[i]$。

从前往后处理 `nums`,假设当前处理到的数值为 $t = nums[i]$(前缀所含数量为 $len = i + 1$)有如下几种情况,我们可以更新最大长度 `ans`:

* $max = 1$:说明当前处理到的所有 $nums[i]$ 均只出现了一次,此时任意删除一个均满足要求;
* $max \times sum[max] + 1 = len$:说明有一个数值单独出现了一次(其余出现次数为 `max` 的数值 + 被删除的一个数 = 总数量 $len$),删除后即满足要求;
* $(max - 1) \times (sum[max - 1] + 1) + 1 = len$:说明出现次数为 `max` 的数值只有一个,其余出现次数均为 `max - 1`,对其删除一次后即满足要求(删除该出现次数为 `max` 的数值后,会导致出现次数为 `max - 1` 的数值多一个,此时有「出现次数为 `max - 1` 的数值 + 被删除的一个数 = 总数量 $len$」)。

Java 代码:
```Java
class Solution {
    static int[] cnt = new int[100010], sum = new int[100010];
    public int maxEqualFreq(int[] nums) {
        Arrays.fill(cnt, 0); Arrays.fill(sum, 0);
        int n = nums.length, max = 0, ans = 0;
        for (int i = 0; i < n; i++) {
            int t = nums[i], cur = ++cnt[t], len = i + 1;
            sum[cur]++; sum[cur - 1]--;
            max = Math.max(max, cur);
            if (max == 1) ans = len;
            if (max * sum[max] + 1 == len) ans = len;
            if ((max - 1) * (sum[max - 1] + 1) + 1 == len) ans = len;
        }
        return ans;
    }
}
```
TypeScript 代码:
```TypeScript
function maxEqualFreq(nums: number[]): number {
    let n = nums.length, max = 0, ans = 0
    const cnt = new Array<number>(100010).fill(0), sum = new Array<number>(100010).fill(0)
    for (let i = 0; i < n; i++) {
        let t = nums[i], len = i + 1, cur = ++cnt[t]
        sum[cur]++; sum[cur - 1]--;
        max = Math.max(max, cur)
        if (max == 1) ans = len
        if (max * sum[max] + 1 == len) ans = len
        if ((max - 1) * (sum[max - 1] + 1) + 1 == len) ans = len
    }
    return ans
};
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


1233. 删除子文件夹

Question:
你是一位系统管理员,手里有一份文件夹列表 `folder`,你的任务是要删除该列表中的所有 子文件夹,并以 任意顺序 返回剩下的文件夹。

如果文件夹 `folder[i]` 位于另一个文件夹 `folder[j]` 下,那么 `folder[i]` 就是 `folder[j]` 的 子文件夹 。

文件夹的「路径」是由一个或多个按以下格式串联形成的字符串:`'/'` 后跟一个或者多个小写英文字母。

例如,`"/leetcode"` 和 `"/leetcode/problems"` 都是有效的路径,而空字符串和 "/" 不是。

示例 1:
```
输入:folder = ["/a","/a/b","/c/d","/c/d/e","/c/f"]

输出:["/a","/c/d","/c/f"]

解释:"/a/b" 是 "/a" 的子文件夹,而 "/c/d/e" 是 "/c/d" 的子文件夹。
```
示例 2:
```
输入:folder = ["/a","/a/b/c","/a/b/d"]

输出:["/a"]

解释:文件夹 "/a/b/c" 和 "/a/b/d" 都会被删除,因为它们都是 "/a" 的子文件夹。
```
示例 3:
```
输入: folder = ["/a/b/c","/a/b/ca","/a/b/d"]

输出: ["/a/b/c","/a/b/ca","/a/b/d"]
```

提示:
* $1 <= folder.length <= 4 \times 10^4$
* $2 <= folder[i].length <= 100$
* `folder[i]` 只包含小写字母和 `'/'`
* `folder[i]` 总是以字符 `'/'` 起始
* 每个文件夹名都是 唯一 的

Solution:
### 字典树

一道字典树裸题,不熟悉字典树的同学可以看前置 🧀 : [【设计数据结构】实现 Trie (前缀树)](https://mp.weixin.qq.com/s?__biz=MzU4NDE3MTEyMA==&mid=2247488490&idx=1&sn=db2998cb0e5f08684ee1b6009b974089)。

定义类 `Trie` 代表字典树节点,对应物理含义为某个文件夹。该节点含有属性 `s`、`isEnd` 和 `stries`,分别代表「当前节点所代表文件夹名」、「是否为当前路径的最终文件夹」以及「当前文件夹下的子文件夹集合」。

并且由于每个文件夹名的长度不定,我们使用 `Map<String, Trie>` 结构来构建 `stries`。

起始先将所有的 $folder[i]$ 加入字典树,随后查询每个 $folder[i]$ 是否为子文件夹,将所有非子文件夹加入答案。

代码:
```Java
class Solution {
    class Trie {
        String s;
        boolean isEnd = false;
        Map<String, Trie> stries = new HashMap<>();
        Trie (String _s) {
            s = _s;
        }
    }
    void add(String f) {
        String[] ss = f.split("/");
        Trie p = root;
        for (int i = 1; i < ss.length; i++) {
            String s = ss[i];
            if (!p.stries.containsKey(s)) p.stries.put(s, new Trie(s));
            p = p.stries.get(s);
        }
        p.isEnd = true;
    }
    boolean isSubFolder(String f) {
        String[] ss = f.split("/");
        Trie p = root;
        for (int i = 1; i < ss.length - 1; i++) {
            String s = ss[i];
            if (p.stries.get(s).isEnd) return true;
            p = p.stries.get(s);
        }
        return false;
    }
    Trie root = new Trie("");
    public List<String> removeSubfolders(String[] folder) {
        for (String f : folder) add(f);
        List<String> ans = new ArrayList<>();
        for (String f : folder) {
            if (!isSubFolder(f)) ans.add(f);
        }
        return ans;
    }
}
```
* 时间复杂度:$O(\sum_{i = 0}^{n - 1} folder[i].length)$
* 空间复杂度:$O(\sum_{i = 0}^{n - 1} folder[i].length)$

### 最后

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

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

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

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


1235. 规划兼职工作

Question:
你打算利用空闲时间来做兼职工作赚些零花钱。

这里有 `n` 份兼职工作,每份工作预计从 `startTime[i]` 开始到 `endTime[i]` 结束,报酬为 `profit[i]`。

给你一份兼职工作表,包含开始时间 `startTime`,结束时间 `endTime` 和预计报酬 `profit` 三个数组,请你计算并返回可以获得的最大报酬。

注意,时间上出现重叠的 `2` 份工作不能同时进行。

如果你选择的工作在时间 `X` 结束,那么你可以立刻进行在时间 `X` 开始的下一份工作。

示例 1:

```
输入:startTime = [1,2,3,3], endTime = [3,4,5,6], profit = [50,10,40,70]

输出:120

解释:
我们选出第 1 份和第 4 份工作,
时间范围是 [1-3]+[3-6],共获得报酬 120 = 50 + 70。
```
示例 2:

```
输入:startTime = [1,2,3,4,6], endTime = [3,5,10,6,9], profit = [20,20,100,70,60]

输出:150

解释:
我们选择第 1,4,5 份工作。
共获得报酬 150 = 20 + 70 + 60。
```
示例 3:

```
输入:startTime = [1,1,1], endTime = [2,3,4], profit = [5,6,4]

输出:6
```

提示:
* $1 <= startTime.length == endTime.length == profit.length <= 5 \times 10^4$
* $1 <= startTime[i] < endTime[i] <= 10^9$
* $1 <= profit[i] <= 10^4$

Solution:
### 序列 DP + 二分

为了方便,我们令 `startTime` 为 `st`,`endTime` 为 `endTime`,`profit` 为 `ps`,同时定义三元组 $job[i] = (st[i], et[i], ps[i])$ 来代指某份工作。

我们知道,在理想情况下,若能将所有工作排成不重叠的直线,我们便能通过完成所有工作来取得最大收益。

归结到每个工作,我们总有「选择完成该工作」和「选择不完成该工作」两种决策。

**定义 $f[i]$ 为考虑前 $i$ 个工作,所能取得的最大收益(注意 $job[i]$ 不一定被选择完成),为了方便,我们令下标从 $1$ 开始:**

* 当不选择该工作时:由于 $job[i]$ 明确不会产生价值,可知 $f[i] = f[i - 1]$;
* 当选择该工作时:可分为「仅选择完成该工作」或「选择 **考虑** 将该工作接在某个工作后面完成」两种情况:
    * 当「仅选择完成该工作」时,我们有 $f[i] = job[i][2]$;
    * 当「选择 **考虑** 将该工作接在某个工作后面完成」时,我们需要在所有满足「$job[j][1] <= job[i][0]$」中选择最适合的 $job[j]$ 接在 $job[i]$ 的前面。
      即在所有能够在 $job[i]$ 开始前顺利结束的 $job[j]$ 中取最大的 $f[j]$,此时有 $f[i] = f[j] + job[i][2]$
        > 需要注意:这里的“接在”是指将 $job[j]$ 纳入考虑,但具体方案中,并不一定选择 $job[j]$ 来执行(好好想想我们的 $f[i]$ 状态定义)

最终 $f[i]$ 为上述三种方案中的最大值,并且最终的 $f[n]$ 即是我们的答案。

**当我们处理到 $job[i]$ 时,为了能够「将所有所能拼接在 $job[i]$ 前面的 $job[j]$ 归结到一边」并且「所能更新 $f[i]$ 的 $f[j]$ 均被计算」,我们可以通过对所有的 $job[i]$ 进行右端点(结束时间)进行排升序,并按照从小到大的方式处理每个 $job[i]$。**

此处排序的意义有两点:

* 由于我们是根据右端点排序,当我们处理到某个 $job[i]$ 时,由于有 $job[X][0] < job[X][1]$,因此所能接在 $job[i]$ 前面(结束时间小于等于 $job[i]$ 开始时间)的 $job[j]$ 必然位于 $[0, i)$ 之间;
* 由于我们对 $f[i]$ 的定义并不限定了必须选 $job[i]$,因此在 $[0, i)$ 范围内以 $job[j]$ 为分割点的数组的具有「二段性」:坐标范围小于等于 $j$ 的 $job[X]$ 均可“接在” $job[i]$ 前面。因此我们可通过「二分」来找所能接在 $job[i]$ 前面的坐标最大的 $job[j]$。

Java 代码:
```Java
class Solution {
    public int jobScheduling(int[] st, int[] et, int[] ps) {
        int n = st.length;
        List<int[]> list = new ArrayList<>();
        for (int i = 0; i < n; i++) list.add(new int[]{st[i], et[i], ps[i]});
        Collections.sort(list, (a, b)->a[1] - b[1]);
        int[] f = new int[n + 10];
        for (int i = 1; i <= n; i++) {
            int[] info = list.get(i - 1);
            int a = info[0], b = info[1], c = info[2];
            f[i] = Math.max(f[i - 1], c);
            int l = 0, r = i - 1;
            while (l < r) {
                int mid = l + r + 1 >> 1;
                if (list.get(mid)[1] <= a) l = mid;
                else r = mid - 1;
            }
            if (list.get(r)[1] <= a) f[i] = Math.max(f[i], f[r + 1] + c);
        }
        return f[n];
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    int jobScheduling(vector<int>& startTime, vector<int>& endTime, vector<int>& profit) {
        int n = startTime.size();
        vector<vector<int>> list(n);
        for (int i = 0; i < n; i++) list[i] = {startTime[i], endTime[i], profit[i]};
        sort(list.begin(), list.end(), [](vector<int> a, vector<int> b)->bool{return a[1] < b[1];});
        vector<int> f(n + 10, 0);
        for(int i = 1; i <= n; i++) {
            int a = list[i - 1][0], b = list[i - 1][1], c = list[i - 1][2];
            f[i] = max(f[i - 1], c);
            int l = 0, r = i - 1;
            while (l < r) {
                int mid = l + r + 1 >> 1;
                if (list[mid][1] <= a) l = mid;
                else r = mid - 1;
            }
            if (list[r][1] <= a) f[i] = max(f[i], f[r + 1] + c);
        }
        return f[n];
    }
};
```
Python 代码:
```Python
class Solution:
    def jobScheduling(self, st: List[int], et: List[int], ps: List[int]) -> int:
        n = len(st)
        jobs = [(st[i], et[i], ps[i]) for i in range(n)]
        jobs.sort(key=lambda x: x[1])
        f = [0] * (n + 10)
        for i in range(1, n + 1):
            a, b, c = jobs[i - 1]
            f[i] = max(f[i - 1], c)
            l, r = 0, i - 1
            while l < r:
                mid = l + r + 1 >> 1
                if jobs[mid][1] <= a:
                    l = mid
                else:
                    r = mid - 1
            if jobs[r][1] <= a:
                f[i] = max(f[i], f[r + 1] + c)
        return f[n]
```
TypeScript 代码:
```TypeScript
function jobScheduling(st: number[], et: number[], ps: number[]): number {
    const n = st.length
    const list = new Array<Array<number>>()
    for (let i = 0; i < n; i++) list.push([st[i], et[i], ps[i]])
    list.sort((a,b)=>a[1]-b[1])
    const f = new Array<number>(n + 10).fill(0)
    for (let i = 1; i <= n; i++) {
        const info = list[i - 1]
        const a = info[0], b = info[1], c = info[2]
        f[i] = Math.max(f[i - 1], c)
        let l = 0, r = i - 1
        while (l < r) {
            const mid = l + r + 1 >> 1
            if (list[mid][1] <= a) l = mid
            else r = mid - 1
        }
        if (list[r][1] <= a) f[i] = Math.max(f[i], f[r + 1] + c)
    }
    return f[n]
}
```
* 时间复杂度:排序复杂度为 $O(n\log{n})$;`DP` 过程共有 $n$ 个状态需要转移,每次转移需要进行二分,单次复杂度为 $O(\log{n})$。整体复杂度为 $O(n\log{n})$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


LeetCode/1231-1240/1239. 串联字符串的最大长度（中等）.md

1239. 串联字符串的最大长度

Question:
给定一个字符串数组 `arr`,字符串 `s` 是将 `arr` 某一子序列字符串连接所得的字符串,如果 `s` 中的每一个字符都只出现过一次,那么它就是一个可行解。

请返回所有可行解 `s` 中最长长度。

示例 1:
```
输入:arr = ["un","iq","ue"]

输出:4

解释:所有可能的串联组合是 "","un","iq","ue","uniq" 和 "ique",最大长度为 4。
```
示例 2:
```
输入:arr = ["cha","r","act","ers"]

输出:6

解释:可能的解答有 "chaers" 和 "acters"。
```
示例 3:
```
输入:arr = ["abcdefghijklmnopqrstuvwxyz"]

输出:26
```

提示:
* $1 <= arr.length <= 16$
* $1 <= arr[i].length <= 26$
* `arr[i]` 中只含有小写英文字母

Solution:
### 基本分析

根据题意,可以将本题看做一类特殊的「数独问题」:在给定的 `arr` 字符数组中选择,尽可能多的覆盖一个 $1 \times 26$ 的矩阵。

对于此类「精确覆盖」问题,换个角度也可以看做「组合问题」。

通常有几种做法:`DFS`、剪枝 `DFS`、二进制枚举、模拟退火、`DLX`。

其中一头一尾解法过于简单和困难,有兴趣的同学自行了解与实现。

---

### 剪枝 DFS

根据题意,可以有如下的剪枝策略:

1. 预处理掉「本身具有重复字符」的无效字符串,并去重;
2. 由于只关心某个字符是否出现,而不关心某个字符在原字符串的位置,因此可以将字符串使用 `int` 进行表示;
3. 由于使用 `int` 进行表示,因而可以使用「位运算」来判断某个字符是否可以被追加到当前状态中;
4. `DFS` 过程中维护一个 `total`,代表后续未经处理的字符串所剩余的“最大价值”是多少,从而实现剪枝;
5. 使用 `lowbit` 计算某个状态对应的字符长度是多少;
6. 使用「全局哈希表」记录某个状态对应的字符长度是多少(使用 `static` 修饰,确保某个状态在所有测试数据中只会被计算一次);
7. 【未应用】由于存在第 $4$ 点这样的「更优性剪枝」,理论上我们可以根据「字符串所包含字符数量」进行从大到小排序,然后再进行 `DFS` 这样效果理论上会更好。想象一下如果存在一个包含所有字母的字符串,先选择该字符串,后续所有字符串将不能被添加,那么由它出发的分支数量为 $0$;而如果一个字符串只包含单个字母,先决策选择该字符串,那么由它出发的分支数量必然大于 $0$。但该策略实测效果不好,没有添加到代码中。

代码:
```Java
class Solution {
    // 本来想使用如下逻辑将「所有可能用到的状态」打表,实现 O(1) 查询某个状态有多少个字符,但是被卡了
    // static int N = 26, M = (1 << N);
    // static int[] cnt = new int[M];
    // static {
    //     for (int i = 0; i < M; i++) {
    //         for (int j = 0; j < 26; j++) {
    //             if (((i >> j) & 1) == 1) cnt[i]++;
    //         }
    //     }
    // }

    static Map<Integer, Integer> map = new HashMap<>();
    int get(int cur) {
        if (map.containsKey(cur)) {
            return map.get(cur);
        }
        int ans = 0;
        for (int i = cur; i > 0; i -= lowbit(i)) ans++;
        map.put(cur, ans);
        return ans;
    }
    int lowbit(int x) {
        return x & -x;
    }

    int n;
    int ans = Integer.MIN_VALUE;
    int[] hash;
    public int maxLength(List<String> _ws) {
        n = _ws.size();
        HashSet<Integer> set = new HashSet<>();
        for (String s : _ws) {
            int val = 0;
            for (char c : s.toCharArray()) {
                int t = (int)(c - 'a');
                if (((val >> t) & 1) != 0) {
                    val = -1;
                    break;
                }
                val |= (1 << t);
            }
            if (val != -1) set.add(val);
        }

        n = set.size();
        if (n == 0) return 0;
        hash = new int[n];

        int idx = 0;
        int total = 0;
        for (Integer i : set) {
            hash[idx++] = i;
            total |= i;
        }
        dfs(0, 0, total);
        return ans;
    }
    void dfs(int u, int cur, int total) {
        if (get(cur | total) <= ans) return;
        if (u == n) {
            ans = Math.max(ans, get(cur));
            return;
        }
        // 在原有基础上,选择该数字(如果可以)
        if ((hash[u] & cur) == 0) {
            dfs(u + 1, hash[u] | cur, total - (total & hash[u]));
        }
        // 不选择该数字
        dfs(u + 1, cur, total);
    }
}
```

---

### 二进制枚举

首先还是对所有字符串进行预处理。

然后使用「二进制枚举」的方式,枚举某个字符串是否被选择。

举个🌰,$(110)_{2}$ 代表选择前两个字符串,$(011)_{2}$ 代表选择后两个字符串,这样我们便可以枚举出所有组合方案。

代码:
```Java
class Solution {
    static Map<Integer, Integer> map = new HashMap<>();
    int get(int cur) {
        if (map.containsKey(cur)) {
            return map.get(cur);
        }
        int ans = 0;
        for (int i = cur; i > 0; i -= lowbit(i)) ans++;
        map.put(cur, ans);
        return ans;
    }
    int lowbit(int x) {
        return x & -x;
    }

    int n;
    int ans = Integer.MIN_VALUE;
    Integer[] hash;
    public int maxLength(List<String> _ws) {
        n = _ws.size();
        HashSet<Integer> set = new HashSet<>();
        for (String s : _ws) {
            int val = 0;
            for (char c : s.toCharArray()) {
                int t = (int)(c - 'a');
                if (((val >> t) & 1) != 0) {
                    val = -1;
                    break;
                }
                val |= (1 << t);
            }
            if (val != -1) set.add(val);
        }

        n = set.size();
        if (n == 0) return 0;
        hash = new Integer[n];
        int idx = 0;
        for (Integer i : set) hash[idx++] = i;

        for (int i = 0; i < (1 << n); i++) {
            int cur = 0, val = 0;
            for (int j = 0; j < n; j++) {
                if (((i >> j) & 1) == 1) {
                    if ((cur & hash[j]) == 0) {
                        cur |= hash[j];
                        val += get(hash[j]);
                    } else {
                        cur = -1;
                        break;
                    }
                }
            }
            if (cur != -1) ans = Math.max(ans, val);
        }
        return ans;
    }
}
```

---

### 模拟退火

事实上,可以将原问题看作求「最优前缀序列」问题,从而使用「模拟退火」进行求解。

具体的,我们可以定义「最优前缀序列」为 **组成最优解所用到的字符串均出现在排列的前面。**

举个🌰,假如构成最优解使用到的字符串集合为 `[a,b,c]`,那么对应 `[a,b,c,...]`、`[a,c,b,...]` 均称为「最优前缀序列」。

不难发现,答案与最优前缀序列是一对多关系,这指导我们可以将「参数」调得宽松一些。

**具有「一对多」关系的问题十分适合使用「模拟退火」,使用「模拟退火」可以轻松将本题 `arr.length` 数据范围上升到 $60$ 甚至以上。**

调整成比较宽松的参数可以跑赢「二进制枚举」,但为了以后增加数据不容易被 hack,还是使用 `N=400` & `fa=0.90` 的搭配。

「模拟退火」的几个参数的作用在 [这里](https://leetcode-cn.com/problems/find-minimum-time-to-finish-all-jobs/solution/gong-shui-san-xie-yi-ti-shuang-jie-jian-4epdd/) 说过了,不再赘述。

代码:
```Java
class Solution {
    static Map<Integer, Integer> map = new HashMap<>();
    int get(int cur) {
        if (map.containsKey(cur)) {
            return map.get(cur);
        }
        int ans = 0;
        for (int i = cur; i > 0; i -= lowbit(i)) ans++;
        map.put(cur, ans);
        return ans;
    }
    int lowbit(int x) {
        return x & -x;
    }

    int n;
    int ans = Integer.MIN_VALUE;
    Random random = new Random(20210619);
    double hi = 1e4, lo = 1e-4, fa = 0.90;
    int N = 400;
    int calc() {
        int mix = 0, cur = 0;
        for (int i = 0; i < n; i++) {
            int hash = ws[i];
            if ((mix & hash) == 0) {
                mix |= hash;
                cur += get(hash);
            } else {
                break;
            }
        }
        ans = Math.max(ans, cur);
        return cur;
    }
    void shuffle(int[] nums) {
        for (int i = n; i > 0; i--) {
            int idx = random.nextInt(i);
            swap(nums, idx, i - 1);
        }
    }
    void swap(int[] nums, int a, int b) {
        int c = nums[a];
        nums[a] = nums[b];
        nums[b] = c;
    }
    void sa() {
        shuffle(ws);
        for (double t = hi; t > lo; t *= fa) {
            int a = random.nextInt(n), b = random.nextInt(n);
            int prev = calc();
            swap(ws, a, b);
            int cur = calc();
            int diff = cur - prev;
            if (Math.log(-diff / t) > random.nextDouble()) swap(ws, a, b);
        }
    }
    int[] ws;
    public int maxLength(List<String> _ws) {
        // 预处理字符串:去重,剔除无效字符
        // 结果这一步后:N 可以下降到 100;fa 可以下降到 0.70,耗时约为 78 ms
        // 为了预留将来添加测试数据,题解还是保持 N = 400 & fa = 0.90 的配置
        n = _ws.size();
        HashSet<Integer> set = new HashSet<>();
        for (String s : _ws) {
            int val = 0;
            for (char c : s.toCharArray()) {
                int t = (int)(c - 'a');
                if (((val >> t) & 1) != 0) {
                    val = -1;
                    break;
                }
                val |= (1 << t);
            }
            if (val != -1) set.add(val);
        }

        n = set.size();
        if (n == 0) return 0;
        ws = new int[n];
        int idx = 0;
        for (Integer i : set) ws[idx++] = i;

        while (N-- > 0) sa();
        return ans;
    }
}
```

### 最后

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

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

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

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


LeetCode/1251-1260/1252. 奇数值单元格的数目（简单）.md

1252. 奇数值单元格的数目

Question:
给你一个 $m \times n$ 的矩阵,最开始的时候,每个单元格中的值都是 $0$。

另有一个二维索引数组 `indices`,$indices[i] = [r_i, c_i]$ 指向矩阵中的某个位置,其中 $r_i$ 和 $c_i$ 分别表示指定的行和列(从 $0$ 开始编号)。

对 $indices[i]$ 所指向的每个位置,应同时执行下述增量操作:
* $r_i$ 行上的所有单元格,加 $1$ 。
* $c_i$ 列上的所有单元格,加 $1$ 。

给你 $m$、$n$ 和 $indices$ 。请你在执行完所有 $indices$ 指定的增量操作后,返回矩阵中 奇数值单元格 的数目。

示例 1:

```
输入:m = 2, n = 3, indices = [[0,1],[1,1]]

输出:6

解释:最开始的矩阵是 [[0,0,0],[0,0,0]]。
第一次增量操作后得到 [[1,2,1],[0,1,0]]。
最后的矩阵是 [[1,3,1],[1,3,1]],里面有 6 个奇数。
```
示例 2:

```
输入:m = 2, n = 2, indices = [[1,1],[0,0]]

输出:0

解释:最后的矩阵是 [[2,2],[2,2]],里面没有奇数。
```

提示:
* $1 <= m, n <= 50$
* $1 <= indices.length <= 100$
* $0 <= r_i < m$
* $0 <= c_i < n$

进阶:你可以设计一个时间复杂度为 $O(n + m + indices.length)$ 且仅用 $O(n + m)$ 额外空间的算法来解决此问题吗?

Solution:
### 基本分析

容易想到时间复杂度为 $O(l + m \times n)$,空间复杂度为 $O(m + n)$ 的做法,在此不再赘述。

对于某个位置最终累加值为奇数的充要条件为「所在行被累加次数的奇偶性」与「所在列被累加次数的奇偶性」不同。

因此我们可以统计累加次数为奇数的行数 $a$(累加次数为偶数的行数为 $m - a$),累加次数为奇数的列数 $b$(累加次数为偶数的列数为 $n - b$),根据乘法原理,最终答案为 $a \times (n - b) + (m - a) \times b$。

---

### 计数模拟

由于我们只需关系某个位置的奇偶性,而不关心具体的累加值,我们可以创建两个数组 `r` 和 `c`,统计每行和每列的累加值的奇偶性。

当 $r[idx]$ 为 `True` 含义为第 $idx$ 行的累加值为奇数,否则为偶数。列数组 `c` 的统计规则同理。

Java 代码:
```Java
class Solution {
    public int oddCells(int m, int n, int[][] ins) {
        boolean[] r = new boolean[m], c = new boolean[n];
        int a = 0, b = 0;
        for (int[] info : ins) {
            a += (r[info[0]] = !r[info[0]]) ? 1 : -1;
            b += (c[info[1]] = !c[info[1]]) ? 1 : -1;
        }
        return a * (n - b) + (m - a) * b;
    }
}
```
TypeScript 代码:
```TypeScript
function oddCells(m: number, n: number, ins: number[][]): number {
    const r = new Array<boolean>(m).fill(false), c = new Array<boolean>(n).fill(false)
    let a = 0, b = 0
    for (const [i, j] of ins) {
        a += (r[i] = !r[i]) ? 1 : -1
        b += (c[j] = !c[j]) ? 1 : -1
    }
    return a * (n - b) + (m - a) * b
};
```
Python 代码:
```Python3
class Solution:
    def oddCells(self, m: int, n: int, ins: List[List[int]]) -> int:
        r, c = [False] * m, [False] * n
        for i, j in ins:
            r[i] ^= 1
            c[j] ^= 1
        a, b = sum(r), sum(c)
        return a * (n - b) + (m - a) * b
```
* 时间复杂度:构建计数数组的复杂度为 $O(m + n)$,统计奇数行和奇数列复杂度为 $O(l)$,其中 $l$ 为数组 `ins` 的长度,复杂度为 $O(m + n + l)$
* 空间复杂度:$O(m + n)$

---

### 位运算

更进一步,我们可以使用两个 `long` 变量 $c1$ 和 $c2$ 来分别充当行和列的计数数组,当 $c1$ 的第 $k$ 位为 $1$,代表第 $k$ 行累加值为奇数,当 $c1$ 的第 $k$ 位为 $0$,代表第 $k$ 行累加值为偶数;$c2$ 的计数规则同理。而翻转二进制中的某一位可使用「异或」操作。

当处理完所有的 `ins` 之后,可通过「遍历 $c1$ 的低 $m$ 位 + 遍历 $c2$ 的低 $n$ 位」来得到行数中奇数个数 $a$,列数中奇数个数 $b$,复杂度为 $O(m + n)$;也可以使用 `bitCount` 统计 `long` 二进制数中 $1$ 的个数(本质是分治操作),复杂度为 $O(\log{64})$。

Java 代码:
```Java
class Solution {
    public int oddCells(int m, int n, int[][] ins) {
        long c1 = 0, c2 = 0;
        for (int[] info : ins) {
            c1 ^= 1L << info[0];
            c2 ^= 1L << info[1];
        }
        int a = 0, b = 0;
        for (int i = 0; i < m; i++) a += ((c1 >> i) & 1);
        for (int i = 0; i < n; i++) b += ((c2 >> i) & 1);
        return a * (n - b) + (m - a) * b;
    }
}
```
Java 代码:
```Java
class Solution {
    public int oddCells(int m, int n, int[][] ins) {
        long c1 = 0, c2 = 0;
        for (int[] info : ins) {
            c1 ^= 1L << info[0];
            c2 ^= 1L << info[1];
        }
        int a = Long.bitCount(c1), b = Long.bitCount(c2);
        return a * (n - b) + (m - a) * b;
    }
}
```
TypeScript 代码:
```TypeScript
function oddCells(m: number, n: number, ins: number[][]): number {
    const c1 = [0, 0], c2 = [0, 0]
    for (const [i, j] of ins) {
        c1[Math.ceil(i / 32)] ^= (1 << (i % 32))
        c2[Math.ceil(j / 32)] ^= (1 << (j % 32))
    }
    let a = 0, b = 0
    for (let i = 0; i < m; i++) a += (c1[Math.ceil(i / 32)] >> (i % 32) & 1)
    for (let i = 0; i < n; i++) b += (c2[Math.ceil(i / 32)] >> (i % 32) & 1)
    return a * (n - b) + (m - a) * b
};
```
Python 代码:
```Python3
class Solution:
    def oddCells(self, m: int, n: int, ins: List[List[int]]) -> int:
        c1, c2 = 0, 0
        for i, j in ins:
            c1 ^= (1 << i)
            c2 ^= (1 << j)
        a, b = 0, 0
        for i in range(m):
            a += (c1 >> i) & 1
        for i in range(n):
            b += (c2 >> i) & 1
        return a * (n - b) + (m - a) * b
```
* 时间复杂度:处理所有的 `ins` 复杂度为 $O(l)$,其中 $l$ 为数组 `ins` 的长度;使用遍历方式统计奇数行和奇数列个数复杂度为 $O(m + n)$;使用 `bitCount` 操作统计二进制中 $1$ 个数,复杂度为 $O(\log{C})$,其中 $C = 64$ 为 `long` 二进制数长度,整体复杂度为 $O(l + m + n)$ 或 $O(l + \log{C})$
* 空间复杂度:$O(1)$

---

### 答疑

评论区有同学提到 `bitCount` 的复杂度问题,如下是 `Long.bitCount` 操作的源码:

```Java
public static int bitCount(long i) {
    // HD, Figure 5-14
    i = i - ((i >>> 1) & 0x5555555555555555L);
    i = (i & 0x3333333333333333L) + ((i >>> 2) & 0x3333333333333333L);
    i = (i + (i >>> 4)) & 0x0f0f0f0f0f0f0f0fL;
    i = i + (i >>> 8);
    i = i + (i >>> 16);
    i = i + (i >>> 32);
    return (int)i & 0x7f;
}
```

这自然不是通过遍历统计位数的 $O(n)$ 做法,普遍会认为是 $O(1)$。

但上述操作目的是进行成组统计(分治),而能够这样写是因为默认了 `long` 是 $64$ 长度,因此严格意义来说这段代码复杂度是 $O(\log{64})$ 的。

作为对比,可以把 `Integer.bitCount`  的源码看一下:

```Java
public static int bitCount(int i) {
    // HD, Figure 5-2
    i = i - ((i >>> 1) & 0x55555555);
    i = (i & 0x33333333) + ((i >>> 2) & 0x33333333);
    i = (i + (i >>> 4)) & 0x0f0f0f0f;
    i = i + (i >>> 8);
    i = i + (i >>> 16);
    return i & 0x3f;
}
```

统计原理如出一辙,而能够这样统计,是因为默认了 `int` 长度为 $32$,其分组统计所需要的操作次数也与二进制数的长度相关,因此复杂度为 $O(\log{32})$。

将上述两个 `bitCount` 视为 $O(1)$ 都是不对的。

### 最后

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

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

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

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


LeetCode/1251-1260/1253. 重构 2 行二进制矩阵（中等）.md

1253. 重构 2 行二进制矩阵

Question:
给你一个 `2` 行 `n` 列的二进制数组:

* 矩阵是一个二进制矩阵,这意味着矩阵中的每个元素不是 0 就是 1。
* 第 `0` 行的元素之和为 `upper`。
* 第 `1` 行的元素之和为 `lower`。
* 第 `i` 列(从 `0` 开始编号)的元素之和为 `colsum[i]`,`colsum` 是一个长度为 `n` 的整数数组。

你需要利用 `upper`,`lower` 和 `colsum` 来重构这个矩阵,并以二维整数数组的形式返回它。

如果有多个不同的答案,那么任意一个都可以通过本题。

如果不存在符合要求的答案,就请返回一个空的二维数组。

示例 1:
```
输入:upper = 2, lower = 1, colsum = [1,1,1]

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

解释:[[1,0,1],[0,1,0]] 和 [[0,1,1],[1,0,0]] 也是正确答案。
```
示例 2:
```
输入:upper = 2, lower = 3, colsum = [2,2,1,1]

输出:[]
```
示例 3:
```
输入:upper = 5, lower = 5, colsum = [2,1,2,0,1,0,1,2,0,1]

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

提示:
* $1 <= colsum.length <= 10^5$
* $0 <= upper, lower <= colsum.length$
* $0 <= colsum[i] <= 2$

Solution:
### 贪心 + 构造

创建数组 `a` 和 `b` 分别代表目标二进制矩阵的第 `0` 行和第 `1` 行。

从前往后处理 `colsum`,复用 `upper` 和 `lower` 分别代表两行剩余 $1$ 的个数。

根据当前的 $colsum[i]$ 进行分情况讨论:

* 若 $colsum[i] = 0$,只有一种情况,当前位置两行均为 $0$
* 若 $colsum[i] = 2$,只有一种情况,当前位置两行均为 $1$
* 若 $colsum[i] = 1$,选剩余 `1` 个数较大的行,填入 $1$,另外行则填入 $0$

若处理完 `colsum` 后,两行剩余 $1$ 个数恰好均为 $0$,说明构造出了合法方案。

容易证明:**不存在某个决策回合中,必须先填入剩余个数少的一方,才能顺利构造。**

可用反证法进行证明,若存在某个回合必须填入剩余个数少的一方(假设该回合上填 `1` 下填 `0`),必然能够找到同为 $colsum[j] = 1$ 的回合进行交换,同时不影响合法性(上下行的总和不变,同时 $colsum[i] = colsum[j] = 1$)。

Java 代码:
```Java
class Solution {
    public List<List<Integer>> reconstructMatrix(int upper, int lower, int[] colsum) {
        int n = colsum.length;
        List<List<Integer>> ans = new ArrayList<>();
        List<Integer> a = new ArrayList<>(), b = new ArrayList<>();
        for (int i = 0; i < n; i++) {
            int m = colsum[i];
            if (m == 0) {
                a.add(0); b.add(0);
            } else if (m == 2) {
                upper--; lower--;
                a.add(1); b.add(1);
            } else {
                if (upper >= lower) {
                    upper--;
                    a.add(1); b.add(0);
                } else {
                    lower--;
                    a.add(0); b.add(1);
                }
            }
        }
        if (upper == 0 && lower == 0) {
            ans.add(a); ans.add(b);
        }
        return ans;
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    vector<vector<int>> reconstructMatrix(int upper, int lower, vector<int>& colsum) {
        int n = colsum.size();
        vector<vector<int>> ans;
        vector<int> a, b;
        for (int i = 0; i < n; i++) {
            int m = colsum[i];
            if (m == 0) {
                a.push_back(0); b.push_back(0);
            } else if (m == 2) {
                upper--; lower--;
                a.push_back(1); b.push_back(1);
            } else {
                if (upper >= lower) {
                    upper--;
                    a.push_back(1); b.push_back(0);
                } else {
                    lower--;
                    a.push_back(0); b.push_back(1);
                }
            }
        }
        if (upper == 0 && lower == 0) {
            ans.push_back(a); ans.push_back(b);
        }
        return ans;
    }
};
```
Python 代码:
```Python
class Solution:
    def reconstructMatrix(self, upper: int, lower: int, colsum: List[int]) -> List[List[int]]:
        n = len(colsum)
        ans = []
        a, b = [], []
        for i in range(n):
            m = colsum[i]
            if m == 0:
                a.append(0)
                b.append(0)
            elif m == 2:
                upper, lower = upper - 1, lower - 1
                a.append(1)
                b.append(1)
            else:
                a.append(1 if upper >= lower else 0)
                b.append(0 if upper >= lower else 1)
                if upper >= lower: upper -= 1
                else: lower -= 1
        if upper == lower == 0:
            ans.append(a)
            ans.append(b)
        return ans
```
TypeScript 代码:
```TypeScript
function reconstructMatrix(upper: number, lower: number, colsum: number[]): number[][] {
    const n = colsum.length;
    let ans: number[][] = [];
    let a: number[] = [], b: number[] = [];
    for (let i = 0; i < n; i++) {
        const m = colsum[i];
        if (m === 0) {
            a.push(0); b.push(0);
        } else if (m === 2) {
            upper--; lower--;
            a.push(1); b.push(1);
        } else {
            if (upper >= lower) {
                upper--;
                a.push(1); b.push(0);
            } else {
                lower--;
                a.push(0); b.push(1);
            }
        }
    }
    if (upper === 0 && lower === 0) {
        ans.push(a); ans.push(b);
    }
    return ans;
};
```
* 时间复杂度:$O(C \times n)$,其中 $C = 2$  代表行数
* 空间复杂度:$O(C \times n)$

### 最后

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

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

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

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


1260. 二维网格迁移

Question:
给你一个 `m` 行 `n` 列的二维网格 `grid` 和一个整数 `k`。

你需要将 `grid` 迁移 `k` 次。

每次「迁移」操作将会引发下述活动:

* 位于 `grid[i][j]` 的元素将会移动到 `grid[i][j + 1]`
* 位于 `grid[i][n - 1]` 的元素将会移动到 `grid[i + 1][0]`
* 位于 `grid[m - 1][n - 1]` 的元素将会移动到 `grid[0][0]`
* 请你返回 `k` 次迁移操作后最终得到的 二维网格

示例 1:

```
输入:grid = [[1,2,3],[4,5,6],[7,8,9]], k = 1

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

```
输入:grid = [[3,8,1,9],[19,7,2,5],[4,6,11,10],[12,0,21,13]], k = 4

输出:[[12,0,21,13],[3,8,1,9],[19,7,2,5],[4,6,11,10]]
```
示例 3:
```
输入:grid = [[1,2,3],[4,5,6],[7,8,9]], k = 9

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

提示:
* $m == grid.length$
* $n == grid[i].length$
* $1 <= m <= 50$
* $1 <= n <= 50$
* $-1000 <= grid[i][j] <= 1000$
* $0 <= k <= 100$

Solution:
### 模拟

为了方便,我们令 `grid` 为 `g`,令 `n` 和 `m` 分别为 `g` 的行数和列数。

由于迁移过程存在明显规律性,因此我们可以直接 $O(1)$ 算得**每一列最终所在的列下标** `tcol = (i + k) % m`(其中 `i` 为原本的列下标),同时 $O(1)$ 算得**当前列的首行元素在新列中的行下标** `trow = ((i + k) / m) % n`,之后就是简单的遍历赋值操作。

Java 代码:
```Java
class Solution {
    public List<List<Integer>> shiftGrid(int[][] g, int k) {
        int n = g.length, m = g[0].length;
        int[][] mat = new int[n][m];
        for (int i = 0; i < m; i++) {
            int tcol = (i + k) % m, trow = ((i + k) / m) % n, idx = 0;
            while (idx != n) {
                mat[trow++][tcol] = g[idx++][i];
                if (trow == n) trow = 0;
            }
        }
        List<List<Integer>> ans = new ArrayList<>();
        for (int i = 0; i < n; i++) {
            List<Integer> alist = new ArrayList<>();
            for (int j = 0; j < m; j++) alist.add(mat[i][j]);
            ans.add(alist);
        }
        return ans;
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    vector<vector<int>> shiftGrid(vector<vector<int>>& g, int k) {
        int n = g.size(), m = g[0].size();
        vector<vector<int>> mat(n, vector<int>(m));
        for(int i = 0; i < m; ++i) {
            int tcol = (i + k) % m, trow = ((i + k) / m) % n, idx = 0;
            while(idx != n) {
                mat[trow++][tcol] = g[idx++][i];
                if(trow == n) trow = 0;
            }
        }
        return mat;
    }
};
```
Python 代码:
```Python
class Solution:
    def shiftGrid(self, g: List[List[int]], k: int) -> List[List[int]]:
        n, m = len(g), len(g[0])
        mat = [[0]*m for _ in range(n)]
        for i in range(m):
            tcol = (i + k) % m
            trow = ((i + k) // m) % n
            idx = 0
            while idx != n:
                mat[trow][tcol] = g[idx][i]
                trow, idx = (trow + 1) % n, idx + 1
        return mat
```
TypeScript 代码:
```TypeScript
function shiftGrid(g: number[][], k: number): number[][] {
    const n = g.length, m = g[0].length
    const ans: number[][] = new Array<Array<number>>()
    for (let i = 0; i < n; i++) ans[i] = new Array<number>(m).fill(0)
    for (let i = 0; i < m; i++) {
        let tcol = (i + k) % m, trow = Math.floor(((i + k) / m)) % n, idx = 0
        while (idx != n) {
            ans[trow++][tcol] = g[idx++][i]
            if (trow == n) trow = 0
        }
    }
    return ans
};
```
* 时间复杂度:$O(n \times m)$
* 空间复杂度:$O(n \times m)$

### 最后

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

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

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

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


1268. 搜索推荐系统

Question:
给你一个产品数组 `products` 和一个字符串 `searchWord`,`products` 数组中每个产品都是一个字符串。

请你设计一个推荐系统,在依次输入单词 `searchWord` 的每一个字母后,推荐 `products` 数组中前缀与 `searchWord` 相同的最多三个产品。如果前缀相同的可推荐产品超过三个,请按字典序返回最小的三个。

请你以二维列表的形式,返回在输入 `searchWord` 每个字母后相应的推荐产品的列表。

示例 1:
```
输入:products = ["mobile","mouse","moneypot","monitor","mousepad"], searchWord = "mouse"
输出:[
["mobile","moneypot","monitor"],
["mobile","moneypot","monitor"],
["mouse","mousepad"],
["mouse","mousepad"],
["mouse","mousepad"]
]

解释:按字典序排序后的产品列表是 ["mobile","moneypot","monitor","mouse","mousepad"]
输入 m 和 mo,由于所有产品的前缀都相同,所以系统返回字典序最小的三个产品 ["mobile","moneypot","monitor"]
输入 mou, mous 和 mouse 后系统都返回 ["mouse","mousepad"]
```
示例 2:
```
输入:products = ["havana"], searchWord = "havana"

输出:[["havana"],["havana"],["havana"],["havana"],["havana"],["havana"]]
```
示例 3:
```
输入:products = ["bags","baggage","banner","box","cloths"], searchWord = "bags"

输出:[["baggage","bags","banner"],["baggage","bags","banner"],["baggage","bags"],["bags"]]
```
示例 4:
```
输入:products = ["havana"], searchWord = "tatiana"

输出:[[],[],[],[],[],[],[]]
```

提示:
* $1 <= products.length <= 1000$
* $1 <= Σ products[i].length <= 2 \times 10^4$
* `products[i]` 中所有的字符都是小写英文字母。
* $1 <= searchWord.length <= 1000$
* `searchWord` 中所有字符都是小写英文字母。

Solution:
### 排序 + 字典树 + 哈希表

为了方便,将 `products` 记为 `ps`,将 `searchWord` 记为 `w`。

这是一个 `"Suggestion string"` 问题,容易想到字典树进行求解,不了解字典树的同学,可看 [前置 🧀](https://mp.weixin.qq.com/s/DBTQyNYsEgmN0Vb5fbolUg)。

由于题目要求「若有超过三个的产品可推荐,返回字典序最小的三个」,我们不妨先对 `ps` 进行排序,使 `ps` 从前往后满足字典序从小到大。

**将所有 `ps[i]` 按顺序添加到字典树 `tr` 中,添加过程中,使用两个哈希表 `minMap` 和 `maxMap` 分别记录经过某个 `tr[i][j]` 时的最小 `ps` 下标和最大 `ps` 下标**。即哈希表的 `key` 为具体的 `tr[i][j]`,对应 `value` 为经过该节点的最小或最大下标。

构建答案时,**根据当前 `w` 子串到字典树 `tr` 中查询,定位到该子串对应的 `tr[i][j]` 为何值,再从哈希表中获取建议字符串在 `ps` 中的左右端点 `l` 和 `r`**,并根据在 `ps[l:r]` (可能为空集)中最多取三个的原则来构建答案。

考虑实现字典树的两个关键方法,**添加** 和 **查询**:

* 添加函数 `void add(String s, int num)` :其中 `s` 为待添加到字典树的字符串,`num` 则是该字符串在 `ps` 中的下标编号。

  往字典树添加过程中,按照首次访问字典树节点 `tr[i][j]` 的下标存入 `minMap`,最后一次访问字典树节点 `tr[i][j]` 的下标存入 `maxMap` 的规则来更新哈希表。

* 查询函数 `int[] query(String s)`:其中 `s` 为某个 `w` 子串,通过查询该子串(最后字符)在字典树的节点值,来得知建议列表对应 `ps` 的左右端点下标为何值,从而构建答案。

Java 代码:
```Java
class Solution {
    int[][] tr = new int[20010][26];
    int idx = 0;
    Map<Integer, Integer> min = new HashMap<>(), max = new HashMap<>();
    void add(String s, int num) {
        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;
                min.put(tr[p][u], num);
            }
            max.put(tr[p][u], num);
            p = tr[p][u];
        }
    }
    int[] query(String s) {
        int a = -1, b = -1, p = 0;
        for (int i = 0; i < s.length(); i++) {
            int u = s.charAt(i) - 'a';
            if (tr[p][u] == 0) return new int[]{-1, -1};
            a = min.get(tr[p][u]); b = max.get(tr[p][u]);
            p = tr[p][u];
        }
        return new int[]{a, b};
    }
    public List<List<String>> suggestedProducts(String[] ps, String w) {
        Arrays.sort(ps);
        List<List<String>> ans = new ArrayList<>();
        for (int i = 0; i < ps.length; i++) add(ps[i], i);
        for (int i = 0; i < w.length(); i++) {
            List<String> list = new ArrayList<>();
            int[] info = query(w.substring(0, i + 1));
            int l = info[0], r = info[1];
            for (int j = l; j <= Math.min(l + 2, r) && l != -1; j++) list.add(ps[j]);
            ans.add(list);
        }
        return ans;
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    int tr[20010][26] = {0};
    int idx = 0;
    unordered_map<int, int> minMap, maxMap;
    void add(string s, int num) {
        int p = 0;
        for (char c : s) {
            int u = c - 'a';
            if (tr[p][u] == 0) {
                tr[p][u] = ++idx;
                minMap[tr[p][u]] = num;
            }
            maxMap[tr[p][u]] = num;
            p = tr[p][u];
        }
    }
    pair<int, int> query(string s) {
        int a = -1, b = -1, p = 0;
        for (char c : s) {
            int u = c - 'a';
            if (tr[p][u] == 0) return {-1, -1};
            a = minMap[tr[p][u]];
            b = maxMap[tr[p][u]];
            p = tr[p][u];
        }
        return {a, b};
    }
    vector<vector<string>> suggestedProducts(vector<string>& ps, string w) {
        sort(ps.begin(), ps.end());
        vector<vector<string>> ans;
        for (int i = 0; i < ps.size(); i++) add(ps[i], i);
        for (int i = 0; i < w.length(); i++) {
            vector<string> list;
            pair<int, int> info = query(w.substr(0, i + 1));
            int l = info.first, r = info.second;
            for (int j = l; j <= min(l + 2, r) && l != -1; j++) list.push_back(ps[j]);
            ans.push_back(list);
        }
        return ans;
    }
};
```
Python 代码:
```Python
class Solution:
    def suggestedProducts(self, ps: List[str], w: str) -> List[List[str]]:
        tr = defaultdict(lambda: defaultdict(int))
        idx = 0
        minMap, maxMap = {}, {}
        def add(s, num):
            nonlocal idx
            p = 0
            for c in s:
                u = ord(c) - ord('a')
                if tr[p][u] == 0:
                    idx += 1
                    tr[p][u] = idx
                    minMap[tr[p][u]] = num
                maxMap[tr[p][u]] = num
                p = tr[p][u]
        def query(s):
            a, b, p = -1, -1, 0
            for c in s:
                u = ord(c) - ord('a')
                if tr[p][u] == 0:
                    return (-1, -1)
                a = minMap[tr[p][u]]
                b = maxMap[tr[p][u]]
                p = tr[p][u]
            return (a, b)
        ps.sort()
        ans = []
        for i in range(len(ps)):
            add(ps[i], i)
        for i in range(len(w)):
            l, r = query(w[:i + 1])
            lst = [ps[j] for j in range(l, min(l + 3, r + 1)) if l != -1]
            ans.append(lst)
        return ans
```
TypeScript 代码:
```TypeScript
let tr: number[][];
let idx: number;
let minMap: Map<number, number>, maxMap: Map<number, number>;
function add(s: string, num: number): void {
    let p = 0;
    for (let i = 0; i < s.length; i++) {
        const u = s.charCodeAt(i) - 'a'.charCodeAt(0);
        if (tr[p][u] === 0) {
            idx++;
            tr[p][u] = idx;
            minMap.set(tr[p][u], num);
        }
        maxMap.set(tr[p][u], num);
        p = tr[p][u];
    }
}
function query(s: string): number[] {
    let a = -1, b = -1, p = 0;
    for (let i = 0; i < s.length; i++) {
        const u = s.charCodeAt(i) - 'a'.charCodeAt(0);
        if (tr[p][u] === 0) return [-1, -1];
        a = minMap.get(tr[p][u])!;
        b = maxMap.get(tr[p][u])!;
        p = tr[p][u];
    }
    return [a, b];
}
function suggestedProducts(ps: string[], w: string): string[][] {
    tr = new Array(20010).fill(0).map(() => new Array(26).fill(0));
    idx = 0;
    minMap = new Map(), maxMap = new Map();
    ps.sort();
    const ans = [];
    for (let i = 0; i < ps.length; i++) add(ps[i], i);
    for (let i = 0; i < w.length; i++) {
        const list = [];
        const [l, r] = query(w.substring(0, i + 1));
        for (let j = l; j <= Math.min(l + 2, r) && l !== -1; j++) list.push(ps[j]);
        ans.push(list);
    }
    return ans;
};
```
* 时间复杂度:将 `ps` 长度记为 `n`,`w` 长度记为 `m`。对 `ps` 进行排序复杂度为 $O(n\log{n})$;构建字典树复杂度为 $O(\sum_{i = 0}^{n - 1}ps[i].length)$;根据 `w` 构建答案复杂度为 $O(m^2)$;整体复杂度为 $O(\sum_{i = 0}^{n - 1}ps[i].length + m^2)$
* 空间复杂度:$O(\sum_{i = 0}^{n - 1}ps[i].length \times C)$,其中 $C = 26$ 为字符集大小

---

### 排序 + 二分

由于每个 `w` 子串只会对应最多三个的建议字符串,同时又可以先通过排序来确保 `ps` 的有序性。

因此对于每个 `w` 子串而言,可以 **先找到满足要求的,字典序最小的建议字符串 `ps[i]`,接着往后逐个检查,组成最终的建议字符串列表(最多检查三个)**。

这个「在 `ps` 中找符合要求,字典序最小的建议字符串」操作,除了能够利用上述解法来做(省掉一个 `maxMap`)以外,还能利用字符串本身的有序性进行「二分」,因为该操作本质上,是在**找第一个满足 `ps[i]` 大于等于当前子串的位置**。

Java 代码:

```Java
class Solution {
    public List<List<String>> suggestedProducts(String[] ps, String w) {
        Arrays.sort(ps);
        int n = ps.length;
        List<List<String>> ans = new ArrayList<>();
        for (int i = 0; i < w.length(); i++) {
            String cur = w.substring(0, i + 1);
            int l = 0, r = n - 1;
            while (l < r) {
                int mid = l + r >> 1;
                if (ps[mid].compareTo(cur) >= 0) r = mid;
                else l = mid + 1;
            }
            List<String> list = new ArrayList<>();
            if (ps[r].compareTo(cur) >= 0) {
                for (int j = r; j <= Math.min(n - 1, r + 2); j++) {
                    if (ps[j].length() < cur.length()) break;
                    if (!ps[j].substring(0, i + 1).equals(cur)) break;
                    list.add(ps[j]);
                }
            }
            ans.add(list);
        }
        return ans;
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    vector<vector<string>> suggestedProducts(vector<string>& ps, string w) {
        sort(ps.begin(), ps.end());
        int n = ps.size();
        vector<vector<string>> ans;
        for (int i = 0; i < w.length(); i++) {
            string cur = w.substr(0, i + 1);
            int l = 0, r = n - 1;
            while (l < r) {
                int mid = (l + r) >> 1;
                if (ps[mid].compare(cur) >= 0) r = mid;
                else l = mid + 1;
            }
            vector<string> list;
            if (ps[r].compare(cur) >= 0) {
                for (int j = r; j <= min(n - 1, r + 2); j++) {
                    if (ps[j].length() < cur.length()) break;
                    if (ps[j].substr(0, i + 1) != cur) break;
                    list.push_back(ps[j]);
                }
            }
            ans.push_back(list);
        }
        return ans;
    }
};
```
Python 代码:
```Python
class Solution:
    def suggestedProducts(self, ps: List[str], w: str) -> List[List[str]]:
        ps.sort()
        n = len(ps)
        ans = []
        for i in range(len(w)):
            cur = w[:i + 1]
            l, r = 0, n - 1
            while l < r:
                mid = (l + r) // 2
                if ps[mid] >= cur:
                    r = mid
                else:
                    l = mid + 1
            lst = []
            if ps[r] >= cur:
                for j in range(r, min(n - 1, r + 2) + 1):
                    if len(ps[j]) < len(cur) or ps[j][:i + 1] != cur:
                        break
                    lst.append(ps[j])
            ans.append(lst)
        return ans
```
TypeScript 代码:
```TypeScript
function suggestedProducts(ps: string[], w: string): string[][] {
    ps.sort();
    const n = ps.length;
    const ans = [];
    for (let i = 0; i < w.length; i++) {
        const cur = w.substring(0, i + 1);
        let l = 0, r = n - 1;
        while (l < r) {
            const mid = (l + r) >> 1;
            if (ps[mid].localeCompare(cur) >= 0) r = mid;
            else l = mid + 1;
        }
        const list: string[] = [];
        if (ps[r].localeCompare(cur) >= 0) {
            for (let j = r; j <= Math.min(n - 1, r + 2); j++) {
                if (ps[j].length < cur.length || !ps[j].startsWith(cur)) break;
                list.push(ps[j]);
            }
        }
        ans.push(list);
    }
    return ans;
};
```
* 时间复杂度:将 `ps` 长度记为 `n`,`w` 长度记为 `m`。对 `ps` 进行排序复杂度为 $O(n\log{n})$;每次二分需要进行字符串比较,复杂度为 $O(m\log{n})$;二分到左端点后需要往后检查最多三个字符串,复杂度为 $O(m)$。整体复杂度为 $O(n\log{n} + m\log{n})$
* 空间复杂度:$O(\log{n})$

### 最后

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

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

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

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


LeetCode/1261-1270/1269. 停在原地的方案数（困难）.md

1269. 停在原地的方案数

Question:
有一个长度为 arrLen 的数组,开始有一个指针在索引 0 处。

每一步操作中,你可以将指针向左或向右移动 1 步,或者停在原地(指针不能被移动到数组范围外)。

给你两个整数 steps 和 arrLen ,请你计算并返回:在恰好执行 steps 次操作以后,指针仍然指向索引 0 处的方案数。

由于答案可能会很大,请返回方案数 模 $10^9$ + 7 后的结果。

示例 1:
```
输入:steps = 3, arrLen = 2

输出:4

解释:3 步后,总共有 4 种不同的方法可以停在索引 0 处。
向右,向左,不动
不动,向右,向左
向右,不动,向左
不动,不动,不动
```
示例  2:
```
输入:steps = 2, arrLen = 4

输出:2

解释:2 步后,总共有 2 种不同的方法可以停在索引 0 处。
向右,向左
不动,不动
```
示例 3:
```
输入:steps = 4, arrLen = 2

输出:8
```

提示:
* 1 <= steps <= 500
* 1 <= arrLen <= $10^6$

Solution:
### 动态规划

这道题的可变维度分析不算复杂,因此这次就不从 `DFS` 开始给大家分析了。

**定义 $f[i][j]$ 代表当前剩余操作数为 $i$,所在位置为 $j$ 的所有方案数。**

起始位置为 $0$,操作次数为 $step$,即有初始化条件 $f[step][0] = 1$,$f[0][0]$ 则是我们的最终答案。

不失一般性的考虑 $f[i][j]$ 可以由哪些状态转移而来:

* 由「原地」操作到达当前状态,消耗一次操作,此时由状态 $f[i + 1][j]$ 转移而来
* 由「向左」操作到达当前状态,消耗一次操作,此时由状态 $f[i + 1][j + 1]$ 转移而来
* 由「向右」操作到达当前状态,消耗一次操作,此时由状态 $f[i + 1][j - 1]$ 转移而来

求的是方案数,即最终的 $f[i][j]$ 为三者累加值。

同时我们发现 $f[i][x]$ 依赖于 $f[i + 1][y]$,因此我们需要按照「$step$ 从大到小」的顺序进行转移。

同时我们根据「最终回到下标 $0$ 位置」可以推断出,最远到达的位置为 $step / 2$(再远就回不来了)。将最远到达位置与数组最大下标取 $min$ 即可确定维度 $step$ 的范围。

代码:
```Java
class Solution {
    int mod = (int)1e9+7;
    public int numWays(int steps, int len) {
        int max = Math.min(steps / 2, len - 1);
        int[][] f = new int[steps + 1][max + 1];
        f[steps][0] = 1;
        for (int i = steps - 1; i >= 0; i--) {
            for (int j = 0; j <= max; j++) {
                f[i][j] = (f[i][j] + f[i + 1][j]) % mod;
                if (j - 1 >= 0) f[i][j] = (f[i][j] + f[i + 1][j - 1]) % mod;
                if (j + 1 <= max) f[i][j] = (f[i][j] + f[i + 1][j + 1]) % mod;
            }
        }
        return f[0][0];
    }
}
```
* 时间复杂度:共有数量级为 $step * max$ 个的状态需要被转移。复杂度为 $O(step * max)$
* 空间复杂度:$O(step * max)$

---

### 优化

**1. 对时间复杂度进行「常数级别的优化」**

$f[0][0]$ 并不依赖于操作次数同为 $0$ 的其他位置的状态,而只依赖于操作次数为 $1$ 的特定位置的状态。同理其他状态也是。

因此我们会发现**随着「可操作次数」的减少,「可达到的最远位置」下标也会逐步缩小。从目标状态 $f[0][0]$ 进行倒推的话,会发现「可达到的最远位置」等于「可操作次数」。**

**所以其实可以从两者取一个 $min$ 就能够有效减少「无效状态」的计算。数据量越大,这个性质带来的剪枝效果越好。**

*PS. 为了方便你看到优化前后的差别,我增加了打印注释,使用测试数据 (500, 100000) 并打开注释,可以看到少计算了多少「无效状态」。*

代码:
```Java
class Solution {
    int mod = (int)1e9+7;
    public int numWays(int steps, int len) {
        int max = Math.min(steps / 2, len - 1);
        int[][] f = new int[steps + 1][max + 1];
        f[steps][0] = 1;
        for (int i = steps - 1; i >= 0; i--) {
            int edge = Math.min(i, max);
            // if (edge != max) System.out.println(edge + " " + max);
            for (int j = 0; j <= edge; j++) {
                f[i][j] = (f[i][j] + f[i + 1][j]) % mod;
                if (j - 1 >= 0) f[i][j] = (f[i][j] + f[i + 1][j - 1]) % mod;
                if (j + 1 <= max) f[i][j] = (f[i][j] + f[i + 1][j + 1]) % mod;
            }
        }
        return f[0][0];
    }
}
```
* 时间复杂度:共有数量级为 $step * max$ 个的状态需要被转移。复杂度为 $O(step * max)$
* 空间复杂度:$O(step * max)$

**2. 对空间复杂度进行「维度级别的优化」**

这个优化思维难度就要低很多了,利用 $f[i][x]$ 依赖于 $f[i + 1][y]$,使用「滚动数组」方式进行优化即可。

代码:
```Java
class Solution {
    int mod = (int)1e9+7;
    public int numWays(int steps, int len) {
        int max = Math.min(steps / 2, len - 1);
        int[][] f = new int[2][max + 1];
        f[steps&1][0] = 1;
        for (int i = steps - 1; i >= 0; i--) {
            int edge = Math.min(i, max);
            int a = i & 1, b = (i + 1) & 1;
            for (int j = 0; j <= edge; j++) {
                f[a][j] = 0;
                f[a][j] = (f[a][j] + f[b][j]) % mod;
                if (j - 1 >= 0) f[a][j] = (f[a][j] + f[b][j - 1]) % mod;
                if (j + 1 <= max) f[a][j] = (f[a][j] + f[b][j + 1]) % mod;
            }
        }
        return f[0][0];
    }
}
```
* 时间复杂度:共有数量级为 $step * max$ 个的状态需要被转移。复杂度为 $O(step * max)$
* 空间复杂度:$O(max)$

### 最后

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

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

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

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


Question:
有 `n` 个人被分成数量未知的组。每个人都被标记为一个从 $0$ 到 $n - 1$ 的唯一`ID` 。

给定一个整数数组 `groupSizes`,其中 `groupSizes[i]` 是第 $i$ 个人所在的组的大小。例如,如果 `groupSizes[1] = 3`,则第 $1$ 个人必须位于大小为  $3$  的组中。

返回一个组列表,使每个人 $i$ 都在一个大小为 `groupSizes[i]` 的组中。

每个人应该恰好只出现在 一个组中,并且每个人必须在一个组中。如果有多个答案,返回其中任何一个。可以保证给定输入至少有一个有效的解。

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

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

解释:
第一组是 [5],大小为 1,groupSizes[5] = 1。
第二组是 [0,1,2],大小为 3,groupSizes[0] = groupSizes[1] = groupSizes[2] = 3。
第三组是 [3,4,6],大小为 3,groupSizes[3] = groupSizes[4] = groupSizes[6] = 3。
其他可能的解决方案有 [[2,1,6],[5],[0,4,3]] 和 [[5],[0,6,2],[4,3,1]]。
```
示例 2:
```
输入:groupSizes = [2,1,3,3,3,2]

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

提示:
* $groupSizes.length == n$
* $1 <= n <= 500$
* $1 <= groupSizes[i] <= n$

Solution:
### 哈希表 + 模拟

我们可以使用「哈希表」将所属组大小相同的下标放到一起。假设组大小为 $k$ 的元素有 $m$ 个,然后我们再将这 $m$ 个元素按照 $k$ 个一组进行划分即可。

Java 代码:
```Java
class Solution {
    public List<List<Integer>> groupThePeople(int[] gs) {
        Map<Integer, List<Integer>> map = new HashMap<>();
        for (int i = 0; i < gs.length; i++) {
            List<Integer> list = map.getOrDefault(gs[i], new ArrayList<>());
            list.add(i);
            map.put(gs[i], list);
        }
        List<List<Integer>> ans = new ArrayList<>();
        for (int k : map.keySet()) {
            List<Integer> list = map.get(k), cur = new ArrayList<>();
            for (int i = 0; i < list.size(); i++) {
                cur.add(list.get(i));
                if (cur.size() == k) {
                    ans.add(cur);
                    cur = new ArrayList<>();
                }
            }
        }
        return ans;
    }
}
```
Typescript 代码:
```Typescript
function groupThePeople(gs: number[]): number[][] {
    const map = new Map<number, Array<number>>()
    for (let i = 0; i < gs.length; i++) {
        if (!map.has(gs[i])) map.set(gs[i], new Array<number>())
        map.get(gs[i]).push(i)
    }
    const ans = new Array<Array<number>>()
    for (let k of map.keys()) {
        let list = map.get(k), cur = new Array<number>()
        for (let i = 0; i < list.length; i++) {
            cur.push(list[i])
            if (cur.length == k) {
                ans.push(cur)
                cur = new Array<number>()
            }
        }
    }
    return ans
};
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


LeetCode/1301-1310/1302. 层数最深叶子节点的和（中等）.md

1302. 层数最深叶子节点的和

Question:
给你一棵二叉树的根节点 `root`,请你返回 层数最深的叶子节点的和 。

示例 1:

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

输出:15
```
示例 2:
```
输入:root = [6,7,8,2,7,1,3,9,null,1,4,null,null,null,5]

输出:19
```

提示:
* 树中节点数目在范围 $[1, 10^4]$ 之间。
* $1 <= Node.val <= 100$

Solution:
### BFS

使用 `BFS` 进行树的遍历,处理过程中记录最大深度 `depth` 以及使用哈希表记录每层元素和。

Java 代码:
```Java
class Solution {
    public int deepestLeavesSum(TreeNode root) {
        Map<Integer, Integer> map = new HashMap<>();
        Deque<TreeNode> d = new ArrayDeque<>();
        d.addLast(root);
        int depth = 0;
        while (!d.isEmpty()) {
            int sz = d.size();
            while (sz-- > 0) {
                TreeNode node = d.pollFirst();
                map.put(depth, map.getOrDefault(depth, 0) + node.val);
                if (node.left != null) d.addLast(node.left);
                if (node.right != null) d.addLast(node.right);
            }
            depth++;
        }
        return map.get(depth - 1);
    }
}
```
TypeScript 代码:
```TypeScript
function deepestLeavesSum(root: TreeNode | null): number {
    const map: Map<number, number> = new Map<number, number>()
    const stk: TreeNode[] = new Array<TreeNode>(10010)
    let he = 0, ta = 0, depth = 0
    stk[ta++] = root
    while (he < ta) {
        let sz = ta - he
        while (sz-- > 0) {
            const node = stk[he++]
            if (!map.has(depth)) map.set(depth, 0)
            map.set(depth, map.get(depth) + node.val)
            if (node.left != null) stk[ta++] = node.left
            if (node.right != null) stk[ta++] = node.right
        }
        depth++
    }
    return map.get(depth - 1)
};
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n)$

---

### DFS

使用 `DFS` 进行树的遍历,处理过程中记录最大深度 `depth` 以及使用哈希表记录每层元素和。

Java 代码:
```Java
class Solution {
    Map<Integer, Integer> map = new HashMap<>();
    int max;
    public int deepestLeavesSum(TreeNode root) {
        dfs(root, 0);
        return map.get(max);
    }
    void dfs(TreeNode root, int depth) {
        if (root == null) return ;
        max = Math.max(max, depth);
        map.put(depth, map.getOrDefault(depth, 0) + root.val);
        dfs(root.left, depth + 1);
        dfs(root.right, depth + 1);
    }
}
```
TypeScript 代码:
```TypeScript
const map: Map<number, number> = new Map<number, number>()
let max: number
function deepestLeavesSum(root: TreeNode | null): number {
    map.clear()
    max = 0
    dfs(root, 0)
    return map.get(max)
};
function dfs(root: TreeNode | null, depth: number): void {
    if (root == null) return
    max = Math.max(max, depth)
    if (!map.has(depth)) map.set(depth, 0)
    map.set(depth, map.get(depth) + root.val)
    dfs(root.left, depth + 1)
    dfs(root.right, depth + 1)
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


LeetCode/1301-1310/1305. 两棵二叉搜索树中的所有元素（中等）.md

1305. 两棵二叉搜索树中的所有元素

Question:
给你 `root1` 和` root2` 这两棵二叉搜索树。请你返回一个列表,其中包含 两棵树 中的所有整数并按 升序 排序。.

示例 1:

```
输入:root1 = [2,1,4], root2 = [1,0,3]

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

```
输入:root1 = [1,null,8], root2 = [8,1]

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

提示:
* 每棵树的节点数在 $[0, 5000]$ 范围内
* $-10^5 <= Node.val <= 10^5$

Solution:
### 中序遍历 + 归并排序

利用 `BST` 中序遍历的有序性质,我们可以先对两棵树进行中序遍历,从而将树的结构转换为线性结构。

将两个有序序列合并成一个有序序列则是利用了经典的「归并排序」。

代码:
```Java
class Solution {
    int INF = 0x3f3f3f3f;
    public List<Integer> getAllElements(TreeNode root1, TreeNode root2) {
        List<Integer> ans = new ArrayList<>();
        List<Integer> l1 = new ArrayList<>(), l2 = new ArrayList<>();
        dfs(root1, l1); dfs(root2, l2);
        int n = l1.size(), m = l2.size(), i = 0, j = 0;
        while (i < n || j < m) {
            int a = i < n ? l1.get(i) : INF, b = j < m ? l2.get(j) : INF;
            if (a <= b) {
                ans.add(a); i++;
            } else {
                ans.add(b); j++;
            }
        }
        return ans;
    }
    void dfs(TreeNode root, List<Integer> list) {
        if (root == null) return ;
        dfs(root.left, list);
        list.add(root.val);
        dfs(root.right, list);
    }
}
```
* 时间复杂度:令 $n$ 和 $m$ 分别为两棵树的节点数量,跑中序遍历的复杂度为 $O(n + m)$,构建答案复杂度为 $O(\max(m, n))$。整体复杂度为 $O(n + m)$
* 空间复杂度:$O(n + m)$

### 最后

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

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

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

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


1310. 子数组异或查询

Question:
有一个正整数数组 arr,现给你一个对应的查询数组 queries,其中 queries[i] = [Li, Ri]。

对于每个查询 i,请你计算从 Li 到 Ri 的 XOR 值(即 arr[Li] xor arr[Li+1] xor ... xor arr[Ri])作为本次查询的结果。

并返回一个包含给定查询 queries 所有结果的数组。

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

输出:[2,7,14,8]

解释:
数组中元素的二进制表示形式是:
1 = 0001
3 = 0011
4 = 0100
8 = 1000
查询的 XOR 值为:
[0,1] = 1 xor 3 = 2
[1,2] = 3 xor 4 = 7
[0,3] = 1 xor 3 xor 4 xor 8 = 14
[3,3] = 8
```
示例 2:
```
输入:arr = [4,8,2,10], queries = [[2,3],[1,3],[0,0],[0,3]]

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

提示:
* 1 <= arr.length <= 3 * $10^4$
* 1 <= arr[i] <= $10^9$
* 1 <= queries.length <= 3 * $10^4$
* queries[i].length == 2
* 0 <= queries[i][0] <= queries[i][1] < arr.length

Solution:
### 基本分析

令数组 `arr` 和数组 `queries` 的长度分别为 `n` 和 `m`。

`n` 和 `m` 的数据范围均为 $10^4$,因此 $O(m * n)$ 的暴力做法我们不用考虑了。

数据范围要求我们做到「对数复杂度」或「线性复杂度」。

**本题主要利用异或运算中的「相同数值进行运算结果为 $0$」的特性。**

对于特定数组 $[a1, a2, a3, ... , an]$,要求得任意区间 $[l, r]$ 的异或结果,可以通过 $[1, r]$ 和 $[1, l - 1]$ 的异或结果得出:

$$
xor(l, r) = xor(1, r) ⊕ xor(1, l - 1)
$$

**本质上还是利用集合(区间结果)的容斥原理。只不过前缀和需要利用「减法(逆运算)」做容斥,而前缀异或是利用「相同数值进行异或结果为 $0$(偶数次的异或结果为 $0$)」的特性实现容斥。**

对于「区间求值」问题,之前在 [【题解】307. 区域和检索 - 数组可修改](https://leetcode-cn.com/problems/range-sum-query-mutable/solution/guan-yu-ge-lei-qu-jian-he-wen-ti-ru-he-x-41hv/) 也做过总结。

针对不同的题目,有不同的方案可以选择(假设有一个数组):

1. 数组不变,求区间和:「前缀和」、「树状数组」、「线段树」
2. 多次修改某个数,求区间和:「树状数组」、「线段树」
3. 多次整体修改某个区间,求区间和:「线段树」、「树状数组」(看修改区间的数据范围)
4. 多次将某个区间变成同一个数,求区间和:「线段树」、「树状数组」(看修改区间的数据范围)

虽然「线段树」能解决的问题最多,但「线段树」代码很长,且常数很大,实际表现不算好。我们只有在不得不用的情况下才考虑「线段树」。

本题我们使用「树状数组」和「前缀和」来求解。

---

### 树状数组

使用「树状数组」分段记录我们某些区间的「异或结果」,再根据 `queries` 中的询问将分段「异或结果」汇总(执行异或运算),得出最终答案。

代码:
```Java
class Solution {
    int n;
    int[] c = new int[100009];
    int lowbit(int x) {
        return x & -x;
    }
    void add(int x, int u) {
        for (int i = x; i <= n; i += lowbit(i)) c[i] ^= u;
    }
    int query(int x) {
        int ans = 0;
        for (int i = x; i > 0; i -= lowbit(i)) ans ^= c[i];
        return ans;
    }
    public int[] xorQueries(int[] arr, int[][] qs) {
        n = arr.length;
        int m = qs.length;
        for (int i = 1; i <= n; i++) add(i, arr[i - 1]);
        int[] ans = new int[m];
        for (int i = 0; i < m; i++) {
            int l = qs[i][0] + 1, r = qs[i][1] + 1;
            ans[i] = query(r) ^ query(l - 1);
        }
        return ans;
    }
}
```
* 时间复杂度:令 `arr` 数组长度为 `n`,`qs` 数组的长度为 `m`。创建树状数组复杂度为 $O(n\log{n})$;查询的复杂度为 $O(m\log{n})$。整体复杂度为 $O((n + m) \log{n})$
* 空间复杂度:$O(n)$

---

### 前缀异或

「树状数组」的查询复杂度为 $O(\log{n})$,而本题其实不涉及「修改操作」,我们可以使用「前缀异或」来代替「树状数组」。

虽说「树状数组」也有 $O(n)$ 的创建方式,但这里使用「前缀异或」主要是为了降低查询的复杂度。

代码:
```Java
class Solution {
    public int[] xorQueries(int[] arr, int[][] qs) {
        int n = arr.length, m = qs.length;
        int[] sum = new int[n + 1];
        for (int i = 1; i <= n; i++) sum[i] = sum[i - 1] ^ arr[i - 1];
        int[] ans = new int[m];
        for (int i = 0; i < m; i++) {
            int l = qs[i][0] + 1, r = qs[i][1] + 1;
            ans[i] = sum[r] ^ sum[l - 1];
        }
        return ans;
    }
}
```
* 时间复杂度:令 `arr` 数组长度为 `n`,`qs` 数组的长度为 `m`。预处理前缀和数组复杂度为 $O(n)$;查询的复杂度为 $O(m)$。整体复杂度为 $O(n + m)$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


Question:
给你一个字符串 `s`,请你将 `s` 分割成一些子串,使每个子串都是回文串。

返回 `s` 所有可能的分割方案。

回文串是正着读和反着读都一样的字符串。

示例 1:
```
输入:s = "aab"
输出:[["a","a","b"],["aa","b"]]
```
示例 2:
```
输入:s = "a"
输出:[["a"]]
```

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

Solution:
### 动态规划 + 回溯算法

求所有的分割方案,凡是求所有方案的题基本上都没有什么优化方案,就是「爆搜」。

问题在于,爆搜什么?显然我们可以爆搜每个回文串的起点。如果有连续的一段是回文串,我们再对剩下连续的一段继续爆搜。

为什么能够直接接着剩下一段继续爆搜?

因为任意的子串最终必然能够分割成若干的回文串(最坏的情况下,每个回文串都是一个字母)。

所以我们每次往下爆搜时,只需要保证自身连续一段是回文串即可。

举个🌰 来感受下我们的爆搜过程,假设有样例 `abababa`,刚开始我们从起点第一个 a 进行爆搜:

1. 发现 `a` 是回文串,先将 `a` 分割出来,再对剩下的 `bababa` 进行爆搜
2. 发现 `aba` 是回文串,先将 `aba` 分割出来,再对剩下的 `baba` 进行爆搜
3. 发现 `ababa` 是回文串,先将 `ababa` 分割出来,再对剩下的 `ba` 进行爆搜
4. 发现 `abababa` 是回文串,先将 `abababa` 分割出来,再对剩下的 `` 进行爆搜

...

然后再对下一个起点(下个字符) `b` 进行爆搜?

不需要。

因为单个字符本身构成了回文串,所以以 `b` 为起点,`b` 之前构成回文串的方案,必然覆盖在我们以第一个字符为起点所展开的爆搜方案内(在这里就是对应了上述的第一步所展开的爆搜方案中)。

**因此我们只需要以首个字符为起点,枚举以其开头所有的回文串方案,加入集合,然后对剩下的字符串部分继续爆搜。就能做到以任意字符作为回文串起点进行分割的效果了。**

一定要好好理解上面那句话 ~

剩下的问题是,我们如何快速判断连续一段 `[i, j]` 是否为回文串,因为爆搜的过程每个位置都可以作为分割点,复杂度为 $O(2^n)$ 的。

因此我们不可能每次都使用双指针去线性扫描一遍 `[i, j]` 判断是否回文。

一个直观的做法是,我们先预处理除所有的 `f[i][j]`,`f[i][j]` 代表 `[i, j]` 这一段是否为回文串。

预处理 `f[i][j]` 的过程可以用递推去做。

要想 `f[i][j] == true` ,必须满足以下两个条件:

1. `f[i + 1][j - 1] == true`
2. `s[i] == s[j]`

由于状态 `f[i][j]` 依赖于状态 `f[i + 1][j - 1]`,因此需要我们左端点 `i` 是**从大到小**进行遍历;而右端点 `j` 是**从小到大**进行遍历。

因此,我们的遍历过程可以整理为:**右端点 `j` 一直往右移动(从小到大),在 `j` 固定情况下,左端点 `i` 在 `j` 在左边开始,一直往左移动(从大到小)**

Java 代码:
```Java
class Solution {
    public List<List<String>> partition(String s) {
        int n = s.length();
        char[] cs = s.toCharArray();
        // f[i][j] 代表 [i, j] 这一段是否为回文串
        boolean[][] f = new boolean[n][n];
        for (int j = 0; j < n; j++) {
            for (int i = j; i >= 0; i--) {
                // 当 [i, j] 只有一个字符时,必然是回文串
                if (i == j) {
                    f[i][j] = true;
                } else {
                    // 当 [i, j] 长度为 2 时,满足 cs[i] == cs[j] 即回文串
                    if (j - i + 1 == 2) {
                        f[i][j] = cs[i] == cs[j];

                    // 当 [i, j] 长度大于 2 时,满足 (cs[i] == cs[j] && f[i + 1][j - 1]) 即回文串
                    } else {
                        f[i][j] = cs[i] == cs[j] && f[i + 1][j - 1];
                    }
                }
            }
        }
        List<List<String>> ans = new ArrayList<>();
        List<String> cur = new ArrayList<>();
        dfs(s, 0, ans, cur, f);
        return ans;
    }
    /**
     * s: 要搜索的字符串
     * u: 以 s 中的那一位作为回文串分割起点
     * ans: 最终结果集
     * cur: 当前结果集
     * f: 快速判断 [i,j] 是否为回文串
     */
    void dfs(String s, int u, List<List<String>> ans, List<String> cur, boolean[][] f) {
        int n = s.length();
        if (u == n) ans.add(new ArrayList<>(cur));
        for (int i = u; i < n; i++) {
            if (f[u][i]) {
                cur.add(s.substring(u, i + 1));
                dfs(s, i + 1, ans, cur, f);
                cur.remove(cur.size() - 1);
            }
        }
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    vector<vector<string>> partition(string s) {
        int n = s.length();
        vector<vector<string>> ans;
        vector<string> cur;
        vector<vector<bool>> f(n, vector<bool>(n, false));
        for (int j = 0; j < n; j++) {
            for (int i = j; i >= 0; i--) {
                if (i == j) {
                    f[i][j] = true;
                } else if (j - i + 1 == 2) {
                    f[i][j] = s[i] == s[j];
                } else {
                    f[i][j] = s[i] == s[j] && f[i + 1][j - 1];
                }
            }
        }
        dfs(s, 0, ans, cur, f);
        return ans;
    }
    void dfs(string& s, int u, vector<vector<string>>& ans, vector<string>& cur, vector<vector<bool>>& f) {
        int n = s.length();
        if (u == n) ans.push_back(cur);
        for (int i = u; i < n; i++) {
            if (f[u][i]) {
                cur.push_back(s.substr(u, i - u + 1));
                dfs(s, i + 1, ans, cur, f);
                cur.pop_back();
            }
        }
    }
};
```
Python 代码:
```Python
class Solution:
    def partition(self, s: str) -> List[List[str]]:
        def dfs(s: str, u: int, ans: List[List[str]], cur: List[str], f: List[List[bool]]):
            n = len(s)
            if u == n:
                ans.append(cur[:])
            for i in range(u, n):
                if f[u][i]:
                    cur.append(s[u:i + 1])
                    dfs(s, i + 1, ans, cur, f)
                    cur.pop()

        n = len(s)
        ans, cur = [], []
        f = [[False] * n for _ in range(n)]
        for j in range(n):
            for i in range(j, -1, -1):
                if i == j:
                    f[i][j] = True
                elif j - i + 1 == 2:
                    f[i][j] = s[i] == s[j]
                else:
                    f[i][j] = s[i] == s[j] and f[i + 1][j - 1]
        dfs(s, 0, ans, cur, f)
        return ans
```
TypeScript 代码:
```TypeScript
function partition(s: string): string[][] {
    const dfs = function(s: string, u: number, ans: string[][], cur: string[], f: boolean[][]): void {
        const n = s.length;
        if (u == n) ans.push([...cur]);
        for (let i = u; i < n; i++) {
            if (f[u][i]) {
                cur.push(s.substring(u, i + 1));
                dfs(s, i + 1, ans, cur, f);
                cur.pop();
            }
        }
    };
    const n = s.length;
    const ans = [], cur = [];
    const f = Array.from({ length: n }, () => Array(n).fill(false));
    for (let j = 0; j < n; j++) {
        for (let i = j; i >= 0; i--) {
            if (i == j) {
                f[i][j] = true;
            } else if (j - i + 1 === 2) {
                f[i][j] = s[i] == s[j];
            } else {
                f[i][j] = s[i] == s[j] && f[i + 1][j - 1];
            }
        }
    }
    dfs(s, 0, ans, cur, f);
    return ans;
};
```
* 时间复杂度:动态规划预处理的复杂度为 $O(n^2)$;爆搜过程中每个字符都可以作为分割点,并且有分割与不分割两种选择,方案数量为 $2^{n - 1}$,每个字符都需要往后检查剩余字符的分割情况,复杂度为 $O(n)$。整体复杂度为 $O(n \times 2^n)$
* 空间复杂度:动态规划部分的复杂度为 $O(n^2)$;方案数量最多为 $2^{n - 1}$,每个方案都是完整字符串 `s` 的分割,复杂度为 $O(n)$,整体复杂度为 $O(n \times 2^n)$

---

### 总结

对于此类要枚举所有方案的题目,我们都应该先想到「回溯算法」。

「回溯算法」从算法定义上来说,不一定要用 DFS 实现,但通常结合 DFS 来做,难度是最低的。

「回溯算法」根据当前决策有多少种选择,对应了两套模板。

每一次独立的决策只对应 选择 和 不选 两种情况:

1. 确定结束回溯过程的 base case

2. 遍历每个位置,对每个位置进行决策(做选择 -> 递归 -> 撤销选择)

```java
void dfs(当前位置, 路径(当前结果), 结果集) {
    if (当前位置 == 结束位置) {
        结果集.add(路径);
        return;
    }

    选择当前位置;
    dfs(下一位置, 路径(当前结果), 结果集);
    撤销选择当前位置;
    dfs(下一位置, 路径(当前结果), 结果集);
}
```

每一次独立的决策都对应了多种选择(通常对应了每次决策能选择什么,或者每次决策能选择多少个 ...):

1. 确定结束回溯过程的 base case

2. 遍历所有的「选择」

3. 对选择进行决策 (做选择 -> 递归 -> 撤销选择)

```java
void dfs(选择列表, 路径(当前结果), 结果集) {
    if (满足结束条件) {
        结果集.add(路径);
        return;
    }

    for (选择 in 选择列表) {
        做选择;
        dfs(路径’, 选择列表, 结果集);
        撤销选择;
    }
}
```

### 最后

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

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

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

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