795. 区间子数组个数

Question:
给你一个整数数组 `nums` 和两个整数:`left` 及 `right` 。

找出 `nums` 中连续、非空且其中最大元素在范围 $[left, right]$ 内的子数组,并返回满足条件的子数组的个数。

生成的测试用例保证结果符合 `32-bit` 整数范围。

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

输出:3

解释:满足条件的三个子数组:[2], [2, 1], [3]
```
示例 2:
```
输入:nums = [2,9,2,5,6], left = 2, right = 8

输出:7
```

提示:
* $1 <= nums.length <= 10^5$
* $0 <= nums[i] <= 10^9$
* $0 <= left <= right <= 10^9$

Solution:
### 单调栈

为了方便,我们令 $[left, right]$ 为 $[a, b]$。

一个容易想到的思路是使用「单调栈」。

**统计所有最大值范围在 $[a, b]$ 之间的子数组个数,可等价为统计每一个范围落在 $[a, b]$ 之间的 $nums[i]$ 作为最大值时子数组的个数。**

由此可以进一步将问题转换为:求解每个 $nums[i]$ 作为子数组最大值时,最远的合法左右端点的位置。也就是求解每一个 $nums[i]$ 左右最近一个比其“大”的位置,这可以使用「单调栈」来进行求解。

> 对于单调栈不了解的同学,可以看前置 🧀 : [【RMQ 专题】关于 RMQ 的若干解法](https://mp.weixin.qq.com/s?__biz=MzU4NDE3MTEyMA==&mid=2247493262&idx=1&sn=2d8e192a5767b49b9a13a6192ab3b833)

**统计所有 $nums[i]$ 对答案的贡献即是最终答案,但我们忽略了「当  `nums` 存在重复元素,且该元素作为子数组最大值时,最远左右端点的边界越过重复元素时,导致重复统计子数组」的问题。**

我们不失一般性的举个 🌰 来理解(下图):

为了消除这种重复统计,我们可以将「最远左右边界」的一端,从「严格小于」调整为「小于等于」,从而实现半开半闭的效果。

Java 代码:
```Java
class Solution {
    public int numSubarrayBoundedMax(int[] nums, int a, int b) {
        int n = nums.length, ans = 0;
        int[] l = new int[n + 10], r = new int[n + 10];
        Arrays.fill(l, -1); Arrays.fill(r, n);
        Deque<Integer> d = new ArrayDeque<>();
        for (int i = 0; i < n; i++) {
            while (!d.isEmpty() && nums[d.peekLast()] < nums[i]) r[d.pollLast()] = i;
            d.addLast(i);
        }
        d.clear();
        for (int i = n - 1; i >= 0; i--) {
            while (!d.isEmpty() && nums[d.peekLast()] <= nums[i]) l[d.pollLast()] = i;
            d.addLast(i);
        }
        for (int i = 0; i < n; i++) {
            if (nums[i] < a || nums[i] > b) continue;
            ans += (i - l[i]) * (r[i] - i);
        }
        return ans;
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    int numSubarrayBoundedMax(vector<int>& nums, int a, int b) {
        int n = nums.size(), ans = 0;
        vector<int> l(n + 10, -1), r(n + 10, n);
        deque<int> d;
        for (int i = 0; i < n; i++) {
            while (!d.empty() && nums[d.back()] < nums[i]) {
                r[d.back()] = i;
                d.pop_back();
            }
            d.push_back(i);
        }
        d.clear();
        for (int i = n - 1; i >= 0; i--) {
            while (!d.empty() && nums[d.back()] <= nums[i]) {
                l[d.back()] = i;
                d.pop_back();
            }
            d.push_back(i);
        }
        for (int i = 0; i < n; i++) {
            if (nums[i] < a || nums[i] > b) continue;
            ans += (i - l[i]) * (r[i] - i);
        }
        return ans;
    }
};
```
Python3 代码:
```Python
class Solution:
    def numSubarrayBoundedMax(self, nums: List[int], a: int, b: int) -> int:
        n, ans = len(nums), 0
        l, r = [-1] * n, [n] * n
        stk = []
        for i in range(n):
            while stk and nums[stk[-1]] < nums[i]:
                r[stk.pop()] = i
            stk.append(i)
        stk = []
        for i in range(n - 1, -1, -1):
            while stk and nums[stk[-1]] <= nums[i]:
                l[stk.pop()] = i
            stk.append(i)
        for i in range(n):
            if a <= nums[i] <= b:
                ans += (i - l[i]) * (r[i] - i)
        return ans
```
TypeScript 代码:
```TypeScript
function numSubarrayBoundedMax(nums: number[], a: number, b: number): number {
    let n = nums.length, ans = 0
    const l = new Array<number>(n).fill(-1), r = new Array<number>(n).fill(n)
    let stk = new Array<number>()
    for (let i = 0; i < n; i++) {
        while (stk.length > 0 && nums[stk[stk.length - 1]] < nums[i]) r[stk.pop()] = i
        stk.push(i)
    }
    stk = new Array<number>()
    for (let i = n - 1; i >= 0; i--) {
        while (stk.length > 0 && nums[stk[stk.length - 1]] <= nums[i]) l[stk.pop()] = i
        stk.push(i)
    }
    for (let i = 0; i < n; i++) {
        if (nums[i] < a || nums[i] > b) continue
        ans += (i - l[i]) * (r[i] - i)
    }
    return ans
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n)$

---

### 模拟

除了统计「每个 $nums[i]$ 作为子数组最大值时,所能贡献的子数组个数」以外,我们还可以统计「每个 $nums[i]$ 作为子数组右端点时,所能贡献的子数组个数」。

具体的,我们从前往后处理每个 $nums[i]$,并统计其作为子数组右端点时,所能贡献的子数组个数。同时使用变量 `j` 和 `k` 分别记录最近一次满足「$nums[i]$ 范围落在 $[a, b]$ 之间」以及「$nums[i]$ 数值大于 $b$」的下标位置。

遍历过程中根据 $nums[i]$ 与规定范围 $[a, b]$ 之间的关系进行分情况讨论:

* $nums[i]$ 大于 $b$,$nums[i]$ 作为右端点,必不可能贡献合法子数组。更新 `k`;
* $nums[i]$ 小于 $a$,此时 $nums[i]$ 想作为右端点的话,子数组必须有其他满足「范围落在 $[a, b]$ 之间」的其他数,而最近一个满足要求的位置为 $j$,若有 $j > k$,说明范围在 $(k, j]$ 均能作为子数组的左端点,累加方案数 $j - k$;若有 $j < k$,说明我们无法找到任何一个左端点,使得形成的子数组满足要求(要么最值不在 $[a, b]$ 范围内,要么有 $[a, b]$ 范围内的数,但最大值又大于 `b` 值);
* $nums[i]$ 落在范围 $[a, b]$,此时 $nums[i]$ 想作为右端点的话,只需要找到左边第一个数值大于 $b$ 的数值即可(即变量 `k`),累加方案数 $i - k$。更新 `j`。

Java 代码:
```Java
class Solution {
    public int numSubarrayBoundedMax(int[] nums, int a, int b) {
        int n = nums.length, ans = 0;
        for (int i = 0, j = -1, k = -1; i < n; i++) {
            if (nums[i] > b) {
                k = i;
            } else {
                if (nums[i] < a) {
                    if (j > k) ans += j - k;
                } else {
                    ans += i - k;
                    j = i;
                }
            }
        }
        return ans;
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    int numSubarrayBoundedMax(vector<int>& nums, int a, int b) {
        int n = nums.size(), ans = 0;
        for (int i = 0, j = -1, k = -1; i < n; i++) {
            if (nums[i] > b) {
                k = i;
            } else {
                if (nums[i] < a) {
                    if (j > k) ans += j - k;
                } else {
                    ans += i - k;
                    j = i;
                }
            }
        }
        return ans;
    }
};
```
Python3 代码:
```Python
class Solution:
    def numSubarrayBoundedMax(self, nums: List[int], a: int, b: int) -> int:
        n, ans = len(nums), 0
        j, k = -1, -1
        for i in range(n):
            if nums[i] > b:
                k = i
            else:
                if nums[i] < a:
                    ans += j - k if j > k else 0
                else:
                    ans += i - k
                    j = i
        return ans
```
TypeScript 代码:
```TypeScript
function numSubarrayBoundedMax(nums: number[], a: number, b: number): number {
    let n = nums.length, ans = 0
    for (let i = 0, j = -1, k = -1; i < n; i++) {
        if (nums[i] > b) {
            k = i
        } else {
            if (nums[i] < a) {
                if (j > k) ans += j - k
            } else {
                ans += i - k
                j = i
            }
        }
    }
    return ans
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


Question:
给定两个字符串,`s` 和 `goal`。如果在若干次旋转操作之后,`s` 能变成 `goal`,那么返回 `true`。

`s` 的旋转操作就是将 `s` 最左边的字符移动到最右边。

* 例如, 若 `s = 'abcde'`,在旋转一次之后结果就是`'bcdea'` 。

示例 1:
```
输入: s = "abcde", goal = "cdeab"

输出: true
```
示例 2:
```
输入: s = "abcde", goal = "abced"

输出: false
```

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

Solution:
### 模拟

由于每次旋转操作都是将最左侧字符移动到最右侧,因此如果 `goal` 可由 `s` 经过多步旋转而来,那么 `goal` 必然会出现在 `s + s` 中,即满足 `(s + s).contains(goal)`,同时为了 `s` 本身过长导致的结果成立,我们需要先确保两字符串长度相等。

代码:
```Java
class Solution {
    public boolean rotateString(String s, String goal) {
        return s.length() == goal.length() && (s + s).contains(goal);
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n)$

---

### 关于 `contains` 操作的复杂度说明

看到不少同学对 `contains` 的复杂度写成 $O(n)$ 有疑问。

在 Java 中,`contains` 实际最终是通过 `indexOf(char[] source, int sourceOffset, int sourceCount, char[] target, int targetOffset, int targetCount, int fromIndex)` 来拿到子串在原串的下标,通过判断下标是否为 $-1$ 来得知子串是否在原串出现过。

我们知道一个较为普通的子串匹配算法的复杂度通为 $O(n*k)$,其中 $n$ 和 $k$ 分别是原串和子串的长度,而一些复杂度上较为优秀的算法可以做到 $O(n + k)$,例如 [KMP](https://leetcode-cn.com/problems/implement-strstr/solution/shua-chuan-lc-shuang-bai-po-su-jie-fa-km-tb86/)。

从复杂度上来看 KMP 似乎要更好,但实际上对于 `indexOf` 这一高频操作而言,KMP 的预处理逻辑和空间开销都是不可接受的。

因此在 OpenJDK 中的 `indexOf` 源码中,你看不到诸如 KMP 这一类「最坏情况下仍为线性复杂度」的算法实现。

但是 `contains` 的复杂度真的就是 $O(n * k)$ 吗?

其实并不是,这取决于 JVM 是否有针对 `indexOf` 的优化,在最为流行 HotSpot VM 中,就有对 `indexOf` 的优化。

使用以下两行命令执行 `Main.java`,会得到不同的用时。

```Java
// Main.java
import java.util.*;
class Main {
    static String ss = "1900216589537958049456207450268985232242852754963049829410964867980510717200606495004259179775210762723370289106970649635773837906542900276476226929871813370344374628795427969854262816333971458418647697497933767559786473164055741512717436542961770628985635269208255141092673831132865";
    static String pp = "830411595466023844647269831101019568881117264597716557501027220546437084223034983361631430958163646150071031688420479928498493050624766427709034028819288384316713084883575266906600102801186671777455503932259958027055697399984336592981698127456301551509241";
    static int cnt = (int) 1e8;
    static public void main(String[] args) {
        long start = System.currentTimeMillis();
        while (cnt-- > 0) ss.contains(pp);
        System.out.println(System.currentTimeMillis() - start);
    }
}
```

环境说明:
```Shell
➜  java -version
java version "1.8.0_131"
Java(TM) SE Runtime Environment (build 1.8.0_131-b11)
Java HotSpot(TM) 64-Bit Server VM (build 25.131-b11, mixed mode)
```

先执行 `javac Main.java` 进行编译后:

1. 使用原始的 `indexOf` 实现进行匹配(执行多次,平均耗时为基准值 $X$):
```
java -XX:+UnlockDiagnosticVMOptions -XX:DisableIntrinsic=_indexOf Main
```
2. 使用 HotSpot VM 优化的 `indexOf` 进行匹配(执行多次,平均耗时为基准值 $X$ 的 $[0.55, 0.65]$ 之间):
```
java Main
```

因此实际运行的 `contains` 操作的复杂度为多少并不好确定,但可以确定是要优于 $O(n * k)$ 的。

### 最后

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

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

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

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


797. 所有可能的路径

Question:
给你一个有 n 个节点的 有向无环图(DAG),请你找出所有从节点 0 到节点 n-1 的路径并输出(不要求按特定顺序)

二维数组的第 i 个数组中的单元都表示有向图中 i 号节点所能到达的下一些节点,空就是没有下一个结点了。

译者注:有向图是有方向的,即规定了 a→b 你就不能从 b→a 。

示例 1:

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

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

解释:有两条路径 0 -> 1 -> 3 和 0 -> 2 -> 3
```
示例 2:

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

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

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

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

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

提示:
* n == graph.length
* 2 <= n <= 15
* 0 <= graph[i][j] < n
* graph[i][j] != i(即,不存在自环)
* graph[i] 中的所有元素 互不相同
* 保证输入为 有向无环图(DAG)

Solution:
### DFS

$n$ 只有 $15$,且要求输出所有方案,因此最直观的解决方案是使用 `DFS` 进行爆搜。

起始将 $0$ 进行加入当前答案,当 $n - 1$ 被添加到当前答案时,说明找到了一条从 $0$ 到 $n - 1$ 的路径,将当前答案加入结果集。

当我们决策到第 $x$ 位(非零)时,该位置所能放入的数值由第 $x - 1$ 位已经填入的数所决定,同时由于给定的 $graph$ 为有向无环图(拓扑图),因此按照第 $x - 1$ 位置的值去决策第 $x$ 位的内容,必然不会决策到已经在当前答案的数值,否则会与 $graph$ 为有向无环图(拓扑图)的先决条件冲突。

换句话说,与一般的爆搜不同的是,我们不再需要 $vis$ 数组来记录某个点是否已经在当前答案中。

代码:
```Java
class Solution {
    int[][] g;
    int n;
    List<List<Integer>> ans = new ArrayList<>();
    List<Integer> cur = new ArrayList<>();
    public List<List<Integer>> allPathsSourceTarget(int[][] graph) {
        g = graph;
        n = g.length;
        cur.add(0);
        dfs(0);
        return ans;
    }
    void dfs(int u) {
        if (u == n - 1) {
            ans.add(new ArrayList<>(cur));
            return ;
        }
        for (int next : g[u]) {
            cur.add(next);
            dfs(next);
            cur.remove(cur.size() - 1);
        }
    }
}
```
* 时间复杂度:共有 $n$ 个节点,每个节点有选和不选两种决策,总的方案数最多为 $2^n$,对于每个方案最坏情况需要 $O(n)$ 的复杂度进行拷贝并添加到结果集。整体复杂度为 $O(n * 2^n)$
* 空间复杂度:最多有 $2^n$ 种方案,每个方案最多有 $n$ 个元素。整体复杂度为 $O(n * 2^n)$

### 最后

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

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

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

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


798. 得分最高的最小轮调

Question:
给你一个数组 $nums$,我们可以将它按一个非负整数 $k$ 进行轮调,这样可以使数组变为 $[nums[k], nums[k + 1], ... nums[nums.length - 1], nums[0], nums[1], ..., nums[k-1]]$ 的形式。此后,任何值小于或等于其索引的项都可以记作一分。

例如,数组为 $nums = [2,4,1,3,0]$,我们按 $k = 2$ 进行轮调后,它将变成 $[1,3,0,2,4]$。这将记为 $3$ 分,因为 $1 > 0$  [不计分]、$3 > 1$ [不计分]、$0 <= 2$ [计 $1$ 分]、$2 <= 3$ [计 $1$ 分],$4 <= 4$ [计 $1$ 分]。
在所有可能的轮调中,返回我们所能得到的最高分数对应的轮调下标 $k$ 。如果有多个答案,返回满足条件的最小的下标 $k$ 。

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

输出:3

解释:
下面列出了每个 k 的得分:
k = 0,  nums = [2,3,1,4,0],    score 2
k = 1,  nums = [3,1,4,0,2],    score 3
k = 2,  nums = [1,4,0,2,3],    score 3
k = 3,  nums = [4,0,2,3,1],    score 4
k = 4,  nums = [0,2,3,1,4],    score 3
所以我们应当选择 k = 3,得分最高。
```
示例 2:
```
输入:nums = [1,3,0,2,4]

输出:0

解释:
nums 无论怎么变化总是有 3 分。
所以我们将选择最小的 k,即 0。
```

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

Solution:
### 上下界分析 + 差分应用

为了方便,令 $n$ 为 $nums$ 长度(中文的数据范围是错的,数组长度应该是 $10^5$,不是 $20000$)。

对于给定的 $nums$ 而言,有效的轮调范围为 $[0, n - 1]$,即对于任意 $nums[i]$ 而言,可取的下标共有 $n$ 种。

假定当前下标为 $i$,轮调次数为 $k$,那么轮调后下标为 $i - k$,当新下标为负数时,相当于 $nums[i]$ 出现在比原数组更“靠后”的位置,此时下标等价于 $(i - k + n) \mod n$。

考虑什么情况下 $nums[i]$ 能够得分?

首先新下标的取值范围为 $[0, n - 1]$,即有 $0 \leqslant i - k \leqslant n - 1$ 。由此可分析出 $k$ 的取值范围为:

$$
0 \leqslant i - k \Leftrightarrow k \leqslant i
$$

$$
i - k \leqslant n - 1 \Leftrightarrow i - (n - 1) \leqslant k
$$

即由新下标取值范围可知 $k$ 的上下界分别为 $i$ 和 $i - (n - 1)$。

同时为了满足得分定义,还有 $nums[i] \leqslant i - k$,进行变形可得:

$$
nums[i] \leqslant i - k \Leftrightarrow k \leqslant i - nums[i]
$$

此时我们有两个关于 $k$ 的上界 $k \leqslant i$ 和 $k \leqslant i - nums[i]$,由于 $nums[i]$ 取值范围为 $[0, n)$,则有 $i - nums[i] \leqslant i$,由于必须同时满足「合法移动(有效下标)」和「能够得分」,我们仅考虑范围更小(更严格)由 $nums[i] \leqslant i - k$ 推导而来的上界 $k \leqslant i - nums[i]$ 即可。

综上,$nums[i]$ 能够得分的 $k$ 的取值范围为 $[i - (n - 1), i - nums[i]]$。

最后考虑 $[i - (n - 1), i - nums[i]]$(均进行加 $n$ 模 $n$ 转为正数)什么情况下为合法的连续段:

* 当 $i - (n - 1) \leqslant i - nums[i]$ 时,$[i - (n - 1), i - nums[i]]$ 为合法连续段;
* 当 $i - (n - 1) > i - nums[i]$ 时,根据负数下标等价于 $(i - k + n) \mod n$,此时 $[i - (n - 1), i - nums[i]]$ 等价于 $[0, i - nums[i]]$ 和 $[i - (n - 1), n - 1]$ 两段。

至此,我们分析出原数组的每个 $nums[i]$ 能够得分的 $k$ 的取值范围,假定取值范围为 $[l, r]$,我们可以对 $[l, r]$ 进行 $+1$ 标记,代表范围为 $k$ 能够得 $1$ 分,当处理完所有的 $nums[i]$ 后,找到标记次数最多的位置 $k$ 即是答案。

标记操作可使用「差分」实现(不了解差分的同学,可以先看前置🧀:[差分入门模板题](https%3A//mp.weixin.qq.com/s?__biz%3DMzU4NDE3MTEyMA%3D%3D%26mid%3D2247490329%26idx%3D1%26sn%3D6d448a53cd722bbd990fda82bd262857%26chksm%3Dfd9cb006caeb3910758522054564348b7eb4bde333889300bd5d249950be12a5b990b5d2c059%26token%3D168273153%26lang%3Dzh_CN%23rd),里面讲解了差分的两个核心操作「区间修改」&「单点查询」),而找标记次数最多的位置可对差分数组求前缀和再进行遍历即可。

代码:
```Java
class Solution {
    static int N = 100010;
    static int[] c = new int[N];
    void add(int l, int r) {
        c[l] += 1; c[r + 1] -= 1;
    }
    public int bestRotation(int[] nums) {
        Arrays.fill(c, 0);
        int n = nums.length;
        for (int i = 0; i < n; i++) {
            int a = (i - (n - 1) + n) % n, b = (i - nums[i] + n) % n;
            if (a <= b) {
                add(a, b);
            } else {
                add(0, b);
                add(a, n - 1);
            }
        }
        for (int i = 1; i <= n; i++) c[i] += c[i - 1];
        int ans = 0, k = c[0];
        for (int i = 1; i <= n; i++) {
            if (c[i] > k) {
                k = c[i]; ans = i;
            }
        }
        return ans;
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


Question:
我们把玻璃杯摆成金字塔的形状,其中 第一层 有 `1` 个玻璃杯, 第二层 有 `2` 个,依次类推到第 `100` 层,每个玻璃杯 (`250ml`) 将盛有香槟。

从顶层的第一个玻璃杯开始倾倒一些香槟,当顶层的杯子满了,任何溢出的香槟都会立刻等流量的流向左右两侧的玻璃杯。当左右两边的杯子也满了,就会等流量的流向它们左右两边的杯子,依次类推。(当最底层的玻璃杯满了,香槟会流到地板上)

例如,在倾倒一杯香槟后,最顶层的玻璃杯满了。倾倒了两杯香槟后,第二层的两个玻璃杯各自盛放一半的香槟。在倒三杯香槟后,第二层的香槟满了 - 此时总共有三个满的玻璃杯。在倒第四杯后,第三层中间的玻璃杯盛放了一半的香槟,他两边的玻璃杯各自盛放了四分之一的香槟,如下图所示。

现在当倾倒了非负整数杯香槟后,返回第 i 行 j 个玻璃杯所盛放的香槟占玻璃杯容积的比例( i 和 j 都从0开始)。

示例 1:
```
输入: poured(倾倒香槟总杯数) = 1, query_glass(杯子的位置数) = 1, query_row(行数) = 1

输出: 0.00000

解释: 我们在顶层(下标是(0,0))倒了一杯香槟后,没有溢出,因此所有在顶层以下的玻璃杯都是空的。
```
示例 2:
```
输入: poured(倾倒香槟总杯数) = 2, query_glass(杯子的位置数) = 1, query_row(行数) = 1

输出: 0.50000

解释: 我们在顶层(下标是(0,0)倒了两杯香槟后,有一杯量的香槟将从顶层溢出,位于(1,0)的玻璃杯和(1,1)的玻璃杯平分了这一杯香槟,所以每个玻璃杯有一半的香槟。
```
示例 3:
```
输入: poured = 100000009, query_row = 33, query_glass = 17

输出: 1.00000
```

提示:
* $0 <= poured <= 10^9$
* $0 <= query_glass <= query_row < 100$

Solution:
### 线性 DP

为了方便,我们令 `poured` 为 `k`,`query_row` 和 `query_glass` 分别为 $n$ 和 $m$。

定义 $f[i][j]$ 为第 $i$ 行第 $j$ 列杯子所经过的水的流量(而不是最终剩余的水量)。

起始我们有 $f[0][0] = k$,最终答案为 $\min(f[n][m], 1)$。

不失一般性考虑 $f[i][j]$ 能够更新哪些状态:显然当 $f[i][j]$ 不足 $1$ 的时候,不会有水从杯子里溢出,即 $f[i][j]$ 将不能更新其他状态;当 $f[i][j]$ 大于 $1$ 时,将会有 $f[i][j] - 1$ 的水会等量留到下一行的杯子里,所流向的杯子分别是「第 $i + 1$ 行第 $j$ 列的杯子」和「第 $i + 1$ 行第 $j + 1$ 列的杯子」,增加流量均为 $\frac{f[i][j] - 1}{2}$,即有 $f[i + 1][j] += \frac{f[i][j] - 1}{2}$ 和 $f[i + 1][j + 1] += \frac{f[i][j] - 1}{2}$。

Java 代码:
```Java
class Solution {
    public double champagneTower(int k, int n, int m) {
        double[][] f = new double[n + 10][n + 10];
        f[0][0] = k;
        for (int i = 0; i <= n; i++) {
            for (int j = 0; j <= i; j++) {
                if (f[i][j] <= 1) continue;
                f[i + 1][j] += (f[i][j] - 1) / 2;
                f[i + 1][j + 1] += (f[i][j] - 1) / 2;
            }
        }
        return Math.min(f[n][m], 1);
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    double champagneTower(int k, int n, int m) {
        vector<vector<double>> f(n + 10, vector<double>(n + 10, 0.0));
        f[0][0] = k;
        for (int i = 0; i <= n; ++i) {
            for (int j = 0; j <= i; ++j) {
                if (f[i][j] <= 1) continue;
                f[i + 1][j] += (f[i][j] - 1.0) / 2.0;
                f[i + 1][j + 1] += (f[i][j] - 1.0) / 2.0;
            }
        }
        return min(f[n][m], 1.0);
    }
};
```
Python3 代码:
```Python
class Solution:
    def champagneTower(self, k: int, n: int, m: int) -> float:
        f = [[0] * (n + 10) for _ in range(n + 10)]
        f[0][0] = k
        for i in range(n + 1):
            for j in range(i + 1):
                if f[i][j] <= 1:
                    continue
                f[i + 1][j] += (f[i][j] - 1) / 2
                f[i + 1][j + 1] += (f[i][j] - 1) / 2
        return min(f[n][m], 1)
```
TypeScript 代码:
```TypeScript
function champagneTower(k: number, n: number, m: number): number {
    const f = new Array<Array<number>>()
    for (let i = 0; i < n + 10; i++) f.push(new Array<number>(n + 10).fill(0))
    f[0][0] = k
    for (let i = 0; i <= n; i++) {
        for (let j = 0; j <= i; j++) {
            if (f[i][j] <= 1) continue
            f[i + 1][j] += (f[i][j] - 1) / 2
            f[i + 1][j + 1] += (f[i][j] - 1) / 2
        }
    }
    return Math.min(f[n][m], 1)
}
```
* 时间复杂度:$O(n^2)$
* 空间复杂度:$O(n^2)$

### 最后

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

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

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

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


LeetCode/801-810/801. 使序列递增的最小交换次数（困难）.md

801. 使序列递增的最小交换次数

Question:
我们有两个长度相等且不为空的整型数组 `nums1` 和 `nums2`。

在一次操作中,我们可以交换 $nums1[i]$ 和 $nums2[i]$ 的元素。

例如,如果 $nums1 = [1,2,3,8]$ ,$nums2 =[5,6,7,4]$ ,你可以交换 $i = 3$ 处的元素,得到 $nums1 =[1,2,3,4]$ 和 $nums2 =[5,6,7,8]$ 。

返回 使 `nums1` 和 `nums2` 严格递增 所需操作的最小次数 。

数组 `arr` 严格递增 且  $arr[0] < arr[1] < arr[2] < ... < arr[arr.length - 1]$ 。

注意:

用例保证可以实现操作。

示例 1:
```
输入: nums1 = [1,3,5,4], nums2 = [1,2,3,7]

输出: 1

解释:
交换 A[3] 和 B[3] 后,两个数组如下:
A = [1, 3, 5, 7] , B = [1, 2, 3, 4]
两个数组均为严格递增的。
```
示例 2:
```
输入: nums1 = [0,3,5,8,9], nums2 = [2,1,4,6,9]

输出: 1
```

提示:
* $2 <= nums1.length <= 10^5$
* $nums2.length = nums1.length$
* $0 <= nums1[i], nums2[i] <= 2 \times 10^5$

Solution:
### 基本分析

这是一道很裸的状态机 DP 运用题。

由于每次交换只会发生在两数组的相同位置上,使得问题变得简单:**仅需考虑交换当前位置后,当前元素与前后元素大小关系变化即可**。

又因为我们会从前往后处理每个位置,因此只需要考虑当前位置与前一位置的大小关系即可。

---

### 状态机 DP

定义 $f[i][j]$ 为考虑下标范围为 $[0, i]$ 的元素,且位置 $i$ 的交换状态为 $j$ 时(其中 $j = 0$ 为不交换,$j = 1$ 为交换)两数组满足严格递增的最小交换次数。

最终答案为 $\min(f[n - 1][0], f[n - 1][1])$,同时我们有显而易见的初始化条件 $f[0][0] = 0$ 和 $f[0][1] = 1$,其余未知状态初始化为正无穷。

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

* 若 $nums1[i] > nums1[i - 1]$ 且 $nums2[i] > nums2[i - 1]$(即顺序位满足要求),此时要么当前位置 $i$ 和前一位置 $i - 1$ 都不交换,要么同时发生交换,此时有(分别对应两个位置「都不交换」和「都交换」):

$$
\begin{cases}
f[i][0] = f[i - 1][0] \\
f[i][1] = f[i - 1][1] + 1
\end{cases}
$$

* 若 $nums1[i] > nums2[i - 1]$ 且 $nums2[i] > nums1[i - 1]$(即交叉位满足要求),此时当前位置 $i$ 和前一位置 $i - 1$ 只能有其一发生交换,此时有(分别对应「前一位置交换」和「当前位置交换」):

$$
\begin{cases}
f[i][0] = \min(f[i][0], f[i - 1][1]); \\
f[i][1] = \min(f[i][1], f[i - 1][0] + 1)
\end{cases}
$$

Java 代码:
```Java
class Solution {
    public int minSwap(int[] nums1, int[] nums2) {
        int n = nums1.length;
        int[][] f = new int[n][2];
        for (int i = 1; i < n; i++) f[i][0] = f[i][1] = n + 10;
        f[0][1] = 1;
        for (int i = 1; i < n; i++) {
            if (nums1[i] > nums1[i - 1] && nums2[i] > nums2[i - 1]) {
                f[i][0] = f[i - 1][0];
                f[i][1] = f[i - 1][1] + 1;
            }
            if (nums1[i] > nums2[i - 1] && nums2[i] > nums1[i - 1]) {
                f[i][0] = Math.min(f[i][0], f[i - 1][1]);
                f[i][1] = Math.min(f[i][1], f[i - 1][0] + 1);
            }
        }
        return Math.min(f[n - 1][0], f[n - 1][1]);
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    int minSwap(vector<int>& nums1, vector<int>& nums2) {
        int n = nums1.size();
        vector<vector<int>> f(n, vector<int>(2, n + 10));
        f[0][0] = 0; f[0][1] = 1;
        for (int i = 1; i < n; ++i) {
            if (nums1[i] > nums1[i - 1] && nums2[i] > nums2[i - 1]) {
                f[i][0] = f[i - 1][0];
                f[i][1] = f[i - 1][1] + 1;
            }
            if (nums1[i] > nums2[i - 1] && nums2[i] > nums1[i - 1]) {
                f[i][0] = min(f[i][0], f[i - 1][1]);
                f[i][1] = min(f[i][1], f[i - 1][0] + 1);
            }
        }
        return min(f[n - 1][0], f[n - 1][1]);
    }
};
```
Python 代码:
```Python
class Solution:
    def minSwap(self, nums1: List[int], nums2: List[int]) -> int:
        n = len(nums1)
        f = [[0, 0] for _ in range(n)]
        for i in range(1, n):
            f[i][0] = f[i][1] = n + 10
        f[0][1] = 1
        for i in range(1, n):
            if nums1[i] > nums1[i - 1] and nums2[i] > nums2[i - 1]:
                f[i][0], f[i][1] = f[i - 1][0], f[i - 1][1] + 1
            if nums1[i] > nums2[i - 1] and nums2[i] > nums1[i - 1]:
                f[i][0], f[i][1] = min(f[i][0], f[i - 1][1]), min(f[i][1], f[i - 1][0] + 1)
        return min(f[n - 1][0], f[n - 1][1])
```
TypeScript 代码:
```TypeScript
function minSwap(nums1: number[], nums2: number[]): number {
    const n = nums1.length
    const f = new Array<Array<number>>(n)
    f[0] = [0, 1]
    for (let i = 1; i < n; i++) f[i] = [n + 10, n + 10]
    for (let i = 1; i < n; i++) {
        if (nums1[i] > nums1[i - 1] && nums2[i] > nums2[i - 1]) {
            f[i][0] = f[i - 1][0]
            f[i][1] = f[i - 1][1] + 1
        }
        if (nums1[i] > nums2[i - 1] && nums2[i] > nums1[i - 1]) {
            f[i][0] = Math.min(f[i][0], f[i - 1][1])
            f[i][1] = Math.min(f[i][1], f[i - 1][0] + 1)
        }
    }
    return Math.min(f[n - 1][0], f[n - 1][1])
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n)$

---

### 滚动数组

利用 $f[i][X]$ 仅依赖于 $f[i - 1][X]$,我们可以采用「滚动数组」的方式,通过机械性操作将空间优化到 $O(1)$。

Java 代码:
```Java
class Solution {
    public int minSwap(int[] nums1, int[] nums2) {
        int n = nums1.length;
        int[][] f = new int[2][2];
        f[0][1] = 1;
        for (int i = 1; i < n; i++) {
            int a = n + 10, b = n + 10;
            int prev = (i - 1) & 1, cur = i & 1; // 避免重复的 & 操作
            if (nums1[i] > nums1[i - 1] && nums2[i] > nums2[i - 1]) {
                a = f[prev][0];
                b = f[prev][1] + 1;
            }
            if (nums1[i] > nums2[i - 1] && nums2[i] > nums1[i - 1]) {
                a = Math.min(a, f[prev][1]);
                b = Math.min(b, f[prev][0] + 1);
            }
            f[cur][0] = a; f[cur][1] = b;
        }
        return Math.min(f[(n - 1) & 1][0], f[(n - 1) & 1][1]);
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    int minSwap(vector<int>& nums1, vector<int>& nums2) {
        int n = nums1.size();
        vector<vector<int>> f(2, vector<int>(2, 0));
        f[0][1] = 1;
        for (int i = 1; i < n; ++i) {
            int a = n + 10, b = n + 10;
            int prev = (i - 1) & 1, cur = i & 1;
            if (nums1[i] > nums1[i - 1] && nums2[i] > nums2[i - 1]) {
                a = f[prev][0];
                b = f[prev][1] + 1;
            }
            if (nums1[i] > nums2[i - 1] && nums2[i] > nums1[i - 1]) {
                a = min(a, f[prev][1]);
                b = min(b, f[prev][0] + 1);
            }
            f[cur][0] = a;
            f[cur][1] = b;
        }
        return min(f[(n - 1) & 1][0], f[(n - 1) & 1][1]);
    }
};
```
Python 代码:
```Python
class Solution:
    def minSwap(self, nums1: List[int], nums2: List[int]) -> int:
        n = len(nums1)
        f = [[0, 1], [0, 0]]
        for i in range(1, n):
            a, b = n + 10, n + 10
            prev, cur = (i - 1) & 1, i & 1
            if nums1[i] > nums1[i - 1] and nums2[i] > nums2[i - 1]:
                a, b = f[prev][0], f[prev][1] + 1
            if nums1[i] > nums2[i - 1] and nums2[i] > nums1[i - 1]:
                a, b = min(a, f[prev][1]), min(b, f[prev][0] + 1)
            f[cur][0], f[cur][1] = a, b
        return min(f[(n - 1) & 1][0], f[(n - 1) & 1][1])
```
TypeScript 代码:
```TypeScript
function minSwap(nums1: number[], nums2: number[]): number {
    const n = nums1.length
    const f = new Array<Array<number>>(2)
    f[0] = [0, 1], f[1] = [0, 0]
    for (let i = 1; i < n; i++) {
        let a = n + 10, b = n + 10
        const prev = (i - 1) & 1, cur = i & 1
        if (nums1[i] > nums1[i - 1] && nums2[i] > nums2[i - 1]) {
            a = f[prev][0]
            b = f[prev][1] + 1
        }
        if (nums1[i] > nums2[i - 1] && nums2[i] > nums1[i - 1]) {
            a = Math.min(a, f[prev][1])
            b = Math.min(b, f[prev][0] + 1)
        }
        f[cur][0] = a; f[cur][1] = b
    }
    return Math.min(f[(n - 1) & 1][0], f[(n - 1) & 1][1])
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


802. 找到最终的安全状态

Question:
在有向图中,以某个节点为起始节点,从该点出发,每一步沿着图中的一条有向边行走。如果到达的节点是终点(即它没有连出的有向边),则停止。

对于一个起始节点,如果从该节点出发,无论每一步选择沿哪条有向边行走,最后必然在有限步内到达终点,则将该起始节点称作是 安全 的。

返回一个由图中所有安全的起始节点组成的数组作为答案。答案数组中的元素应当按 升序 排列。

该有向图有 n 个节点,按 0 到 n - 1 编号,其中 n 是 graph 的节点数。图以下述形式给出:graph[i] 是编号 j 节点的一个列表,满足 (i, j) 是图的一条有向边。

示例 1:

```
输入:graph = [[1,2],[2,3],[5],[0],[5],[],[]]

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

解释:示意图如上。
```
示例 2:
```
输入:graph = [[1,2,3,4],[1,2],[3,4],[0,4],[]]

输出:[4]
```

提示:
* n == graph.length
* 1 <= n <= $10^4$
* 0 <= graph[i].length <= n
* graph[i] 按严格递增顺序排列。
* 图中可能包含自环。
* 图中边的数目在范围 [1, 4 * $10^4$] 内。

Solution:
### 基本分析 & 拓扑排序

为了方便,我们令点数为 $n$,边数为 $m$。

**在图论中,一个有向无环图必然存在至少一个拓扑序与之对应,反之亦然。**

如果对拓扑排序不熟悉的小伙伴,可以看看 [拓扑排序](https://baike.baidu.com/item/%E6%8B%93%E6%89%91%E6%8E%92%E5%BA%8F)。

**简单来说,就是将图中的所有节点展开成一维序列,对于序列中任意的节点 $(u, v)$,如果在序列中 $u$ 在 $v$ 的前面,则说明在图中存在从 $u$ 出发达到 $v$ 的通路,即 $u$ 排在 $v$ 的前面。反之亦然。**

同时,我们需要知晓「入度」和「出度」的概念:

* 入度:有多少条边直接指向该节点;
* 出度:由该节点指出边的有多少条。

因此,对于有向图的拓扑排序,我们可以使用如下思路输出拓扑序(`BFS` 方式):

1. 起始时,将所有入度为 $0$ 的节点进行入队(入度为 $0$,说明没有边指向这些节点,将它们放到拓扑排序的首部,不会违反拓扑序定义);
2. **从队列中进行节点出队操作,出队序列就是对应我们输出的拓扑序**。
    对于当前弹出的节点 $x$,遍历 $x$ 的所有出度,即遍历所有由 $x$ 直接指向的节点 $y$,对 $y$ 做入度减一操作(因为 $x$ 节点已经从队列中弹出,被添加到拓扑序中,等价于从 $x$ 节点从有向图中被移除,相应的由 $x$ 发出的边也应当被删除,带来的影响是与 $x$ 相连的节点 $y$ 的入度减一);
3. 对 $y$ 进行入度减一之后,检查 $y$ 的入度是否为 $0$,如果为 $0$ 则将 $y$ 入队(当 $y$ 的入度为 $0$,说明有向图中在 $y$ 前面的所有的节点均被添加到拓扑序中,此时 $y$ 可以作为拓扑序的某个片段的首部被添加,而不是违反拓扑序的定义);
3. 循环流程 $2$、$3$ 直到队列为空。

---

### 证明

上述 `BFS` 方法能够求得「某个有向无环图的拓扑序」的前提是:**我们必然能够找到(至少)一个「入度为 $0$ 的点」,在起始时将其入队。**

这可以使用反证法进行证明:假设有向无环图的拓扑序不存在入度为 $0$ 的点。

**那么从图中的任意节点 $x$ 进行出发,沿着边进行反向检索,由于不存在入度为 $0$ 的节点,因此每个点都能够找到上一个节点。**

**当我们找到一条长度为 $n + 1$ 的反向路径时,由于我们图中只有 $n$ 个节点,因此必然有至少一个节点在该路径中重复出现,即该反向路径中存在环,与我们「有向无环图」的起始条件冲突。**

得证「有向无环图的拓扑序」必然存在(至少)一个「入度为 $0$ 的点」。

即按照上述的 `BFS` 方法,我们能够按照流程迭代下去,直到将有向无环图的所有节点从队列中弹出。

反之,如果一个图不是「有向无环图」的话,我们是无法将所有节点入队的,因此能够通过入队节点数量是否为 $n$ 来判断是否为有向无环图。

---

### 反向图 + 拓扑排序

回到本题,根据题目对「安全节点」的定义,我们知道如果一个节点无法进入「环」的话则是安全的,否则是不安全的。

另外我们发现,**如果想要判断某个节点数 $x$ 是否安全,起始时将 $x$ 进行入队,并跑一遍拓扑排序是不足够的。**

因为我们无法事先确保 $x$ 满足入度为 $0$ 的要求,所以当我们处理到与 $x$ 相连的节点 $y$ 时,可能会存在 $y$ 节点入度无法减到 $0$ 的情况,即我们无法输出真实拓扑序中,从 $x$ 节点开始到结尾的完整部分。

但是根据我们「证明」部分的启发,我们可以将所有边进行反向,这时候「入度」和「出度」翻转了。

对于那些反向图中「入度」为 $0$ 的点集 $x$,其实就是原图中「出度」为 $0$ 的节点,它们「出度」为 $0$,根本没指向任何节点,必然无法进入环,是安全的;同时由它们在反向图中指向的节点(在原图中**只指向**它们的节点),必然也是无法进入环的,对应到反向图中,就是那些减去 $x$ 对应的入度之后,入度为 $0$ 的节点。

因此整个过程就是将图进行反向,再跑一遍拓扑排序,如果某个节点出现在拓扑序列,说明其进入过队列,说明其入度为 $0$,其是安全的,其余节点则是在环内非安全节点。

> 另外,这里的存图方式还是使用前几天一直使用的「链式前向星」,关于几个数组的定义以及其他的存图方式,如果还是有不熟悉的小伙伴可以在 [这里](https://mp.weixin.qq.com/s?__biz=MzU4NDE3MTEyMA==&mid=2247488007&idx=1&sn=9d0dcfdf475168d26a5a4bd6fcd3505d&chksm=fd9cb918caeb300e1c8844583db5c5318a89e60d8d552747ff8c2256910d32acd9013c93058f&token=1711035050&lang=zh_CN#rd) 查阅,本次不再赘述。

代码:
```Java
class Solution {
    int N = (int)1e4+10, M = 4 * N;
    int idx;
    int[] he = new int[N], e = new int[M], ne = new int[M];
    int[] cnts = new int[N];
    void add(int a, int b) {
        e[idx] = b;
        ne[idx] = he[a];
        he[a] = idx++;
    }
    public List<Integer> eventualSafeNodes(int[][] g) {
        int n = g.length;
        // 存反向图,并统计入度
        Arrays.fill(he, -1);
        for (int i = 0; i < n; i++) {
            for (int j : g[i]) {
                add(j, i);
                cnts[i]++;
            }
        }
        // BFS 求反向图拓扑排序
        Deque<Integer> d = new ArrayDeque<>();
        for (int i = 0; i < n; i++) {
            if (cnts[i] == 0) d.addLast(i);
        }
        while (!d.isEmpty()) {
            int poll = d.pollFirst();
            for (int i = he[poll]; i != -1; i = ne[i]) {
                int j = e[i];
                if (--cnts[j] == 0) d.addLast(j);
            }
        }
        // 遍历答案:如果某个节点出现在拓扑序列,说明其进入过队列,说明其入度为 0
        List<Integer> ans = new ArrayList<>();
        for (int i = 0; i < n; i++) {
            if (cnts[i] == 0) ans.add(i);
        }
        return ans;
    }
}
```
* 时间复杂度:$O(n + m)$
* 空间复杂度:$O(n + m)$

### 最后

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

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

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

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


804. 唯一摩尔斯密码词

Question:
国际摩尔斯密码定义一种标准编码方式,将每个字母对应于一个由一系列点和短线组成的字符串, 比如:

* `'a'` 对应 `".-"` ,
* `'b'` 对应 `"-..."` ,
* `'c'` 对应 `"-.-."`,以此类推。

为了方便,所有 $26$ 个英文字母的摩尔斯密码表如下:
```
[".-","-...","-.-.","-..",".","..-.","--.","....","..",".---","-.-",".-..","--","-.","---",".--.","--.-",".-.","...","-","..-","...-",".--","-..-","-.--","--.."]
```
给你一个字符串数组 `words`,每个单词可以写成每个字母对应摩尔斯密码的组合。

例如,`"cab"` 可以写成 `"-.-..--..."` ,(即 `"-.-."` + `".-"` + `"-..."` 字符串的结合)。我们将这样一个连接过程称作单词翻译。

对 `words` 中所有单词进行单词翻译,返回不同单词翻译的数量。

示例 1:
```
输入: words = ["gin", "zen", "gig", "msg"]

输出: 2

解释:
各单词翻译如下:
"gin" -> "--...-."
"zen" -> "--...-."
"gig" -> "--...--."
"msg" -> "--...--."

共有 2 种不同翻译, "--...-." 和 "--...--.".
```
示例 2:
```
输入:words = ["a"]

输出:1
```

提示:
* $1 <= words.length <= 100$
* $1 <= words[i].length <= 12$
* $words[i]$ 由小写英文字母组成

Solution:
### 模拟

根据题意进行模拟即可。

代码:
```Java
class Solution {
    String[] ss = new String[]{".-","-...","-.-.","-..",".","..-.","--.","....","..",".---","-.-",".-..","--","-.","---",".--.","--.-",".-.","...","-","..-","...-",".--","-..-","-.--","--.."};
    public int uniqueMorseRepresentations(String[] words) {
        Set<String> set = new HashSet<>();
        for (String s : words) {
            StringBuilder sb = new StringBuilder();
            for (char c : s.toCharArray()) sb.append(ss[c - 'a']);
            set.add(sb.toString());
        }
        return set.size();
    }
}
```
* 时间复杂度:$O(\sum_{i = 0}^{words.length - 1}words[i].length())$
* 空间复杂度:$O(\sum_{i = 0}^{words.length - 1}words[i].length())$

### 最后

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

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

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

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


805. 数组的均值分割

Question:
给定你一个整数数组 `nums`。

我们要将 `nums` 数组中的每个元素移动到 `A` 数组 或者 `B` 数组中,使得 `A` 数组和 `B` 数组不为空,并且 `average(A) == average(B)` 。

如果可以完成则返回 `true`, 否则返回 `false`。

注意:对于数组 `arr`,  `average(arr)` 是 `arr` 的所有元素除以 `arr` 长度的和。

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

输出: true

解释: 我们可以将数组分割为 [1,4,5,8] 和 [2,3,6,7], 他们的平均值都是4.5。
```
示例 2:
```
输入: nums = [3,1]

输出: false
```

提示:
* $1 <= nums.length <= 30$
* $0 <= nums[i] <= 10^4$

Solution:
### 折半搜索 + 二进制枚举 + 哈希表 + 数学

##### 提示一:将长度为 $n$,总和为 $sum$ 的原数组划分为两组,使得两数组平均数相同,可推导出该平均数 $avg = \frac{sum}{n}$

若两数组平均数相同,则由两数组组成的新数组(对应原数组 `nums`)平均数不变,而原数组的平均数可直接算得。

##### 提示二:原数组长度为 $30$,直接通过「二进制枚举」的方式来做,计算量为 $2^{30}$,该做法无须额外空间,但会 `TLE`。

所谓的直接使用「二进制枚举」来做,是指用二进制表示中的 `0` 和 `1` 分别代表在划分数组两边。

如果直接对原数组进行「二进制枚举」,由于每个 $nums[i]$ 都有两种决策(归属于数组 `A` 或 `B`),共有 $2^{30}$ 个状态需要计算。同时每个状态 `state` 而言,需要 $O(n)$ 的时间复杂度来判定,但整个过程只需要有限个变量。

因此直接使用「二进制枚举」是一个无须额外空间 `TLE`  做法。

##### 提示三:空间换时间

我们不可避免需要使用「枚举」的思路,也不可避免对每个 $nums[i]$ 有两种决策。**但我们可以考虑缩减每次搜索的长度,将搜索分多次进行。**

具体的,我们可以先对 `nums` 的前半部分进行搜索,并将搜索记录以「二元组 $(tot, cnt)$ 的形式」进行缓存(`map` 套 `set`),其中 `tot` 为划分元素总和,`cnt` 为划分元素个数;随后再对 `nums` 的后半部分进行搜索,假设当前搜索到结果为 $(tot', cnt')$,假设我们能够通过“某种方式”算得另外一半的结果为何值,并能在缓存结果中查得该结果,则说明存在合法划分方案,返回 `true`。

通过「折半 + 缓存结果」的做法,将「累乘」的计算过程优化成「累加」计算过程。

##### 提示四:何为“某种方式”

假设我们已经缓存了前半部分的所有搜索结果,并且在搜索后半部分数组时,当前搜索结果为 $(tot', cnt')$,应该在缓存结果中搜索何值来确定是否存在合法划分方案。

假设存在合法方案,且在缓存结果应当被搜索的结果为 $(x, y)$。我们有 $\frac{tot' + x}{cnt' + y} = avg = \frac{sum}{n}$。

因此我们可以直接枚举系数 $k$ 来进行判定,其中 $k$ 的取值范围为 $[\max(1, cnt'), n - 1]$,结合上式算得 $t = k \times \frac{sum}{n}$,若在缓存结果中存在 $(t - tot', k - cnt')$,说明存在合法方案。

Java 代码:
```Java
class Solution {
    public boolean splitArraySameAverage(int[] nums) {
        int n = nums.length, m = n / 2, sum = 0;
        for (int x : nums) sum += x;
        Map<Integer, Set<Integer>> map = new HashMap<>();
        for (int s = 0; s < (1 << m); s++) {
            int tot = 0, cnt = 0;
            for (int i = 0; i < m; i++) {
                if (((s >> i) & 1) == 1) {
                    tot += nums[i]; cnt++;
                }
            }
            Set<Integer> set = map.getOrDefault(tot, new HashSet<>());
            set.add(cnt);
            map.put(tot, set);
        }
        for (int s = 0; s < (1 << (n - m)); s++) {
            int tot = 0, cnt = 0;
            for (int i = 0; i < (n - m); i++) {
                if (((s >> i) & 1) == 1) {
                    tot += nums[i + m]; cnt++;
                }
            }
            for (int k = Math.max(1, cnt); k < n; k++) {
                if (k * sum % n != 0) continue;
                int t = k * sum / n;
                if (!map.containsKey(t - tot)) continue;
                if (!map.get(t - tot).contains(k - cnt)) continue;
                return true;
            }
        }
        return false;
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    bool splitArraySameAverage(vector<int>& nums) {
        int n = nums.size(), m = n / 2, sum = 0;
        for (int x : nums) sum += x;
        map<int, unordered_set<int>> hashMap;
        for (int s = 0; s < (1 << m); s++) {
            int tot = 0, cnt = 0;
            for (int i = 0; i < m; i++) {
                if ((s >> i) & 1) {
                    tot += nums[i]; cnt++;
                }
            }
            hashMap[tot].insert(cnt);
        }
        for (int s = 0; s < (1 << (n - m)); s++) {
            int tot = 0, cnt = 0;
            for (int i = 0; i < (n - m); i++) {
                if ((s >> i) & 1) {
                    tot += nums[i + m]; cnt++;
                }
            }
            for (int k = max(1, cnt); k < n; k++) {
                if (k * sum % n != 0) continue;
                int t = k * sum / n;
                if (hashMap.count(t - tot) == 0) continue;
                if (!hashMap[t - tot].count(k - cnt)) continue;
                return true;
            }
        }
        return false;
    }
};
```
Python 代码:
```Python
from collections import defaultdict

class Solution:
    def splitArraySameAverage(self, nums: List[int]) -> bool:
        n, m = len(nums), len(nums) // 2
        sum_nums = sum(nums)
        hash_map = defaultdict(set)
        for s in range(1 << m):
            tot = cnt = 0
            for i in range(m):
                if ((s >> i) & 1):
                    tot += nums[i]
                    cnt += 1
            hash_map[tot].add(cnt)
        for s in range(1 << (n - m)):
            tot = cnt = 0
            for i in range(n - m):
                if ((s >> i) & 1):
                    tot += nums[i + m]
                    cnt += 1
            for k in range(max(1, cnt), n):
                if (k * sum_nums) % n != 0: continue
                t = (k * sum_nums) // n
                if (t - tot) not in hash_map: continue
                if (k - cnt) not in hash_map[t - tot]: continue
                return True
        return False
```
TypeScript 代码:
```TypeScript
function splitArraySameAverage(nums: number[]): boolean {
    let n = nums.length, m = Math.floor(n / 2), sum = 0;
    for (let x of nums) sum += x;
    let map = new Map();
    for (let s = 0; s < (1 << m); s++) {
        let tot = 0, cnt = 0;
        for (let i = 0; i < m; i++) {
            if (((s >> i) & 1) == 1) {
                tot += nums[i]; cnt++;
            }
        }
        let set = map.get(tot) || new Set();
        set.add(cnt);
        map.set(tot, set);
    }
    for (let s = 0; s < (1 << (n - m)); s++) {
        let tot = 0, cnt = 0;
        for (let i = 0; i < (n - m); i++) {
            if (((s >> i) & 1) == 1) {
                tot += nums[i + m]; cnt++;
            }
        }
        for (let k = Math.max(1, cnt); k < n; k++) {
            if (k * sum % n != 0) continue;
            let t = Math.floor(k * sum / n);
            if (!map.has(t - tot)) continue;
            if (!map.get(t - tot).has(k - cnt)) continue;
            return true;
        }
    }
    return false;
};
```
* 时间复杂度:对原数组前半部分搜索复杂度为 $O(2^{\frac{n}{2}})$;对原数组后半部分搜索复杂度为 $O(2^{\frac{n}{2}})$,搜索同时检索前半部分的结果需要枚举系数 `k`,复杂度为 $O(n)$。整体复杂度为 $O(n \times 2^{\frac{n}{2}})$
* 空间复杂度:$O(2^{\frac{n}{2}})$

### 最后

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

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

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

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


806. 写字符串需要的行数

Question:
我们要把给定的字符串 `S` 从左到右写到每一行上,每一行的最大宽度为 $100$ 个单位,如果我们在写某个字母的时候会使这行超过了 $100$ 个单位,那么我们应该把这个字母写到下一行。

我们给定了一个数组 `widths`,这个数组 $widths[0]$ 代表 `'a'` 需要的单位, $widths[1]$ 代表 `'b'` 需要的单位,..., $widths[25]$ 代表 `'z'` 需要的单位。

现在回答两个问题:至少多少行能放下 `S`,以及最后一行使用的宽度是多少个单位?

将你的答案作为长度为 $2$ 的整数列表返回。

示例 1:
```
输入:
widths = [10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10]
S = "abcdefghijklmnopqrstuvwxyz"

输出: [3, 60]

解释:
所有的字符拥有相同的占用单位10。所以书写所有的26个字母,
我们需要2个整行和占用60个单位的一行。
```
示例 2:
```
输入:
widths = [4,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10]
S = "bbbcccdddaaa"

输出: [2, 4]

解释:
除去字母'a'所有的字符都是相同的单位10,并且字符串 "bbbcccdddaa" 将会覆盖 9 * 10 + 2 * 4 = 98 个单位.
最后一个字母 'a' 将会被写到第二行,因为第一行只剩下2个单位了。
所以,这个答案是2行,第二行有4个单位宽度。
```

注:
* 字符串 `S` 的长度在 $[1, 1000]$ 的范围。
* `S` 只包含小写字母。
* `widths` 是长度为 $26$ 的数组。
* $widths[i]$ 值的范围在 $[2, 10]$。

Solution:
### 模拟

根据题意进行模拟即可。

使用变量 `a` 代指当前有多少行是满的,使用变量 `b` 代指当前填充光标所在的位置。

代码:
```Java
class Solution {
    public int[] numberOfLines(int[] widths, String s) {
        int a = 0, b = 0;
        for (char c : s.toCharArray()) {
            int t = widths[c - 'a'];
            if (b + t > 100 && ++a >= 0) b = t;
            else b += t;
        }
        if (b != 0) a++;
        return new int[]{a, b};
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:不使用 `toCharArray` 为 $O(1)$,否则为 $O(n)$

### 最后

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

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

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

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


807. 保持城市天际线

Question:
在二维数组 $grid$ 中,$grid[i][j]$ 代表位于某处的建筑物的高度。 我们被允许增加任何数量(不同建筑物的数量可能不同)的建筑物的高度。 高度 $0$ 也被认为是建筑物。

最后,从新数组的所有四个方向(即顶部,底部,左侧和右侧)观看的“天际线”必须与原始数组的天际线相同。 城市的天际线是从远处观看时,由所有建筑物形成的矩形的外部轮廓。 请看下面的例子。

建筑物高度可以增加的最大总和是多少?

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

输出: 35

解释:
The grid is:
[ [3, 0, 8, 4],
  [2, 4, 5, 7],
  [9, 2, 6, 3],
  [0, 3, 1, 0] ]

从数组竖直方向(即顶部,底部)看“天际线”是:[9, 4, 8, 7]
从水平水平方向(即左侧,右侧)看“天际线”是:[8, 7, 9, 3]

在不影响天际线的情况下对建筑物进行增高后,新数组如下:

gridNew = [ [8, 4, 8, 7],
            [7, 4, 7, 7],
            [9, 4, 8, 7],
            [3, 3, 3, 3] ]
```
说明:
* $1 < grid.length = grid[0].length <= 50。$
* $ grid[i][j]$ 的高度范围是:$[0, 100]$。
* 一座建筑物占据一个$grid[i][j]$:换言之,它们是 $1 x 1 x grid[i][j]$ 的长方体。

Solution:
### 贪心

根据题意,我们需要确保在调整建筑物高度后,从「水平」和「竖直」两个方向所看到的「行」和「列」的最大高度不变。

因此我们可以先通过 $O(n * m)$ 的复杂度预处理出 `grid` 中每行的最大值(使用 $r$ 数组存储),以及每列的最大值(使用 $c$ 数组存储)。

然后在统计答案时,通过判断当前位置 $g[i][j]$ 与 $\min(r[i], c[j])$ 的大小关系来决定当前位置能够增高多少。

可以证明,当每个位置都取得最大的增大高度(局部最优)时,可使得总的增加高度最大(全局最优)。

代码:
```Java
class Solution {
    public int maxIncreaseKeepingSkyline(int[][] grid) {
        int n = grid.length, m = grid[0].length;
        int[] r = new int[n], c = new int[m];
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < m; j++) {
                r[i] = Math.max(r[i], grid[i][j]);
                c[j] = Math.max(c[j], grid[i][j]);
            }
        }
        int ans = 0;
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < m; j++) {
                ans += Math.min(r[i], c[j]) - grid[i][j];
            }
        }
        return ans;
    }
}
```
* 时间复杂度:$O(n * m)$
* 空间复杂度:$O(n + m)$

### 最后

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

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

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

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


Question:
有 `A` 和 `B` 两种类型 的汤,一开始每种类型的汤有 `n` 毫升。

有四种分配操作:

1. 提供 `100ml` 的 汤A 和 `0ml` 的 汤B 。
2. 提供 `75ml` 的 汤A 和 `25ml` 的 汤B 。
3. 提供 `50ml` 的 汤A 和 `50ml` 的 汤B 。
4. 提供 `25ml` 的 汤A 和 `75ml` 的 汤B 。

当我们把汤分配给某人之后,汤就没有了。

每个回合,我们将从四种概率同为 `0.25` 的操作中进行分配选择。

如果汤的剩余量不足以完成某次操作,我们将尽可能分配。

当两种类型的汤都分配完时,停止操作。

注意 不存在先分配 `100 ml` 汤B 的操作。

需要返回的值:汤A 先分配完的概率 +  汤A和汤B 同时分配完的概率 / 2。

返回值在正确答案 $10^{-5}$ 的范围内将被认为是正确的。

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

输出: 0.62500

解释:如果我们选择前两个操作,A 首先将变为空。
对于第三个操作,A 和 B 会同时变为空。
对于第四个操作,B 首先将变为空。
所以 A 变为空的总概率加上 A 和 B 同时变为空的概率的一半是 0.25 *(1 + 1 + 0.5 + 0)= 0.625。
```
示例 2:
```
输入: n = 100

输出: 0.71875
```

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

Solution:
### 数学 + 动态规划

四种分配方式都是 $25$ 的倍数,因此我们可以将 $n$ 进行除以 $25$ 上取整的缩放操作,并将四类操作等价成:

1. 提供 `4ml` 的 汤A 和 `0ml` 的 汤B 。
2. 提供 `3ml` 的 汤A 和 `1ml` 的 汤B 。
3. 提供 `2ml` 的 汤A 和 `2ml` 的 汤B 。
4. 提供 `1ml` 的 汤A 和 `3ml` 的 汤B 。

定义 $f[i][j]$ 为 汤A 剩余 $i$ 毫升,汤B 剩余 $j$ 毫升时的最终概率($概率 = 汤A先分配完的概率 + 汤A和汤B同时分配完的概率 \times 0.5$)。

最终答案为 $f[n][n]$ 为最终答案,考虑任意项存在为 $0$ 情况时的边界情况:

* 若 $i = 0$ 且 $j = 0$,结果为 $0 + \frac{1}{2} = \frac{1}{2}$,即有 $f[0][0] = 0.5$
* 若 $i = 0$ 且 $j > 0$,结果为 $1 + 0 = 1$,即有 $f[0][X] = 1$,其中 $X > 1$
* 若 $i > 0$ 且 $j = 0$,结果为 $0 + 0 = 0$,即有 $f[X][0] = 0$,其中 $X > 1$

其余一般情况为 $i$ 和 $j$ 均不为 $0$,由于四类操作均为等概率,结合题意和状态定义可知:

$$
f[i][j] = \frac{1}{4} \times (f[i - 4][j] + f[i - 3][j - 1] + f[i - 2][j - 2] + f[i - 1][j - 3])
$$

由于 $n = 1e9$,即使进行了除 $25$ 的缩放操作,过多的状态数仍会导致 `TLE`。

此时需要利用「返回值在正确答案 $10^{-5}$ 的范围内将被认为是正确的」来做优化(一下子不太好想到):由于四类操作均是等概率,单个回合期望消耗汤 A 的量为 $2.5$,消耗汤 B 的量为 $1.5$。

因此当 $n$ 足够大,操作回合足够多,汤 A 将有较大的概率结束分配,即当 $n$ 足够大,概率值会趋向于 $1$。

我们考虑多大的 $n$ 能够配合精度误差 $10^{-5}$ 来减少计算量:一个可行的操作是利用上述的 DP 思路 + 二分的方式找到符合精度要求的验算值(不超过 $200$)。

Java 代码:
```Java
class Solution {
    public double soupServings(int n) {
        n = Math.min(200, (int) Math.ceil(n / 25.0));
        double[][] f = new double[n + 10][n + 10];
        f[0][0] = 0.5;
        for (int j = 1; j <= n; j++) f[0][j] = 1;
        for (int i = 1; i <= n; i++) {
            for (int j = 1; j <= n; j++) {
                double a = f[Math.max(i - 4, 0)][j], b = f[Math.max(i - 3, 0)][Math.max(j - 1, 0)];
                double c = f[Math.max(i - 2, 0)][Math.max(j - 2, 0)], d = f[Math.max(i - 1, 0)][Math.max(j - 3, 0)];
                f[i][j] = 0.25 * (a + b + c + d);
            }
        }
        return f[n][n];
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    double soupServings(int n) {
        n = min(200, (int)ceil(n / 25.0));
        vector<vector<double>> f(n + 10, vector<double>(n + 10, 0));
        f[0][0] = 0.5;
        for (int j = 1; j <= n; ++j) f[0][j] = 1;
        for (int i = 1; i <= n; ++i) {
            for (int j = 1; j <= n; ++j) {
                double a = f[max(i - 4, 0)][j], b = f[max(i - 3, 0)][max(j - 1, 0)];
                double c = f[max(i - 2, 0)][max(j - 2, 0)], d = f[max(i - 1, 0)][max(j - 3, 0)];
                f[i][j] = 0.25 * (a + b + c + d);
            }
        }
        return f[n][n];
    }
};
```
Python 代码:
```Python
class Solution:
    def soupServings(self, n: int) -> float:
        n = min(200, math.ceil(n / 25))
        f = [[0] * (n + 10) for _ in range(n + 10)]
        f[0][0] = 0.5
        for j in range(1, n + 10):
            f[0][j] = 1
        for i in range(1, n + 1):
            for j in range(1, n + 1):
                a, b = f[max(i - 4, 0)][j], f[max(i - 3, 0)][max(j - 1, 0)]
                c, d = f[max(i - 2, 0)][max(j - 2, 0)], f[max(i - 1, 0)][max(j - 3, 0)]
                f[i][j] = 0.25 * (a + b + c + d)
        return f[n][n]
```
TypeScript 代码:
```TypeScript
function soupServings(n: number): number {
    n = Math.min(200, Math.ceil(n / 25.0));
    const f: number[][] = Array(n + 10).fill(0).map(() => Array(n + 10).fill(0));
    f[0][0] = 0.5;
    for (let j = 1; j <= n; j++) f[0][j] = 1;
    for (let i = 1; i <= n; i++) {
        for (let j = 1; j <= n; j++) {
            let a = f[Math.max(i - 4, 0)][j], b = f[Math.max(i - 3, 0)][Math.max(j - 1, 0)];
            let c = f[Math.max(i - 2, 0)][Math.max(j - 2, 0)], d = f[Math.max(i - 1, 0)][Math.max(j - 3, 0)];
            f[i][j] = 0.25 * (a + b + c + d);
        }
    }
    return f[n][n];
};
```
* 时间复杂度:$O(m^2)$,其中 $m = 200$ 为验算值
* 空间复杂度:$O(m^2)$

### 最后

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

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

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

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


809. 情感丰富的文字

Question:
有时候人们会用重复写一些字母来表示额外的感受,比如 `"hello" -> "heeellooo", "hi" -> "hiii"`。我们将相邻字母都相同的一串字符定义为相同字母组,例如:`"h", "eee", "ll", "ooo"`。

对于一个给定的字符串 `S` ,如果另一个单词能够通过将一些字母组扩张从而使其和 `S` 相同,我们将这个单词定义为可扩张的(`stretchy`)。

扩张操作定义如下:选择一个字母组(包含字母 `c` ),然后往其中添加相同的字母 `c` 使其长度达到 `3` 或以上。

例如,以 `"hello"` 为例:

我们可以对字母组 `"o"` 扩张得到 `"hellooo"`,但是无法以同样的方法得到 `"helloo"` 因为字母组 `"oo"` 长度小于 `3`。此外,我们可以进行另一种扩张 `"ll" -> "lllll"` 以获得 `"helllllooo"`。

如果 `S = "helllllooo"`,那么查询词 `"hello"` 是可扩张的,因为可以对它执行这两种扩张操作使得 `query = "hello" -> "hellooo" -> "helllllooo" = S`。

输入一组查询单词,输出其中可扩张的单词数量。

示例:
```
输入:
S = "heeellooo"
words = ["hello", "hi", "helo"]

输出:1

解释:
我们能通过扩张 "hello" 的 "e" 和 "o" 来得到 "heeellooo"。
我们不能通过扩张 "helo" 来得到 "heeellooo" 因为 "ll" 的长度小于 3 。
```

提示:
* $0 <= len(S) <= 100$
* $0 <= len(words) <= 100$
* $0 <= len(words[i]) <= 100$
* `S` 和所有在 `words` 中的单词都只由小写字母组成。

Solution:
### 双指针

该题最难的部分就是理解 “扩张” 操作:假设有两个字符相同的连续段 `a` 和 `b`,如何判断 `a` 是否能由 `b` 扩张而来。

忘记掉题目所说的规则,我们重新定义 “扩张” 操作:

* 当 `a` 和 `b` 长度相同,定义为可扩张;
* 当 `a` 和 `b` 长度不同,根据「`a` 和 `b` 长度对比」以及「`a` 的长度大小」分情况讨论:
    * 当 `b` 长度大于 `a`,不可扩张;
    * 当 `a` 长度大于 `b`,**我们不一定要拿整一段的 `b` 进行扩张,可以拿 `b` 中的一个字符进行扩张。** 因此只需要满足扩张后的长度(`a` 的长度)大于等于 $3$ 即可定义为可扩张。

搞明白何为 “扩张” 后,剩余的则是简单的「双指针 + 模拟」做法。

Java 代码:
```Java
class Solution {
    public int expressiveWords(String s, String[] words) {
        int n = s.length(), ans = 0;
        out:for (String word : words) {
            int m = word.length(), i = 0, j = 0;
            while (i < n && j < m) {
                if (s.charAt(i) != word.charAt(j)) continue out;
                int a = i, b = j;
                while (a < n && s.charAt(a) == s.charAt(i)) a++;
                while (b < m && word.charAt(b) == word.charAt(j)) b++;
                a -= i; b -= j;
                if (a != b && (b > a || a < 3)) continue out;
                i += a; j += b;
            }
            if (i == n && j == m) ans++;
        }
        return ans;
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    int expressiveWords(string s, vector<string>& words) {
        int n = s.size(), ans = 0;
        for (const string &word : words) {
            int m = word.size(), i = 0, j = 0;
            while (i < n && j < m) {
                if (s[i] != word[j]) break;
                int a = i, b = j;
                while (a < n && s[a] == s[i]) a++;
                while (b < m && word[b] == word[j]) b++;
                a -= i; b -= j;
                if (a != b && (b > a || a < 3)) break;
                i += a; j += b;
                if (i == n && j == m) ans++;
            }
        }
        return ans;
    }
};
```
Python 代码:
```Python
class Solution:
    def expressiveWords(self, s: str, words: List[str]) -> int:
        n, ans = len(s), 0
        for word in words:
            m, i, j = len(word), 0, 0
            ok = True
            while ok and i < n and j < m:
                if s[i] != word[j]:
                    ok = False
                a, b = i, j
                while a < n and s[a] == s[i]:
                    a += 1
                while b < m and word[b] == word[j]:
                    b += 1
                a, b = a - i, b - j
                if a != b and (b > a or a < 3):
                    ok = False
                i, j = i + a, j + b
            if ok and i == n and j == m:
                ans += 1
        return ans
```
TypeScript 代码:
```TypeScript
function expressiveWords(s: string, words: string[]): number {
    let n = s.length, ans = 0
    out:for (const word of words) {
        let m = word.length, i = 0, j = 0
        while (i < n && j < m) {
            if (s[i] != word[j]) continue out
            let a = i, b = j
            while (a < n && s[a] == s[i]) a++
            while (b < m && word[b] == word[j]) b++
            a -= i; b -= j;
            if (a != b && (b > a || a < 3)) continue out
            i += a; j += b;
        }
        if (i == n && j == m) ans++;
    }
    return ans
}
```
* 时间复杂度:$O(n \times m + \sum_{i = 0}^{m - 1}words[i].length)$,其中 `n` 为字符串 `s` 的长度,`m` 为数组 `words` 的长度
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


Question:
黑板上写着一个非负整数数组 `nums[i]` 。

`Alice` 和 `Bob` 轮流从黑板上擦掉一个数字,`Alice` 先手。如果擦除一个数字后,剩余的所有数字按位异或运算得出的结果等于 `0` 的话,当前玩家游戏失败。 (另外,如果只剩一个数字,按位异或运算得到它本身;如果无数字剩余,按位异或运算结果为 `0`。)

换种说法就是,轮到某个玩家时,如果当前黑板上所有数字按位异或运算结果等于 `0`,这个玩家获胜。

假设两个玩家每步都使用最优解,当且仅当 `Alice` 获胜时返回 `true`。

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

输出: false

解释:
Alice 有两个选择: 擦掉数字 1 或 2。
如果擦掉 1, 数组变成 [1, 2]。剩余数字按位异或得到 1 XOR 2 = 3。那么 Bob 可以擦掉任意数字,因为 Alice 会成为擦掉最后一个数字的人,她总是会输。
如果 Alice 擦掉 2,那么数组变成[1, 1]。剩余数字按位异或得到 1 XOR 1 = 0。Alice 仍然会输掉游戏。
```

提示:
* $1 <= N <= 1000$
* $0 <= nums[i] <= 2^{16}$

Solution:
### 基本分析

这是一道「博弈论」题。

如果没接触过博弈论,其实很难想到,特别是数据范围为 $10^3$,很具有迷惑性。

如果接触过博弈论,对于这种「判断先手后手的必胜必败」的题目,博弈论方向是一个优先考虑的方向。

根据题意,如果某位玩家在操作前所有数值异或和为 $0$,那么该玩家胜利。要我们判断给定序列时,先手是处于「必胜态」还是「必败态」,如果处于「必胜态」返回 `True`,否则返回 `False`。

对于博弈论的题目,通常有两类的思考方式:

1. 经验分析:见过类似的题目,猜一个性质,然后去证明该性质是否可推广。
2. 状态分析:根据题目给定的规则是判断「胜利」还是「失败」来决定优先分析「必胜态」还是「必败态」时具有何种性质,然后证明性质是否可推广。

---

### 博弈论

**对于本题,给定的是判断「胜利」的规则(在给定序列的情况下,如果所有数值异或和为 $0$ 可立即判断胜利,其他情况无法立即判断胜负),那么我们应该优先判断何为「先手必胜态」,如果不好分析,才考虑分析后手的「必败态」。**

接下来是分情况讨论:

#### 1. 如果给定的序列异或和为 $0$,游戏开始时,先手直接获胜:

由此推导出性质一:**给定序列 `nums` 的异或和为 $0$,先手处于「必胜态」,返回 `True`。**

#### 2. 如果给定序列异或和不为 $0$,我们需要分析,先手获胜的话,序列会满足何种性质:

显然如果要先手获胜,则需要满足「**先手去掉一个数,剩余数值异或和必然不为 $0$;同时后手去掉一个数后,剩余数值异或和必然为 $0$**」。

换句话说,我们需要分析**什么情况下「经过一次后手操作」后,序列会以上述情况 $1$ 的状态,回到先手的局面**。

也就是反过来分析想要出现「后手必败态」,序列会有何种性质。

假设后手操作前的异或和为 $Sum$($Sum \neq 0$),「后手必败态」意味着去掉任意数字后异或和为 $0$。

同时根据「相同数值异或结果为 $0$」的特性,我们知道去掉某个数值,等价于在原有异或和的基础上异或上这个值。

则有:

$$
Sum' = Sum ⊕ nums[i] = 0
$$

由于是「后手必败态」,因此 $i$ 取任意一位,都满足上述式子。

则有:

$$
Sum ⊕ nums[0] = ... = Sum ⊕ nums[k] = ... = Sum ⊕ nums[n - 1] = 0
$$

同时根据「任意数值与 $0$ 异或数值不变」的特性,我们将每一项进行异或:

$$
(Sum ⊕ nums[0]) ⊕ ... ⊕ (Sum ⊕ nums[k]) ⊕ ... ⊕ (Sum ⊕ nums[n - 1]) = 0
$$

根据交换律进行变换:

$$
(Sum ⊕ Sum ⊕ ... ⊕ Sum) ⊕ (nums[0] ⊕ ... ⊕ nums[k] ⊕ ... ⊕ nums[n - 1]) = 0
$$

再结合 $Sum$ 为原序列的异或和可得:

$$
(Sum ⊕ Sum ⊕ ... ⊕ Sum) ⊕ Sum = 0 , Sum \neq 0
$$

至此,我们分析出当处于「后手必败态」时,去掉任意一个数值会满足上述式子。

根据「相同数值偶数次异或结果为 $0$」的特性,可推导出「后手必败态」会导致交回到先手的序列个数为偶数,由此推导后手操作前序列个数为奇数,后手操作前一个回合为偶数。

到这一步,我们推导出想要出现「后手必败态」,先手操作前的序列个数应当为偶数。

那么根据先手操作前序列个数为偶数(且异或和不为 $0$),是否能够推导出必然出现「后手必败态」呢?

**显然是可以的,因为如果不出现「后手必败态」,会与我们前面分析过程矛盾。**

假设先手操作前异或和为 $Xor$(序列数量为偶数,同时 $Xor \neq 0$),如果最终不出现「后手必败态」的话,也就是先手会输掉的话,那么意味着有 $Xor ⊕ nums[i] = 0$,其中 $i$ 为序列的任意位置。利用此性质,像上述分析那样,将每一项进行展开异或,会得到奇数个 $Xor$ 异或结果为 $0$,这与开始的 $Xor \neq 0$ 矛盾。

由此推导出性质二:**只需要保证先手操作前序列个数为偶数时就会出现「后手必败态」,从而确保先手必胜。**

**综上,如果序列 `nums` 本身异或和为 $0$,天然符合「先手必胜态」的条件,答案返回 `True` ;如果序列 `nums` 异或和不为 $0$,但序列长度为偶数,那么最终会出现「后手必败态」,推导出先手必胜,答案返回 `True`。**

代码:
```Java
class Solution {
    public boolean xorGame(int[] nums) {
        int sum = 0;
        for (int i : nums) sum ^= i;
        return sum == 0 || nums.length % 2 == 0;
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(1)$

---

### 总结

事实上,在做题的时候,我也是采取「先假定奇偶性,再证明」的做法,因为这样比较快。

但「假定奇偶性」这一步是比较具有跳跃性的,这有点像我前面说到的「经验分析解法」,而本题解证明没有做任何的前置假定,单纯从「先手必胜态」和「后手必败态」进行推导,最终推导出「先手序列偶数必胜」的性质 ,更符合前面说到的「状态分析解法」。

两种做法殊途同归,在某些博弈论问题上,「经验分析解法」可以通过「归纳」&「反证」很好分析出来,但这要求选手本身具有一定的博弈论基础;而「状态分析解法」则对选手的题量要求低些,逻辑推理能力高些。

两种方法并无优劣之分,都是科学严谨的做法。

### 最后

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

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

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

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


81. 搜索旋转排序数组 II

Question:
已知存在一个按非降序排列的整数数组 nums ,数组中的值不必互不相同。

在传递给函数之前,nums 在预先未知的某个下标 k(0 <= k < nums.length)上进行了 旋转 ,使数组变为 [nums[k], nums[k+1], ..., nums[n-1], nums[0], nums[1], ..., nums[k-1]](下标 从 0 开始 计数)。例如, [0,1,2,4,4,4,5,6,6,7] 在下标 5 处经旋转后可能变为 [4,5,6,6,7,0,1,2,4,4] 。

给你 旋转后 的数组 nums 和一个整数 target ,请你编写一个函数来判断给定的目标值是否存在于数组中。如果 nums 中存在这个目标值 target ,则返回 true ,否则返回 false 。

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

输出:true
```
示例 2:
```
输入:nums = [2,5,6,0,0,1,2], target = 3

输出:false
```

提示:
* 1 <= nums.length <= 5000
* -$10^4$ <= nums[i] <= $10^4$
* 题目数据保证 nums 在预先未知的某个下标上进行了旋转
* -$10^4$ <= target <= $10^4$

Solution:
### 二分

根据题意,我们知道,所谓的旋转其实就是「将某个下标前面的所有数整体移到后面,使得数组从整体有序变为分段有序」。

但和 [33. 搜索旋转排序数组](https://leetcode-cn.com/problems/search-in-rotated-sorted-array/solution/sou-suo-xuan-zhuan-pai-xu-shu-zu-by-leetcode-solut/) 不同的是,本题元素并不唯一。

**这意味着我们无法直接根据与** $nums[0]$ **的大小关系,将数组划分为两段,即无法通过「二分」来找到旋转点。**

**因为「二分」的本质是二段性,并非单调性。只要一段满足某个性质,另外一段不满足某个性质,就可以用「二分」。**

如果你有看过我 [严格 O(logN),一起看清二分的本质](https://leetcode-cn.com/problems/search-in-rotated-sorted-array/solution/shua-chuan-lc-yan-ge-ologn100yi-qi-kan-q-xifo/) 这篇题解,你应该很容易就理解上句话的意思。如果没有也没关系,我们可以先解决本题,在理解后你再去做 [33. 搜索旋转排序数组](https://leetcode-cn.com/problems/search-in-rotated-sorted-array/solution/sou-suo-xuan-zhuan-pai-xu-shu-zu-by-leetcode-solut/),我认为这两题都是一样的,不存在先后关系。

举个🌰,我们使用数据 [0,1,2,2,2,3,4,5] 来理解为什么不同的旋转点会导致「二段性丢失」:

代码:
```java []
class Solution {
    public boolean search(int[] nums, int t) {
        int n = nums.length;
        int l = 0, r = n - 1;
        // 恢复二段性
        while (l < r && nums[0] == nums[r]) r--;

        // 第一次二分,找旋转点
        while (l < r) {
            int mid = l + r + 1 >> 1;
            if (nums[mid] >= nums[0]) {
                l = mid;
            } else {
                r = mid - 1;
            }
        }

        int idx = n;
        if (nums[r] >= nums[0] && r + 1 < n) idx = r + 1;

        // 第二次二分,找目标值
        int ans = find(nums, 0, idx - 1, t);
        if (ans != -1) return true;
        ans = find(nums, idx, n - 1, t);
        return ans != -1;
    }
    int find(int[] nums, int l, int r, int t) {
        while (l < r) {
            int mid = l + r >> 1;
            if (nums[mid] >= t) {
                r = mid;
            } else {
                l = mid + 1;
            }
        }
        return nums[r] == t ? r : -1;
    }
}
```
* 时间复杂度:恢复二段性处理中,最坏的情况下(考虑整个数组都是同一个数)复杂度是 $O(n)$,而之后的找旋转点和目标值都是「二分」,复杂度为 $O(log{n})$。整体复杂度为 $O(n)$ 的。
* 空间复杂度:$O(1)$。

***

### 进阶

如果真正理解「二分」的话,本题和 [33. 搜索旋转排序数组](https://leetcode-cn.com/problems/search-in-rotated-sorted-array/solution/sou-suo-xuan-zhuan-pai-xu-shu-zu-by-leetcode-solut/) 区别不大。

建议大家在完成两题的基础上试试 [面试题 10.03. 搜索旋转数组](https://leetcode-cn.com/problems/search-rotate-array-lcci/) 。

***

### 其他「二分」相关题解

* 二分模板
    [29. 两数相除](https://leetcode-cn.com/problems/divide-two-integers/) : [二分 + 倍增乘法解法(含模板)](https://leetcode-cn.com/problems/divide-two-integers/solution/shua-chuan-lc-er-fen-bei-zeng-cheng-fa-j-m73b/)

* 二分本质 & 恢复二段性处理

    [33. 搜索旋转排序数组(找目标值)](https://leetcode-cn.com/problems/search-in-rotated-sorted-array/) : [严格 O(logN),一起看清二分的本质](https://leetcode-cn.com/problems/search-in-rotated-sorted-array/solution/shua-chuan-lc-yan-ge-ologn100yi-qi-kan-q-xifo/)

    [81. 搜索旋转排序数组 II(找目标值)](https://leetcode-cn.com/problems/search-in-rotated-sorted-array-ii/) : [详解为何元素相同会导致 O(n),一起看清二分的本质](https://leetcode-cn.com/problems/search-in-rotated-sorted-array-ii/solution/gong-shui-san-xie-xiang-jie-wei-he-yuan-xtam4/)

    [153. 寻找旋转排序数组中的最小值(找最小值)](https://leetcode-cn.com/problems/find-minimum-in-rotated-sorted-array/) : [严格 O(logN),一起看清二分的本质](https://leetcode-cn.com/problems/find-minimum-in-rotated-sorted-array/solution/gong-shui-san-xie-yan-ge-olognyi-qi-kan-6d969/)

    [154. 寻找旋转排序数组中的最小值 II(找最小值)](https://leetcode-cn.com/problems/find-minimum-in-rotated-sorted-array-ii/) : [详解为何元素相同会导致 O(n),一起看清二分的本质](https://leetcode-cn.com/problems/find-minimum-in-rotated-sorted-array-ii/solution/gong-shui-san-xie-xiang-jie-wei-he-yuan-7xbty/)

* 二分 check 函数如何确定
    [34. 在排序数组中查找元素的第一个和最后一个位置](https://leetcode-cn.com/problems/find-first-and-last-position-of-element-in-sorted-array/) : [考察对「二分」的理解,以及 check 函数的「大于 小于」怎么写](https://leetcode-cn.com/problems/find-first-and-last-position-of-element-in-sorted-array/solution/gong-shui-san-xie-kao-cha-dui-er-fen-de-86bk0/)

* 二分答案的题目
    [1482. 制作 m 束花所需的最少天数](https://leetcode-cn.com/problems/minimum-number-of-days-to-make-m-bouquets/) : [利用「二段性」找分割点,以及优化 check 函数](https://leetcode-cn.com/problems/minimum-number-of-days-to-make-m-bouquets/solution/gong-shui-san-xie-li-yong-er-duan-xing-z-ysv4/)
    [1011. 在 D 天内送达包裹的能力](https://leetcode-cn.com/problems/capacity-to-ship-packages-within-d-days/) : [利用「二段性」找分割点,以及如何确定「二分范围」](https://leetcode-cn.com/problems/capacity-to-ship-packages-within-d-days/solution/gong-shui-san-xie-li-yong-er-duan-xing-z-95zj/)

### 最后

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

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

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

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


LeetCode/81-90/82. 删除排序链表中的重复元素 II（中等）.md

82. 删除排序链表中的重复元素 II

Question:
存在一个按升序排列的链表,给你这个链表的头节点 `head`,请你删除链表中所有存在数字重复情况的节点,只保留原始链表中没有重复出现的数字。

返回同样按升序排列的结果链表。

示例 1:

```
输入:head = [1,2,3,3,4,4,5]

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

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

输出:[2,3]
```

提示:
* 链表中节点数目在范围 $[0, 300]$ 内
* $-100 <= Node.val <= 100$
* 题目数据保证链表已经按升序排列

Solution:
### 基本思路

几乎所有的链表题目,都具有相似的解题思路。

1. 建一个虚拟头节点 `dummy` 以减少边界判断,往后的答案链表会接在 `dummy` 后面
2. 使用 `tail` 代表当前有效链表的结尾
3. 通过原输入的 `head` 指针进行链表扫描

我们会确保「进入外层循环时 `head` 不会与上一节点相同」,因此插入时机:

1. `head` 已经没有下一个节点,`head` 可以被插入
2. `head` 有一下个节点,但是值与 `head` 不相同,`head` 可以被插入

Java 代码:
```Java
class Solution {
    public ListNode deleteDuplicates(ListNode head) {
        ListNode dummy = new ListNode();
        ListNode tail = dummy;
        while (head != null) {
            // 进入循环时,确保了 head 不会与上一节点相同
            if (head.next == null || head.val != head.next.val) {
                tail.next = head;
                tail = head;
            }
            // 如果 head 与下一节点相同,跳过相同节点
            while (head.next != null && head.val == head.next.val) head = head.next;
            head = head.next;
        }
        tail.next = null;
        return dummy.next;
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    ListNode* deleteDuplicates(ListNode* head) {
        ListNode* dummy = new ListNode();
        ListNode* tail = dummy;
        while (head != nullptr) {
            // 进入循环时,确保了 head 不会与上一节点相同
            if (head->next == nullptr || head->val != head->next->val) {
                tail->next = head;
                tail = head;
            }
            // 如果 head 与下一节点相同,跳过相同节点
            while (head->next != nullptr && head->val == head->next->val) head = head->next;
            head = head->next;
        }
        tail->next = nullptr;
        return dummy->next;
    }
};
```
Python 代码:
```Python
class Solution:
    def deleteDuplicates(self, head: Optional[ListNode]) -> Optional[ListNode]:
        dummy = ListNode()
        tail = dummy
        while head:
            # 进入循环时,确保了 head 不会与上一节点相同
            if not head.next or head.val != head.next.val:
                tail.next = head
                tail = head
            # 如果 head 与下一节点相同,跳过相同节点
            while head.next and head.val == head.next.val: head = head.next
            head = head.next
        tail.next = None
        return dummy.next
```
TypeScript 代码:
```TypeScript
function deleteDuplicates(head: ListNode | null): ListNode | null {
    const dummy: ListNode = new ListNode();
    let tail: ListNode | null = dummy;
    while (head) {
        // 进入循环时,确保了 head 不会与上一节点相同
        if (!head.next || head.val !== head.next.val) {
            tail.next = head;
            tail = head;
        }
        // 如果 head 与下一节点相同,跳过相同节点
        while (head.next && head.val === head.next.val) head = head.next;
        head = head.next;
    }
    tail.next = null;
    return dummy.next;
};
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(1)$

---

### 拓展

如果问题变为「相同节点保留一个」,该如何实现?

还是类似的解题思路。

1. 建一个虚拟头节点 `dummy` 以减少边界判断,往后的答案链表会接在 `dummy` 后面
2. 使用 `tail` 代表当前有效链表的结尾
3. 通过原输入的 `head` 指针进行链表扫描

Java 代码:
```Java
class Solution {
    public ListNode deleteDuplicates(ListNode head) {
        if (head == null) return head;
        ListNode dummy = new ListNode(-109);
        ListNode tail = dummy;
        while (head != null) {
            // 值不相等才追加,确保了相同的节点只有第一个会被添加到答案
            if (tail.val != head.val) {
                tail.next = head;
                tail = tail.next;
            }
            head = head.next;
        }
        tail.next = null;
        return dummy.next;
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    ListNode* deleteDuplicates(ListNode* head) {
        if (!head) return head;
        ListNode* dummy = new ListNode(-109);
        ListNode* tail = dummy;
        while (head) {
            // 值不相等才追加,确保了相同的节点只有第一个会被添加到答案
            if (tail->val != head->val) {
                tail->next = head;
                tail = tail->next;
            }
            head = head->next;
        }
        tail->next = nullptr;
        return dummy->next;
    }
};
```
Python 代码:
```Python
class Solution:
    def deleteDuplicates(self, head: Optional[ListNode]) -> Optional[ListNode]:
        if not head: return head
        dummy = ListNode(-109)
        tail = dummy
        while head:
            # 值不相等才追加,确保了相同的节点只有第一个会被添加到答案
            if tail.val != head.val:
                tail.next = head
                tail = tail.next
            head = head.next
        tail.next = None
        return dummy.next
```
TypeScript 代码:
```TypeScript
function deleteDuplicates(head: ListNode | null): ListNode | null {
    if (!head) return head;
    const dummy: ListNode = new ListNode(-109);
    let tail: ListNode | null = dummy;
    while (head) {
        // 值不相等才追加,确保了相同的节点只有第一个会被添加到答案
        if (tail.val !== head.val) {
            tail.next = head;
            tail = tail.next;
        }
        head = head.next;
    }
    tail.next = null;
    return dummy.next;
};
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


83. 删除排序链表中的重复元素

Question:
存在一个按升序排列的链表,给你这个链表的头节点 `head`,请你删除所有重复的元素,使每个元素只出现一次 。

返回同样按升序排列的结果链表。

示例 1:

```
输入:head = [1,1,2]

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

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

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

提示:
* 链表中节点数目在范围 $[0, 300]$ 内
* $-100 <= Node.val <= 100$
* 题目数据保证链表已经按升序排列

Solution:
### 基本思路

几乎所有的链表题目,都具有相似的解题思路。

1. 建一个虚拟头节点 `dummy` 以减少边界判断,往后的答案链表会接在 `dummy` 后面
2. 使用 `tail` 代表当前有效链表的结尾
3. 通过原输入的 `head` 指针进行链表扫描

Java 代码:
```Java
class Solution {
    public ListNode deleteDuplicates(ListNode head) {
        if (head == null) return head;
        ListNode dummy = new ListNode(-109);
        ListNode tail = dummy;
        while (head != null) {
            // 值不相等才追加,确保了相同的节点只有第一个会被添加到答案
            if (tail.val != head.val) {
                tail.next = head;
                tail = tail.next;
            }
            head = head.next;
        }
        tail.next = null;
        return dummy.next;
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    ListNode* deleteDuplicates(ListNode* head) {
        if (!head) return head;
        ListNode* dummy = new ListNode(-109);
        ListNode* tail = dummy;
        while (head) {
            // 值不相等才追加,确保了相同的节点只有第一个会被添加到答案
            if (tail->val != head->val) {
                tail->next = head;
                tail = tail->next;
            }
            head = head->next;
        }
        tail->next = nullptr;
        return dummy->next;
    }
};
```
Python 代码:
```Python
class Solution:
    def deleteDuplicates(self, head: Optional[ListNode]) -> Optional[ListNode]:
        if not head: return head
        dummy = ListNode(-109)
        tail = dummy
        while head:
            # 值不相等才追加,确保了相同的节点只有第一个会被添加到答案
            if tail.val != head.val:
                tail.next = head
                tail = tail.next
            head = head.next
        tail.next = None
        return dummy.next
```
TypeScript 代码:
```TypeScript
function deleteDuplicates(head: ListNode | null): ListNode | null {
    if (!head) return head;
    const dummy: ListNode = new ListNode(-109);
    let tail: ListNode | null = dummy;
    while (head) {
        // 值不相等才追加,确保了相同的节点只有第一个会被添加到答案
        if (tail.val !== head.val) {
            tail.next = head;
            tail = tail.next;
        }
        head = head.next;
    }
    tail.next = null;
    return dummy.next;
};
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(1)$

---

### 拓展

如果问题变为「重复元素全部删除」,该如何实现?

还是类似的解题思路。

1. 建一个虚拟头节点 `dummy` 以减少边界判断,往后的答案链表会接在 `dummy` 后面
2. 使用 `tail` 代表当前有效链表的结尾
3. 通过原输入的 `head` 指针进行链表扫描

我们会确保「进入外层循环时 `head` 不会与上一节点相同」,因此插入时机:

1. `head` 已经没有下一个节点,`head` 可以被插入
2. `head` 有一下个节点,但是值与 `head` 不相同,`head` 可以被插入

Java 代码:
```Java
class Solution {
    public ListNode deleteDuplicates(ListNode head) {
        ListNode dummy = new ListNode();
        ListNode tail = dummy;
        while (head != null) {
            // 进入循环时,确保了 head 不会与上一节点相同
            if (head.next == null || head.val != head.next.val) {
                tail.next = head;
                tail = head;
            }
            // 如果 head 与下一节点相同,跳过相同节点
            while (head.next != null && head.val == head.next.val) head = head.next;
            head = head.next;
        }
        tail.next = null;
        return dummy.next;
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    ListNode* deleteDuplicates(ListNode* head) {
        ListNode* dummy = new ListNode();
        ListNode* tail = dummy;
        while (head != nullptr) {
            // 进入循环时,确保了 head 不会与上一节点相同
            if (head->next == nullptr || head->val != head->next->val) {
                tail->next = head;
                tail = head;
            }
            // 如果 head 与下一节点相同,跳过相同节点
            while (head->next != nullptr && head->val == head->next->val) head = head->next;
            head = head->next;
        }
        tail->next = nullptr;
        return dummy->next;
    }
};
```
Python 代码:
```Python
class Solution:
    def deleteDuplicates(self, head: Optional[ListNode]) -> Optional[ListNode]:
        dummy = ListNode()
        tail = dummy
        while head:
            # 进入循环时,确保了 head 不会与上一节点相同
            if not head.next or head.val != head.next.val:
                tail.next = head
                tail = head
            # 如果 head 与下一节点相同,跳过相同节点
            while head.next and head.val == head.next.val: head = head.next
            head = head.next
        tail.next = None
        return dummy.next
```
TypeScript 代码:
```TypeScript
function deleteDuplicates(head: ListNode | null): ListNode | null {
    const dummy: ListNode = new ListNode();
    let tail: ListNode | null = dummy;
    while (head) {
        // 进入循环时,确保了 head 不会与上一节点相同
        if (!head.next || head.val !== head.next.val) {
            tail.next = head;
            tail = head;
        }
        // 如果 head 与下一节点相同,跳过相同节点
        while (head.next && head.val === head.next.val) head = head.next;
        head = head.next;
    }
    tail.next = null;
    return dummy.next;
};
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


84. 柱状图中最大的矩形

Question:
给定 `n` 个非负整数,用来表示柱状图中各个柱子的高度。每个柱子彼此相邻,且宽度为 `1` 。

求在该柱状图中,能够勾勒出来的矩形的最大面积。

示例 1:

```
输入:heights = [2,1,5,6,2,3]

输出:10

解释:最大的矩形为图中红色区域,面积为 10
```
示例 2:

```
输入: heights = [2,4]

输出: 4
```

提示:
* $1 <= heights.length <=10^5$
* $0 <= heights[i] <= 10^4$

Solution:
### 单调栈 + 枚举高度

为了方便,我们令 `heights` 为 `hs`。

最终矩形的高度必然取自某个 $hs[i]$,因此我们可以枚举最终矩形的高度来做。

问题转换为当使用某个 $hs[i]$ 作为矩形高度时,该矩形所能取得的最大宽度为多少。

假设我们能够预处理出 `l` 和 `r` 数组,其中 $l[i]$ 代表位置 $i$ 左边最近一个比其小的位置(初始值为 $-1$),$r[i]$ 代表位置 $i$ 右边最近一个比其小的位置(初始值为 $n$),那么 $r[i] - l[i] - 1$ 则是以 $hs[i]$ 作为矩形高度时所能取得的最大宽度。

预处理 `l` 和 `r` 则是经典的「求最近一个比当前值大的位置」单调栈问题。

Java 代码:
```Java
class Solution {
    public int largestRectangleArea(int[] hs) {
        int n = hs.length;
        int[] l = new int[n], r = new int[n];
        Arrays.fill(l, -1); Arrays.fill(r, n);
        Deque<Integer> d = new ArrayDeque<>();
        for (int i = 0; i < n; i++) {
            while (!d.isEmpty() && hs[d.peekLast()] > hs[i]) r[d.pollLast()] = i;
            d.addLast(i);
        }
        d.clear();
        for (int i = n - 1; i >= 0; i--) {
            while (!d.isEmpty() && hs[d.peekLast()] > hs[i]) l[d.pollLast()] = i;
            d.addLast(i);
        }
        int ans = 0;
        for (int i = 0; i < n; i++) {
            int t = hs[i], a = l[i], b = r[i];
            ans = Math.max(ans, (b - a - 1) * t);
        }
        return ans;
    }
}
```
TypeScript 代码:
```TypeScript
function largestRectangleArea(hs: number[]): number {
    const n = hs.length
    const l = new Array<number>(n).fill(-1), r = new Array<number>(n).fill(n)
    const stk = new Array<number>(n).fill(-1)
    let he = 0, ta = 0
    for (let i = 0; i < n; i++) {
        while (he < ta && hs[stk[ta - 1]] > hs[i]) r[stk[--ta]] = i
        stk[ta++] = i
    }
    he = ta = 0
    for (let i = n - 1; i >= 0; i--) {
        while (he < ta && hs[stk[ta - 1]] > hs[i]) l[stk[--ta]] = i
        stk[ta++] = i
    }
    let ans = 0
    for (let i = 0; i < n; i++) {
        const t = hs[i], a = l[i], b = r[i]
        ans = Math.max(ans, (b - a - 1) * t)
    }
    return ans
};
```
Python 代码:
```Python
class Solution:
    def largestRectangleArea(self, hs: List[int]) -> int:
        n, he, ta = len(hs), 0, 0
        stk = [0] * n
        l, r = [-1] * n, [n] * n
        for i in range(n):
            while he < ta and hs[stk[ta - 1]] > hs[i]:
                ta -= 1
                r[stk[ta]] = i
            stk[ta] = i
            ta += 1
        he = ta = 0
        for i in range(n - 1, -1, -1):
            while he < ta and hs[stk[ta - 1]] > hs[i]:
                ta -= 1
                l[stk[ta]] = i
            stk[ta] = i
            ta += 1
        ans = 0
        for i in range(n):
            ans = max(ans, (r[i] - l[i] - 1) * hs[i])
        return ans
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


Question:
给定一个仅包含 `0` 和 `1` 、大小为 `rows x cols` 的二维二进制矩阵,找出只包含 `1` 的最大矩形,并返回其面积。

示例 1:

```
输入:matrix = [["1","0","1","0","0"],["1","0","1","1","1"],["1","1","1","1","1"],["1","0","0","1","0"]]

输出:6

解释:最大矩形如上图所示。
```
示例 2:
```
输入:matrix = []

输出:0
```
示例 3:
```
输入:matrix = [["0"]]

输出:0
```
示例 4:
```
输入:matrix = [["1"]]

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

输出:0
```

提示:
* $rows = matrix.length$
* $cols = matrix[0].length$
* $1 <= row, cols <= 200$
* `matrix[i][j]` 为 `'0'` 或 `'1'`

Solution:
### 前缀和 + 单调栈

为了方便,我们令 `matrix` 为 `mat`,记矩阵的行高为 $n$,矩阵的列宽为 $m$。

定义坐标系 : 左上角坐标为 $(1, 1)$,右下角坐标为 $(n, m)$。

我们将 `mat` 的每一行作为基准,以基准线为起点,往上连续 $1$ 的个数为高度。

以题目样例进行说明:

1. 红框部分代表「以第一行作为基准线,统计每一列中基准线及以上连续 $1$ 的个数」,此时只有第一行,可得:$[1, 0, 1, 0, 0]$
2. 黄框部分代表「以第二行作为基准线,统计每一列中基准线及以上连续 $1$ 的个数」,此时有第一行和第二行,可得:$[2, 0, 2, 1, 1]$
3. 蓝框部分代表「以第三行作为基准线,统计每一列中基准线及以上连续 $1$ 的个数」,此时有三行,可得:$[3, 1, 3, 2, 2]$
...

**将原矩阵进行这样的转换好处是 : 对于原矩中面积最大的矩形,其下边缘必然对应了某一条基准线,从而将问题转换为[(题解)84. 柱状图中最大的矩形](https://leetcode.cn/problems/largest-rectangle-in-histogram/solution/by-ac_oier-i470/) 。**

预处理基准线数据可以通过前缀和思想来做,构建一个二维数组 `sum`(为了方便,我们令二维数组下标从 $1$ 开始)并从上往下地构造:

$$
sum[i][j] = \begin{cases}
0 & mat[i][j] = 0 \\
sum[i - 1][j] + 1 & mat[i][j] = 1 \\
\end{cases}
$$

当有了 `sum` 之后,则是和 [(题解)84. 柱状图中最大的矩形](https://leetcode.cn/problems/largest-rectangle-in-histogram/solution/by-ac_oier-i470/) 一样的做法 : 枚举高度 + 单调栈。

Java 代码:
```Java
class Solution {
    public int maximalRectangle(char[][] mat) {
        int n = mat.length, m = mat[0].length, ans = 0;
        int[][] sum = new int[n + 10][m + 10];
        for (int i = 1; i <= n; i++) {
            for (int j = 1; j <= m; j++) {
                sum[i][j] = mat[i - 1][j - 1] == '0' ? 0 : sum[i - 1][j] + 1;
            }
        }
        int[] l = new int[m + 10], r = new int[m + 10];
        for (int i = 1; i <= n; i++) {
            int[] cur = sum[i];
            Arrays.fill(l, 0); Arrays.fill(r, m + 1);
            Deque<Integer> d = new ArrayDeque<>();
            for (int j = 1; j <= m; j++) {
                while (!d.isEmpty() && cur[d.peekLast()] > cur[j]) r[d.pollLast()] = j;
                d.addLast(j);
            }
            d.clear();
            for (int j = m; j >= 1; j--) {
                while (!d.isEmpty() && cur[d.peekLast()] > cur[j]) l[d.pollLast()] = j;
                d.addLast(j);
            }
            for (int j = 1; j <= m; j++) ans = Math.max(ans, cur[j] * (r[j] - l[j] - 1));
        }
        return ans;
    }
}
```
TypeScript 代码:
```TypeScript
function maximalRectangle(mat: string[][]): number {
    const n = mat.length, m = mat[0].length
    const sum = new Array<Array<number>>(n + 10)
    sum[0] = new Array<number>(m + 10).fill(0)
    for (let i = 1; i <= n; i++) {
        sum[i] = new Array<number>(m + 10).fill(0)
        for (let j = 1; j <= m; j++) {
            sum[i][j] = mat[i - 1][j - 1] == '0' ? 0 : sum[i - 1][j] + 1
        }
    }
    let ans = 0
    const l = new Array<number>(m + 10), r = new Array<number>(m + 10)
    const stk = new Array<number>(m + 10).fill(0)
    let he = 0, ta = 0
    for (let i = 1; i <= n; i++) {
        const cur = sum[i]
        l.fill(0); r.fill(m + 1)
        he = ta = 0
        for (let j = 1; j <= m; j++) {
            while (he < ta && cur[stk[ta - 1]] > cur[j]) r[stk[--ta]] = j
            stk[ta++] = j
        }
        he = ta = 0
        for (let j = m; j >= 1; j--) {
            while (he < ta && cur[stk[ta - 1]] > cur[j]) l[stk[--ta]] = j
            stk[ta++] = j
        }
        for (let j = 1; j <= m; j++) ans = Math.max(ans, cur[j] * (r[j] - l[j] - 1))
    }
    return ans
}
```
Python 代码:
```Python
class Solution:
    def maximalRectangle(self, mat: List[List[str]]) -> int:
        n, m, ans = len(mat), len(mat[0]), 0
        psum = [[0] * (m + 10) for _ in range(n + 10)]
        for i in range(1, n + 1):
            for j in range(1, m + 1):
                psum[i][j] = 0 if mat[i - 1][j - 1] == '0' else psum[i - 1][j] + 1
        stk = [0] * (m + 10)
        he, ta = 0, 0
        for i in range(1, n + 1):
            cur = psum[i]
            l, r = [0] * (m + 10), [m + 1] * (m + 10)
            he = ta = 0
            for j in range(1, m + 1):
                while he < ta and cur[stk[ta - 1]] > cur[j]:
                    ta -= 1
                    r[stk[ta]] = j
                stk[ta] = j
                ta += 1
            he = ta = 0
            for j in range(m, 0, -1):
                while he < ta and cur[stk[ta - 1]] > cur[j]:
                    ta -= 1
                    l[stk[ta]] = j
                stk[ta] = j
                ta += 1
            for j in range(1, m + 1):
                ans = max(ans, cur[j] * (r[j] - l[j] - 1))
        return ans
```
* 时间复杂度:$O(n \times m)$
* 空间复杂度:$O(n \times m)$

### 最后

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

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

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

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


Question:
使用下面描述的算法可以扰乱字符串 s 得到字符串 t :
1. 如果字符串的长度为 1 ,算法停止
2. 如果字符串的长度 > 1 ,执行下述步骤:
	* 在一个随机下标处将字符串分割成两个非空的子字符串。即,如果已知字符串 s ,则可以将其分成两个子字符串 x 和 y ,且满足 s = x + y 。
	* 随机 决定是要「交换两个子字符串」还是要「保持这两个子字符串的顺序不变」。即,在执行这一步骤之后,s 可能是 s = x + y 或者 s = y + x 。
	* 在 x 和 y 这两个子字符串上继续从步骤 1 开始递归执行此算法。
给你两个 长度相等 的字符串 s1 和 s2,判断 s2 是否是 s1 的扰乱字符串。如果是,返回 true ;否则,返回 false 。

示例 1:
```
输入:s1 = "great", s2 = "rgeat"

输出:true

解释:s1 上可能发生的一种情形是:
"great" --> "gr/eat" // 在一个随机下标处分割得到两个子字符串
"gr/eat" --> "gr/eat" // 随机决定:「保持这两个子字符串的顺序不变」
"gr/eat" --> "g/r / e/at" // 在子字符串上递归执行此算法。两个子字符串分别在随机下标处进行一轮分割
"g/r / e/at" --> "r/g / e/at" // 随机决定:第一组「交换两个子字符串」,第二组「保持这两个子字符串的顺序不变」
"r/g / e/at" --> "r/g / e/ a/t" // 继续递归执行此算法,将 "at" 分割得到 "a/t"
"r/g / e/ a/t" --> "r/g / e/ a/t" // 随机决定:「保持这两个子字符串的顺序不变」
算法终止,结果字符串和 s2 相同,都是 "rgeat"
这是一种能够扰乱 s1 得到 s2 的情形,可以认为 s2 是 s1 的扰乱字符串,返回 true
```
示例 2:
```
输入:s1 = "abcde", s2 = "caebd"

输出:false
```
示例 3:
```
输入:s1 = "a", s2 = "a"

输出:true
```

提示:

s1.length == s2.length
1 <= s1.length <= 30
s1 和 s2 由小写英文字母组成

Solution:
### 朴素解法(TLE)

一个朴素的做法根据「扰乱字符串」的生成规则进行判断。

由于题目说了整个生成「扰乱字符串」的过程是通过「递归」来进行。

我们要实现 $isScramble$ 函数的作用是判断 $s1$ 是否可以生成出 $s2$。

这样判断的过程,同样我们可以使用「递归」来做:

假设 $s1$ 的长度为 $n$, 的第一次分割的分割点为 $i$,那么 $s1$ 会被分成 $[0, i)$ 和 $[i, n)$ 两部分。

同时由于生成「扰乱字符串」时,可以选交换也可以选不交换。因此我们的 $s2$ 会有两种可能性:

**因为对于某个确定的分割点,$s1$ 固定分为两部分,分别为 $[0,i)$ & $[i, n)$。**

**而 $s2$ 可能会有两种分割方式,分别 $[0,i)$ & $[i,n)$ 和 $[0, n-i)$ & $[n-i,n)$。**

**我们只需要递归调用 $isScramble$ 检查 $s1$ 的 $[0,i)$ & $[i, n)$ 部分能否与 「$s2$ 的 $[0,i)$ & $[i,n)$」 或者 「$s2$ 的 $[0, n-i)$ & $[n-i,n)$」 匹配即可。**

**同时,我们将「$s1$ 和 $s2$ 相等」和「$s1$ 和 $s2$ 词频不同」作为「递归」出口。**

**理解这套做法十分重要,后续的解法都是基于此解法演变过来。**

代码:
```java []
class Solution {
    public boolean isScramble(String s1, String s2) {
        if (s1.equals(s2)) return true;
        if (!check(s1, s2)) return false;
        int n = s1.length();
        for (int i = 1; i < n; i++) {
            // s1 的 [0,i) 和 [i,n)
            String a = s1.substring(0, i), b = s1.substring(i);
            // s2 的 [0,i) 和 [i,n)
            String c = s2.substring(0, i), d = s2.substring(i);
            if (isScramble(a, c) && isScramble(b, d)) return true;
            // s2 的 [0,n-i) 和 [n-i,n)
            String e = s2.substring(0, n - i), f = s2.substring(n - i);
            if (isScramble(a, f) && isScramble(b, e)) return true;
        }
        return false;
    }
    // 检查 s1 和 s2 词频是否相同
    boolean check(String s1, String s2) {
        if (s1.length() != s2.length()) return false;
        int n = s1.length();
        int[] cnt1 = new int[26], cnt2 = new int[26];
        char[] cs1 = s1.toCharArray(), cs2 = s2.toCharArray();
        for (int i = 0; i < n; i++) {
            cnt1[cs1[i] - 'a']++;
            cnt2[cs2[i] - 'a']++;
        }
        for (int i = 0; i < 26; i++) {
            if (cnt1[i] != cnt2[i]) return false;
        }
        return true;
    }
}
```
* 时间复杂度:$O(5^n)$
* 空间复杂度:忽略递归与生成子串带来的空间开销,复杂度为 $O(1)$

---

### 记忆化搜索

朴素解法卡在了 $286 / 288$ 个样例。

我们考虑在朴素解法的基础上,增加「记忆化搜索」功能。

我们可以重新设计我们的「爆搜」逻辑:假设 $s1$ 从 $i$ 位置开始,$s2$ 从 $j$ 位置开始,后面的长度为 $len$ 的字符串是否能形成「扰乱字符串」(互为翻转)。

那么在单次处理中,我们可分割的点的范围为 $[1, len)$,然后和「递归」一下,将 $s1$ 分割出来的部分尝试去和 $s2$ 的对应位置匹配。

同样的,我们将「入参对应的子串相等」和「入参对应的子串词频不同」作为「递归」出口。

代码:
```java []
class Solution {
    String s1; String s2;
    int n;
    int[][][] cache;
    int N = -1, Y = 1, EMPTY = 0;
    public boolean isScramble(String _s1, String _s2) {
        s1 = _s1; s2 = _s2;
        if (s1.equals(s2)) return true;
        if (s1.length() != s2.length()) return false;
        n = s1.length();
        // cache 的默认值是 EMPTY
        cache = new int[n][n][n + 1];
        return dfs(0, 0, n);
    }
    boolean dfs(int i, int j, int len) {
        if (cache[i][j][len] != EMPTY) return cache[i][j][len] == Y;
        String a = s1.substring(i, i + len), b = s2.substring(j, j + len);
        if (a.equals(b)) {
            cache[i][j][len] = Y;
            return true;
        }
        if (!check(a, b)) {
            cache[i][j][len] = N;
            return false;
        }
        for (int k = 1; k < len; k++) {
            // 对应了「s1 的 [0,i) & [i,n)」匹配「s2 的 [0,i) & [i,n)」
            if (dfs(i, j, k) && dfs(i + k, j + k, len - k)) {
                cache[i][j][len] = Y;
                return true;
            }
            // 对应了「s1 的 [0,i) & [i,n)」匹配「s2 的 [n-i,n) & [0,n-i)」
            if (dfs(i, j + len - k, k) && dfs(i + k, j, len - k)) {
                cache[i][j][len] = Y;
                return true;
            }
        }
        cache[i][j][len] = N;
        return false;
    }
    // 检查 s1 和 s2 词频是否相同
    boolean check(String s1, String s2) {
        if (s1.length() != s2.length()) return false;
        int n = s1.length();
        int[] cnt1 = new int[26], cnt2 = new int[26];
        char[] cs1 = s1.toCharArray(), cs2 = s2.toCharArray();
        for (int i = 0; i < n; i++) {
            cnt1[cs1[i] - 'a']++;
            cnt2[cs2[i] - 'a']++;
        }
        for (int i = 0; i < 26; i++) {
            if (cnt1[i] != cnt2[i]) return false;
        }
        return true;
    }
}
```
* 时间复杂度:$O(n^4)$
* 空间复杂度:$O(n^3)$

---

### 动态规划(区间 DP)

其实有了上述「记忆化搜索」方案之后,我们就已经可以直接忽略原问题,将其改成「动态规划」了。

**根据「dfs 方法的几个可变入参」作为「状态定义的几个维度」,根据「dfs 方法的返回值」作为「具体的状态值」。**

**我们可以得到状态定义 $f[i][j][len]$:**

**$f[i][j][len]$ 代表 $s1$ 从 $i$ 开始,$s2$ 从 $j$ 开始,后面长度为 $len$ 的字符是否能形成「扰乱字符串」(互为翻转)。**

状态转移方程其实就是翻译我们「记忆化搜索」中的 dfs 主要逻辑部分:
```java
    // 对应了「s1 的 [0,i) & [i,n)」匹配「s2 的 [0,i) & [i,n)」
    if (dfs(i, j, k) && dfs(i + k, j + k, len - k)) {
        cache[i][j][len] = Y;
        return true;
    }
    // 对应了「s1 的 [0,i) & [i,n)」匹配「s2 的 [n-i,n) & [0,n-i)」
    if (dfs(i, j + len - k, k) && dfs(i + k, j, len - k)) {
        cache[i][j][len] = Y;
        return true;
    }
```

**从状态定义上,我们就不难发现这是一个「区间 DP」问题,区间长度大的状态值可以由区间长度小的状态值递推而来。**

**而且由于本身我们在「记忆化搜索」里面就是从小到大枚举 $len$,因此这里也需要先将 $len$ 这层循环提前,确保我们转移 $f[i][j][len]$ 时所需要的状态都已经被计算好。**

代码:
```java []
class Solution {
    public boolean isScramble(String s1, String s2) {
        if (s1.equals(s2)) return true;
        if (s1.length() != s2.length()) return false;
        int n = s1.length();
        char[] cs1 = s1.toCharArray(), cs2 = s2.toCharArray();
        boolean[][][] f = new boolean[n][n][n + 1];

        // 先处理长度为 1 的情况
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < n; j++) {
                f[i][j][1] = cs1[i] == cs2[j];
            }
        }

        // 再处理其余长度情况
        for (int len = 2; len <= n; len++) {
            for (int i = 0; i <= n - len; i++) {
                for (int j = 0; j <= n - len; j++) {
                    for (int k = 1; k < len; k++) {
                        boolean a = f[i][j][k] && f[i + k][j + k][len - k];
                        boolean b = f[i][j + len - k][k] && f[i + k][j][len - k];
                        if (a || b) {
                            f[i][j][len] = true;
                        }
                    }
                }
            }
        }
        return f[0][0][n];
    }
}
```

* 时间复杂度:$O(n^4)$
* 空间复杂度:$O(n^3)$

### 最后

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

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

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

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


88. 合并两个有序数组

Question:
给你两个有序整数数组 `nums1` 和 `nums2`,请你将 `nums2` 合并到 `nums1` 中,使 `nums1` 成为一个有序数组。

初始化 `nums1` 和 `nums2` 的元素数量分别为 `m` 和 `n` 。

你可以假设 `nums1` 的空间大小等于 `m + n`,这样它就有足够的空间保存来自 `nums2` 的元素。

示例 1:
```
输入:nums1 = [1,2,3,0,0,0], m = 3, nums2 = [2,5,6], n = 3

输出:[1,2,2,3,5,6]
```
示例 2:
```
输入:nums1 = [1], m = 1, nums2 = [], n = 0

输出:[1]
```

提示:
* $nums1.length = m + n$
* $nums2.length == n$
* $0 <= m, n <= 200$
* $1 <= m + n <= 200$
* $-10^9 <= nums1[i], nums2[i] <= 10^9$

Solution:
### 模拟

最简单的做法,我们可以将 $nums2$ 的内容先搬到 $nums1$ 去,再对 $nums1$ 进行排序。

Java 代码:
```Java
class Solution {
    public void merge(int[] nums1, int m, int[] nums2, int n) {
        System.arraycopy(nums2, 0, nums1, m, n);
        Arrays.sort(nums1);
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    void merge(vector<int>& nums1, int m, vector<int>& nums2, int n) {
        copy(nums2.begin(), nums2.end(), nums1.begin() + m);
        sort(nums1.begin(), nums1.end());
    }
};
```
Python 代码:
```Python
class Solution:
    def merge(self, nums1: List[int], m: int, nums2: List[int], n: int) -> None:
        nums1[m:m+n] = nums2
        nums1.sort()
```
TypeScript 代码:
```TypeScript
function merge(nums1: number[], m: number, nums2: number[], n: number): void {
    nums1.splice(m, n, ...nums2);
    nums1.sort((a, b) => a - b);
};
```
* 时间复杂度:$O((m + n)\log{(m + n)})$
* 空间复杂度:$O(1)$

**PS. Java 中的 sort 排序是一个综合排序。包含插入/双轴快排/归并/timsort,这里假定 `Arrays.sort` 使用的是「双轴快排」,并忽略递归带来的空间开销。**

---

### 双指针 - 额外空间

上述方法,粗暴地将两个数组结合,再对结果进行排序,没有很好利用俩数组本身有序的特性。

一个容易想到的,可以避免对结果排序的做法是:创建一个和 $nums1$ 等长的数组 $arr$,使用双指针将 $num1$ 和 $nums2$ 的数据迁移到 $arr$,最后再将 $arr$ 复制到 $nums1$ 中。

Java 代码:
```Java
class Solution {
    public void merge(int[] nums1, int m, int[] nums2, int n) {
        int total = m + n, idx = 0;
        int[] arr = new int[total];
        for (int i = 0, j = 0; i < m || j < n;) {
            if (i < m && j < n) {
                arr[idx++] = nums1[i] < nums2[j] ? nums1[i++] : nums2[j++];
            } else if (i < m) {
                arr[idx++] = nums1[i++];
            } else if (j < n) {
                arr[idx++] = nums2[j++];
            }
        }
        System.arraycopy(arr, 0, nums1, 0, total);
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    void merge(vector<int>& nums1, int m, vector<int>& nums2, int n) {
        int total = m + n, idx = 0;
        vector<int> arr(total);
        for (int i = 0, j = 0; i < m || j < n;) {
            if (i < m && j < n) {
                arr[idx++] = nums1[i] < nums2[j] ? nums1[i++] : nums2[j++];
            } else if (i < m) {
                arr[idx++] = nums1[i++];
            } else if (j < n) {
                arr[idx++] = nums2[j++];
            }
        }
        copy(arr.begin(), arr.end(), nums1.begin());
    }
};
```
Python 代码:
```Python
class Solution:
    def merge(self, nums1: List[int], m: int, nums2: List[int], n: int) -> None:
        total, idx = m + n, 0
        arr = [0] * total
        i, j = 0, 0
        while i < m or j < n:
            if i < m and j < n:
                if nums1[i] < nums2[j]:
                    arr[idx] = nums1[i]
                    i += 1
                else:
                    arr[idx] = nums2[j]
                    j += 1
                idx += 1
            elif i < m:
                arr[idx] = nums1[i]
                idx += 1
                i += 1
            elif j < n:
                arr[idx] = nums2[j]
                idx += 1
                j += 1

        for i in range(total):
            nums1[i] = arr[i]
```
TypeScript 代码:
```TypeScript
function merge(nums1: number[], m: number, nums2: number[], n: number): void {
    let total = m + n, idx = 0;
    const arr = new Array(total);
    for (let i = 0, j = 0; i < m || j < n;) {
        if (i < m && j < n) {
            arr[idx++] = nums1[i] < nums2[j] ? nums1[i++] : nums2[j++];
        } else if (i < m) {
            arr[idx++] = nums1[i++];
        } else if (j < n) {
            arr[idx++] = nums2[j++];
        }
    }
    for (let i = 0; i < total; i++) nums1[i] = arr[i];
};
```
* 时间复杂度:$O(m + n)$
* 空间复杂度:$O(m + n)$

---

### 双指针 - 原地合并

上述两类做法都不是最优:

* 要么使用到了“排序”操作,时间复杂度带 $\log$,不是最优
* 要么使用到了“额外辅助数组”空间,空间复杂度不为 $O(1)$,也不是最优

那么是否有“不排序”且“不消耗额外空间”的原地做法呢?

答案是有的。

**使用两个指针 `i` 和 `j` 分别指向 `nums1` 和 `nums2` 的结尾位置,从后往前地原地构造出答案。**

所谓的原地构造是指:直接将 $nums1[i]$ 和 $nums2[j]$ 之间的较大值,放到合并后该在 `nums1` 出现的位置。

这也是为什么我们要「从后往前」而不是「从前往后」的原因。

使用 `idx` 代表当构造到答案的下标值,起始有 `idx = m + n - 1`(答案是一个长度为 $m + n$ 的数组,因此下标是 $m + n - 1$),根据指针 `i` 和 `j` 所达位置,以及 $nums1[i]$ 和 $nums2[j]$ 大小关系,分情况讨论:

* 若 `i` 和 `j` 任一到达边界,说明其中一个数组已用完,用另一数组的剩余元素进行填充即可
* 若 `i` 和 `j` 均未到达边界,进一步比较 $nums1[i]$ 和 $nums2[j]$ 的大小关系:
  * 若 $nums1[i] > nums2[j]$,说明在合并答案中 $nums1[i]$ 出现 $nums2[j]$ 后面,将 $nums1[i]$ 填入到 $nums1[idx]$ 中,让 `idx` 和 `i` 同时前移
  * 否则,将 $nums2[j]$ 填入到 $nums1[idx]$ 中,让 `idx` 和 `j` 同时前移

直到 `i` 和 `j` 均到达边界,此时合并答案已全部填充到 `nums1` 中。

Java 代码:
```Java
class Solution {
    public void merge(int[] nums1, int m, int[] nums2, int n) {
        int i = m - 1, j = n - 1, idx = m + n - 1;
        while (i >= 0 || j >= 0) {
            if (i >= 0 && j >= 0) {
                nums1[idx--] = nums1[i] >= nums2[j] ? nums1[i--] : nums2[j--];
            } else if (i >= 0) {
                nums1[idx--] = nums1[i--];
            } else {
                nums1[idx--] = nums2[j--];
            }
        }
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    void merge(vector<int>& nums1, int m, vector<int>& nums2, int n) {
        int i = m - 1, j = n - 1, idx = m + n - 1;
        while (i >= 0 || j >= 0) {
            if (i >= 0 && j >= 0) {
                nums1[idx--] = nums1[i] >= nums2[j] ? nums1[i--] : nums2[j--];
            } else if (i >= 0) {
                nums1[idx--] = nums1[i--];
            } else {
                nums1[idx--] = nums2[j--];
            }
        }
    }
};
```
Python 代码:
```Python
class Solution:
    def merge(self, nums1: List[int], m: int, nums2: List[int], n: int) -> None:
        i, j, idx = m - 1, n - 1, m + n - 1
        while i >= 0 or j >= 0:
            if i >= 0 and j >= 0:
                if nums1[i] > nums2[j]:
                    nums1[idx] = nums1[i]
                    i -= 1
                else:
                    nums1[idx] = nums2[j]
                    j -= 1
                idx -= 1
            elif i >= 0:
                nums1[idx] = nums1[i]
                idx, i = idx - 1, i - 1
            else:
                nums1[idx] = nums2[j]
                idx, j = idx - 1, j - 1
```
TypeScript 代码:
```TypeScript
function merge(nums1: number[], m: number, nums2: number[], n: number): void {
    let i = m - 1, j = n - 1, idx = m + n - 1;
    while (i >= 0 || j >= 0) {
        if (i >= 0 && j >= 0) {
            nums1[idx--] = nums1[i] >= nums2[j] ? nums1[i--] : nums2[j--];
        } else if (i >= 0) {
            nums1[idx--] = nums1[i--];
        } else {
            nums1[idx--] = nums2[j--];
        }
    }
};
```
* 时间复杂度:$O(m + n)$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


Question:
$n$ 位格雷码序列 是一个由 $2^n$ 个整数组成的序列,其中:
* 每个整数都在范围 $[0, 2^n - 1]$ 内(含 $0$ 和 $2^n - 1$)
* 第一个整数是 0
* 一个整数在序列中出现 不超过一次
* 每对 相邻 整数的二进制表示 恰好一位不同 ,且
* 第一个 和 最后一个 整数的二进制表示 恰好一位不同

给你一个整数 `n` ,返回任一有效的 `n` 位格雷码序列 。

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

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

解释:
[0,1,3,2] 的二进制表示是 [00,01,11,10] 。
- 00 和 01 有一位不同
- 01 和 11 有一位不同
- 11 和 10 有一位不同
- 10 和 00 有一位不同

[0,2,3,1] 也是一个有效的格雷码序列,其二进制表示是 [00,10,11,01] 。
- 00 和 10 有一位不同
- 10 和 11 有一位不同
- 11 和 01 有一位不同
- 01 和 00 有一位不同
```
示例 2:
```
输入:n = 1

输出:[0,1]
```

提示:
* `1 <= n <= 16`

Solution:
### 对称性构造

根据格雷码的定义,我们需要构造一个合法序列,序列之间每两个数的二进制表示中只有一位不同,同时序列第一位和最后一位对应的二进制也只有一位不同。

我们知道 $k + 1$ 位的格雷码序列是 $k$ 位格雷码序列长度的两倍,利用合法 $k$ 位格雷码序列,我们可以「对称性」地构造出 $k + 1$ 位格雷码。

具体的,假定 $k$ 位格雷码序列长度为 $n$,我们将这 $k$ 位的格雷序列进行翻转,并追加到原有序列的尾部,得到长度为 $2 * n$ 的序列,此时新序列的前后两部分均为合法的格雷码。

考虑如何进行解决衔接点的合法性:我们可以对于序列的后半(翻转而来)的部分中的每个数进行「尾部」追加 $1$ 的操作,确保链接点的两个数只有有一位二进制位不同,同时并不影响前后两半部分的合法性。

而且由于后半部分本身是由前半部分翻转而来,序列中的第一个数和最后一个数原本为同一个值,经过追加 $1$ 的操作之后,首尾两个数的二进制表示只有一位不同,整个序列的合法性得以保证。

代码:
```Java
class Solution {
    public List<Integer> grayCode(int n) {
        List<Integer> ans = new ArrayList<>();
        ans.add(0);
        while (n-- > 0) {
            int m = ans.size();
            for (int i = m - 1; i >= 0; i--) {
                ans.set(i, ans.get(i) << 1);
                ans.add(ans.get(i) + 1);
            }
        }
        return ans;
    }
}
```
* 时间复杂度:$O(2^n)$
* 空间复杂度:$O(2^n)$

### 最后

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

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

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

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