LeetCode/761-770/762. 二进制表示中质数个计算置位（简单）.md

762. 二进制表示中质数个计算置位

Question:
给你两个整数 `left` 和 `right`,在闭区间 $[left, right]$ 范围内,统计并返回计算置位位数为质数的整数个数。

计算置位位数就是二进制表示中 $1$ 的个数。

例如, $21$ 的二进制表示 `10101` 有 $3$ 个计算置位。

示例 1:
```
输入:left = 6, right = 10

输出:4

解释:
6 -> 110 (2 个计算置位,2 是质数)
7 -> 111 (3 个计算置位,3 是质数)
9 -> 1001 (2 个计算置位,2 是质数)
10-> 1010 (2 个计算置位,2 是质数)
共计 4 个计算置位为质数的数字。
```
示例 2:
```
输入:left = 10, right = 15

输出:5

解释:
10 -> 1010 (2 个计算置位, 2 是质数)
11 -> 1011 (3 个计算置位, 3 是质数)
12 -> 1100 (2 个计算置位, 2 是质数)
13 -> 1101 (3 个计算置位, 3 是质数)
14 -> 1110 (3 个计算置位, 3 是质数)
15 -> 1111 (4 个计算置位, 4 不是质数)
共计 5 个计算置位为质数的数字。
```

提示:
* $1 <= left <= right <= 10^6$
* $0 <= right - left <= 10^4$

Solution:
### 模拟 + lowbit

利用一个 `int` 的二进制表示不超过 $32$,我们可以先将 $32$ 以内的质数进行打表。

从前往后处理 $[left, right]$ 中的每个数 $x$,利用 `lowbit` 操作统计 $x$ 共有多少位 $1$,记为 $cnt$,若 $cnt$ 为质数,则对答案进行加一操作。

代码:
```Java
class Solution {
    static boolean[] hash = new boolean[40];
    static {
        int[] nums = new int[]{2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31};
        for (int x : nums) hash[x] = true;
    }
    public int countPrimeSetBits(int left, int right) {
        int ans = 0;
        for (int i = left; i <= right; i++) {
            int x = i, cnt = 0;
            while (x != 0 && ++cnt >= 0) x -= (x & -x);
            if (hash[cnt]) ans++;
        }
        return ans;
    }
}
```
* 时间复杂度:$O((right - left) * \log{right})$
* 空间复杂度:$O(C)$

---

### 模拟 + 分治

枚举 $[left, right]$ 范围内的数总是不可避免,上述解法的复杂度取决于复杂度为 $O(\log{x})$ 的 `lowbit` 操作。

而比 `lowbit` 更加优秀的统计「二进制 $1$ 的数量」的做法最早在 [(题解) 191. 位1的个数](https://leetcode-cn.com/problems/number-of-1-bits/solution/yi-ti-san-jie-wei-shu-jian-cha-you-yi-to-av1r/) 讲过,采用「分治」思路对二进制进行成组统计,复杂度为 $O(\log{\log{x}})$。

代码:
```Java
class Solution {
    static boolean[] hash = new boolean[40];
    static {
        int[] nums = new int[]{2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31};
        for (int x : nums) hash[x] = true;
    }
    public int countPrimeSetBits(int left, int right) {
        int ans = 0;
        for (int i = left; i <= right; i++) {
            int x = i;
            x = (x & 0x55555555) + ((x >>> 1)  & 0x55555555);
            x = (x & 0x33333333) + ((x >>> 2)  & 0x33333333);
            x = (x & 0x0f0f0f0f) + ((x >>> 4)  & 0x0f0f0f0f);
            x = (x & 0x00ff00ff) + ((x >>> 8)  & 0x00ff00ff);
            x = (x & 0x0000ffff) + ((x >>> 16) & 0x0000ffff);
            if (hash[x]) ans++;
        }
        return ans;
    }
}
```
* 时间复杂度:$O((right - left) * \log{\log{right}})$
* 空间复杂度:$O(C)$

### 最后

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

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

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

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


Question:
在一个 $n \times n$ 的矩阵 `grid` 中,除了在数组 `mines` 中给出的元素为 `0`,其他每个元素都为 `1`。$mines[i] = [x_i, y_i]$ 表示 $grid[x_i][y_i] = 0$。

返回  `grid` 中包含 `1` 的最大的轴对齐加号标志的阶数 。

如果未找到加号标志,则返回 `0` 。

一个 `k` 阶由 `1` 组成的 “轴对称”加号标志 具有中心网格 $grid[r][c] = 1$ ,以及 `4` 个从中心向上、向下、向左、向右延伸,长度为 `k-1`,由 `1` 组成的臂。

注意,只有加号标志的所有网格要求为 `1` ,别的网格可能为 `0` 也可能为 `1` 。

示例 1:

```
输入: n = 5, mines = [[4, 2]]

输出: 2

解释: 在上面的网格中,最大加号标志的阶只能是2。一个标志已在图中标出。
```
示例 2:

```
输入: n = 1, mines = [[0, 0]]

输出: 0

解释: 没有加号标志,返回 0 。
```

提示:
* $1 <= n <= 500$
* $1 <= mines.length <= 5000$
* $0 <= x_i, y_i < n$
* 每一对 $(x_i, y_i)$ 都 不重复

Solution:
### 预处理 + 模拟

假设点 $(x, y)$ 能够取得最大长度 $k$,我们知道 $k$ 取决于以点 $(x, y)$ 为起点,四联通方向中「最短的连续 $1$ 的长度」。

基于此,我们可以建立四个大小为 $n \times n$ 的矩阵(二维数组)`a`、`b`、`c` 和 `d` 分别代表四个方向连续 $1$ 的前缀数:

数据范围为 $500$,预处理前缀数组复杂度为 $O(n^2)$,统计答案复杂度为 $O(n^2)$,时间复杂度没有问题。

再考虑空间,建立四个方向的前缀数组所需空间为 $500 \times 500 \times 4 = 10^6$,即使加上原矩阵 `g` 也不会有 `MLE` 风险,空间复杂度也没有问题。

Java 代码:
```Java
class Solution {
    public int orderOfLargestPlusSign(int n, int[][] mines) {
        int[][] g = new int[n + 10][n + 10];
        for (int i = 1; i <= n; i++) Arrays.fill(g[i], 1);
        for (int[] info : mines) g[info[0] + 1][info[1] + 1] = 0;
        int[][] a = new int[n + 10][n + 10], b = new int[n + 10][n + 10], c = new int[n + 10][n + 10], d = new int[n + 10][n + 10];
        for (int i = 1; i <= n; i++) {
            for (int j = 1; j <= n; j++) {
                if (g[i][j] == 1) {
                    a[i][j] = a[i - 1][j] + 1;
                    b[i][j] = b[i][j - 1] + 1;
                }
                if (g[n + 1 - i][n + 1 - j] == 1) {
                    c[n + 1 - i][n + 1 - j] = c[n + 2 - i][n + 1 - j] + 1;
                    d[n + 1 - i][n + 1 - j] = d[n + 1 - i][n + 2 - j] + 1;
                }
            }
        }
        int ans = 0;
        for (int i = 1; i <= n; i++) {
            for (int j = 1; j <= n; j++) {
                ans = Math.max(ans, Math.min(Math.min(a[i][j], b[i][j]), Math.min(c[i][j], d[i][j])));
            }
        }
        return ans;
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    int orderOfLargestPlusSign(int n, vector<vector<int>>& mines) {
        vector<vector<int>> g(n + 10, vector<int>(n + 10, 1));
        for (vector<int>& info : mines) g[info[0] + 1][info[1] + 1] = 0;
        vector<vector<int>> a(n + 10, vector<int>(n + 10)), b(n + 10, vector<int>(n + 10)), c(n + 10, vector<int>(n + 10)), d(n + 10, vector<int>(n + 10));
        for (int i = 1; i <= n; i++) {
            for (int j = 1; j <= n; j++) {
                if (g[i][j] == 1) {
                    a[i][j] = a[i - 1][j] + 1;
                    b[i][j] = b[i][j - 1] + 1;
                }
                if (g[n + 1 - i][n + 1 - j] == 1) {
                    c[n + 1 - i][n + 1 - j] = c[n + 2 - i][n + 1 - j] + 1;
                    d[n + 1 - i][n + 1 - j] = d[n + 1 - i][n + 2 - j] + 1;
                }
            }
        }
        int ans = 0;
        for (int i = 1; i <= n; i++) {
            for (int j = 1; j <= n; j++) {
                ans = max(ans, min(min(a[i][j], b[i][j]), min(c[i][j], d[i][j])));
            }
        }
        return ans;
    }
};
```
Python 代码:
```Python
class Solution:
    def orderOfLargestPlusSign(self, n: int, mines: List[List[int]]) -> int:
        g = [[1] * (n + 10) for _ in range(n + 10)]
        for x, y in mines:
            g[x + 1][y + 1] = 0
        a, b, c, d = [[0] * (n + 10) for _ in range(n + 10)], [[0] * (n + 10) for _ in range(n + 10)], [[0] * (n + 10) for _ in range(n + 10)], [[0] * (n + 10) for _ in range(n + 10)]
        for i in range(1, n + 1):
            for j in range(1, n + 1):
                if g[i][j] == 1:
                    a[i][j] = a[i - 1][j] + 1
                    b[i][j] = b[i][j - 1] + 1
                if g[n + 1 - i][n + 1 - j] == 1:
                    c[n + 1 - i][n + 1 - j] = c[n + 2 - i][n + 1 - j] + 1
                    d[n + 1 - i][n + 1 - j] = d[n + 1 - i][n + 2 - j] + 1
        ans = 0
        for i in range(1, n + 1):
            for j in range(1, n + 1):
                ans = max(ans, min(min(a[i][j], b[i][j]), min(c[i][j], d[i][j])))
        return ans
```
TypeScript 代码:
```TypeScript
function orderOfLargestPlusSign(n: number, mines: number[][]): number {
    function getMat(x: number, y: number, val: number): number[][] {
        const ans = new Array<Array<number>>(x)
        for (let i = 0; i < x; i++) ans[i] = new Array<number>(y).fill(val)
        return ans
    }
    const g = getMat(n + 10, n + 10, 1)
    for (const info of mines) g[info[0] + 1][info[1] + 1] = 0
    const a = getMat(n + 10, n + 10, 0), b = getMat(n + 10, n + 10, 0), c = getMat(n + 10, n + 10, 0), d = getMat(n + 10, n + 10, 0)
    for (let i = 1; i <= n; i++) {
        for (let j = 1; j <= n; j++) {
            if (g[i][j] == 1) {
                a[i][j] = a[i - 1][j] + 1
                b[i][j] = b[i][j - 1] + 1
            }
            if (g[n + 1 - i][n + 1 - j] == 1) {
                c[n + 1 - i][n + 1 - j] = c[n + 2 - i][n + 1 - j] + 1
                d[n + 1 - i][n + 1 - j] = d[n + 1 - i][n + 2 - j] + 1
            }
        }
    }
    let ans = 0
    for (let i = 1; i <= n; i++) {
        for (let j = 1; j <= n; j++) {
            ans = Math.max(ans, Math.min(Math.min(a[i][j], b[i][j]), Math.min(c[i][j], d[i][j])))
        }
    }
    return ans
}
```
* 时间复杂度:$O(n^2)$
* 空间复杂度:$O(n^2)$

### 最后

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

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

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

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


Question:
N 对情侣坐在连续排列的 2N 个座位上,想要牵到对方的手。 计算最少交换座位的次数,以便每对情侣可以并肩坐在一起。 一次交换可选择任意两人,让他们站起来交换座位。

人和座位用 0 到 2N-1 的整数表示,情侣们按顺序编号,第一对是 (0, 1),第二对是 (2, 3),以此类推,最后一对是 (2N-2, 2N-1)。

这些情侣的初始座位  row[i] 是由最初始坐在第 i 个座位上的人决定的。

示例 1:
```
输入: row = [0, 2, 1, 3]
输出: 1
解释: 我们只需要交换row[1]和row[2]的位置即可。
```
示例 2:
```
输入: row = [3, 2, 0, 1]
输出: 0
解释: 无需交换座位,所有的情侣都已经可以手牵手了。
```
说明:
* len(row) 是偶数且数值在 [4, 60]范围内。
* 可以保证row 是序列 `0...len(row)-1` 的一个全排列。

Solution:
### 并查集

首先,我们总是以「情侣对」为单位进行设想:

1. 当有两对情侣相互坐错了位置,ta们两对之间形成了一个环。需要进行一次交换,使得每队情侣独立(相互牵手)

2. 如果三对情侣相互坐错了位置,ta们三对之间形成了一个环,需要进行两次交换,使得每队情侣独立(相互牵手)

3. 如果四对情侣相互坐错了位置,ta们四对之间形成了一个环,需要进行三次交换,使得每队情侣独立(相互牵手)

也就是说,如果我们有 `k` 对情侣形成了错误环,需要交换 `k - 1` 次才能让情侣牵手。

**于是问题转化成 `n / 2` 对情侣中,有多少个这样的环。**

可以直接使用「并查集」来做。

由于 0和1配对、2和3配对 ... 因此互为情侣的两个编号除以 2 对应同一个数字,可直接作为它们的「情侣组」编号:

```Java
 class Solution {
    int[] p = new int[70];
    void union(int a, int b) {
        p[find(a)] = p[find(b)];
    }
    int find(int x) {
        if (p[x] != x) p[x] = find(p[x]);
        return p[x];
    }
    public int minSwapsCouples(int[] row) {
        int n = row.length, m = n / 2;
        for (int i = 0; i < m; i++) p[i] = i;
        for (int i = 0; i < n; i += 2) union(row[i] / 2, row[i + 1] / 2);
        int cnt = 0;
        for (int i = 0; i < m; i++) {
            if (i == find(i)) cnt++;
        }
        return m - cnt;
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n)$

***

### 贪心

还是以「情侣对」为单位进行分析:

由于题目保证有解,我们也可以从前往后(每两格作为一步)处理,对于某一个位置而言,如果下一个位置不是应该出现的情侣的话。

则对下一个位置进行交换。

同时为了方便我们找到某个值的下标,需要先对 `row` 进行预处理(可以使用哈希表或数组)。

```Java
class Solution {
    public int minSwapsCouples(int[] row) {
        int n = row.length;
        int ans = 0;
        int[] cache = new int[n];
        for (int i = 0; i < n; i++) cache[row[i]] = i;
        for (int i = 0; i < n - 1; i += 2) {
            int a = row[i], b = a ^ 1;
            if (row[i + 1] != b) {
                int src = i + 1, tar = cache[b];
                cache[row[tar]] = src;
                cache[row[src]] = tar;
                swap(row, src, tar);
                ans++;
            }
        }
        return ans;
    }
    void swap(int[] nums, int a, int b) {
        int c = nums[a];
        nums[a] = nums[b];
        nums[b] = c;
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n)$

***

### 证明/分析

我们这样的做法本质是什么?

**其实相当于,当我处理到第 `k` 个位置的时候,前面的 `k - 1` 个位置的情侣已经牵手成功了。我接下来怎么处理,能够使得总花销最低。**

分两种情况讨论:

a. 现在处理第 `k` 个位置,使其牵手成功:

那么我要使得第 `k` 个位置的情侣也牵手成功,那么必然是保留第 `k` 个位置的情侣中其中一位,再进行修改,这样的成本是最小的(因为只需要交换一次)。

而且由于前面的情侣已经牵手成功了,因此交换的情侣必然在 `k` 位置的后面。

然后我们再考虑交换左边或者右边对最终结果的影响。

分两种情况来讨论:

1. 与第 `k` 个位置的匹配的两个情侣不在同一个位置上:这时候无论交换左边还是右边,后面需要调整的「情侣对数量」都是一样。假设处理第 `k` 个位置前需要调整的数量为 `n` 的话,处理完第 `k` 个位置(交换左边或是右边),需要调整的「情侣对数量」都为 `n - 1` 。

2. 与第 `k` 个位置的匹配的两个情侣在同一个位置上:这时候无论交换左边还是右边,后面需要调整的「情侣对数量」都是一样。假设处理第 `k` 个位置前需要调整的数量为 `n` 的话,处理完第 `k` 个位置(交换左边或是右边),需要调整的「情侣对数量」都为 `n - 2` 。

因此对于第 `k` 个位置而言,交换左边还是右边,并不会影响后续需要调整的「情侣对数量」。

b. 现在先不处理第 `k` 个位置,等到后面的情侣处理的时候「顺便」处理第 `k` 位置:

由于我们最终都是要所有位置的情侣牵手,而且每一个数值对应的情侣数值是唯一确定的。

因此我们这个等“后面”的位置处理,其实就是等与第 `k` 个位置互为情侣的位置处理(对应上图的就是我们是在等 【0 x】和【8 y】或者【0 8】这些位置被处理)。

由于被处理都是同一批的联通位置,因此和「a. 现在处理第 `k` 个位置」的分析结果是一样的。

---

不失一般性的,我们可以将这个分析推广到第一个位置,其实就已经是符合「当我处理到第 `k` 个位置的时候,前面的 `k - 1` 个位置的情侣已经牵手成功了」的定义了。

**综上所述,我们只需要确保从前往后处理,并且每次处理都保留第 `k` 个位置的其中一位,无论保留的左边还是右边都能得到最优解。**

### 最后

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

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

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

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


Question:
给你一个 m x n 的矩阵 matrix 。如果这个矩阵是托普利茨矩阵,返回 true ;否则,返回 false 。

如果矩阵上每一条由左上到右下的对角线上的元素都相同,那么这个矩阵是 托普利茨矩阵 。

示例 1:

```
输入:matrix = [[1,2,3,4],[5,1,2,3],[9,5,1,2]]
输出:true
解释:
在上述矩阵中, 其对角线为:
"[9]", "[5, 5]", "[1, 1, 1]", "[2, 2, 2]", "[3, 3]", "[4]"。
各条对角线上的所有元素均相同, 因此答案是 True 。
```
示例 2:

```
输入:matrix = [[1,2],[2,2]]
输出:false
解释:
对角线 "[1, 2]" 上的元素不同。
```

提示:
* m == matrix.length
* n == matrix[i].length
* 1 <= m, n <= 20
* 0 <= matrix[i][j] <= 99

进阶:
* 如果矩阵存储在磁盘上,并且内存有限,以至于一次最多只能将矩阵的一行加载到内存中,该怎么办?
* 如果矩阵太大,以至于一次只能将不完整的一行加载到内存中,该怎么办?

Solution:
### 按「格子」遍历

对于一个合格的「托普利茨矩阵」而言,每个格子总是与其左上角格子相等(如果有的话)。

我们以「格子」为单位进行遍历,每次与左上角的格子进行检查即可。

这样我们每对一个格子进行判断,都要读 `matrix` 上的两个格子的值(即**非边缘格子其实会被读取两次**)。

```Java
class Solution {
    public boolean isToeplitzMatrix(int[][] matrix) {
        int m = matrix.length, n = matrix[0].length;
        for (int i = 1; i < m; i++) {
            for (int j = 1; j < n; j++) {
                if (matrix[i][j] != matrix[i - 1][j - 1]) return false;
            }
        }
        return true;
    }
}
```
* 时间复杂度:$O(n * m)$
* 空间复杂度:$O(1)$

***

### 按「线」遍历

如果稍微增加一点点难度:限制每个格子只能被读取一次呢?

这时候我们也可以按照「线」为单位进行检查。

当一条完整的斜线值都相等,我们再对下一条斜线做检查。

这样对于每个格子,我们都是**严格只读取一次**(如果整个矩阵是存在磁盘中,且不考虑操作系统的按页读取等机制,那么 IO 成本将下降为原来的一半)。

```Java
class Solution {
    public boolean isToeplitzMatrix(int[][] matrix) {
        int m = matrix.length, n = matrix[0].length;
        int row = m, col = n;
        while (col-- > 0) {
            for (int i = 0, j = col, val = matrix[i++][j++]; i < m && j < n; i++, j++) {
                if (matrix[i][j] != val) return false;
            }
        }
        while (row-- > 0) {
            for (int i = row, j = 0, val = matrix[i++][j++]; i < m && j < n; i++, j++) {
                if (matrix[i][j] != val) return false;
            }
        }
        return true;
    }
}
```
* 时间复杂度:$O(n * m)$
* 空间复杂度:$O(1)$

***

### 进阶

1. 如果矩阵存储在磁盘上,并且内存有限,以至于一次最多只能将矩阵的一行加载到内存中,该怎么办?
使用「背包问题」的一维优化思路:假设我们有装载一行数据大小的内存,每次读取新的行时都进行「从右往左」的覆盖,每次覆盖都与前一个位置的进行比较(其实就是和上一行的左上角位置进行比较)。

如图:

如果你对更多「背包问题」相关内容感兴趣,可以看我这篇总结:[深度讲解背包问题](https://leetcode-cn.com/circle/discuss/GWpXCM/)

2. 如果矩阵太大,以至于一次只能将不完整的一行加载到内存中,该怎么办?
亲,这边建议升级内存,啊呸 ...
将问题看做子问题进行解决:调整读取矩阵的方式(按照垂直方向进行读取);或使用额外数据记录当前当前片段的行/列数。
更为合理的解决方案是:存储的时候按照「数组」的形式进行存储(行式存储),然后读取的时候计算偏移量直接读取其「左上角」或者「右下角」的值。

### 最后

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

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

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

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


LeetCode/761-770/768. 最多能完成排序的块 II（困难）.md

768. 最多能完成排序的块 II

Question:
这个问题和“最多能完成排序的块”相似,但给定数组中的元素可以重复,输入数组最大长度为$2000$,其中的元素最大为 $10^8$。

`arr` 是一个可能包含重复元素的整数数组,我们将这个数组分割成几个“块”,并将这些块分别进行排序。之后再连接起来,使得连接的结果和按升序排序后的原数组相同。

我们最多能将数组分成多少块?

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

输出: 1

解释:
将数组分成2块或者更多块,都无法得到所需的结果。
例如,分成 [5, 4], [3, 2, 1] 的结果是 [4, 5, 1, 2, 3],这不是有序的数组。
```
示例 2:
```
输入: arr = [2,1,3,4,4]

输出: 4

解释:
我们可以把它分成两块,例如 [2, 1], [3, 4, 4]。
然而,分成 [2, 1], [3], [4], [4] 可以得到最多的块数。
```
注意:
* `arr` 的长度在 $[1, 2000]$ 之间。
* `arr[i]` 的大小在 $[0, 10^8]$ 之间。

Solution:
### 贪心 + 构造

一种容易想到的构造方法,是与目标序列(已排升序的数组 `clone`)做区间比较。

由于题目要求尽可能划分出多的区间,我们可以从前往后处理 `arr` 和 `clone` 时统计区间内数的情况,若有 `arr[i...j]` 和 `clone[i...j]` 词频完全相同,可知 `arr[i...j]` 可通过内部排序调整为 `clone[i...j]`,此时我们将范围 $[i...j]$ 划分为一个区间,然后继续往后处理直到整个数组处理完。

容易证明该做法的正确性:可从边界开始进行归纳分析,起始两者均从下标为 $0$ 的位置进行扫描。假设最优解和贪心解的第一个区间的结束位置相同,问题就会归结到子问题上(即双方均从相同的子数组起始位置开始构造),因此无须额外证明;而当起始位置相同,结束位置不同时,假设分别为 $clone[i...j_1]$ 和 $arr[i...j_2]$,则必然有 $j_1 > j_2$(因为如果有 $j_1 < j_2$,那么在 $arr$ 扫描到 $j_1$ 位置时已经满足划分区间的条件,已经会停下来,即与贪心决策逻辑冲突),而当 $j_1 > j_2$ 时,我们可以将最优解中的区间 $clone[i...j_1]$ 进一步划分为 $clone[i...j_2]$ 和 $clone[j_2+1 ... j_1]$ 两段,同时不影响后续的构造过程,使得最终划分的区间数增大,即与最优解本身无法划分冲突。

根据数值之间满足严格全序,可知当 $j_1 > j_2$ 和 $j_1 < j_2$ 均不满足时,必然有 $j_1 = j_2$ 为真。

综上,我们证明了对于相同起点,贪心解与最优解结束位置必然相同,从而证明贪心解区间数与最优解相等。

于是原问题转换为如何快速对两数组(原数组 `arr` 和目标数组 `clone`)进行词频比较,由于数值的范围为 $10^8$,如果使用最裸的词频对比方案的话,需要先进行离散化,最终算法的复杂度为 $O(n\log{n} + n^2)$。而更好的解决方案是使用哈希表进行计数,同时维护当前计数不为 $0$ 的数值数量 `tot`,具体的,当我们处理 $arr[i]$ 时,我们在哈希表中对 $arr[i]$ 进行计数加一,而在处理 $clone[i]$ 时,对 $clone[i]$ 进行计数减一。从而将词频比较的复杂度从 $O(n^2)$ 下降到 $O(n)$。

Java 代码:
```Java
class Solution {
    public int maxChunksToSorted(int[] arr) {
        int[] clone = arr.clone();
        Arrays.sort(clone);
        int n = arr.length, ans = 0;
        Map<Integer, Integer> map = new HashMap<>();
        for (int i = 0, tot = 0; i < n; i++) {
            int a = arr[i], b = clone[i];
            if (map.getOrDefault(a, 0) == -1) tot--;
            else if (map.getOrDefault(a, 0) == 0) tot++;
            map.put(a, map.getOrDefault(a, 0) + 1);
            if (map.getOrDefault(b, 0) == 1) tot--;
            else if (map.getOrDefault(b, 0) == 0) tot++;
            map.put(b, map.getOrDefault(b, 0) - 1);
            if (tot == 0) ans++;
        }
        return ans;
    }
}
```
TypeScript 代码:
```TypeScript
function maxChunksToSorted(arr: number[]): number {
    let clone = [...arr].sort((a,b)=>a-b)
    let n = arr.length, ans = 0
    const map = new Map<number, number>()
    for (let i = 0, tot = 0; i < n; i++) {
        const a = arr[i], b = clone[i]
        if (!map.has(a)) map.set(a, 0)
        if (map.get(a) == 0) tot++
        else if (map.get(a) == -1) tot--;
        map.set(a, map.get(a) + 1)
        if (!map.has(b)) map.set(b, 0)
        if (map.get(b) == 0) tot++
        else if (map.get(b) == 1) tot--
        map.set(b, map.get(b) - 1)
        if (tot == 0) ans++
    }
    return ans
};
```
* 时间复杂度:$O(n\log{n})$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


769. 最多能完成排序的块

Question:
给定一个长度为 `n` 的整数数组 `arr` ,它表示在 $[0, n - 1]$ 范围内的整数的排列。

我们将 `arr` 分割成若干块 (即分区),并对每个块单独排序。

将它们连接起来后,使得连接的结果和按升序排序后的原数组相同。

返回数组能分成的最多块数量。

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

输出: 1

解释:
将数组分成2块或者更多块,都无法得到所需的结果。
例如,分成 [4, 3], [2, 1, 0] 的结果是 [3, 4, 0, 1, 2],这不是有序的数组。
```
示例 2:
```
输入: arr = [1,0,2,3,4]

输出: 4

解释:
我们可以把它分成两块,例如 [1, 0], [2, 3, 4]。
然而,分成 [1, 0], [2], [3], [4] 可以得到最多的块数。
```

提示:
* $n = arr.length$
* $1 <= n <= 10$
* $0 <= arr[i] < n$
* `arr` 中每个元素都不同

Solution:
### 模拟

本题考察的是简单模拟能力,或者说是简单的对「循环不变量」的设计。

我们从前往后处理所有的 $arr[i]$(即 `i` 定义为当前划分块的右边界下标),处理过程中定义变量 `j` 为当前划分块的左边界下标(初始值为 $0$),定义 `min` 为当前划分块中元素最小值(初始值为 $arr[0]$ 或 $n$),定义 `max` 为当前划分块中元素最大值(初始值为 $arr[0]$ 或 $-1$)。

当且仅当 $j = min$ 且 $i = max$ 时,下标范围 $[j, i]$ 排序结果为 $[min, max]$,此时块的个数加一,并重新初始化几个变量,继续循环统计下个块的信息。

Java 代码:
```Java
class Solution {
    public int maxChunksToSorted(int[] arr) {
        int n = arr.length, ans = 0;
        for (int i = 0, j = 0, min = n, max = -1; i < n; i++) {
            min = Math.min(min, arr[i]);
            max = Math.max(max, arr[i]);
            if (j == min && i == max) {
                ans++; j = i + 1; min = n; max = -1;
            }
        }
        return ans;
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    int maxChunksToSorted(vector<int>& arr) {
        int n = arr.size(), ans = 0;
        int j = 0, minv = n, maxv = -1;
        for (int i = 0; i < n; i++) {
            minv = min(minv, arr[i]);
            maxv = max(maxv, arr[i]);
            if (j == minv && i == maxv) {
                ans++; j = i + 1; minv = n; maxv = -1;
            }
        }
        return ans;
    }
};
```
Python 代码:
```Python
class Solution:
    def maxChunksToSorted(self, arr: List[int]) -> int:
        n, ans = len(arr), 0
        j, minv, maxv = 0, n, -1
        for i in range(n):
            minv, maxv = min(minv, arr[i]), max(maxv, arr[i])
            if j == minv and i == maxv:
                ans, j, minv, maxv = ans + 1, i + 1, n, -1
        return ans
```
TypeScript 代码:
```TypeScript
function maxChunksToSorted(arr: number[]): number {
    let n = arr.length, ans = 0
    for (let i = 0, j = 0, min = n, max = -1; i < n; i++) {
        min = Math.min(min, arr[i])
        max = Math.max(max, arr[i])
        if (j == min && i == max) {
            ans++; j = i + 1; min = n; max = -1;
        }
    }
    return ans
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


Question:
在一个 `2 x 3` 的板上(`board`)有 $5$ 块砖瓦,用数字 `1~5` 来表示, 以及一块空缺用 $0$ 来表示.

一次移动定义为选择 $0$ 与一个相邻的数字(上下左右)进行交换.

最终当板 `board` 的结果是 $[[1,2,3],[4,5,0]]$ 谜板被解开。

给出一个谜板的初始状态,返回最少可以通过多少次移动解开谜板,如果不能解开谜板,则返回 $-1$ 。

示例 1:
```
输入:board = [[1,2,3],[4,0,5]]

输出:1

解释:交换 0 和 5 ,1 步完成
```
示例 2:
```
输入:board = [[1,2,3],[5,4,0]]

输出:-1

解释:没有办法完成谜板
```
示例 3:
```
输入:board = [[4,1,2],[5,0,3]]

输出:5

解释:
最少完成谜板的最少移动次数是 5 ,
一种移动路径:
尚未移动: [[4,1,2],[5,0,3]]
移动 1 次: [[4,1,2],[0,5,3]]
移动 2 次: [[0,1,2],[4,5,3]]
移动 3 次: [[1,0,2],[4,5,3]]
移动 4 次: [[1,2,0],[4,5,3]]
移动 5 次: [[1,2,3],[4,5,0]]
```
示例 4:
```
输入:board = [[3,2,4],[1,5,0]]

输出:14
```
提示:
* `board` 是一个如上所述的 `2 x 3` 的数组.
* `board[i][j]` 是一个 $[0, 1, 2, 3, 4, 5]$ 的排列.

Solution:
### 基本分析

这是八数码问题的简化版:将 $3 * 3$ 变为 $2 * 3$,同时将「输出路径」变为「求最小步数」。

通常此类问题可以使用「BFS」、「AStar 算法」、「康拓展开」进行求解。

由于问题简化到了 $2 * 3$,我们使用前两种解法即可。

---

### BFS

为了方便,将原来的二维矩阵转成字符串(一维矩阵)进行处理。

这样带来的好处直接可以作为哈希 `Key` 使用,也可以很方便进行「二维坐标」与「一维下标」的转换。

由于固定是 $2 * 3$ 的格子,因此任意的合法二维坐标 $(x, y)$ 和对应一维下标 $idx$ 可通过以下转换:

* $idx = x * 3 + y$
* $x = idx / 3,y = idx \% 3$

其余的就是常规的 `BFS` 过程了。

代码:
```Java
class Solution {
    class Node {
        String str;
        int x, y;
        Node(String _str, int _x, int _y) {
            str = _str; x = _x; y = _y;
        }
    }
    int n = 2, m = 3;
    String s, e;
    int x, y;
    public int slidingPuzzle(int[][] board) {
        s = "";
        e = "123450";
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < m; j++) {
                s += board[i][j];
                if (board[i][j] == 0) {
                    x = i; y = j;
                }
            }
        }
        int ans = bfs();
        return ans;
    }
    int[][] dirs = new int[][]{{1,0},{-1,0},{0,1},{0,-1}};
    int bfs() {
        Deque<Node> d = new ArrayDeque<>();
        Map<String, Integer> map = new HashMap<>();
        Node root = new Node(s, x, y);
        d.addLast(root);
        map.put(s, 0);
        while (!d.isEmpty()) {
            Node poll = d.pollFirst();
            int step = map.get(poll.str);
            if (poll.str.equals(e)) return step;
            int dx = poll.x, dy = poll.y;
            for (int[] di : dirs) {
                int nx = dx + di[0], ny = dy + di[1];
                if (nx < 0 || nx >= n || ny < 0 || ny >= m) continue;
                String nStr = update(poll.str, dx, dy, nx, ny);
                if (map.containsKey(nStr)) continue;
                Node next = new Node(nStr, nx, ny);
                d.addLast(next);
                map.put(nStr, step + 1);
            }
        }
        return -1;
    }
    String update(String cur, int i, int j, int p, int q) {
        char[] cs = cur.toCharArray();
        char tmp = cs[i * m + j];
        cs[i * m + j] = cs[p * m + q];
        cs[p * m + q] = tmp;
        return String.valueOf(cs);
    }
}
```

---

### AStar 算法

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

比如对于两个状态 `a` 和 `b` 可直接计算出「理论最小转换次数」:**所有位置的数值「所在位置」与「目标位置」的曼哈顿距离之和(即横纵坐标绝对值之和)** 。

注意,我们只需要计算「非空格」位置的曼哈顿距离即可,因为空格的位置会由其余数字占掉哪些位置而唯一确定。

**AStar 求最短路的正确性问题:由于我们衡量某个状态 `str` 的估值是以目标字符串 `e=123450` 为基准,因此我们只能确保 `e` 出队时为「距离最短」,而不能确保中间节点出队时「距离最短」,因此我们不能单纯根据某个节点是否「曾经入队」而决定是否入队,还要结合当前节点的「最小距离」是否被更新而决定是否入队。**

这一点十分关键,在代码层面上体现在 `map.get(nStr) > step + 1` 的判断上。

**我们知道,AStar 算法在有解的情况下,才会发挥「启发式搜索」的最大价值,因此如果我们能够提前判断无解的情况,对 AStar 算法来说会是巨大的提升。**

而对于通用的 $N * N$ 数码问题,判定有解的一个充要条件是:**「逆序对」数量为偶数,如果不满足,必然无解,直接返回 $-1$ 即可。**

对该结论的充分性证明和必要性证明完全不在一个难度上,所以建议记住这个结论即可。

代码:
```Java
class Solution {
    class Node {
        String str;
        int x, y;
        int val;
        Node(String _str, int _x, int _y, int _val) {
            str = _str; x = _x; y = _y; val = _val;
        }
    }
    int f(String str) {
        int ans = 0;
        char[] cs1 = str.toCharArray(), cs2 = e.toCharArray();
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < m; j++) {
                // 跳过「空格」,计算其余数值的曼哈顿距离
                if (cs1[i * m + j] == '0' || cs2[i * m + j] == '0') continue;
                int cur = cs1[i * m + j], next = cs2[i * m + j];
                int xd = Math.abs((cur - 1) / 3 - (next - 1) / 3);
                int yd = Math.abs((cur - 1) % 3 - (next - 1) % 3);
                ans += (xd + yd);
            }
        }
        return ans;
    }
    int n = 2, m = 3;
    String s, e;
    int x, y;
    public int slidingPuzzle(int[][] board) {
        s = "";
        e = "123450";
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < m; j++) {
                s += board[i][j];
                if (board[i][j] == 0) {
                    x = i; y = j;
                }
            }
        }

        // 提前判断无解情况
        if (!check(s)) return -1;

        int[][] dirs = new int[][]{{1,0},{-1,0},{0,1},{0,-1}};
        Node root = new Node(s, x, y, f(s));
        PriorityQueue<Node> q = new PriorityQueue<>((a,b)->a.val-b.val);
        Map<String, Integer> map = new HashMap<>();
        q.add(root);
        map.put(s, 0);
        while (!q.isEmpty()) {
            Node poll = q.poll();
            int step = map.get(poll.str);
            if (poll.str.equals(e)) return step;
            int dx = poll.x, dy = poll.y;
            for (int[] di : dirs) {
                int nx = dx + di[0], ny = dy + di[1];
                if (nx < 0 || nx >= n || ny < 0 || ny >= m) continue;
                String nStr = update(poll.str, dx, dy, nx, ny);
                if (!map.containsKey(nStr) || map.get(nStr) > step + 1) {
                    Node next = new Node(nStr, nx, ny, step + 1 + f(nStr));
                    q.add(next);
                    map.put(nStr, step + 1);
                }
            }
        }
        return 0x3f3f3f3f; // never
    }
    String update(String cur, int i, int j, int p, int q) {
        char[] cs = cur.toCharArray();
        char tmp = cs[i * m + j];
        cs[i * m + j] = cs[p * m + q];
        cs[p * m + q] = tmp;
        return String.valueOf(cs);
    }
    boolean check(String str) {
        char[] cs = str.toCharArray();
        List<Integer> list = new ArrayList<>();
        for (int i = 0; i < n * m; i++) {
            if (cs[i] != '0') list.add(cs[i] - '0');
        }
        int cnt = 0;
        for (int i = 0; i < list.size(); i++) {
            for (int j = i + 1; j < list.size(); j++) {
                if (list.get(i) > list.get(j)) cnt++;
            }
        }
        return cnt % 2 == 0;
    }
}
```

### 最后

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

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

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

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


775. 全局倒置与局部倒置

Question:
给你一个长度为 `n` 的整数数组 `nums`,表示由范围 $[0, n - 1]$ 内所有整数组成的一个排列。

全局倒置 的数目等于满足下述条件不同下标对 $(i, j)$ 的数目:

* $0 <= i < j < n$
* $nums[i] > nums[j]$

局部倒置 的数目等于满足下述条件的下标 i 的数目:

* $0 <= i < n - 1$
* $nums[i] > nums[i + 1]$

当数组 `nums` 中 全局倒置 的数量等于 局部倒置 的数量时,返回 `true` ;否则,返回 `false` 。

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

输出:true

解释:有 1 个全局倒置,和 1 个局部倒置。
```
示例 2:
```
输入:nums = [1,2,0]

输出:false

解释:有 2 个全局倒置,和 1 个局部倒置。
```

提示:
* $n = nums.length$
* $1 <= n <= 10^5$
* $0 <= nums[i] < n$
* `nums` 中的所有整数 互不相同
* `nums` 是范围 $[0, n - 1]$ 内所有数字组成的一个排列

Solution:
### 树状数组

根据题意,对于每个 $nums[i]$ 而言:

* 其左边比它大的 $nums[j]$ 的个数,是以 $nums[i]$ 为右端点的“全局倒置”数量,统计所有以 $nums[i]$ 为右端点的“全局倒置”数量即是总的“全局倒置”数量 `a`

* 同时我们可以将每个 $nums[i]$ 与前一个值进行比较,从而统计总的“局部倒置”数量 `b`,其中 $i$ 的取值范围为 $[1, n - 1)$

一个容易想到的做法是利用「树状数组」,虽然该做法没有利用到核心条件「$nums$ 是一个 $[0, n - 1]$ 的排列」,但根据数据范围 $n$ 可知该复杂度为 $O(n\log{n})$ 的做法可过,且依赖的条件更少,适用范围更广。

Java 代码:
```Java
class Solution {
    int n;
    int[] tr;
    int lowbit(int x) {
        return x & -x;
    }
    void add(int x) {
        for (int i = x; i <= n; i += lowbit(i)) tr[i]++;
    }
    int query(int x) {
        int ans = 0;
        for (int i = x; i > 0; i -= lowbit(i)) ans += tr[i];
        return ans;
    }
    public boolean isIdealPermutation(int[] nums) {
        n = nums.length;
        tr = new int[n + 10];
        add(nums[0] + 1);
        int a = 0, b = 0;
        for (int i = 1; i < n; i++) {
            a += query(n) - query(nums[i] + 1);
            b += nums[i] < nums[i - 1] ? 1 : 0;
            add(nums[i] + 1);
        }
        return a == b;
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    int n;
    vector<int> tr;
    int lowbit(int x) {
        return x & -x;
    }
    void add(int x) {
        for (int i = x; i <= n; i += lowbit(i)) tr[i]++;
    }
    int query(int x) {
        int ans = 0;
        for (int i = x; i > 0; i -= lowbit(i)) ans += tr[i];
        return ans;
    }
    bool isIdealPermutation(vector<int>& nums) {
        n = nums.size();
        tr.resize(n + 10);
        add(nums[0] + 1);
        long a = 0, b = 0;
        for (int i = 1; i < n; i++) {
            a += query(n) - query(nums[i] + 1);
            b += nums[i] < nums[i - 1] ? 1 : 0;
            add(nums[i] + 1);
        }
        return a == b;
    }
};
```
Python 代码:
```Python
class Solution:
    def lowbit(self, x):
        return x & -x

    def add(self, tr, x, n):
        i = x
        while i <= n:
            tr[i] += 1
            i += self.lowbit(i)

    def query(self, tr, x):
        i, ans = x, 0
        while i > 0:
            ans += tr[i]
            i -= self.lowbit(i)
        return ans

    def isIdealPermutation(self, nums: List[int]) -> bool:
        n = len(nums)
        tr = [0] * (n + 10)
        self.add(tr, nums[0] + 1, n)
        a, b = 0, 0
        for i in range(1, n):
            a += self.query(tr, n) - self.query(tr, nums[i] + 1)
            b += nums[i] < nums[i - 1]
            self.add(tr, nums[i] + 1, n)
        return a == b
```
* 时间复杂度:$O(n\log{n})$
* 空间复杂度:$O(n)$

---

### 数学

解法一中并没有利用到核心条件「$nums$ 是一个 $[0, n - 1]$ 的排列」,我们可以从该条件以及两类倒置的定义出发进行分析。

#### 提示一:由“局部倒置”组成的集合为由“全局倒置”组成的集合的子集

任意一个“局部倒置”均满足“全局倒置”的定义,因此要判定两者数量是否相同,可转换为统计是否存在「不满足“局部倒置”定义的“全局倒置”」。

#### 提示二:何为不满足“局部倒置”定义的“全局倒置”

结合题意,若存在坐标 $j$ 和 $i$,满足 $nums[j] > nums[i]$ 且 $j + 1 < i$,那么该倒置满足“全局倒置”定义,且不满足“局部倒置”定义。

若存在这样的逆序对,不满足,则有两类倒置数量不同。

#### 提示三:考虑「如何构造」或「如何避免构造」不满足“全局倒置”定义的“局部倒置”

如果我们能够总结出「如何构造」或「如何避免构造」一个不满足“全局倒置”定义的“局部倒置” 所需的条件,问题可以转换为检查 `nums` 是否满足这样的条件,来得知 `nums` 是否存在不满足“全局倒置”定义的“局部倒置”。

我们可以结合「$nums$ 是一个 $[0, n - 1]$ 的排列」来分析,若需要避免所有 $nums[j] > nums[i]$ 的逆序对均不满足 $j + 1 < i$,只能是所有逆序对均由相邻数值产生。

Java 代码:
```Java
class Solution {
    public boolean isIdealPermutation(int[] nums) {
        for (int i = 0; i < nums.length; i++) {
            if (Math.abs(nums[i] - i) >= 2) return false;
        }
        return true;
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    bool isIdealPermutation(vector<int>& nums) {
        for (int i = 0; i < nums.size(); ++i) {
            if (abs(nums[i] - i) >= 2) return false;
        }
        return true;
    }
};
```
Python 代码:
```Python
class Solution:
    def isIdealPermutation(self, nums: List[int]) -> bool:
        for i in range(len(nums)):
            if abs(nums[i] - i) >= 2:
                return False
        return True
```
TypeScript 代码:
```TypeScript
function isIdealPermutation(nums: number[]): boolean {
    for (let i = 0; i < nums.length; i++) {
        if (Math.abs(nums[i] - i) >= 2) return false;
    }
    return true;
};
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


LeetCode/771-780/777. 在LR字符串中交换相邻字符（中等）.md

777. 在LR字符串中交换相邻字符

Question:
在一个由 `'L'` ,`'R'` 和 `'X'` 三个字符组成的字符串(例如 `"RXXLRXRXL"`)中进行移动操作。

一次移动操作指用一个 `"LX"` 替换一个 `"XL"`,或者用一个 `"XR"` 替换一个 `"RX"`。

现给定起始字符串 `start` 和结束字符串 `end`,请编写代码,当且仅当存在一系列移动操作使得 `start` 可以转换成 `end` 时, 返回 `True`。

示例 :
```
输入: start = "RXXLRXRXL", end = "XRLXXRRLX"

输出: True

解释:
我们可以通过以下几步将start转换成end:
RXXLRXRXL ->
XRXLRXRXL ->
XRLXRXRXL ->
XRLXXRRXL ->
XRLXXRRLX
```

提示:
* $1 <= len(start) = len(end) <= 10000$
* `start` 和 `end` 中的字符串仅限于 `'L'`, `'R'` 和 `'X'`

Solution:
### 双指针

根据题意,我们每次移动要么是将 `XL` 变为 `LX`,要么是将 `RX` 变为 `XR`,而该两者操作可分别看做将 `L` 越过多个 `X` 向左移动,将 `R` 越过多个 `X` 向右移动。

因此在 `start` 和 `end` 中序号相同的 `L` 和 `R` 必然满足坐标性质:

1. 序号相同的 `L` : `start` 的下标不小于 `end` 的下标(即 `L` 不能往右移动)
2. 序号相同的 `R` : `start` 的下标不大于 `end` 的下标(即 `R` 不能往左移动)

其中「序号」是指在 `LR` 字符串中出现的相对顺序。

Java 代码:
```Java
class Solution {
    public boolean canTransform(String start, String end) {
        int n = start.length(), i = 0, j = 0;
        while (i < n || j < n) {
            while (i < n && start.charAt(i) == 'X') i++;
            while (j < n && end.charAt(j) == 'X') j++;
            if (i == n || j == n) return i == j;
            if (start.charAt(i) != end.charAt(j)) return false;
            if (start.charAt(i) == 'L' && i < j) return false;
            if (start.charAt(i) == 'R' && i > j) return false;
            i++; j++;
        }
        return i == j;
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    bool canTransform(string start, string end) {
        int n = start.size();
        int i = 0, j = 0;
        while (i < n || j < n) {
            while (i < n && start[i] == 'X') i++;
            while (j < n && end[j] == 'X') j++;
            if (i == n || j == n) return i == j;
            if (start[i] != end[j]) return false;
            if (start[i] == 'L' && i < j) return false;
            if (start[i] == 'R' && i > j) return false;
            i++; j++;
        }
        return i == j;
    }
};
```
Python 代码:
```Python
class Solution:
    def canTransform(self, start: str, end: str) -> bool:
        i, j, n = 0, 0, len(start)
        while i < n or j < n:
            while i < n and start[i] == 'X': i += 1
            while j < n and end[j] == 'X': j += 1
            if i == n or j == n: return i == j
            if start[i] != end[j]: return False
            if start[i] == 'L' and i < j: return False
            if start[i] == 'R' and i > j: return False
            i, j = i + 1, j + 1
        return i == j
```
TypeScript 代码:
```TypeScript
function canTransform(start: string, end: string): boolean {
    let n = start.length;
    let i = 0, j = 0;
    while (i < n || j < n) {
        while (i < n && start.charAt(i) === 'X') i++;
        while (j < n && end.charAt(j) === 'X') j++;
        if (i === n || j === n) return i === j;
        if (start.charAt(i) !== end.charAt(j)) return false;
        if (start.charAt(i) === 'L' && i < j) return false;
        if (start.charAt(i) === 'R' && i > j) return false;
        i++; j++;
    }
    return i === j;
};
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


778. 水位上升的泳池中游泳

Question:
在一个 `N x N` 的坐标方格 `grid` 中,每一个方格的值 $grid[i][j]$ 表示在位置 $(i,j)$ 的平台高度。

现在开始下雨了。当时间为 $t$ 时,此时雨水导致水池中任意位置的水位为 $t$ 。

你可以从一个平台游向四周相邻的任意一个平台,但是前提是此时水位必须同时淹没这两个平台。

假定你可以瞬间移动无限距离,也就是默认在方格内部游动是不耗时的。

当然,在你游泳的时候你必须待在坐标方格里面。

你从坐标方格的左上平台 $(0, 0)$ 出发,最少耗时多久你才能到达坐标方格的右下平台 $(N-1,N-1)$?

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

输出: 3

解释:
时间为0时,你位于坐标方格的位置为 (0, 0)。
此时你不能游向任意方向,因为四个相邻方向平台的高度都大于当前时间为 0 时的水位。
等时间到达 3 时,你才可以游向平台 (1, 1). 因为此时的水位是 3,坐标方格中的平台没有比水位 3 更高的,所以你可以游向坐标方格中的任意位置
```
示例2:
```
输入: [[0,1,2,3,4],[24,23,22,21,5],[12,13,14,15,16],[11,17,18,19,20],[10,9,8,7,6]]
输出: 16

解释:
 0  1  2  3  4
             5
12 13 14 15 16
11
10  9  8  7  6
```

提示:
* 2 <= N <= 50.
* grid[i][j] 是 `[0, ..., N*N - 1]` 的排列。

Solution:
### Kruskal

由于在任意点可以往任意方向移动,所以相邻的点(四个方向)之间存在一条无向边。

边的权重 $w$ 是指两点节点中的最大高度。

按照题意,我们需要找的是从左上角点到右下角点的最优路径,其中最优路径是指**途径的边的最大权重值最小**,然后输入最优路径中的最大权重值。

我们可以先遍历所有的点,将所有的边加入集合,存储的格式为数组 $[a, b, w]$ ,代表编号为 $a$ 的点和编号为 $b$ 的点之间的权重为 $w$(按照题意,$w$ 为两者的最大高度)。

对集合进行排序,按照 $w$ 进行从小到达排序。

当我们有了所有排好序的候选边集合之后,我们可以对边从前往后处理,每次加入一条边之后,使用并查集来查询左上角的点和右下角的点是否连通。

当我们的合并了某条边之后,判定左上角和右下角的点联通,那么该边的权重即是答案。

这道题和前天的 [1631. 最小体力消耗路径](https://mp.weixin.qq.com/s?__biz=MzU4NDE3MTEyMA==&mid=2247486139&idx=1&sn=37083315de3c6a7e1b675e0363ee7d98&chksm=fd9ca1a4caeb28b220eba888ebc773b460d41123a1042e3b01cfd2001ddfb487ff2a3959f3b9&token=990510480&lang=zh_CN#rd) 几乎是完全一样的思路。

你甚至可以将那题的代码拷贝过来,改一下对于 $w$ 的定义即可。

代码:
```Java
class Solution {
    int n;
    int[] p;
    void union(int a, int b) {
        p[find(a)] = p[find(b)];
    }
    boolean query(int a, int b) {
        return find(a) == find(b);
    }
    int find(int x) {
        if (p[x] != x) p[x] = find(p[x]);
        return p[x];
    }

    public int swimInWater(int[][] grid) {
        n = grid.length;

        // 初始化并查集
        p = new int[n * n];
        for (int i = 0; i < n * n; i++) p[i] = i;

        // 预处理出所有的边
        // edge 存的是 [a, b, w]:代表从 a 到 b 所需要的时间为 w
        // 虽然我们可以往四个方向移动,但是只要对于每个点都添加「向右」和「向下」两条边的话,其实就已经覆盖了所有边了
        List<int[]> edges =  new ArrayList<>();
        for (int i = 0; i < n ;i++) {
            for (int j = 0; j < n; j++) {
                int idx = getIndex(i, j);
                p[idx] = idx;
                if (i + 1 < n) {
                    int a = idx, b = getIndex(i + 1, j);
                    int w = Math.max(grid[i][j], grid[i + 1][j]);
                    edges.add(new int[]{a, b, w});
                }
                if (j + 1 < n) {
                    int a = idx, b = getIndex(i, j + 1);
                    int w = Math.max(grid[i][j], grid[i][j + 1]);
                    edges.add(new int[]{a, b, w});
                }
            }
        }

        // 根据权值 w 升序
        Collections.sort(edges, (a,b)->a[2]-b[2]);

        // 从「小边」开始添加,当某一条边别应用之后,恰好使用得「起点」和「结点」联通
        // 那么代表找到了「最短路径」中的「权重最大的边」
        int start = getIndex(0, 0), end = getIndex(n - 1, n - 1);
        for (int[] edge : edges) {
            int a = edge[0], b = edge[1], w = edge[2];
            union(a, b);
            if (query(start, end)) {
                return w;
            }
        }
        return 0;
    }
    int getIndex(int i, int j) {
        return i * n + j;
    }
}
```
节点的数量为 $n * n$,无向边的数量严格为 $2 * n * (n - 1)$,数量级上为 $n^2$。

* 时间复杂度:获取所有的边复杂度为 $O(n^2)$,排序复杂度为 $O(n^2\log{n})$,遍历得到最终解复杂度为 $O(n^2)$。整体复杂度为 $O(n^2\log{n})$。
* 空间复杂度:使用了并查集数组。复杂度为 $O(n^2)$。

注意:假定 Collections.sort() 使用 Arrays.sort() 中的双轴快排实现。

---

### 二分 + BFS/DFS

在与本题类型的 [1631. 最小体力消耗路径](https://mp.weixin.qq.com/s?__biz=MzU4NDE3MTEyMA==&mid=2247486139&idx=1&sn=37083315de3c6a7e1b675e0363ee7d98&chksm=fd9ca1a4caeb28b220eba888ebc773b460d41123a1042e3b01cfd2001ddfb487ff2a3959f3b9&token=990510480&lang=zh_CN#rd)中,有同学问到是否可以用「二分」。

答案是可以的。

题目给定了 $grid[i][j]$ 的范围是 $[0, n^2 - 1]$,所以答案必然落在此范围。

假设最优解为 $min$ 的话(恰好能到达右下角的时间)。那么小于 $min$ 的时间无法到达右下角,大于 $min$ 的时间能到达右下角。

*因此在以最优解 $min$ 为分割点的数轴上具有两段性,可以通过「二分」来找到分割点 $min$。*

> 注意:「二分」的本质是两段性,并非单调性。只要一段满足某个性质,另外一段不满足某个性质,就可以用「二分」。其中 [33. 搜索旋转排序数组](https://mp.weixin.qq.com/s?__biz=MzU4NDE3MTEyMA==&mid=2247486020&idx=2&sn=9ada4b6e7eccecddbd8faa18f14c4eeb&chksm=fd9ca15bcaeb284d727905c174624e32b39b196e2337a2bfc791b22fcce35eb543b552031530&token=990510480&lang=zh_CN#rd) 是一个很好的说明例子。

接着分析,假设最优解为 $min$,我们在 $[l, r]$ 范围内进行二分,当前二分到的时间为 $mid$ 时:

1. 能到达右下角:必然有 $min \leqslant mid$,让 $r = mid$

2. 不能到达右下角:必然有 $min > mid$,让 $l = mid + 1$

**当确定了「二分」逻辑之后,我们需要考虑如何写 $check$ 函数。**

显然 $check$ 应该是一个判断给定 时间/步数 能否从「起点」到「终点」的函数。

我们只需要按照规则走特定步数,边走边检查是否到达终点即可。

实现 $check$ 既可以使用 DFS 也可以使用 BFS。两者思路类似,这里就只以 BFS 为例。

代码:

```Java
class Solution {
    int[][] dirs = new int[][]{{1,0}, {-1,0}, {0,1}, {0,-1}};
    public int swimInWater(int[][] grid) {
        int n = grid.length;
        int l = 0, r = n * n;
        while (l < r) {
            int mid = l + r >> 1;
            if (check(grid, mid)) {
                r = mid;
            } else {
                l = mid + 1;
            }
        }
        return r;
    }
    boolean check(int[][] grid, int time) {
        int n = grid.length;
        boolean[][] visited = new boolean[n][n];
        Deque<int[]> queue = new ArrayDeque<>();
        queue.addLast(new int[]{0, 0});
        visited[0][0] = true;
        while (!queue.isEmpty()) {
            int[] pos = queue.pollFirst();
            int x = pos[0], y = pos[1];
            if (x == n - 1 && y == n - 1) return true;

            for (int[] dir : dirs) {
                int newX = x + dir[0], newY = y + dir[1];
                int[] to = new int[]{newX, newY};
                if (inArea(n, newX, newY) && !visited[newX][newY] && canMove(grid, pos, to, time)) {
                    visited[newX][newY] = true;
                    queue.addLast(to);
                }
            }
        }
        return false;
    }
    boolean inArea(int n, int x, int y) {
        return x >= 0 && x < n && y >= 0 && y < n;
    }
    boolean canMove(int[][] grid, int[] from, int[] to, int time) {
        return time >= Math.max(grid[from[0]][from[1]], grid[to[0]][to[1]]);
    }
}
```
* 时间复杂度:在 $[0, n^2]$ 范围内进行二分,复杂度为 $O(\log{n})$;每一次 BFS 最多有 $n^2$ 个节点入队,复杂度为 $O(n^2)$。整体复杂度为 $O({n^2}\log{n})$
* 空间复杂度:使用了 visited 数组。复杂度为 $O(n^2)$

### 最后

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

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

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

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


779. 第K个语法符号

Question:
我们构建了一个包含 `n` 行( 索引从 `1`  开始 )的表。首先在第一行我们写上一个 `0`。接下来的每一行,将前一行中的 `0` 替换为 `01`,`1` 替换为 `10`。

* 例如,对于 `n = 3`,第 `1` 行是 `0` ,第 `2` 行是 `01`,第 `3` 行是 `0110` 。

给定行数 `n` 和序数 `k`,返回第 `n` 行中第 `k` 个字符。( `k` 从索引 `1` 开始)

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

输出: 0

解释: 第一行:0
```
示例 2:
```
输入: n = 2, k = 1

输出: 0

解释:
第一行: 0
第二行: 01
```
示例 3:
```
输入: n = 2, k = 2

输出: 1

解释:
第一行: 0
第二行: 01
```

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

Solution:
### 递归(倒推验证)

整理一下条件:首行为 `0`,每次用当前行拓展出下一行时,字符数量翻倍(将 `0` 拓展为 `01`,将 `1` 拓展为 `10`),且字符种类仍为 `01`。

要求我们输出第 $n$ 行第 $k$ 列的字符,我们可以通过「倒推验证」的方式来求解:假设第 $n$ 行第 $k$ 为 `1`,若能倒推出首行为 $0$,说明假设成立,返回 `1`,否则返回 `0`。

倒推验证可通过实现递归函数 `int dfs(int r, int c, int cur)` 来做,含义为当第 $r$ 行第 $c$ 列的字符为 $cur$ 时,首行首列字符为何值。同时实现该函数是容易的:

* 若「当前列 $c$ 为偶数且 $cur = 0$」或「当前列 $c$ 为奇数且 $cur = 1$」时,说明当前列所在的组为 `10`,由此可推出其是由上一行的 `1` 拓展而来,结合每次拓展新行字符数量翻倍的条件,可知是由第 $r - 1$ 行的第 $\left \lfloor \frac{c - 1}{2} \right \rfloor + 1$ 列的 `1` 拓展而来,递归处理;
* 否则,同理,必然是上一行(第 $r - 1$ 行)对应位置的 `0` 拓展而来,递归处理。

最终,当倒推到首行时,我们找到了递归出口,直接返回 `cur`。

Java 代码:
```Java
class Solution {
    public int kthGrammar(int n, int k) {
        return dfs(n, k, 1) == 0 ? 1 : 0;
    }
    int dfs(int r, int c, int cur) {
        if (r == 1) return cur;
        if ((c % 2 == 0 && cur == 0) || (c % 2 == 1 && cur == 1)) return dfs(r - 1, (c - 1) / 2 + 1, 1);
        else return dfs(r - 1, (c - 1) / 2 + 1, 0);
    }
}
```
TypeScript 代码:
```TypeScript
function kthGrammar(n: number, k: number): number {
    function dfs(r: number, c: number, cur: number): number {
        if (r == 1) return cur
        if ((c % 2 == 0 && cur == 0) || (c % 2 == 1 && cur == 1)) return dfs(r - 1, Math.floor((c - 1) / 2) + 1, 1)
        else return dfs(r - 1, Math.floor((c - 1) / 2) + 1, 0)
    }
    return dfs(n, k, 1) == 0 ? 1 : 0
}
```
Python 代码:
```Python
class Solution:
    def kthGrammar(self, n: int, k: int) -> int:
        def dfs(r, c, cur):
            if r == 1:
                return cur
            if (c % 2 == 0 and cur == 0) or (c % 2 == 1 and cur == 1):
                return dfs(r - 1, (c - 1) // 2 + 1, 1)
            else:
                return dfs(r - 1, (c - 1) // 2 + 1, 0)
        return 1 if dfs(n, k, 1) == 0 else 0
```
* 时间复杂度:$O(n)$
* 空间复杂度:忽略递归带来的额外空间开销,复杂度为 $O(1)$

### 最后

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

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

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

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


Question:
给定四个整数 `sx`,`sy`,`tx` 和 `ty`,如果通过一系列的转换可以从起点 $(sx, sy)$ 到达终点 $(tx, ty)$,则返回 `true`,否则返回 `false`。

从点 $(x, y)$ 可以转换到 $(x, x+y)$  或者 $(x+y, y)$。

示例 1:
```
输入: sx = 1, sy = 1, tx = 3, ty = 5

输出: true

解释:
可以通过以下一系列转换从起点转换到终点:
(1, 1) -> (1, 2)
(1, 2) -> (3, 2)
(3, 2) -> (3, 5)
```
示例 2:
```
输入: sx = 1, sy = 1, tx = 2, ty = 2

输出: false
```
示例 3:
```
输入: sx = 1, sy = 1, tx = 1, ty = 1

输出: true
```

提示:
* $1 <= sx, sy, tx, ty <= 10^9$

Solution:
### 数学

给定的 $(sx, sy)$ 的数据范围为 $[1, 10^9]$(即均为正整数),且每次转换,只能将另外一维的数值累加到当前维,因此对于每一维的数值而言,随着转换次数的进行,呈(非严格)递增趋势,再结合起始值为正整数,可知在转换过程中均不会出现负数。

**由此得知从 $(tx, ty)$ 到 $(sx, sy)$ 的转换过程唯一确定:总是取较大数减去较小数来进行反推(否则会出现负数)。**

但即使反向转换唯一确定,数据范围为 $10^9$,线性模拟仍会超时。

我们考虑将「相同操作的连续段转换动作」进行合并,在某次反向转换中,如果有 $tx < ty$,我们会将 $(tx, ty)$ 转换为 $(tx, ty - tx)$,若相减完仍有 $tx < ty - tx$,该操作会继续进行,得到 $(tx, ty - 2 * tx)$,直到不满足 $tx < ty - k * tx$,其中 $k$ 为转换次数。

即对于一般性的情况而言,$(tx, ty)$ 中的较大数会一直消减到「与较小数的余数」为止。

因此我们可以先使用 $O(\log{max(tx, ty)})$ 的复杂度将其消减到不超过 $(sx, sy)$ 为止。此时如果消减后的结果 $(tx, ty)$ 任一维度小于 $(sx, sy)$,必然不能进行转换,返回 `False`;如果任一维度相等(假定是 $x$ 维度),则检查另一维度($y$ 维度)的差值,能够由当前维度($x$ 维度)拼凑而来。

代码:
```Java
class Solution {
    public boolean reachingPoints(int sx, int sy, int tx, int ty) {
        while (sx < tx && sy < ty) {
            if (tx < ty) ty %= tx;
            else tx %= ty;
        }
        if (tx < sx || ty < sy) return false;
        return sx == tx ? (ty - sy) % tx == 0 : (tx - sx) % ty == 0;
    }
}
```
* 时间复杂度:$O(\log{\max(tx, ty)})$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


Question:
森林中,每个兔子都有颜色。其中一些兔子(可能是全部)告诉你还有多少其他的兔子和自己有相同的颜色。我们将这些回答放在 `answers` 数组里。

返回森林中兔子的最少数量。

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

输出: 5

解释:
两只回答了 "1" 的兔子可能有相同的颜色,设为红色。
之后回答了 "2" 的兔子不会是红色,否则他们的回答会相互矛盾。
设回答了 "2" 的兔子为蓝色。
此外,森林中还应有另外 2 只蓝色兔子的回答没有包含在数组中。
因此森林中兔子的最少数量是 5: 3 只回答的和 2 只没有回答的。

输入: answers = [10, 10, 10]
输出: 11

输入: answers = []
输出: 0
```

说明:
* `answers` 的长度最大为 $1000$
* `answers[i]` 是在 $[0, 999]$ 范围内的整数。

Solution:
### 基本分析

首先,兔子它不会说谎 (`・ω・ ́),因此我们可以得出以下结论:

* **同一颜色的兔子回答的数值必然是一样的**
* **但回答同样数值的,不一定就是同颜色兔子**

举个🌰,假如有 3 只白兔,每只白兔的回答必然都是 2(对应结论 1);但假如有兔子回答了数值 2,可能只是三只白兔,也可能是三只白兔和三只灰兔同时进行了回答(对应结论 2)。

**答案要我们求最少的兔子数量。**

不妨设有某种颜色的兔子 $m$ 只,它们回答的答案数值为 $cnt$,那么 $m$ 和 $cnt$ 满足什么关系?

显然两者关系为 $m = cnt + 1$。

但如果是在 $answers$ 数组里,回答 $cnt$ 的数量为 $t$ 的话呢?这时候我们需要分情况讨论:

* $t \leqslant cnt + 1$ : 为达到「最少的兔子数量」,我们可以假定这 $t$ 只兔子为同一颜色,这样能够满足题意,同时不会导致「额外」兔子数量增加(颜色数量最少)。
* $t > cnt + 1$ : 我们知道回答 $cnt$ 的兔子应该有 $cnt + 1$ 只。这时候说明有数量相同的不同颜色的兔子进行了回答。为达到「最少的兔子数量」,我们应当将 $t$ 分为若干种颜色,并尽可能让某一种颜色的兔子为 $cnt + 1$ 只,这样能够满足题意,同时不会导致「额外」兔子数量增加(颜色数量最少)。

**换句话说,我们应该让「同一颜色的兔子数量」尽量多,从而实现「总的兔子数量」最少。**

***

### 证明

我们来证明一下,为什么这样的贪心思路是对的:

基于上述分析,我们其实是在处理 $answers$ 数组中每一个 $cnt$ ,使得满足题意的前提下,数值 $cnt$ 带来的影响(总的兔子数量,或者说总的颜色数量)最小。

首先 $answers$ 中的每个数都会导致我们总的兔子数量增加,**因此我们应该「让 $answers$ 的数字尽可能变少」,也就是我们需要去抵消掉 $answers$ 中的一些数字。**

对于 $answers$ 中的某个 $cnt$ 而言(注意是某个 $cnt$,含义为一只兔子的回答),必然代表了有 $cnt + 1$ 只兔子,同时也代表了数值 $cnt$ 最多在 $answers$ 数组中出现 $cnt + 1$ 次(与其颜色相同的兔子都进行了回答)。

这时候我们可以从数组中移走 $cnt + 1$ 个数值 $cnt$(如果有的话)。

**当每次处理 $cnt$ 的时候,我们都执行这样的抵消操作。最后得到的 $answers$ 数值数量必然最少(而固定),抵消完成后的 $answers$ 中的每个 $cnt$ 对答案的影响也固定(增加 $cnt + 1$),从而实现「总的兔子数量」最少。**

**相反,如果不执行这样的操作的话,得到的 $answers$ 数值数量必然会更多,「总的兔子数量」也必然会更多,也必然不会比这样做法更优。**

***

### 模拟解法

按照上述思路,我们可以先对 $answers$ 进行排序,然后根据遍历到某个 $cnt$ 时,将其对答案的影响应用到 $ans$ 中(`ans += cnt + 1`),并将后面的 $cnt$ 个 $cnt$ 进行忽略。

代码:
```Java
class Solution {
    public int numRabbits(int[] cs) {
        Arrays.sort(cs);
        int n = cs.length;
        int ans = 0;
        for (int i = 0; i < n; i++) {
            int cnt = cs[i];
            ans += cnt + 1;
            // 跳过「数值 cnt」后面的 cnt 个「数值 cnt」
            int k = cnt;
            while (k-- > 0 && i + 1 < n && cs[i] == cs[i + 1]) i++;
        }
        return ans;
    }
}
```
* 时间复杂度:$O(n\log{n})$
* 空间复杂度:$O(1)$

***

### 统计分配

我们也可以先对所有出现过的数字进行统计,然后再对数值进行(颜色)分配。

代码:
```Java
class Solution {
    int N = 1009;
    int[] counts = new int[N];
    public int numRabbits(int[] cs) {
        // counts[x] = cnt 代表在 cs 中数值 x 的数量为 cnt
        for (int i : cs) counts[i]++;
        int ans = counts[0];
        for (int i = 1; i < N; i++) {
            int per = i + 1;
            int cnt = counts[i];
            int k = cnt / per;
            if (k * per < cnt) k++;
            ans += k * per;
        }
        return ans;
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n)$

***

### 拓展

保持题目现有的条件不变,假定颜色相同的兔子至少有一只发声,问题改为「问兔子颜色数量可能有多少种」,又该如何求解呢?

### 最后

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

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

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

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


Question:
一个 `n x n` 的二维网络 `board` 仅由 `0` 和 `1` 组成 。每次移动,你能任意交换两列或是两行的位置。

返回 将这个矩阵变为  “棋盘”  所需的最小移动次数 。如果不存在可行的变换,输出 `-1`。

“棋盘” 是指任意一格的上下左右四个方向的值均与本身不同的矩阵。

示例 1:

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

输出: 2

解释:一种可行的变换方式如下,从左到右:
第一次移动交换了第一列和第二列。
第二次移动交换了第二行和第三行。
```
示例 2:

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

输出: 0

解释: 注意左上角的格值为0时也是合法的棋盘,也是合法的棋盘.
```
示例 3:

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

输出: -1

解释: 任意的变换都不能使这个输入变为合法的棋盘。
```

提示:
* $n == board.length$
* $n == board[i].length$
* $2 <= n <= 30$
* `board[i][j]` 将只包含 `0`或 `1`

Solution:
### 构造分析

数据范围具有一定的迷惑性,但其并不是一个棋盘搜索问题。

我们需要考虑何种情况下无解,以及有解情况的最小步数。

在给定棋盘大小 $n$ 的前提下,所能构造的合法棋盘只有两种情况:首个格子为 $0$ 或首个格子为 $1$,即问题转化为能否构造出合法棋盘,以及构造哪种合法棋盘所用步数更小。

同时,**交换行和交换列均不会影响行的种类数量和列的种类数量**,因此我们可以得到第一个判断无解的条件:若起始棋盘的行/列种类数不为 $2$,必然无法构造出合法棋盘。

假设起始的行分别为 `r1` 和 `r2`,起始的列分别为 `c1` 和 `c2`。不难发现第二性质:**若能构成合法棋盘,`r1` 和 `r2` 中 $0$ 和 $1$ 的数量必然相等,`c1` 和 `c2` 中的 $0$ 和 $1$ 的数量必然相等**。

同时由于交换行和交换列具有对称性和独立性,我们可以先使用「交换列」来进行分析,交换列不会导致行种类发生变化,但会导致行的数值分布发生变化。

因此第二性质可拓展为:**`r1` 和 `r2` 对称位置为必然不同,`c1` 和 `c2` 对称位置必然不同,即两者异或结果为必然为 $(111...111)_2$,即为 `mask = (1 << n) - 1`,否则必然无解。**

若上述两性质满足,可能有解。

由于 `r1` 和 `r2` 及 `c1` 和 `c2` 对称位置必然不同,因此我们调整好 `r1` 后,`r2` 唯一确定(`c1` 和 `c2` 同理),同时构造其中一种间隔行为 `t` = $(...101)_2$,根据合法棋盘定义可知要么是将首行调整为 $t$,要么是将次行调整为 $t$。

我们设置函数 `int getCnt(int a, int b)` 计算将 `a` 变为 `b` 所需要的最小转换次数,两状态转换所需次数为不同位个数除以 $2$(一次交换可实现消除两个不同位)。

分别计算「将 `r1` 和 `r2` 转换为 `t` 所需步数」和「将 `c1` 和 `c2` 转换为 `t` 所需步数」,两者之和即为答案。

Java 代码:
```Java
class Solution {
    int n = 0, INF = 0x3f3f3f3f;
    int getCnt(int a, int b) {
        return Integer.bitCount(a) != Integer.bitCount(b) ? INF : Integer.bitCount(a ^ b) / 2;
    }
    public int movesToChessboard(int[][] g) {
        n = g.length;
        int r1 = -1, r2 = -1, c1 = -1, c2 = -1, mask = (1 << n) - 1;
        for (int i = 0; i < n; i++) {
            int a = 0, b = 0;
            for (int j = 0; j < n; j++) {
                if (g[i][j] == 1) a += (1 << j);
                if (g[j][i] == 1) b += (1 << j);
            }
            if (r1 == -1) r1 = a;
            else if (r2 == -1 && a != r1) r2 = a;
            if (c1 == -1) c1 = b;
            else if (c2 == -1 && b != c1) c2 = b;
            if (a != r1 && a != r2) return -1;
            if (b != c1 && b != c2) return -1;
        }
        if (Integer.bitCount(r1) + Integer.bitCount(r2) != n) return -1;
        if (Integer.bitCount(c1) + Integer.bitCount(c2) != n) return -1;
        if ((r1 ^ r2) != mask || (c1 ^ c2) != mask) return -1;
        int t = 0;
        for (int i = 0; i < n; i += 2) t += (1 << i);
        int ans = Math.min(getCnt(r1, t), getCnt(r2, t)) + Math.min(getCnt(c1, t), getCnt(c2, t));
        return ans >= INF ? -1 : ans;
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    int n = 0, INF = 0x3f3f3f3f;
    int getCnt(int a, int b) {
        return __builtin_popcount(a) != __builtin_popcount(b) ? INF : (__builtin_popcount(a ^ b) / 2);
    }
    int movesToChessboard(vector<vector<int>>& g) {
        n = g.size();
        int r1 = -1, r2 = -1, c1 = -1, c2 = -1, mask = (1 << n) - 1;
        for (int i = 0; i < n; i++) {
            int a = 0, b = 0;
            for (int j = 0; j < n; j++) {
                if (g[i][j] == 1) a |= (1 << j);
                if (g[j][i] == 1) b |= (1 << j);
            }
            if (r1 == -1) r1 = a;
            else if (r2 == -1 && a != r1) r2 = a;
            if (c1 == -1) c1 = b;
            else if (c2 == -1 && b != c1) c2 = b;
            if (a != r1 && a != r2) return -1;
            if (b != c1 && b != c2) return -1;
        }
        if (__builtin_popcount(r1) + __builtin_popcount(r2) != n) return -1;
        if (__builtin_popcount(c1) + __builtin_popcount(c2) != n) return -1;
        if ((r1 ^ r2) != mask || (c1 ^ c2) != mask) return -1;
        int t = 0;
        for (int i = 0; i < n; i += 2) t |= (1 << i);
        int ans = min(getCnt(r1, t), getCnt(r2, t)) + min(getCnt(c1, t), getCnt(c2, t));
        return (ans >= INF) ? -1 : ans;
    }
};
```
TypeScript 代码:
```TypeScript
let n: number = 0, INF = 0x3f3f3f3f
function bitCount(x: number): number {
    let ans = 0
    while (x != 0 && ++ans >= 0) x -= (x & -x)
    return ans
}
function getCnt(a: number, b: number): number {
    return bitCount(a) != bitCount(b) ? INF : bitCount(a ^ b) / 2
}
function movesToChessboard(g: number[][]): number {
    n = g.length
    let r1 = -1, r2 = -1, c1 = -1, c2 = -1, mask = (1 << n) - 1
    for (let i = 0; i < n; i++) {
        let a = 0, b = 0
        for (let j = 0; j < n; j++) {
            if (g[i][j] == 1) a += (1 << j)
            if (g[j][i] == 1) b += (1 << j)
        }
        if (r1 == -1) r1 = a
        else if (r2 == -1 && a != r1) r2 = a
        if (c1 == -1) c1 = b
        else if (c2 == -1 && b != c1) c2 = b
        if (a != r1 && a != r2) return -1
        if (b != c1 && b != c2) return -1
    }
    if (bitCount(r1) + bitCount(r2) != n) return -1
    if (bitCount(c1) + bitCount(c2) != n) return -1
    if ((r1 ^ r2) != mask || (c1 ^ c2) != mask) return -1
    let t = 0
    for (let i = 0; i < n; i += 2) t += (1 << i)
    const ans = Math.min(getCnt(r1, t), getCnt(r2, t)) + Math.min(getCnt(c1, t), getCnt(c2, t))
    return ans >= INF ? -1 : ans
};
```
* 时间复杂度:$O(n^2)$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


LeetCode/781-790/783. 二叉搜索树节点最小距离（简单）.md

783. 二叉搜索树节点最小距离

Question:
给你一个二叉搜索树的根节点 root ,返回 树中任意两不同节点值之间的最小差值 。

注意:本题与 530:https://leetcode-cn.com/problems/minimum-absolute-difference-in-bst/ 相同

示例 1:

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

输出:1
```
示例 2:

```
输入:root = [1,0,48,null,null,12,49]

输出:1
```

提示:
* 树中节点数目在范围 [2, 100] 内
* 0 <= Node.val <= $10^5$
* 差值是一个正数,其数值等于两值之差的绝对值

Solution:
### 朴素解法(BFS & DFS)

如果不考虑利用二叉搜索树特性的话,一个朴素的做法是将所有节点的 $val$ 存到一个数组中。

对数组进行排序,并获取答案。

将所有节点的 $val$ 存入数组,可以使用 BFS 或者 DFS。

代码:
```java []
class Solution {
    public int minDiffInBST(TreeNode root) {
        List<Integer> list = new ArrayList<>();

        // BFS
        Deque<TreeNode> d = new ArrayDeque<>();
        d.addLast(root);
        while (!d.isEmpty()) {
            TreeNode poll = d.pollFirst();
            list.add(poll.val);
            if (poll.left != null) d.addLast(poll.left);
            if (poll.right != null) d.addLast(poll.right);
        }

        // DFS
        // dfs(root, list);

        Collections.sort(list);
        int n = list.size();
        int ans = Integer.MAX_VALUE;
        for (int i = 1; i < n; i++) {
            int cur = Math.abs(list.get(i) - list.get(i - 1));
            ans = Math.min(ans, cur);
        }
        return ans;
    }
    void dfs(TreeNode root, List<Integer> list) {
        list.add(root.val);
        if (root.left != null) dfs(root.left, list);
        if (root.right != null) dfs(root.right, list);
    }
}
```
* 时间复杂度:$O(n\log{n})$
* 空间复杂度:$O(n)$

***

### 中序遍历(栈模拟 & 递归)

不难发现,在朴素解法中,我们对树进行搜索的目的是为了获取一个「有序序列」,然后从「有序序列」中获取答案。

而二叉搜索树的中序遍历是有序的,因此我们可以直接对「二叉搜索树」进行中序遍历,保存遍历过程中的相邻元素最小值即是答案。

代码:
```java []
class Solution {
    int ans = Integer.MAX_VALUE;
    TreeNode prev = null;
    public int minDiffInBST(TreeNode root) {
        // 栈模拟
        Deque<TreeNode> d = new ArrayDeque<>();
        while (root != null || !d.isEmpty()) {
            while (root != null) {
                d.addLast(root);
                root = root.left;
            }
            root = d.pollLast();
            if (prev != null) {
                ans = Math.min(ans, Math.abs(prev.val - root.val));
            }
            prev = root;
            root = root.right;
        }

        // 递归
        // dfs(root);

        return ans;
    }
    void dfs(TreeNode root) {
        if (root == null) return;
        dfs(root.left);
        if (prev != null) {
            ans = Math.min(ans, Math.abs(prev.val - root.val));
        }
        prev = root;
        dfs(root.right);
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


784. 字母大小写全排列

Question:
给定一个字符串 `s` ,通过将字符串 `s` 中的每个字母转变大小写,我们可以获得一个新的字符串。

返回 所有可能得到的字符串集合 。以 任意顺序 返回输出。

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

输出:["a1b2", "a1B2", "A1b2", "A1B2"]
```
示例 2:
```
输入: s = "3z4"

输出: ["3z4","3Z4"]
```

提示:
* $1 <= s.length <= 12$
* `s` 由小写英文字母、大写英文字母和数字组成

Solution:
### DFS

数据范围为 $12$,同时要我们求所有具体方案,容易想到使用 `DFS` 进行「爆搜」。

我们可以从前往后考虑每个 $s[i]$,根据 $s[i]$ 是否为字母进行分情况讨论:

* 若 $s[i]$ 不是字母,直接保留
* 若 $s[i]$ 是字母,则有「保留原字母」或「进行大小写转换」两种决策

设计 `DFS` 函数为 `void dfs(int idx, int n, String cur)`:其中 $n$ 固定为具体方案的长度(即原字符串长度),而 `idx` 和 `cur` 分别为当前处理到哪一个 $s[idx]$,而 `cur` 则是当前具体方案。

根据上述分析可知,当 $s[idx]$ 不为字母,将其直接追加到 `cur` 上,并决策下一个位置 $idx + 1$;而当 $s[idx]$ 为字母时,我们可以选择将 $s[idx]$ 追加到 `cur` 上(保留原字母)或是将 $s[idx]$ 进行翻转后再追加到 `cur` 上(进行大小写转换)。

最后当我们满足 `idx = n` 时,说明已经对原字符串的每一位置决策完成,将当前具体方案 `cur` 加入答案。

> 一些细节:我们可以通过与 `32` 异或来进行大小写转换

Java 代码:
```Java
class Solution {
    char[] cs;
    List<String> ans = new ArrayList<>();
    public List<String> letterCasePermutation(String s) {
        cs = s.toCharArray();
        dfs(0, s.length(), new char[s.length()]);
        return ans;
    }
    void dfs(int idx, int n, char[] cur) {
        if (idx == n) {
            ans.add(String.valueOf(cur));
            return ;
        }
        cur[idx] = cs[idx];
        dfs(idx + 1, n, cur);
        if (Character.isLetter(cs[idx])) {
            cur[idx] = (char) (cs[idx] ^ 32);
            dfs(idx + 1, n, cur);
        }
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    vector<string> ans;
    string s;
    void dfs(int idx, int n, string cur) {
        if (idx == n) {
            ans.push_back(cur);
            return;
        }
        cur.push_back(s[idx]);
        dfs(idx + 1, n, cur);
        if (isalpha(s[idx])) {
            cur[idx] = s[idx] ^ 32;
            dfs(idx + 1, n, cur);
        }
    }
    vector<string> letterCasePermutation(string s) {
        this->s = s;
        dfs(0, s.length(), "");
        return ans;
    }
};
```
Python 代码:
```Python
class Solution:
    def letterCasePermutation(self, s: str) -> List[str]:
        ans = []
        def dfs(idx, n, cur):
            if idx == n:
                ans.append(cur)
                return
            dfs(idx + 1, n, cur + s[idx])
            if 'a' <= s[idx] <= 'z' or 'A' <= s[idx] <= 'Z':
                dfs(idx + 1, n, cur + s[idx].swapcase())
        dfs(0, len(s), '')
        return ans
```
TypeScript 代码:
```TypeScript
function letterCasePermutation(s: string): string[] {
    const ans = new Array<string>()
    function dfs(idx: number, n: number, cur: string): void {
        if (idx == n) {
            ans.push(cur)
            return
        }
        dfs(idx + 1, n, cur + s[idx])
        if ((s[idx] >= 'a' && s[idx] <= 'z') || (s[idx] >= 'A' && s[idx] <= 'Z')) {
            dfs(idx + 1, n, cur + String.fromCharCode(s.charCodeAt(idx) ^ 32))
        }
    }
    dfs(0, s.length, "")
    return ans
}
```
* 时间复杂度:最坏情况下原串 `s` 的每一位均为字母(均有保留和转换两种决策),此时具体方案数量共 $2^n$ 种;同时每一种具体方案我们都需要用与原串长度相同的复杂度来进行构造。复杂度为 $O(n \times 2^n)$
* 空间复杂度:$O(n \times 2^n)$

---

### 二进制枚举

根据解法一可知,具体方案的个数与字符串 `s1` 存在的字母个数相关,当 `s1` 存在 `m` 个字母时,由于每个字母存在两种决策,总的方案数个数为 $2^m$ 个。

因此可以使用「二进制枚举」的方式来做:使用变量 `s` 代表字符串 `s1` 的字母翻转状态,`s` 的取值范围为 `[0, 1 << m)`。若 `s` 的第 $j$ 位为 `0` 代表在 `s1` 中第 $j$ 个字母不进行翻转,而当为 `1` 时则代表翻转。

每一个状态 `s` 对应了一个具体方案,通过枚举所有翻转状态 `s`,可构造出所有具体方案。

Java 代码:
```Java
class Solution {
    public List<String> letterCasePermutation(String str) {
        List<String> ans = new ArrayList<String>();
        int n = str.length(), m = 0;
        for (int i = 0; i < n; i++) m += Character.isLetter(str.charAt(i)) ? 1 : 0;
        for (int s = 0; s < (1 << m); s++) {
            char[] cs = str.toCharArray();
            for (int i = 0, j = 0; i < n; i++) {
                if (!Character.isLetter(cs[i])) continue;
                cs[i] = ((s >> j) & 1) == 1 ? (char) (cs[i] ^ 32) : cs[i];
                j++;
            }
            ans.add(String.valueOf(cs));
        }
        return ans;
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    vector<string> letterCasePermutation(string str) {
        vector<string> ans;
        int n = str.length(), m = 0;
        for (int i = 0; i < n; i++) m += isalpha(str[i]) ? 1 : 0;
        for (int s = 0; s < (1 << m); s++) {
            string cs = str;
            for (int i = 0, j = 0; i < n; i++) {
                if (!isalpha(cs[i])) continue;
                cs[i] = ((s >> j) & 1) ? (cs[i] ^ 32) : cs[i];
                j++;
            }
            ans.push_back(cs);
        }
        return ans;
    }
};
```
Python 代码:
```Python
class Solution:
    def letterCasePermutation(self, s1: str) -> List[str]:
        def isLetter(ch):
            return 'a' <= ch <= 'z' or 'A' <= ch <= 'Z'
        ans = []
        n, m = len(s1), len([ch for ch in s1 if isLetter(ch)])
        for s in range(1 << m):
            cur = ''
            j = 0
            for i in range(n):
                if not isLetter(s1[i]) or not ((s >> j) & 1):
                    cur += s1[i]
                else:
                    cur += s1[i].swapcase()
                if isLetter(s1[i]):
                    j += 1
            ans.append(cur)
        return ans
```
TypeScript 代码:
```TypeScript
function letterCasePermutation(str: string): string[] {
    function isLetter(ch: string): boolean {
        return (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z')
    }
    const ans = new Array<string>()
    let n = str.length, m = 0
    for (let i = 0; i < n; i++) m += isLetter(str[i]) ? 1 : 0
    for (let s = 0; s < (1 << m); s++) {
        let cur = ''
        for (let i = 0, j = 0; i < n; i++) {
            if (!isLetter(str[i]) || ((s >> j) & 1) == 0) cur += str[i]
            else cur += String.fromCharCode(str.charCodeAt(i) ^ 32)
            if (isLetter(str[i])) j++
        }
        ans.push(cur)
    }
    return ans
}
```
* 时间复杂度:最坏情况下原串 `s` 的每一位均为字母(均有保留和转换两种决策),此时具体方案数量共 $2^n$ 种;同时每一种具体方案我们都需要用与原串长度相同的复杂度来进行构造。复杂度为 $O(n \times 2^n)$
* 空间复杂度:$O(n \times 2^n)$

### 最后

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

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

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

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


LeetCode/781-790/786. 第 K 个最小的素数分数（中等）.md

786. 第 K 个最小的素数分数

Question:
给你一个按递增顺序排序的数组 `arr` 和一个整数 `k` 。

数组 `arr` 由 $1$ 和若干 **素数**  组成,且其中所有整数互不相同。

对于每对满足 $0 < i < j < arr.length$ 的 $i$ 和 $j$ ,可以得到分数 $arr[i] / arr[j]$ 。

那么第 $k$ 个最小的分数是多少呢?  以长度为 $2$ 的整数数组返回你的答案, 这里 $answer[0] == arr[i]$ 且 $answer[1] == arr[j]$ 。

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

输出:[2,5]

解释:已构造好的分数,排序后如下所示:
1/5, 1/3, 2/5, 1/2, 3/5, 2/3
很明显第三个最小的分数是 2/5
```
示例 2:
```
输入:arr = [1,7], k = 1

输出:[1,7]
```

提示:
* $2 <= arr.length <= 1000$
* $1 <= arr[i] <= 3 \times 10^4$
* $arr[0] = 1$
* $arr[i]$ 是一个 素数 ,$i > 0$
* $arr$ 中的所有数字 互不相同 ,且按**严格递增**排序
* $1 <= k <= arr.length \times (arr.length - 1) / 2$

Solution:
### 优先队列(堆)

数据范围只有 $10^3$,直接扫描所有点对的计算量不超过 $10^6$。

因此我们可以使用「扫描点对」+「优先队列(堆)」的做法,使用二元组 $(arr[i], arr[j])$ 进行存储,构建大小为 $k$ 的大根堆。

根据「堆内元素多少」和「当前计算值与堆顶元素的大小关系」决定入堆行为:

* 若堆内元素不足 $k$ 个,直接将当前二元组进行入堆;
* 若堆内元素已达 $k$ 个,根据「当前计算值 $\frac{arr[i]}{arr[j]}$ 与堆顶元素 $\frac{peek[0]}{peek[1]}$ 的大小关系」进行分情况讨论:
    * 如果当前计算值比堆顶元素大,那么当前元素不可能是第 $k$ 小的值,直接丢弃;
    * 如果当前计算值比堆顶元素小,那么堆顶元素不可能是第 $k$ 小的值,使用当前计算值置换掉堆顶元素。

代码:
```Java
class Solution {
    public int[] kthSmallestPrimeFraction(int[] arr, int k) {
        int n = arr.length;
        PriorityQueue<int[]> q = new PriorityQueue<>((a,b)->Double.compare(b[0]*1.0/b[1],a[0]*1.0/a[1]));
        for (int i = 0; i < n; i++) {
            for (int j = i + 1; j < n; j++) {
                double t = arr[i] * 1.0 / arr[j];
                if (q.size() < k || q.peek()[0] * 1.0 / q.peek()[1] > t) {
                    if (q.size() == k) q.poll();
                    q.add(new int[]{arr[i], arr[j]});
                }
            }
        }
        return q.poll();
    }
}
```
* 时间复杂度:扫描所有的点对复杂度为 $O(n^2)$;将二元组入堆和出堆的复杂度为 $O(\log{k})$。整体复杂度为 $O(n^2\log{k})$
* 空间复杂度:$O(k)$

---

### 多路归并

在解法一中,我们没有利用「数组内元素严格单调递增」的特性。

由于题目规定所有的点对 $(i, j)$ 必须满足 $i < j$,即给定 $arr[j]$ 后,其所能构建的分数个数为 $j$ 个,而这 $j$ 个分数值满足严格单调递增:$\frac{arr[0]}{arr[j]} < \frac{arr[1]}{arr[j]} < \frac{arr[2]}{arr[j]} < ... < \frac{arr[j - 1]}{arr[j]}$。

问题等价于我们从 $n - 1$ 个(下标 $0$ 作为分母的话,不存在任何分数)有序序列中找到第 $k$ 小的数值。这 $n - 1$ 个序列分别为:

* $[\frac{arr[0]}{arr[1]}]$
* $[\frac{arr[0]}{arr[2]}, \frac{arr[1]}{arr[2]}]$
* $[\frac{arr[0]}{arr[3]}, \frac{arr[1]}{arr[3]}, \frac{arr[2]}{arr[3]}]$
    ...
* $[\frac{arr[0]}{arr[j]}, \frac{arr[1]}{arr[j]}, \frac{arr[2]}{arr[j]}, ... , \frac{arr[j - 1]}{arr[j]}]$

问题彻底切换为「多路归并」问题,我们使用「优先队列(堆)」来维护多个有序序列的当前头部的最小值即可。

代码:
```Java
class Solution {
    public int[] kthSmallestPrimeFraction(int[] arr, int k) {
        int n = arr.length;
        PriorityQueue<int[]> q = new PriorityQueue<>((a,b)->{
            double i1 = arr[a[0]] * 1.0 / arr[a[1]], i2 = arr[b[0]] * 1.0 / arr[b[1]];
            return Double.compare(i1, i2);
        });
        for (int i = 1; i < n; i++) q.add(new int[]{0, i});
        while (k-- > 1) {
            int[] poll = q.poll();
            int i = poll[0], j = poll[1];
            if (i + 1 < j) q.add(new int[]{i + 1, j});
        }
        int[] poll = q.poll();
        return new int[]{arr[poll[0]], arr[poll[1]]};
    }
}
```
* 时间复杂度:起始将 $n - 1$ 个序列的头部元素放入堆中,复杂度为 $O(n\log{n})$;然后重复 $k$ 次操作得到第 $k$ 小的值,复杂度为 $O(k\log{n})$。整体复杂度为 $O(\max(n, k) \times \log{n})$
* 空间复杂度:$O(n)$

---

### 二分 + 双指针

进一步,利用 $arr$ 递增,且每个点对 $(i, j)$ 满足 $i < j$,我们可以确定 $(i, j)$ 对应的分数 $\frac{arr[i]}{arr[j]}$ 必然落在 $[0, 1]$ 范围内。

假设最终答案 $\frac{arr[i]}{arr[j]}$ 为 $x$,那么以 $x$ 为分割点的数轴(该数轴上的点为 $arr$ 所能构造的分数值)上具有「二段性」:

* 小于等于 $x$ 的值满足:其左边分数值个数小于 $k$ 个;
* 大于 $x$ 的值不满足:其左边分数值个数小于 $k$ 个(即至少有 $k$ 个)。

而当确定 $arr[j]$ 时,利用 $arr$ 有序,我们可以通过「双指针」快速得知,满足 $\frac{arr[i]}{arr[j]} <= x$ 的分子位置在哪(找到最近一个满足 $\frac{arr[i]}{arr[j]} > x$ 的位置)。

另外,我们可以在每次 `check` 的同时,记录下相应的 $arr[i]$ 和 $arr[j]$。

代码:
```Java
class Solution {
    double eps = 1e-8;
    int[] arr;
    int n, a, b;
    public int[] kthSmallestPrimeFraction(int[] _arr, int k) {
        arr = _arr;
        n = arr.length;
        double l = 0, r = 1;
        while (r - l > eps) {
            double mid = (l + r) / 2;
            if (check(mid) >= k) r = mid;
            else l = mid;
        }
        return new int[]{a, b};
    }
    int check(double x){
        int ans = 0;
        for (int i = 0, j = 1; j < n; j++) {
            while (arr[i + 1] * 1.0 / arr[j] <= x) i++;
            if (arr[i] * 1.0 / arr[j] <= x) ans += i + 1;
            if (Math.abs(arr[i] * 1.0 / arr[j] - x) < eps) {
                a = arr[i]; b = arr[j];
            }
        }
        return ans;
    }
}
```
* 时间复杂度:二分次数取决于精度,精度为 $C = 10^8$,二分复杂度为 $O(\log{C});$`check` 的复杂度为 $O(n)$。整体复杂度为 $O(n\log{C})$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


LeetCode/781-790/787. K 站中转内最便宜的航班（中等）.md

787. K 站中转内最便宜的航班

Question:
有 `n` 个城市通过一些航班连接。给你一个数组 `flights`,其中 $flights[i] = [from_i, to_i, price_i]$ ,表示该航班都从城市 $from_i$ 开始,以价格 $price_i$ 抵达 $to_i$。

现在给定所有的城市和航班,以及出发城市 `src` 和目的地 `dst`,你的任务是找到出一条最多经过 `k` 站中转的路线,使得从 `src` 到 `dst` 的 价格最便宜 ,并返回该价格。 如果不存在这样的路线,则输出 `-1`。

示例 1:
```
输入:
n = 3, edges = [[0,1,100],[1,2,100],[0,2,500]]
src = 0, dst = 2, k = 1

输出: 200

解释:
城市航班图如下
从城市 0 到城市 2 在 1 站中转以内的最便宜价格是 200,如图中红色所示。
```

示例 2:
```
输入:
n = 3, edges = [[0,1,100],[1,2,100],[0,2,500]]
src = 0, dst = 2, k = 0

输出: 500

解释:
城市航班图如下
从城市 0 到城市 2 在 0 站中转以内的最便宜价格是 500,如图中蓝色所示。
```

提示:
* 1 <= n <= 100
* 0 <= flights.length <= (n * (n - 1) / 2)
* flights[i].length == 3
* 0 <= fromi, toi < n
* fromi != toi
* 1 <= pricei <= $10^4$
* 航班没有重复,且不存在自环
* 0 <= src, dst, k < n
* src != dst

Solution:
### 基本分析

从题面看就能知道,这是一类「有限制」的最短路问题。

「限制最多经过不超过 $k$ 个点」等价于「限制最多不超过 $k + 1$ 条边」,而解决「有边数限制的最短路问题」是 SPFA 所不能取代 Bellman Ford 算法的经典应用之一(SPFA 能做,但不能直接做)。

**Bellman Ford/SPFA 都是基于动态规划,其原始的状态定义为 $f[i][k]$ 代表从起点到 $i$ 点,且经过最多 $k$ 条边的最短路径。这样的状态定义引导我们能够使用 Bellman Ford 来解决有边数限制的最短路问题。**

**同样多源汇最短路算法 Floyd 也是基于动态规划,其原始的三维状态定义为 $f[i][j][k]$ 代表从点 $i$ 到点 $j$,且经过的所有点编号不会超过 $k$(即可使用点编号范围为 $[1, k]$)的最短路径。这样的状态定义引导我们能够使用 Floyd 求最小环或者求“重心点”(即删除该点后,最短路值会变大)。**

如果你对几类最短算法不熟悉,可以看 [这里](https://mp.weixin.qq.com/s?__biz=MzU4NDE3MTEyMA==&mid=2247488007&idx=1&sn=9d0dcfdf475168d26a5a4bd6fcd3505d&chksm=fd9cb918caeb300e1c8844583db5c5318a89e60d8d552747ff8c2256910d32acd9013c93058f&token=754098973&lang=zh_CN#rd),里面涵盖所有的「最短路算法」和「存图方式」。

---

### Bellman Ford + 邻接矩阵

回到本题,「限制最多经过不超过 $k$ 个点」等价于「限制最多不超过 $k + 1$ 条边」,因此可以使用 Bellman Ford 来求解。

点的数量只有 $100$,可以直接使用「邻接矩阵」的方式进行存图。

> 需要注意的是,在遍历所有的“点对/边”进行松弛操作前,需要先对 $dist$ 进行备份,否则会出现「本次松弛操作所使用到的边,也是在同一次迭代所更新的」,从而不满足边数限制的要求。
举个 🌰,例如本次松弛操作使用了从 $a$ 到 $b$ 的当前最短距离来更新 $dist[b]$,直接使用 $dist[a]$ 的话,不能确保 $dist[a]$ 不是在同一次迭代中所更新,如果 $dist[a]$ 是同一次迭代所更新的话,那么使用的边数将会大于 $k$ 条。
因此在每次迭代开始前,我们都应该对 $dist$ 进行备份,在迭代时使用备份来进行松弛操作。

代码:
```Java
class Solution {
    int N = 110, INF = 0x3f3f3f3f;
    int[][] g = new int[N][N];
    int[] dist = new int[N];
    int n, m, s, t, k;
    public int findCheapestPrice(int _n, int[][] flights, int _src, int _dst, int _k) {
        n = _n; s = _src; t = _dst; k = _k + 1;
        for (int i = 0; i < N; i++) {
            for (int j = 0; j < N; j++) {
                g[i][j] = i == j ? 0 : INF;
            }
        }
        for (int[] f : flights) {
            g[f[0]][f[1]] = f[2];
        }
        int ans = bf();
        return ans > INF / 2 ? -1 : ans;
    }
    int bf() {
        Arrays.fill(dist, INF);
        dist[s] = 0;
        for (int limit = 0; limit < k; limit++) {
            int[] clone = dist.clone();
            for (int i = 0; i < n; i++) {
                for (int j = 0; j < n; j++) {
                    dist[j] = Math.min(dist[j], clone[i] + g[i][j]);
                }
            }
        }
        return dist[t];
    }
}
```
* 时间复杂度:$O(k * n^2)$
* 空间复杂度:$O(n^2)$

---

### Bellman Ford + 类

我们知道 Bellman Ford 需要遍历所有的边,而使用「邻接矩阵」的存图方式让我们不得不遍历所有的点对,复杂度为 $O(n^2)$。

而边的数量 $m$ 的数据范围为 $0 <= flights.length <= (n * (n - 1) / 2)$,因此我们可以使用「类」的方式进行存图,从而确保在遍历所有边的时候,复杂度严格为 $O(m)$,而不是 $O(n^2)$。

代码:
```Java
class Solution {
    class Edge {
        int x, y, w;
        Edge(int _x, int _y, int _w) {
            x = _x; y = _y; w = _w;
        }
    }
    int N = 110, INF = 0x3f3f3f3f;
    int[] dist = new int[N];
    List<Edge> list = new ArrayList<>();
    int n, m, s, t, k;
    public int findCheapestPrice(int _n, int[][] flights, int _src, int _dst, int _k) {
        n = _n; s = _src; t = _dst; k = _k + 1;
        for (int[] f : flights) {
            list.add(new Edge(f[0], f[1], f[2]));
        }
        m = list.size();
        int ans = bf();
        return ans > INF / 2 ? -1 : ans;
    }
    int bf() {
        Arrays.fill(dist, INF);
        dist[s] = 0;
        for (int i = 0; i < k; i++) {
            int[] clone = dist.clone();
            for (Edge e : list) {
                int x = e.x, y = e.y, w = e.w;
                dist[y] = Math.min(dist[y], clone[x] + w);
            }
        }
        return dist[t];
    }
}
```
* 时间复杂度:共进行 $k + 1$ 次迭代,每次迭代备份数组复杂度为 $O(n)$,然后遍历所有的边进行松弛操作,复杂度为 $O(m)$。整体复杂度为 $O(k * (n + m))$
* 空间复杂度:$O(n + m)$

---

### Bellman Ford

更进一步,由于 Bellman Ford 核心操作需要遍历所有的边,因此也可以直接使用 $flights$ 数组作为存图信息,而无须额外存图。

代码:
```Java
class Solution {
    int N = 110, INF = 0x3f3f3f3f;
    int[] dist = new int[N];
    public int findCheapestPrice(int n, int[][] flights, int src, int dst, int k) {
        Arrays.fill(dist, INF);
        dist[src] = 0;
        for (int limit = 0; limit < k + 1; limit++) {
            int[] clone = dist.clone();
            for (int[] f : flights) {
                int x = f[0], y = f[1], w = f[2];
                dist[y] = Math.min(dist[y], clone[x] + w);
            }
        }
        return dist[dst] > INF / 2 ? -1 : dist[dst];
    }
}
```
* 时间复杂度:共进行 $k + 1$ 次迭代,每次迭代备份数组复杂度为 $O(n)$,然后遍历所有的边进行松弛操作,复杂度为 $O(m)$。整体复杂度为 $O(k * (n + m))$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


Question:
我们称一个数 `X` 为好数, 如果它的每位数字逐个地被旋转 `180` 度后,我们仍可以得到一个有效的,且和 `X` 不同的数。要求每位数字都要被旋转。

如果一个数的每位数字被旋转以后仍然还是一个数字, 则这个数是有效的。0, 1, 和 8 被旋转后仍然是它们自己;2 和 5 可以互相旋转成对方(在这种情况下,它们以不同的方向旋转,换句话说,2 和 5 互为镜像);6 和 9 同理,除了这些以外其他的数字旋转以后都不再是有效的数字。

现在我们有一个正整数 `N`, 计算从 `1` 到 `N` 中有多少个数 X 是好数?

示例:
```
输入: 10

输出: 4

解释:
在[1, 10]中有四个好数: 2, 5, 6, 9。
注意 1 和 10 不是好数, 因为他们在旋转之后不变。
```

提示:
* `N` 的取值范围是 $[1, 10000]$。

Solution:
### 模拟

利用 $n$ 的范围为 $1e4$,我们可以直接检查 $[1, n]$ 的每个数。

由于每一个位数都需要翻转,因此如果当前枚举到的数值 `x` 中包含非有效翻转数字(非 `0125689`)则必然不是好数;而在每一位均为有效数字的前提下,若当前枚举到的数值 `x` 中包含翻转后能够发生数值上变化的数值(`2569`),则为好数。

Java 代码:
```Java
class Solution {
    public int rotatedDigits(int n) {
        int ans = 0;
        out:for (int i = 1; i <= n; i++) {
            boolean ok = false;
            int x = i;
            while (x != 0) {
                int t = x % 10;
                x /= 10;
                if (t == 2 || t == 5 || t == 6 || t == 9) ok = true;
                else if (t != 0 && t != 1 && t != 8) continue out;
            }
            if (ok) ans++;
        }
        return ans;
    }
}
```
TypeScript 代码:
```TypeScript
function rotatedDigits(n: number): number {
    let ans = 0
    out:for (let i = 1; i <= n; i++) {
        let ok = false
        let x = i
        while (x != 0) {
            const t = x % 10
            x = Math.floor(x / 10)
            if (t == 2 || t == 5 || t == 6 || t == 9) ok = true
            else if (t != 0 && t != 1 && t != 8) continue out
        }
        if (ok) ans++
    }
    return ans
};
```
Python3 代码:
```Python3
class Solution:
    def rotatedDigits(self, n: int) -> int:
        ans = 0
        for i in range(1, n + 1):
            ok, x = False, i
            while x != 0:
                t = x % 10
                x = x // 10
                if t == 2 or t == 5 or t == 6 or t == 9:
                    ok = True
                elif t != 0 and t != 1 and t != 8:
                    ok = False
                    break
            ans = ans + 1 if ok else ans
        return ans
```
* 时间复杂度:共有 $n$ 个数需要枚举,检查一个数需要遍历其每个数字,复杂度为 $O(\log{n})$。整体复杂度为 $O(n\log{n})$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


Question:
你在进行一个简化版的吃豆人游戏。你从 [0, 0] 点开始出发,你的目的地是 target = [xtarget, ytarget] 。地图上有一些阻碍者,以数组 ghosts 给出,第 i 个阻碍者从 ghosts[i] = [xi, yi] 出发。所有输入均为 整数坐标 。

每一回合,你和阻碍者们可以同时向东,西,南,北四个方向移动,每次可以移动到距离原位置 1 个单位 的新位置。当然,也可以选择 不动 。所有动作 同时 发生。

如果你可以在任何阻碍者抓住你 之前 到达目的地(阻碍者可以采取任意行动方式),则被视为逃脱成功。如果你和阻碍者同时到达了一个位置(包括目的地)都不算是逃脱成功。

只有在你有可能成功逃脱时,输出 true ;否则,输出 false 。

示例 1:
```
输入:ghosts = [[1,0],[0,3]], target = [0,1]

输出:true

解释:你可以直接一步到达目的地 (0,1) ,在 (1, 0) 或者 (0, 3) 位置的阻碍者都不可能抓住你。
```
示例 2:
```
输入:ghosts = [[1,0]], target = [2,0]

输出:false

解释:你需要走到位于 (2, 0) 的目的地,但是在 (1, 0) 的阻碍者位于你和目的地之间。
```
示例 3:
```
输入:ghosts = [[2,0]], target = [1,0]

输出:false

解释:阻碍者可以和你同时达到目的地。
```
示例 4:
```
输入:ghosts = [[5,0],[-10,-2],[0,-5],[-2,-2],[-7,1]], target = [7,7]

输出:false
```
示例 5:
```
输入:ghosts = [[-1,0],[0,1],[-1,0],[0,1],[-1,0]], target = [0,0]

输出:true
```

提示:
* 1 <= ghosts.length <= 100
* ghosts[i].length == 2
* -$10^4$ <= xi, yi <= $10^4$
* 同一位置可能有 多个阻碍者 。
* target.length == 2
* -$10^4$ <= xtarget, ytarget <= $10^4$

Solution:
### 数学

从数据范围 $-10^4 <= x_{target}, y_{target} <= 10^4$ 且每次只能移动一个单位(或不移动)就注定了不能使用朴素的 `BFS` 进行求解。

朴素的 `BFS` 是指每次在玩家移动一步前,先将阻碍者可以一步到达的位置“置灰”(即设为永不可达),然后判断玩家是否能够到达 $target$。

朴素 `BFS` 本质是模拟,由于棋盘足够大,步长只有 $1$,因此该做法显然会 TLE。

是否有比模拟更快的做法呢?

**根据「树的直径」类似的证明,我们可以证明出「如果一个阻碍者能够抓到玩家,必然不会比玩家更晚到达终点」。**

为了方便,我们设玩家起点、阻碍者起点、终点分别为 $s$、$e$ 和 $t$,计算两点距离的函数为 $dist(x, y)$。

假设玩家从 $s$ 到 $t$ 的路径中会经过点 $k$,**当且仅当 $dist(e, k) <= dist(s, k)$,即「阻碍者起点与点 $k$ 的距离」小于等于「玩家起点与点 $k$ 的距离」时,阻碍者可以在点 $k$ 抓到玩家。**

由于「玩家到终点」以及「阻碍者到终点」的路径存在公共部分 $dist(k, t)$,可推导出:

$$
dist(e, k) + dist(k, t) <= dist(s, k) + dist(k, t)
$$

即得证 **如果一个阻碍者能够抓到玩家,那么该阻碍者必然不会比玩家更晚到达终点。**

由于步长为 $1$,且移动规则为上下左右四联通方向,因此 $dist(x, y)$ 的实现为计算两点的曼哈顿距离。

代码:
```Java
class Solution {
    int dist(int x1, int y1, int x2, int y2) {
        return Math.abs(x1 - x2) + Math.abs(y1 - y2);
    }
    public boolean escapeGhosts(int[][] gs, int[] t) {
        int cur = dist(0, 0, t[0], t[1]);
        for (int[] g : gs) {
            if (dist(g[0], g[1], t[0], t[1]) <= cur) return false;
        }
        return true;
    }
}
```
* 时间复杂度:令 $gs$ 长度为 $n$,计算曼哈顿距离复杂度为 $O(1)$,整体复杂度为 $O(n)$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


790. 多米诺和托米诺平铺

Question:
有两种形状的瓷砖:一种是 `2 x 1` 的多米诺形,另一种是形如 `"L"` 的托米诺形,两种形状都可以旋转。

给定整数 `n` ,返回可以平铺 `2 x n` 的面板的方法的数量,返回对 $10^9 + 7$ 取模 的值。

平铺指的是每个正方形都必须有瓷砖覆盖。

两个平铺不同,当且仅当面板上有四个方向上的相邻单元中的两个,使得恰好有一个平铺有一个瓷砖占据两个正方形。

示例 1:

```
输入: n = 3

输出: 5

解释: 五种不同的方法如上所示。
```
示例 2:
```
输入: n = 1

输出: 1
```

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

Solution:
### 状态机 DP

定义 $f[i][j]$ 为无须考虑前 $i - 1$ 列(含义为前 $i - 1$ 列已铺满),当前第 $i$ 列状态为 $j$ 时的方案数。

其中 $j$ 取值范围为 $[0, 4)$ 分别对应了当前列的填充情况:

为了方便,我们人为规定列数从 $1$ 开始。

由于骨牌只能在 $2 \times n$ 的棋盘内填充(不能延伸出棋盘两端),因此我们有显而易见的初始化状态:

$$
f[1][0] = f[1][1] = 1
$$

分别对应「第一列不放置任何骨牌」和「第一列竖着放一块 $1 \times 2$ 骨牌」合法方案。

而 $f[1][2]$ 和 $f[1][3]$ 由于没法在棋盘左侧以外的位置放置骨牌,不存在合法方案,其值均为 $0$。

同时可知 $f[n][1]$ 为我们最终答案,含义为所有列都恰好铺完,不溢出棋盘右侧位置。

不失一般性考虑 $f[i][j]$ 该如何计算,其实就是一个简单的状态机转移分情况讨论:

* $f[i][0]$ : 需要前 $i - 1$ 列铺满,同时第 $i$ 列没有被铺,只能由 $f[i - 1][1]$ 转移而来,即有 $f[i][0] = f[i - 1][1]$
    > 这里需要尤其注意:虽然我们能够在上一步留空第 $i - 1$ 列,然后在 $i - 1$ 列竖放一块 $1 \times 2$ 的骨牌(如下图)

    > 但我们不能从 $f[i - 1][0]$ 转移到 $f[i][0]$,因为此时放置的骨牌,仅对第 $i - 1$ 列产生影响,不会对第 $i$ 列产生影响,该决策所产生的方案数,已在 $f[i - 1][X]$ 时被统计

* $f[i][1]$ : 可由 $f[i - 1][j]$ 转移而来(见下图),其中 $j \in [0, 4)$,即有 $f[i][1] = \sum_{j = 0}^{3} f[i - 1][j]$

* $f[i][2]$ : 可由 $f[i - 1][0]$ 和 $f[i - 1][3]$ 转移而来

* $f[i][3]$ : 可由 $f[i - 1][0]$ 和 $f[i - 1][2]$ 转移而来

Java 代码:
```Java
class Solution {
    int MOD = (int)1e9+7;
    public int numTilings(int n) {
        int[][] f = new int[n + 10][4];
        f[1][0] = f[1][1] = 1;
        for (int i = 2; i <= n; i++) {
            f[i][0] = f[i - 1][1];
            int cur = 0;
            for (int j = 0; j < 4; j++) cur = (cur + f[i - 1][j]) % MOD;
            f[i][1] = cur;
            f[i][2] = (f[i - 1][0] + f[i - 1][3]) % MOD;
            f[i][3] = (f[i - 1][0] + f[i - 1][2]) % MOD;
        }
        return f[n][1];
    }
}
```
C++ 代码:
```C++
class Solution {
    const int MOD = 1e9 + 7;
public:
    int numTilings(int n){
        vector<vector<int>> f(n + 10, vector<int>(4));
        f[1][0] = f[1][1] = 1;
        for (int i = 2; i <= n; ++i){
            f[i][0] = f[i - 1][1];
            int cur = 0;
            for (int j = 0; j < 4; ++j) cur = (cur + f[i - 1][j]) % MOD;
            f[i][1] = cur;
            f[i][2] = (f[i - 1][0] + f[i - 1][3]) % MOD;
            f[i][3] = (f[i - 1][0] + f[i - 1][2]) % MOD;
        }
        return f[n][1];
    }
};
```
Python3 代码:
```Python
class Solution:
    def numTilings(self, n: int) -> int:
        f = [[0] * 4 for _ in range(n + 10)]
        f[1][0] = f[1][1] = 1
        for i in range(2, n + 1):
            f[i][0] = f[i - 1][1]
            f[i][1] = sum([f[i - 1][j] for j in range(4)])
            f[i][2] = f[i - 1][0] + f[i - 1][3]
            f[i][3] = f[i - 1][0] + f[i - 1][2]
        return f[n][1] % 1000000007
```
TypeScript 代码:
```TypeScript
function numTilings(n: number): number {
    const MOD = 1e9+7
    const f = new Array<Array<number>>()
    for (let i = 0; i <= n; i++) f[i] = new Array<number>(4).fill(0)
    f[1][0] = f[1][1] = 1
    for (let i = 2; i <= n; i++) {
        f[i][0] = f[i - 1][1]
        let cur = 0
        for (let j = 0; j < 4; j++) cur = (cur + f[i - 1][j]) % MOD
        f[i][1] = cur
        f[i][2] = (f[i - 1][0] + f[i - 1][3]) % MOD
        f[i][3] = (f[i - 1][0] + f[i - 1][2]) % MOD
    }
    return f[n][1]
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n)$

---

### 滚动数组优化

利用 $f[i][X]$ 仅依赖于 $f[i - 1][X]$,我们可以采用「滚动数组」方式将其空间优化至 $O(1)$。

Java 代码:
```Java
class Solution {
    int MOD = (int)1e9+7;
    public int numTilings(int n) {
        int[][] f = new int[2][4];
        f[1][0] = f[1][1] = 1;
        for (int i = 2; i <= n; i++) {
            int a = i & 1, b = (i - 1) & 1;
            f[a][0] = f[b][1];
            int cur = 0;
            for (int j = 0; j < 4; j++) cur = (cur + f[b][j]) % MOD;
            f[a][1] = cur;
            f[a][2] = (f[b][0] + f[b][3]) % MOD;
            f[a][3] = (f[b][0] + f[b][2]) % MOD;
        }
        return f[n & 1][1];
    }
}
```
C++ 代码:
```C++
class Solution {
    const int MOD = 1e9 + 7;
public:
    int numTilings(int n){
        vector<vector<int>> f(2, vector<int>(4));
        f[1][0] = f[1][1] = 1;
        for (int i = 2; i <= n; ++i){
            int a = i & 1, b = (i - 1) & 1;
            f[a][0] = f[b][1];
            int cur = 0;
            for (int j = 0; j < 4; ++j) cur = (cur + f[b][j]) % MOD;
            f[a][1] = cur;
            f[a][2] = (f[b][0] + f[b][3]) % MOD;
            f[a][3] = (f[b][0] + f[b][2]) % MOD;
        }
        return f[n & 1][1];
    }
};
```
Python3 代码:
```Python
class Solution:
    def numTilings(self, n: int) -> int:
        f = [[0] * 4 for _ in range(2)]
        f[1][0] = f[1][1] = 1
        for i in range(2, n + 1):
            a, b = i & 1, (i - 1) & 1
            f[a][0] = f[b][1]
            f[a][1] = sum([f[b][j] for j in range(4)])
            f[a][2] = f[b][0] + f[b][3]
            f[a][3] = f[b][0] + f[b][2]
        return f[n & 1][1] % 1000000007
```
TypeScript 代码:
```TypeScript
function numTilings(n: number): number {
    const MOD = 1e9+7
    const f = new Array<Array<number>>()
    for (let i = 0; i <= 1; i++) f[i] = new Array<number>(4).fill(0)
    f[1][0] = f[1][1] = 1
    for (let i = 2; i <= n; i++) {
        const a = i & 1, b = (i - 1) & 1
        f[a][0] = f[b][1]
        let cur = 0
        for (let j = 0; j < 4; j++) cur = (cur + f[b][j]) % MOD
        f[a][1] = cur
        f[a][2] = (f[b][0] + f[b][3]) % MOD
        f[a][3] = (f[b][0] + f[b][2]) % MOD
    }
    return f[n & 1][1]
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


791. 自定义字符串排序

Question:
给定两个字符串 `order` 和 `s` 。

`order` 的所有单词都是唯一的,并且以前按照一些自定义的顺序排序。

对 `s` 的字符进行置换,使其与排序的 `order` 相匹配。

更具体地说,如果在 `order` 中的字符 `x` 出现字符 `y` 之前,那么在排列后的字符串中, `x` 也应该出现在 `y` 之前。

返回满足这个性质的 `s` 的任意排列 。

示例 1:
```
输入: order = "cba", s = "abcd"

输出: "cbad"

解释:
“a”、“b”、“c”是按顺序出现的,所以“a”、“b”、“c”的顺序应该是“c”、“b”、“a”。
因为“d”不是按顺序出现的,所以它可以在返回的字符串中的任何位置。“dcba”、“cdba”、“cbda”也是有效的输出。
```
示例 2:
```
输入: order = "cbafg", s = "abcd"

输出: "cbad"
```

提示:
* $1 <= order.length <= 26$
* $1 <= s.length <= 200$
* `order` 和 `s` 由小写英文字母组成
* `order` 中的所有字符都 不同

Solution:
### 构造

根据题意进行模拟即可:起始先使用大小为 $C = 26$ 的数组 `cnts` 对 `s` 的所有字符进行词频统计,随后根据 `order` 的优先级进行构造。

若字符 `x` 在 `order` 中排于 `y` 前面,则先往答案追加 `cnts[x]` 个字符 `x`,再往答案追加 `cnts[y]` 个字符 `y`,并更新对应词频,最后将仅出现在 `s` 中的字符追加到答案尾部。

Java 代码:
```Java
class Solution {
    public String customSortString(String order, String s) {
        int[] cnts = new int[26];
        for (char c : s.toCharArray()) cnts[c - 'a']++;
        StringBuilder sb = new StringBuilder();
        for (char c : order.toCharArray()) {
            while (cnts[c - 'a']-- > 0) sb.append(c);
        }
        for (int i = 0; i < 26; i++) {
            while (cnts[i]-- > 0) sb.append((char)(i + 'a'));
        }
        return sb.toString();
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    string customSortString(string order, string s) {
        vector<int> cnts(26, 0);
        for (char c : s) cnts[c - 'a']++;
        string result = "";
        for (char c : order) {
            while (cnts[c - 'a']-- > 0) result += c;
        }
        for (int i = 0; i < 26; i++) {
            while (cnts[i]-- > 0) result += (char)(i + 'a');
        }
        return result;
    }
};
```
Python 代码:
```Python
class Solution:
    def customSortString(self, order: str, s: str) -> str:
        cnts = [0] * 26
        for c in s:
            cnts[ord(c) - ord('a')] += 1
        ans = ''
        for c in order:
            num = ord(c) - ord('a')
            if cnts[num] > 0:
                ans += c * cnts[num]
                cnts[num] = 0
        for i in range(26):
            if cnts[i] > 0:
                ans += chr(i + ord('a')) * cnts[i]
        return ans
```
TypeScript 代码:
```TypeScript
function customSortString(order: string, s: string): string {
    const cnts = new Array<number>(26).fill(0)
    for (const c of s) cnts[c.charCodeAt(0) - 'a'.charCodeAt(0)]++
    let ans = ''
    for (const c of order) {
        while (cnts[c.charCodeAt(0) - 'a'.charCodeAt(0)]-- > 0) ans += c
    }
    for (let i = 0; i < 26; i++) {
        while (cnts[i]-- > 0) ans += String.fromCharCode(i + 'a'.charCodeAt(0));
    }
    return ans
}
```
* 时间复杂度:$O(n + m)$
* 空间复杂度:$O(C)$,其中 $C = 26$ 为字符集大小

### 最后

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

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

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

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


792. 匹配子序列的单词数

Question:
给定字符串 `s` 和字符串数组 `words`, 返回  `words[i]` 中是 `s` 的子序列的单词个数 。

字符串的子序列是从原始字符串中生成的新字符串,可以从中删去一些字符(可以是`""`),而不改变其余字符的相对顺序。

例如, `“ace”` 是 `“abcde”` 的子序列。

示例 1:
```
输入: s = "abcde", words = ["a","bb","acd","ace"]

输出: 3

解释: 有三个是 s 的子序列的单词: "a", "acd", "ace"。
```
示例 2:
```
输入: s = "dsahjpjauf", words = ["ahjpjau","ja","ahbwzgqnuk","tnmlanowax"]

输出: 2
```

提示:
* $1 <= s.length <= 5 \times 10^4$
* $1 <= words.length <= 5000$
* $1 <= words[i].length <= 50$
* `words[i]` 和 `s` 都只由小写字母组成。

Solution:
### 预处理 + 哈希表 + 二分

朴素判定某个字符串是为另一字符串的子序列的复杂度为 $O(n + m)$,对于本题共有 $5000$ 个字符串需要判定,每个字符串最多长为 $50$,因此整体计算量为 $(5 \times 10^4 + 50) \times 5000 \approx 2.5 \times 10^8$,会超时。

不可避免的是,我们要对每个 $words[i]$ 进行检查,因此优化的思路可放在如何优化单个 $words[i]$ 的判定操作。

朴素的判定过程需要使用双指针扫描两个字符串,其中对于原串的扫描,会有大量的字符会被跳过(无效匹配),即只有两指针对应的字符相同时,匹配串指针才会后移。

我们考虑如何优化这部分无效匹配。

对于任意一个 $w = words[i]$ 而言,假设我们当前匹配到 $w[j]$ 位置,此时我们已经明确下一个待匹配的字符为 $w[j + 1]$,因此我们可以直接在 `s` 中字符为 $w[j + 1]$ 的位置中找候选。

具体的,我们可以使用哈希表 `map` 对 `s` 进行预处理:以字符 $c = s[i]$ 为哈希表的 `key`,对应的下标 $i$ 集合为 `value`,由于我们从前往后处理 `s` 进行预处理,因此对于所有的 `value` 均满足递增性质。

> 举个 🌰 : 对于 `s = abcabc` 而言,预处理的哈希表为 `{a=[0,3], b=[1,4], c=[2,5]}`

最后考虑如何判定某个 $w = words[i]$ 是否满足要求:待匹配字符串 `w` 长度为 `m`,我们从前往后对 `w` 进行判定,假设当前判待匹配位置为 $w[i]$,我们使用变量 `idx` 代表能够满足匹配 $w[0:i]$ 的最小下标(贪心思路)。

对于匹配的 $w[i]$ 字符,可以等价为在 `map[w[i]]` 中找到第一个大于 `idx` 的下标,含义在原串 `s` 中找到字符为 `w[i]` 且下标大于 `idx` 的最小值,由于我们所有的 `map[X]` 均满足单调递增,该过程可使用「二分」进行。

Java 代码:
```Java
class Solution {
    public int numMatchingSubseq(String s, String[] words) {
        int n = s.length(), ans = 0;
        Map<Character, List<Integer>> map = new HashMap<>();
        for (int i = 0; i < n; i++) {
            List<Integer> list = map.getOrDefault(s.charAt(i), new ArrayList<>());
            list.add(i);
            map.put(s.charAt(i), list);
        }
        for (String w : words) {
            boolean ok = true;
            int m = w.length(), idx = -1;
            for (int i = 0; i < m && ok; i++) {
                List<Integer> list = map.getOrDefault(w.charAt(i), new ArrayList<>());
                int l = 0, r = list.size() - 1;
                while (l < r) {
                    int mid = l + r >> 1;
                    if (list.get(mid) > idx) r = mid;
                    else l = mid + 1;
                }
                if (r < 0 || list.get(r) <= idx) ok = false;
                else idx = list.get(r);
            }
            if (ok) ans++;
        }
        return ans;
    }
}
```
TypeScript 代码:
```TypeScript
function numMatchingSubseq(s: string, words: string[]): number {
    let n = s.length, ans = 0
    const map = new Map<String, Array<number>>()
    for (let i = 0; i < n; i++) {
        if (!map.has(s[i])) map.set(s[i], new Array<number>())
        map.get(s[i]).push(i)
    }
    for (const w of words) {
        let ok = true
        let m = w.length, idx = -1
        for (let i = 0; i < m && ok; i++) {
            if (!map.has(w[i])) {
                ok = false
            } else {
                const list = map.get(w[i])
                let l = 0, r = list.length - 1
                while (l < r) {
                    const mid = l + r >> 1
                    if (list[mid] > idx) r = mid
                    else l = mid + 1
                }
                if (r < 0 || list[r] <= idx) ok = false
                else idx = list[r]
            }
        }
        if (ok) ans++
    }
    return ans
}
```
Python3 代码:
```Python
class Solution:
    def numMatchingSubseq(self, s: str, words: List[str]) -> int:
        dmap = defaultdict(list)
        for i, c in enumerate(s):
            dmap[c].append(i)
        ans = 0
        for w in words:
            ok = True
            idx = -1
            for i in range(len(w)):
                idxs = dmap[w[i]]
                l, r = 0, len(idxs) - 1
                while l < r :
                    mid = l + r >> 1
                    if dmap[w[i]][mid] > idx:
                        r = mid
                    else:
                        l = mid + 1
                if r < 0 or dmap[w[i]][r] <= idx:
                    ok = False
                    break
                else:
                    idx = dmap[w[i]][r]
            ans += 1 if ok else 0
        return ans
```
* 时间复杂度:令 `n` 为 `s` 长度,`m` 为 `words` 长度,`l = 50` 为 $words[i]$ 长度的最大值。构造 `map` 的复杂度为 $O(n)$;统计符合要求的 $words[i]$ 的数量复杂度为 $O(m \times l \times \log{n})$。整体复杂度为 $O(n + m \times l \times \log{n})$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


793. 阶乘函数后 K 个零

Question:
`f(x)` 是 `x!` 末尾是 `0` 的数量。回想一下 `x! = 1 * 2 * 3 * ... * x`,且 `0! = 1`。

例如, `f(3) = 0`,因为 `3! = 6` 的末尾没有 `0` ;而 `f(11) = 2`,因为 `11!= 39916800` 末端有 `2` 个 `0` 。

给定 `k`,找出返回能满足 `f(x) = k` 的非负整数 `x` 的数量。

示例 1:
```
输入:k = 0

输出:5

解释:0!, 1!, 2!, 3!, 和 4! 均符合 k = 0 的条件。
```
示例 2:
```
输入:k = 5

输出:0

解释:没有匹配到这样的 x!,符合 k = 5 的条件。
```
示例 3:
```
输入: k = 3

输出: 5
```

提示:
* $0 <= k <= 10^9$

Solution:
### 数学 + 二分

对于一个 $n! = 1 \times 2 \times ... \times (n - 1) \times n$ 而言,其最终结果尾部包含 $0$ 的数量取决于其被累乘 $10$ 的次数,而 $10$ 可通过质因数 $2$ 和 $5$ 相乘而来,因此假设对 $n!$ 进行阶乘分解,最终分解出 $2^p$ 和 $5^q$ 的话,那么最终结果尾部包含 $0$ 的个数为 $q$ 个(可证明 $p >= q$ 始终满足)。

因此原问题转化为:在非负整数中,有多少个数进行阶乘分解后,所含质因数 $5$ 的个数恰好为 $k$ 个。

同时我们可知:随着 $n$ 的增大,其所能分解出来的 $5$ 的个数必然是递增的。

基于此,我们可以通过「二分 + 容斥原理」来得出分解 $5$ 个数恰好为 $k$ 的连续段长度。假设我们存在函数 `f(k)` 可得到非负整数中分解 $5$ 个数为小于等于 `k` 的个数,那么最终 `f(k) - f(k - 1)` 即是答案。

在单调函数上求解小于等于 `k` 分割点,可通过「二分」来做。剩下的问题是,如何求得给定 `x` 时,其阶乘分解所包含 $5$ 个个数,这可以通过 $O(\log{x})$ 的筛法来做。

最后还要确定二分的值域,由于我们是求阶乘分解中 $5$ 的个数,因此值域的上界为 $5k$,利用 $k$ 的范围为 $1e9$,直接取成 $1e10$ 即可。

Java 代码:
```Java
class Solution {
    public int preimageSizeFZF(int k) {
        if (k <= 1) return 5;
        return f(k) - f(k - 1);
    }
    int f(int x) {
        long l = 0, r = (long) 1e10;
        while (l < r) {
            long mid = l + r + 1 >> 1;
            if (getCnt(mid) <= x) l = mid;
            else r = mid - 1;
        }
        return (int)r;
    }
    long getCnt(long x) {
        long ans = 0;
        while (x != 0) {
            ans += x / 5; x /= 5;
        }
        return ans;
    }
}
```
TypeScript 代码:
```TypeScript
function preimageSizeFZF(k: number): number {
    if (k <= 1) return 5
    return f(k) - f(k - 1)
};
function f(x: number): number {
    let l = 0n, r = BigInt("10000000000")
    while (l < r) {
        const mid = l + r + 1n >> 1n
        if (getCnt(mid) <= x) l = mid
        else r = mid - 1n
    }
    return Number(r)
}
function getCnt(x: bigint): bigint {
    let ans = 0n
    while (x != 0n) {
        ans += BigInt(Math.floor(Number(x / 5n))); x = BigInt(Math.floor(Number(x / 5n)))
    }
    return ans
}
```
* 时间复杂度:$O(\log^2{k})$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


794. 有效的井字游戏

Question:
给你一个字符串数组 `board` 表示井字游戏的棋盘。当且仅当在井字游戏过程中,棋盘有可能达到 `board` 所显示的状态时,才返回 `true` 。

井字游戏的棋盘是一个 `3 x 3` 数组,由字符 `' '`,`'X'` 和 `'O'` 组成。字符 `' '` 代表一个空位。

以下是井字游戏的规则:
* 玩家轮流将字符放入空位(`' '`)中。
* 玩家 $1$ 总是放字符 `'X'` ,而玩家 $2$ 总是放字符 `'O'` 。
* 'X' 和 'O' 只允许放置在空位中,不允许对已放有字符的位置进行填充。
* 当有 $3$ 个相同(且非空)的字符填充任何行、列或对角线时,游戏结束。
* 当所有位置非空时,也算为游戏结束。
* 如果游戏结束,玩家不允许再放置字符。

示例 1:

```
输入:board = ["O  ","   ","   "]

输出:false

解释:玩家 1 总是放字符 "X" 。
```
示例 2:

```
输入:board = ["XOX"," X ","   "]

输出:false

解释:玩家应该轮流放字符。
```
示例 3:

```
输入:board = ["XXX","   ","OOO"]

输出:false
```
示例 4:

```
输入:board = ["XOX","O O","XOX"]

输出:true
```

提示:
* `board.length == 3`
* `board[i].length == 3`
* `board[i][j]` 为 `'X'`、`'O'` 或 `' '`

Solution:
### 分情况讨论

给定的棋盘大小固定,对于无效情况进行分情况讨论即可:

1. 由于 `X` 先手,`O` 后手,两者轮流下子。因此 `O` 的数量不会超过 `X`,且两者数量差不会超过 $1$,否则为无效局面;
2. 若局面是 `X` 获胜,导致该局面的最后一个子必然是 `X`,此时必然有 `X` 数量大于 `O`(`X` 为先手),否则为无效局面;
3. 若局面是 `O` 获胜,导致该局面的最后一个子必然是 `O`,此时必然有 `X` 数量等于 `O`(`X` 为先手),否则为无效局面;
4. 局面中不可能出现两者同时赢(其中一方赢后,游戏结束)。

代码:
```Java
class Solution {
    public boolean validTicTacToe(String[] board) {
        char[][] cs = new char[3][3];
        int x = 0, o = 0;
        for (int i = 0; i < 3; i++) {
            for (int j = 0; j < 3; j++) {
                char c = board[i].charAt(j);
                if (c == 'X') x++;
                else if (c == 'O') o++;
                cs[i][j] = c;
            }
        }
        boolean a = check(cs, 'X'), b = check(cs, 'O');
        if (o > x || x - o > 1) return false;
        if (a && x <= o) return false;
        if (b && o != x) return false;
        if (a && b) return false;
        return true;
    }
    boolean check(char[][] cs, char c) {
        for (int i = 0; i < 3; i++) {
            if (cs[i][0] == c && cs[i][1] == c && cs[i][2] == c) return true;
            if (cs[0][i] == c && cs[1][i] == c && cs[2][i] == c) return true;
        }
        boolean a = true, b = true;
        for (int i = 0; i < 3; i++) {
            for (int j = 0; j < 3; j++) {
                if (i == j) a &= cs[i][j] == c;
                if (i + j == 2) b &= cs[i][j] == c;
            }
        }
        return a || b;
    }
}
```
* 时间复杂度:棋盘大小固定,遍历棋盘次数为常数。复杂度为 $O(C)$
* 空间复杂度:使用了 $char$ 二维数组对 $board$ 进行转存,复杂度为 $O(C)$;全程使用 `charAt` 的话复杂度为 $O(1)$

### 最后

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

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

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

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