LeetCode/111-120/117. 填充每个节点的下一个右侧节点指针 II（中等）.md

117. 填充每个节点的下一个右侧节点指针 II

Question:
给定一个二叉树:

```
struct Node {
  int val;
  Node *left;
  Node *right;
  Node *next;
}
```

填充它的每个 `next` 指针,让这个指针指向其下一个右侧节点。如果找不到下一个右侧节点,则将 `next` 指针设置为 `NULL`。

初始状态下,所有 `next` 指针都被设置为 `NULL`。

示例 1:

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

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

解释:给定二叉树如图 A 所示,你的函数应该填充它的每个 next 指针,以指向其下一个右侧节点,如图 B 所示。序列化输出按层序遍历顺序(由 next 指针连接),'#' 表示每层的末尾。
```
示例 2:
```
输入:root = []

输出:[]
```

提示:
* 树中的节点数在范围 $[0, 6000]$ 内
* $-100 <= Node.val <= 100$

进阶:
* 你只能使用常量级额外空间。
* 使用递归解题也符合要求,本题中递归程序的隐式栈空间不计入额外空间复杂度。

Solution:
### BFS - 层序遍历

一个最直观的做法,是利用 `BFS`  对树进行「层序遍历」。

每个 `BFS`  回合中,对整层节点进行处理。

首先通过 `pop` 拿到当前节点,同时通过 `peek` 拿到当前层的下一节点,并建立 `next` 关系,注意需要跳过每层的最后一个节点。

Java 代码:

```Java
class Solution {
    public Node connect(Node root) {
        Node ans = root;
        if (root == null) return ans;
        Deque<Node> d = new ArrayDeque<>();
        d.addLast(root);
        while (!d.isEmpty()) {
            int sz = d.size();
            while (sz-- > 0) {
                Node a = d.pollFirst();
                if (sz != 0) a.next = d.peekFirst();
                if (a.left != null) d.addLast(a.left);
                if (a.right != null) d.addLast(a.right);
            }
        }
        return ans;
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    Node* connect(Node* root) {
        Node* ans = root;
        if (root == nullptr) return ans;
        deque<Node*> d;
        d.push_back(root);
        while (!d.empty()) {
            int sz = d.size();
            while (sz-- > 0) {
                Node* a = d.front();
                d.pop_front();
                if (sz != 0) a->next = d.front();
                if (a->left) d.push_back(a->left);
                if (a->right) d.push_back(a->right);
            }
        }
        return ans;
    }
};
```
Python 代码:
```Python
class Solution:
    def connect(self, root: 'Node') -> 'Node':
        ans = root
        if root is None:
            return ans
        d = [root]
        while d:
            sz = len(d)
            for i in range(sz):
                a = d.pop(0)
                if i != sz - 1:
                    a.next = d[0]
                if a.left:
                    d.append(a.left)
                if a.right:
                    d.append(a.right)
        return ans
```
TypeScript 代码:
```TypeScript
function connect(root: Node | null): Node | null {
    let ans = root;
    if (root == null) return ans;
    const d = [root];
    while (d.length > 0) {
        let sz = d.length;
        while (sz-- > 0) {
            const a = d.shift() as Node;
            if (sz != 0) a.next = d[0];
            if (a.left) d.push(a.left);
            if (a.right) d.push(a.right);
        }
    }
    return ans;
};
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n)$

---

### BFS - 逐层构建

上述解法中,每一层节点的 `next` 关系的构建都是独立的。我们构建第 $k$ 层的 `next` 关系时,并没有利用已建好的到第 $k - 1$ 层的 `next` 关系。

实际上,每一层构建好的 `next` 关系都可看做「链表」,可参与到下一层 `next` 关系的构建,同时由于所有节点的起始 `next` 指向都是 `null`,相当于首层的 `next` 关系已经默认建好了。

我们使用图解方式,来感受逐层构建过程:

Java 代码:
```Java
class Solution {
    public Node connect(Node root) {
        Node ans = root;
        if (root == null) return ans;
        Node cur = root;
        while (cur != null) {
            Node head = new Node(-1), tail = head;
            for (Node i = cur; i != null; i = i.next) {
                if (i.left != null) tail = tail.next = i.left;
                if (i.right != null) tail = tail.next = i.right;
            }
            cur = head.next;
        }
        return ans;
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    Node* connect(Node* root) {
        auto ans = root;
        if (root == nullptr) return ans;
        auto cur = root;
        while (cur) {
            auto head = new Node(-1), tail = head;
            for (auto i = cur; i != nullptr; i = i->next) {
                if (i->left) tail = tail->next = i->left;
                if (i->right) tail = tail->next = i->right;
            }
            cur = head->next;
        }
        return ans;
    }
};
```
Python 代码:
```Python
class Solution:
    def connect(self, root: 'Node') -> 'Node':
        ans = root
        if not root:
            return ans
        cur = root
        while cur:
            head = Node(-1)
            tail = head
            i = cur
            while i:
                if i.left:
                    tail.next = i.left
                    tail = tail.next
                if i.right:
                    tail.next = i.right
                    tail = tail.next
                i = i.next
            cur = head.next
        return ans
```
TypeScript 代码:
```TypeScript
function connect(root: Node | null): Node | null {
    let ans = root;
    if (root == null) return ans;
    let cur = root;
    while (cur != null) {
        let head = new Node(-1), tail = head;
        for (let i = cur; i != null; i = i.next) {
            if (i.left) tail = tail.next = i.left;
            if (i.right) tail = tail.next = i.right;
        }
        cur = head.next;
    }
    return ans;
};
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


Question:
给定一个非负索引 k,其中 k ≤ 33,返回杨辉三角的第 k 行。

在杨辉三角中,每个数是它左上方和右上方的数的和。

示例:
```
输入: 3
输出: [1,3,3,1]
```
进阶:
* 你可以优化你的算法到 O(k) 空间复杂度吗?

Solution:
### 动态规划

```java
class Solution {
    public List<Integer> getRow(int idx) {
        int[][] f = new int[idx + 1][idx + 1];
        f[0][0] = 1;
        for (int i = 1; i < idx + 1; i++) {
            for (int j = 0; j < i + 1; j++) {
                f[i][j] = f[i - 1][j];
                if (j - 1 >= 0) f[i][j] += f[i - 1][j - 1];
                if (f[i][j] == 0) f[i][j] = 1;
            }
        }
        List<Integer> ans = new ArrayList<>();
        for (int i = 0; i < idx + 1; i++) ans.add(f[idx][i]);
        return ans;
    }
}
```
* 时间复杂度:$O(n^2)$
* 空间复杂度:$O(n^2)$

***

### 滚动数组

滚动数组优化十分机械,直接将滚动的维度从 `i` 改造为 `i % 2` 或 `i & 1` 即可。

`i & 1` 相比于 `i % 2` 在不同架构的机器上,效率会更稳定些 ~

```java
class Solution {
    public List<Integer> getRow(int idx) {
        int[][] f = new int[2][idx + 1];
        f[0][0] = 1;
        for (int i = 1; i < idx + 1; i++) {
            for (int j = 0; j < i + 1; j++) {
                f[i & 1][j] = f[(i - 1) & 1][j];
                if (j - 1 >= 0) f[i & 1][j] += f[(i - 1) & 1][j - 1];
                if (f[i & 1][j] == 0) f[i & 1][j] = 1;
            }
        }
        List<Integer> ans = new ArrayList<>();
        for (int i = 0; i < idx + 1; i++) ans.add(f[idx & 1][i]);
        return ans;
    }
}
```
* 时间复杂度:$O(n^2)$
* 空间复杂度:$O(n)$

***

### 一维优化

只有第 `i` 行的更新只依赖于 `i - 1` 行,因此可以直接消除行的维度:

```java
class Solution {
    public List<Integer> getRow(int idx) {
        int[] f = new int[idx + 1];
        f[0] = 1;
        for (int i = 1; i < idx + 1; i++) {
            for (int j = i; j >= 0; j--) {
                if (j - 1 >= 0) f[j] += f[j - 1];
                if (f[j] == 0) f[j] = 1;
            }
        }
        List<Integer> ans = new ArrayList<>();
        for (int i = 0; i < idx + 1; i++) ans.add(f[i]);
        return ans;
    }
}
```
* 时间复杂度:$O(n^2)$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


LeetCode/1131-1140/1137. 第 N 个泰波那契数（简单）.md

1137. 第 N 个泰波那契数

Question:
泰波那契序列 $T_{n}$ 定义如下:

$T_{0} = 0$, $T_{1} = 1$, $T_{2} = 1$, 且在 $n >= 0$ 的条件下 $T_{n+3} = T_{n} + T_{n+1} + T_{n+2}$

给你整数 $n$,请返回第 $n$ 个泰波那契数 $T_n$ 的值。

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

输出:4

解释:
T_3 = 0 + 1 + 1 = 2
T_4 = 1 + 1 + 2 = 4
```
示例 2:
```
输入:n = 25

输出:1389537
```

提示:
* $0 <= n <= 37$
* 答案保证是一个 $32$ 位整数,即 $answer <= 2^{31} - 1$。

Solution:
### 动态规划(迭代)

都直接给出状态转移方程了,其实就是道模拟题。

使用三个变量,从前往后算一遍即可。

Java 代码:
```Java
class Solution {
    public int tribonacci(int n) {
        if (n == 0) return 0;
        if (n == 1 || n == 2) return 1;
        int a = 0, b = 1, c = 1;
        for (int i = 3; i <= n; i++) {
            int d = a + b + c;
            a = b; b = c; c = d;
        }
        return c;
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    int tribonacci(int n) {
        if (n == 0) return 0;
        if (n == 1 || n == 2) return 1;
        int a = 0, b = 1, c = 1;
        for (int i = 3; i <= n; i++) {
            int d = a + b + c;
            a = b; b = c; c = d;
        }
        return c;
    }
};
```
Python 代码:
```Python
class Solution:
    def tribonacci(self, n: int) -> int:
        if n == 0: return 0
        if n == 1 or n == 2: return 1
        a, b, c = 0, 1, 1
        for i in range(3, n + 1):
            d = a + b + c
            a, b, c = b, c, d
        return c
```
TypeScript 代码:
```TypeScript
function tribonacci(n: number): number {
    if (n === 0) return 0;
    if (n === 1 || n === 2) return 1;
    let a = 0, b = 1, c = 1;
    for (let i = 3; i <= n; i++) {
        const d = a + b + c;
        a = b; b = c; c = d;
    }
    return c;
};
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(1)$

---

### 动态规划(递归)

也就是记忆化搜索,创建一个 `cache` 数组用于防止重复计算。

Java 代码:
```Java
class Solution {
    int[] cache = new int[40];
    public int tribonacci(int n) {
        if (n == 0) return 0;
        if (n == 1 || n == 2) return 1;
        if (cache[n] != 0) return cache[n];
        cache[n] = tribonacci(n - 1) + tribonacci(n - 2) + tribonacci(n - 3);
        return cache[n];
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    vector<int> cache = vector<int>(40, 0);
    int tribonacci(int n) {
        if (n == 0) return 0;
        if (n == 1 || n == 2) return 1;
        if (cache[n] != 0) return cache[n];
        cache[n] = tribonacci(n - 1) + tribonacci(n - 2) + tribonacci(n - 3);
        return cache[n];
    }
};
```
Python 代码:
```Python
class Solution:
    def __init__(self):
        self.cache = [0] * 40
    def tribonacci(self, n: int) -> int:
        if n == 0: return 0
        if n == 1 or n == 2: return 1
        if self.cache[n] != 0: return self.cache[n]
        self.cache[n] = self.tribonacci(n - 1) + self.tribonacci(n - 2) + self.tribonacci(n - 3)
        return self.cache[n]
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n)$

---

### 矩阵快速幂

这还是一道「矩阵快速幂」的板子题。

首先你要对「快速幂」和「矩阵乘法」概念有所了解。

矩阵快速幂用于求解一般性问题:给定大小为 $n \times n$ 的矩阵 $M$,求答案矩阵 $M^k$,并对答案矩阵中的每位元素对 $P$ 取模。

在上述两种解法中,当我们要求解 $f[i]$ 时,需要将 $f[0]$ 到 $f[n - 1]$ 都算一遍,因此需要线性的复杂度。

对于此类的「数列递推」问题,我们可以使用「矩阵快速幂」来进行加速(比如要递归一个长度为 $1e9$ 的数列,线性复杂度会被卡)。

使用矩阵快速幂,我们只需要 $O(\log{n})$ 的复杂度。

根据题目的递推关系($i >= 3$):

$$
f(i) = f(i - 1) + f(i - 2) + f(i - 3)
$$

我们发现要求解 $f(i)$,其依赖的是 $f(i - 1)$、$f(i - 2)$ 和 $f(i - 3)$。

我们可以将其存成一个列向量:

$$
\begin{bmatrix}
f(i - 1)\\
f(i - 2)\\
f(i - 3)
\end{bmatrix}
$$

当我们整理出依赖的列向量之后,不难发现,我们想求的 $f(i)$ 所在的列向量是这样的:

$$
\begin{bmatrix}
f(i)\\
f(i - 1)\\
f(i - 2)
\end{bmatrix}
$$

利用题目给定的依赖关系,对目标矩阵元素进行展开:

$$
\begin{bmatrix}
f(i)\\
f(i - 1)\\
f(i - 2)
\end{bmatrix} = \begin{bmatrix}
f(i - 1) * 1 + f(i - 2) * 1 + f(i - 3) * 1\\
f(i - 1) * 1 + f(i - 2) * 0 + f(i - 3) * 0\\
f(i - 1) * 0 + f(i - 2) * 1 + f(i - 3) * 0
\end{bmatrix}
$$

那么根据矩阵乘法,即有:

$$
\begin{bmatrix}
f(i)\\
f(i - 1)\\
f(i - 2)
\end{bmatrix} = \begin{bmatrix}
1 &1 &1 \\
1 &0 &0 \\
0 &1 &0
\end{bmatrix} * \begin{bmatrix}
f(i - 1)\\
f(i - 2)\\
f(i - 3)
\end{bmatrix}
$$

我们令

$$
Mat = \begin{bmatrix}
1 &1 &1 \\
1 &0 &0 \\
0 &1 &0
\end{bmatrix}
$$

然后发现,利用 $Mat$ 我们也能实现数列递推(公式太难敲了,随便列两项吧):

$$
Mat * \begin{bmatrix}
f(i - 1)\\
f(i - 2)\\
f(i - 3)
\end{bmatrix} = \begin{bmatrix}
f(i)\\
f(i - 1)\\
f(i - 2)
\end{bmatrix}
$$

$$
Mat * \begin{bmatrix}
f(i )\\
f(i - 1)\\
f(i - 2)
\end{bmatrix} = \begin{bmatrix}
f(i + 1)\\
f(i)\\
f(i - 1)
\end{bmatrix}
$$

再根据矩阵运算的结合律,最终有:

$$
\begin{bmatrix}
f(n)\\
f(n - 1)\\
f(n - 2)
\end{bmatrix} = Mat^{n - 2} * \begin{bmatrix}
f(2)\\
f(1)\\
f(0)
\end{bmatrix}
$$

从而将问题转化为求解 $Mat^{n - 2}$ ,这时候可以套用「矩阵快速幂」解决方案。

Java 代码:
```Java
class Solution {
    int N = 3;
    int[][] mul(int[][] a, int[][] b) {
        int[][] c = new int[N][N];
        for (int i = 0; i < N; i++) {
            for (int j = 0; j < N; j++) {
                c[i][j] = a[i][0] * b[0][j] + a[i][1] * b[1][j] + a[i][2] * b[2][j];
            }
        }
        return c;
    }
    public int tribonacci(int n) {
        if (n == 0) return 0;
        if (n == 1 || n == 2) return 1;
        int[][] ans = new int[][]{
            {1,0,0},
            {0,1,0},
            {0,0,1}
        };
        int[][] mat = new int[][]{
            {1,1,1},
            {1,0,0},
            {0,1,0}
        };
        int k = n - 2;
        while (k != 0) {
            if ((k & 1) != 0) ans = mul(ans, mat);
            mat = mul(mat, mat);
            k >>= 1;
        }
        return ans[0][0] + ans[0][1];
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    int N = 3;
    vector<vector<long long>> mul(vector<vector<long long>>& a, vector<vector<long long>>& b) {
        vector<vector<long long>> c(N, vector<long long>(N, 0));
        for (int i = 0; i < N; i++) {
            for (int j = 0; j < N; j++) {
                c[i][j] = a[i][0] * b[0][j] + a[i][1] * b[1][j] + a[i][2] * b[2][j];
            }
        }
        return c;
    }
    int tribonacci(int n) {
        if (n == 0) return 0;
        if (n == 1 || n == 2) return 1;
        vector<vector<long long>> ans = {{1, 0, 0}, {0, 1, 0}, {0, 0, 1}};
        vector<vector<long long>> mat = {{1, 1, 1}, {1, 0, 0}, {0, 1, 0}};
        int k = n - 2;
        while (k != 0) {
            if ((k & 1) != 0) ans = mul(ans, mat);
            mat = mul(mat, mat);
            k >>= 1;
        }
        return ans[0][0] + ans[0][1];
    }
};
```
Python 代码:
```Python
class Solution:
    def __init__(self):
        self.N = 3

    def mul(self, a, b):
        c = [[0] * self.N for _ in range(self.N)]
        for i in range(self.N):
            for j in range(self.N):
                c[i][j] = a[i][0] * b[0][j] + a[i][1] * b[1][j] + a[i][2] * b[2][j]
        return c

    def tribonacci(self, n: int) -> int:
        if n == 0: return 0
        if n == 1 or n == 2: return 1
        ans = [[1, 0, 0], [0, 1, 0], [0, 0, 1]]
        mat = [[1, 1, 1], [1, 0, 0], [0, 1, 0]]
        k = n - 2
        while k != 0:
            if k & 1: ans = self.mul(ans, mat)
            mat = self.mul(mat, mat)
            k >>= 1
        return ans[0][0] + ans[0][1]
```
TypeScript 代码:
```TypeScript
function tribonacci(n: number): number {
    const N = 3;
    const mul = function(a: number[][], b: number[][]): number[][] {
        const c = Array.from({ length: N }, () => Array(N).fill(0));
        for (let i = 0; i < N; i++) {
            for (let j = 0; j < N; j++) {
                c[i][j] = a[i][0] * b[0][j] + a[i][1] * b[1][j] + a[i][2] * b[2][j];
            }
        }
        return c;
    };
    if (n === 0) return 0;
    if (n === 1 || n === 2) return 1;
    let ans = [[1, 0, 0], [0, 1, 0], [0, 0, 1]];
    let mat = [[1, 1, 1], [1, 0, 0], [0, 1, 0]];
    let k = n - 2;
    while (k !== 0) {
        if (k & 1) ans = mul(ans, mat);
        mat = mul(mat, mat);
        k >>= 1;
    }
    return ans[0][0] + ans[0][1];
};
```
* 时间复杂度:$O(\log{n})$
* 空间复杂度:$O(1)$

---

### 打表

当然,我们也可以将数据范围内的所有答案进行打表预处理,然后在询问时直接查表返回。

但对这种题目进行打表带来的收益没有平常打表题的大,因为打表内容不是作为算法必须的一个环节,而直接是作为该询问的答案,但测试样例是不会相同的,即不会有两个测试数据都是 $n = 37$。

这时候打表节省的计算量是不同测试数据之间的相同前缀计算量,例如 $n = 36$ 和 $n = 37$,其 $35$ 之前的计算量只会被计算一次。

因此直接为「解法二」的 `cache` 添加 `static` 修饰其实是更好的方式:代码更短,同时也能起到同样的节省运算量的效果。

Java 代码:
```Java
class Solution {
    static int[] cache = new int[40];
    static {
        cache[0] = 0; cache[1] = 1; cache[2] = 1;
        for (int i = 3; i < cache.length; i++) {
            cache[i] = cache[i - 1] + cache[i - 2] + cache[i - 3];
        }
    }
    public int tribonacci(int n) {
        return cache[n];
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    static vector<long long> cache;
    Solution() {
        cache.resize(40);
        cache[0] = 0; cache[1] = 1; cache[2] = 1;
        for (int i = 3; i < 40; ++i) {
            cache[i] = cache[i - 1] + cache[i - 2] + cache[i - 3];
        }
    }
    int tribonacci(int n) {
        return static_cast<int>(cache[n]);
    }
};
vector<long long> Solution::cache;
```
Python 代码:
```Python
class Solution:
    cache = [0, 1, 1] + [0] * 37
    for i in range(3, 40):
        cache[i] = cache[i - 1] + cache[i - 2] + cache[i - 3]

    def tribonacci(self, n: int) -> int:
        return self.cache[n]
```
* 时间复杂度:将打表逻辑交给 $OJ$,复杂度为 $O(C)$,$C$ 固定为 $40$。将打表逻辑放到本地进行,复杂度为 $O(1)$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


1143. 最长公共子序列

Question:
给定两个字符串 `s1` 和 `s2`,返回这两个字符串的最长公共子序列的长度。

如果不存在公共子序列,返回 $0$ 。

一个字符串的子序列是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。

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

两个字符串的公共子序列是这两个字符串所共同拥有的子序列。

示例 1:
```
输入:s1 = "abcde", s2 = "ace"

输出:3

解释:最长公共子序列是 "ace" ,它的长度为 3 。
```
示例 2:
```
输入:s1 = "abc", s2 = "abc"

输出:3

解释:最长公共子序列是 "abc" ,它的长度为 3 。
```
示例 3:
```
输入:s1 = "abc", s2 = "def"

输出:0

解释:两个字符串没有公共子序列,返回 0 。
```

提示:
* $1 <= s1.length, s2.length <= 1000$
* `s1` 和 `s2` 仅由小写英文字符组成。

Solution:
### 动态规划(空格技巧)

这是一道「最长公共子序列(LCS)」的裸题。

对于这类题的都使用如下「状态定义」即可:

**$f[i][j]$ 代表考虑 $s1$ 的前 $i$ 个字符、考虑 $s2$ 的前 $j$ 的字符,形成的最长公共子序列长度。**

当有了「状态定义」之后,基本上「转移方程」就是呼之欲出:

* `s1[i]==s2[j]` : $f[i][j]=f[i-1][j-1]+1$。代表**必然使用 $s1[i]$ 与 $s2[j]$ 时** LCS 的长度。
* `s1[i]!=s2[j]` : $f[i][j]=max(f[i-1][j], f[i][j-1])$。代表**必然不使用 $s1[i]$(但可能使用$s2[j]$)时** 和 **必然不使用 $s2[j]$(但可能使用$s1[i]$)时** LCS 的长度。

一些编码细节:

通常我会习惯性往字符串头部追加一个空格,以减少边界判断(使下标从 1 开始,并很容易构造出可滚动的「有效值」)。

Java 代码:
```Java
class Solution {
    public int longestCommonSubsequence(String s1, String s2) {
        int n = s1.length(), m = s2.length();
        s1 = " " + s1; s2 = " " + s2;
        char[] cs1 = s1.toCharArray(), cs2 = s2.toCharArray();
        int[][] f = new int[n + 1][m + 1];

        // 因为有了追加的空格,我们有了显然的初始化值(以下两种初始化方式均可)
        // for (int i = 0; i <= n; i++) Arrays.fill(f[i], 1);
        for (int i = 0; i <= n; i++) f[i][0] = 1;
        for (int j = 0; j <= m; j++) f[0][j] = 1;

        for (int i = 1; i <= n; i++) {
            for (int j = 1; j <= m; j++) {
                if (cs1[i] == cs2[j]) f[i][j] = f[i -1][j - 1] + 1;
                else f[i][j] = Math.max(f[i - 1][j], f[i][j - 1]);
            }
        }
        // 减去最开始追加的空格
        return f[n][m] - 1;
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    int longestCommonSubsequence(string s1, string s2) {
        int n = s1.size(), m = s2.size();
        s1 = " " + s1, s2 = " " + s2;
        int f[n+1][m+1];
        memset(f, 0, sizeof(f));

        for(int i = 0; i <= n; i++) f[i][0] = 1;
        for(int j = 0; j <= m; j++) f[0][j] = 1;

        for(int i = 1; i <= n; i++) {
            for(int j = 1; j <= m; j++) {
                if(s1[i] == s2[j]) f[i][j] = max(f[i-1][j-1] + 1, max(f[i-1][j], f[i][j-1]));
                else f[i][j] = max(f[i-1][j], f[i][j-1]);
            }
        }
        return f[n][m] - 1;
    }
};
```
Python 代码:
```Python
class Solution:
    def longestCommonSubsequence(self, s1: str, s2: str) -> int:
        n, m = len(s1), len(s2)
        s1, s2 = " " + s1, " " + s2
        f = [[0] * (m + 1) for _ in range(n + 1)]

        for i in range(n + 1): f[i][0] = 1
        for j in range(m + 1): f[0][j] = 1

        for i in range(1, n + 1):
            for j in range(1, m + 1):
                if s1[i] == s2[j]:
                    f[i][j] = f[i - 1][j - 1] + 1
                else:
                    f[i][j] = max(f[i - 1][j], f[i][j - 1])

        return f[n][m] - 1
```
TypeScript 代码:
```TypeScript
function longestCommonSubsequence(s1: string, s2: string): number {
    const n = s1.length, m = s2.length;
    s1 = " " + s1; s2 = " " + s2;
    const f = Array.from({ length: n + 1 }, () => Array(m + 1).fill(0));

    for (let i = 0; i <= n; i++) f[i][0] = 1;
    for (let j = 0; j <= m; j++) f[0][j] = 1;

    for (let i = 1; i <= n; i++) {
        for (let j = 1; j <= m; j++) {
            if (s1[i] == s2[j]) f[i][j] = f[i - 1][j - 1] + 1;
            else f[i][j] = Math.max(f[i - 1][j], f[i][j - 1]);
        }
    }

    return f[n][m] - 1;
};
```
* 时间复杂度:$O(n \times m)$
* 空间复杂度:$O(n \times m)$

---

### 动态规划(利用偏移)

上述「追加空格」的做法只是个人比较习惯的做法。

事实上,我们也可以通过修改「状态定义」来实现递推:

**$f[i][j]$ 代表考虑 $s1$ 的前 $i - 1$ 个字符、考虑 $s2$ 的前 $j - 1$ 的字符,形成的最长公共子序列长度。**

那么最终的 $f[n][m]$ 就是我们的答案,$f[0][0]$ 当做无效值,不处理即可。

* `s1[i-1]==s2[j-1]` : $f[i][j]=f[i-1][j-1]+1$。代表使用 $s1[i-1]$ 与 $s2[j-1]$形成最长公共子序列的长度。
* `s1[i-1]!=s2[j-1]` : $f[i][j]=max(f[i-1][j], f[i][j-1])$。代表不使用 $s1[i-1]$ 形成最长公共子序列的长度、不使用 $s2[j-1]$ 形成最长公共子序列的长度。这两种情况中的最大值。

Java 代码:
```Java
class Solution {
    public int longestCommonSubsequence(String s1, String s2) {
        int n = s1.length(), m = s2.length();
        char[] cs1 = s1.toCharArray(), cs2 = s2.toCharArray();
        int[][] f = new int[n + 1][m + 1];
        for (int i = 1; i <= n; i++) {
            for (int j = 1; j <= m; j++) {
                if (cs1[i - 1] == cs2[j - 1]) f[i][j] = f[i - 1][j - 1] + 1;
                else f[i][j] = Math.max(f[i - 1][j], f[i][j - 1]);
            }
        }
        return f[n][m];
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    int longestCommonSubsequence(string s1, string s2) {
        int m = s1.size(), n = s2.size();
        vector<vector<int>> f(m + 1,vector<int>(n + 1,0));
        for(int i = 1; i <= m; i++){
            for(int j = 1; j <= n; j++){
                if(s1[i - 1] == s2[j - 1]) f[i][j] = f[i - 1][j - 1] + 1;
                else f[i][j] = max(f[i - 1][j],f[i][j - 1]);
            }
        }
        return f[m][n];
    }
};
```
Python 代码:
```Python
class Solution:
    def longestCommonSubsequence(self, s1: str, s2: str) -> int:
        m, n = len(s1), len(s2)
        f = [[0] * (n + 1) for _ in range(m + 1)]
        for i in range(1, m + 1):
            for j in range(1, n + 1):
                if s1[i - 1] == s2[j - 1]:
                    f[i][j] = f[i - 1][j - 1] + 1
                else:
                    f[i][j] = max(f[i - 1][j],f[i][j - 1])
        return f[m][n]
```
TypeScript 代码:
```TypeScript
function longestCommonSubsequence(s1: string, s2: string): number {
    const n = s1.length, m = s2.length;
    const cs1 = s1.split(''), cs2 = s2.split('');
    const f: number[][] = Array.from({ length: n + 1 }, () => Array(m + 1).fill(0));
    for (let i = 1; i <= n; i++) {
        for (let j = 1; j <= m; j++) {
            if (cs1[i - 1] == cs2[j - 1]) f[i][j] = f[i - 1][j - 1] + 1;
            else f[i][j] = Math.max(f[i - 1][j], f[i][j - 1]);
        }
    }
    return f[n][m];
};
```
* 时间复杂度:$O(n \times m)$
* 空间复杂度:$O(n \times m)$

### 最后

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

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

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

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


1154. 一年中的第几天

Question:
给你一个字符串 `date`,按 `YYYY-MM-DD` 格式表示一个 现行公元纪年法 日期。

请你计算并返回该日期是当年的第几天。

通常情况下,我们认为 `1` 月 `1` 日是每年的第 `1` 天,`1` 月 `2` 日是每年的第 `2` 天,依此类推。每个月的天数与现行公元纪年法(格里高利历)一致。

示例 1:
```
输入:date = "2019-01-09"

输出:9
```
示例 2:
```
输入:date = "2019-02-10"

输出:41
```
示例 3:
```
输入:date = "2003-03-01"

输出:60
```
示例 4:
```
输入:date = "2004-03-01"

输出:61
```

提示:
* `date.length == 10`
* `date[4] == date[7] == '-'`,其他的 `date[i]` 都是数字
* `date` 表示的范围从 `1900` 年 `1` 月 `1` 日至 `2019` 年 `12` 月 `31` 日

Solution:
### 模拟

根据题意进行模拟即可。

由于每年月份的天数都相对固定(仅有闰年 $2$ 月份不同),因此我们可以使用 `static` 进行打表预处理。

使用 `nums` 构建一个平年的月份数组,再通过「前缀和」得到平年的每个月的天数总和。

最后将 `date` 按照 `-` 分割为年月日,分别进行计数。

注意,当年份为闰年且当前月份超过了 $2$ 月份,要追加一天。

代码:
```Java
class Solution {
    static int[] nums = new int[]{31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
    static int[] f = new int[13];
    static {
        for (int i = 1; i <= 12; i++) f[i] = f[i - 1] + nums[i - 1];
    }
    public int dayOfYear(String date) {
        String[] ss = date.split("-");
        int y = Integer.parseInt(ss[0]), m = Integer.parseInt(ss[1]), d = Integer.parseInt(ss[2]);
        boolean isLeap = (y % 4 == 0 && y % 100 != 0) || y % 400 == 0;
        int ans = m > 2 && isLeap ? f[m - 1] + 1 : f[m - 1];
        return ans + d;
    }
}
```
* 时间复杂度:$O(1)$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


LeetCode/1151-1160/1155. 掷骰子的N种方法（中等）.md

1155. 掷骰子的N种方法

Question:
这里有 d 个一样的骰子,每个骰子上都有 f 个面,分别标号为 `1,2,...,f`。

我们约定:掷骰子的得到总点数为各骰子面朝上的数字的总和。

如果需要掷出的总点数为 target,请你计算出有多少种不同的组合情况(所有的组合情况总共有 $f^d$ 种),模 $10^9 + 7$ 后返回。

示例 1:
```
输入:d = 1, f = 6, target = 3

输出:1
```
示例 2:
```
输入:d = 2, f = 6, target = 7

输出:6
```
示例 3:
```
输入:d = 2, f = 5, target = 10

输出:1
```
示例 4:
```
输入:d = 1, f = 2, target = 3

输出:0
```
示例 5:
```
输入:d = 30, f = 30, target = 500

输出:222616187
```

提示:

* 1 <= d, f <= 30
* 1 <= target <= 1000

Solution:
### 分组背包

在 [分组背包问题](https://mp.weixin.qq.com/s?__biz=MzU4NDE3MTEyMA==&mid=2247487504&idx=1&sn=9ac523ec0ac14c8634a229f8c3f919d7&chksm=fd9cbb0fcaeb32196b80a40e4408f6a7e2651167e0b9e31aa6d7c6109fbc2117340a59db12a1&token=1936267333&lang=zh_CN#rd) 中我们提到,分组背包不仅仅有「组内物品最多选择一个」的情况,还存在「组内物品必须选择一个」的情况。

对于本题,可以将每个骰子看作一个物品组,且每次 **必须** 从物品组中选择一个物品(所掷得的数值大小视作具体物品)。

这样就把问题转换为:**用 $d$ 个骰子(物品组)进行掷,掷出总和(取得的总价值)为 $t$ 的方案数。**

虽然,我们还没专门讲过「背包问题求方案数」,但基本分析与「背包问题求最大价值」并无本质区别。

我们可以套用「分组背包求最大价值」的状态定义来微调:**$f[i][j]$ 表示考虑前 $i$ 个物品组,凑成价值为 $j$ 的方案数。**

为了方便,我们令物品组的编号从 $1$ 开始,因此有显而易见的初始化条件 $f[0][0] = 1$。

代表在不考虑任何物品组的情况下,只有凑成总价值为 $0$ 的方案数为 $1$,凑成其他总价值的方案不存在。

**不失一般性考虑 $f[i][j]$ 该如何转移,也就是考虑第 $i$ 个物品组有哪些决策。**

根据题意,对于第 $i$ 个物品组而言,可能决策的方案有:

* 第 $i$ 个骰子的结果为 $1$,有 $f[i][j] = f[i - 1][j - 1]$

* 第 $i$ 个骰子的结果为 $2$,有 $f[i][j] = f[i - 1][j - 2]$

  ...

* 第 $i$ 个骰子的结果为 $m$,有 $f[i][j] = f[i - 1][j - m]$

$f[i][j]$ 则是上述所有可能方案的方案数总和,即有:

$$
f[i][j] = \sum_{k = 1}^{m}f[i - 1][j - k], j >= k
$$

#### 朴素二维

代码:
```Java
class Solution {
    int mod = (int)1e9+7;
    public int numRollsToTarget(int n, int m, int t) {
        int[][] f = new int[n + 1][t + 1];
        f[0][0] = 1;
        // 枚举物品组(每个骰子)
        for (int i = 1; i <= n; i++) {
            // 枚举背包容量(所掷得的总点数)
            for (int j = 0; j <= t; j++) {
                // 枚举决策(当前骰子所掷得的点数)
                for (int k = 1; k <= m; k++) {
                    if (j >= k) {
                        f[i][j] = (f[i][j] + f[i-1][j-k]) % mod;
                    }
                }
            }
        }
        return f[n][t];
    }
}
```
* 时间复杂度:$O(n * m * t)$
* 空间复杂度:$O(n * t)$

#### 滚动数组

根据状态转移方程,我们发现 $f[i][j]$ 明确只依赖于 $f[i - 1][x]$,且 $x < j$。

因此我们可以使用之前学过的「滚动数组」,用很机械的方式将空间从 $O(n * t)$ 优化至 $O(t)$。

需要注意的是,由于我们直接是在 $f[i][j]$ 格子的基础上进行方案数累加,因此在计算 $f[i][j]$ 记得手动置零。

代码:
```Java
class Solution {
    int mod = (int)1e9+7;
    public int numRollsToTarget(int n, int m, int t) {
        int[][] f = new int[2][t + 1];
        f[0][0] = 1;
        for (int i = 1; i <= n; i++) {
            int a = i & 1, b = (i - 1) & 1;
            for (int j = 0; j <= t; j++) {
                f[a][j] = 0; // 先手动置零
                for (int k = 1; k <= m; k++) {
                    if (j >= k) {
                        f[a][j] = (f[a][j] + f[b][j-k]) % mod;
                    }
                }
            }
        }
        return f[n&1][t];
    }
}
```
* 时间复杂度:$O(n * m * t)$
* 空间复杂度:$O(t)$

#### 一维空间优化

更进一步,利用「$f[i][j]$ 明确只依赖于 $f[i - 1][x]$,且 $x < j$ 」,我们能通过「[01 背包](https://mp.weixin.qq.com/s?__biz=MzU4NDE3MTEyMA==&mid=2247485638&idx=1&sn=d7468955c5b121664031fd5c1b5a6f10&chksm=fd9ca3d9caeb2acf2dd4e9f512b2d4dc820107489a094749ffe7cae646f0cc60de06f2d18898&token=800772286&lang=zh_CN#rd)」一维空间优化方式:**将物品维度取消,调整容量维度遍历顺序为「从大到小」。**

代码:
```Java
class Solution {
    int mod = (int)1e9+7;
    public int numRollsToTarget(int n, int m, int t) {
        int[] f = new int[t + 1];
        f[0] = 1;
        for (int i = 1; i <= n; i++) {
            for (int j = t; j >= 0; j--) {
                f[j] = 0;
                for (int k = 1; k <= m; k++) {
                    if (j >= k) {
                        f[j] = (f[j] + f[j-k]) % mod;
                    }
                }
            }
        }
        return f[t];
    }
}
```
* 时间复杂度:$O(n * m * t)$
* 空间复杂度:$O(t)$

---

### 总结

不难发现,不管是「组内物品最多选一件」还是「组内物品必须选一件」。

我们都是直接套用分组背包基本思路 *「枚举物品组-枚举容量-枚举决策」* 进行求解。

**分组背包的空间优化并不会降低时间复杂度,所以对于分组背包问题,我们可以直接写方便调试的朴素多维版本(在空间可接受的情况下),如果遇到卡空间,再通过机械的方式改为「滚动数组」形式。**

另外今天我们使用「分组背包问题求方案数」来作为「分组背包问题求最大价值」的练习题。

可以发现,两者其实并无本质区别,都是套用「背包问题求最大价值」的状态定义来微调。

更多的关于「背包问题求方案数」相关内容,在后面也会继续细讲。

### 最后

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

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

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

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


1161. 最大层内元素和

Question:
给你一个二叉树的根节点 `root`。设根节点位于二叉树的第 $1$ 层,而根节点的子节点位于第 $2$ 层,依此类推。

请返回层内元素之和 最大 的那几层(可能只有一层)的层号,并返回其中 最小 的那个。

示例 1:

```
输入:root = [1,7,0,7,-8,null,null]

输出:2

解释:
第 1 层各元素之和为 1,
第 2 层各元素之和为 7 + 0 = 7,
第 3 层各元素之和为 7 + -8 = -1,
所以我们返回第 2 层的层号,它的层内元素之和最大。
```
示例 2:
```
输入:root = [989,null,10250,98693,-89388,null,null,null,-32127]

输出:2
```

提示:
* 树中的节点数在 $[1, 10^4]$范围内
* $-10^5 <= Node.val <= 10^5$

Solution:
### 层序遍历

根据题意,使用 `BFS` 进行层序遍历即可。

每次以「层」为单位进行拓展,统计该层的元素和,维护处理过程中的最大值层数和,以及层深度。

Java 代码:
```Java
class Solution {
    public int maxLevelSum(TreeNode root) {
        Deque<TreeNode> d = new ArrayDeque<>();
        int max = -0x3f3f3f3f, depth = 1, ans = 0;
        d.addLast(root);
        while (!d.isEmpty()) {
            int sz = d.size(), cur = 0;
            while (sz-- > 0) {
                TreeNode t = d.pollFirst();
                if (t.left != null) d.addLast(t.left);
                if (t.right != null) d.addLast(t.right);
                cur += t.val;
            }
            if (cur > max) {
                max = cur; ans = depth;
            }
            depth++;
        }
        return ans;
    }
}
```
TypeScript 代码:
```TypeScript
function maxLevelSum(root: TreeNode | null): number {
    const d: TreeNode[] = new Array<TreeNode>()
    let he = 0, ta = 0
    d[ta++] = root
    let max = -0x3f3f3f3f, depth = 1, ans = 0
    while (he < ta) {
        let sz = ta - he, cur = 0
        while (sz-- > 0) {
            const t = d[he++]
            if (t.left != null) d[ta++] = t.left
            if (t.right != null) d[ta++] = t.right
            cur += t.val
        }
        if (cur > max) {
            max = cur; ans = depth
        }
        depth++
    }
    return ans
};
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


Question:
你现在手里有一份大小为 $N \times N$ 的 网格 $grid$,上面的每个 单元格 都用 $0$ 和 $1$ 标记好了。

其中 $0$ 代表海洋,$1$ 代表陆地,请你找出一个海洋单元格,这个海洋单元格到离它最近的陆地单元格的距离是最大的。

我们这里说的距离是「曼哈顿距离」:$(x_0, y_0)$ 和 $(x_1, y_1)$ 这两个单元格之间的距离是 $|x_0 - x_1| + |y_0 - y_1|$。

如果网格上只有陆地或者海洋,请返回 $-1$。

示例 1:

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

输出:2

解释:海洋单元格 (1, 1) 和所有陆地单元格之间的距离都达到最大,最大距离为 2。
```
示例 2:

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

输出:4

解释:海洋单元格 (2, 2) 和所有陆地单元格之间的距离都达到最大,最大距离为 4。
```

提示:
* 1 <= grid.length == grid[0].length <= 100
* `grid[i][j]` 不是 $0$ 就是 $1$

Solution:
### 单源 BFS

通常我们使用 BFS 求最短路,都是针对如下场景:从特定的起点出发,求解到达特定终点的最短距离。

**这是一类特殊的「单源最短路」问题:本质是在一个边权为 $1$ 的图上,求从特定「源点」出发到达特定「汇点」的最短路径。**

对于本题,如果套用「单源最短路」做法,我们需要对每个「海洋」位置做一次 BFS:求得每个「海洋」的最近陆地距离,然后在所有的距离中取 $max$ 作为答案。

单次 BFS 的最坏情况需要扫描完整个矩阵,复杂度为 $O(n^2)$。

同时,最多有 $n^2$ 个海洋区域需要做 BFS,因此这样的做法复杂度为 $O(n^4)$,并且 $O(n^4)$ 可直接取满。

PS. 数据范围为 $10^2$,理论上是一定会超时,但本题数据较弱,Java 2021/06/28 可过。

一些细节:为了方便,我们在使用哈希表记录距离时,将二维坐标 $(x, y)$ 转化为对应的一维下标 $idx = x \times n + y$ 作为 key 进行存储。

代码:
```Java
class Solution {
    int n;
    int[][] grid;
    public int maxDistance(int[][] _grid) {
        grid = _grid;
        n = grid.length;
        int ans = -1;
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < n; j++) {
                if (grid[i][j] == 0) {
                    ans = Math.max(ans, bfs(i, j));
                }
            }
        }
        return ans;
    }
    // 单次 BFS:求解海洋位置 (x,y) 最近的陆地距离
    int bfs(int x, int y) {
        int[][] dirs = new int[][]{{1,0},{-1,0},{0,1},{0,-1}};
        Deque<int[]> d = new ArrayDeque<>();
        Map<Integer, Integer> map = new HashMap<>();
        d.addLast(new int[]{x, y});
        map.put(x * n + y, 0);
        while (!d.isEmpty()) {
            int[] poll = d.pollFirst();
            int dx = poll[0], dy = poll[1];
            int step = map.get(dx * n + dy);
            if (grid[dx][dy] == 1) return step;
            for (int[] di : dirs) {
                int nx = dx + di[0], ny = dy + di[1];
                if (nx < 0 || nx >= n || ny < 0 || ny >= n) continue;
                int key = nx * n + ny;
                if (map.containsKey(key)) continue;
                d.addLast(new int[]{nx, ny});
                map.put(key, step + 1);
            }
        }
        return -1;
    }
}
```
* 时间复杂度:$O(n^4)$
* 空间复杂度:$O(n^2)$

---

### 多源 BFS

这其实还是道「多源 BFS」入门题。

**与「单源最短路」不同,「多源最短路」问题是求从「多个源点」到达「一个/多个汇点」的最短路径。**

在实现上,最核心的搜索部分,「多源 BFS」与「单源 BFS」并无区别。

**并且通过建立「虚拟源点」的方式,我们可以「多源 BFS」转换回「单源 BFS」问题。**

什么意思?

以本题为例,题面要我们求每个「海洋」区域到最近的「陆地」区域的最大值。

我们可以将「源点/起点」和「汇点/终点」进行反转:**从每个「陆地」区域出发,多个「陆地」区域每次同时向往扩散一圈,每个「海洋」区域被首次覆盖时所对应的圈数,就是「海洋」区域距离最近的「陆地」区域的距离。**

不过,这是如何与「单源 BFS」联系起来的呢?

我们可以想象存在一个「虚拟源点」,其与所有「真实源点」(陆地)存在等权的边,那么任意「海洋」区域与「最近的陆地」区域的最短路等价于与「虚拟源点」的最短路:

**实现上,我们并不需要真的将这个虚拟源点建立出来,只需要将所有的「真实源点」进行入队即可。**

这个过程相当于从队列中弹出「虚拟源点」,并把它所能到点(真实源点)进行入队,然后再进行常规的 BFS 即可。

一些细节:实现上为了方便,在进行常规 BFS 时,如果一个「海洋」区域被访问到,说明其被离它「最近的陆地」覆盖到了,修改值为最小距离。这样我们只需要考虑那些值仍然为 $0$ 的「海洋」区域即可(代表尚未被更新)。

代码:
```Java
class Solution {
    public int maxDistance(int[][] grid) {
        int n = grid.length;
        Deque<int[]> d = new ArrayDeque<>();
        Map<Integer, Integer> map = new HashMap<>();
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < n; j++) {
                if (grid[i][j] == 1) {
                    d.add(new int[]{i, j});
                    map.put(i * n + j, 0);
                }
            }
        }
        int ans = -1;
        int[][] dirs = new int[][]{{1,0},{-1,0},{0,1},{0,-1}};
        while (!d.isEmpty()) {
            int[] poll = d.poll();
            int dx = poll[0], dy = poll[1];
            int step = map.get(dx * n + dy);
            for (int[] di : dirs) {
                int nx = dx + di[0], ny = dy + di[1];
                if (nx < 0 || nx >= n || ny < 0 || ny >= n) continue;
                if (grid[nx][ny] != 0) continue;
                grid[nx][ny] = step + 1;
                d.add(new int[]{nx, ny});
                map.put(nx * n + ny, step + 1);
                ans = Math.max(ans, step + 1);
            }
        }
        return ans;
    }
}
```
* 时间复杂度:$O(n^2)$
* 空间复杂度:$O(n^2)$

---

### 总结

今天我们介绍了「多源 BFS」,通过建立「虚拟源点」,我们可以将其转化回「单源 BFS」问题。

实现上我们只需要将所有的「真实源点」进行入队,然后再进行 BFS 即可。

看起来两者区别不大,但其本质是通过源点/汇点转换,应用常规的 Flood Fill 将多次朴素 BFS 转化为一次 BFS,可以有效降低我们算法的时间复杂度。

### 最后

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

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

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

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


Question:
请你帮忙给从 $1$ 到 $n$ 的数设计排列方案,使得所有的「质数」都应该被放在「质数索引」(索引从 $1$ 开始)上;你需要返回可能的方案总数。

让我们一起来回顾一下「质数」:质数一定是大于 $1$ 的,并且不能用两个小于它的正整数的乘积来表示。

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

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

输出:12
解释:举个例子,[1,2,5,4,3] 是一个有效的排列,但 [5,2,3,4,1] 不是,因为在第二种情况里质数 5 被错误地放在索引为 1 的位置上。
```
示例 2:
```
输入:n = 100

输出:682289015
```

提示:
* $1 <= n <= 100$

Solution:
### 打表 + 二分 + 数学

根据题意,可将问题转换为求 $n$ 以内的质数个数,记为 $a$,同时可得非质数个数为 $b = n - a$。

质数的放置方案数为 $a!$,而非质数的放置方案数为 $b!$,根据「乘法原理」总的放置方案数为 $a! \times b!$。

我们可以通过「打表」的方式将 $100$ 以内的质数预处理到数组 `list` 中,对于每个 $n$ 而言,我们找到第一个满足「值小于等于 $n$」的位置,从而得知 $n$ 范围以内的质数个数。

代码:
```Java
class Solution {
    static int MOD = (int)1e9+7;
    static List<Integer> list = new ArrayList<>();
    static {
        for (int i = 2; i <= 100; i++) {
            boolean ok = true;
            for (int j = 2; j * j <= i; j++) {
                if (i % j == 0) ok = false;
            }
            if (ok) list.add(i);
        }
    }
    public int numPrimeArrangements(int n) {
        int l = 0, r = list.size() - 1;
        while (l < r) {
            int mid = l + r + 1 >> 1;
            if (list.get(mid) <= n) l = mid;
            else r = mid - 1;
        }
        int a = r + 1, b = n - a;
        long ans = 1;
        for (int i = b; i > 1; i--) ans = ans * i % MOD ;
        for (int i = a; i > 1; i--) ans = ans * i % MOD ;
        return (int)ans;
    }
}
```
* 时间复杂度:二分的复杂度为 $O(\log{C})$,其中 $C = 25$ 为 $100$ 以内的质数个数;计算方案数的复杂度为 $O(n)$。整体复杂度为 $O(n)$
* 空间复杂度:$O(C)$,其中 $C = 25$ 为 $100$ 以内的质数个数

---

### 打表 + 数学

更进一步,对于特定的 $n$ 而言,我们在预处理 $100$ 以内的质数时,已经可以确定在 $[1, n]$ 内有多少个质数,从而省掉二分操作。

使用数组 `cnts` 记录下不超过当前值范围内质数的个数,$cnts[i] = x$ 含义为在 $[1, i]$ 范围内质数数量为 $x$。

代码:
```Java
class Solution {
    static int MOD = (int)1e9+7;
    static int[] cnts = new int[110];
    static {
        List<Integer> list = new ArrayList<>();
        for (int i = 2; i <= 100; i++) {
            boolean ok = true;
            for (int j = 2; j * j <= i; j++) {
                if (i % j == 0) ok = false;
            }
            if (ok) list.add(i);
            cnts[i] = list.size();
        }
    }
    public int numPrimeArrangements(int n) {
        int a = cnts[n], b = n - a;
        long ans = 1;
        for (int i = b; i > 1; i--) ans = ans * i % MOD ;
        for (int i = a; i > 1; i--) ans = ans * i % MOD ;
        return (int)ans;
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(C)$,其中 $C = 100$

### 最后

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

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

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

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


Question:
外国友人仿照中国字谜设计了一个英文版猜字谜小游戏,请你来猜猜看吧。

字谜的迷面 puzzle 按字符串形式给出,如果一个单词 word 符合下面两个条件,那么它就可以算作谜底:
* 单词 word 中包含谜面 puzzle 的第一个字母。
* 单词 word 中的每一个字母都可以在谜面 puzzle 中找到。
  例如,如果字谜的谜面是 "abcdefg",那么可以作为谜底的单词有 "faced", "cabbage", 和 "baggage";而 "beefed"(不含字母 "a")以及 "based"(其中的 "s" 没有出现在谜面中)都不能作为谜底。

返回一个答案数组 answer,数组中的每个元素 answer[i] 是在给出的单词列表 words 中可以作为字谜迷面 puzzles[i] 所对应的谜底的单词数目。

示例:
```
输入:
words = ["aaaa","asas","able","ability","actt","actor","access"],
puzzles = ["aboveyz","abrodyz","abslute","absoryz","actresz","gaswxyz"]
输出:[1,1,3,2,4,0]
解释:
1 个单词可以作为 "aboveyz" 的谜底 : "aaaa"
1 个单词可以作为 "abrodyz" 的谜底 : "aaaa"
3 个单词可以作为 "abslute" 的谜底 : "aaaa", "asas", "able"
2 个单词可以作为 "absoryz" 的谜底 : "aaaa", "asas"
4 个单词可以作为 "actresz" 的谜底 : "aaaa", "asas", "actt", "access"
没有单词可以作为 "gaswxyz" 的谜底,因为列表中的单词都不含字母 'g'。
```

提示:
* 1 <= words.length <= $10^5$
* 4 <= words[i].length <= 50
* 1 <= puzzles.length <= $10^4$
* puzzles[i].length == 7
* words[i][j], puzzles[i][j] 都是小写英文字母。
* 每个 puzzles[i] 所包含的字符都不重复。

Solution:
### 朴素位运算解法(TLE)

根据「谜底」和「谜面」的对应条件:
* 单词 `word` 中包含谜面 `puzzle` 的第一个字母。
* 单词 `word` 中的每一个字母都可以在谜面 `puzzle` 中找到

`puzzle` 本身长度只有 7 位,而且不重复;我们可以发现对应条件与 `word` 的重复字母无关。

因此我们可以使用「二进制」数来表示每一个 `word` 和 `puzzle`:

一个长度为 26 的二进制数来表示(直接使用长度为 32 的 int 即可,使用低 26 位),假如有 `str = "abz"` 则对应了 `100...011`(共 26 位,从右往左是 a - z)。

至此我们可以已经可以得出一个朴素解法的思路了:

1. 预处理除所有的 `word` 对应的二进制数字。计算量为 50 * $10^5$,数量级为 $10^6$
2. 对每个 `puzzle` 进行条件判定(每一个 `puzzle` 都需要遍历所有的 `word` 进行检查)。计算量为 $10^5$ * $10^4$,数量级为 $10^9$

计算机单秒的计算量为 $10^7$ 左右(OJ 测评器通常在 $10^6$ ~ $10^7$ 之间),哪怕忽略常数后,我们的总运算也超过了上限,铁定超时。

代码:
```java
class Solution {
    public List<Integer> findNumOfValidWords(String[] ws, String[] ps) {
        // 预处理出所有的 word 所对应的二进制数值
        List<Integer> list = new ArrayList<>();
        for (String w : ws) list.add(getBin(w));
        // 判定每个 puzzles 有多少个谜底
        List<Integer> ans = new ArrayList<>();
        for (String p : ps) ans.add(getCnt(list, p));
        return ans;
    }
    // 判定某个 puzzles 有多少个谜底
    int getCnt(List<Integer> ws, String str) {
        int ans = 0;
        // 获取当前 puzzles 对应的二进制数字
        int t = getBin(str);
        // 当前 puzzles 的首个字符在二进制数值中的位置
        int first = str.charAt(0) - 'a';
        for (int w : ws) {
            // check 条件一:单词 word 中包含谜面 puzzle 的第一个字母
            if ((w >> first & 1) == 0) continue;
            // check 条件二:单词 word 中的每一个字母都可以在谜面 puzzle 中找到
            if ((w & t) == w) ans++;
        }
        return ans;
    }
    // 将 str 所包含的字母用二进制标识
    // 如果 str = abz 则对应的二进制为 100...011 (共 26 位,从右往左是 a - z)
    int getBin(String str) {
        int t = 0;
        char[] cs = str.toCharArray();
        for (char c : cs) {
            // 每一位字符所对应二进制数字中哪一位
            int u = c - 'a';
            // 如果当前位置为 0,代表还没记录过,则进行记录 (不重复记录)
            if ((t >> u & 1) == 0) t += 1 << u;
        }
        return t;
    }
}
```
* 时间复杂度:$O(words.length * (words[i].length + puzzles.length))$
* 空间复杂度:每个 `word` 对应了一个 int,每个 `puzzle` 对应了一个答案。复杂度为 $O(words.length + puzzles.length)$

***

### 哈希表 & 位运算解法

因此我们需要优化上述步骤 1 或者步骤 2 。显然超时的主要原因是步骤 2 计算量太多了。

一个很显眼的突破口是利用 `puzzles[i].length == 7`,同时判定条件 1 对 `puzzle` 的首字母进行了限定。

**对于一个确定的 `puzzle` 而言,我们要找它有多少个「谜底」。可以通过枚举它所有可能的「谜底」,再去 `words` 里面找每一个「谜底」出现了多少次。**

**结合题意的话,就是固定住 `puzzle` 的首位,去枚举其余后面的 6 位的所有的可能性(每一位都有保留和不保留两种选择),即枚举子集的过程。**

你可能还是无法理解,其实就是一个通过 `puzzle` 反推 `word` 的过程:

举个🌰吧,假如我们有 `puzzle` 是 `gabc`(假定现在的 `puzzle` 长度只有 4) ,那么可能的 `word` 有哪些?

1. 首先要满足条件一,也就是 `word` 必然包含首字母 `g`;
2. 然后是条件二,`word` 中的每一位都在 `puzzle` 出现过,因此可能的 `word` 包括 `g`、`ga`、`gb`、`gc`、`gab`、`gac`、`gbc`、`gabc`。

使用 1 和 0 代表 `puzzle` 每一位选择与否的话,其实就是对应了 1000、1100、1010、1001、1110、1101、1011、1111。

搞明白了这个过程之后,我们需要对 `words` 进行词频统计,我们可以使用「哈希表」记录相同含义的 `word` 出现了多少次(相同含义的意思是包含字母类型一样的 `word`,因为答案和 `word` 的重复字符无关)

这样做的复杂度/计算量是多少呢?

1. 统计所有 `word` 的词频。计算量为 50 * $10^5$,数量级为 $10^6$
2. 对应每个 `puzzle` 而言,由于其长度确定为 7,因此所有枚举所有可能「谜底」的数量不为 $2^6$=64 个,可以看做是 $O(1)$ 的,检查每个可能的「谜底」在 `words` 出现次数是通过哈希表,也是近似 $O(1)$ 的。因此在确定一个 `puzzle` 的答案时,与 `words` 的长度无关。计算量为 $10^4$,数量级为 $10^4$

计算机单秒的计算量为 $10^7$ 左右(OJ 测评器通常在 $10^6$ ~ $10^7$ 之间),因此可以过。

代码:
```java
class Solution {
    public List<Integer> findNumOfValidWords(String[] ws, String[] ps) {
        // 转用 「哈希表」来统计出所有的 word 所对应的二进制数值
        Map<Integer, Integer> map = new HashMap<>();
        for (String w : ws) {
            int t = getBin(w);
            map.put(t, map.getOrDefault(t, 0) + 1);
        }
        // 判定每个 puzzle 有多少个谜底
        List<Integer> ans = new ArrayList<>();
        for (String p : ps) ans.add(getCnt(map, p));
        return ans;
    }
    int getCnt(Map<Integer, Integer> map, String str) {
        int ans = 0;
        int m = str.length();
        char[] cs = str.toCharArray();
        // 当前 puzzle 的首个字符在二进制数值中的位置
        int first = cs[0] - 'a';
        // 枚举「保留首个字母」的所有子集
        // 即我们需要先固定 puzzle 的首位字母,然后枚举剩余的 6 位是否保留
        // 由于是二进制,每一位共有 0 和 1 两种选择,因此共有 2^6 种可能性,也就是 2^6 = 1 << (7 - 1) = 64 种
        // i 代表了所有「保留首个字母」的子集的「后六位」的二进制表示
        for (int i = 0; i < (1 << (m - 1)); i++) {
            // u 代表了当前可能的谜底。先将首字母提取出来
            int u = 1 << first;
            // 枚举「首个字母」之后的每一位
            for (int j = 1; j < m; j++) {
                // 如果当前位为 1,代表该位置要保留,将该位置的字母追加到谜底 u 中
                if (((i >> (j - 1)) & 1) != 0) u += 1 << (cs[j] - 'a');
            }
            // 查询这样的字符是否出现在 `words` 中,出现了多少次
            if (map.containsKey(u)) ans += map.get(u);
        }
        return ans;
    }
    // 将 str 所包含的字母用二进制标识
    // 如果 str = abz 则对应的二进制为 100...011 (共 26 位,从右往左是 a - z)
    int getBin(String str) {
        int t = 0;
        char[] cs = str.toCharArray();
        for (char c : cs) {
            // 每一位字符所对应二进制数字中哪一位
            int u = c - 'a';
            // 如果当前位置为 0,代表还没记录过,则进行记录 (不重复记录)
            if ((t >> u & 1) == 0) t += 1 << u;
        }
        return t;
    }
}
```
* 时间复杂度:$O(words.length * words[i].length + puzzles.length)$
* 空间复杂度:`word` 和 `puzzle` 分别具有最大长度和固定长度,使用空间主要取决于量数组的长度。复杂度为 $O(words.length + puzzles.length)$

***

### 位运算说明

a >> b & 1 代表检查 a 的第 b 位是否为 1,有两种可能性 0 或者 1

a += 1 << b 代表将 a 的第 b 位设置为 1 (当第 b 位为 0 的时候适用)

如不想写对第 b 位为 0 的前置判断,a += 1 << b 也可以改成 a |= 1 << b

*PS. 1 的二进制就是最低位为 1,其他位为 0 哦*

以上两个操作在位运算中出现频率超高,建议每位同学都加深理解。

***

### 点评

这道题解发到 LeetCode 之后,很多同学反映还是看不懂,还是不理解。

于是我重新的思考了这道题的每一个环节。

这道题之所是 Hard,是因为考察的都是违反人性”直觉”的东西:

1. 状态压缩:对一个单词出现过哪些字母,不能采用我们直观中的 map/set 进行记录,而要利用一个长度为 26 的二进制数来记录,对于某个字母需要计算在二进制数中的哪一位,如果出现过用 1 表示,没出现过用 0 表示
2. 正难则反:不能从 `words` 数组出发,去检查有哪些 `word` 符合要求;而要反过来从 `puzzle` 出发,去枚举当前 `puzzle` 所有合法的 `word`,再去确定这些合法的 `word` 在真实的 `words` 数组中出现了多少次

大家要尽量去理解这种思路的合理性,当这种思路也形成意识的时候,这种题也就不难了。

### 最后

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

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

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

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


1184. 公交站间的距离

Question:
环形公交路线上有 $n$ 个站,按次序从 $0$ 到 $n - 1$ 进行编号。我们已知每一对相邻公交站之间的距离,$distance[i]$ 表示编号为 $i$ 的车站和编号为 `(i + 1) % n` 的车站之间的距离。

环线上的公交车都可以按顺时针和逆时针的方向行驶。

返回乘客从出发点 `start` 到目的地 `destination` 之间的最短距离。

示例 1:

```
输入:distance = [1,2,3,4], start = 0, destination = 1

输出:1

解释:公交站 0 和 1 之间的距离是 1 或 9,最小值是 1。
```
示例 2:

```
输入:distance = [1,2,3,4], start = 0, destination = 2

输出:3

解释:公交站 0 和 2 之间的距离是 3 或 7,最小值是 3。
```
示例 3:

```
输入:distance = [1,2,3,4], start = 0, destination = 3

输出:4

解释:公交站 0 和 3 之间的距离是 6 或 4,最小值是 4。
```

提示:
* $1 <= n <= 10^4$
* $distance.length == n$
* $0 <= start, destination < n$
* $0 <= distance[i] <= 10^4$

Solution:
### 模拟

根据题意进行模拟即可。

用 `i` 和 `j` 分别代表往前和往后走的指针,`a` 和 `b` 分别统计两种走法的总成本。

Java 代码:
```Java
class Solution {
    public int distanceBetweenBusStops(int[] dist, int s, int t) {
        int n = dist.length, i = s, j = s, a = 0, b = 0;
        while (i != t) {
            a += dist[i];
            if (++i == n) i = 0;
        }
        while (j != t) {
            if (--j < 0) j = n - 1;
            b += dist[j];
        }
        return Math.min(a, b);
    }
}
```
TypeScript 代码:
```TypeScript
function distanceBetweenBusStops(dist: number[], s: number, t: number): number {
    let n = dist.length, i = s, j = s, a = 0, b = 0
    while (i != t) {
        a += dist[i]
        if (++i == n) i = 0
    }
    while (j != t) {
        if (--j < 0) j = n - 1
        b += dist[j]
    }
    return Math.min(a, b)
};
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


1185. 一周中的第几天

Question:
给你一个日期,请你设计一个算法来判断它是对应一周中的哪一天。

输入为三个整数:`day`、`month` 和 `year`,分别表示日、月、年。

您返回的结果必须是这几个值中的一个 `{"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"}`。

示例 1:
```
输入:day = 31, month = 8, year = 2019

输出:"Saturday"
```
示例 2:
```
输入:day = 18, month = 7, year = 1999

输出:"Sunday"
```
示例 3:
```
输入:day = 15, month = 8, year = 1993

输出:"Sunday"
```

提示:
给出的日期一定是在 `1971` 到 `2100` 年之间的有效日期。

Solution:
### 模拟

题目保证日期是在 `1971` 到 `2100` 之间,我们可以计算给定日期距离 `1970` 的最后一天(星期四)间隔了多少天,从而得知给定日期是周几。

具体的,可以先通过循环处理计算年份在 $[1971, year - 1]$ 时间段,经过了多少天(注意平年为 $365$,闰年为 $366$);然后再处理当前年 $year$ 的月份在 $[1, month - 1]$ 时间段 ,经过了多少天(注意当天年是否为闰年,特殊处理 $2$ 月份),最后计算当前月 $month$ 经过了多少天,即再增加 $day$ 天。

得到距离 `1970` 的最后一天(星期四)的天数后进行取模,即可映射回答案。

代码:
```Java
class Solution {
    static String[] ss = new String[]{"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"};
    static int[] nums = new int[]{31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
    public String dayOfTheWeek(int day, int month, int year) {
        int ans = 4;
        for (int i = 1971; i < year; i++) {
            boolean isLeap = (i % 4 == 0 && i % 100 != 0) || i % 400 == 0;
            ans += isLeap ? 366 : 365;
        }
        for (int i = 1; i < month; i++) {
            ans += nums[i - 1];
            if (i == 2 && ((year % 4 == 0 && year % 100 != 0) || year % 400 == 0)) ans++;
        }
        ans += day;
        return ss[ans % 7];
    }
}
```
* 时间复杂度:令当前时间 `2100-12-xx`,此时达到数据范围对应的计算量上界,设为 $C$。整体复杂度为 $O(C)$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


LeetCode/1181-1190/1189. “气球” 的最大数量（简单）.md

1189. “气球” 的最大数量

Question:
给你一个字符串 `text`,你需要使用 `text` 中的字母来拼凑尽可能多的单词 `"balloon"`(气球)。

字符串 `text` 中的每个字母最多只能被使用一次。请你返回最多可以拼凑出多少个单词 `"balloon"`。

示例 1:

```
输入:text = "nlaebolko"

输出:1
```
示例 2:

```
输入:text = "loonbalxballpoon"

输出:2
```
示例 3:
```
输入:text = "leetcode"

输出:0
```

提示:
* $1 <= text.length <= 10^4$
* `text` 全部由小写英文字母组成

Solution:
### 模拟

根据题意模拟即可。

先统计 `text` 中与单词 $balloon$ 相关的字符数量,由于一个单词需要消耗两个 `l` 和 `o` 字符,对其统计数量进行除 $2$ 下取整,然后所有字符的最小出现次数即是能够凑成 $balloon$ 的最大数量。

代码:
```Java
class Solution {
    public int maxNumberOfBalloons(String text) {
        int[] cnts = new int[5];
        for (int i = 0; i < text.length(); i++) {
            char c = text.charAt(i);
            if (c == 'b') cnts[0]++;
            else if (c == 'a') cnts[1]++;
            else if (c == 'l') cnts[2]++;
            else if (c == 'o') cnts[3]++;
            else if (c == 'n') cnts[4]++;
        }
        cnts[2] /= 2; cnts[3] /= 2;
        int ans = cnts[0];
        for (int i = 0; i < 5; i++) ans = Math.min(ans, cnts[i]);
        return ans;
    }
}
```
* 时间复杂度:令 $C$ 为目标字符串的字符种类数量,本题 $C = 5$,统计 `text` 的词频复杂度为 $O(n)$,计算答案复杂度为 $O(C)$。整体复杂度为 $O(n + C)$
* 空间复杂度:$O(C)$

### 最后

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

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

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

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


LeetCode/1181-1190/1190. 反转每对括号间的子串（中等）.md

1190. 反转每对括号间的子串

Question:
给出一个字符串 `s`(仅含有小写英文字母和括号)。

请你按照从括号内到外的顺序,逐层反转每对匹配括号中的字符串,并返回最终的结果。

注意,您的结果中 不应 包含任何括号。

示例 1:
```
输入:s = "(abcd)"

输出:"dcba"
```
示例 2:
```
输入:s = "(u(love)i)"

输出:"iloveu"
```
示例 3:
```
输入:s = "(ed(et(oc))el)"

输出:"leetcode"
```
示例 4:
```
输入:s = "a(bcdefghijkl(mno)p)q"

输出:"apmnolkjihgfedcbq"
```

提示:
* $0 <= s.length <= 2000$
* `s` 中只有小写英文字母和括号
* 我们确保所有括号都是成对出现的

Solution:
### 双端队列

根据题意,我们可以设计如下处理流程:

* 从前往后遍历字符串,将不是 `)` 的字符串从「尾部」放入队列中
* 当遇到 `)` 时,从队列「尾部」取出字符串,直到遇到 `(` 为止,并对取出字符串进行翻转
* 将翻转完成后字符串重新从「尾部」放入队列
* 循环上述过程,直到原字符串全部出来完成
* 从队列「头部」开始取字符,得到最终的答案

可以发现,上述过程需要用到双端队列(或者栈,使用栈的话,需要在最后一步对取出字符串再进行一次翻转)。

在 `Java` 中,双端队列可以使用自带的 `ArrayDeque`, 也可以直接使用数组进行模拟。

代码(使用 `ArrayDeque` ):
```Java
class Solution {
    public String reverseParentheses(String s) {
        int n = s.length();
        char[] cs = s.toCharArray();
        Deque<Character> d = new ArrayDeque<>();
        for (int i = 0; i < n; i++) {
            char c = cs[i];
            if (c == ')') {
                StringBuilder path = new StringBuilder();
                while (!d.isEmpty()) {
                    if (d.peekLast() != '(') {
                        path.append(d.pollLast());
                    } else {
                        d.pollLast();
                        for (int j = 0; j < path.length(); j++) d.addLast(path.charAt(j));
                        break;
                    }
                }
            } else {
                d.addLast(c);
            }
        }
        StringBuilder sb = new StringBuilder();
        while (!d.isEmpty()) sb.append(d.pollFirst());
        return sb.toString();
    }
}
```
代码(数组模拟双端队列):
```Java
class Solution {
    int N = 2010, he = 0, ta = 0;
    char[] d = new char[N], path = new char[N];
    public String reverseParentheses(String s) {
        int n = s.length();
        char[] cs = s.toCharArray();
        for (int i = 0; i < n; i++) {
            char c = cs[i];
            if (c == ')') {
                int idx = 0;
                while (he < ta) {
                    if (d[ta - 1] == '(' && --ta >= 0) {
                        for (int j = 0; j < idx; j++) d[ta++] = path[j];
                        break;
                    } else {
                        path[idx++] = d[--ta];
                    }
                }
            } else {
                d[ta++] = c;
            }
        }
        StringBuilder sb = new StringBuilder();
        while (he < ta) sb.append(d[he++]);
        return sb.toString();
    }
}
```
* 时间复杂度:每个 `(` 字符只会进出队列一次;`)` 字符串都不会进出队列,也只会被扫描一次;分析的重点在于普通字符,可以发现每个普通字符进出队列的次数取决于其右边的 `)` 的个数,最坏情况下每个字符右边全是右括号,因此复杂度可以当做 $O(n^2)$,但实际计算量必然取不满 $n^2$,将普通字符的重复弹出均摊到整个字符串处理过程,可以看作是每个字符串都被遍历常数次,复杂度为 $O(n)$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


Question:
不使用任何库函数,设计一个 跳表 。

跳表 是在 $O(log{n})$ 时间内完成增加、删除、搜索操作的数据结构。跳表相比于树堆与红黑树,其功能与性能相当,并且跳表的代码长度相较下更短,其设计思想与链表相似。

例如,一个跳表包含 `[30, 40, 50, 60, 70, 90]`,然后增加 `80`、`45` 到跳表中,以下图的方式操作:

跳表中有很多层,每一层是一个短的链表。在第一层的作用下,增加、删除和搜索操作的时间复杂度不超过 $O(n)$。跳表的每一个操作的平均时间复杂度是 $O(\log{n})$,空间复杂度是 $O(n)$。

在本题中,你的设计应该要包含这些函数:

* `bool search(int target)`: 返回 `target` 是否存在于跳表中。
* `void add(int num)`: 插入一个元素到跳表。
* `bool erase(int num)`: 在跳表中删除一个值,如果 `num` 不存在,直接返回 `false`. 如果存在多个 `num` ,删除其中任意一个即可。

注意,跳表中可能存在多个相同的值,你的代码需要处理这种情况。

示例 1:
```
输入
["Skiplist", "add", "add", "add", "search", "add", "search", "erase", "erase", "search"]
[[], [1], [2], [3], [0], [4], [1], [0], [1], [1]]

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

解释
Skiplist skiplist = new Skiplist();
skiplist.add(1);
skiplist.add(2);
skiplist.add(3);
skiplist.search(0);   // 返回 false
skiplist.add(4);
skiplist.search(1);   // 返回 true
skiplist.erase(0);    // 返回 false,0 不在跳表中
skiplist.erase(1);    // 返回 true
skiplist.search(1);   // 返回 false,1 已被擦除
```

提示:
* $0 <= num, target <= 2 \times 10^4$
* 调用 `search`, `add`,  `erase` 操作次数不大于 $5 \times 10^44$

Solution:
### 数据结构

对于单链表而言,所有的操作(增删改查)都遵循「先查找,再操作」的步骤,这导致在单链表上所有操作复杂度均为 $O(n)$(瓶颈在于查找过程)。

跳表相对于单链表,则是通过引入「多层」链表来优化查找过程,其中每层链表均是「有序」链表:

* 对于单链表的 `Node` 设计而言,我们只需存储对应的节点值 `val`,以及当前节点的下一节点的指针 `ne` 即可(`ne` 为一指针变量)

* 对于跳表来说,除了存储对应的节点值 `val` 以外,我们需要存储当前节点在「每一层」的下一节点指针 `ne`(`ne` 为指针数组)

跳表的 `level` 编号从下往上递增,最下层的链表为元素最全的有序单链表,而查找过程则是按照 `level` 从上往下进行。

操作次数的数据范围为 $n = 5 \times 10^4$,因此设计最大的 `level` 为 $\log{n}$ 即可确保复杂度,但由于操作次数 $n = 5 \times 10^4$ 不可能全是 `add` 操作,因此这里直接取 `level` 为 $10$。

同时为了简化,建立一个哨兵节点 `he`,哨兵值的值应当足够小(根据数据范围,设定为 $-1$ 即可),所有的操作(假设当前操作的传入值为 `t`),先进行统一化的查找:**查找出每一层比 `t` 严格小的最后一个节点,将其存成 `ns` 数组。即 $ns[i]$ 为 $level = i$ 层严格比 $t$ 小的最后一个节点。**

再根据不同的操作进行下一步动作:

* `search` 操作:由于最后一层必然是元素最全的单链表,因此可以直接访问 `ns[0].ne[0]` 即是所有元素中满足大于等于 `t` 的第一个元素,通过判断其值与传入值 `t` 的大小关系来决定结果;
* `add` 操作:由于最后一层必然是元素最全的单链表,因此我们「从下往上」进行插入,最底下一层必然要插入,然后以一半的概率往上传递;
* `erase` 操作:与 `add` 操作互逆,按照「从下往上」的顺序进行删除。需要注意的是,由于相同的值在跳表中可能存在多个,因此我们在「从下往上」删除过程中需要判断待删除的元素与 `ns[0].ne[0]` 是否为同一元素(即要判断地址是否相同,而不是值相同)。

Java 代码:
```Java
class Skiplist {
    int level = 10;
    class Node {
        int val;
        Node[] ne = new Node[level];
        Node (int _val) {
            val = _val;
        }
    }
    Random random = new Random();
    Node he = new Node(-1);
    void find(int t, Node[] ns) {
        Node cur = he;
        for (int i = level - 1; i >= 0; i--) {
            while (cur.ne[i] != null && cur.ne[i].val < t) cur = cur.ne[i];
            ns[i] = cur;
        }
    }
    public boolean search(int t) {
        Node[] ns = new Node[level];
        find(t, ns);
        return ns[0].ne[0] != null && ns[0].ne[0].val == t;
    }
    public void add(int t) {
        Node[] ns = new Node[level];
        find(t, ns);
        Node node = new Node(t);
        for (int i = 0; i < level; i++) {
            node.ne[i] = ns[i].ne[i];
            ns[i].ne[i] = node;
            if (random.nextInt(2) == 0) break;
        }
    }
    public boolean erase(int t) {
        Node[] ns = new Node[level];
        find(t, ns);
        Node node = ns[0].ne[0];
        if (node == null || node.val != t) return false;
        for (int i = 0; i < level && ns[i].ne[i] == node; i++) ns[i].ne[i] = ns[i].ne[i].ne[i];
        return true;
    }
}
```
TypeScript 代码:
```TypeScript
const level: number = 10
class TNode {
    val: number
    ne: TNode[] = new Array<TNode>(level)
    constructor(_val: number) {
        this.val = _val
    }
}
class Skiplist {
    he: TNode = new TNode(-1)
    find(t: number, ns: TNode[]): void {
        let cur = this.he
        for (let i = level - 1; i >= 0; i--) {
            while (cur.ne[i] != null && cur.ne[i].val < t) cur = cur.ne[i]
            ns[i] = cur
        }
    }
    search(t: number): boolean {
        let ns: TNode[] = new Array<TNode>(level)
        this.find(t, ns)
        return ns[0].ne[0] != null && ns[0].ne[0].val == t
    }
    add(t: number): void {
        let ns: TNode[] = new Array<TNode>(level)
        this.find(t, ns)
        const node = new TNode(t)
        for (let i = 0; i < level; i++) {
            node.ne[i] = ns[i].ne[i]
            ns[i].ne[i] = node
            if (Math.round(Math.random()) == 0) break
        }
    }
    erase(t: number): boolean {
        let ns: TNode[] = new Array<TNode>(level)
        this.find(t, ns)
        const node = ns[0].ne[0]
        if (node == null || node.val != t) return false
        for (let i = 0; i < level && ns[i].ne[i] == node; i++) ns[i].ne[i] = ns[i].ne[i].ne[i]
        return true
    }
}
```
* 时间复杂度:所有操作的复杂度瓶颈在于 `find` 查找,`find` 查找期望复杂度为 $O(\log{n})$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


LeetCode/1201-1210/1208. 尽可能使字符串相等（中等）.md

1208. 尽可能使字符串相等

Question:
给你两个长度相同的字符串,s 和 t。

将 s 中的第 i 个字符变到 t 中的第 i 个字符需要 |s[i] - t[i]| 的开销(开销可能为 0),也就是两个字符的 ASCII 码值的差的绝对值。

用于变更字符串的最大预算是 maxCost。在转化字符串时,总开销应当小于等于该预算,这也意味着字符串的转化可能是不完全的。

如果你可以将 s 的子字符串转化为它在 t 中对应的子字符串,则返回可以转化的最大长度。

如果 s 中没有子字符串可以转化成 t 中对应的子字符串,则返回 0。

示例 1:
```
输入:s = "abcd", t = "bcdf", maxCost = 3

输出:3

解释:s 中的 "abc" 可以变为 "bcd"。开销为 3,所以最大长度为 3。
```
示例 2:
```
输入:s = "abcd", t = "cdef", maxCost = 3

输出:1

解释:s 中的任一字符要想变成 t 中对应的字符,其开销都是 2。因此,最大长度为 1。
```
示例 3:
```
输入:s = "abcd", t = "acde", maxCost = 0

输出:1

解释:a -> a, cost = 0,字符串未发生变化,所以最大长度为 1。
```

提示:
* 1 <= s.length, t.length <= $10^5$
* 0 <= maxCost <= $10^6$
* s 和 t 都只含小写英文字母。

Solution:
### 前缀和 + 二分 + 滑动窗口

给定了长度相同的 `s` 和 `t`,那么对于每一位而言,修改的成本都是相互独立而确定的。

我们可以先预处理出修改成本的前缀和数组 `sum`。

当有了前缀和数组之后,对于任意区间 `[i,j]` 的修改成本,便可以通过 `sum[j] - sum[i - 1]` 得出。

那么接下来我们只需要找出成本不超过 `maxCost` 的最大长度区间,这个长度区间其实就是滑动窗口长度,滑动窗口长度的范围为 `[1, n]` (n 为字符串的长度)。

通过枚举来找答案可以吗?

我们可以通过数据范围大概分析一下哈,共有 `n` 个滑动窗口长度要枚举,复杂度为 $O(n)$,对于每个滑动窗口长度,需要对整个前缀和数组进行滑动检查,复杂度为 $O(n)$。也就是整体复杂度是 $O(n^2)$ 的。

数据范围是 $10^5$,那么单个样本的计算量是 $10^{10}$,计算机单秒肯定算不完,会超时 ~

所以我们直接放弃通过枚举的朴素做法。

那么如何优化呢?其实有了对于朴素解法的分析之后,无非就是两个方向:

1. 优化第一个 $O(n)$:减少需要枚举的滑动窗口长度
2. 优化第二个 $O(n)$:实现不完全滑动前缀和数组,也能确定滑动窗口长度是否合法

事实上第 2 点是无法实现的,我们只能「减少需要枚举的滑动窗口长度」。

一个 $O(n)$ 的操作往下优化,通常就是优化成 $O(\log{n})$,$O(\log{n})$ 基本上我们可以先猜一个「二分」查找。

然后我们再来分析是否可以二分:假设我们有满足要求的长度 `ans`,那么在以 `ans` 为分割点的数轴上(数轴的范围是滑动窗口长度的范围:`[1, n]`):

1. 所有满足 `<= ans` 的点的修改成本必然满足 `<= maxCost`
2. 所有满足 `> ans` 的点的修改成本必然满足 `> maxCost` (否则 `ans` 就不会是答案)

因此 `ans` 在数轴 `[1, n]` 上具有二段性,我们可以使用「二分」找 `ans`。得证「二分」的合理性。

**可见二分的本质是二段性,而非单调性。**

##### 编码细节:

1. 为了方便的预处理前缀和和减少边界处理,我会往字符串头部添加一个空格,使之后的数组下标从 1 开始
2. 二分出来滑动窗口长度,需要在返回时再次检查,因为可能没有符合条件的有效滑动窗口长度

代码:
```Java
class Solution {
    public int equalSubstring(String ss, String tt, int max) {
        int n = ss.length();
        ss = " " + ss;
        tt = " " + tt;
        char[] s = ss.toCharArray();
        char[] t = tt.toCharArray();
        int[] sum = new int[n + 1];
        for (int i = 1; i <= n; i++) sum[i] = sum[i - 1] + Math.abs(s[i] - t[i]);
        int l = 1, r = n;
        while (l < r) {
            int mid = l + r + 1 >> 1;
            if (check(sum, mid, max)) {
                l = mid;
            } else {
                r = mid - 1;
            }
        }
        return check(sum, r, max) ? r : 0;
    }
    boolean check(int[] nums, int mid, int max) {
        for (int i = mid; i < nums.length; i++) {
            int tot = nums[i] - nums[i - mid];
            if (tot <= max) return true;
        }
        return false;
    }
}
```
* 时间复杂度:预处理出前缀和的复杂度为 $O(n)$;二分出「滑动窗口长度」的复杂度为 $O(\log{n})$,对于每个窗口长度,需要扫描一遍数组进行检查,复杂度为 $O(n)$,因此二分的复杂度为 $O(n\log{n})$。整体复杂度为 $O(n\log{n})$
* 空间复杂度:使用了前缀和数组。复杂度为 $O(n)$

### 最后

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

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

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

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


LeetCode/1201-1210/1210. 穿过迷宫的最少移动次数（困难）.md

1210. 穿过迷宫的最少移动次数

Question:
你还记得那条风靡全球的贪吃蛇吗?

我们在一个 `n*n` 的网格上构建了新的迷宫地图,蛇的长度为 `2`,也就是说它会占去两个单元格。蛇会从左上角(`(0, 0)` 和 `(0, 1)`)开始移动。我们用 `0` 表示空单元格,用 `1` 表示障碍物。

蛇需要移动到迷宫的右下角(`(n-1, n-2)` 和 `(n-1, n-1)`)。

每次移动,蛇可以这样走:

* 如果没有障碍,则向右移动一个单元格。并仍然保持身体的水平/竖直状态。
* 如果没有障碍,则向下移动一个单元格。并仍然保持身体的水平/竖直状态。
* 如果它处于水平状态并且其下面的两个单元都是空的,就顺时针旋转 `90` 度。蛇从(`(r, c)`、`(r, c+1)`)移动到 (`(r, c)`、`(r+1, c)`)。

* 如果它处于竖直状态并且其右面的两个单元都是空的,就逆时针旋转 `90` 度。蛇从(`(r, c)`、`(r+1, c)`)移动到(`(r, c)`、`(r, c+1)`)。

返回蛇抵达目的地所需的最少移动次数。

如果无法到达目的地,请返回 `-1`。

示例 1:

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

输出:11

解释:
一种可能的解决方案是 [右, 右, 顺时针旋转, 右, 下, 下, 下, 下, 逆时针旋转, 右, 下]。
```
示例 2:
```
输入:grid = [[0,0,1,1,1,1],
               [0,0,0,0,1,1],
               [1,1,0,0,0,1],
               [1,1,1,0,0,1],
               [1,1,1,0,0,1],
               [1,1,1,0,0,0]]

输出:9
```

提示:
* $2 <= n <= 100$
* $0 <= grid[i][j] <= 1$
* 蛇保证从空单元格开始出发。

Solution:
### BFS

题目要我们求从特定起点到特定终点的最少步数,由于我们蛇的长度固定为 $2$,因此我们可用三元组 $(x, y, cd)$ 来代表蛇的实际位置。其中 $(x, y)$ 代表蛇尾位置,$cd$ 代表当前蛇的方向状态,$0$ 代表水平状态,$1$ 代表竖直状态。

蛇尾加上方向状态可确定其蛇头位置 :`tx = cd == 0 ? nx : nx + 1`、`ty = cd == 0 ? ny + 1 : ny`。

对四种移动规则所导致三元组变化进行分情况讨论:

1. 往右移动:对于蛇尾而言,只有维度 $y$ 进行加一,其余维度不变。三元组变化总结为 $(0, 1, 0)$
2. 往下移动:对于蛇尾而言,只有维度 $x$ 进行加一,其余维度不变。三元组变化总结为 $(1, 0, 0)$
3. 旋转:对于蛇尾,只有 $cd$ 维度对进行翻转,其余维度不变。三元组变化总结定为 $(0, 0, 1)$

综上,所有移动规则可总结为 `int[][] dirs = new int[][]{{1,0,0},{0,1,0},{0,0,1}}`。

在进行 `BFS` 时,通过遍历 `dirs` 来得到新的三元组:原位置 `(x, y, cd)` 转换到新位置 `(x + dir[0], y + dir[1], cd ^ dir[2])`。

在得到新蛇尾位置 $(nx, ny)$ 之后,计算新蛇头的位置 $(tx, ty)$。需要确保整条蛇没有越界,没有碰到障碍物,并且旋转转移时,额外检查 $(x + 1, y + 1)$ 位置是否合法。

Java 代码:
```Java
class Solution {
    int[][] dirs = new int[][]{{1,0,0},{0,1,0},{0,0,1}};
    public int minimumMoves(int[][] g) {
        int n = g.length;
        Deque<int[]> d = new ArrayDeque<>();
        d.addLast(new int[]{0,0,0,0});
        boolean[][][] vis = new boolean[n][n][2];
        vis[0][0][0] = true;
        while (!d.isEmpty()) {
            int[] info = d.pollFirst();
            int x = info[0], y = info[1], cd = info[2], step = info[3];
            for (int[] dir : dirs) {
                int nx = x + dir[0], ny = y + dir[1], nd = cd ^ dir[2]; // 新蛇尾位置和方向
                int tx = nd == 0 ? nx : nx + 1, ty = nd == 0 ? ny + 1 : ny; // 新蛇头
                if (nx >= n || ny >= n || tx >= n || ty >= n) continue; // 整条蛇不越界
                if (g[nx][ny] == 1 || g[tx][ty] == 1) continue; // 没有触及障碍物
                if (vis[nx][ny][nd]) continue;
                if (cd != nd && g[x + 1][y + 1] == 1) continue; // 旋转时,额外检查多一个位置
                if (nx == n - 1 && ny == n - 2 && nd == 0) return step + 1;
                d.addLast(new int[]{nx, ny, nd, step + 1});
                vis[nx][ny][nd] = true;
            }
        }
        return -1;
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    int minimumMoves(vector<vector<int>>& g) {
        vector<vector<int>> dirs = {{1,0,0}, {0,1,0}, {0,0,1}};
        int n = g.size();
        queue<vector<int>> d;
        d.push({0, 0, 0, 0});
        vector<vector<vector<bool>>> vis(n, vector<vector<bool>>(n, vector<bool>(2, false)));
        vis[0][0][0] = true;
        while (!d.empty()) {
            vector<int> info = d.front();
            d.pop();
            int x = info[0], y = info[1], cd = info[2], step = info[3];
            for (vector<int>& dir : dirs) {
                int nx = x + dir[0], ny = y + dir[1], nd = cd ^ dir[2];
                int tx = nd == 0 ? nx : nx + 1, ty = nd == 0 ? ny + 1 : ny;
                if (nx >= n || ny >= n || tx >= n || ty >= n) continue;
                if (g[nx][ny] == 1 || g[tx][ty] == 1) continue;
                if (vis[nx][ny][nd]) continue;
                if (cd != nd && g[x + 1][y + 1] == 1) continue;
                if (nx == n - 1 && ny == n - 2 && nd == 0) return step + 1;
                d.push({nx, ny, nd, step + 1});
                vis[nx][ny][nd] = true;
            }
        }
        return -1;
    }
};
```
Python 代码:
```Python
class Solution:
    def minimumMoves(self, g: List[List[int]]) -> int:
        dirs = [(1, 0, 0), (0, 1, 0), (0, 0, 1)]
        n = len(g)
        d = deque([(0,0,0,0)])
        vis = [[[0]*2 for _ in range(n)] for _ in range(n)]
        vis[0][0][0] = 1
        while d:
            x, y, cd, step = d.popleft()
            for dir in dirs:
                nx, ny, nd = x + dir[0], y + dir[1], cd ^ dir[2]
                tx, ty = nx + (nd == 1), ny + (nd == 0)
                if nx >= n or ny >= n or tx >= n or ty >= n: continue
                if g[nx][ny] == 1 or g[tx][ty] == 1: continue
                if vis[nx][ny][nd]: continue
                if cd != nd and g[x + 1][y + 1] == 1: continue
                if nx == n - 1 and ny == n - 2 and nd == 0: return step + 1
                d.append((nx, ny, nd, step + 1))
                vis[nx][ny][nd] = 1
        return -1
```
TypeScript 代码:
```TypeScript
function minimumMoves(g: number[][]): number {
    const n = g.length;
    const d: [number, number, number, number][] = [[0,0,0,0]];
    const vis: boolean[][][] = Array.from({ length: n }, () => Array.from({ length: n }, () => [false, false]));
    vis[0][0][0] = true;
    const dirs: [number, number, number][] = [[1,0,0], [0,1,0], [0,0,1]];
    while (d.length > 0) {
        const [x, y, cd, step] = d.shift()!;
        for (const dir of dirs) {
            const nx = x + dir[0], ny = y + dir[1], nd = cd ^ dir[2];
            const tx = nd === 0 ? nx : nx + 1, ty = nd === 0 ? ny + 1 : ny;
            if (nx >= n || ny >= n || tx >= n || ty >= n) continue;
            if (g[nx][ny] === 1 || g[tx][ty] === 1) continue
            if (vis[nx][ny][nd]) continue;
            if (cd !== nd && g[x + 1][y + 1] === 1) continue;
            if (nx === n - 1 && ny === n - 2 && nd === 0) return step + 1;
            d.push([nx, ny, nd, step + 1]);
            vis[nx][ny][nd] = true;
        }
    }
    return -1;
};
```
* 时间复杂度:$O(n^2)$
* 空间复杂度:$O(n^2 \times C)$,其中 $C = 2$ 代表蛇可变状态方向

### 最后

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

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

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

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


124. 二叉树中的最大路径和

Question:
二叉树中的 路径 被定义为一条节点序列,序列中每对相邻节点之间都存在一条边。同一个节点在一条路径序列中 至多出现一次 。该路径 至少包含一个 节点,且不一定经过根节点。

路径和 是路径中各节点值的总和。

给你一个二叉树的根节点 `root`,返回其 最大路径和 。

示例 1:

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

输出:6

解释:最优路径是 2 -> 1 -> 3 ,路径和为 2 + 1 + 3 = 6
```
示例 2:

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

输出:42

解释:最优路径是 15 -> 20 -> 7 ,路径和为 15 + 20 + 7 = 42
```

提示:
* 树中节点数目范围是 $[1, 3 \times 10^4]$
* $-1000 <= Node.val <= 1000$

Solution:
### 递归

一个简单的做法是直接递归来做:设计 `DFS`  函数,传入当前节点 `cur`,返回以该节点“往下”延伸所能取得的最大路径和。

即在 **仅使用当前节点**、**使用当前节点和左子树路径** 和 **使用当前节点和右子树路径** 三者中取最大值进行返回。

同时在 `DFS`  过程中,我们计算「以当前节点 `cur` 为路径最高点」时的最大路径和,并用此来更新全局变量 `ans`。

具体的,在以节点 `cur` 为路径最高点时的最大路径和,首先包含当前节点本身(`cur.val`),以及可选的左右节点路径和(`dfs(cur.left)` 和 `dfs(cur.right)`)。当左右节点路径和不为负数时,说明能够对当前路径起到正向贡献作用,将其添加到路径中。

Java 代码:
```Java
class Solution {
    int ans = Integer.MIN_VALUE;
    public int maxPathSum(TreeNode root) {
        dfs(root);
        return ans;
    }
    int dfs(TreeNode root) {
        if (root == null) return 0;
        int left = dfs(root.left), right = dfs(root.right);
        int t = root.val;
        if (left >= 0) t += left;
        if (right >= 0) t += right;
        ans = Math.max(ans, t);
        return Math.max(root.val, Math.max(left, right) + root.val);
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    int ans = INT_MIN;
    int maxPathSum(TreeNode* root) {
        dfs(root);
        return ans;
    }
    int dfs(TreeNode* root) {
        if (root == nullptr) return 0;
        int left = dfs(root->left), right = dfs(root->right);
        int t = root->val;
        if (left >= 0) t += left;
        if (right >= 0) t += right;
        ans = max(ans, t);
        return max(root->val, max(left, right) + root->val);
    }
};
```
Python 代码:
```Python
class Solution:
    def maxPathSum(self, root: Optional[TreeNode]) -> int:
        ans = float('-inf')
        def dfs(root):
            nonlocal ans
            if not root: return 0
            l, r = dfs(root.left), dfs(root.right)
            t = root.val
            if l >= 0: t += l
            if r >= 0: t += r
            ans = max(ans, t)
            return max(root.val, max(l, r) + root.val)
        dfs(root)
        return ans
```
TypeScript 代码:
```TypeScript
function maxPathSum(root: TreeNode | null): number {
    let ans = -0x3f3f3f3f;
    const dfs = function(root: TreeNode | null): number {
        if (root == null) return 0;
        const l = dfs(root.left), r = dfs(root.right);
        let t = root.val;
        if (l >= 0) t += l;
        if (r >= 0) t += r;
        ans = Math.max(ans, t);
        return Math.max(root.val, Math.max(l, r) + root.val);
    };
    dfs(root);
    return ans;
};
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n)$

---

### 树形 DP

另一个做法是使用「树形 DP」思路来做。

将某个节点所在最佳路径根据方向进行拆分:**往左子节点进行延伸**、**往右子节点进行延伸** 和 **往父节点进行延伸**。

若能分别求解出三个方向的最大路径和,从中选择路径和最大(路径和非零)的两个方向进行累加,便是当前节点的最大路径和。

具体的,创建哈希表记录某个节点三个方向的路径和情况:节点对象作为哈希表的键,三个方向路径和情况作为值。

即键值对格式:`节点对象: [父节点方向最大路径和, 左子节点方向最大路径和, 右子节点方向最大路径和]`。

然后是常规的树形 DP 求解思路:第一次 `DFS`  先求解两个“方向往下”的最大路径和。第二次 `DFS`  根据 **首次 `DFS` 已算好的“方向往下”的路径和情况** 以及 **本轮 `DFS`  已递归处理好的当前节点“方向往上”的路径和情况**,来求解 当前节点子节点 的“方向往上”路径和情况。

> 对「树形 DP」接触不深的同学,可以看 [前置 🧀](https://leetcode-cn.com/problems/minimum-height-trees/solution/by-ac_oier-7xio/)

Java 代码:
```Java
class Solution {
    Map<TreeNode, int[]> map = new HashMap<>(); // 节点: [父, 左, 右]
    public int maxPathSum(TreeNode root) {
        dfs1(root, null);
        dfs2(root, null);
        int ans = Integer.MIN_VALUE;
        for (TreeNode cur : map.keySet()) {
            int t = cur.val;
            int[] info = map.get(cur);
            Arrays.sort(info);
            if (info[2] >= 0) t += info[2];
            if (info[1] >= 0) t += info[1];
            ans = Math.max(ans, t);
        }
        return ans;
    }
    int dfs1(TreeNode cur, TreeNode fa) {
        if (cur == null) return 0;
        int left = dfs1(cur.left, cur), right = dfs1(cur.right, cur);
        int[] info = map.getOrDefault(cur, new int[3]);
        info[1] = left; info[2] = right;
        map.put(cur, info);
        return Math.max(cur.val, cur.val + Math.max(left, right));
    }
    void dfs2(TreeNode cur, TreeNode fa) {
        if (cur == null) return ;
        int[] curInfo = map.get(cur);
        if (cur.left != null) {
            int[] leftInfo = map.get(cur.left);
            leftInfo[0] = Math.max(cur.val, cur.val + Math.max(curInfo[0], curInfo[2]));
        }
        if (cur.right != null) {
            int[] rightInfo = map.get(cur.right);
            rightInfo[0] = Math.max(cur.val, cur.val + Math.max(curInfo[0], curInfo[1]));
        }
        dfs2(cur.left, cur);
        dfs2(cur.right, cur);
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


Question:
字典 `wordList` 中从单词 `beginWord `和 `endWord` 的 转换序列 是一个按下述规格形成的序列:

* 序列中第一个单词是 `beginWord` 。
* 序列中最后一个单词是 `endWord` 。
* 每次转换只能改变一个字母。
* 转换过程中的中间单词必须是字典 `wordList` 中的单词。

给你两个单词 `beginWord `和 `endWord `和一个字典 `wordList `,找到从 `beginWord `到 `endWord `的 最短转换序列 中的 单词数目 。如果不存在这样的转换序列,返回 $0$。

示例 1:
```
输入:beginWord = "hit", endWord = "cog", wordList = ["hot","dot","dog","lot","log","cog"]

输出:5

解释:一个最短转换序列是 "hit" -> "hot" -> "dot" -> "dog" -> "cog", 返回它的长度 5。
```
示例 2:
```
输入:beginWord = "hit", endWord = "cog", wordList = ["hot","dot","dog","lot","log"]

输出:0

解释:endWord "cog" 不在字典中,所以无法进行转换。
```

提示:
* $1 <= beginWord.length <= 10$
* $endWord.length == beginWord.length$
* $1 <= wordList.length <= 5000$
* $wordList[i].length == beginWord.length$
* `beginWord`、`endWord` 和 `wordList[i]` 由小写英文字母组成
* `beginWord != endWord`
* `wordList` 中的所有字符串 互不相同

Solution:
### 基本分析

根据题意,每次只能替换一个字符,且每次产生的新单词必须在 `wordList` 出现过。

一个朴素的实现方法是,使用 BFS 的方式求解。

从 `beginWord` 出发,枚举所有替换一个字符的方案,如果方案存在于 `wordList` 中,则加入队列中,这样队列中就存在所有替换次数为 $1$ 的单词。然后从队列中取出元素,继续这个过程,直到遇到 `endWord` 或者队列为空为止。

同时为了「防止重复枚举到某个中间结果」和「记录每个中间结果是经过多少次转换而来」,我们需要建立一个「哈希表」进行记录。

哈希表的 KV 形式为 `{ 单词 : 由多少次转换得到 }`。

当枚举到新单词 `str` 时,需要先检查是否已经存在与「哈希表」中,如果不存在则更新「哈希表」并将新单词放入队列中。

**这样的做法可以确保「枚举到所有由 `beginWord` 到 `endWord` 的转换路径」,并且由 `beginWord` 到 `endWord` 的「最短转换路径」必然会最先被枚举到。**

---

### 双向 BFS

经过分析,BFS 确实可以做,但本题的数据范围较大:`1 <= beginWord.length <= 10`

朴素的 BFS 可能会带来「搜索空间爆炸」的情况。

想象一下,如果我们的 `wordList` 足够丰富(包含了所有单词),对于一个长度为 $10$ 的 `beginWord` 替换一次字符可以产生 $10 \times 25$ 个新单词(每个替换点可以替换另外 $25$ 个小写字母),第一层就会产生 $250$ 个单词;第二层会产生超过 $6 \times 10^4$ 个新单词 ...

**随着层数的加深,这个数字的增速越快,这就是「搜索空间爆炸」问题。**

**在朴素的 BFS 实现中,空间的瓶颈主要取决于搜索空间中的最大宽度。**

那么有没有办法让我们不使用这么宽的搜索空间,同时又能保证搜索到目标结果呢?

「双向 BFS」 可以很好的解决这个问题:

**同时从两个方向开始搜索,一旦搜索到相同的值,意味着找到了一条联通起点和终点的最短路径。**

「双向 BFS」的基本实现思路如下:

1. 创建「两个队列」分别用于两个方向的搜索;
2. 创建「两个哈希表」用于「解决相同节点重复搜索」和「记录转换次数」;
3. 为了尽可能让两个搜索方向“平均”,每次从队列中取值进行扩展时,先判断哪个队列容量较少;
4. 如果在搜索过程中「搜索到对方搜索过的节点」,说明找到了最短路径。

「双向 BFS」基本思路对应的伪代码大致如下:

```Java
d1、d2 为两个方向的队列
m1、m2 为两个方向的哈希表,记录每个节点距离起点的

// 只有两个队列都不空,才有必要继续往下搜索
// 如果其中一个队列空了,说明从某个方向搜到底都搜不到该方向的目标节点
while(!d1.isEmpty() && !d2.isEmpty()) {
    if (d1.size() < d2.size()) {
        update(d1, m1, m2);
    } else {
        update(d2, m2, m1);
    }
}

// update 为将当前队列 d 中包含的元素取出,进行「一次完整扩展」的逻辑(按层拓展)
void update(Deque d, Map cur, Map other) {}
```

回到本题,我们看看如何使用「双向 BFS」进行求解。

估计不少同学是第一次接触「双向 BFS」,因此这次我写了大量注释。

建议大家带着对「双向 BFS」的基本理解去阅读。

Java 代码:
```Java
class Solution {
    String s, e;
    Set<String> set = new HashSet<>();
    public int ladderLength(String beginWord, String endWord, List<String> wordList) {
        s = beginWord; e = endWord;
        // 将所有 word 存入 set,如果目标单词不在 set 中,说明无解
        for (String w : wordList) set.add(w);
        if (!set.contains(e)) return 0;
        int ans = bfs();
        return ans == -1 ? 0 : ans + 1;
    }

    int bfs() {
        // d1 代表从起点 beginWord 开始搜索(正向)
        // d2 代表从结尾 endWord 开始搜索(反向)
        Deque<String> d1 = new ArrayDeque<>(), d2 = new ArrayDeque();

        /*
         * m1 和 m2 分别记录两个方向出现的单词是经过多少次转换而来
         * e.g.
         * m1 = {"abc":1} 代表 abc 由 beginWord 替换 1 次字符而来
         * m2 = {"xyz":3} 代表 xyz 由 endWord 替换 3 次字符而来
         */
        Map<String, Integer> m1 = new HashMap<>(), m2 = new HashMap<>();
        d1.add(s);
        m1.put(s, 0);
        d2.add(e);
        m2.put(e, 0);

        /*
         * 只有两个队列都不空,才有必要继续往下搜索
         * 如果其中一个队列空了,说明从某个方向搜到底都搜不到该方向的目标节点
         * e.g.
         * 例如,如果 d1 为空了,说明从 beginWord 搜索到底都搜索不到 endWord,反向搜索也没必要进行了
         */
        while (!d1.isEmpty() && !d2.isEmpty()) {
            int t = -1;
            // 为了让两个方向的搜索尽可能平均,优先拓展队列内元素少的方向
            if (d1.size() <= d2.size()) {
                t = update(d1, m1, m2);
            } else {
                t = update(d2, m2, m1);
            }
            if (t != -1) return t;
        }
        return -1;
    }

    // update 代表从 deque 中取出一个单词进行扩展,
    // cur 为当前方向的距离字典;other 为另外一个方向的距离字典
    int update(Deque<String> deque, Map<String, Integer> cur, Map<String, Integer> other) {
        int m = deque.size();
        while (m-- > 0) {
            // 获取当前需要扩展的原字符串
            String poll = deque.pollFirst();
            int n = poll.length();

            // 枚举替换原字符串的哪个字符 i
            for (int i = 0; i < n; i++) {
                // 枚举将 i 替换成哪个小写字母
                for (int j = 0; j < 26; j++) {
                    // 替换后的字符串
                    String sub = poll.substring(0, i) + String.valueOf((char)('a' + j)) + poll.substring(i + 1);
                    if (set.contains(sub)) {
                        // 如果该字符串在「当前方向」被记录过(拓展过),跳过即可
                        if (cur.containsKey(sub) && cur.get(sub) <= cur.get(poll) + 1) continue;

                        // 如果该字符串在「另一方向」出现过,说明找到了联通两个方向的最短路
                        if (other.containsKey(sub)) {
                            return cur.get(poll) + 1 + other.get(sub);
                        } else {
                            // 否则加入 deque 队列
                            deque.addLast(sub);
                            cur.put(sub, cur.get(poll) + 1);
                        }
                    }
                }
            }
        }
        return -1;
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    string s, e;
    unordered_set<string> wordSet;
    int ladderLength(string beginWord, string endWord, vector<string>& wordList) {
        s = beginWord; e = endWord;
        for (string w : wordList) wordSet.insert(w);
        if (wordSet.find(e) == wordSet.end()) return 0;
        int ans = bfs();
        return ans == -1 ? 0 : ans + 1;
    }
    int bfs() {
        deque<string> dq1, dq2;
        unordered_map<string, int> m1, m2;
        dq1.push_back(s);
        m1[s] = 0;
        dq2.push_back(e);
        m2[e] = 0;
        while (!dq1.empty() && !dq2.empty()) {
            int t = -1;
            if (dq1.size() <= dq2.size()) t = update(dq1, m1, m2);
            else t = update(dq2, m2, m1);
            if (t != -1) return t;
        }
        return -1;
    }
    int update(deque<string>& dq, unordered_map<string, int>& cur, unordered_map<string, int>& other) {
        int m = dq.size();
        while (m-- > 0) {
            string poll = dq.front();
            dq.pop_front();
            int n = poll.length();
            for (int i = 0; i < n; i++) {
                for (int j = 0; j < 26; j++) {
                    string sub = poll.substr(0, i) + static_cast<char>('a' + j) + poll.substr(i + 1);
                    if (wordSet.find(sub) != wordSet.end()) {
                        if (cur.find(sub) != cur.end() && cur[sub] <= cur[poll] + 1) continue;
                        if (other.find(sub) != other.end()) {
                            return cur[poll] + 1 + other[sub];
                        } else {
                            dq.push_back(sub);
                            cur[sub] = cur[poll] + 1;
                        }
                    }
                }
            }
        }
        return -1;
    }
};
```
Python 代码:
```Python
class Solution:
    def ladderLength(self, beginWord: str, endWord: str, wordList: List[str]) -> int:
        def bfs():
            dq1, dq2 = deque(), deque()
            m1, m2 = {}, {}
            dq1.append(s)
            m1[s] = 0
            dq2.append(e)
            m2[e] = 0
            while dq1 and dq2:
                t = update(dq1, m1, m2) if len(dq1) <= len(dq2) else update(dq2, m2, m1)
                if t != -1: return t
            return -1

        def update(dq, cur, other):
            m = len(dq)
            for _ in range(m):
                poll = dq.popleft()
                n = len(poll)
                for i in range(n):
                    for j in range(26):
                        sub = poll[:i] + chr(ord('a') + j) + poll[i+1:]
                        if sub in wordSet:
                            if sub in cur and cur[sub] <= cur[poll] + 1: continue
                            if sub in other:
                                return cur[poll] + 1 + other[sub]
                            else:
                                dq.append(sub)
                                cur[sub] = cur[poll] + 1
            return -1

        s, e = beginWord, endWord
        wordSet = set(wordList)
        if e not in wordSet: return 0
        ans = bfs()
        return 0 if ans == -1 else ans + 1
```
* 时间复杂度:令 `wordList` 长度为 $n$,`beginWord` 字符串长度为 $m$。由于所有的搜索结果必须都在 `wordList` 出现过,因此算上起点最多有 $n + 1$ 节点,最坏情况下,所有节点都联通,搜索完整张图复杂度为 $O(n^2)$ ;从 `beginWord` 出发进行字符替换,替换时进行逐字符检查,复杂度为 $O(m)$。整体复杂度为 $O(m \times n^2)$
* 空间复杂度:同等空间大小。$O(m \times n^2)$

---

### 总结

**这本质其实是一个「所有边权均为 `1`」最短路问题:将 `beginWord` 和所有在 `wordList` 出现过的字符串看做是一个点。每一次转换操作看作产生边权为 `1` 的边。问题求以 `beginWord` 为源点,以 `endWord` 为汇点的最短路径。**

借助这个题,我向你介绍了「双向 BFS」,「双向 BFS」可以有效解决「搜索空间爆炸」问题。

**对于那些搜索节点随着层数增加呈倍数或指数增长的搜索问题,可以使用「双向 BFS」进行求解。**

---

### 启发式搜索 `AStar`

可以直接根据本题规则来设计 AStar 的「启发式函数」。

比如对于两个字符串 `a` `b` 直接使用它们不同字符的数量来充当估值距离,我觉得是合适的。

因为不同字符数量的差值可以确保不会超过真实距离(是一个理论最小替换次数)。

注意:本题数据比较弱,用 AStar  过了,但通常我们需要「确保有解」,AStar  的启发搜索才会发挥真正价值。而本题,除非 `endWord` 本身就不在 `wordList` 中,其余情况我们无法很好提前判断「是否有解」。这时候 AStar  将不能带来「搜索空间的优化」,效果不如「双向 BFS」。

Java 代码:
```Java
class Solution {
    class Node {
        String str;
        int val;
        Node (String _str, int _val) {
            str = _str;
            val = _val;
        }
    }
    String s, e;
    int INF = 0x3f3f3f3f;
    Set<String> set = new HashSet<>();
    public int ladderLength(String beginWord, String endWord, List<String> wordList) {
        s = beginWord; e = endWord;
        for (String w : wordList) set.add(w);
        if (!set.contains(e)) return 0;
        int ans = astar();
        return ans == -1 ? 0 : ans + 1;
    }
    int astar() {
        PriorityQueue<Node> q = new PriorityQueue<>((a,b)->a.val-b.val);
        Map<String, Integer> dist = new HashMap<>();
        dist.put(s, 0);
        q.add(new Node(s, f(s)));

        while (!q.isEmpty()) {
            Node poll = q.poll();
            String str = poll.str;
            int distance = dist.get(str);
            if (str.equals(e)) {
                break;
            }
            int n = str.length();
            for (int i = 0; i < n; i++) {
                for (int j = 0; j < 26; j++) {
                    String sub = str.substring(0, i) + String.valueOf((char)('a' + j)) + str.substring(i + 1);
                    if (!set.contains(sub)) continue;
                    if (!dist.containsKey(sub) || dist.get(sub) > distance + 1) {
                        dist.put(sub, distance + 1);
                        q.add(new Node(sub, dist.get(sub) + f(sub)));
                    }
                }
            }
        }
        return dist.containsKey(e) ? dist.get(e) : -1;
    }
    int f(String str) {
        if (str.length() != e.length()) return INF;
        int n = str.length();
        int ans = 0;
        for (int i = 0; i < n; i++) {
            ans += str.charAt(i) == e.charAt(i) ? 0 : 1;
        }
        return ans;
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    struct Node {
        string str;
        int val;
        Node(string _str, int _val) : str(_str), val(_val) {}
    };

    string s, e;
    const int INF = 0x3f3f3f3f;
    unordered_set<string> set;

    int ladderLength(string beginWord, string endWord, vector<string>& wordList) {
        s = beginWord; e = endWord;
        for (const string& w : wordList) set.insert(w);
        if (set.find(e) == set.end()) return 0;
        int ans = astar();
        return ans == -1 ? 0 : ans + 1;
    }
    int astar() {
        priority_queue<Node, vector<Node>, function<bool(Node, Node)>> q([](Node a, Node b) {
            return a.val > b.val;
        });
        unordered_map<string, int> dist;
        dist[s] = 0;
        q.push(Node(s, f(s)));
        while (!q.empty()) {
            Node poll = q.top();
            q.pop();
            string str = poll.str;
            int distance = dist[str];
            if (str == e) break;
            int n = str.length();
            for (int i = 0; i < n; i++) {
                for (int j = 0; j < 26; j++) {
                    string sub = str.substr(0, i) + char('a' + j) + str.substr(i + 1);
                    if (set.find(sub) == set.end()) continue;
                    if (dist.find(sub) == dist.end() || dist[sub] > distance + 1) {
                        dist[sub] = distance + 1;
                        q.push(Node(sub, dist[sub] + f(sub)));
                    }
                }
            }
        }
        return dist.find(e) != dist.end() ? dist[e] : -1;
    }
    int f(string str) {
        if (str.length() != e.length()) return INF;
        int n = str.length(), ans = 0;
        for (int i = 0; i < n; i++) ans += str[i] == e[i] ? 0 : 1;
        return ans;
    }
};
```
* 时间复杂度:启发式搜索分析时空复杂度意义不大
* 空间复杂度:启发式搜索分析时空复杂度意义不大

### 最后

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

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

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

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


Question:
有 `n` 个筹码。第 $i$ 个筹码的位置是 `position[i]`。

我们需要把所有筹码移到同一个位置。在一步中,我们可以将第 $i$ 个筹码的位置从 $position[i]$ 改变为:

* `position[i] + 2 或 position[i] - 2`,此时 `cost = 0`
* `position[i] + 1 或 position[i] - 1`,此时 `cost = 1`

返回将所有筹码移动到同一位置上所需要的 最小代价 。

示例 1:

```
输入:position = [1,2,3]

输出:1

解释:第一步:将位置3的筹码移动到位置1,成本为0。
第二步:将位置2的筹码移动到位置1,成本= 1。
总成本是1。
```
示例 2:

```
输入:position = [2,2,2,3,3]

输出:2

解释:我们可以把位置3的两个筹码移到位置2。每一步的成本为1。总成本= 2。
```
示例 3:
```
输入:position = [1,1000000000]

输出:1
```

提示:
* $1 <= chips.length <= 100$
* $1 <= chips[i] <= 10^9$

Solution:
### 贪心 + 枚举目标位置

假设移动的目标位置是 $a$,当前所在位置是 $b$,将小球从 $b$ 移动到 $a$ 的成本取决于两位置距离的「奇偶性」,距离为偶数时成本固定为 $0$,距离为奇数时成本固定为 $1$。

同时我们可以通过「分情况讨论」来证明,所有小球移动到一个全新位置(起始没有小球的位置),结果不会变好,假设所选择的最终(全新)位置为 $t$:

* 假设选择的位置 $t$ 导致所有数到位置 $t$ 距离均为偶数,此时总成本为 $0$,同时可知所有数的位置奇偶性相同,此时选择所有数中的任意一个的位置,同样可得总成本为 $0$ 的结果,因此选全新的位置不会让结果变好;
* 假设选择的位置 $t$ 导致所有数到位置 $t$ 距离均为奇数,此时总成本为 $n$,同时可知所有数的位置奇偶性相同,此时选择所有数中的任意一个的位置,可得总成本为 $0$ 的结果,因此选全新的位置会让结果变差;
* 假设选择的位置 $t$ 导致所有数到位置 $t$ 距离奇数结果为 $c1$,偶数结果为 $c2$,可知 $n = c1 + c2$,同时我们通过调整 $t$ 的奇偶性来确保 $c1 <= c2$。此时总的成本为 $c1$,同时可知所有与 $t$ 距离为奇数的数所在位置奇偶性相同,所有与 $t$ 距离为偶数的数所在位置奇偶性也相同,此时将 $t$ 调整为与 $t$ 奇偶性相同的原有数的位置,同样能够得到总成本为 $c1$ 的结果,因此选全新的位置不会让结果变好。

综上,我们可以枚举所有已有的位置为目标位置,并通过奇偶性统计其余位置到目标位置的成本,在所有已有位置中取最小的总成本即是答案。

代码:
```Java
class Solution {
    public int minCostToMoveChips(int[] ps) {
        int n = ps.length, ans = Integer.MAX_VALUE;
        for (int i = 0; i < n; i++) {
            int a = ps[i], cur = 0;
            for (int j = 0; j < n; j++) {
                int b = ps[j];
                cur += Math.abs(a - b) % 2;
            }
            ans = Math.min(ans, cur);
        }
        return ans;
    }
}
```
* 时间复杂度:$O(n^2)$
* 空间复杂度:$O(1)$

---

### 贪心 + 统计奇偶性

更进一步,我们可以发现要使得「总的移动成本最优」的目标位置有无数个,只要目标位置的奇偶性不变,即可确保总成本不变。

因此我们可以省去枚举具体位置的操作,转而统计原有数的奇偶位置个数,假设偶数位置有 $a$ 个,奇数位置有 $b$ 个,最终目标位置选为偶数的成本为 $b$,最终目标位置选为奇数的成本为 $a$,即两者中的最小值即是答案。

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

### 最后

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

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

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

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


1218. 最长定差子序列

Question:
给你一个整数数组 `arr` 和一个整数 `difference`,请你找出并返回 `arr` 中最长等差子序列的长度,该子序列中相邻元素之间的差等于 `difference` 。

子序列 是指在不改变其余元素顺序的情况下,通过删除一些元素或不删除任何元素而从 `arr` 派生出来的序列。

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

输出:4

解释:最长的等差子序列是 [1,2,3,4]。
```
示例 2:
```
输入:arr = [1,3,5,7], difference = 1

输出:1

解释:最长的等差子序列是任意单个元素。
```
示例 3:
```
输入:arr = [1,5,7,8,5,3,4,2,1], difference = -2

输出:4

解释:最长的等差子序列是 [7,5,3,1]。
```

提示:
* $1 <= arr.length <= 10^5$
* $-10^4 <= arr[i], difference <= 10^4$

Solution:
### 状态机序列 DP + 哈希表

**定义 $f[i][j]$($j$ 非 $0$ 即 $1$) 为代表考虑前 $i$ 个数,且第 $i$ 个数的选择情况为 $j$ 时,得到的最长定差子序列长度。**

最终答案为 $\max(f[n - 1][0], f[n - 1][1])$,同时我们有显然的初始化条件 $f[0][0] = 0$ 和 $f[0][1] = 1$。

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

* $f[i][0]$:明确了第 $i$ 个不选,那么此时最大长度为前一个位置的结果。即有:

$$
f[i][0] = \max(f[i - 1][0], f[i - 1][1])
$$

* $f[i][1]$:明确了第 $i$ 个要选,此时进行分情况讨论:

    * $arr[i]$ 独立成为一个子序列,此时有:$f[i][1] = 1$;
    * $arr[i]$ 接在某一个数的后面,由于给定了差值 $difference$,可直接算得上一位的值为 $prev = arr[i] - difference$,此时应当找到值为 $prev$,下标最大(下标小于 $i$)的位置,然后从该位置转移过来,即有:$f[i][1] = f[hash[prev]][1] + 1$;

    > 容易证明:如果存在多个位置的值为 $prev$,从中选择一个下标最大的位置(下标小于 $i$)进行转移,结果相比于最优位置不会变差。因此我们「贪心」选择下标最大的位置(下标小于 $i$)即可,这引导我们在转移过程中使用「哈希表」记录处理过的位置的值信息。

    综上,我们有:

$$
f[i][1] = \begin{cases}
1 & hash[arr[i] - difference] = -1 \\
f[hash[prev]][1] + 1 & hash[arr[i] - difference] \neq -1
\end{cases}
$$

代码(使用数组充当哈希表的代码在 $P2$):
```Java
class Solution {
    public int longestSubsequence(int[] arr, int d) {
        int n = arr.length;
        Map<Integer, Integer> map = new HashMap<>();
        int[][] f = new int[n][2];
        f[0][1] = 1;
        map.put(arr[0], 0);
        for (int i = 1; i < n; i++) {
            f[i][0] = Math.max(f[i - 1][0], f[i - 1][1]);
            f[i][1] = 1;
            int prev = arr[i] - d;
            if (map.containsKey(prev)) f[i][1] = Math.max(f[i][1], f[map.get(prev)][1] + 1);
            map.put(arr[i], i);
        }
        return Math.max(f[n - 1][0], f[n - 1][1]);
    }
}
```

```Java
class Solution {
    int N = 40009, M = N / 2;
    public int longestSubsequence(int[] arr, int d) {
        int n = arr.length;
        int[] hash = new int[N];
        Arrays.fill(hash, -1);
        int[][] f = new int[n][2];
        f[0][1] = 1;
        hash[arr[0] + M] = 0;
        for (int i = 1; i < n; i++) {
            f[i][0] = Math.max(f[i - 1][0], f[i - 1][1]);
            f[i][1] = 1;
            int prev = arr[i] - d;
            if (hash[prev + M] != -1) f[i][1] = Math.max(f[i][1], f[hash[prev + M]][1] + 1);
            hash[arr[i] + M] = i;
        }
        return Math.max(f[n - 1][0], f[n - 1][1]);
    }
}
```
* 时间复杂度:令 $n$ 为数组长度,共有 $n * 2$ 个状态需要被计算,每个状态转移的复杂度为 $O(1)$。整体复杂度为 $O(n)$
* 空间复杂度:$O(n)$

---

### 优化状态定义

不难发现,我们多定义一维状态来区分某个位置的值是否被选择,目的是为了正确转移出第 $i$ 位被选择的情况。

事实上,利用哈希表本身我们就能轻松做到这一点。

我们调整状态定义为:**$f[i]$ 为考虑前 $i$ 个数(第 $i$ 个数必选)时,得到的最长定差子序列长度。**

不失一般性考虑 $f[i]$ 该如何转移,分情况讨论:

* $arr[i]$ 独立成为一个子序列,此时有:$f[i] = 1$;
* $arr[i]$ 接在某一个数的后面,由于给定了差值 $difference$,可直接算得上一位的值为 $prev = arr[i] - difference$,此时应当找到 $arr[j]$ 为 $prev$ 的最新位置(下标最大,同时满足 $j < i$)当时的转移结果,在此基础上加一即可,即有:$f[i] = hash[prev] + 1$;

综上,我们有($hash$ 初始化为 $0$):

$$
f[i] = hash[prev] + 1
$$

代码(使用数组充当哈希表的代码在 $P2$):
```Java
class Solution {
    public int longestSubsequence(int[] arr, int d) {
        int ans = 1;
        Map<Integer, Integer> map = new HashMap<>();
        for (int i : arr) {
            map.put(i, map.getOrDefault(i - d, 0) + 1);
            ans = Math.max(ans, map.get(i));
        }
        return ans;
    }
}
```

```Java
class Solution {
    int N = 40009, M = N / 2;
    public int longestSubsequence(int[] arr, int d) {
        int ans = 1;
        int[] hash = new int[N];
        for (int i : arr) {
            hash[i + M] = hash[i - d + M] + 1;
            ans = Math.max(ans, hash[i + M]);
        }
        return ans;
    }
}
```
* 时间复杂度:令 $n$ 为数组长度,共有 $n$ 个状态需要被计算,每个状态转移的复杂度为 $O(1)$。整体复杂度为 $O(n)$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


Question:
你要开发一座金矿,地质勘测学家已经探明了这座金矿中的资源分布,并用大小为 `m * n` 的网格 $grid$ 进行了标注。每个单元格中的整数就表示这一单元格中的黄金数量;如果该单元格是空的,那么就是 $0$。

为了使收益最大化,矿工需要按以下规则来开采黄金:

* 每当矿工进入一个单元,就会收集该单元格中的所有黄金。
* 矿工每次可以从当前位置向上下左右四个方向走。
* 每个单元格只能被开采(进入)一次。
* 不得开采(进入)黄金数目为 $0$ 的单元格。
* 矿工可以从网格中 **任意一个** 有黄金的单元格出发或者是停止。

示例 1:
```
输入:grid = [[0,6,0],[5,8,7],[0,9,0]]

输出:24

解释:
[[0,6,0],
 [5,8,7],
 [0,9,0]]
一种收集最多黄金的路线是:9 -> 8 -> 7。
```
示例 2:
```
输入:grid = [[1,0,7],[2,0,6],[3,4,5],[0,3,0],[9,0,20]]

输出:28

解释:
[[1,0,7],
 [2,0,6],
 [3,4,5],
 [0,3,0],
 [9,0,20]]
一种收集最多黄金的路线是:1 -> 2 -> 3 -> 4 -> 5 -> 6 -> 7。
```

提示:
* $1 <= grid.length, grid[i].length <= 15$
* $0 <= grid[i][j] <= 100$
* 最多 $254 个单元格中有黄金。

Solution:
### 回溯算法

根据题意,我们可以枚举每个黄金点作为起点,然后使用 `DFS` 回溯搜索以该点作为起点所能得到的最大收益。

代码:
```Java
class Solution {
    int[][] g;
    boolean[][] vis;
    int m, n;
    int[][] dirs = new int[][]{{1,0},{-1,0},{0,1},{0,-1}};
    public int getMaximumGold(int[][] grid) {
        g = grid;
        m = g.length; n = g[0].length;
        vis = new boolean[m][n];
        int ans = 0;
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                if (g[i][j] != 0) {
                    vis[i][j] = true;
                    ans = Math.max(ans, dfs(i, j));
                    vis[i][j] = false;
                }
            }
        }
        return ans;
    }
    int dfs(int x, int y) {
        int ans = g[x][y];
        for (int[] d : dirs) {
            int nx = x + d[0], ny = y + d[1];
            if (nx < 0 || nx >= m || ny < 0 || ny >= n) continue;
            if (g[nx][ny] == 0) continue;
            if (vis[nx][ny]) continue;
            vis[nx][ny] = true;
            ans = Math.max(ans, g[x][y] + dfs(nx, ny));
            vis[nx][ny] = false;
        }
        return ans;
    }
}
```
* 时间复杂度:爆搜复杂度为指数级别,分析时空复杂度意义不大
* 空间复杂度:爆搜复杂度为指数级别,分析时空复杂度意义不大

### 最后

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

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

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

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