564. 寻找最近的回文数

Question:
给定一个表示整数的字符串 $n$ ,返回与它最近的回文整数(不包括自身)。如果不止一个,返回较小的那个。

“最近的”定义为两个整数差的绝对值最小。

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

输出: "121"
```
示例 2:
```
输入: n = "1"

输出: "0"

解释: 0 和 2是最近的回文,但我们返回最小的,也就是 0。
```

提示:
* $1 <= n.length <= 18$
* $n$ 只由数字组成
* $n$ 不含前导 $0$
* $n$ 代表在 $[1, 10^{18} - 1]$ 范围内的整数

Solution:
### 上下界分析 + 边界处理

对于任意字符串数值 $s$ 而言(令其长度为 $n$),先考虑如何找到「第一个比其大」和「第一个比其小」的回文串数值(上下界)。

由于是找「最近」的数值,因此一个自然的想法是优先修改低位数字,但由于回文串本身的对称性质,每个「低位」的修改带来的副作用是需要同时修改对应的「高位」,因此一个真正可以修改的位置是(相对)中间的位置。

举个 🌰,对于长度为奇数回文串数值 $abcde$,其中 $de$ 的修改会导致 $ab$ 同步修改,因此最近一个可以修改的低位是 $c$;对于长度为偶数的回文串数值 $abcd$ 而言,其中 $d$ 的修改会导致 $a$ 同步修改,因此最近一个可以修改的位置是 $bc$。

**当对目标位置进行修改后,其余位置的数值可由回文串性质唯一确定,因此只需要找到可修改位置相邻数值即可。**

例如对于 $abcde$ 来说,最近的回文数值的前三位可能是 $abc$、$abc+1$ 和 $abc-1$ 三者之一,其他位置的数值随着前三位的确定而唯一确定。

上述分析对于一般情况成立,而边界情况是指 $abc + 1$ 和 $abc - 1$ 导致整体长度发生变化,即 $abc=999$ 和 $abc=100$ 的情况,此时直接套用上述方法得到的值分别为 $1000001$ 和 $999$,但真实最近的值为 $100001$ 和 $9999$。

> 展开来说就是:对于 $abc = 999$(对应原串为 $999xx$ 的情况)而言,按照上述逻辑得到的是 $1000$,进行回文补充后得到 $1000001$(事实上应该是 $100001$ 更优);对于 $abc = 100$(对应原串为 $100xx$ 的情况)而言,按照上述逻辑得到是 $99$,进行回文补充后得到 $999$(事实上应该是 $9999$ 更优)。

因此对于长度为 $n$ 的数值,值考虑枚举前一半数的三种情况(前一半数不变,前一半数加一或减一)可能会错过最优解,我们需要将与长度相关的边界值也纳入考虑,即将「长度为 $n - 1$ 的回文串最大值($99...99$)」和「长度为 $n + 1$ 的回文串最小值($10...01$)」也纳入考虑,也就是对应的 $10^{n - 1} - 1$ 和 $10^n + 1$ 也纳入考虑,最后从上述 $5$ 个值中选绝对差值最小的作为答案。

> 一些细节:在枚举完前一半数后,我们需要根据原串长度的奇偶性来决定,如何生成后一半数,从而确保构建的回文串长度仍和原串长度相等。
> 即对于原串长度 $n$ 为奇数的 $abcde$ 而言,在处理完 $abc$ 之后,生成后一半数时,只需要根据前两位 $ab$ 生成对应的 $ba$,而无须处理中间字符 $c$;而对于原串长度 $n$ 为偶数的 $abcd$ 而言,在处理完 $ab$ 之后,生成另外一般数值需要根据整一个 $ab$ 来生成 $ba$,没有中间字符需要被跳过。
>
> 另外由于数据范围为 $[1, 10^{18} - 1]$,而 $999999999999999999$ 也属于合法输入($2022/03/03$ 目前样例里还没有这个数据),因此我们需要考虑 `getNum` 爆 `long` 的问题,一个比直接特判更好做法是:利用答案长度必然不会超过 $19$ 来做处理,对于长度超过 $19$ 的直接当成 $-1$ 来处理,由于个位数均为回文,$-1$ 注定不会成为答案。

代码:
```Java
class Solution {
    public String nearestPalindromic(String s) {
        int n = s.length();
        long cur = Long.parseLong(s);
        Set<Long> set = new HashSet<>();
        set.add((long) Math.pow(10, (n - 1)) - 1);
        set.add((long) Math.pow(10, n) + 1);
        long t = Long.parseLong(s.substring(0, (n + 1) / 2));
        for (long i = t - 1; i <= t + 1; i++) {
            long temp = getNum(i, n % 2 == 0);
            if (temp != cur) set.add(temp);
        }
        long ans = -1;
        for (long i : set) {
            if (ans == -1) ans = i;
            else if (Math.abs(i - cur) < Math.abs(ans - cur)) ans = i;
            else if (Math.abs(i - cur) == Math.abs(ans - cur) && i < ans) ans = i;
        }
        return String.valueOf(ans);
    }
    long getNum(long k, boolean isEven) {
        StringBuilder sb = new StringBuilder();
        sb.append(k);
        int n = sb.length(), idx = isEven ? n - 1 : n - 2;
        while (idx >= 0) sb.append(sb.charAt(idx--));
        String str = sb.toString();
        return str.length() > 19 ? -1 : Long.parseLong(str);
    }
}
```
* 时间复杂度:令 $N$ 为字符串 `s` 所代指的数值,$n$ 为字符串 `s` 的长度,复杂度为 $O(\log{N})$ 或者说是 $O(n)$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


Question:
索引从 $0$ 开始长度为 `N` 的数组 `A`,包含 $0$ 到 $N - 1$ 的所有整数。找到最大的集合 `S` 并返回其大小,其中 $S[i] = {A[i], A[A[i]], A[A[A[i]]], ... }$ 且遵守以下的规则。

假设选择索引为 `i` 的元素 $A[i]$ 为 `S` 的第一个元素,`S` 的下一个元素应该是 $A[A[i]]$,之后是 $A[A[A[i]]]$ ... 以此类推,不断添加直到 `S` 出现重复的元素。

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

输出: 4

解释:
A[0] = 5, A[1] = 4, A[2] = 0, A[3] = 3, A[4] = 1, A[5] = 6, A[6] = 2.

其中一种最长的 S[K]:
S[0] = {A[0], A[5], A[6], A[2]} = {5, 6, 2, 0}
```

提示:
* `N` 是 $[1, 20,000]$ 之间的整数。
* `A` 中不含有重复的元素。
* `A` 中的元素大小在 $[0, N-1]$ 之间。

Solution:
### 模拟

将 $A[i]$ 与 $A[A[i]]$ 之间看作存在一条有向边,由于所有数范围都在 $[0, N - 1]$,且不重复,因此至少存在一个环,而问题本质是求所有环的最大长度。

直接根据题意记进行模拟即可,从前往后处理每个 $nums[i]$,并尝试从 $nums[i]$ 出发遍历其所在的环,为了防止某些环被重复处理,对于当前经过的 $nums[i]$ 标记为 $-1$,这样每个数被访问的次数最多不超过 $3$ 次,整体复杂度为 $O(n)$。

Java 代码:
```Java
class Solution {
    public int arrayNesting(int[] nums) {
        int n = nums.length, ans = 0;
        for (int i = 0; i < n; i++) {
            int cur = i, cnt = 0;
            while (nums[cur] != -1) {
                cnt++;
                int c = cur;
                cur = nums[cur];
                nums[c] = -1;
            }
            ans = Math.max(ans, cnt);
        }
        return ans;
    }
}
```
TypeScript 代码:
```TypeScript
function arrayNesting(nums: number[]): number {
    let n = nums.length, ans = 0
    for (let i = 0; i < n; i++) {
        let cur = i, cnt = 0
        while (nums[cur] != -1) {
            cnt++
            const c = cur
            cur = nums[cur]
            nums[c] = -1
        }
        ans = Math.max(ans, cnt)
    }
    return ans
};
```
* 时间复杂度:每个数字被访问的次数为常数次,复杂度为 $O(n)$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


Question:
在 MATLAB 中,有一个非常有用的函数 `reshape`,它可以将一个矩阵重塑为另一个大小不同的新矩阵,但保留其原始数据。

给出一个由二维数组表示的矩阵,以及两个正整数 `r` 和 `c`,分别表示想要的重构的矩阵的行数和列数。

重构后的矩阵需要将原始矩阵的所有元素以相同的行遍历顺序填充。

如果具有给定参数的 `reshape` 操作是可行且合理的,则输出新的重塑矩阵;否则,输出原始矩阵。

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

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

解释:
行遍历nums的结果是 [1,2,3,4]。新的矩阵是 1 * 4 矩阵, 用之前的元素值一行一行填充新矩阵。
```
示例 2:
```
输入:
nums =
[[1,2],
 [3,4]]
r = 2, c = 4

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

解释:
没有办法将 2 * 2 矩阵转化为 2 * 4 矩阵。 所以输出原矩阵。
```
注意:
* 给定矩阵的宽和高范围在 $[1, 100]$。
* 给定的 $r$ 和 $c$ 都是正数。

Solution:
### 模拟

按照题意,从头模拟一遍即可。

使用 $idx$ 记录新矩阵当前分配到的位置(一维),利用通用的一维转二维方式对应回行列坐标即可。

代码:
```Java
class Solution {
    public int[][] matrixReshape(int[][] nums, int r, int c) {
        int or = nums.length, oc = nums[0].length;
        if (or * oc != r * c) return nums;
        int[][] ans = new int[r][c];
        for (int i = 0, idx = 0; i < or; i++) {
            for (int j = 0; j < oc; j++, idx++) {
                ans[idx / c][idx % c] = nums[i][j];
            }
        }
        return ans;
    }
}
```
* 时间复杂度:$O(r * c)$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


Question:
给定两个字符串 s1 和 s2,写一个函数来判断 s2 是否包含 s1 的排列。

换句话说,第一个字符串的排列之一是第二个字符串的 子串 。

示例 1:
```
输入: s1 = "ab" s2 = "eidbaooo"
输出: True
解释: s2 包含 s1 的排列之一 ("ba").
```
示例 2:
```
输入: s1= "ab" s2 = "eidboaoo"
输出: False
```

提示:
* 输入的字符串只包含小写字母
* 两个字符串的长度都在 [1, 10,000] 之间

Solution:
### 滑动窗口

由于是 `s2` 中判断是否包含 `s1` 的排列,而且 `s1` 和 `s2` 均为小数。

可以使用数组先对 `s1` 进行统计,之后使用滑动窗口进行扫描,每滑动一次检查窗口内的字符频率和 `s1` 是否相等 ~

以下代码,可以作为滑动窗口模板使用:

**PS. 你会发现以下代码和 [643. 子数组最大平均数 I](https://leetcode-cn.com/problems/maximum-average-subarray-i/) 和 [1423. 可获得的最大点数](https://leetcode-cn.com/problems/maximum-points-you-can-obtain-from-cards/) 代码很相似,因为是一套模板。**

1. 初始化将滑动窗口压满,取得第一个滑动窗口的目标值

2. 继续滑动窗口,每往前滑动一次,需要删除一个和添加一个元素

代码:
```java
class Solution {
    public boolean checkInclusion(String s1, String s2) {
        int m = s1.length(), n = s2.length();
        if (m > n) return false;
        int[] cnt = new int[26];
        for (char c : s1.toCharArray()) cnt[c - 'a']++;
        int[] cur = new int[26];
        for (int i = 0; i < m; i++) cur[s2.charAt(i) - 'a']++;
        if (check(cnt, cur)) return true;
        for (int i = m; i < n; i++) {
            cur[s2.charAt(i) - 'a']++;
            cur[s2.charAt(i - m) - 'a']--;
            if (check(cnt, cur)) return true;
        }
        return false;
    }
    boolean check(int[] cnt1, int[] cnt2) {
        for (int i = 0; i < 26; i++) {
            if (cnt1[i] != cnt2[i]) return false;
        }
        return true;
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n)$

经过了前面几天的「滑动窗口」,相信你做这题已经很有感觉了。

但其实这是某道困难题的简化版,本题根据「字符」滑动,而 [30. 串联所有单词的子串](https://leetcode-cn.com/problems/substring-with-concatenation-of-all-words/) 则是根据「单词」来。但基本思路都是一样的,强烈建议你来试试 ~

***

### 相关链接

[30. 串联所有单词的子串:【刷穿LC】朴素哈希表解法 + 滑动窗口解法](https://leetcode-cn.com/problems/substring-with-concatenation-of-all-words/solution/shua-chuan-lc-po-su-ha-xi-biao-jie-fa-hu-ml3x/)

[643. 子数组最大平均数 I:滑动窗口裸题 (含模板)~](https://leetcode-cn.com/problems/maximum-average-subarray-i/solution/hua-dong-chuang-kou-luo-ti-han-mo-ban-by-buo3/)

[1423. 可获得的最大点数:详解滑动窗口基本思路(含模板)~](https://leetcode-cn.com/problems/maximum-points-you-can-obtain-from-cards/solution/jian-dan-de-hua-dong-chuang-kou-he-kuai-1go5h/)

***

### 最后

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

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

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

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


Question:
给定一个偶数长度的数组,其中不同的数字代表着不同种类的糖果,每一个数字代表一个糖果。你需要把这些糖果平均分给一个弟弟和一个妹妹。

返回妹妹可以获得的最大糖果的种类数。

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

输出: 3

解析: 一共有三种种类的糖果,每一种都有两个。
     最优分配方案:妹妹获得[1,2,3],弟弟也获得[1,2,3]。这样使妹妹获得糖果的种类数最多。
```

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

输出: 2

解析: 妹妹获得糖果[2,3],弟弟获得糖果[1,1],妹妹有两种不同的糖果,弟弟只有一种。这样使得妹妹可以获得的糖果种类数最多。
```

注意:
* 数组的长度为[2, 10,000],并且确定为偶数。
* 数组中数字的大小在范围[-100,000, 100,000]内。

Solution:
### 贪心

由于题目规定糖果数量 $n$ 为偶数,因此一定能将糖果平均分配成两份,每份数量固定为 $\frac{n}{2}$。

假设糖果种类数量为 $m$,那么单份中可使得糖果种类数量最大为 $\min(m, \frac{n}{2})$。

可以使用「分情况讨论」进行证明:

1. $m > \frac{n}{2}$:糖果种类大于单份的糖果数量。此时可以从 $m$ 类糖果中找出 $\frac{n}{2}$ 类不同的糖果组成单份,此时可取得的最大种类数量为 $\frac{n}{2}$;

2. $m = \frac{n}{2}$:糖果种类等于单份的糖果数量。同理,此时可以从 $m$ 类糖果中找出 $\frac{n}{2}$ 类不同的糖果组成单份,此时可取得的最大种类数量为 $m = \frac{n}{2}$;

3. $m < \frac{n}{2}$:糖果种类小于单份的糖果数量。此时先从 $m$ 类糖果中找出 $m$ 类不同糖果组成单份,再使用相同种类的糖果凑齐 $\frac{n}{2}$,此时可取得的最大种类数量为 $m$。

综上,无论糖果种类数与单份糖果数呈何种关系,我们可取得的最大糖果种类数量均为 $\min(m, \frac{n}{2})$。

代码:
```Java
class Solution {
    public int distributeCandies(int[] candyType) {
        Set<Integer> set = new HashSet<>();
        for (int i : candyType) set.add(i);
        return Math.min(candyType.length / 2, set.size());
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


Question:
给你一个大小为 m x n 的网格和一个球。球的起始坐标为 [startRow, startColumn] 。

你可以将球移到在四个方向上相邻的单元格内(可以穿过网格边界到达网格之外)。

你**最多**可以移动 maxMove 次球。

给你五个整数 m、n、maxMove、startRow 以及 startColumn ,找出并返回可以将球移出边界的路径数量。

因为答案可能非常大,返回对 $10^9 + 7$ 取余 后的结果。

示例 1:

```
输入:m = 2, n = 2, maxMove = 2, startRow = 0, startColumn = 0

输出:6
```
示例 2:

```
输入:m = 1, n = 3, maxMove = 3, startRow = 0, startColumn = 1

输出:12
```

提示:
* 1 <= m, n <= 50
* 0 <= maxMove <= 50
* 0 <= startRow < m
* 0 <= startColumn < n

Solution:
### 基本分析

通常来说,朴素的路径 DP 问题之所以能够使用常规 DP 方式进行求解,是因为只能往某一个方向(一维棋盘的路径问题)或者只能往某两个方向(二维棋盘的路径问题)移动。

这样的移动规则意味着,我们不会重复进入同一个格子。

**从图论的意义出发:将每个格子视为点的话,如果能够根据移动规则从 `a` 位置一步到达 `b` 位置,则说明存在一条由 `a` 指向 `b` 的有向边。**

**也就是说,在朴素的路径 DP 问题中,“单向”的移动规则注定了我们的图不存在环,是一个存在拓扑序的有向无环图,因此我们能够使用常规 DP 手段来求解。**

回到本题,移动规则是四联通,并不是“单向”的,在某条出界的路径中,我们是有可能重复进入某个格子,即存在环。

因此我们需要换一种 DP 思路进行求解。

---

### 记忆化搜索

通常在直接 DP 不好入手的情况下,我们可以先尝试写一个「记忆化搜索」的版本。

那么如果是让你设计一个 DFS 函数来解决本题,你会如何设计?

我大概会这样设计:

```Java
int dfs(int x, int y, int k) {}
```

重点放在几个「可变参数」与「返回值」上:$(x,y)$ 代表当前所在的位置,$k$ 代表最多使用多少步,返回值代表路径数量。

根据 [DP-动态规划 第八讲](https://mp.weixin.qq.com/s?__biz=MzU4NDE3MTEyMA==&mid=2247485297&idx=1&sn=5ee4ce31c42d368af0653f60aa263c82&chksm=fd9cac6ecaeb25787e6da90423c5467e1679da0a8aaf1a3445475199a8f148d8629e851fea57&scene=178&cur_album_id=1773144264147812354#rd) 的学习中,我们可以确定递归出口为:

1. 当前到达了棋盘外的位置,说明找到了一条出界路径,返回 $1$;
2. 在条件 $1$ 不满足的前提下,当剩余步数为 $0$(不能再走下一步),说明没有找到一条合法的出界路径,返回 $0$。

主逻辑则是根据四联通规则进行移动即可,最终答案为 `dfs(startRow, startColumn, maxMove)`。

代码:
```Java
class Solution {
    int MOD = (int)1e9+7;
    int m, n, max;
    int[][] dirs = new int[][]{{1,0},{-1,0},{0,1},{0,-1}};
    int[][][] cache;
    public int findPaths(int _m, int _n, int _max, int r, int c) {
        m = _m; n = _n; max = _max;
        cache = new int[m][n][max + 1];
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                for (int k = 0; k <= max; k++) {
                    cache[i][j][k] = -1;
                }
            }
        }
        return dfs(r, c, max);
    }
    int dfs(int x, int y, int k) {
        if (x < 0 || x >= m || y < 0 || y >= n) return 1;
        if (k == 0) return 0;
        if (cache[x][y][k] != -1) return cache[x][y][k];
        int ans = 0;
        for (int[] d : dirs) {
            int nx = x + d[0], ny = y + d[1];
            ans += dfs(nx, ny, k - 1);
            ans %= MOD;
        }
        cache[x][y][k] = ans;
        return ans;
    }
}
```

---

### 动态规划

根据我们的「记忆化搜索」,我们可以设计一个二维数组 $f[][]$ 作为我们的 dp 数组:

* **第一维代表 DFS 可变参数中的 $(x,y)$ 所对应 $index$。取值范围为 $[0, m*n)$**
* **第二维代表 DFS 可变参数中的 $k$。取值范围为 $[0,max]$**

**dp 数组中存储的就是我们 `DFS` 的返回值:路径数量。**

根据 dp 数组中的维度设计和存储目标值,我们可以得知「状态定义」为:

**$f[i][j]$ 代表从位置 $i$ 出发,可用步数不超过 $j$ 时的路径数量。**

至此,我们只是根据「记忆化搜索」中的 `DFS` 函数的签名,就已经得出我们的「状态定义」了,接下来需要考虑「转移方程」。

当有了「状态定义」之后,我们需要从「最后一步」来推导出「转移方程」:

由于题目允许往四个方向进行移动,因此我们的最后一步也要统计四个相邻的方向。

由此可得我们的状态转移方程:

$$
f[(x,y)][step] = f[(x-1,y)][step-1]+f[(x+1,y)][step-1]+f[(x,y-1)][step-1]+f[(x,y+1)][step-1]
$$

注意,转移方程中 dp 数组的第一维存储的是 $(x,y)$ 对应的 $idx$。

从转移方程中我们发现,更新 $f[i][j]$ 依赖于 $f[x][j-1]$,因此我们转移过程中需要将最大移动步数进行「从小到大」枚举。

至此,我们已经完成求解「路径规划」问题的两大步骤:「状态定义」&「转移方程」。

但这还不是所有,我们还需要一些 **有效值** 来滚动下去。

**其实就是需要一些「有效值」作为初始化状态。**

观察我们的「转移方程」可以发现,整个转移过程是一个累加过程,如果没有一些有效的状态(非零值)进行初始化的话,整个递推过程并没有意义。

那么哪些值可以作为成为初始化状态呢?

显然,当我们已经位于矩阵边缘的时候,我们可以一步跨出矩阵,这算作一条路径。

同时,由于我们能够往四个方向进行移动,因此不同的边缘格子会有不同数量的路径。

换句话说,我们需要先对边缘格子进行初始化操作,预处理每个边缘格子直接走出矩阵的路径数量。

目的是为了我们整个 DP 过程可以有效的递推下去。

**可以发现,动态规划的实现,本质是将问题进行反向:原问题是让我们求从棋盘的特定位置出发,出界的路径数量。实现时,我们则是从边缘在状态出发,逐步推导回起点的出界路径数量为多少。**

代码:
```Java
class Solution {
    int MOD = (int)1e9+7;
    int m, n, max;
    int[][] dirs = new int[][]{{1,0},{-1,0},{0,1},{0,-1}};
    public int findPaths(int _m, int _n, int _max, int r, int c) {
        m = _m; n = _n; max = _max;
        int[][] f = new int[m * n][max + 1];
        // 初始化边缘格子的路径数量
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                if (i == 0) add(i, j, f);
                if (j == 0) add(i, j, f);
                if (i == m - 1) add(i, j, f);
                if (j == n - 1) add(i, j, f);
            }
        }
        // 从小到大枚举「可移动步数」
        for (int k = 1; k <= max; k++) {
            // 枚举所有的「位置」
            for (int idx = 0; idx < m * n; idx++) {
                int[] info = parseIdx(idx);
                int x = info[0], y = info[1];
                for (int[] d : dirs) {
                    int nx = x + d[0], ny = y + d[1];
                    if (nx < 0 || nx >= m || ny < 0 || ny >= n) continue;
                    int nidx = getIdx(nx, ny);
                    f[idx][k] += f[nidx][k - 1];
                    f[idx][k] %= MOD;
                }
            }
        }
        return f[getIdx(r, c)][max];
    }
    void add(int x, int y, int[][] f) {
        for (int k = 1; k <= max; k++) {
            f[getIdx(x, y)][k]++;
        }
    }
    int getIdx(int x, int y) {
        return x * n + y;
    }
    int[] parseIdx(int idx) {
        return new int[]{idx / n, idx % n};
    }
}
```
* 时间复杂度:共有 $m * n * max$ 个状态需要转移,复杂度为 $O(m * n * max)$
* 空间复杂度:$O(m * n * max)$

### 最后

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

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

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

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


581. 最短无序连续子数组

Question:
给你一个整数数组 `nums` ,你需要找出一个 连续子数组 ,如果对这个子数组进行升序排序,那么整个数组都会变为升序排序。

请你找出符合题意的 最短 子数组,并输出它的长度。

示例 1:
```
输入:nums = [2,6,4,8,10,9,15]

输出:5

解释:你只需要对 [6, 4, 8, 10, 9] 进行升序排序,那么整个表都会变为升序排序。
```
示例 2:
```
输入:nums = [1,2,3,4]

输出:0
```
示例 3:
```
输入:nums = [1]

输出:0
```

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

* 进阶:你可以设计一个时间复杂度为 $O(n)$ 的解决方案吗?

Solution:
### 双指针 + 排序

最终目的是让整个数组有序,那么我们可以先将数组拷贝一份进行排序,然后使用两个指针 $i$ 和 $j$ 分别找到左右两端第一个不同的地方,那么 $[i, j]$ 这一区间即是答案。

代码:
```Java
class Solution {
    public int findUnsortedSubarray(int[] nums) {
        int n = nums.length;
        int[] arr = nums.clone();
        Arrays.sort(arr);
        int i = 0, j = n - 1;
        while (i <= j && nums[i] == arr[i]) i++;
        while (i <= j && nums[j] == arr[j]) j--;
        return j - i + 1;
    }
}
```
* 时间复杂度:$O(n\log{n})$
* 空间复杂度:$O(n)$

---

### 双指针 + 线性扫描

另外一个做法是,我们把整个数组分成三段处理。

起始时,先通过双指针 $i$ 和 $j$ 找到左右两次侧满足 **单调递增** 的分割点。

即此时 $[0, i]$ 和 $[j, n)$ 满足升序要求,而中间部分 $(i, j)$ **不确保有序**。

然后我们对中间部分 $[i, j]$ 进行遍历:

* 发现 $nums[x] < nums[i - 1]$:由于对 $[i, j]$ 部分进行排序后 $nums[x]$ 会出现在 $nums[i - 1]$ 后,将不满足整体升序,此时我们需要调整分割点 $i$ 的位置;
* 发现 $nums[x] > nums[j + 1]$:由于对 $[i, j]$ 部分进行排序后 $nums[x]$ 会出现在 $nums[j + 1]$ 前,将不满足整体升序,此时我们需要调整分割点 $j$ 的位置。

> 一些细节:在调整 $i$ 和 $j$ 的时候,我们可能会到达数组边缘,这时候可以建立两个哨兵:数组左边存在一个足够小的数,数组右边存在一个足够大的数。

代码:
```Java
class Solution {
    int MIN = -100005, MAX = 100005;
    public int findUnsortedSubarray(int[] nums) {
        int n = nums.length;
        int i = 0, j = n - 1;
        while (i < j && nums[i] <= nums[i + 1]) i++;
        while (i < j && nums[j] >= nums[j - 1]) j--;
        int l = i, r = j;
        int min = nums[i], max = nums[j];
        for (int u = l; u <= r; u++) {
            if (nums[u] < min) {
                while (i >= 0 && nums[i] > nums[u]) i--;
                min = i >= 0 ? nums[i] : MIN;
            }
            if (nums[u] > max) {
                while (j < n && nums[j] < nums[u]) j++;
                max = j < n ? nums[j] : MAX;
            }
        }
        return j == i ? 0 : (j - 1) - (i + 1) + 1;
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


583. 两个字符串的删除操作

Question:
给定两个单词 `s1` 和 `s2`,找到使得 `s1` 和 `s2` 相同所需的最小步数,每步可以删除任意一个字符串中的一个字符。

示例:

```
输入: "sea", "eat"

输出: 2

解释: 第一步将"sea"变为"ea",第二步将"eat"变为"ea"
```

提示:
* 给定单词的长度不超过 $500$。
* 给定单词中的字符只含有小写字母。

Solution:
### 转换为 LCS 问题

首先,给定两字符 `s1` 和 `s2`,求经过多少次删除操作,可使得两个相等字符串。

该问题等价于求解两字符的「最长公共子序列」,若两者长度分别为 $n$ 和 $m$,而最长公共子序列长度为 $max$,则 $n - max + m - max$ 即为答案。

对「最长公共子序列(LCS)」不熟悉的同学,可以看 [(题解) 1143. 最长公共子序列](https://leetcode-cn.com/problems/longest-common-subsequence/solution/gong-shui-san-xie-zui-chang-gong-gong-zi-xq0h/)。

**$f[i][j]$ 代表考虑 $s1$ 的前 $i$ 个字符、考虑 $s2$ 的前 $j$ 个字符(但最长公共子序列中不一定包含 $s1[i]$ 或者 $s2[j]$)时形成的「最长公共子序列(LCS)」长度。**

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

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

可以发现,上述两种讨论已经包含了「不使用 $s1[i]$ 和 $s2[j]$」、「仅使用 $s1[i]$」、「仅使用 $s2[j]$」和「使用 $s1[i]$ 和 $s2[j]$」四种情况。

虽然「不使用 $s1[i]$ 和 $s2[j]$」会被 $f[i - 1][j]$ 和 $f[i][j - 1]$ 重复包含,但对于求最值问题,重复比较并不想影响答案正确性。

因此最终的 $f[i][j]$ 为上述两种讨论中的最大值。

一些编码细节:

通常会习惯性往字符串头部追加一个空格,以减少边界判断(使下标从 1 开始,并很容易构造出可滚动的「有效值」)。但实现上,不用真的往字符串中最佳空格,只需在初始化动规值时假定存在首部空格,以及对最后的 LCS 长度进行减一操作即可。

Java 代码:
```Java
class Solution {
    public int minDistance(String s1, String s2) {
        char[] cs1 = s1.toCharArray(), cs2 = s2.toCharArray();
        int n = s1.length(), m = s2.length();
        int[][] f = new int[n + 1][m + 1];
        // 假定存在哨兵空格,初始化 f[0][x] 和 f[x][0]
        for (int i = 0; i <= n; i++) f[i][0] = 1;
        for (int j = 0; j <= m; j++) f[0][j] = 1;
        for (int i = 1; i <= n; i++) {
            for (int j = 1; j <= m; j++) {
                f[i][j] = Math.max(f[i - 1][j], f[i][j - 1]);
                if (cs1[i - 1] == cs2[j - 1]) f[i][j] = Math.max(f[i][j], f[i - 1][j - 1] + 1);
            }
        }
        int max = f[n][m] - 1; // 减去哨兵空格
        return n - max + m - max;
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    int minDistance(string s1, string s2) {
        int n = s1.size(), m = s2.size();
        vector<vector<int>> f(n + 1, vector<int>(m + 1));
        for (int i = 0; i <= n; i++) f[i][0] = 1;
        for (int j = 0; j <= m; j++) f[0][j] = 1;
        for (int i = 1; i <= n; i++) {
            for (int j = 1; j <= m; j++) {
                f[i][j] = max(f[i - 1][j], f[i][j - 1]);
                if (s1[i - 1] == s2[j - 1]) f[i][j] = max(f[i][j], f[i - 1][j - 1] + 1);
            }
        }
        int max = f[n][m] - 1;
        return n - max + m - max;
    }
};
```
Python 代码:
```Python
class Solution:
    def minDistance(self, s1: str, s2: str) -> int:
        n, m = len(s1), len(s2)
        f = [[1]* (m + 1) for _ in range(n + 1)]
        for i in range(1, n + 1):
            for j in range(1, m + 1):
                f[i][j] = max(f[i - 1][j], f[i][j - 1])
                if s1[i - 1] == s2[j - 1]:
                    f[i][j] = max(f[i][j], f[i - 1][j - 1] + 1)
        max_val = f[n][m] - 1
        return n - max_val + m - max_val
```
TypeScript 代码:
```TypeScript
function minDistance(s1: string, s2: string): number {
    const n = s1.length, m = s2.length;
    const f: number[][] = Array(n + 1).fill(0).map(() => Array(m + 1).fill(1));
    for (let i = 1; i <= n; i++) {
        for (let j = 1; j <= m; j++) {
            f[i][j] = Math.max(f[i - 1][j], f[i][j - 1]);
            if (s1[i - 1] === s2[j - 1]) {
                f[i][j] = Math.max(f[i][j], f[i - 1][j - 1] + 1);
            }
        }
    }
    const max = f[n][m] - 1;
    return n - max + m - max;
};
```
* 时间复杂度:$O(n \times m)$
* 空间复杂度:$O(n \times m)$

---

### 序列 DP

上述解决方案是套用了「最长公共子序列(LCS)」进行求解,最后再根据 LCS 长度计算答案。

而更加契合题意的状态定义是根据「最长公共子序列(LCS)」的原始状态定义进行微调:**定义 $f[i][j]$ 代表考虑 $s1$ 的前 $i$ 个字符、考虑 $s2$ 的前 $j$ 个字符(最终字符串不一定包含 $s1[i]$ 或 $s2[j]$)时形成相同字符串的最小删除次数。**

同理,不失一般性的考虑 $f[i][j]$ 该如何计算:

* `s1[i]==s2[j]`:$f[i][j] = f[i - 1][j - 1]$,代表可以不用必然删掉 $s1[i]$ 和 $s2[j]$ 形成相同字符串;
* `s1[i]!=s2[j]`:$f[i][j] = \min(f[i - 1][j] + 1, f[i][j - 1] + 1)$,代表至少一个删除 $s1[i]$ 和 $s2[j]$ 中的其中一个。

$f[i][j]$ 为上述方案中的最小值,最终答案为 $f[n][m]$。

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

### 最后

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

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

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

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


Question:
在一个二维的花园中,有一些用 $(x, y)$ 坐标表示的树。由于安装费用十分昂贵,你的任务是先用最短的绳子围起所有的树。只有当所有的树都被绳子包围时,花园才能围好栅栏。

你需要找到正好位于栅栏边界上的树的坐标。

示例 1:

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

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

```
示例 2:

```
输入: [[1,2],[2,2],[4,2]]

输出: [[1,2],[2,2],[4,2]]

解释:即使树都在一条直线上,你也需要先用绳子包围它们。
```

注意:
* 所有的树应当被围在一起。你不能剪断绳子来包围树或者把树分成一组以上。
* 输入的整数在 $0$ 到 $100$ 之间。
* 花园至少有一棵树。
* 所有树的坐标都是不同的。
* 输入的点没有顺序。输出顺序也没有要求。

Solution:
### 二维凸包(Andrew 算法)

这是一道「二维凸包」板子题,需要注意的是网上大多数 `Andrew` 算法的板子都是有问题的(下面会说)。

`Andrew` 算法正是用于求解凸包上的所有点(围成所有点的最小周长),其算法逻辑将凸包分为「上凸壳」和「下凸壳」,并分别画出(蓝色分割线将凸包分为两部分):

基本流程为:

1. 对所有点进行双关键字排序,先根据 $x$ 坐标排升序,后根据 $y$ 排升序;
    根据 $x$ 排升序的目的,是为了我们能够往一个方向画出凸包边缘(从左往后画出一半凸壳,从右往左画出另外一半),而将 $y$ 升序目的是可以确保一旦我们现在从 $a$ 到 $b$ 进行连线,那么 $a$ 到 $b$ 之间的所有点能够确保被围住;

2. 使用栈来维护所有凸包上的点,或者说凸包上的边,会更为准确,凸包起点元素会在栈中出现两次(首尾),因此更为准确的描述应该是使用栈维护凸包的所有的边,栈中相邻元素代表凸包上的一条边;

3. 分别「从前往后」和「从后往前」处理排序好的所有点,来分别画出凸包的两半部分,根据画的是第一部分还是第二部分,维护栈内元的处理逻辑稍有不同:

    **a. 画的是凸包的第一部分:**
    * 若栈内元素少于 $2$ 个,组成一条线至少需要两个点,说明此时第一条边都还没画出,直接将元素添加到栈中;
    * 若栈内元素不少于 $2$ 个,考虑是否要将栈顶的边删掉(由栈顶前两个元素组成的边)假设栈顶元素为 $b$,栈顶元素的下一位为 $a$,即栈顶存在一条 $a$ 到 $b$ 的边,当前处理到的点为 $c$,此时我们根据 $ac$ 边是否在 $ab$ 边的时针方向来决定是否要将 $ab$ 边去掉:

        按照上述逻辑处理完所有点,凸包第一部分的点(边)都存在于栈中。

    **b. 画的是凸包的第二部分:逻辑同理,唯一需要注意的是,第一部分的凸包边我们不能删去,假定处理完第一部分凸包,我们栈内有 $m$ 个元素,我们需要将上述「栈顶元素不少于 $2$ 个」的逻辑替换为「栈顶元素大于 $m$ 个」,同时已参与到凸包第一部分的点,不能再考虑,因此需要额外使用一个 $vis$ 数组来记录使用过的点。**

一些细节,为了方便取得栈顶的前两位元素,我们使用数组实现栈,$stk$ 代表栈容器,$tp$ 代表栈顶元素下标。

正如刚刚讲到,起点会被入栈两次(对应第一条边和最后一条边),因此输出方案时,栈顶和栈底我们只选其一即可。

代码:
```Java
class Solution {
    int[] subtraction(int[] a, int[] b) { // 向量相减
        return new int[]{a[0] - b[0], a[1] - b[1]};
    }
    double cross(int[] a, int[] b) { // 叉乘
        return a[0] * b[1] - a[1] * b[0];
    }
    double getArea(int[] a, int[] b, int[] c) { // 向量 ab 转为 向量 ac 过程中扫过的面积
        return cross(subtraction(b, a), subtraction(c, a));
    }
    public int[][] outerTrees(int[][] trees) {
        Arrays.sort(trees, (a, b)->{
            return a[0] != b[0] ? a[0] - b[0] : a[1] - b[1];
        });
        int n = trees.length, tp = 0;
        int[] stk = new int[n + 10];
        boolean[] vis = new boolean[n + 10];
        stk[++tp] = 0; // 不标记起点
        for (int i = 1; i < n; i++) {
            int[] c = trees[i];
            while (tp >= 2) {
                int[] a = trees[stk[tp - 1]], b = trees[stk[tp]];
                if (getArea(a, b, c) > 0) vis[stk[tp--]] = false;
                else break;
            }
            stk[++tp] = i;
            vis[i] = true;
        }
        int size = tp;
        for (int i = n - 1; i >= 0; i--) {
            if (vis[i]) continue;
            int[] c = trees[i];
            while (tp > size) {
                int[] a = trees[stk[tp - 1]], b = trees[stk[tp]];
                if (getArea(a, b, c) > 0) tp--;
                // vis[stk[tp--]] = false; // 非必须
                else break;
            }
            stk[++tp] = i;
            // vis[i] = true; // 非必须
        }
        int[][] ans = new int[tp - 1][2];
        for (int i = 1; i < tp; i++) ans[i - 1] = trees[stk[i]];
        return ans;
    }
}
```
* 时间复杂度:排序复杂度为 $O(n\log{n})$,统计凸包上的点复杂度为 $O(n)$。整体复杂度为 $O(n)$
* 空间复杂度:$O(n)$

---

### 网上 `Andrew` 板子问题

由于我太久没写计算几何了,翻了一下以前在其他 OJ 提交的题,找回自己几年前的板子,好奇心驱使,想看看现在是否有板子上的突破(通常很难,尤其对于计算几何而言),结果发现网上绝大多数板子都存在严重逻辑错误:

1. 最为严重的错误是,处理凸包第一部分和第二部分,都是使用「栈顶元素不少于 $2$ 个」的逻辑:即使存在 $vis$ 数组也无法避免该逻辑错误,在某些数据上,该逻辑**会导致最右边的一些点无法被围住**;
2. 存在对 `vis` 数组的错误理解:`vis` 的作用仅是为了处理凸包第二部分的时候,不要使用到凸包第一部分的点而已。**含义并非是处理过的点,或者当前凸包上的点**,因此你可以看到我代码注释中写了「不标记起点」和「非必须」,在画第二部分的凸包时,只需要确保第一部分使用的点不参与即可,**重复点问题本身就由遍历所保证,而并非 $vis$ 数组**。当然,为了记忆方便,让模板更具有“对称性”,也可以保留在画第二部分凸包的时候,保留对 $vis$ 的维护逻辑,但千万不要搞错了 $vis$ 的含义。

### 最后

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

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

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

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


589. N 叉树的前序遍历

Question:
给定一个 $n$ 叉树的根节点  $root$ ,返回 其节点值的 前序遍历 。

$n$ 叉树 在输入中按层序遍历进行序列化表示,每组子节点由空值 `null` 分隔(请参见示例)。

示例 1:

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

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

```
输入:root = [1,null,2,3,4,5,null,null,6,7,null,8,null,9,10,null,null,11,null,12,null,13,null,null,14]

输出:[1,2,3,6,7,11,14,4,8,12,5,9,13,10]
```

提示:
* 节点总数在范围 $[0, 10^4]$ 内
* $0 <= Node.val <= 10^4$
* $n$ 叉树的高度小于或等于 $1000$

进阶:递归法很简单,你可以使用迭代法完成此题吗?

Solution:
### 递归

常规做法,不再赘述。

代码:
```Java
class Solution {
    List<Integer> ans = new ArrayList<>();
    public List<Integer> preorder(Node root) {
        dfs(root);
        return ans;
    }
    void dfs(Node root) {
        if (root == null) return ;
        ans.add(root.val);
        for (Node node : root.children) dfs(node);
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:忽略递归带来的额外空间开销,复杂度为 $O(1)$

---

### 迭代

针对本题,使用「栈」模拟递归过程。

迭代过程中记录 `(node = 当前节点, cnt = 当前节点遍历过的子节点数量)` 二元组,每次取出栈顶元素,如果当前节点是首次出队(当前遍历过的子节点数量为 $cnt = 0$),则将当前节点的值加入答案,然后更新当前元素遍历过的子节点数量,并重新入队,即将 $(node, cnt + 1)$ 入队,以及将下一子节点 $(node.children[cnt], 0)$ 进行首次入队。

代码;
```Java
class Solution {
    public List<Integer> preorder(Node root) {
        List<Integer> ans = new ArrayList<>();
        Deque<Object[]> d = new ArrayDeque<>();
        d.addLast(new Object[]{root, 0});
        while (!d.isEmpty()) {
            Object[] poll = d.pollLast();
            Node t = (Node)poll[0]; Integer cnt = (Integer)poll[1];
            if (t == null) continue;
            if (cnt == 0) ans.add(t.val);
            if (cnt < t.children.size()) {
                d.addLast(new Object[]{t, cnt + 1});
                d.addLast(new Object[]{t.children.get(cnt), 0});
            }
        }
        return ans;
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n)$

---

### 通用「递归」转「迭代」

另外一种「递归」转「迭代」的做法,是直接模拟系统执行「递归」的过程,这是一种更为通用的做法。

**由于现代编译器已经做了很多关于递归的优化,现在这种技巧已经无须掌握。**

在迭代过程中记录当前栈帧位置状态 `loc`,在每个状态流转节点做相应操作。

代码:
```Java
class Solution {
    public List<Integer> preorder(Node root) {
        List<Integer> ans = new ArrayList<>();
        Deque<Object[]> d = new ArrayDeque<>();
        d.addLast(new Object[]{0, root});
        while (!d.isEmpty()) {
            Object[] poll = d.pollLast();
            Integer loc = (Integer)poll[0]; Node t = (Node)poll[1];
            if (t == null) continue;
            if (loc == 0) {
                ans.add(t.val);
                d.addLast(new Object[]{1, t});
            } else if (loc == 1) {
                int n = t.children.size();
                for (int i = n - 1; i >= 0; i--) {
                    d.addLast(new Object[]{0, t.children.get(i)});
                }
            }
        }
        return ans;
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


590. N 叉树的后序遍历

Question:
给定一个 $n$ 叉树的根节点 $root$ ,返回 其节点值的**后序遍历**。

$n$ 叉树在输入中按层序遍历进行序列化表示,每组子节点由空值 `null` 分隔(请参见示例)。

示例 1:

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

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

```
输入:root = [1,null,2,3,4,5,null,null,6,7,null,8,null,9,10,null,null,11,null,12,null,13,null,null,14]

输出:[2,6,14,11,7,3,12,8,4,13,9,10,5,1]
```

提示:
* 节点总数在范围 $[0, 10^4]$ 内
* $0 <= Node.val <= 10^4$
* $n$ 叉树的高度小于或等于 $1000$

进阶:递归法很简单,你可以使用迭代法完成此题吗?

Solution:
### 递归

常规做法,不再赘述。

Java 代码:
```Java
class Solution {
    List<Integer> ans = new ArrayList<>();
    public List<Integer> postorder(Node root) {
        dfs(root);
        return ans;
    }
    void dfs(Node root) {
        if (root == null) return;
        for (Node node : root.children) dfs(node);
        ans.add(root.val);
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    vector<int> postorder(Node* root) {
        vector<int> ans;
        dfs(root, ans);
        return ans;
    }
    void dfs(Node* root, vector<int>& ans) {
        if (!root) return;
        for (Node* child : root->children) dfs(child, ans);
        ans.push_back(root->val);
    }
};
```
Python 代码:
```Python
class Solution:
    def postorder(self, root: 'Node') -> List[int]:
        def dfs(root, ans):
            if not root: return
            for child in root.children:
                dfs(child, ans)
            ans.append(root.val)
        ans = []
        dfs(root, ans)
        return ans
```
TypeScript 代码:
```TypeScript
function postorder(root: Node | null): number[] {
    const dfs = function(root: Node | null, ans: number[]): void {
        if (!root) return ;
        for (const child of root.children) dfs(child, ans);
        ans.push(root.val);
    };
    const ans: number[] = [];
    dfs(root, ans);
    return ans;
};
```
* 时间复杂度:$O(n)$
* 空间复杂度:忽略递归带来的额外空间开销,复杂度为 $O(1)$

---

### 非递归

针对本题,使用「栈」模拟递归过程。

迭代过程中记录 `(cnt = 当前节点遍历过的子节点数量, node = 当前节点)` 二元组,每次取出栈顶元素,如果当前节点已经遍历完所有的子节点(当前遍历过的子节点数量为 $cnt = 子节点数量$),则将当前节点的值加入答案。

否则更新当前元素遍历过的子节点数量,并重新入队,即将 $(cnt + 1, node)$ 入队,以及将下一子节点 $(0, node.children[cnt])$ 进行首次入队。

Java 代码:
```Java
class Solution {
    public List<Integer> postorder(Node root) {
        List<Integer> ans = new ArrayList<>();
        Deque<Object[]> d = new ArrayDeque<>();
        d.addLast(new Object[]{0, root});
        while (!d.isEmpty()) {
            Object[] poll = d.pollLast();
            Integer cnt = (Integer)poll[0]; Node t = (Node)poll[1];
            if (t == null) continue;
            if (cnt == t.children.size()) ans.add(t.val);
            if (cnt < t.children.size()) {
                d.addLast(new Object[]{cnt + 1, t});
                d.addLast(new Object[]{0, t.children.get(cnt)});
            }
        }
        return ans;
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    vector<int> postorder(Node* root) {
        vector<int> ans;
        stack<pair<int, Node*>> st;
        st.push({0, root});
        while (!st.empty()) {
            auto [cnt, t] = st.top();
            st.pop();
            if (!t) continue;
            if (cnt == t->children.size()) ans.push_back(t->val);
            if (cnt < t->children.size()) {
                st.push({cnt + 1, t});
                st.push({0, t->children[cnt]});
            }
        }
        return ans;
    }
};
```
Python 代码:
```Python
class Solution:
    def postorder(self, root: 'Node') -> List[int]:
        ans = []
        stack = [(0, root)]
        while stack:
            cnt, t = stack.pop()
            if not t: continue
            if cnt == len(t.children):
                ans.append(t.val)
            if cnt < len(t.children):
                stack.append((cnt + 1, t))
                stack.append((0, t.children[cnt]))
        return ans
```
TypeScript 代码:
```TypeScript
function postorder(root: Node | null): number[] {
    const ans = [], stack = [];
    stack.push([0, root]);
    while (stack.length > 0) {
        const [cnt, t] = stack.pop()!;
        if (!t) continue;
        if (cnt === t.children.length) ans.push(t.val);
        if (cnt < t.children.length) {
            stack.push([cnt + 1, t]);
            stack.push([0, t.children[cnt]]);
        }
    }
    return ans;
};
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n)$

---

### 通用「非递归」

另外一种「递归」转「迭代」的做法,是直接模拟系统执行「递归」的过程,这是一种更为通用的做法。

**由于现代编译器已经做了很多关于递归的优化,现在这种技巧已经无须掌握。**

在迭代过程中记录当前栈帧位置状态 `loc`,在每个状态流转节点做相应操作。

Java 代码:
```Java
class Solution {
    public List<Integer> postorder(Node root) {
        List<Integer> ans = new ArrayList<>();
        Deque<Object[]> d = new ArrayDeque<>();
        d.addLast(new Object[]{0, root});
        while (!d.isEmpty()) {
            Object[] poll = d.pollLast();
            Integer loc = (Integer)poll[0]; Node t = (Node)poll[1];
            if (t == null) continue;
            if (loc == 0) {
                d.addLast(new Object[]{1, t});
                int n = t.children.size();
                for (int i = n - 1; i >= 0; i--) d.addLast(new Object[]{0, t.children.get(i)});
            } else if (loc == 1) {
                ans.add(t.val);
            }
        }
        return ans;
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    vector<int> postorder(Node* root) {
        vector<int> ans;
        stack<pair<int, Node*>> st;
        st.push({0, root});
        while (!st.empty()) {
            int loc = st.top().first;
            Node* t = st.top().second;
            st.pop();
            if (!t) continue;
            if (loc == 0) {
                st.push({1, t});
                for (int i = t->children.size() - 1; i >= 0; i--) {
                    st.push({0, t->children[i]});
                }
            } else if (loc == 1) {
                ans.push_back(t->val);
            }
        }
        return ans;
    }
};
```
Python 代码:
```Python
class Solution:
    def postorder(self, root: 'Node') -> List[int]:
        ans = []
        stack = [(0, root)]
        while stack:
            loc, t = stack.pop()
            if not t: continue
            if loc == 0:
                stack.append((1, t))
                for child in reversed(t.children):
                    stack.append((0, child))
            elif loc == 1:
                ans.append(t.val)
        return ans
```
TypeScript 代码:
```TypeScript
function postorder(root: Node | null): number[] {
    const ans: number[] = [];
    const stack: [number, Node | null][] = [[0, root]];
    while (stack.length > 0) {
        const [loc, t] = stack.pop()!;
        if (!t) continue;
        if (loc === 0) {
            stack.push([1, t]);
            for (let i = t.children.length - 1; i >= 0; i--) {
                stack.push([0, t.children[i]]);
            }
        } else if (loc === 1) {
            ans.push(t.val);
        }
    }
    return ans;
};
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


Question:
给定一个表示代码片段的字符串,你需要实现一个验证器来解析这段代码,并返回它是否合法。合法的代码片段需要遵守以下的所有规则:

1. 代码必须被合法的闭合标签包围。否则,代码是无效的。
2. 闭合标签(不一定合法)要严格符合格式:`<TAG_NAME>TAG_CONTENT</TAG_NAME>`。其中,`<TAG_NAME>` 是起始标签,`</TAG_NAME>` 是结束标签。起始和结束标签中的 `TAG_NAME` 应当相同。当且仅当 `TAG_NAME` 和 `TAG_CONTENT` 都是合法的,闭合标签才是合法的。
3. 合法的 `TAG_NAME` 仅含有大写字母,长度在范围 $[1,9]$ 之间。否则,该 `TAG_NAME` 是不合法的。
4. 合法的 `TAG_CONTENT` 可以包含其他合法的闭合标签,`cdata` ( 请参考规则 $7$ )和任意字符( 注意参考规则 $1$ )除了不匹配的 `<`、不匹配的起始和结束标签、不匹配的或带有不合法 `TAG_NAME` 的闭合标签。否则,`TAG_CONTENT` 是不合法的。
5. 一个起始标签,如果没有具有相同 `TAG_NAME` 的结束标签与之匹配,是不合法的。反之亦然。不过,你也需要考虑标签嵌套的问题。
6. 一个 `<`,如果你找不到一个后续的 `>` 与之匹配,是不合法的。并且当你找到一个 `<` 或`</` 时,所有直到下一个 `>` 的前的字符,都应当被解析为 `TAG_NAME`(不一定合法)。
7. `cdata` 有如下格式:`<![CDATA[CDATA_CONTENT]]>`。`CDATA_CONTENT` 的范围被定义成 `<![CDATA[` 和后续的第一个 `]]>`之间的字符。
8. `CDATA_CONTENT` 可以包含任意字符。`cdata` 的功能是阻止验证器解析 `CDATA_CONTENT`,所以即使其中有一些字符可以被解析为标签(无论合法还是不合法),也应该将它们视为常规字符。

合法代码的例子:
```
输入: "<DIV>This is the first line <![CDATA[<div>]]></DIV>"

输出: True

解释:

代码被包含在了闭合的标签内: <DIV> 和 </DIV> 。

TAG_NAME 是合法的,TAG_CONTENT 包含了一些字符和 cdata 。

即使 CDATA_CONTENT 含有不匹配的起始标签和不合法的 TAG_NAME,它应该被视为普通的文本,而不是标签。

所以 TAG_CONTENT 是合法的,因此代码是合法的。最终返回True。

输入: "<DIV>>>  ![cdata[]] <![CDATA[<div>]>]]>]]>>]</DIV>"

输出: True

解释:

我们首先将代码分割为: start_tag|tag_content|end_tag 。

start_tag -> "<DIV>"

end_tag -> "</DIV>"

tag_content 也可被分割为: text1|cdata|text2 。

text1 -> ">>  ![cdata[]] "

cdata -> "<![CDATA[<div>]>]]>" ,其中 CDATA_CONTENT 为 "<div>]>"

text2 -> "]]>>]"

start_tag 不是 "<DIV>>>" 的原因参照规则 6 。
cdata 不是 "<![CDATA[<div>]>]]>]]>" 的原因参照规则 7 。
```
不合法代码的例子:
```
输入: "<A>  <B> </A>   </B>"
输出: False
解释: 不合法。如果 "<A>" 是闭合的,那么 "<B>" 一定是不匹配的,反之亦然。

输入: "<DIV>  div tag is not closed  <DIV>"
输出: False

输入: "<DIV>  unmatched <  </DIV>"
输出: False

输入: "<DIV> closed tags with invalid tag name  <b>123</b> </DIV>"
输出: False

输入: "<DIV> unmatched tags with invalid tag name  </1234567890> and <CDATA[[]]>  </DIV>"
输出: False

输入: "<DIV>  unmatched start tag <B>  and unmatched end tag </C>  </DIV>"
输出: False
```
注意:
* 为简明起见,你可以假设输入的代码(包括提到的任意字符)只包含`数字`, `字母`, `'<'`,`'>'`,`'/'`,`'!'`,`'['`,`']'`和`' '`。

Solution:
### 模拟(栈)

字符串大模拟,假设字符串 `s` 长度为 $n$,当前处理到的位置为 $i$,根据以下优先级进行检查:

1. 优先尝试检查以 $i$ 为开始的连续段是否为 `CDATA`,若能匹配到开头,则尝试匹配到 `CDATA` 的结尾处,并更新 $i$,若无法找到结尾,返回 `False`;
2. 尝试匹配 $s[i]$ 是否为 `<`,若满足,则根据 $s[i + 1]$ 是否为 `/` 来判断当前 `TAG_NAME` 是处于右边还是左边,然后将 `TAG_NAME` 取出,记为 $tag$,判断 $tag$ 长度是否合法,不合法返回 `False`,合法则根据是左边还是右边的 `TAG_NAME` 分情况讨论:
    * 位于左边的 `TAG_NAME`:将其加入栈中,等待右边的 `TAG_NAME` 与其匹配;
    * 位于右边的 `TAG_NAME`:将其与当前栈顶的元素进行匹配,若栈为空或匹配不上,返回 `False`(注意:由于整个 `s` 应当被一对 `TAG_NAME` 所包裹,因此如果取出后栈顶元素匹配后,栈为空,同时又还没处理完整个 `s`,此时 `s` 也不合法,返回 `Fasle`);
3. 其余情况则为普通字符。

最后由于整个 `s` 应当被一对 `TAG_NAME` 所包裹,因此当 $i = 0$ 时,不能是情况 $1$ 和情况 $3$,需要特判一下。

代码:
```Java
class Solution {
    String CDATA1 = "<![CDATA[", CDATA2 = "]]>";
    public boolean isValid(String s) {
        Deque<String> d = new ArrayDeque<>();
        int n = s.length(), i = 0;
        while (i < n) {
            if (i + 8 < n && s.substring(i, i + 9).equals(CDATA1)) {
                if (i == 0) return false;
                int j = i + 9;
                boolean ok = false;
                while (j < n && !ok) {
                    if (j + 2 < n && s.substring(j, j + 3).equals(CDATA2)) {
                        j = j + 3; ok = true;
                    } else {
                        j++;
                    }
                }
                if (!ok) return false;
                i = j;
            } else if (s.charAt(i) == '<') {
                if (i == n - 1) return false;
                boolean isEnd = s.charAt(i + 1) == '/';
                int p = isEnd ? i + 2 : i + 1, j = p;
                while (j < n && s.charAt(j) != '>') {
                    if (!Character.isUpperCase(s.charAt(j))) return false;
                    j++;
                }
                if (j == n) return false;
                int len = j - p;
                if (len < 1 || len > 9) return false;
                String tag = s.substring(p, j);
                i = j + 1;
                if (!isEnd) {
                    d.addLast(tag);
                } else {
                    if (d.isEmpty() || !d.pollLast().equals(tag)) return false;
                    if (d.isEmpty() && i < n) return false;
                }
            } else {
                if (i == 0) return false;
                i++;
            }
        }
        return d.isEmpty();
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


Question:
给定一个表示分数加减运算的字符串 `expression`,你需要返回一个字符串形式的计算结果。

这个结果应该是不可约分的分数,即最简分数。 如果最终结果是一个整数,例如 $2$,你需要将它转换成分数形式,其分母为 $1$。所以在上述例子中, $2$ 应该被转换为 `2/1`。

示例 1:
```
输入: expression = "-1/2+1/2"

输出: "0/1"
```
示例 2:
```
输入: expression = "-1/2+1/2+1/3"

输出: "1/3"
```
示例 3:
```
输入: expression = "1/3-1/2"

输出: "-1/6"
```

提示:
* 输入和输出字符串只包含 `'0'` 到 `'9'` 的数字,以及 `'/'`, `'+'` 和 `'-'`。
* 输入和输出分数格式均为 `±分子/分母`。如果输入的第一个分数或者输出的分数是正数,则 `'+'` 会被省略掉。
* 输入只包含合法的最简分数,每个分数的分子与分母的范围是  $[1,10]$。 如果分母是 $1$,意味着这个分数实际上是一个整数。
* 输入的分数个数范围是 $[1,10]$。
* 最终结果的分子与分母保证是 $32$ 位整数范围内的有效整数。

Solution:
### 表达式计算

为了方便,令 `expression` 为 `s`。

由于给定的表达式中只有 `+` 和 `-`,因此无须考虑优先级问题,直接从前往后计算即可。

使用变量 `ans` 代指计算过程中的结果,从前往后处理表达式 `s`,每次以 `±分子/分母` 的形式取出当前操作数(若为表达式的首个操作数,且为正数时,需要手动补一个 `+`)。

假设当前取出的操作数为 `num`,根据 `ans` 的情况进行运算:

* 若 `ans` 为空串,说明 `num` 是首个操作数,直接将 `num` 赋值给 `ans` 即可
* 若 `ans` 不为空串,此时计算 `num` 和 `ans` 的计算结果赋值给 `ans`

考虑实现一个计算函数 `String calc(String a, String b)` 计算两个操作 `a` 和 `b` 的结果,其中入参 `a` 和 `b` 以及返回值均满足 `±分子/分母` 形式。

首先通过读取 `a` 和 `b` 的首个字符,得到两操作数的正负情况。若为一正一负,通过交换的形式,确保 `a` 为正数,`b` 为负数。

然后通过 `parse` 方法拆解出字符串操作数的分子和分母,`parse` 使用指针扫描的方式实现即可,以数组形式将结果返回(第 $0$ 位为分子数值,第 $1$ 位分母数值)。

假设操作数 `a` 对应的值为 $\frac{p[0]}{p[1]}$,操作数的值为 $\frac{q[0]}{q[1]}$,先将其转换为 $\frac{p[0] \times q[1]}{p[1] \times q[1]}$ 和 $\frac{q[0] \times p[1]}{q[1] \times p[1]}$,进行运算后,再通过求最大公约数 `gcd` 的方式进行化简。

Java 代码:
```Java
class Solution {
    public String fractionAddition(String s) {
        int n = s.length();
        char[] cs = s.toCharArray();
        String ans = "";
        for (int i = 0; i < n; ) {
            int j = i + 1;
            while (j < n && cs[j] != '+' && cs[j] != '-') j++;
            String num = s.substring(i, j);
            if (cs[i] != '+' && cs[i] != '-') num = "+" + num;
            if (!ans.equals("")) ans = calc(num, ans);
            else ans = num;
            i = j;
        }
        return ans.charAt(0) == '+' ? ans.substring(1) : ans;
    }
    String calc(String a, String b) {
        boolean fa = a.charAt(0) == '+', fb = b.charAt(0) == '+';
        if (!fa && fb) return calc(b, a);
        long[] p = parse(a), q = parse(b);
        long p1 = p[0] * q[1], q1 = q[0] * p[1];
        if (fa && fb) {
            long r1 = p1 + q1, r2 = p[1] * q[1], c = gcd(r1, r2);
            return "+" + (r1 / c) + "/" + (r2 / c);
        } else if (!fa && !fb) {
            long r1 = p1 + q1, r2 = p[1] * q[1], c = gcd(r1, r2);
            return "-" + (r1 / c) + "/" + (r2 / c);
        } else {
            long r1 = p1 - q1, r2 = p[1] * q[1], c = gcd(Math.abs(r1), r2);
            String ans = (r1 / c) + "/" + (r2 / c);
            if (p1 >= q1) ans = "+" + ans;
            return ans;
        }
    }
    long[] parse(String s) {
        int n = s.length(), idx = 1;
        while (idx < n && s.charAt(idx) != '/') idx++;
        long a = Long.parseLong(s.substring(1, idx)), b = Long.parseLong(s.substring(idx + 1));
        return new long[]{a, b};
    }
    long gcd(long a, long b) {
        return b == 0 ? a : gcd(b, a % b);
    }
}
```
TypeScript 代码:
```TypeScript
function fractionAddition(s: string): string {
    const n = s.length
    let ans = ""
    for (let i = 0; i < n; ) {
        let j = i + 1
        while (j < n && s[j] != '+' && s[j] != '-') j++
        let num = s.substring(i, j)
        if (s[i] != '+' && s[i] != '-') num = "+" + num
        if (ans != "") ans = calc(num, ans)
        else ans = num
        i = j
    }
    return ans[0] == "+" ? ans.substring(1) : ans
};
function calc(a: string, b: string): string {
    const fa = a[0] == "+", fb = b[0] == "+"
    if (!fa && fb) return calc(b, a)
    const p = parse(a), q = parse(b)
    const p1 = p[0] * q[1], q1 = q[0] * p[1]
    if (fa && fb) {
        const r1 = p1 + q1, r2 = p[1] * q[1], c = gcd(r1, r2)
        return "+" + (r1 / c) + "/" + (r2 / c)
    } else if (!fa && !fb) {
        const r1 = p1 + q1, r2 = p[1] * q[1], c = gcd(r1, r2)
        return "-" + (r1 / c) + "/" + (r2 / c)
    } else {
        const r1 = p1 - q1, r2 = p[1] * q[1], c = gcd(Math.abs(r1), r2)
        let ans = (r1 / c) + "/" + (r2 / c)
        if (p1 > q1) ans = "+" + ans
        return ans
    }
}
function parse(s: string): number[] {
    let n = s.length, idx = 1
    while (idx < n && s[idx] != "/") idx++
    const a = Number(s.substring(1, idx)), b = Number(s.substring(idx + 1))
    return [a, b]
}
function gcd(a: number, b: number): number {
    return b == 0 ? a : gcd(b, a % b)
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(1)$

---

### 加餐 & 加练

**加餐一道更贴合笔试面试的「表达式计算」问题 : [双栈 : 表达式计算问题的通用解法](https://mp.weixin.qq.com/s?__biz=MzU4NDE3MTEyMA==&mid=2247492602&idx=1&sn=135fd5b530189f13e0395414a6b47893) 🎉🎉**

### 最后

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

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

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

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


Question:
给定 `2D` 空间中四个点的坐标 `p1`, `p2`, `p3` 和 `p4`,如果这四个点构成一个正方形,则返回 `true` 。

点的坐标 `pi` 表示为 $[x_i, y_i]$ 。输入 不是 按任何顺序给出的。

一个 有效的正方形 有四条等边和四个等角(`90`度角)。

示例 1:
```
输入: p1 = [0,0], p2 = [1,1], p3 = [1,0], p4 = [0,1]

输出: True
```
示例 2:
```
输入:p1 = [0,0], p2 = [1,1], p3 = [1,0], p4 = [0,12]

输出:false
```
示例 3:
```
输入:p1 = [1,0], p2 = [-1,0], p3 = [0,1], p4 = [0,-1]

输出:true
```

提示:
* $p1.length == p2.length == p3.length == p4.length == 2$
* $-10^4 <= x_i, y_i <= 10^4$

Solution:
### 计算几何

根据题意进行模拟即可。

从给定的 $4$ 个顶点中选 $3$ 个顶点,检查其能否形成「直角三角形」,同时保存下来首个直角三角形的直角边边长,供后续其余直角三角形进行对比(注意不能共点,即直角边长不能为 $0$)。

Java 代码:
```Java
class Solution {
    long len = -1;
    public boolean validSquare(int[] a, int[] b, int[] c, int[] d) {
        return calc(a, b, c) && calc(a, b, d) && calc(a, c, d) && calc(b, c, d);
    }
    boolean calc(int[] a, int[] b, int[] c) {
        long l1 = (a[0] - b[0]) * (a[0] - b[0]) + (a[1] - b[1]) * (a[1] - b[1]);
        long l2 = (a[0] - c[0]) * (a[0] - c[0]) + (a[1] - c[1]) * (a[1] - c[1]);
        long l3 = (b[0] - c[0]) * (b[0] - c[0]) + (b[1] - c[1]) * (b[1] - c[1]);
        boolean ok = (l1 == l2 && l1 + l2 == l3) || (l1 == l3 && l1 + l3 == l2) || (l2 == l3 && l2 + l3 == l1);
        if (!ok) return false;
        if (len == -1) len = Math.min(l1, l2);
        else if (len == 0 || len != Math.min(l1, l2)) return false;
        return true;
    }
}
```
TypeScript 代码:
```TypeScript
let len = -1
function validSquare(a: number[], b: number[], c: number[], d: number[]): boolean {
    len = -1
    return calc(a, b, c) && calc(a, b, d) && calc(a, c, d) && calc(b, c, d)
};
function calc(a: number[], b: number[], c: number[]): boolean {
    const l1 = (a[0] - b[0]) * (a[0] - b[0]) + (a[1] - b[1]) * (a[1] - b[1])
    const l2 = (a[0] - c[0]) * (a[0] - c[0]) + (a[1] - c[1]) * (a[1] - c[1])
    const l3 = (b[0] - c[0]) * (b[0] - c[0]) + (b[1] - c[1]) * (b[1] - c[1])
    const ok = (l1 == l2 && l1 + l2 == l3) || (l1 == l3 && l1 + l3 == l2) || (l2 == l3 && l2 + l3 == l1)
    if (!ok) return false
    if (len == -1) len = Math.min(l1, l2)
    else if (len == 0 || len != Math.min(l1, l2)) return false
    return true
}
```
* 时间复杂度:$O(1)$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


594. 最长和谐子序列

Question:
和谐数组是指一个数组里元素的最大值和最小值之间的差别 **正好是 $1$**。

现在,给你一个整数数组 $nums$ ,请你在所有可能的子序列中找到最长的和谐子序列的长度。

数组的子序列是一个由数组派生出来的序列,它可以通过删除一些元素或不删除元素、且不改变其余元素的顺序而得到。

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

输出:5

解释:最长的和谐子序列是 [3,2,2,2,3]
```
示例 2:
```
输入:nums = [1,2,3,4]

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

输出:0
```

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

Solution:
### 排序 + 滑动窗口

一个直观的想法是,先对 $nums$ 进行排序,然后从前往后使用「双指针」实现「滑动窗口」进行扫描,统计所有符合条件的窗口长度,并在所有长度中取最大值即是答案。

代码:
```Java
class Solution {
    public int findLHS(int[] nums) {
        Arrays.sort(nums);
        int n = nums.length, ans = 0;
        for (int i = 0, j = 0; j < n; j++) {
            while (i < j && nums[j] - nums[i] > 1) i++;
            if (nums[j] - nums[i] == 1) ans = Math.max(ans, j - i + 1);
        }
        return ans;
    }
}
```
* 时间复杂度:排序的复杂度为 $O(n\log{n})$,通过双指针实现的滑动窗口复杂度为 $O(n)$。整体复杂度为 $O(n\log{n})$
* 空间复杂度:$O(\log{n})$

---

### 哈希计数

题目规定的「和谐子序列」中的最值差值正好为 $1$,因而子序列排序后必然符合 $[a,a,..,a+1,a+1]$ 形式,即符合条件的和谐子序列长度为相邻两数(差值为 $1$) 的出现次数之和。

我们可以使用「哈希表」记录所有 $nums[i]$ 的出现次数,然后通过 $O(n)$ 的复杂度找出所有可能的数对(两数差值为 $1$),并在所有符合条件的数对所能构成的「和谐子序列」长度中取最大值。

代码:
```Java
class Solution {
    public int findLHS(int[] nums) {
        Map<Integer, Integer> map = new HashMap<>();
        for (int i : nums) map.put(i, map.getOrDefault(i, 0) + 1);
        int ans = 0;
        for (int i : nums) {
            if (map.containsKey(i - 1)) {
                ans = Math.max(ans, map.get(i) + map.get(i - 1));
            }
        }
        return ans;
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


Question:
给定一个初始元素全部为 $0$,大小为 $m*n$ 的矩阵 $M$ 以及在 $M$ 上的一系列更新操作。

操作用二维数组表示,其中的每个操作用一个含有两个正整数 $a$ 和 $b$ 的数组表示,含义是将所有符合 $0 <= i < a$ 以及 $0 <= j < b$ 的元素 $M[i][j]$ 的值都增加 $14。

在执行给定的一系列操作后,你需要返回矩阵中含有最大整数的元素个数。

示例 1:
```
输入:
m = 3, n = 3
operations = [[2,2],[3,3]]

输出: 4

解释:
初始状态, M =
[[0, 0, 0],
 [0, 0, 0],
 [0, 0, 0]]

执行完操作 [2,2] 后, M =
[[1, 1, 0],
 [1, 1, 0],
 [0, 0, 0]]

执行完操作 [3,3] 后, M =
[[2, 2, 1],
 [2, 2, 1],
 [1, 1, 1]]

M 中最大的整数是 2, 而且 M 中有4个值为2的元素。因此返回 4。
```
注意:
* $m$ 和 $n$ 的范围是 $[1,40000]$。
* $a$ 的范围是 $[1,m]$,$b$ 的范围是 $[1,n]$。
* 操作数目不超过 $10000$。

Solution:
### 模拟

由于每次都是对 $0 \leq i < a$ 和 $0 \leq j < b$ 进行操作,因此每次操作都会包含点 $(0, 0)$,最后的最大值一定出现在位置 $(0, 0)$ 上。

问题转换为:什么范围内的数与位置 $(0, 0)$ 上的值相等,即什么范围会被每一次操作所覆盖。

不难发现,在所有的 $ops[i]$ 中的横纵坐标 $(x, y)$ 与左上角 $(0, 0)$ 形成的区域面积可确保被每次操作覆盖,$x * y$ 即是答案。

代码:
```Java
class Solution {
    public int maxCount(int m, int n, int[][] ops) {
        for (int[] op : ops) {
            m = Math.min(m, op[0]);
            n = Math.min(n, op[1]);
        }
        return m * n;
    }
}
```
* 时间复杂度:令 $k$ 为 $ops$ 数组的长度。复杂度为 $O(k)$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


LeetCode/591-600/599. 两个列表的最小索引总和（简单）.md

599. 两个列表的最小索引总和

Question:
假设 Andy 和 Doris 想在晚餐时选择一家餐厅,并且他们都有一个表示最喜爱餐厅的列表,每个餐厅的名字用字符串表示。

你需要帮助他们用最少的索引和找出他们共同喜爱的餐厅。 如果答案不止一个,则输出所有答案并且不考虑顺序。 你可以假设答案总是存在。

示例 1:
```
输入: list1 = ["Shogun", "Tapioca Express", "Burger King", "KFC"],list2 = ["Piatti", "The Grill at Torrey Pines", "Hungry Hunter Steakhouse", "Shogun"]

输出: ["Shogun"]

解释: 他们唯一共同喜爱的餐厅是“Shogun”。
```
示例 2:
```
输入:list1 = ["Shogun", "Tapioca Express", "Burger King", "KFC"],list2 = ["KFC", "Shogun", "Burger King"]

输出: ["Shogun"]

解释: 他们共同喜爱且具有最小索引和的餐厅是“Shogun”,它有最小的索引和1(0+1)。
```

提示:
* $1 <= list1.length, list2.length <= 1000$
* $1 <= list1[i].length, list2[i].length <= 30$
* $list1[i]$ 和 $list2[i]$ 由空格 `' '` 和英文字母组成。
* $list1$ 的所有字符串都是 唯一 的。
* $list2$ 中的所有字符串都是 唯一 的。

Solution:
### 哈希表

为了快速判断某个字符串是否在另外一个数组中出现,我们可以先使用「哈希表」对 `list1` 中的字符串进行处理,以 $(list1[i]: i)$ 键值对形式进行存储。

然后遍历 `list2`,判断每个 $list2[i]$ 是否在哈希表中出现过,同时维护一个当前的 **最小索引总和** $min$,以及 **用于存储能够取得最小索引总和的字符串数组** $ans$。

假设当前遍历到的元素是 $list2[i]$,根据「$list2[i]$ 是否在哈希表中出现」以及「当前索引和与 $min$ 的大小关系」分情况讨论:

* 如果 $list2[i]$ 不在哈希表中,跳过:
* 如果 $list2[i]$ 在哈希表中:
    * 索引之和等于 $min$,将 $list2[i]$ 加入 $ans$;
    * 索引之和小于 $min$,更新 $min$,清空 $ans$,将 $list2[i]$ 加入 $ans$;
    * 索引之和大于 $min$,跳过。

代码:
```Java
class Solution {
    public String[] findRestaurant(String[] list1, String[] list2) {
        int n = list1.length, m = list2.length;
        Map<String, Integer> map = new HashMap<>();
        for (int i = 0; i < n; i++) map.put(list1[i], i);
        List<String> ans = new ArrayList<>();
        int min = 3000;
        for (int i = 0; i < m; i++) {
            String s = list2[i];
            if (!map.containsKey(s)) continue;
            if (i + map.get(s) < min) {
                ans.clear();
                min = i + map.get(s);
                ans.add(s);
            } else if (i + map.get(s) == min) {
                ans.add(s);
            }
        }
        return ans.toArray(new String[ans.size()]);
    }
}
```
* 时间复杂度:$O(n + m)$
* 空间复杂度:$O(n)$

---

### 答疑

评论区有位同学提出了一个挺有意思的疑问,或许是部分同学的共同疑问,这里集中答疑一下。

Q0: `for` 循环里的 `ans.clear()` 这个函数也是 $O(n)$ 复杂度吧,为什么合起来还是 $O(n)$ ?

A0: 在 `ArrayList` 源码中的 `clear` 实现会为了消除容器对对象的强引用,遍历容器内的内容并置空来帮助 GC。

但不代表这会导致复杂度上界变成 $n^2$。

不会导致复杂度退化的核心原因是:**由于 `clear` 导致的循环计算量总共必然不会超过 $n$**。因为最多只有 $n$ 个元素在 `ans` 里面,且同一元素不会被删除多次(即每个元素对 `clear` 的贡献不会超过 $1$)。

如果有同学还是觉得不好理解,可以考虑一种极端情况:`clear` 操作共发生 $n$ 次,但发生 $n$ 次的前提条件是每次 `ans` 中只有 $1$ 位元素,此时由 `clear` 操作带来的额外计算量为最大值 $n$。

因此这里的 `clear` 操作对复杂度影响是「累加」,而不是「累乘」,即复杂度仍为 $O(n)$,而不是 $O(n^2)$。

<br/>

Q1: 判断 $list[i]$ 是否在哈希中的操作,复杂度是多少?

A1: 在 Java 的 `HashMap` 实现中,当键值对中的键数据类型为 `String` 时,会先计算一次(之后使用缓存)该字符串的 `HashCode`,计算 `HashCode` 的过程需要遍历字符串,因此该操作是与字符串长度相关的(对于本题字符串长度不超过 $30$),然后根据 `HashCode`「近似」$O(1)$ 定位到哈希桶位置并进行插入/更新。

因此在 Java 中,该操作与「当前的字符串长度」相关,而与「当前容器所包含元素多少」无关。

### 最后

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

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

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

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


600. 不含连续1的非负整数

Question:
给定一个正整数 n,找出小于或等于 n 的非负整数中,其二进制表示不包含**连续的1**的个数。

示例 1:
```
输入: 5

输出: 5

解释:
下面是带有相应二进制表示的非负整数<= 5:
0 : 0
1 : 1
2 : 10
3 : 11
4 : 100
5 : 101
其中,只有整数3违反规则(有两个连续的1),其他5个满足规则。
```

说明: 1 <= n <= 109

Solution:
### 数位 DP

这是一道典型的「数位 DP」题。

**对于「数位 DP」题,都存在「询问 $[a, b]$($a$ 和 $b$ 均为正整数,且 $a < b$)区间内符合条件的数值个数为多少」的一般形式,通常我们需要实现一个查询 $[0, x]$ 有多少合法数值的函数 `int dp(int x)`,然后应用「容斥原理」求解出 $[a, b]$ 的个数:$dp(b) - dp(a - 1)$。**

对于本题,虽然只需要求解 $[0, n]$ 范围内数的个数,但其实拓展到求 $[a, b]$ 区间个数的也不会增加难度。

具体的,对于「数位 DP」问题通常是「从高位到低位」的分情况讨论。

不失一般性的考虑数值 $n$ 的某一位 $cur$ 是如何被处理的:

1. 如果当前位 $cur = 1$ 的话,由于我们需要满足「小于等于 $n$」的要求,因此如果该位填 $0$ 的话,后面的低位填什么都是满足要求的,因此我们期望能够查表得出「长度为 $i + 1$,且二进制位置 $i$ 数值为 $0$ 时」有多少合法数值,将其累加到答案中;
    与此同时,我们需要确保当前位选 $1$ 是合法的,即我们需要记录上一位 $prev$ 是什么,确保 $cur$ 和 $prev$ 不同时为 $1$。
2. 如果当前位 $cur = 0$ 的话,我们只能选 $0$,并决策下一位。

当出现「当前位无法填入 $cur$」或者「决策到最低位」时,则完成了所有合法答案的统计。

至于流程 $1$ 中的查表操作,我们可以使用 `static` 预处理出 `f` 数组,定义 $f[i][j]$ 为考虑二进制长度为 $i$,且最高位为 $j$($0$ or $1$)时的合法数个数(值不超过)。

PS. 值不超过的含义代表了不仅仅统计高位为 $j$ 的情况。例如 $f[4][1]$ 代表长度为 $4$,最高为 $1$,其包含了 1xxx 和 0xxx 的合法数的个数。

> 注意:为了防止重复计数问题,我们在不失一般性的计算 $f[i][0]$ 和 $f[i][1]$ 时,既能采用诸如 $f[i][cur] += f[i - 1][prev]$ 的 “后向查找依赖” 的方式进行转移,也能采用 $f[i + 1][cur] += f[i][prev]$ “前向主动更新” 的方式进行转移。

不失一般性的考虑 $f[i][0]$ 和 $f[i][1]$ 能够更新哪些状态:

* 如果期望当前位填 $0$ 的话,需要统计所有满足 $(0...)_2$ 形式的合法数值,当前位的低一位只能填 $1$(填 $0$ 会出现重复计数,即需要忽略前导零的数值),此时有:$f[i + 1][0] = f[i][1]$;

* 如果期望当前位填 $1$ 的话,需要统计所有满足 $(1...)_2$ 和 $(0...)_2$ 形式的合法数值:
    * $(1...)_2$ 时,当前位的低一位只能填 $0$;此时有:$f[i + 1][1] += f[i][0]$;
    * $(0...)_2$ 时,当前位的低一位只能填 $1$;此时有:$f[i + 1][1] += f[i][1]$。

代码:
```Java
class Solution {
    static int N = 50;
    // f[i][j] 为考虑二进制长度为 i,而且最高位为 j(0 or 1)时的合法数个数(值不超过)
    // 如 f[2][1] 代表二进制长度为 2,且(值不超过)最高位为 1 的合法数的个数:10、01、00
    static int[][] f = new int[N][2];
    static {
        f[1][0] = 1; f[1][1] = 2;
        for (int i = 1; i < N - 1; i++) {
            f[i + 1][0] = f[i][1];
            f[i + 1][1] = f[i][0] + f[i][1];
        }
    }
    int getLen(int n) {
        for (int i = 31; i >= 0; i--) {
            if (((n >> i) & 1) == 1) return i;
        }
        return 0;
    }
    public int findIntegers(int n) {
        int len = getLen(n);
        int ans = 0, prev = 0;
        for (int i = len; i >= 0; i--) {
            // 当前位是 0 还是 1
            int cur = ((n >> i) & 1);
            // 如果当前位是 1,那么填 0 的话,后面随便填都符合,将方案数累加
            if (cur == 1) ans += f[i + 1][0];
            // 出现连续位为 1,分支结束,方案数被计算完
            if (prev == 1 && cur == 1) break;
            prev = cur;
            if (i == 0) ans++;
        }
        return ans;
    }
}
```
* 时间复杂度:由于我们预处理 `f` 数组的操作使用了 `static` 修饰(在跑样例数据前已经预处理完,且预处理结果被所有样例数据所共享),因此访问 `f` 数组是 $O(1)$ 的查表操作;统计答案的复杂度与二进制长度相关,复杂度为 $O(\log{n})$。整体复杂度为 $O(\log{n})$
* 空间复杂度:令 $C$ 为预处理数值的大小,固定为 $50 * 2$,复杂度为 $O(C)$

### 最后

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

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

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

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


606. 根据二叉树创建字符串

Question:
你需要采用前序遍历的方式,将一个二叉树转换成一个由括号和整数组成的字符串。

空节点则用一对空括号 `"()"` 表示。而且你需要省略所有不影响字符串与原始二叉树之间的一对一映射关系的空括号对。

示例 1:
```
输入: 二叉树: [1,2,3,4]
       1
     /   \
    2     3
   /
  4

输出: "1(2(4))(3)"

解释: 原本将是“1(2(4)())(3())”,
在你省略所有不必要的空括号对之后,
它将是“1(2(4))(3)”。
```
示例 2:
```
输入: 二叉树: [1,2,3,null,4]
       1
     /   \
    2     3
     \
      4

输出: "1(2()(4))(3)"

解释: 和第一个示例相似,
除了我们不能省略第一个对括号来中断输入和输出之间的一对一映射关系。
```

Solution:
### 递归

生成字符串的规则其实就是在「前序遍历」输出节点值的同时,在每颗子树的左右添加一对 `()`(根节点除外),同时需要忽略掉一些不必要的 `()` 。

所谓的不必要就是指当以某个节点 $x$ 为根时,其只「有左子树」而「没有右子树」时,右子树的 `()` 可被忽略,或者「左右子树都没有」时,两者的 `()` 可被忽略。

或者反过来说,如果对于每个非空节点才添加 `()` 的话,那么当「有右子树」同时「没有左子树」时,左子树的 `()` 不能被忽略,需要额外添加,从而确保生成出来的字符串能够与「有左子树」同时「没有右子树」的情况区分开来,而不会产生二义性。

代码:
```Java
class Solution {
    StringBuilder sb = new StringBuilder();
    public String tree2str(TreeNode root) {
        dfs(root);
        return sb.substring(1, sb.length() - 1);
    }
    void dfs(TreeNode root) {
        sb.append("(");
        sb.append(root.val);
        if (root.left != null) dfs(root.left);
        else if (root.right != null) sb.append("()");
        if (root.right != null) dfs(root.right);
        sb.append(")");
    }
}
```
* 时间复杂度:令点数为 $n$,边数为 $m$。整体复杂度为 $O(n + m)$
* 空间复杂度:$O(n)$

---

### 迭代(非递归)

自然也能使用「迭代(非递归)」来做,使用栈来模拟上述递归过程。

由于当以某个节点 $x$ 为根节点时,其需要在 **开始** 前序遍历当前子树时添加 `(`,在 **结束** 前序遍历时添加 `)`,因此某个节点需要出入队两次。

同时区分是首次出队(开始前序遍历)还是二次出队(结束前序遍历),这需要使用一个 `set` 来做记录,其余逻辑与「递归」做法类似。

代码:
```Java
class Solution {
    public String tree2str(TreeNode root) {
        StringBuilder sb = new StringBuilder();
        Set<TreeNode> vis = new HashSet<>();
        Deque<TreeNode> d = new ArrayDeque<>();
        d.addLast(root);
        while (!d.isEmpty()) {
            TreeNode t = d.pollLast();
            if (vis.contains(t)) {
                sb.append(")");
            } else {
                d.addLast(t);
                sb.append("(");
                sb.append(t.val);
                if (t.right != null) d.addLast(t.right);
                if (t.left != null) d.addLast(t.left);
                else if (t.right != null) sb.append("()");
                vis.add(t);
            }
        }
        return sb.substring(1, sb.length() - 1);
    }
}
```
* 时间复杂度:令点数为 $n$,边数为 $m$。整体复杂度为 $O(n + m)$
* 空间复杂度:$O(n)$

---

### 迭代(通用非递归)

上述的「迭代(非递归)」解法,我们还是需要针对题目再做简单分析。

而利用通用的「递归」转「非递归」技巧,我们可以直接对「递归」解法进行改写,同时利用通用非递归过程中的 `loc` 可以帮助我们省掉用作区分是否首次出队的 `set` 结构。

需要特别说明的是:**由于现代编译器已经做了很多关于递归的优化,现在这种技巧已经无须掌握。**

代码:
```Java
class Solution {
    public String tree2str(TreeNode root) {
        StringBuilder sb = new StringBuilder();
        Deque<Object[]> d = new ArrayDeque<>();
        d.addLast(new Object[]{0, root});
        while (!d.isEmpty()) {
            Object[] poll = d.pollLast();
            int loc = (Integer)poll[0]; TreeNode t = (TreeNode)poll[1];
            if (loc == 0) {
                sb.append("(");
                sb.append(t.val);
                d.addLast(new Object[]{1, t});
            } else if (loc == 1) {
                d.addLast(new Object[]{2, t});
                if (t.right != null) d.addLast(new Object[]{0, t.right});
                if (t.left != null) d.addLast(new Object[]{0, t.left});
                else if (t.right != null) sb.append("()");
            } else if (loc == 2) {
                sb.append(")");
            }
        }
        return sb.substring(1, sb.length() - 1);
    }
}
```
* 时间复杂度:令点数为 $n$,边数为 $m$。整体复杂度为 $O(n + m)$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


Question:
给你一个链表的头节点 `head`,旋转链表,将链表每个节点向右移动 `k` 个位置。

示例 1:

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

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

```
输入:head = [0,1,2], k = 4

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

提示:
* 链表中节点的数目在范围 $[0, 500]$ 内
* $-100 <= Node.val <= 100$
* $0 <= k <= 2 * 10^9$

Solution:
### 快慢指针

本质还是道模拟题,分步骤处理即可:

* 避免不必要的旋转:与链表长度成整数倍的「旋转」都是没有意义的(旋转前后链表不变)
* 使用「快慢指针」找到倒数第 `k` 个节点(新头结点),然后完成基本的链接与断开与断开操作

Java 代码:
```Java
class Solution {
    public ListNode rotateRight(ListNode head, int k) {
        if (head == null || k == 0) return head;
        // 计算有效的 k 值:对于与链表长度成整数倍的「旋转」都是没有意义的(旋转前后链表不变)
        int tot = 0;
        ListNode tmp = head;
        while (tmp != null && ++tot > 0) tmp = tmp.next;
        k %= tot;
        if (k == 0) return head;
        // 使用「快慢指针」找到倒数第 k 个节点(新头结点):slow 会停在「新头结点」的「前一位」,也就是「新尾结点」
        ListNode slow = head, fast = head;
        while (k-- > 0) fast = fast.next;
        while (fast.next != null) {
            slow = slow.next;
            fast = fast.next;
        }
        // 保存新头结点,并将新尾结点的 next 指针置空
        ListNode nHead = slow.next;
        slow.next = null;
        // 将新链表的前半部分(原链表的后半部分)与原链表的头结点链接上
        fast.next = head;
        return nHead;
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    ListNode* rotateRight(ListNode* head, int k) {
        if (head == NULL || k == 0) return head;
        // 计算有效的 k 值:对于与链表长度成整数倍的「旋转」都是没有意义的(旋转前后链表不变)
        int tot = 0;
        ListNode* tmp = head;
        while (tmp != NULL && ++tot > 0) tmp = tmp->next;
        k %= tot;
        // 使用「快慢指针」找到倒数第 k 个节点(新头结点):slow 会停在「新头结点」的「前一位」,也就是「新尾结点」
        if (k == 0) return head;
        // 使用快慢指针找到倒数第 k 个节点
        ListNode* slow = head;
        ListNode* fast = head;
        while (k-- > 0) fast = fast->next;
        while (fast->next != NULL) {
            slow = slow->next;
            fast = fast->next;
        }
        // 保存新头结点,并将新尾结点的 next 指针置空
        ListNode* nHead = slow->next;
        slow->next = NULL;
        // 将新链表的前半部分(原链表的后半部分)与原链表的头结点链接上
        fast->next = head;
        return nHead;
    }
};
```
Python 代码:
```Python
class Solution:
    def rotateRight(self, head: Optional[ListNode], k: int) -> Optional[ListNode]:
        if not head or k == 0: return head
        # 计算有效的 k 值:对于与链表长度成整数倍的「旋转」都是没有意义的(旋转前后链表不变)
        tot, tmp = 0, head
        while tmp:
            tmp = tmp.next
            tot += 1
        k %= tot
        # 使用「快慢指针」找到倒数第 k 个节点(新头结点):slow 会停在「新头结点」的「前一位」,也就是「新尾结点」
        if k == 0: return head
        # 使用快慢指针找到倒数第 k 个节点
        slow, fast = head, head
        while k > 0:
            fast = fast.next
            k -= 1
        while fast.next:
            slow = slow.next
            fast = fast.next
        # 保存新头结点,并将新尾结点的 next 指针置空
        nHead = slow.next
        slow.next = None
        # 将新链表的前半部分(原链表的后半部分)与原链表的头结点链接上
        fast.next = head
        return nHead
```
TypeScript 代码:
```TypeScript
function rotateRight(head: ListNode | null, k: number): ListNode | null {
    if (head == null || k == 0) return head;
    // 计算有效的 k 值:对于与链表长度成整数倍的「旋转」都是没有意义的(旋转前后链表不变)
    let tot = 0, tmp = head;
    while (tmp !== null && ++tot > 0) tmp = tmp.next;
    k %= tot;
    // 使用「快慢指针」找到倒数第 k 个节点(新头结点):slow 会停在「新头结点」的「前一位」,也就是「新尾结点」
    if (k == 0) return head;
    // 使用快慢指针找到倒数第 k 个节点
    let slow = head, fast = head;
    while (k-- > 0) fast = fast.next;
    while (fast.next !== null) {
        slow = slow.next;
        fast = fast.next;
    }
    // 保存新头结点,并将新尾结点的 next 指针置空
    let nHead = slow.next;
    slow.next = null;
    // 将新链表的前半部分(原链表的后半部分)与原链表的头结点链接上
    fast.next = head;
    return nHead;
};
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(1)$

---

### 闭合成环

另外一个做法是,先成环,再断开:

* 找到原链表的最后一个节点,将其与原链表的头结点相连(成环),并统计链表长度,更新有效 `k` 值
* 从原链表的头节点出发,找到需要断开的点,进行断开

Java 代码:
```java
class Solution {
    public ListNode rotateRight(ListNode head, int k) {
        if (head == null || k == 0) return head;
        // 先将链表成环,并记录链表的长度
        // tmp 会记录住原链表最后一位节点
        int tot = 1;
        ListNode tmp = head;
        while (tmp.next != null && ++tot > 0) tmp = tmp.next;
        k %= tot;
        if (k == 0) return head;
        // 正式成环
        tmp.next = head;
        // 从原链表 head 出发,走 tot - k - 1 步,找到「新尾结点」进行断开,并将其下一个节点作为新节点返回
        k = tot - k - 1;
        while (k-- > 0) head = head.next;
        ListNode nHead = head.next;
        head.next = null;
        return nHead;
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    ListNode* rotateRight(ListNode* head, int k) {
        if (head == NULL || k == 0) return head;
        // 先将链表成环,并记录链表的长度
        // tmp 会记录住原链表最后一位节点
        int tot = 1;
        ListNode* tmp = head;
        while (tmp->next != NULL && ++tot > 0) tmp = tmp->next;
        k %= tot;
        if (k == 0) return head;
        // 正式成环
        tmp->next = head;
        // 从原链表 head 出发,走 tot - k - 1 步,找到「新尾结点」进行断开,并将其下一个节点作为新节点返回
        k = tot - k - 1;
        while (k-- > 0) head = head->next;
        ListNode* nHead = head->next;
        head->next = NULL;
        return nHead;
    }
};
```
Python 代码:
```Python
class Solution:
    def rotateRight(self, head: Optional[ListNode], k: int) -> Optional[ListNode]:
        if not head or k == 0: return head
        # 先将链表成环,并记录链表的长度
        # tmp 会记录住原链表最后一位节点
        tot, tmp = 1, head
        while tmp.next:
            tmp = tmp.next
            tot += 1
        k %= tot
        if k == 0: return head
        # 正式成环
        tmp.next = head
        # 从原链表 head 出发,走 tot - k - 1 步,找到「新尾结点」进行断开,并将其下一个节点作为新节点返回
        k = tot - k - 1
        while k > 0:
            head = head.next
            k -= 1
        nHead = head.next
        head.next = None
        return nHead
```
TypeScript 代码:
```TypeScript
function rotateRight(head: ListNode | null, k: number): ListNode | null {
    if (head == null || k == 0) return head;
    // 先将链表成环,并记录链表的长度
    // tmp 会记录住原链表最后一位节点
    let tot = 1, tmp = head;
    while (tmp.next !== null && ++tot > 0) tmp = tmp.next;
    k %= tot;
    if (k == 0) return head;
    // 正式成环
    tmp.next = head;
    k = tot - k - 1;
    while (k-- > 0) head = head.next;
    let nHead = head.next;
    head.next = null;
    return nHead;
};
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


Question:
有效数字(按顺序)可以分成以下几个部分:

1. 一个 小数 或者 整数
2. (可选)一个 `'e'` 或 `'E'` ,后面跟着一个 整数

小数(按顺序)可以分成以下几个部分:

1. (可选)一个符号字符(`'+'` 或 `'-'`)
2. 下述格式之一:
	1. 至少一位数字,后面跟着一个点 `'.'`
	2. 至少一位数字,后面跟着一个点 `'.'` ,后面再跟着至少一位数字
	3. 一个点 `'.'` ,后面跟着至少一位数字

整数(按顺序)可以分成以下几个部分:

1. (可选)一个符号字符(`'+'` 或 `'-'`)
2. 至少一位数字

部分有效数字列举如下:

* `["2", "0089", "-0.1", "+3.14", "4.", "-.9", "2e10", "-90E3", "3e+7", "+6e-1", "53.5e93", "-123.456e789"]`

部分无效数字列举如下:

* `["abc", "1a", "1e", "e3", "99e2.5", "--6", "-+3", "95a54e53"]`

给你一个字符串 `s`,如果 `s` 是一个 有效数字 ,请返回 `true`。

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

输出:true
```
示例 2:
```
输入:s = "e"

输出:false
```
示例 3:
```
输入:s = "."

输出:false
```
示例 4:
```
输入:s = ".1"

输出:true
```

提示:
* $1 <= s.length <= 20$
* `s` 仅含英文字母(大写和小写),数字(`0-9`),加号 `'+'` ,减号 `'-'` ,或者点 `'.'` 。

Solution:
### 模拟

字符串大模拟,根据「有效数字定义」梳理规则即可。

本题有多种解法:「正则」、「DFA」、「模拟」...

**「模拟」是在各类场景中最容易实现的方式,只要头脑不发热都能写出来。**

将字符串以 `e/E` 进行分割后,其实规则十分简单:

* 如果存在 `e/E` :左侧可以「整数」或「浮点数」,右侧必须是「整数」
* 如果不存在 `e/E` :整段可以是「整数」或「浮点数」

关键在于如何实现一个 `check` 函数用于判断「整数」或「浮点数」:

* `+/-` 只能出现在头部
* `.` 最多出现一次
* 至少存在一个数字

代码:
```Java
class Solution {
    public boolean isNumber(String s) {
        int n = s.length();
        char[] cs = s.toCharArray();
        int idx = -1;
        for (int i = 0; i < n; i++) {
            if (cs[i] == 'e' || cs[i] == 'E') {
                if (idx == -1) idx = i;
                else return false;
            }
        }
        boolean ans = true;
        if (idx != -1) {
            ans &= check(cs, 0, idx - 1, false);
            ans &= check(cs, idx + 1, n - 1, true);
        } else {
            ans &= check(cs, 0, n - 1, false);
        }
        return ans;
    }
    boolean check(char[] cs, int start, int end, boolean mustInteger) {
        if (start > end) return false;
        if (cs[start] == '+' || cs[start] == '-') start++;
        boolean hasDot = false, hasNum = false;
        for (int i = start; i <= end; i++) {
            if (cs[i] == '.') {
                if (mustInteger || hasDot) return false;
                hasDot = true;
            } else if (cs[i] >= '0' && cs[i] <= '9') {
                hasNum = true;
            } else {
                return false;
            }
        }
        return hasNum;
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:将 `toCharArray` 替换为 `charAt` 为 $O(1)$,否则为 $O(n)$

### 最后

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

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

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

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


Question:
给定一个由 整数 组成的 非空 数组所表示的非负整数,在该数的基础上加一。

最高位数字存放在数组的首位, 数组中每个元素只存储单个数字。

你可以假设除了整数 0 之外,这个整数不会以零开头。

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

输出:[1,2,4]

解释:输入数组表示数字 123。
```
示例 2:
```
输入:digits = [4,3,2,1]

输出:[4,3,2,2]

解释:输入数组表示数字 4321。
```

示例 3:
```
输入:digits = [0]

输出:[1]
```

提示:
* 1 <= digits.length <= 100
* 0 <= digits[i] <= 9

Solution:
### 模拟

这是 [2. 两数相加](https://leetcode-cn.com/problems/add-two-numbers/solution/po-su-jie-fa-shao-bing-ji-qiao-by-ac_oie-etln/) 的简化版,其中一个操作固定为 $1$。

只需要按照题目进行模拟即可,使用 $t$ 记录进位。

代码:
```Java
class Solution {
    public int[] plusOne(int[] digits) {
        int n = digits.length;
        digits[n - 1]++;
        List<Integer> list = new ArrayList<>();
        for (int i = n - 1, t = 0; i >= 0 || t != 0; i--) {
            int x = i >= 0 ? digits[i] + t : t;
            list.add(x % 10);
            t = x / 10;
        }
        Collections.reverse(list);
        int[] ans = new int[list.size()];
        for (int i = 0; i < list.size(); i++) ans[i] = list.get(i);
        return ans;
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


Question:
给定一个单词数组和一个长度 `maxWidth`,重新排版单词,使其成为每行恰好有 `maxWidth` 个字符,且左右两端对齐的文本。

你应该使用“贪心算法”来放置给定的单词;也就是说,尽可能多地往每行中放置单词。必要时可用空格 ' ' 填充,使得每行恰好有 `maxWidth` 个字符。

要求尽可能均匀分配单词间的空格数量。如果某一行单词间的空格不能均匀分配,则左侧放置的空格数要多于右侧的空格数。

文本的最后一行应为左对齐,且单词之间不插入额外的空格。

说明:
* 单词是指由非空格字符组成的字符序列
* 每个单词的长度大于 $0$,小于等于 `maxWidth`
* 输入单词数组 `words` 至少包含一个单词

示例:

```
输入:
words = ["This", "is", "an", "example", "of", "text", "justification."]
maxWidth = 16

输出:
[
   "This    is    an",
   "example  of text",
   "justification.  "
]
```
示例 2:
```
输入:
words = ["What","must","be","acknowledgment","shall","be"]

maxWidth = 16

输出:
[
  "What   must   be",
  "acknowledgment  ",
  "shall be        "
]

解释: 注意最后一行的格式应为 "shall be    " 而不是 "shall     be",
     因为最后一行应为左对齐,而不是左右两端对齐。
     第二行同样为左对齐,这是因为这行只包含一个单词。
```
示例 3:
```
输入:
words = ["Science","is","what","we","understand","well","enough","to","explain",
         "to","a","computer.","Art","is","everything","else","we","do"]
maxWidth = 20

输出:
[
  "Science  is  what we",
  "understand      well",
  "enough to explain to",
  "a  computer.  Art is",
  "everything  else  we",
  "do                  "
]
```

Solution:
### 模拟

字符串大模拟,分情况讨论即可:

1. 如果当前行只有一个单词,特殊处理为左对齐;
2. 如果当前行为最后一行,特殊处理为左对齐;
3. 其余为一般情况,分别计算「当前行单词总长度」、「当前行空格总长度」和「往下取整后的单位空格长度」,然后依次进行拼接。当空格无法均分时,每次往靠左的间隙多添加一个空格,直到剩余的空格能够被后面的间隙所均分。

代码:
```Java
class Solution {
    public List<String> fullJustify(String[] words, int maxWidth) {
        List<String> ans = new ArrayList<>();
        int n = words.length;
        List<String> list = new ArrayList<>();
        for (int i = 0; i < n; ) {
            // list 装载当前行的所有 word
            list.clear();
            list.add(words[i]);
            int cur = words[i++].length();
            while (i < n && cur + 1 + words[i].length() <= maxWidth) {
                cur += 1 + words[i].length();
                list.add(words[i++]);
            }

            // 当前行为最后一行,特殊处理为左对齐
            if (i == n) {
                StringBuilder sb = new StringBuilder(list.get(0));
                for (int k = 1; k < list.size(); k++) {
                    sb.append(" ").append(list.get(k));
                }
                while (sb.length() < maxWidth) sb.append(" ");
                ans.add(sb.toString());
                break;
            }

            // 如果当前行只有一个 word,特殊处理为左对齐
            int cnt = list.size();
            if (cnt == 1) {
                String str = list.get(0);
                while (str.length() != maxWidth) str += " ";
                ans.add(str);
                continue;
            }

            /**
            * 其余为一般情况
            * wordWidth : 当前行单词总长度;
            * spaceWidth : 当前行空格总长度;
            * spaceItem : 往下取整后的单位空格长度
            */
            int wordWidth = cur - (cnt - 1);
            int spaceWidth = maxWidth - wordWidth;
            int spaceItemWidth = spaceWidth / (cnt - 1);
            String spaceItem = "";
            for (int k = 0; k < spaceItemWidth; k++) spaceItem += " ";
            StringBuilder sb = new StringBuilder();
            for (int k = 0, sum = 0; k < cnt; k++) {
                String item = list.get(k);
                sb.append(item);
                if (k == cnt - 1) break;
                sb.append(spaceItem);
                sum += spaceItemWidth;
                // 剩余的间隙数量(可填入空格的次数)
                int remain = cnt - k - 1 - 1;
                // 剩余间隙数量 * 最小单位空格长度 + 当前空格长度 < 单词总长度,则在当前间隙多补充一个空格
                if (remain * spaceItemWidth + sum < spaceWidth) {
                    sb.append(" ");
                    sum++;
                }
            }
            ans.add(sb.toString());
        }
        return ans;
    }
}
```
* 时间复杂度:会对 $words$ 做线性扫描,最坏情况下每个 $words[i]$ 独占一行,此时所有字符串的长度为 $n * maxWidth$。复杂度为 $O(n * maxWidth)$
* 空间复杂度:最坏情况下每个 $words[i]$ 独占一行,复杂度为 $O(n * maxWidth)$

### 最后

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

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

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

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


611. 有效三角形的个数

Question:
给定一个包含非负整数的数组,你的任务是统计其中可以组成三角形三条边的三元组个数。

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

输出: 3

解释:
有效的组合是:
2,3,4 (使用第一个 2)
2,3,4 (使用第二个 2)
2,2,3
```
注意:
1. 数组长度不超过 $1000$。
2. 数组里整数的范围为 $[0, 1000]$。

Solution:
### 基本分析

**根据题意,是要我们统计所有符合 $nums[k] + nums[j] > nums[i]$ 条件的三元组 $(k,j,i)$ 的个数。**

为了防止统计重复的三元组,我们可以先对数组进行排序,然后采取「先枚举较大数;在下标不超过较大数下标范围内,找次大数;在下标不超过次大数下标范围内,找较小数」的策略。

---

### 排序 + 暴力枚举

根据「基本分析」,我们可以很容易写出「排序 + 三层循环」的实现。

代码:
```Java
class Solution {
    public int triangleNumber(int[] nums) {
        int n = nums.length;
        Arrays.sort(nums);
        int ans = 0;
        for (int i = 0; i < n; i++) {
            for (int j = i - 1; j >= 0; j--) {
                for (int k = j - 1; k >= 0; k--) {
                    if (nums[j] + nums[k] > nums[i]) ans++;
                }
            }
        }
        return ans;
    }
}
```
* 时间复杂度:排序时间复杂度为 $O(n\log{n})$;三层遍历找所有三元祖的复杂度为 $O(n^3)$。整体复杂度为 $O(n^3)$
* 空间复杂度:$O(\log{n})$

---

### 排序 + 二分

根据我们以前讲过的的 [优化枚举的基本思路](https://leetcode-cn.com/problems/number-of-submatrices-that-sum-to-target/solution/gong-shui-san-xie-you-hua-mei-ju-de-ji-b-uttw/),要找符合条件的三元组,其中一个切入点可以是「枚举三元组中的两个值,然后优化找第三数的逻辑」。

我们发现,在数组有序的前提下,**当枚举到较大数下标 $i$ 和次大数下标 $j$ 时,在 $[0, j)$ 范围内找的符合 $nums[k'] + nums[j] > nums[i]$ 条件的 $k'$ 的集合时,以符合条件的最小下标 $k$ 为分割点的数轴上具有「二段性」。**

令 $k$ 为符合条件的最小下标,那么在 $nums[i]$ 和 $nums[j]$ 固定时,$[0,j)$ 范围内:

* 下标大于等于 $k$ 的点集符合条件 $nums[k'] + nums[j] > nums[i]$;
* 下标小于 $k$ 的点集合不符合条件 $nums[k'] + nums[j] > nums[i]$。

因此我们可以通过「二分」找到这个分割点 $k$,在 $[k,j)$ 范围内即是固定 $j$ 和 $i$ 时,符合条件的 $k'$ 的个数。

代码:
```Java
class Solution {
    public int triangleNumber(int[] nums) {
        int n = nums.length;
        Arrays.sort(nums);
        int ans = 0;
        for (int i = 0; i < n; i++) {
            for (int j = i - 1; j >= 0; j--) {
                int l = 0, r = j - 1;
                while (l < r) {
                    int mid = l + r >> 1;
                    if (nums[mid] + nums[j] > nums[i]) r = mid;
                    else l = mid + 1;
                }
                if (l == r && nums[r] + nums[j] > nums[i]) ans += j - r;
            }
        }
        return ans;
    }
}
```
* 时间复杂度:排序时间复杂度为 $O(n\log{n})$;两层遍历加二分所有符合条件的三元组的复杂度为 $O(n^2*\log{n})$。整体复杂度为 $O(n^2*\log{n})$
* 空间复杂度:$O(\log{n})$

---

### 排序 + 双指针

更进一步我们发现,当我们在枚举较大数下标 $i$,并在 $[0, i)$ 范围内逐步减小下标(由于数组有序,也就是逐步减少值)找次大值下标 $j$ 时,符合条件的 $k'$ 必然是从 $0$ 逐步递增(这是由三角不等式 $nums[k] + nums[j] > nums[i]$ 所决定的)。

因此,我们可以枚举较大数下标 $i$ 时,在 $[0, i)$ 范围内通过双指针,以逐步减少下标的方式枚举 $j$,并在遇到不满足条件的 $k$ 时,增大 $k$ 下标。从而找到所有符合条件三元组的个数。

代码:
```Java
class Solution {
    public int triangleNumber(int[] nums) {
        int n = nums.length;
        Arrays.sort(nums);
        int ans = 0;
        for (int i = 0; i < n; i++) {
            for (int j = i - 1, k = 0; k < j; j--) {
                while (k < j && nums[k] + nums[j] <= nums[i]) k++;
                ans += j - k;
            }
        }
        return ans;
    }
}
```
* 时间复杂度:排序时间复杂度为 $O(n\log{n})$,双指针找所有符合条件的三元组的复杂度为 $O(n^2)$。整体复杂度为 $O(n^2)$
* 空间复杂度:$O(\log{n})$

### 最后

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

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

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

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


Question:
给你一个用字符数组 `tasks` 表示的 `CPU` 需要执行的任务列表,其中每个字母表示一种不同种类的任务。

任务可以以任意顺序执行,并且每个任务都可以在 `1` 个单位时间内执行完。

在任何一个单位时间,`CPU` 可以完成一个任务,或者处于待命状态。

然而,两个相同种类的任务之间必须有长度为整数 `n` 的冷却时间,因此至少有连续 `n` 个单位时间内 `CPU` 在执行不同的任务,或者在待命状态。

你需要计算完成所有任务所需要的最短时间。

示例 1:
```
输入:tasks = ["A","A","A","B","B","B"], n = 2

输出:8

解释:A -> B -> (待命) -> A -> B -> (待命) -> A -> B
     在本示例中,两个相同类型任务之间必须间隔长度为 n = 2 的冷却时间,而执行一个任务只需要一个单位时间,所以中间出现了(待命)状态。
```
示例 2:
```
输入:tasks = ["A","A","A","B","B","B"], n = 0

输出:6

解释:在这种情况下,任何大小为 6 的排列都可以满足要求,因为 n = 0
["A","A","A","B","B","B"]
["A","B","A","B","A","B"]
["B","B","B","A","A","A"]
...
诸如此类
```
示例 3:
```
输入:tasks = ["A","A","A","A","A","A","B","C","D","E","F","G"], n = 2

输出:16

解释:一种可能的解决方案是:
     A -> B -> C -> A -> D -> E -> A -> F -> G -> A -> (待命) -> (待命) -> A -> (待命) -> (待命) -> A
```

提示:
* $1 <= task.length <= 10^4$
* `tasks[i]` 是大写英文字母
* `n` 的取值范围为 $[0, 100]$

Solution:
### 构造

先考虑最为简单的情况:假设只有一类任务,除了最后一个任务以外,其余任务在安排后均需要增加 $n$ 个单位的冻结时间。

将任务数记为 $m$ 个,其中前 $m - 1$ 个任务均要消耗 $n + 1$ 的单位时间,最后一个任务仅消耗 $1$ 个单位时间,即所需要的时间为 $(n + 1) \times (m - 1) + 1$。

**当存在多个任务时,由于每一类任务都需要被完成,因此本质上我们最需要考虑的是将数量最大的任务安排掉,其他任务则是间插其中。**

假设数量最大的任务数为 `max`,共有 `tot` 个任务数为 `max` 的任务种类。

实际上,当任务总数不超过 $(n + 1) \times (\max - 1) + tot$ 时,我们总能将其他任务插到空闲时间中去,不会引入额外的冻结时间(下左图);而当任务数超过该值时,我们可以在将其横向添加每个 $n + 1$ 块的后面,同时不会引入额外的冻结时间(下右图):

综上,我们所需要的最小时间为上述两种情况中的较大值即可:

$$
\max(task.length, (n + 1) \times (max - 1) + tot)
$$

Java 代码:
```Java
class Solution {
    public int leastInterval(char[] tasks, int n) {
        int[] cnts = new int[26];
        for (char c : tasks) cnts[c - 'A']++;
        int max = 0, tot = 0;
        for (int i = 0; i < 26; i++) max = Math.max(max, cnts[i]);
        for (int i = 0; i < 26; i++) tot += max == cnts[i] ? 1 : 0;
        return Math.max(tasks.length, (n + 1) * (max - 1) + tot);
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    int leastInterval(vector<char>& tasks, int n) {
        vector<int> cnts(26, 0);
        for (char c : tasks) cnts[c - 'A']++;
        int maxv = *max_element(cnts.begin(), cnts.end());
        int tot = count(cnts.begin(), cnts.end(), maxv);
        return max(static_cast<int>(tasks.size()), (n + 1) * (maxv - 1) + tot);
    }
};
```
Python 代码:
```Python
class Solution:
    def leastInterval(self, tasks: List[str], n: int) -> int:
        cnts = [0] * 26
        for c in tasks:
            cnts[ord(c) - ord('A')] += 1
        maxv, tot = 0, 0
        for i in range(26):
            maxv = max(maxv, cnts[i])
        for i in range(26):
            tot += 1 if maxv == cnts[i] else 0
        return max(len(tasks), (n + 1) * (maxv - 1) + tot)
```
TypeScript 代码:
```TypeScript
function leastInterval(tasks: string[], n: number): number {
    const cnts = new Array<number>(26).fill(0)
    for (const c of tasks) cnts[c.charCodeAt(0) - 'A'.charCodeAt(0)]++
    let max = 0, tot = 0
    for (let i = 0; i < 26; i++) max = Math.max(max, cnts[i])
    for (let i = 0; i < 26; i++) tot += max == cnts[i] ? 1 : 0
    return Math.max(tasks.length, (n + 1) * (max - 1) + tot)
}
```
* 时间复杂度:$O(n + C)$
* 空间复杂度:$O(C)$,其中 $C = 26$ 为任务字符集大小

### 最后

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

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

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

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


Question:
设计你的循环队列实现。 循环队列是一种线性数据结构,其操作表现基于 `FIFO`(先进先出)原则并且队尾被连接在队首之后以形成一个循环。它也被称为“环形缓冲器”。

循环队列的一个好处是我们可以利用这个队列之前用过的空间。在一个普通队列里,一旦一个队列满了,我们就不能插入下一个元素,即使在队列前面仍有空间。但是使用循环队列,我们能使用这些空间去存储新的值。

你的实现应该支持如下操作:

* `MyCircularQueue(k)`: 构造器,设置队列长度为 $k$ 。
* `Front`: 从队首获取元素。如果队列为空,返回 $-1$ 。
* `Rear`: 获取队尾元素。如果队列为空,返回 $-1$ 。
* `enQueue(value)`: 向循环队列插入一个元素。如果成功插入则返回真。
* `deQueue()`: 从循环队列中删除一个元素。如果成功删除则返回真。
* `isEmpty()`: 检查循环队列是否为空。
* `isFull()`: 检查循环队列是否已满。

示例:
```
MyCircularQueue circularQueue = new MyCircularQueue(3); // 设置长度为 3
circularQueue.enQueue(1);  // 返回 true
circularQueue.enQueue(2);  // 返回 true
circularQueue.enQueue(3);  // 返回 true
circularQueue.enQueue(4);  // 返回 false,队列已满
circularQueue.Rear();  // 返回 3
circularQueue.isFull();  // 返回 true
circularQueue.deQueue();  // 返回 true
circularQueue.enQueue(4);  // 返回 true
circularQueue.Rear();  // 返回 4
```

提示:
* 所有的值都在 $0$ 至 $1000$ 的范围内;
* 操作数将在 $1$ 至 $1000$ 的范围内;
* 请不要使用内置的队列库。

Solution:
### 数据结构

创建一个长度为 $k$ 的数组充当循环队列,使用两个变量 `he` 和 `ta` 来充当队列头和队列尾(起始均为 $0$),整个过程 `he` 始终指向队列头部,`ta` 始终指向队列尾部的下一位置(待插入元素位置)。

两变量始终自增,通过与 $k$ 取模来确定实际位置。

分析各类操作的基本逻辑:
* `isEmpty` 操作:当 `he` 和 `ta` 相等,队列存入元素和取出元素的次数相同,此时队列为空;
* `isFull` 操作:`ta - he` 即队列元素个数,当元素个数为 $k$ 个时,队列已满;
* `enQueue` 操作:若队列已满,返回 $-1$,否则在 `nums[ta % k]` 位置存入目标值,并将 `ta` 指针后移;
* `deQueue` 操作:若队列为空,返回 $-1$,否则将 `he` 指针后移,含义为弹出队列头部元素;
* `Front` 操作:若队列为空,返回 $-1$,否则返回 `nums[he % k]` 队头元素;
* `Rear` 操作:若队列为空,返回 $-1$,否则返回 `nums[(ta - 1) % k]` 队尾元素;

Java 代码:
```Java
class MyCircularQueue {
    int k, he, ta;
    int[] nums;
    public MyCircularQueue(int _k) {
        k = _k;
        nums = new int[k];
    }
    public boolean enQueue(int value) {
        if (isFull()) return false;
        nums[ta % k] = value;
        return ++ta >= 0;
    }
    public boolean deQueue() {
        if (isEmpty()) return false;
        return ++he >= 0;
    }
    public int Front() {
        return isEmpty() ? -1 : nums[he % k];
    }
    public int Rear() {
        return isEmpty() ? -1 : nums[(ta - 1) % k];
    }
    public boolean isEmpty() {
        return he == ta;
    }
    public boolean isFull() {
        return ta - he == k;
    }
}
```
TypeScript 代码:
```TypeScript
class MyCircularQueue {
    k: number = 0; he: number = 0; ta: number = 0;
    nums: number[];
    constructor(k: number) {
        this.k = k
        this.nums = new Array<number>(this.k)
    }
    enQueue(value: number): boolean {
        if (this.isFull()) return false
        this.nums[this.ta % this.k] = value
        return this.ta++ >= 0
    }
    deQueue(): boolean {
        if (this.isEmpty()) return false
        return this.he++ >= 0
    }
    Front(): number {
        return this.isEmpty() ? -1 : this.nums[this.he % this.k]
    }
    Rear(): number {
        return this.isEmpty() ? -1 : this.nums[(this.ta - 1) % this.k]
    }
    isEmpty(): boolean {
        return this.he == this.ta
    }
    isFull(): boolean {
        return this.ta - this.he == this.k
    }
}
```
* 时间复杂度:构造函数复杂度为 $O(k)$,其余操作复杂度为 $O(1)$
* 空间复杂度:$O(k)$

### 最后

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

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

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

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


623. 在二叉树中增加一行

Question:
给定一个二叉树的根 `root` 和两个整数 `val` 和 `depth`,在给定的深度 `depth` 处添加一个值为 `val` 的节点行。

注意,根节点 `root` 位于深度 $1$ 。

加法规则如下:

* 给定整数 `depth`,对于深度为 `depth - 1` 的每个非空树节点 `cur`,创建两个值为 `val` 的树节点作为 `cur` 的左子树根和右子树根。
* `cur` 原来的左子树应该是新的左子树根的左子树。
* `cur` 原来的右子树应该是新的右子树根的右子树。
* 如果 `depth == 1` 意味着 `depth - 1` 根本没有深度,那么创建一个树节点,值 `val` 作为整个原始树的新根,而原始树就是新根的左子树。

示例 1:

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

输出: [4,1,1,2,null,null,6,3,1,5]
```
示例 2:

```
输入: root = [4,2,null,3,1], val = 1, depth = 3

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

提示:
* 节点数在 $[1, 10^4]$ 范围内
* 树的深度在 $[1, 10^4]$范围内
* $-100 <= Node.val <= 100$
* $-10^5 <= val <= 10^5$
* $1 <= depth <= the depth of tree + 1$

Solution:
### BFS

根据 `BFS` 来做,每次 `BFS` 将整一层进行拓展,同时记录当前深度,当到达第 `depth - 1` 层,则进行加点操作。

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

---

### DFS

同理,使用 `DFS` 也可进行求解,在 `DFS` 过程中记录当前深度。

Java 代码:
```Java
class Solution {
    int d, v;
    public TreeNode addOneRow(TreeNode root, int val, int depth) {
        d = depth; v = val;
        if (d == 1) return new TreeNode(val, root, null);
        dfs(root, 1);
        return root;
    }
    void dfs(TreeNode root, int cur) {
        if (root == null) return ;
        if (cur == d - 1) {
            TreeNode a = new TreeNode(v), b = new TreeNode(v);
            a.left = root.left; b.right = root.right;
            root.left = a; root.right = b;
        } else {
            dfs(root.left, cur + 1);
            dfs(root.right, cur + 1);
        }
    }
}
```
TypeScript 代码:
```TypeScript
let d = 0, v = 0
function addOneRow(root: TreeNode | null, val: number, depth: number): TreeNode | null {
    d = depth; v = val
    if (d == 1) return new TreeNode(v, root, null);
    dfs(root, 1)
    return root
};
function dfs(root: TreeNode | null, cur: number): void {
    if (root == null) return
    if (cur == d - 1) {
        const a = new TreeNode(v), b = new TreeNode(v)
        a.left = root.left; b.right = root.right
        root.left = a; root.right = b
    } else {
        dfs(root.left, cur + 1)
        dfs(root.right, cur + 1)
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


629. K个逆序对数组

Question:
给出两个整数 `n` 和 `k`,找出所有包含从 `1` 到 `n` 的数字,且恰好拥有 `k` 个逆序对的不同的数组的个数。

逆序对的定义如下:对于数组的第 `i` 个和第 `j` 个元素,如果满 `i < j` 且 `a[i] > a[j]`,则其为一个逆序对;否则不是。

由于答案可能很大,只需要返回 答案 mod $10^9 + 7$ 的值。

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

输出: 1

解释:
只有数组 [1,2,3] 包含了从1到3的整数并且正好拥有 0 个逆序对。
```
示例 2:
```
输入: n = 3, k = 1

输出: 2

解释:
数组 [1,3,2] 和 [2,1,3] 都有 1 个逆序对。
```

说明:
*  `n` 的范围是 `[1, 1000]` 并且 `k` 的范围是 `[0, 1000]`。

Solution:
### 序列 DP

从 $n$ 和 $k$ 数据范围均为 $10^3$ 可以看出这是一道二维的动态规划题。

**定义 $f[i][j]$ 为考虑使用数值 $[1,i]$,凑成逆序对数量恰好为 $j$ 的数组个数。**

不失一般性的考虑 $f[i][j]$ 该如何计算,对第 $i$ 个数(即数值为 $i$ 的数)所在位置进行讨论,共有 $i$ 种选择。

假设第 $i$ 个数所在位置为 $k$,由于数值 $i$ 为整个数组的最大值,因此数值 $i$ 与前面所有数均不形成逆序对,与后面的所有数均形成逆序对。因此与数值 $i$ 直接相关的逆向对的数量为 $(i - 1)- k$,由此也得出与 $i$ 不相关的逆序对数量为 $j - (i - 1 - k)$,而与 $i$ 不相关的逆序对数量由 $f[i - 1][x]$ 可得出。

举个 🌰 帮助大家理解:

* 当数值 $i$ 放置在下标为 $0$ 的位置上,那么由数值 $i$ 产生的逆序对数量为 $i - 1$,总的逆序对数量为 $j$,因此由数值范围为 $[1, i - 1]$(与数值 $i$ 不相关)构成的逆序对数量为 $j - (i - 1)$,即 $f[i - 1][j - (i - 1)]$;

* 当数值 $i$ 放置在下标为 $1$ 的位置上,那么由数值 $i$ 产生的逆序对数量为 $(i - 1) - 1$,总的逆序对数量为 $j$,因此由数值范围为 $[1, i - 1]$(与数值 $i$ 不相关)构成的逆序对数量为 $j - (i - 1 - 1)$,即 $f[i - 1][j - (i - 1 - 1)]$;

    ...

* 当数值 $i$ 放置在下标为 $k$ 的位置上,那么由数值 $i$ 产生的逆序对数量为 $(i - 1) - k$,总的逆序对数量为 $j$,因此由数值范围为 $[1, i - 1]$(与数值 $i$ 不相关)构成的逆序对数量为 $j - (i - 1 - k)$,即 $f[i - 1][j - (i - 1 - k)]$。

综上,最终 $f[i][j]$ 转移方程为($k$ 为数值 $i$ 放置的位置):

$$
f[i][j] = \sum_{k = 0}^{i - 1}(f[i - 1][j - (i - 1 - k)])
$$

共有 $n * k$ 个状态,每个 $f[i][j]$ 的计算需要枚举数值 $i$ 所在位置并进行累加,总的复杂度为 $O(n^2 *k)$,计算量为 $10^9$,会 TLE。

状态数量不可减少,考虑如何优化单个状态的转移过程。

不难发现 $\sum_{k = 0}^{i - 1}(f[i - 1][j - (i - 1 - k)])$ 部分为上一次转移结果 $f[i - 1][x]$ 的某个前缀,可以使用前缀和数组进行优化,从而将计算单个状态的复杂度从 $O(n)$ 降到 $O(1)$。

> 一些细节:为处理负数问题,我们可以在取模之前先加一次 mod;另外需要对 $j$ 和 $i$ 的大小进行分情况讨论,防止数值 $i$ 放置的位置“过于靠前”导致组成逆序对的数量超过 $j$。

代码($P1$ $P2$ 分别为使用 `long` 和不使用 `long`):
```Java
class Solution {
    int mod = (int)1e9+7;
    public int kInversePairs(int n, int k) {
        long[][] f = new long[n + 1][k + 1];
        long[][] sum = new long[n + 1][k + 1];
        f[1][0] = 1;
        Arrays.fill(sum[1], 1);
        for (int i = 2; i <= n; i++) {
            for (int j = 0; j <= k; j++) {
                f[i][j] = j < i ? sum[i - 1][j] : sum[i - 1][j] - sum[i - 1][j - (i - 1) - 1];
                f[i][j] = (f[i][j] + mod) % mod;
                sum[i][j] = j == 0 ? f[i][j] : sum[i][j - 1] + f[i][j];
                sum[i][j] = (sum[i][j] + mod) % mod;
            }
        }
        return (int)f[n][k];
    }
}
```

```Java
class Solution {
    int mod = (int)1e9+7;
    public int kInversePairs(int n, int k) {
        int[][] f = new int[n + 1][k + 1];
        int[][] sum = new int[n + 1][k + 1];
        f[1][0] = 1;
        Arrays.fill(sum[1], 1);
        for (int i = 2; i <= n; i++) {
            for (int j = 0; j <= k; j++) {
                f[i][j] = j < i ? sum[i - 1][j] : (sum[i - 1][j] - sum[i - 1][j - (i - 1) - 1] + mod) % mod;
                sum[i][j] = j == 0 ? f[i][j] : (sum[i][j - 1] + f[i][j]) % mod;
            }
        }
        return f[n][k];
    }
}
```
* 时间复杂度:$O(n \times k)$
* 空间复杂度:$O(n \times k)$

### 最后

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

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

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

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