Question:
编写一个 `StockSpanner` 类,它收集某些股票的每日报价,并返回该股票当日价格的跨度。

今天股票价格的跨度被定义为股票价格小于或等于今天价格的最大连续日数(从今天开始往回数,包括今天)。

例如,如果未来 `7` 天股票的价格是 `[100, 80, 60, 70, 60, 75, 85]`,那么股票跨度将是 `[1, 1, 1, 2, 1, 4, 6]`。

示例:
```
输入:["StockSpanner","next","next","next","next","next","next","next"], [[],[100],[80],[60],[70],[60],[75],[85]]

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

解释:
首先,初始化 S = StockSpanner(),然后:
S.next(100) 被调用并返回 1,
S.next(80) 被调用并返回 1,
S.next(60) 被调用并返回 1,
S.next(70) 被调用并返回 2,
S.next(60) 被调用并返回 1,
S.next(75) 被调用并返回 4,
S.next(85) 被调用并返回 6。

注意 (例如) S.next(75) 返回 4,因为截至今天的最后 4 个价格
(包括今天的价格 75) 小于或等于今天的价格。
```

提示:
* 调用 `StockSpanner.next(int price)` 时,将有 $1 <= price <= 10^5$。
* 每个测试用例最多可以调用  `10000` 次 `StockSpanner.next`。
* 在所有测试用例中,最多调用 `150000` 次 `StockSpanner.next`。
* 此问题的总时间限制减少了 `50%`。

Solution:
### 分块

又名优雅的暴力。

这是一道在线问题,在调用 `next` 往数据流存入元素的同时,返回连续段不大于当前元素的数的个数。

一个朴素的想法是:使用数组 `nums` 将所有 `price` 进行存储,每次返回时往前找到第一个不满足要求的位置,并返回连续段的长度。

但对于 $10^4$ 的调用次数来看,该做法的复杂度为 $O(n^2)$,计算量为 $10^8$,不满足 `OJ` 要求。

实际上我们可以利用「分块」思路对其进行优化,将与连续段的比较转换为与最值的比较。

具体的,我们仍然使用 `nums` 对所有的 `price` 进行存储,同时使用 `region` 数组来存储每个连续段的最大值,其中 $region[loc] = x$ 含义为块编号为 `loc` 的最大值为 `x`,其中块编号 `loc` 块对应了数据编号 `idx` 的范围 $[(loc - 1) \times len + 1, loc \times len]$。

对于 `next` 操作而言,除了直接更新数据数组 `nums[++idx] = price` 以外,我们还需要更新 `idx` 所在块的最值 `region[loc]`,然后从当前块 `loc` 开始往前扫描其余块,使用 `left` 和 `right` 代指当前处理到的块的左右端点,若当前块满足 `region[loc] <= price`,说明块内所有元素均满足要求,直接将当前块 `loc` 所包含元素个数累加到答案中,直到遇到不满足的块或达到块数组边界,若存在遇到不满足要求的块,再使用 `right` 和 `left` 统计块内满足要求 `nums[i] <= price` 的个数。

对于块个数和大小的设定,是运用分块降低复杂度的关键,数的个数为 $10^4$,我们可以设定块大小为 $\sqrt{n} = 100$,这样也限定了块的个数为 $\sqrt{n} = 100$ 个。这样对于单次操作而言,我们最多遍历进行 $\sqrt{n}$ 次的块间操作,同时最多进行一次块内操作,整体复杂度为 $O(\sqrt{n})$,单次 `next` 操作计算量为 $2 \times 10^2$ 以内,单样例计算量为 $2 \times 10^6$,可以过。

为了方便,我们令块编号 `loc` 和数据编号 `idx` 均从 $1$ 开始;同时为了防止每个样例都 `new` 大数组,我们采用 `static` 优化,并在 `StockSpanner` 的初始化中做重置工作。

Java 代码:
```Java
class StockSpanner {
    static int N = 10010, len = 100, idx = 0;
    static int[] nums = new int[N], region = new int[N / len + 10];
    public StockSpanner() {
        for (int i = 0; i <= getIdx(idx); i++) region[i] = 0;
        idx = 0;
    }
    int getIdx(int x) {
        return (x - 1) / len + 1;
    }
    int query(int price) {
        int ans = 0, loc = getIdx(idx), left = (loc - 1) * len + 1, right = idx;
        while (loc >= 1 && region[loc] <= price) {
            ans += right - left + 1;
            loc--; right = left - 1; left = (loc - 1) * len + 1;
        }
        for (int i = right; loc >= 1 && i >= left && nums[i] <= price; i--) ans++;
        return ans;
    }
    public int next(int price) {
        nums[++idx] = price;
        int loc = getIdx(idx);
        region[loc] = Math.max(region[loc], price);
        return query(price);
    }
}
```
TypeScript 代码:
```TypeScript
class StockSpanner {
    N: number = 10010; sz: number = 100; idx: number = 0
    nums: number[] = new Array<number>(this.N).fill(0);
    region = new Array<number>(Math.floor(this.N / this.sz) + 10).fill(0)
    getIdx(x: number): number {
        return Math.floor((x - 1) / this.sz) + 1
    }
    query(price: number): number {
        let ans = 0, loc = this.getIdx(this.idx), left = (loc - 1) * this.sz + 1, right = this.idx
        while (loc >= 1 && this.region[loc] <= price) {
            ans += right - left + 1
            loc--; right = left - 1; left = (loc - 1) * this.sz + 1
        }
        for (let i = right; loc >= 1 && i >= left && this.nums[i] <= price; i--) ans++
        return ans
    }
    next(price: number): number {
        this.nums[++this.idx] = price
        const loc = this.getIdx(this.idx)
        this.region[loc] = Math.max(this.region[loc], price)
        return this.query(price)
    }
}
```
Python3 代码:
```Python
class StockSpanner:
    def __init__(self):
        self.N, self.sz, self.idx = 10010, 110, 0
        self.nums, self.region = [0] * self.N, [0] * (self.N // self.sz + 10)

    def next(self, price: int) -> int:
        def getIdx(x):
            return (x - 1) // self.sz + 1

        def query(price):
            ans, loc = 0, getIdx(self.idx)
            left, right = (loc - 1) * self.sz + 1, self.idx
            while loc >= 1 and self.region[loc] <= price:
                ans += right - left + 1
                loc -= 1
                right, left = left - 1, (loc - 1) * self.sz + 1
            while loc >= 1 and right >= left and self.nums[right] <= price:
                right, ans = right - 1, ans + 1
            return ans

        self.idx += 1
        loc = getIdx(self.idx)
        self.nums[self.idx] = price
        self.region[loc] = max(self.region[loc], price)
        return query(price)
```
* 时间复杂度:由于使用了 `static` 优化,`StockSpanner` 初始化时,需要对上一次使用的块进行重置,复杂度为 $O(\sqrt{n})$;由于块大小和数量均为 $\sqrt{n}$,`next` 操作复杂度为 $O(\sqrt{n})$
* 空间复杂度:$O(n)$

---

### 单调栈

另外一个容易想到的想法是使用「单调栈」,栈内以二元组 $(idx, price)$ 形式维护比当前元素 `price` 大的元素。

每次执行 `next` 操作时,从栈顶开始处理,将所有满足「不大于 `price`」的元素进行出栈,从而找到当前元素 `price` 左边最近一个比其大的位置。

Java 代码:
```Java
class StockSpanner {
    Deque<int[]> d = new ArrayDeque<>();
    int cur = 0;
    public int next(int price) {
        while (!d.isEmpty() && d.peekLast()[1] <= price) d.pollLast();
        int prev = d.isEmpty() ? -1 : d.peekLast()[0], ans = cur - prev;
        d.addLast(new int[]{cur++, price});
        return ans;
    }
}
```
TypeScript 代码:
```TypeScript
class StockSpanner {
    stk = new Array<Array<number>>(10010).fill([0, 0])
    he = 0; ta = 0; cur = 0
    next(price: number): number {
        while (this.he < this.ta && this.stk[this.ta - 1][1] <= price) this.ta--
        const prev = this.he >= this.ta ? -1 : this.stk[this.ta - 1][0], ans = this.cur - prev
        this.stk[this.ta++] = [this.cur++, price]
        return ans
    }
}
```
Python3 代码:
```Python
class StockSpanner:
    def __init__(self):
        self.stk = []
        self.cur = 0

    def next(self, price: int) -> int:
        while self.stk and self.stk[-1][1] <= price:
            self.stk.pop()
        prev = -1 if not self.stk else self.stk[-1][0]
        ans = self.cur - prev
        self.stk.append([self.cur, price])
        self.cur += 1
        return ans
```
* 时间复杂度:`next` 操作的均摊复杂度为 $O(1)$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


LeetCode/901-910/902. 最大为 N 的数字组合（困难）.md

902. 最大为 N 的数字组合

Question:
给定一个按 非递减顺序 排列的数字数组 `digits`。你可以用任意次数 $digits[i]$ 来写的数字。例如,如果 $digits = [1,3,5]$,我们可以写数字,如 `'13'`, `'551'`, 和 `'1351315'`。

返回 **可以生成的小于或等于给定整数** $n$ 的正整数的个数 。

示例 1:
```
输入:digits = ["1","3","5","7"], n = 100

输出:20

解释:
可写出的 20 个数字是:
1, 3, 5, 7, 11, 13, 15, 17, 31, 33, 35, 37, 51, 53, 55, 57, 71, 73, 75, 77.
```
示例 2:
```
输入:digits = ["1","4","9"], n = 1000000000

输出:29523

解释:
我们可以写 3 个一位数字,9 个两位数字,27 个三位数字,
81 个四位数字,243 个五位数字,729 个六位数字,
2187 个七位数字,6561 个八位数字和 19683 个九位数字。
总共,可以使用D中的数字写出 29523 个整数。
```
示例 3:
```
输入:digits = ["7"], n = 8

输出:1
```

提示:
* $1 <= digits.length <= 9$
* $digits[i].length == 1$
* $digits[i]$ 是从 `'1'` 到 `'9'` 的数
* `digits` 中的所有值都 不同
* `digits` 按 非递减顺序 排列
* $1 <= n <= 10^9$

Solution:
### 数位 DP + 二分

这是一道「数位 DP」的经典运用题。

由于题目给定的 `digits` 不包含 $0$,因此相当于只需要回答使用 `digits` 的数值能够覆盖 $[1, x]$ 范围内的多少个数字。

起始先将字符串数组 `digits` 转为数字数组 `nums`,假定 `nums` 的长度为 $m$,然后考虑如何求得 $[1, x]$ 范围内合法数字的个数。

假定我们存在函数 `int dp(int x)` 函数,能够返回区间 $[1, x]$ 内合法数的个数,那么配合「容斥原理」我们便能够回答任意区间合法数的查询:
$$
ans_{(l, r)} = dp(r) - dp(l - 1)
$$
对于本题,查询区间的左端点固定为 $1$,同时 $dp(0) = 0$,因此答案为 $dp(x)$。

然后考虑如何实现 `int dp(int x)` 函数,我们将组成 $[1, x]$ 的合法数分成三类:
* 位数和 $x$ 相同,且最高位比 $x$ 最高位要小的,这部分统计为 `res1`;
* 位数和 $x$ 相同,且最高位与 $x$ 最高位相同的,这部分统计为 `res2`;
* 位数比 $x$ 少,这部分统计为 `res3`。

其中 `res1` 和 `res3` 求解相对简单,重点落在如何求解 `res2` 上。

**对 $x$ 进行「从高到低」的处理(假定 $x$ 数位为 $n$),对于第 $k$ 位而言($k$ 不为最高位),假设在 $x$ 中第 $k$ 位为 $cur$,那么为了满足「大小限制」关系,我们只能在 $[1, cur - 1]$ 范围内取数,同时为了满足「数字只能取自 `nums`」的限制,因此我们可以利用 `nums` 本身有序,对其进行二分,找到满足 `nums[mid] <= cur` 的最大下标 $r$,根据 $nums[r]$ 与 $cur$ 的关系进行分情况讨论:**

* $nums[r] = cur$: 此时位置 $k$ 共有 $r$ 种选择,而后面的每个位置由于 $nums[i]$ 可以使用多次,每个位置都有 $m$ 种选择,共有 $n - p$ 个位置,因此该分支往后共有 $r * m^{n - p}$ 种合法方案。且由于 $nums[r] = cur$,往后还有分支可决策(需要统计),因此需要继续处理;
* $nums[r] < cur$:此时算上 $nums[r]$,位置 $k$ 共有 $r + 1$ 种选择,而后面的每个位置由于 $nums[i]$ 可以使用多次,每个位置都有 $m$ 种选择,共有 $n - p$ 个位置,因此该分支共有 $(r + 1) * m^{n - p}$ 种合法方案,由于 $nums[r] < cur$,往后的方案数(均满足小于关系)已经在这次被统计完成,累加后进行 `break`;
* $nums[r] > cur$:该分支往后不再满足「大小限制」要求,合法方案数为 $0$,直接 `break`。

其他细节:实际上,我们可以将 `res1` 和 `res2` 两种情况进行合并处理。

代码:

```Java
class Solution {
    int[] nums;
    int dp(int x) {
        List<Integer> list = new ArrayList<>();
        while (x != 0) {
            list.add(x % 10);
            x /= 10;
        }
        int n = list.size(), m = nums.length, ans = 0;
        // 位数和 x 相同
        for (int i = n - 1, p = 1; i >= 0; i--, p++) {
            int cur = list.get(i);
            int l = 0, r = m - 1;
            while (l < r) {
                int mid = l + r + 1 >> 1;
                if (nums[mid] <= cur) l = mid;
                else r = mid - 1;
            }
            if (nums[r] > cur) {
                break;
            } else if (nums[r] == cur) {
                ans += r * (int) Math.pow(m, (n - p));
                if (i == 0) ans++;
            } else if (nums[r] < cur) {
                ans += (r + 1) * (int) Math.pow(m, (n - p));
                break;
            }
        }
        // 位数比 x 少的
        for (int i = 1, last = 1; i < n; i++) {
            int cur = last * m;
            ans += cur; last = cur;
        }
        return ans;
    }
    public int atMostNGivenDigitSet(String[] digits, int max) {
        int n = digits.length;
        nums = new int[n];
        for (int i = 0; i < n; i++) nums[i] = Integer.parseInt(digits[i]);
        return dp(max);
    }
}
```
* 时间复杂度:由于 `digits` 最多存在 $9$ 个元素,因此二分的复杂度可以忽略,整体复杂度为 $O(\log{n})$
* 空间复杂度:$O(C)$

---

### 总结

数位 DP 的难度取决于「限制条件」的多少,而 LC 上仅有的几道数位 DP 题目限制条件都很少,且不需要引入额外的数据结构来记录状态,因此都属于数位 DP 的入门难度(LC 难度均为 Hard)。

几乎所有的数位 DP 问题都可以归纳到上述的解法 :「将问题抽象为求解一个 $[0, x]$ / $[1, x]$ 范围方案数的方法」->「对方案数统计根据 位数 来分情况讨论:数位相等的情况 + 数位不等情况」->「统计数位相等的方案数时,需要按位处理,并根据限制条件做逻辑;统计数位不等的方案数时,通常要做一些预处理,然后配合乘法原理直接算得」。

在还没卷到数位 DP 烂大街的现在,掌握此类求解方式单一,普遍定位为「困难」的数位 DP 类型,还是极具性价比的。

### 最后

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

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

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

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


Question:
你正在探访一家农场,农场从左到右种植了一排果树。

这些树用一个整数数组 `fruits` 表示,其中 `fruits[i]` 是第 `i` 棵树上的水果种类。

你想要尽可能多地收集水果。然而,农场的主人设定了一些严格的规矩,你必须按照要求采摘水果:

* 你只有两个篮子,并且每个篮子只能装单一类型 的水果。每个篮子能够装的水果总量没有限制。
* 你可以选择任意一棵树开始采摘,你必须从每棵树(包括开始采摘的树)上恰好摘一个水果。采摘的水果应当符合篮子中的水果类型。每采摘一次,你将会向右移动到下一棵树,并继续采摘。
* 一旦你走到某棵树前,但水果不符合篮子的水果类型,那么就必须停止采摘。

给你一个整数数组 `fruits`,返回你可以收集的水果的最大数目。

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

输出:3

解释:可以采摘全部 3 棵树。
```
示例 2:
```
输入:fruits = [0,1,2,2]

输出:3

解释:可以采摘 [1,2,2] 这三棵树。
如果从第一棵树开始采摘,则只能采摘 [0,1] 这两棵树。
```
示例 3:
```
输入:fruits = [1,2,3,2,2]

输出:4

解释:可以采摘 [2,3,2,2] 这四棵树。
如果从第一棵树开始采摘,则只能采摘 [1,2] 这两棵树。
```
示例 4:
```
输入:fruits = [3,3,3,1,2,1,1,2,3,3,4]

输出:5

解释:可以采摘 [1,2,1,1,2] 这五棵树。
```

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

Solution:
### 滑动窗口

本题至少一半的难度在于理解题意。

坏消息是,像此类描述不够清晰的算法题,并不在少数。

好消息是,此类题目,通常都有对应的解决方案。就是如果读一遍题面,不能理解题意的时候,切勿来回阅读题面,而是从示例数据进行入手进行理解。

通过对先看「示例」再阅读「题面」的理解方式,整理出基本题意:从任意位置开始,**同时使用**两个篮子采集,一旦选择后不能修改篮子所装的水果种类,当所有树处理完或遇到第一棵种类不同的树则停止。

核心求解思路为滑动窗口:使用 `j` 和 `i` 分别代表滑动窗口的两端,窗口种类不超过 $2$ 种为合法。

Java 代码:
```Java
class Solution {
    public int totalFruit(int[] fruits) {
        int n = fruits.length, ans = 0;
        int[] cnts = new int[n + 10];
        for (int i = 0, j = 0, tot = 0; i < n; i++) {
            if (++cnts[fruits[i]] == 1) tot++;
            while (tot > 2) {
                if (--cnts[fruits[j++]] == 0) tot--;
            }
            ans = Math.max(ans, i - j + 1);
        }
        return ans;
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    int totalFruit(vector<int>& fruits) {
        int n = fruits.size(), ans = 0;
        unordered_map<int, int> cnts;
        for (int i = 0, j = 0, tot = 0; i < n; i++) {
            if (++cnts[fruits[i]] == 1) tot++;
            while (tot > 2) {
                if (--cnts[fruits[j++]] == 0) tot--;
            }
            ans = max(ans, i - j + 1);
        }
        return ans;
    }
};
```
Python 代码:
```Python
class Solution:
    def totalFruit(self, fruits: List[int]) -> int:
        n, ans = len(fruits), 0
        j, tot = 0, 0
        cnts = defaultdict(int)
        for i in range(n):
            cnts[fruits[i]] += 1
            if cnts[fruits[i]] == 1:
                tot += 1
            while tot > 2:
                cnts[fruits[j]] -= 1
                if cnts[fruits[j]] == 0:
                    tot -= 1
                j += 1
            ans = max(ans, i - j + 1)
        return ans
```
TypeScript 代码:
```TypeScript
function totalFruit(fruits: number[]): number {
    let n = fruits.length, ans = 0
    const cnts = new Array<number>(n + 10).fill(0)
    for (let i = 0, j = 0, tot = 0; i < n; i++) {
        if (++cnts[fruits[i]] == 1) tot++
        while (tot > 2) {
            if (--cnts[fruits[j++]] == 0) tot--
        }
        ans = Math.max(ans, i - j + 1)
    }
    return ans
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


905. 按奇偶排序数组

Question:
给你一个整数数组 `nums`,将 `nums` 中的的所有偶数元素移动到数组的前面,后跟所有奇数元素。

返回满足此条件的 任一数组 作为答案。

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

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

解释:[4,2,3,1]、[2,4,1,3] 和 [4,2,1,3] 也会被视作正确答案。
```
示例 2:
```
输入:nums = [0]

输出:[0]
```

提示:
* $1 <= nums.length <= 5000$
* $0 <= nums[i] <= 5000$

Solution:
### 模拟

根据题意进行模拟即可:使用指针 $i$ 和 $j$ 分别代表未处理区间的左右端点,当 $nums[i]$ 不为偶数时,将 $i$ 和 $j$ 两个位置互换,原有位置 $j$ 必然是奇数(已处理好),让 $j$ 自减左移,但原有位置 $i$ 交换后不确保是偶数,需要再次检查。

代码:
```Java
class Solution {
    public int[] sortArrayByParity(int[] nums) {
        int n = nums.length;
        for (int i = 0, j = n - 1; i < j; i++) {
            if (nums[i] % 2 == 1) {
                int c = nums[j];
                nums[j--] = nums[i];
                nums[i--] = c;
            }
        }
        return nums;
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


907. 子数组的最小值之和

Question:
给定一个整数数组 `arr`,找到 `min(b)` 的总和,其中 `b` 的范围为 `arr` 的每个(连续)子数组。

由于答案可能很大,因此 返回答案模 $10^9 + 7$ 。

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

输出:17

解释:
子数组为 [3],[1],[2],[4],[3,1],[1,2],[2,4],[3,1,2],[1,2,4],[3,1,2,4]。
最小值为 3,1,2,4,1,1,2,1,1,1,和为 17。
```
示例 2:
```
输入:arr = [11,81,94,43,3]

输出:444
```

提示:
* $1 <= arr.length <= 3 \times 10^4$
* $1 <= arr[i] <= 3 \times 10^4$

Solution:
### 单调栈 + 数学

原问题为求所有子数组的最小值之和。

统计所有子数组需要枚举左右端点,复杂度为 $O(n^2)$,对于每个子数组,我们还需要通过线性扫描的方式找到其最小值,复杂度为 $O(n)$,因此朴素解法的整体复杂度为 $O(n^3)$,题目给定数据范围为 $3 \times 10^4$,会 `TLE`。

由于我们是从子数组中取最小值来进行累加,即参与答案构成的每个数必然某个具体的 $arr[i]$。

**因此我们可以将原问题转化为「考虑统计每个 $arr[i]$ 对答案的贡献」。**

对于某一个 $arr[i]$ 而言,我们考虑其能够作为哪些子数组的最小值。

我们可以想象以 $arr[i]$ 为中心,分别往两端进行拓展,只要新拓展的边界不会改变「$arr[i]$ 为当前子数组的最小值」的性质即可。

换句话说,我们需要找到 $arr[i]$ 作为最小值的最远左右边界,即找到 $arr[i]$ 左右最近一个比其小的位置 `l` 和 `r`。

**在给定序列中,找到任意 $A[i]$ 最近一个比其大/小的位置,可使用「单调栈」进行求解。**

到这里,我们会自然想到,通过单调栈的方式,分别预处理除 `l` 和 `r` 数组:

* `l[i] = loc` 含义为下标 `i` 左边最近一个比 `arr[i]` 小的位置是 `loc`(若在 $arr[i]$ 左侧不存在比其小的数,则 `loc = -1`)
* `r[i] = loc` 含义为下标 `i` 右边最近一个比 `arr[i]` 小的位置是 `loc`(若在 $arr[i]$ 左侧不存在比其小的数,则 `loc = n`)

当我们预处理两数组后,通过简单「乘法原理」即可统计以 $arr[i]$ 为最小值时,子数组的个数:

* 包含 $arr[i]$ 的子数组左端点个数为 $a = i - l[i]$ 个
* 包含 $arr[i]$ 的子数组右端点个数为 $b = r[i] - i$ 个

子数组的个数 $\times$ 子数组最小值 $arr[i]$,即是当前 $arr[i]$ 对答案的贡献:$a \times b \times arr[i]$。

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

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

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

Java 代码:
```Java
class Solution {
    int MOD = (int)1e9+7;
    public int sumSubarrayMins(int[] arr) {
        int n = arr.length, ans = 0;
        int[] l = new int[n], r = new int[n];
        Arrays.fill(l, -1); Arrays.fill(r, n);
        Deque<Integer> d = new ArrayDeque<>();
        for (int i = 0; i < n; i++) {
            while (!d.isEmpty() && arr[d.peekLast()] >= arr[i]) r[d.pollLast()] = i;
            d.addLast(i);
        }
        d.clear();
        for (int i = n - 1; i >= 0; i--) {
            while (!d.isEmpty() && arr[d.peekLast()] > arr[i]) l[d.pollLast()] = i;
            d.addLast(i);
        }
        for (int i = 0; i < n; i++) {
            int a = i - l[i], b = r[i] - i;
            ans += a * 1L * b % MOD * arr[i] % MOD;
            ans %= MOD;
        }
        return ans;
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    int MOD = 1e9 + 7;
    int sumSubarrayMins(vector<int>& arr) {
        int n = arr.size(), ans = 0;
        vector<int> l(n, -1), r(n, n);
        stack<int> d;
        for (int i = 0; i < n; i++) {
            while (!d.empty() && arr[d.top()] >= arr[i]) {
                r[d.top()] = i;
                d.pop();
            }
            d.push(i);
        }
        while (!d.empty()) d.pop();
        for (int i = n - 1; i >= 0; i--) {
            while (!d.empty() && arr[d.top()] > arr[i]) {
                l[d.top()] = i;
                d.pop();
            }
            d.push(i);
        }
        for (int i = 0; i < n; i++) {
            long long a = i - l[i], b = r[i] - i;
            ans = (ans + a * b % MOD * arr[i] % MOD) % MOD;
        }
        return ans;
    }
};
```
Python 代码:
```Python
class Solution:
    def sumSubarrayMins(self, arr: List[int]) -> int:
        n, ans = len(arr), 0
        l, r = [-1] * n, [n] * n
        stk = []
        for i in range(n):
            while stk and arr[stk[-1]] >= arr[i]:
                r[stk.pop()] = i
            stk.append(i)
        stk = []
        for i in range(n - 1, -1, -1):
            while stk and arr[stk[-1]] > arr[i]:
                l[stk.pop()] = i
            stk.append(i)
        for i in range(n):
            a, b = i - l[i], r[i] - i
            ans += a * b * arr[i]
        return ans % (10 ** 9 + 7)
```
TypeScript 代码:
```TypeScript
const MOD = 1000000007
function sumSubarrayMins(arr: number[]): number {
    let n = arr.length, ans = 0
    const l = new Array<number>(n).fill(-1), r = new Array<number>(n).fill(n)
    const stk = new Array<number>(n).fill(0)
    let he = 0, ta = 0
    for (let i = 0; i < n; i++) {
        while (he < ta && arr[stk[ta - 1]] >= arr[i]) r[stk[--ta]] = i
        stk[ta++] = i
    }
    he = ta = 0
    for (let i = n - 1; i >= 0; i--) {
        while (he < ta && arr[stk[ta - 1]] > arr[i]) l[stk[--ta]] = i
        stk[ta++] = i
    }
    for (let i = 0; i < n; i++) {
        const a = i - l[i], b = r[i] - i
        ans += a * b % MOD * arr[i] % MOD
        ans %= MOD
    }
    return ans
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n)$

---

### 优化

实际上,当我们从栈中弹出某个 $arr[cur]$ 时,其右边界必然是导致其弹出的 `arr[r]`(当前所遍历到的元素),而 $arr[cur]$ 若存在左边界,必然是位于 $cur$ 栈中的前一位置,即 $arr[cur]$ 弹出后的新栈顶元素(若不存在物理左边界,则左边界为 $-1$)。

Java 代码:
```Java
class Solution {
    int MOD = (int)1e9+7;
    public int sumSubarrayMins(int[] arr) {
        int n = arr.length, ans = 0;
        Deque<Integer> d = new ArrayDeque<>();
        for (int r = 0; r <= n; r++) {
            int t = r < n ? arr[r] : 0;
            while (!d.isEmpty() && arr[d.peekLast()] >= t) {
                int cur = d.pollLast();
                int l = d.isEmpty() ? -1 : d.peekLast();
                int a = cur - l, b = r - cur;
                ans += a * 1L * b % MOD * arr[cur] % MOD;
                ans %= MOD;
            }
            d.addLast(r);
        }
        return ans;
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    int MOD = 1e9 + 7;
    int sumSubarrayMins(vector<int>& arr) {
        int n = arr.size(), ans = 0;
        deque<int> d;
        for (int r = 0; r <= n; r++) {
            int t = (r < n) ? arr[r] : 0;
            while (!d.empty() && arr[d.back()] >= t) {
                int cur = d.back();
                d.pop_back();
                int l = d.empty() ? -1 : d.back();
                long long a = cur - l, b = r - cur;
                ans = (ans + a * b % MOD * arr[cur] % MOD) % MOD;
            }
            d.push_back(r);
        }
        return ans;
    }
};
```
Python 代码:
```Python
class Solution:
    def sumSubarrayMins(self, arr: List[int]) -> int:
        n, ans = len(arr), 0
        stk = []
        for r in range(n + 1):
            t = arr[r] if r < n else 0
            while stk and arr[stk[-1]] >= t:
                cur = stk.pop()
                l = stk[-1] if stk else -1
                a, b = cur - l, r - cur
                ans += a * b * arr[cur]
            stk.append(r)
        return ans % (10 ** 9 + 7)
```
TypeScript 代码:
```TypeScript
const MOD = 1000000007
function sumSubarrayMins(arr: number[]): number {
    let n = arr.length, ans = 0
    const stk = new Array<number>(n).fill(0)
    let he = 0, ta = 0
    for (let r = 0; r <= n; r++) {
        const t = r < n ? arr[r] : 0
        while (he < ta && arr[stk[ta - 1]] >= t) {
            const cur = stk[--ta]
            const l = he < ta ? stk[ta - 1] : -1
            const a = cur - l, b = r - cur
            ans += a * b % MOD * arr[cur] % MOD
            ans %= MOD
        }
        stk[ta++] = r
    }
    return ans
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


Question:
给你一个整数数组 `nums`,和一个整数 `k` 。

在一个操作中,您可以选择 $0 <= i < nums.length$ 的任何索引 `i` 。将 $nums[i]$ 改为 $nums[i] + x$ ,其中 $x$ 是一个范围为 $[-k, k]$ 的整数。对于每个索引 `i` ,最多 只能 应用 一次 此操作。

`nums` 的 分数 是 `nums` 中最大和最小元素的差值。

在对  `nums` 中的每个索引最多应用一次上述操作后,返回 `nums` 的最低 分数 。

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

输出:0

解释:分数是 max(nums) - min(nums) = 1 - 1 = 0。
```
示例 2:
```
输入:nums = [0,10], k = 2

输出:6

解释:将 nums 改为 [2,8]。分数是 max(nums) - min(nums) = 8 - 2 = 6。
```
示例 3:
```
输入:nums = [1,3,6], k = 3

输出:0

解释:将 nums 改为 [4,4,4]。分数是 max(nums) - min(nums) = 4 - 4 = 0。
```

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

Solution:
### 脑筋急转弯

根据题意,对于任意一个数 $nums[i]$ 而言,其所能变化的范围为 $[nums[i] - k, nums[i] + k]$,我们需要最小化变化后的差值。而当 $k$ 足够大时,我们必然能够将所有数变为同一个值,此时答案为 $0$,而更一般的情况,我们能够缩减的数值距离为 $2 * k$,因此如果原来的最大差值为 $d = \max - \min$,若 $d <= 2 * k$ 时,答案为 $0$,否则答案为 $d - 2 * k$。

代码:
```Java
class Solution {
    public int smallestRangeI(int[] nums, int k) {
        int max = nums[0], min = nums[0];
        for (int i : nums) {
            max = Math.max(max, i);
            min = Math.min(min, i);
        }
        return Math.max(0, max - min - 2 * k);
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


Question:
给你一个大小为 `n x n` 的整数矩阵 `board` ,方格按从 $1$ 到 $n^2$ 编号,编号遵循 转行交替方式 ,从左下角开始 (即,从 `board[n - 1][0]` 开始)每一行交替方向。

玩家从棋盘上的方格 $1$ (总是在最后一行、第一列)开始出发。

每一回合,玩家需要从当前方格 $curr$ 开始出发,按下述要求前进:

* 选定目标方格 `next`,目标方格的编号符合范围 $[curr + 1, min(curr + 6, n^2)]$。
* 该选择模拟了掷 六面体骰子 的情景,无论棋盘大小如何,玩家最多只能有 $6$ 个目的地。
* 传送玩家:如果目标方格 `next` 处存在蛇或梯子,那么玩家会传送到蛇或梯子的目的地。否则,玩家传送到目标方格 `next`。
* 当玩家到达编号 $n^2$ 的方格时,游戏结束。

`r` 行 `c` 列的棋盘,按前述方法编号,棋盘格中可能存在 “蛇” 或 “梯子”;

如果 `board[r][c] != -1`,那个蛇或梯子的目的地将会是 `board[r][c]`。

编号为 $1$ 和 $n^2$ 的方格上没有蛇或梯子。

注意,玩家在每回合的前进过程中最多只能爬过蛇或梯子一次:就算目的地是另一条蛇或梯子的起点,玩家也 不能 继续移动。

* 举个例子,假设棋盘是 $[[-1,4],[-1,3]]$ ,第一次移动,玩家的目标方格是 $2$ 。那么这个玩家将会顺着梯子到达方格 $3$ ,但 不能 顺着方格 $3$ 上的梯子前往方格 $4$ 。

返回达到编号为 $n^2$ 的方格所需的最少移动次数,如果不可能,则返回 $-1$。

示例 1:

```
输入:board =
[
[-1,-1,-1,-1,-1,-1],
[-1,-1,-1,-1,-1,-1],
[-1,-1,-1,-1,-1,-1],
[-1,35,-1,-1,13,-1],
[-1,-1,-1,-1,-1,-1],
[-1,15,-1,-1,-1,-1]
]

输出:4

解释:
首先,从方格 1 [第 5 行,第 0 列] 开始。
先决定移动到方格 2 ,并必须爬过梯子移动到到方格 15 。
然后决定移动到方格 17 [第 3 行,第 4 列],必须爬过蛇到方格 13 。
接着决定移动到方格 14 ,且必须通过梯子移动到方格 35 。
最后决定移动到方格 36 , 游戏结束。
可以证明需要至少 4 次移动才能到达最后一个方格,所以答案是 4 。
```

提示:
* $n == board.length == board[i].length$
* $2 <= n <= 20$
* $grid[i][j]$ 的值是 $-1$ 或在范围 $[1, n^2]$ 内
* 编号为 $1$ 和 $n^2$ 的方格上没有蛇或梯子

Solution:
### BFS

最多有 $20 * 20$ 个格子,直接使用常规的单向 `BFS` 进行求解即可。

为了方便我们可以按照题目给定的意思,将二维的矩阵「扁平化」为一维的矩阵,然后再按照规则进行 `BFS`。

代码:
```Java
class Solution {
    int n;
    int[] nums;
    public int snakesAndLadders(int[][] board) {
        n = board.length;
        if (board[0][0] != -1) return -1;
        nums = new int[n * n + 1];
        boolean isRight = true;
        for (int i = n - 1, idx = 1; i >= 0; i--) {
            for (int j = (isRight ? 0 : n - 1); isRight ? j < n : j >= 0; j += isRight ? 1 : -1) {
                nums[idx++] = board[i][j];
            }
            isRight = !isRight;
        }
        int ans = bfs();
        return ans;
    }
    int bfs() {
        Deque<Integer> d = new ArrayDeque<>();
        Map<Integer, Integer> m = new HashMap<>();
        d.addLast(1);
        m.put(1, 0);
        while (!d.isEmpty()) {
            int poll = d.pollFirst();
            int step = m.get(poll);
            if (poll == n * n) return step;
            for (int i = 1; i <= 6; i++) {
                int np = poll + i;
                if (np <= 0 || np > n * n) continue;
                if (nums[np] != -1) np = nums[np];
                if (m.containsKey(np)) continue;
                m.put(np, step + 1);
                d.addLast(np);
            }
        }
        return -1;
    }
}
```
* 时间复杂度:$O(n^2)$
* 空间复杂度:$O(n^2)$

### 最后

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

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

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

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


Question:
一条包含字母 A-Z 的消息通过以下映射进行了 编码 :
```
'A' -> 1
'B' -> 2
...
'Z' -> 26
```
要 解码 已编码的消息,所有数字必须基于上述映射的方法,反向映射回字母(可能有多种方法)。例如,"11106" 可以映射为:

* "AAJF" ,将消息分组为 (1 1 10 6)
* "KJF" ,将消息分组为 (11 10 6)

注意,消息不能分组为  (1 11 06) ,因为 "06" 不能映射为 "F" ,这是由于 "6" 和 "06" 在映射中并不等价。

给你一个只含数字的 非空 字符串 s ,请计算并返回 解码 方法的 总数 。

题目数据保证答案肯定是一个 32 位 的整数。

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

输出:2

解释:它可以解码为 "AB"(1 2)或者 "L"(12)。
```
示例 2:
```
输入:s = "226"

输出:3

解释:它可以解码为 "BZ" (2 26), "VF" (22 6), 或者 "BBF" (2 2 6) 。
```
示例 3:
```
输入:s = "0"

输出:0

解释:没有字符映射到以 0 开头的数字。
含有 0 的有效映射是 'J' -> "10" 和 'T'-> "20" 。
由于没有字符,因此没有有效的方法对此进行解码,因为所有数字都需要映射。
```
示例 4:
```
输入:s = "06"

输出:0

解释:"06" 不能映射到 "F" ,因为字符串含有前导 0("6" 和 "06" 在映射中并不等价)。
```

提示:
* 1 <= s.length <= 100
* s 只包含数字,并且可能包含前导零。

Solution:
### 基本分析

我们称一个解码内容为一个 `item`。

为根据题意,每个 `item` 可以由一个数字组成,也可以由两个数字组成。

数据范围为 100,很具有迷惑性,可能会有不少同学会想使用 DFS 进行爆搜。

我们可以大致分析一下这样的做法是否可行:不失一般性的考虑字符串 `s` 中的任意位置 `i`,位置 `i` 既可以作为一个独立 `item`,也可以与上一位置组成新 `item`,那么相当于每个位置都有两种分割选择(先不考虑分割结果的合法性问题),这样做法的复杂度是 $O(2^n)$ 的,当 `n` 范围是 100 时,远超我们计算机单秒运算量($10^7$)。即使我们将「判断分割结果是否合法」的操作放到爆搜过程中做剪枝,也与我们的单秒最大运算量相差很远。

递归的方法不可行,我们需要考虑递推的解法。

---

### 动态规划

**这其实是一道字符串类的动态规划题,不难发现对于字符串 `s` 的某个位置 `i` 而言,我们只关心「位置 `i` 自己能否形成独立 `item` 」和「位置 `i` 能够与上一位置(`i-1`)能否形成 `item`」,而不关心 `i-1` 之前的位置。**

有了以上分析,我们可以从前往后处理字符串 `s`,使用一个数组记录以字符串 `s` 的每一位作为结尾的解码方案数。即定义 $f[i]$ 为考虑前 $i$ 个字符的解码方案数。

对于字符串 `s` 的任意位置 `i` 而言,其存在三种情况:

* 只能由位置 `i` 的单独作为一个 `item`,设为 `a`,转移的前提是 `a` 的数值范围为 $[1,9]$,转移逻辑为 $f[i] = f[i - 1]$。
* 只能由位置 `i` 的与前一位置(`i-1`)共同作为一个 `item`,设为 `b`,转移的前提是 `b` 的数值范围为 $[10,26]$,转移逻辑为 $f[i] = f[i - 2]$。
* 位置 `i` 既能作为独立 `item` 也能与上一位置形成 `item`,转移逻辑为 $f[i] = f[i - 1] + f[i - 2]$。

因此,我们有如下转移方程:

$$
\begin{cases}

f[i] = f[i - 1], 1 \leqslant a \leq 9 \\

f[i] = f[i - 2], 10 \leqslant b \leqslant 26 \\

f[i] = f[i - 1] + f[i - 2], 1 \leqslant a \leq 9, 10 \leqslant b \leqslant 26 \\

\end{cases}
$$

其他细节:由于题目存在前导零,而前导零属于无效 `item`。可以进行特判,但个人习惯往字符串头部追加空格作为哨兵,追加空格既可以避免讨论前导零,也能使下标从 1 开始,简化 `f[i-1]` 等负数下标的判断。

代码:
```Java
class Solution {
    public int numDecodings(String s) {
        int n = s.length();
        s = " " + s;
        char[] cs = s.toCharArray();
        int[] f = new int[n + 1];
        f[0] = 1;
        for (int i = 1; i <= n; i++) {
            // a : 代表「当前位置」单独形成 item
            // b : 代表「当前位置」与「前一位置」共同形成 item
            int a = cs[i] - '0', b = (cs[i - 1] - '0') * 10 + (cs[i] - '0');
            // 如果 a 属于有效值,那么 f[i] 可以由 f[i - 1] 转移过来
            if (1 <= a && a <= 9) f[i] = f[i - 1];
            // 如果 b 属于有效值,那么 f[i] 可以由 f[i - 2] 或者 f[i - 1] & f[i - 2] 转移过来
            if (10 <= b && b <= 26) f[i] += f[i - 2];
        }
        return f[n];
    }
}
```
* 时间复杂度:共有 `n` 个状态需要被转移。复杂度为 $O(n)$。
* 空间复杂度:$O(n)$。

---

### 空间优化

不难发现,我们转移 `f[i]` 时只依赖 `f[i-1]` 和 `f[i-2]` 两个状态。

因此我们可以采用与「滚动数组」类似的思路,只创建长度为 3 的数组,通过取余的方式来复用不再需要的下标。

代码:
```Java
class Solution {
    public int numDecodings(String s) {
        int n = s.length();
        s = " " + s;
        char[] cs = s.toCharArray();
        int[] f = new int[3];
        f[0] = 1;
        for (int i = 1; i <= n; i++) {
            f[i % 3] = 0;
            int a = cs[i] - '0', b = (cs[i - 1] - '0') * 10 + (cs[i] - '0');
            if (1 <= a && a <= 9) f[i % 3] = f[(i - 1) % 3];
            if (10 <= b && b <= 26) f[i % 3] += f[(i - 2) % 3];
        }
        return f[n % 3];
    }
}
```
* 时间复杂度:共有 `n` 个状态需要被转移。复杂度为 $O(n)$。
* 空间复杂度:$O(1)$。

### 最后

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

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

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

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


Question:
给你单链表的头指针 head 和两个整数 left 和 right ,其中 left <= right 。请你反转从位置 left 到位置 right 的链表节点,返回 反转后的链表 。

示例 1:

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

提示:
* 链表中节点数目为 n
* 1 <= n <= 500
* -500 <= Node.val <= 500
* 1 <= left <= right <= n

Solution:
### 朴素解法

为了减少边界判断,我们可以建立一个虚拟头结点 dummy,使其指向 head,最终返回 dummy.next。

这种「哨兵」技巧能应用在所有的「链表」题目。

黄色部分的节点代表需要「翻转」的部分:

之后就是常规的模拟,步骤我写在示意图里啦 ~

代码:

```java []
class Solution {
    public ListNode reverseBetween(ListNode head, int l, int r) {
        ListNode dummy = new ListNode(0);
        dummy.next = head;

        r -= l;
        ListNode hh = dummy;
        while (l-- > 1) hh = hh.next;

        ListNode a = hh.next, b = a.next;
        while (r-- > 0) {
            ListNode tmp = b.next;
            b.next = a;
            a = b;
            b = tmp;
        }

        hh.next.next = b;
        hh.next = a;
        return dummy.next;
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


Question:
有效 `IP` 地址 正好由四个整数(每个整数位于 `0` 到 `255` 之间组成,且不能含有前导 `0`),整数之间用 `'.'` 分隔。

例如:`"0.1.2.201"` 和 `"192.168.1.1"` 是 有效 `IP` 地址,但是 `"0.011.255.245"`、`"192.168.1.312"` 和 `"192.168@1.1"` 是 无效 `IP` 地址。

给定一个只包含数字的字符串 `s` ,用以表示一个 `IP` 地址,返回所有可能的有效 `IP` 地址,这些地址可以通过在 `s` 中插入 `'.'` 来形成。你 不能 重新排序或删除 `s` 中的任何数字。你可以按 任何 顺序返回答案。

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

输出:["255.255.11.135","255.255.111.35"]
```
示例 2:
```
输入:s = "0000"

输出:["0.0.0.0"]
```
示例 3:
```
输入:s = "101023"

输出:["1.0.10.23","1.0.102.3","10.1.0.23","10.10.2.3","101.0.2.3"]
```

提示:
* $1 <= s.length <= 20$
* `s` 仅由数字组成

Solution:
### 回溯算法

和 [131. 分割回文串](https://mp.weixin.qq.com/s?__biz=MzU4NDE3MTEyMA==&mid=2247487047&idx=1&sn=117c48f20778868442fce44e100d2ea8) 一样,同样是一道求所有方案的题目,只能是没有太多优化的「爆搜」做法。

设计递归函数 `void dfs(int idx, int n, List<Integer> cur)`,其中 `idx` 代表当前处理字符串 `s` 的哪个位置,`n` 代表字符串 `s` 总长度,而 `cur` 的则是代表子串 $s[0 ... (idx - 1)]$ 部分的具体划分方案。

用题目样例 `s = "25525511135"` 作为 🌰,`n` 固定为 `11`。

当 `idx = 3` 时,`cur` 为 $s[0...2] = 255$,`cur` 部分的划分方案可能是 `[2,5,5]`、`[2,55]`、`[25,5]`、`[255]` 之一。

在 `cur` 的基础上,我们继续爆搜剩余部分,即递归执行 `dfs(idx, n, cur)`,算法会将剩余部分的划分方案添加到 `cur` 上,我们只需要确保每次追加到 `cur` 的数值符合要求即可(没有前导零 且 范围在 $[0, 255]$ 中)。

在单次回溯过程中,我们可以将 `idx` 作为当前划分数字的左端点,通过枚举的形式找到右端点 `j`,并将当前数字 $s[idx ... (j - 1)]$ 加到 `cur` 中(若合法),回溯到底后再添加到 `cur` 的元素进行移除。

当 `idx = n` 代表整个 `s` 已经处理完成,若此时 `cur` 恰好有 $4$ 个元素,说明我们找到了一组合法方案,将其拼接成字符串追加到答案数组中。

同时也是由于划分过程中 `cur` 最多只有 $4$ 个元素,我们可以用此做简单剪枝。

Java 代码:
```Java
class Solution {
    List<String> ans = new ArrayList<>();
    char[] cs;
    public List<String> restoreIpAddresses(String s) {
        cs = s.toCharArray();
        dfs(0, cs.length, new ArrayList<>());
        return ans;
    }
    void dfs(int idx, int n, List<Integer> cur) {
        if (cur.size() > 4) return ;
        if (idx == n) {
            if (cur.size() == 4) {
                StringBuilder sb = new StringBuilder();
                for (int i = 0; i < 4; i++) sb.append(cur.get(i)).append(".");
                ans.add(sb.substring(0, sb.length() - 1));
            }
        } else {
            for (int i = idx; i < n; i++) {
                int t = 0;
                for (int j = idx; j <= i; j++) t = t * 10 + (cs[j] - '0');
                if (cs[idx] == '0' && i != idx) break;
                if (t > 255) break;
                cur.add(t);
                dfs(i + 1, n, cur);
                cur.remove(cur.size() - 1);
            }
        }
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    vector<string> ans;
    string s;
    void dfs(int idx, int n, vector<int>& cur) {
        if (cur.size() > 4) return ;
        if (idx == n) {
            if (cur.size() == 4) {
                string sb;
                for (int i = 0; i < 4; i++) sb += to_string(cur[i]) + ".";
                ans.push_back(sb.substr(0, sb.length() - 1));
            }
        } else {
            for (int i = idx; i < n; i++) {
                int t = 0;
                for (int j = idx; j <= i; j++) t = t * 10 + (s[j] - '0');
                if (s[idx] == '0' && i != idx) break;
                if (t > 255) break;
                cur.push_back(t);
                dfs(i + 1, n, cur);
                cur.pop_back();
            }
        }
    }
    vector<string> restoreIpAddresses(string _s) {
        s = _s;
        vector<int> cur;
        dfs(0, s.length(), cur);
        return ans;
    }
};
```
Python 代码:
```Python
class Solution:
    def restoreIpAddresses(self, s: str) -> List[str]:
        ans = []
        def dfs(idx, n, cur):
            if len(cur) > 4:
                return
            if idx == n:
                if len(cur) == 4:
                    ans.append('.'.join(cur))
            else:
                for i in range(idx, n):
                    t = 0
                    for j in range(idx, i + 1):
                        t = t * 10 + (ord(s[j]) - ord('0'))
                    if s[idx] == '0' and i != idx:
                        break
                    if t > 255:
                        break
                    cur.append(str(t))
                    dfs(i + 1, n, cur)
                    cur.pop()
        dfs(0, len(s), [])
        return ans
```
TypeScript 代码:
```TypeScript
function restoreIpAddresses(s: string): string[] {
    const ans = new Array<string>()
    function dfs(idx: number, n: number, cur: Array<number>): void {
        if (cur.length > 4) return
        if (idx == n) {
            if (cur.length == 4) {
                let str = ''
                for (let i = 0; i < 4; i++) str += cur[i] + "."
                ans.push(str.substring(0, str.length - 1))
            }
        } else {
            for (let i = idx; i < n; i++) {
                let t = 0
                for (let j = idx; j <= i; j++) t = t * 10 + (s.charCodeAt(j) - '0'.charCodeAt(0))
                if (s[idx] == '0' && i != idx) break
                if (t > 255) break
                cur.push(t)
                dfs(i + 1, n, cur)
                cur.pop()
            }
        }
    }
    dfs(0, s.length, new Array<number>())
    return ans
}
```
* 时间复杂度:爆搜不讨论时空复杂度
* 空间复杂度:爆搜不讨论时空复杂度

### 最后

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

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

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

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


95. 不同的二叉搜索树 II

Question:
给你一个整数 `n`,请你生成并返回所有由 `n` 个节点组成且节点值从 `1` 到 `n` 互不相同的不同 二叉搜索树 。

可以按 任意顺序 返回答案。

示例 1:

```
输入:n = 3

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

输出:[[1]]
```

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

Solution:
### 回溯算法

题目要我们求所有所能构造的二叉搜索树的具体方案,而给定 $n$ 个节点所能构成的二叉搜索树的个数为 [卡特兰数](https://baike.baidu.com/item/%E5%8D%A1%E7%89%B9%E5%85%B0%E6%95%B0)。

其他方案数同为卡特兰数的还包括:凸多边形三角划分、`n` 对括号正确匹配数目 ...

回到本题,根据二叉搜索搜索的特性,若某个子树的根节点为 `root`,那么 `root` 的左子树任意节点值均比 `root.val` 要小,`root` 的右子树任意节点值均比 `root.val` 要大。

因此,假设我们使用 $[l, r]$ 连续段来构造二叉搜索树,并且选择了节点 `t` 作为二叉搜索树的根节点时:

* 那么使用 $[l, t - 1]$ 构造出来的二叉搜索树均可作为根节点 $t$ 的左子树
* 使用 $[t + 1, r]$ 构造出来的二叉搜索树均可作为根节点 $t$ 的右子树

也就是说,我们可以设计递归函数 `List<TreeNode> dfs(int l, int r)`,含义为使用连续段 $[l, r]$ 进行二叉搜索树构造,并返回相应集合。

最终答案为 `dfs(1,n)`,起始我们可以枚举 $[1, n]$ 范围内的的每个数 `t` 作为根节点,并递归 `dfs(l,t-1)` 和 `dfs(t+1,r)` 获取左右子树的集合 `left` 和 `right`。

结合「乘法原理」,枚举任意左子树和任意右子树,即可得到 `t` 作为根节点的二叉搜索树方案集,枚举所有 `t` 后即可得到所有二叉搜索树的总集。

> 注意:当我们运用乘法原理,来构造以 `t` 为根节点的二叉搜索树时,其 `left` 或 `right` 某一边可能为空集,但此时我们仍要将非空的一边子树进行挂载。
为了确保两层新循环的逻辑会被执行,对于空集我们不能使用 `null` 来代指,而要使用 `[null]` 来代指。

Java 代码:
```Java
class Solution {
    public List<TreeNode> generateTrees(int n) {
        return dfs(1, n);
    }
    List<TreeNode> dfs(int l, int r) {
        if (l > r) return new ArrayList<>(){{add(null);}};
        List<TreeNode> ans = new ArrayList<>();
        for (int i = l; i <= r; i++) {
            for (TreeNode x : dfs(l, i - 1)) {
                for (TreeNode y : dfs(i + 1, r)) {
                    TreeNode root = new TreeNode(i);
                    root.left = x; root.right = y;
                    ans.add(root);
                }
            }
        }
        return ans;
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    vector<TreeNode*> generateTrees(int n) {
        return dfs(1, n);
    }
    vector<TreeNode*> dfs(int l, int r) {
        if (l > r) return vector<TreeNode*>{NULL};
        vector<TreeNode*> ans;
        for (int i = l; i <= r; i++) {
            for (TreeNode* x : dfs(l, i - 1)) {
                for (TreeNode* y : dfs(i + 1, r)) {
                    // 创建当前节点并添加到结果列表中
                    TreeNode* root = new TreeNode(i);
                    root->left = x;
                    root->right = y;
                    ans.push_back(root);
                }
            }
        }
        return ans;
    }
};
```
Python 代码:
```Python
class Solution:
    def generateTrees(self, n: int) -> List[Optional[TreeNode]]:
        def dfs(l, r):
            if l > r:
                return [None]
            ans = []
            for i in range(l, r + 1):
                for x in dfs(l, i - 1):
                    for y in dfs(i + 1, r):
                        root = TreeNode(i)
                        root.left, root.right = x, y
                        ans.append(root)
            return ans
        return dfs(1, n)
```
TypeScript 代码:
```TypeScript
function generateTrees(n: number): Array<TreeNode | null> {
    function dfs(l: number, r: number): Array<TreeNode | null> {
        if (l > r) return [null]
        const ans = new Array<TreeNode>()
        for (let i = l; i <= r; i++) {
            for (const x of dfs(l, i - 1)) {
                for (const y of dfs(i + 1, r)) {
                    const root = new TreeNode(i)
                    root.left = x; root.right = y
                    ans.push(root)
                }
            }
        }
        return ans
    }
    return dfs(1, n)
}
```
* 时间复杂度:卡特兰数
* 空间复杂度:卡特兰数

### 最后

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

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

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

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


96. 不同的二叉搜索树

Question:
给你一个整数 `n` ,求恰由 `n` 个节点组成且节点值从 `1` 到 `n` 互不相同的 二叉搜索树 有多少种?

返回满足题意的二叉搜索树的种数。

示例 1:

```
输入:n = 3

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

输出:1
```

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

Solution:
### 区间 DP

沿用 [95. 不同的二叉搜索树 II](https://www.acoier.com/2022/12/09/95.%20%E4%B8%8D%E5%90%8C%E7%9A%84%E4%BA%8C%E5%8F%89%E6%90%9C%E7%B4%A2%E6%A0%91%20II%EF%BC%88%E4%B8%AD%E7%AD%89%EF%BC%89/) 的基本思路,只不过本题不是求具体方案,而是求个数。

除了能用 [95. 不同的二叉搜索树 II](https://www.acoier.com/2022/12/09/95.%20%E4%B8%8D%E5%90%8C%E7%9A%84%E4%BA%8C%E5%8F%89%E6%90%9C%E7%B4%A2%E6%A0%91%20II%EF%BC%88%E4%B8%AD%E7%AD%89%EF%BC%89/) 提到的「卡特兰数」直接求解 $n$ 个节点的以外,本题还能通过常规「区间 DP」的方式进行求解。

> 求数量使用 DP,求所有具体方案使用爆搜,是极其常见的一题多问搭配。

**定义 $f[l][r]$ 为使用数值范围在 $[l, r]$ 之间的节点,所能构建的 `BST` 个数**。

不失一般性考虑 $f[l][r]$ 该如何求解,仍用 $[l, r]$ 中的根节点 `i` 为何值,作为切入点进行思考。

根据「`BST` 定义」及「乘法原理」可知:$[l, i - 1]$ 相关节点构成的 `BST`  子树只能在 `i` 的左边,而 $[i + 1, r]$ 相关节点构成的 `BST`  子树只能在 `i` 的右边。所有的左右子树相互独立,因此以 `i` 为根节点的 `BST` 数量为 $f[l][i - 1] \times f[i + 1][r]$,而 `i` 共有 $r - l + 1$ 个取值($i \in [l, r]$)。

即有:
$$
f[l][r] = \sum_{i = l}^{r} (f[l][i - 1] \times f[i + 1][r])
$$
不难发现,求解区间 $[l, r]$ 的 `BST`  数量 $f[l][r]$ 依赖于比其小的区间 $[l, i - 1]$ 和 $[i + 1, r]$,这引导我们使用「区间 DP」的方式进行递推。

> 不了解区间 DP 的同学,可看 [前置 🧀](https://mp.weixin.qq.com/s/RuIKpDEuxkhhKiTeVRMXng)

一些细节:由于我们 `i` 的取值可能会取到区间中的最值 `l` 和 `r`,为了能够该情况下,$f[l][i - 1]$ 和 $f[i + 1][r]$ 能够顺利参与转移,起始我们需要先对所有满足 $i >= j$ 的 $f[i][j]$ 初始化为 `1`。

Java 代码:
```Java
class Solution {
    public int numTrees(int n) {
        int[][] f = new int[n + 10][n + 10];
        for (int i = 0; i <= n + 1; i++) {
            for (int j = 0; j <= n + 1; j++) {
                if (i >= j) f[i][j] = 1;
            }
        }
        for (int len = 2; len <= n; len++) {
            for (int l = 1; l + len - 1 <= n; l++) {
                int r = l + len - 1;
                for (int i = l; i <= r; i++) {
                    f[l][r] += f[l][i - 1] * f[i + 1][r];
                }
            }
        }
        return f[1][n];
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    int numTrees(int n) {
        vector<vector<int>> f(n + 2, vector<int>(n + 2, 0));
        for (int i = 0; i <= n + 1; i++) {
            for (int j = 0; j <= n + 1; j++) {
                if (i >= j) f[i][j] = 1;
            }
        }
        for (int len = 2; len <= n; len++) {
            for (int l = 1; l + len - 1 <= n; l++) {
                int r = l + len - 1;
                for (int i = l; i <= r; i++) {
                    f[l][r] += f[l][i - 1] * f[i + 1][r];
                }
            }
        }
        return f[1][n];
    }
};
```
Python 代码:
```Python
class Solution(object):
    def numTrees(self, n):
        f = [[0] * (n + 2) for _ in range(n + 2)]
        for i in range(n + 2):
            for j in range(n + 2):
                if i >= j:
                    f[i][j] = 1
        for length in range(2, n + 1):
            for l in range(1, n - length + 2):
                r = l + length - 1
                for i in range(l, r + 1):
                    f[l][r] += f[l][i - 1] * f[i + 1][r]
        return f[1][n]
```
TypeScript 代码:
```TypeScript
function numTrees(n: number): number {
    const f = new Array(n + 2).fill(0).map(() => new Array(n + 2).fill(0));
    for (let i = 0; i <= n + 1; i++) {
        for (let j = 0; j <= n + 1; j++) {
            if (i >= j) f[i][j] = 1;
        }
    }
    for (let len = 2; len <= n; len++) {
        for (let l = 1; l + len - 1 <= n; l++) {
            const r = l + len - 1;
            for (let i = l; i <= r; i++) {
                f[l][r] += f[l][i - 1] * f[i + 1][r];
            }
        }
    }
    return f[1][n];
};
```
* 时间复杂度:$O(n^3)$
* 空间复杂度:$O(n^2)$

---

### 区间 DP(优化)

求解完使用 $[1, n]$ 共 $n$ 个连续数所能构成的 `BST`  个数后,再来思考一个问题:使用 $[L, R]$ 共 $n = R - L + 1$ 个连续数,所能构成的 `BST`  个数又是多少。

答案是一样的。

**由 $n$ 个连续数构成的 `BST`  个数仅与数值个数有关系,与数值大小本身并无关系**。

由于可知,我们上述的「区间 DP」必然进行了大量重复计算,例如 $f[1][3]$ 和 $f[2][4]$ 同为大小为 $3$ 的区间,却被计算了两次。

调整我们的状态定义:**定义 $f[k]$ 为考虑连续数个数为 $k$ 时,所能构成的 `BST`  的个数**。

不失一般性考虑 $f[i]$ 如何计算,仍用 $[1, i]$ 中哪个数值作为根节点进行考虑。假设使用数值 $j$ 作为根节点,则有 $f[j - 1] \times f[i - j]$ 个 `BST` 可贡献到 $f[i]$,而 $j$ 共有 $i$ 个取值($j \in [1, i]$)。

即有:
$$
f[i] = \sum_{j = 1}^{i}(f[j - 1] \times f[i - j])
$$

同时有初始化 $f[0] = 1$,含义为没有任何连续数时,只有“空树”一种合法方案。

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

---

### 数学 - 卡特兰数

在「区间 DP(优化)」中的递推过程,正是“卡特兰数”的 $O(n^2)$ 递推过程。通过对常规「区间 DP」的优化,我们得证 [95. 不同的二叉搜索树 II](https://www.acoier.com/2022/12/09/95.%20%E4%B8%8D%E5%90%8C%E7%9A%84%E4%BA%8C%E5%8F%89%E6%90%9C%E7%B4%A2%E6%A0%91%20II%EF%BC%88%E4%B8%AD%E7%AD%89%EF%BC%89/) 中「给定 $n$ 个节点所能构成的 `BST`  的个数为卡特兰数」这一结论。

对于精确求卡特兰数,存在时间复杂度为 $O(n)$ 的通项公式做法,公式为 $C_{n+1} = \frac{C_n \cdot (4n + 2)}{n + 2}$。

Java 代码:

```Java
class Solution {
    public int numTrees(int n) {
        if (n <= 1) return 1;
        long ans = 1;
        for (int i = 0; i < n; i++) ans = ans * (4 * i + 2) / (i + 2);
        return (int)ans;
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    int numTrees(int n) {
        if (n <= 1) return 1;
        long long ans = 1;
        for (int i = 0; i < n; i++) ans = ans * (4 * i + 2) / (i + 2);
        return (int)ans;
    }
};
```
Python 代码:
```Python
class Solution:
    def numTrees(self, n: int) -> int:
        if n <= 1:
            return 1
        ans = 1
        for i in range(n):
            ans = ans * (4 * i + 2) // (i + 2)
        return ans
```
TypeScript 代码:
```TypeScript
function numTrees(n: number): number {
    if (n <= 1) return 1;
    let ans = 1;
    for (let i = 0; i < n; i++) ans = ans * (4 * i + 2) / (i + 2);
    return ans;
};
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


Question:
给定三个字符串 `s1`、`s2`、`s3`,请你帮忙验证 `s3` 是否是由 `s1` 和 `s2` 交错 组成的。

两个字符串 `s` 和 `t` **交错** 的定义与过程如下,其中每个字符串都会被分割成若干 **非空** 子字符串:

* `s = s1 + s2 + ... + sn`
* `t = t1 + t2 + ... + tm`
* `|n - m| <= 1`

交错是 `s1 + t1 + s2 + t2 + s3 + t3 + ...` 或者 `t1 + s1 + t2 + s2 + t3 + s3 + ...`

注意:`a + b` 意味着字符串 `a` 和 `b` 连接。

示例 1:

```
输入:s1 = "aabcc", s2 = "dbbca", s3 = "aadbbcbcac"

输出:true
```
示例 2:
```
输入:s1 = "aabcc", s2 = "dbbca", s3 = "aadbbbaccc"

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

输出:true
```

提示:
* $0 <= s1.length, s2.length <= 100$
* $0 <= s3.length <= 200$
* `s1`、`s2`、和 `s3` 都由小写英文字母组成

进阶:您能否仅使用 $O(s2.length)$ 额外的内存空间来解决它?

Solution:
### 记忆化搜索

数据范围相比于「暴搜」而言有点大,但将「暴搜」解法作为前置思考,总能为我们带来灵感。

将 `s1`、`s2` 和 `s3` 的长度分别记为 $n$、$m$ 和 $l$。

一个显然的情况是若 $n + m$ 不为 $l$,必然不能用 `s1` 和 `s2` 来凑成 `s3`,返回 `false`。

定义暴搜函数为 `boolean dfs(int i, int j)`,代表当前处理到 `s1` 的第 $i$ 个字符,`s2` 的第 $j$ 个字符,能否凑成 `s3` 的前 $i + j$ 个字符。

最终答案为 `dfs(0, 0)`。

根据 $s1[i]$、$s2[j]$ 和 $s3[i + j]$ 的关系分情况讨论:

* $s1[i] = s3[i + j]$,可使用 $s1[i]$ 充当 $s3[i + j]$ ,暴搜 $s1[i]$ 已被使用的情况,决策下一位 `dfs(i + 1, j)`
* $s2[j] = s3[i + j]$,可使用 $s2[j]$ 充当 $s3[i + j]$ ,暴搜 $s2[j]$ 已被使用的情况,决策下一位`dfs(i, j + 1)`

当 $i + j = l$ 时,代表我们成功用 `s1` 和 `s2` 凑成了 `s3`,返回 `true`;而若在任一回合出现 $s1[i] \neq s3[i + j]$ 且 $s2[j] \neq s3[i + j]$,说明构造无法进行,返回 `false`。

`TLE` Java 代码:

```Java
class Solution {
    char[] cs1, cs2, cs3;
    int n, m, l;
    public boolean isInterleave(String s1, String s2, String s3) {
        cs1 = s1.toCharArray(); cs2 = s2.toCharArray(); cs3 = s3.toCharArray();
        n = s1.length(); m = s2.length(); l = s3.length();
        if (n + m != l) return false;
        return dfs(0, 0);
    }
    boolean dfs(int i, int j) {
        if (i + j == l) return true;
        boolean ans = false;
        if (i < n && cs1[i] == cs3[i + j]) ans |= dfs(i + 1, j);
        if (j < m && cs2[j] == cs3[i + j]) ans |= dfs(i, j + 1);
        return ans;
    }
}
```

可分析上述 `TLE`  做法的时间复杂度上界为交错序列个数。

长度分别为 $n$ 和 $m$ 形成的交错序列数量为 $C(n+m, n) = \frac{(n+m)!}{n! \times m!}$ : 从 $n + m$ 个位置中选 $n$ 个位置按顺序放置 `s1`,剩下的 $m$ 个位置唯一确定的放置 `s2`。

实际上,不同交错序列之间有着相同的前缀(例如 `s1 = aac` 和 `s2 = bbd`,具体交错方案中的 `aabbcd` 和 `aabbdc` 之间有着相同的前缀 `aabb__`),可通过「记忆化搜索」来进行减少这些重复的构造。

直接根据 `dfs` 函数的入参构建缓存器 `cache`,起始 `cache[i][j] = 0`;若 `cache[i][j] = 1` 代表 `true`;`cache[i][j] = -1` 代表 `false`。

如此简单的改动,即可将朴素的 `DFS`  改成记忆化搜索。

Java 代码:
```Java
class Solution {
    char[] cs1, cs2, cs3;
    int n, m, l;
    int[][] cache;
    public boolean isInterleave(String s1, String s2, String s3) {
        cs1 = s1.toCharArray(); cs2 = s2.toCharArray(); cs3 = s3.toCharArray();
        n = s1.length(); m = s2.length(); l = s3.length();
        if (n + m != l) return false;
        cache = new int[n + 10][m + 10];
        return dfs(0, 0);
    }
    boolean dfs(int i, int j) {
        if (cache[i][j] != 0) return cache[i][j] == 1;
        if (i + j == l) return true;
        boolean ans = false;
        if (i < n && cs1[i] == cs3[i + j]) ans |= dfs(i + 1, j);
        if (j < m && cs2[j] == cs3[i + j]) ans |= dfs(i, j + 1);
        cache[i][j] = ans ? 1 : -1;
        return ans;
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    bool isInterleave(string s1, string s2, string s3) {
        int n = s1.length(), m = s2.length(), l = s3.length();
        if (n + m != l) return false;
        vector<vector<int>> cache(n + 10, vector<int>(m + 10, 0));
        function<bool(int, int)> dfs = [&](int i, int j) {
            if (cache[i][j] != 0) return cache[i][j] == 1;
            if (i + j == l) return true;
            bool ans = false;
            if (i < n && s1[i] == s3[i + j]) ans |= dfs(i + 1, j);
            if (j < m && s2[j] == s3[i + j]) ans |= dfs(i, j + 1);
            cache[i][j] = ans ? 1 : -1;
            return ans;
        };
        return dfs(0, 0);
    }
};
```
Python 代码:
```Python
class Solution:
    def isInterleave(self, s1: str, s2: str, s3: str) -> bool:
        n, m, l = len(s1), len(s2), len(s3)
        if n + m != l:
            return False
        @lru_cache
        def dfs(i, j):
            if i + j == l:
                return True
            ans = False
            if i < n and s1[i] == s3[i + j]:
                ans |= dfs(i + 1, j)
            if j < m and s2[j] == s3[i + j]:
                ans |= dfs(i, j + 1)
            return ans
        return dfs(0, 0)
```
TypeScript 代码:
```TypeScript
function isInterleave(s1: string, s2: string, s3: string): boolean {
    const n = s1.length, m = s2.length, l = s3.length;
    if (n + m !== l) return false;
    const cache = new Array(n + 10).fill(0).map(() => new Array(m + 10).fill(0));
    const dfs = (i: number, j: number): boolean => {
        if (cache[i][j] !== 0) return cache[i][j] === 1;
        if (i + j === l) return true;
        let ans = false;
        if (i < n && s1[i] === s3[i + j]) ans ||= dfs(i + 1, j);
        if (j < m && s2[j] === s3[i + j]) ans ||= dfs(i, j + 1);
        cache[i][j] = ans ? 1 : -1;
        return ans;
    };
    return dfs(0, 0);
};
```
* 时间复杂度:共有 $n \times m$ 个状态,复杂度为 $O(n \times m)$
* 空间复杂度:$O(n \times m)$

---

### 线性 DP

直接将「记忆化搜索」中的缓存器修改为我们的动态规划状态定义。

**定义 $f[i][j]$ 为使用 `s1` 的前 $i$ 个字符,使用 `s2` 的前 $j$ 个字符,能否凑出 `s3` 的前 $i + j$ 个字符**。

为了方便,我们令所有字符串的下标均从 $1$ 开始。

不失一般性,考虑 $f[i][j]$ 如何转移,根据 $s1[i]$、$s2[j]$ 和 $s3[i+j]$ 是否相等进行分析,即**根据当前 `s3` 的最后一个字符 $s3[i + j]$ 由 $s1[i]$ 和 $s2[j]$ 谁来提供进行讨论**:

* $s1[i] = s3[i + j]$:说明当前 `s3` 的最后一个字符 $s3[i + j]$ 可由 $s1[i]$ 提供,而 `s3` 此前的 $i + j - 1$ 个字符,可由 `s1` 的前  $i - 1$ 字符和 `s2` 的前 $j$ 个字符共同提供。此时若 $f[i - 1][j]$ 为真,则有 $f[i][j]$ 为真。
* $s2[j] = s3[i+j]$:说明当前 `s3` 的最后一个字符串 $s3[i+j]$ 可由 $s2[j]$ 提供,而 `s3` 此前的 $i + j - 1$ 个字符,可由 `s1` 的前  $i$ 字符和 `s2` 的前 $j - 1$ 个字符共同提供。此时若 $f[i][j - 1]$ 为真,则有 $f[i][j]$ 为真。

综上,只有上述条件任一成立,$f[i][j]$ 即为真。

一些细节:为了在转移过程中减少边界处理,我们先预处理出 $f[0][X]$ 和 $f[X][0]$ 的状态值(即 `s1`、`s2` 和 `s3` 之间的公共前缀)。

Java 代码:

```Java
class Solution {
    public boolean isInterleave(String s1, String s2, String s3) {
        char[] cs1 = s1.toCharArray(), cs2 = s2.toCharArray(), cs3 = s3.toCharArray();
        int n = s1.length(), m = s2.length(), l = s3.length();
        if (n + m != l) return false;
        boolean[][] f = new boolean[n + 10][m + 10];
        f[0][0] = true;
        for (int i = 1; i <= n && f[i - 1][0]; i++) f[i][0] = cs1[i - 1] == cs3[i - 1];
        for (int i = 1; i <= m && f[0][i - 1]; i++) f[0][i] = cs2[i - 1] == cs3[i - 1];
        for (int i = 1; i <= n; i++) {
            for (int j = 1; j <= m; j++) {
                if (cs1[i - 1] == cs3[i + j - 1]) f[i][j] |= f[i - 1][j];
                if (cs2[j - 1] == cs3[i + j - 1]) f[i][j] |= f[i][j - 1];
            }
        }
        return f[n][m];
    }
}
```
Python 代码:
```Python
class Solution:
    def isInterleave(self, s1: str, s2: str, s3: str) -> bool:
        n, m, l = len(s1), len(s2), len(s3)
        if n + m != l:
            return False
        f = [[False] * (m + 10) for _ in range(n + 10)]
        f[0][0] = True
        for i in range(1, n + 1):
            f[i][0] = f[i - 1][0] and s1[i - 1] == s3[i - 1]
        for i in range(1, m + 1):
            f[0][i] = f[0][i - 1] and s2[i - 1] == s3[i - 1]
        for i in range(1, n + 1):
            for j in range(1, m + 1):
                if s1[i - 1] == s3[i + j - 1]:
                    f[i][j] |= f[i - 1][j]
                if s2[j - 1] == s3[i + j - 1]:
                    f[i][j] |= f[i][j - 1]
        return f[n][m]
```
C++ 代码:
```C++
class Solution {
public:
    bool isInterleave(string s1, string s2, string s3) {
        int n = s1.length(), m = s2.length(), l = s3.length();
        if (n + m != l) return false;
        vector<vector<bool>> f(n + 10, vector<bool>(m + 10, false));
        f[0][0] = true;
        for (int i = 1; i <= n && f[i - 1][0]; i++) f[i][0] = (s1[i - 1] == s3[i - 1]);
        for (int i = 1; i <= m && f[0][i - 1]; i++) f[0][i] = (s2[i - 1] == s3[i - 1]);
        for (int i = 1; i <= n; i++) {
            for (int j = 1; j <= m; j++) {
                if (s1[i - 1] == s3[i + j - 1]) f[i][j] = f[i][j] | f[i - 1][j];
                if (s2[j - 1] == s3[i + j - 1]) f[i][j] = f[i][j] | f[i][j - 1];
            }
        }
        return f[n][m];
    }
};
```
TypeScript 代码:
```TypeScript
function isInterleave(s1: string, s2: string, s3: string): boolean {
    const n = s1.length, m = s2.length, l = s3.length;
    if (n + m !== l) return false;
    const f = new Array(n + 10).fill(false).map(() => new Array(m + 10).fill(false));
    f[0][0] = true;
    for (let i = 1; i <= n && f[i - 1][0]; i++) f[i][0] = s1[i - 1] === s3[i - 1];
    for (let i = 1; i <= m && f[0][i - 1]; i++) f[0][i] = s2[i - 1] === s3[i - 1];
    for (let i = 1; i <= n; i++) {
        for (let j = 1; j <= m; j++) {
            if (s1[i - 1] === s3[i + j - 1]) f[i][j] ||= f[i - 1][j];
            if (s2[j - 1] === s3[i + j - 1]) f[i][j] ||= f[i][j - 1];
        }
    }
    return f[n][m];
};
```
* 时间复杂度:$O(n \times m)$
* 空间复杂度:$O(n \times m)$

---

### 进阶

根据状态转移方程可知 $f[i][j]$ 仅依赖于 $f[i - 1][j]$ 和 $f[i][j - 1]$,因此可通过「滚动数组」的方式进行优化。

状态定义不变,将动规数组开成 $f[2][m + 10]$ 大小(另外一维的 `+10` 操作仅为个人习惯)。

随后,当我们需要更新 `f[i][X]` 时,需要写成 `f[i & 1][X]`;需要访问 `f[i - 1][X]` 时,则写成 `f[(i - 1) & 1][X]` 即可。

最后,同样是为了转移过程中减少边界处理,我们需要使用两个变量 `a` 和 `b` 记住 `s1` 和 `s2` 与 `s3` 的公共前缀,并特判掉 `s1` 或 `s2` 为空的情况。

Java 代码:
```Java
class Solution {
    public boolean isInterleave(String s1, String s2, String s3) {
        char[] cs1 = s1.toCharArray(), cs2 = s2.toCharArray(), cs3 = s3.toCharArray();
        int n = s1.length(), m = s2.length(), l = s3.length();
        if (n + m != l) return false;
        int a = 0, b = 0;
        while (a < n && cs1[a] == cs3[a]) a++;
        while (b < m && cs2[b] == cs3[b]) b++;
        if (m == 0) return n <= a;
        if (n == 0) return m <= b;
        boolean[][] f = new boolean[2][m + 10];
        f[0][0] = true;
        for (int i = 1; i <= n; i++) {
            for (int j = 1; j <= m; j++) {
                boolean cur = false;
                if (cs1[i - 1] == cs3[i + j - 1]) {
                    if (i == 1) cur |= j <= b;
                    else cur |= f[(i - 1) & 1][j];
                }
                if (cs2[j - 1] == cs3[i + j - 1]) {
                    if (j == 1) cur |= i <= a;
                    else cur |= f[i & 1][j - 1];
                }
                f[i & 1][j] = cur;
            }
        }
        return f[n & 1][m];
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    bool isInterleave(string s1, string s2, string s3) {
        int n = s1.length(), m = s2.length(), l = s3.length();
        if (n + m != l) return false;
        int a = 0, b = 0;
        while (a < n && s1[a] == s3[a]) a++;
        while (b < m && s2[b] == s3[b]) b++;
        if (n == 0) return m <= b;
        if (m == 0) return n <= a;
        vector<vector<bool>> f(2, vector<bool>(m + 1, false));
        f[0][0] = true;
        for (int i = 1; i <= n; i++) {
            for (int j = 1; j <= m; j++) {
                bool cur = false;
                if (s1[i - 1] == s3[i + j - 1]) {
                    if (i == 1) cur |= j <= b;
                    else cur |= f[(i - 1) & 1][j];
                }
                if (s2[j - 1] == s3[i + j - 1]) {
                    if (j == 1) cur |= i <= a;
                    else cur |= f[i & 1][j - 1];
                }
                f[i & 1][j] = cur;
            }
        }
        return f[n & 1][m];
    }
};
```
Python 代码:
```Python
class Solution:
    def isInterleave(self, s1: str, s2: str, s3: str) -> bool:
        n, m, l = len(s1), len(s2), len(s3)
        if n + m != l:
            return False
        a, b = 0, 0
        while a < n and s1[a] == s3[a]:
            a += 1
        while b < m and s2[b] == s3[b]:
            b += 1
        if m == 0:
            return n <= a
        if n == 0:
            return m <= b
        f = [[False for _ in range(m + 1)] for _ in range(2)]
        f[0][0] = True
        for i in range(1, n+1):
            for j in range(1, m+1):
                cur = False
                if s1[i - 1] == s3[i + j - 1]:
                    if i == 1:
                        cur |= j <= b
                    else:
                        cur |= f[(i - 1) & 1][j]
                if s2[j - 1] == s3[i + j - 1]:
                    if j == 1:
                        cur |= i <= a
                    else:
                        cur |= f[i & 1][j - 1]
                f[i & 1][j] = cur
        return f[n & 1][m]
```
TypeScript 代码:
```TypeScript
function isInterleave(s1: string, s2: string, s3: string): boolean {
    const n = s1.length, m = s2.length, l = s3.length;
    if (n + m !== l) return false;
    let a = 0, b = 0;
    while (a < n && s1[a] === s3[a]) a++;
    while (b < m && s2[b] === s3[b]) b++;
    if (m == 0) return n <= a;
    if (n == 0) return m <= b;
    const f = new Array(2).fill(null).map(() => new Array(m + 1).fill(false));
    f[0][0] = true;
    for (let i = 1; i <= n; i++) {
        for (let j = 1; j <= m; j++) {
            let cur = false;
            if (s1[i - 1] === s3[i + j - 1]) {
                if (i === 1) cur ||= j <= b;
                else cur ||= f[(i - 1) & 1][j];
            }
            if (s2[j - 1] === s3[i + j - 1]) {
                if (j === 1) cur ||= i <= a;
                else cur ||= f[i & 1][j - 1];
            }
            f[i & 1][j] = cur;
        }
    }
    return f[n & 1][m];
};
```
* 时间复杂度:$O(n \times m)$
* 空间复杂度:$O(m)$

### 最后

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

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

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

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


Question:
给你二叉搜索树的根节点 `root`,该树中的 恰好 两个节点的值被错误地交换。

请在不改变其结构的情况下,恢复这棵树 。

示例 1:

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

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

解释:3 不能是 1 的左孩子,因为 3 > 1 。交换 1 和 3 使二叉搜索树有效。
```
示例 2:

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

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

解释:2 不能在 3 的右子树中,因为 2 < 3 。交换 2 和 3 使二叉搜索树有效。
```

提示:
* 树上节点的数目在范围 $[2, 1000]$ 内
* $-2^{31} <= Node.val <= 2^{31} - 1$

进阶:使用 $O(n)$ 空间复杂度的解法很容易实现。你能想出一个只使用 $O(1)$ 空间的解决方案吗?

Solution:
### 基本分析

首先,别想复杂了。

所谓的恢复二叉树(两节点互换),只需要将两节点的 `val` 进行互换即可,而不需要对节点本身进行互换。

---

### 中序遍历 - 递归 & 迭代

二叉搜索树,其中序遍历是有序的。

要找到哪两个节点被互换,可通过比对中序遍历序列来实现。

但将整个中序遍历序列保存下来,再检测序列有序性的做法,复杂度是 $O(n)$ 的(不要说题目要求的 $O(1)$,连 $O(h)$ 都达不到)。

所以第一步,**这个「递归 & 迭代」的次优解,我们先考虑如何做到 $O(h)$ 的空间复杂度,即在中序遍历过程中找到互换节点**。

其实也很简单,除了使用 `a` 和 `b` 来记录互换节点,额外使用变量 `last` 来记录当前遍历过程中的前一节点即可:

若存在前一节点 `last` 存在,而且满足前一节点值大于当前节点(`last.val > root.val`),违反“有序性”,根据是否为首次出现该情况分情况讨论:

* 若是首次满足条件,即 `a == null`,此时上一节点 `last` 必然是两个互换节点之一,而当前 `root` 只能说是待定,因为有可能是 `last` 和 `root` 实现互换,也有可能是 `last` 和后续的某个节点实现互换。

  此时有 `a = last, b = root`

* 若是非首次满足条件,即 `a != null`,此时当前节点 `root` 必然是两个互换节点中的另外一个。

  此时有 `b = root`

综上:如果整个中序遍历的序列中“逆序对”为一对,那么互换节点为该“逆序对”的两个成员;若“逆序对”数量为两对,则互换节点为「第一对“逆序对”的首个节点」和「第二对“逆序对”的第二个节点」。

Java 代码(递归):

```Java
class Solution {
    TreeNode a = null, b = null, last = null;
    public void recoverTree(TreeNode root) {
        dfs(root);
        int val = a.val;
        a.val = b.val;
        b.val = val;
    }
    void dfs(TreeNode root) {
        if (root == null) return ;
        dfs(root.left);
        if (last != null && last.val > root.val) {
            if (a == null) {
                a = last; b = root;
            } else {
                b = root;
            }
        }
        last = root;
        dfs(root.right);
    }
}
```
Java 代码(迭代):
```Java
class Solution {
    public void recoverTree(TreeNode root) {
        Deque<TreeNode> d = new ArrayDeque<>();
        TreeNode a = null, b = null, last = null;
        while (root != null || !d.isEmpty()) {
            while (root != null) {
                d.addLast(root);
                root = root.left;
            }
            root = d.pollLast();
            if (last != null && last.val > root.val) {
                if (a == null) {
                    a = last; b = root;
                } else {
                    b = root;
                }
            }
            last = root;
            root = root.right;
        }
        int val = a.val;
        a.val = b.val;
        b.val = val;
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(h)$,其中 $h$ 为树高

---

### 中序遍历 - Morris 遍历

Morris 遍历也就是经常说到的“神级遍历”,其本质是通过做大常数来降低空间复杂度。

还是以二叉树的中序遍历为例,无论是递归或是迭代,为了在遍历完左节点(也可以是左子树)时,仍能回到父节点,我们需要使用数据结构栈,只不过在递归做法中是用函数调用充当栈,而在迭代做法则是显式栈。

**这使得空间复杂度为 $O(h)$,而 Morris 遍历的核心则是利用“路径底部节点”中的空闲指针进行线索。**

举个 🌰,对于一棵最简单的二叉树:

在中序遍历过程中,如果选择递归或迭代方式,并且不使用栈的情况,当遍历完左子节点(或左子树的最后一个节点)后,将会面临无法返回根节点的问题。

在 Morris 遍历中,从根节点开始,尚未真正遍历左子节点之前,就会先建立「左子节点(或左子树的最后一个节点)」与「当前根节点」之间的链接,从而避免使用栈。

具体的,Morris 遍历的中序遍历遵循如下流程(喜欢的话可以背过):

1. 令根节点为当前节点

2. 只要当前节点不为空(`while (root != null) `),重复执行如下流程:

   * 若当前节点的左子节点为空(`root.left = null`),将当前节点更新为其右子节点(`root = root.right`)

   * 若当前节点的左子节点不为空,利用临时变量 `t` 存储,找到当前节点的前驱节点(左子树中最后一个节点):
     * 若前驱节点的右子节点为空(`t.right = null`),将前驱节点的右子节点链接到当前节点(`t.right = root`),并将当前节点更新为左子节点(`root = root.left`)
     * 若前驱节点的右子节点不为空,说明已链接到当前节点,此时将前驱节点的右子节点置空(删除链接 `t.right = null`),遍历当前节点,并将当前节点更新为右子节点(`root = root.right`)

Java 代码:
```Java
class Solution {
    public void recoverTree(TreeNode root) {
        TreeNode a = null, b = null, last = null;
        while (root != null) {
            if (root.left == null) {
                if (last != null && last.val > root.val) {
                    if (a == null) {
                        a = last; b = root;
                    } else {
                        b = root;
                    }
                }
                last = root;
                root = root.right;
            } else {
                TreeNode t = root.left;
                while (t.right != null && t.right != root) t = t.right;
                if (t.right == null) { // 若前驱节点右子树为空, 说明是真正遍历左子树前, 建立与当前根节点的链接, 然后开始真正遍历左子树
                    t.right = root;
                    root = root.left;
                } else {  // 若已存在链接, 说明是第二次访问根节点, 左子树(前驱节点)已遍历完, 此时应该解开链接, 遍历当前节点以及右子树
                    t.right = null;
                    if (last != null && last.val > root.val) {
                        if (a == null) {
                            a = last; b = root;
                        } else {
                            b = root;
                        }
                    }
                    last = root;
                    root = root.right;
                }
            }
        }
        int val = a.val;
        a.val = b.val;
        b.val = val;
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    void recoverTree(TreeNode* root) {
        TreeNode *a = nullptr, *b = nullptr, *last = nullptr;
        while (root != nullptr) {
            if (root->left == nullptr) {
                if (last != nullptr && last->val > root->val) {
                    if (a == nullptr) {
                        a = last; b = root;
                    } else {
                        b = root;
                    }
                }
                last = root;
                root = root->right;
            } else {
                TreeNode *t = root->left;
                while (t->right != nullptr && t->right != root) t = t->right;
                if (t->right == nullptr) {
                    t->right = root;
                    root = root->left;
                } else {
                    t->right = nullptr;
                    if (last != nullptr && last->val > root->val) {
                        if (a == nullptr) {
                            a = last, b = root;
                        } else {
                            b = root;
                        }
                    }
                    last = root;
                    root = root->right;
                }
            }
        }
        swap(a->val, b->val);
    }
};
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


Question:
给你两个整数数组 $persons$ 和 $times$ 。

在选举中,第 $i$ 张票是在时刻为 $times[i]$ 时投给候选人 $persons[i]$ 的。

对于发生在时刻 $t$ 的每个查询,需要找出在 t 时刻在选举中领先的候选人的编号。

在 $t$ 时刻投出的选票也将被计入我们的查询之中。在平局的情况下,最近获得投票的候选人将会获胜。

实现 TopVotedCandidate 类:
* `TopVotedCandidate(int[] persons, int[] times)` 使用 $persons$ 和 $times$ 数组初始化对象。
* `int q(int t)` 根据前面描述的规则,返回在时刻 $t$ 在选举中领先的候选人的编号。

示例:
```
输入:
["TopVotedCandidate", "q", "q", "q", "q", "q", "q"]
[[[0, 1, 1, 0, 0, 1, 0], [0, 5, 10, 15, 20, 25, 30]], [3], [12], [25], [15], [24], [8]]

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

解释:
TopVotedCandidate topVotedCandidate = new TopVotedCandidate([0, 1, 1, 0, 0, 1, 0], [0, 5, 10, 15, 20, 25, 30]);
topVotedCandidate.q(3); // 返回 0 ,在时刻 3 ,票数分布为 [0] ,编号为 0 的候选人领先。
topVotedCandidate.q(12); // 返回 1 ,在时刻 12 ,票数分布为 [0,1,1] ,编号为 1 的候选人领先。
topVotedCandidate.q(25); // 返回 1 ,在时刻 25 ,票数分布为 [0,1,1,0,0,1] ,编号为 1 的候选人领先。(在平局的情况下,1 是最近获得投票的候选人)。
topVotedCandidate.q(15); // 返回 0
topVotedCandidate.q(24); // 返回 0
topVotedCandidate.q(8); // 返回 1
```

提示:
* $1 <= persons.length <= 5000$
* $times.length == persons.length$
* $0 <= persons[i] < persons.length$
* $0 <= times[i] <= 10^9$
* $times$ 是一个严格递增的有序数组
* $times[0] <= t <= 10^9$
* 每个测试用例最多调用 $10^4$ 次 `q`

Solution:
### 二分

根据题意,我们会在 $times[i]$ 时刻为 $persons[i]$ 候选人增加一票。

**利用 $times$ 数组严格递增,我们可以在处理 $times$ 时(模拟加票过程),使用一个变量 $val$ 来维护当前得票的最大数量,使用 $list$ 来记录不同时刻点的候选人交替情况。**

起始时 $val = 0$,当出现票数大于等于 $val$ 时,我们往 $list$ 追加二元组记录 $list[idx] = (times[i], persons[i])$,并更新 $val$。

**每个 $list[idx]$ 代表了当前候选人 $list[idx][1]$ 的首次当选时刻为 $list[idx][0]$。**

举个 🌰,若 $i = list[idx][0], j = list[idx + 1][0]$,则意味着在时间段 $[i, j)$ 内当选的候选人为 $list[idx][1]$。

在查询时,我们只需要通过「二分」找到 $list$ 中满足 $list[i][0] <= t$ 的分割点 $r$(最大下标),取 $list[r][1]$ 即是答案。

代码:
```Java
class TopVotedCandidate {
    List<int[]> list = new ArrayList<>();
    public TopVotedCandidate(int[] persons, int[] times) {
        int val = 0;
        Map<Integer, Integer> map = new HashMap<>();
        for (int i = 0; i < times.length; i++) {
            map.put(persons[i], map.getOrDefault(persons[i], 0) + 1);
            if (map.get(persons[i]) >= val) {
                val = map.get(persons[i]);
                list.add(new int[]{times[i], persons[i]});
            }
        }
    }
    public int q(int t) {
        int l = 0, r = list.size() - 1;
        while (l < r) {
            int mid = l + r + 1 >> 1;
            if (list.get(mid)[0] <= t) l = mid;
            else r = mid - 1;
        }
        return list.get(r)[0] <= t ? list.get(r)[1] : 0;
    }
}
```
* 时间复杂度:令 $n$ 为数组长度,$m$ 为查询次数(调用 `q` 的次数)。预处理出 $list$ 的复杂度为 $O(n)$;最坏情况下,每个 $time[i]$ 时刻都会交替产生新的候选人编号,即最坏情况下 $list$ 的数组长度为 $n$,总的查询复杂度为 $O(m * \log{n})$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


Question:
两位玩家分别扮演猫和老鼠,在一张**无向**图上进行游戏,两人轮流行动。

图的形式是:`graph[a]` 是一个列表,由满足 `ab` 是图中的一条边的所有节点 `b` 组成。

老鼠从节点 `1` 开始,第一个出发;猫从节点 `2` 开始,第二个出发。在节点 `0` 处有一个洞。

在每个玩家的行动中,他们 必须 沿着图中与所在当前位置连通的一条边移动。

例如,如果老鼠在节点 `1` ,那么它必须移动到 `graph[1]` 中的任一节点。

此外,猫无法移动到洞中(节点 `0`)。

然后,游戏在出现以下三种情形之一时结束:

如果猫和老鼠出现在同一个节点,猫获胜。
如果老鼠到达洞中,老鼠获胜。
如果某一位置重复出现(即,玩家的位置和移动顺序都与上一次行动相同),游戏平局。
给你一张图 `graph`,并假设两位玩家都都以最佳状态参与游戏:
* 如果老鼠获胜,则返回 `1`;
* 如果猫获胜,则返回 `2`;
* 如果平局,则返回 `0` 。

示例 1:

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

输出:0
```
示例 2:

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

输出:1
```

提示:
* 3 <= graph.length <= 50
* 1 <= graph[i].length < graph.length
* 0 <= graph[i][j] < graph.length
* graph[i][j] != i
* graph[i] 互不相同
* 猫和老鼠在游戏中总是移动

Solution:
### 动态规划

数据范围只有 $50$,使得本题的难度大大降低。

**定义 $f[k][i][j]$ 为当前进行了 $k$ 步,老鼠所在位置为 $i$,猫所在的位置为 $j$ 时,最终的获胜情况($0$ 为平局,$1$ 和 $2$ 分别代表老鼠和猫获胜),起始我们让所有的 $f[k][i][j] = -1$(为无效值),最终答案为 $f[0][1][2]$。**

不失一般性的考虑 $f[i][j][k]$ 该如何转移,根据题意,将当前所在位置 $i$ 和 $j$ 结合「游戏结束,判定游戏」的规则来分情况讨论:

* 若 $i = 0$,说明老鼠位于洞中,老鼠获胜,此时有 $f[k][i][j] = 1$;
* 若 $i = j$,说明两者为与同一位置,猫获胜,此时有 $f[k][i][j] = 2$;
* 考虑如何定义平局,即 $f[k][i][j] = 0$ 的情况?

我们最多有 $n$ 个位置,因此 $(i, j)$ 位置对组合数最多为 $n^2$ 种,然后 $k$ 其实代表的是老鼠先手还是猫先手,共有 $2$ 种可能,因此状态数量数据有明确上界为 $2 * n^2$。

所以我们可以设定 $k$ 的上界为 $2 * n^2$(抽屉原理,走超过 $2 * n^2$ 步,必然会有相同的局面出现过至少两次),但是这样的计算量为 $2 * 50^2 * 50 * 50 = 1.25 * 10^7$,有 `TLE` 风险,转移过程增加剪枝,可以过。

而更小的 $k$ 值需要证明:在最优策略中,相同局面(状态)成环的最大长度的最小值为 $k$。

题目规定轮流移动,且只能按照 $graph$ 中存在的边进行移动。

1. 如果当前 $k$ 为偶数(该回合老鼠移动),此时所能转移所有点为 $f[k + 1][ne][j]$,其中 $ne$ 为 $i$ 所能到达的点。由于采用的是最优移动策略,因此 $f[k][i][j]$ 会优先往 $f[k + 1][ne][j] = 1$ 的点移动(自身获胜),否则往 $f[k + 1][ne][j] = 0$ 的点移动(平局),再否则才是往 $f[k + 1][ne][j] = 2$ 的点移动(对方获胜);

2. 同理,如果当前 $k$ 为奇数(该回合猫移动),此时所能转移所有点为 $f[k + 1][i][ne]$,其中 $ne$ 为 $j$ 所能到达的点。由于采用的是最优移动策略,因此 $f[k][i][j]$ 会优先往 $f[k + 1][i][ne] = 2$ 的点移动(自身获胜),否则往 $f[k + 1][i][ne] = 0$ 的点移动(平局),再否则才是往 $f[k + 1][i][ne] = 1$ 的点移动(对方获胜)。

使用该转移优先级进行递推即可,为了方便我们使用「记忆化搜索」的方式来实现动态规划。

注意,该优先级转移其实存在「贪心」决策,但证明这样的决策会使得「最坏情况最好」是相对容易的,而证明存在更小的 $k$ 值才是本题难点。

> 一些细节:由于本身就要对动规数组进行无效值的初始化,为避免每个样例都 `new` 大数组,我们使用 `static` 来修饰大数组,可以有效减少一点时间(不使用 `static` 的话,`N` 只能取到 $51$)。

代码:
```Java
class Solution {
    static int N = 55;
    static int[][][] f = new int[2 * N * N][N][N];
    int[][] g;
    int n;
    public int catMouseGame(int[][] graph) {
        g = graph;
        n = g.length;
        for (int k = 0; k < 2 * n * n; k++) {
            for (int i = 0; i < n; i++) {
                for (int j = 0; j < n; j++) {
                    f[k][i][j] = -1;
                }
            }
        }
        return dfs(0, 1, 2);
    }
    // 0:draw / 1:mouse / 2:cat
    int dfs(int k, int a, int b) {
        int ans = f[k][a][b];
        if (a == 0) ans = 1;
        else if (a == b) ans = 2;
        else if (k >= 2 * n * n) ans = 0;
        else if (ans == -1) {
            if (k % 2 == 0) { // mouse
                boolean win = false, draw = false;
                for (int ne : g[a]) {
                    int t = dfs(k + 1, ne, b);
                    if (t == 1) win = true;
                    else if (t == 0) draw = true;
                    if (win) break;
                }
                if (win) ans = 1;
                else if (draw) ans = 0;
                else ans = 2;
            } else { // cat
                boolean win = false, draw = false;
                for (int ne : g[b]) {
                    if (ne == 0) continue;
                    int t = dfs(k + 1, a, ne);
                    if (t == 2) win = true;
                    else if (t == 0) draw = true;
                    if (win) break;
                }
                if (win) ans = 2;
                else if (draw) ans = 0;
                else ans = 1;
            }
        }
        f[k][a][b] = ans;
        return ans;
    }
}
```
* 时间复杂度:状态的数量级为 $n^4$,可以确保每个状态只会被计算一次。复杂度为 $O(n^4)$
* 空间复杂度:$O(n^4)$

### 最后

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

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

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

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


Question:
给定一个数组 `nums`,将其划分为两个连续子数组 `left` 和 `right`,使得:

* `left` 中的每个元素都小于或等于 `right` 中的每个元素。
* `left` 和 `right` 都是非空的。
* `left` 的长度要尽可能小。

在完成这样的分组后返回 `left` 的长度。

用例可以保证存在这样的划分方法。

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

输出:3

解释:left = [5,0,3],right = [8,6]
```
示例 2:
```
输入:nums = [1,1,1,0,6,12]

输出:4

解释:left = [1,1,1,0],right = [6,12]
```

提示:
* $2 <= nums.length <= 10^5$
* $0 <= nums[i] <= 10^6$
* 可以保证至少有一种方法能够按题目所描述的那样对 `nums` 进行划分。

Solution:
### 模拟

根据题意,我们知道本质是求分割点,使得分割点的「左边的子数组的最大值」小于等于「右边的子数组的最小值」。

我们可以先通过一次遍历(从后往前)统计出所有后缀的最小值 `min`,其中 `min[i] = x` 含义为下标范围在 $[i, n - 1]$ 的 $nums[i]$ 的最小值为 `x`,然后再通过第二次遍历(从前往后)统计每个前缀的最大值(使用单变量进行维护),找到第一个符合条件的分割点即是答案。

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

### 最后

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

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

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

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


Question:
给你一个字符串 `s` ,根据下述规则反转字符串:

* 所有非英文字母保留在原有位置。
* 所有英文字母(小写或大写)位置反转。

返回反转后的 `s` 。

示例 1:
```
输入:s = "ab-cd"

输出:"dc-ba"
```
示例 2:
```
输入:s = "a-bC-dEf-ghIj"

输出:"j-Ih-gfE-dCba"
```
示例 3:
```
输入:s = "Test1ng-Leet=code-Q!"

输出:"Qedo1ct-eeLg=ntse-T!"
```

提示
* $1 <= s.length <= 100$
* `s` 仅由 `ASCII` 值在范围 $[33, 122]$ 的字符组成
* `s` 不含 `'\"'` 或 `'\\'`

Solution:
### 双指针

根据题意进行模拟即可,每次都使用 `i` 和 `j` 分别指向左端和右端可以被交换的字母,若当前指针指向的不是字母,则分别往中间移动,直到找到下一个可交换的字母位置,每次交换结束,两指针均往中间移动一位。

代码:
```Java
class Solution {
    public String reverseOnlyLetters(String s) {
        char[] cs = s.toCharArray();
        int n = cs.length;
        for (int i = 0, j = n - 1; i < j; ) {
            while (i < j && !Character.isLetter(cs[i])) i++;
            while (i < j && !Character.isLetter(cs[j])) j--;
            if (i < j) {
                char c = cs[i];
                cs[i++] = cs[j];
                cs[j--] = c;
            }
        }
        return String.valueOf(cs);
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:调用 `toCharArray` 和构造新字符串均需要与字符串长度等同的空间。复杂度为 $O(n)$

### 最后

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

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

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

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


919. 完全二叉树插入器

Question:
完全二叉树 是每一层(除最后一层外)都是完全填充(即,节点数达到最大)的,并且所有的节点都尽可能地集中在左侧。

设计一种算法,将一个新节点插入到一个完整的二叉树中,并在插入后保持其完整。

实现 `CBTInserter` 类:

* `CBTInserter(TreeNode root)` 使用头节点为 `root` 的给定树初始化该数据结构;
* `CBTInserter.insert(int v)`  向树中插入一个值为 `Node.val == val` 的新节点 `TreeNode`。使树保持完全二叉树的状态,并返回插入节点 `TreeNode` 的父节点的值
* `CBTInserter.get_root()` 将返回树的头节点。

示例 1:

```
输入
["CBTInserter", "insert", "insert", "get_root"]
[[[1, 2]], [3], [4], []]

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

解释
CBTInserter cBTInserter = new CBTInserter([1, 2]);
cBTInserter.insert(3);  // 返回 1
cBTInserter.insert(4);  // 返回 2
cBTInserter.get_root(); // 返回 [1, 2, 3, 4]
```

提示:
* 树中节点数量范围为 $[1, 1000]$
* $0 <= Node.val <= 5000$
* `root` 是完全二叉树
* $0 <= val <= 5000 $
* 每个测试用例最多调用 `insert` 和 `get_root` 操作 $10^4$ 次

Solution:
### BFS

起始使用数组对构造函数传入的 `root` 进行 `BFS` 层序遍历(由于仍要保留层序遍历顺序,因此使用下标指针 `cur` 来模拟出队位置)。

对于 `insert` 操作而言,我们要在数组(层序遍历顺序)中找到首个「存在左右空子节点」的父节点 `fa`,由于找到 `fa` 节点的过程数组下标单调递增,因此可以使用全局变量 `idx` 不断往后搜索,每次将新节点 `node` 添加到当前树后,需要将 `node` 添加到数组尾部。

`get_root` 操作则始终返回数组首位元素即可。

Java 代码:
```Java
class CBTInserter {
    List<TreeNode> list = new ArrayList<>();
    int idx = 0;
    public CBTInserter(TreeNode root) {
        list.add(root);
        int cur = 0;
        while (cur < list.size()) {
            TreeNode node = list.get(cur);
            if (node.left != null) list.add(node.left);
            if (node.right != null) list.add(node.right);
            cur++;
        }
    }
    public int insert(int val) {
        TreeNode node = new TreeNode(val);
        while (list.get(idx).left != null && list.get(idx).right != null) idx++;
        TreeNode fa = list.get(idx);
        if (fa.left == null) fa.left = node;
        else if (fa.right == null) fa.right = node;
        list.add(node);
        return fa.val;
    }
    public TreeNode get_root() {
        return list.get(0);
    }
}
```
TypeScript 代码:
```TypeScript
class CBTInserter {
    list: TreeNode[] = new Array<TreeNode>()
    idx: number = 0
    constructor(root: TreeNode | null) {
        this.list.push(root)
        let cur = 0
        while (cur < this.list.length) {
            const node = this.list[cur]
            if (node.left != null) this.list.push(node.left)
            if (node.right != null) this.list.push(node.right)
            cur++
        }
    }
    insert(val: number): number {
        const node = new TreeNode(val)
        while (this.list[this.idx].left != null && this.list[this.idx].right != null) this.idx++
        const fa = this.list[this.idx]
        if (fa.left == null) fa.left = node
        else if (fa.right == null) fa.right = node
        this.list.push(node)
        return fa.val
    }
    get_root(): TreeNode | null {
        return this.list[0]
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


921. 使括号有效的最少添加

Question:
只有满足下面几点之一,括号字符串才是有效的:

* 它是一个空字符串,或者
* 它可以被写成 `AB` (`A` 与 `B` 连接), 其中 `A` 和 `B` 都是有效字符串,或者
* 它可以被写作 (`A`),其中 `A` 是有效字符串。

给定一个括号字符串 `s` ,移动 `N` 次,你就可以在字符串的任何位置插入一个括号。

* 例如,如果 `s = "()))"` ,你可以插入一个开始括号为 `"(()))"` 或结束括号为 `"())))"` 。

返回 为使结果字符串 `s` 有效而必须添加的最少括号数。

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

输出:1
```
示例 2:
```
输入:s = "((("

输出:3
```

提示:
* $1 <= s.length <= 1000$
* `s` 只包含 `'('` 和 `')'` 字符

Solution:
### 模拟

一个介绍过很多次的做法 : 将「有效括号问题」转化为「分值有效性」的数学判定。

使用 `score` 代指处理过程中的得分,将 `(` 记为 `+1`,将 `)` 记为 `-1`。

一个有效的括号应当在整个过程中不出现负数,因此一旦 `score` 出现负数,我们需要马上增加 `(` 来确保合法性;当整个 `s` 处理完后,还需要添加 `socre` 等同的 `)` 来确保合法性。

Java 代码:
```Java
class Solution {
    public int minAddToMakeValid(String s) {
        int score = 0, ans = 0;
        for (char c : s.toCharArray()) {
            score += c == '(' ? 1 : -1;
            if (score < 0) {
                score = 0; ans++;
            }
        }
        return ans + score;
    }
}
```
TypeScript 代码:
```TypeScript
function minAddToMakeValid(s: string): number {
    let scroe = 0, ans = 0
    for (const c of s) {
        scroe += c == '(' ? 1 : -1
        if (scroe < 0) {
            scroe = 0; ans++
        }
    }
    return ans + scroe
};
```
Python 代码:
```Python
class Solution:
    def minAddToMakeValid(self, s: str) -> int:
        score, ans = 0, 0
        for c in s:
            score += 1 if c == '(' else -1
            if score < 0:
                score = 0
                ans += 1
        return ans + score
```
* 时间复杂度:$O(n)$
* 空间复杂度:使用 `charAt` 代替 `toCharArray` 操作,复杂度为 $O(1)$

### 最后

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

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

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

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


LeetCode/921-930/926. 将字符串翻转到单调递增（中等）.md

926. 将字符串翻转到单调递增

Question:
如果一个二进制字符串,是以一些 $0$(可能没有 $0$)后面跟着一些 $1$(也可能没有 $1$)的形式组成的,那么该字符串是 单调递增 的。

给你一个二进制字符串 `s`,你可以将任何 $0$ 翻转为 $1$ 或者将 $1$ 翻转为 $0$ 。

返回使 `s` 单调递增的最小翻转次数。

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

输出:1

解释:翻转最后一位得到 00111.
```
示例 2:
```
输入:s = "010110"

输出:2

解释:翻转得到 011111,或者是 000111。
```
示例 3:
```
输入:s = "00011000"

输出:2

解释:翻转得到 00000000。
```

提示:
* $1 <= s.length <= 10^5$
* `s[i]` 为 `'0'` 或 `'1'`

Solution:
### LIS 问题贪心解

根据题意,不难想到将原题进行等价转换:**令 `s` 长度为 $n$,原问题等价于在 `s` 中找到最长不下降子序列,设其长度为 $ans$,那么对应的 $n - ans$ 即是答案。**

由于数据范围为 $1e5$,因此我们需要使用 `LIS` 问题的贪心求解方式:**使用 `g` 数组记录每个长度的最小结尾元素,即 `g[len] = x` 含义为长度为 $len$ 的最长不下降子序列的结尾元素为 $x$,然后在从前往后处理每个 $t = s[i]$ 时,由于是求解「最长不下降子序列」,等价于找「满足大于 $t$ 的最小下标」,这可以运用「二分」进行求解。**

> 不了解 `LIS` 问题或者不清楚 `LIS`  问题贪心解法的同学可以看前置 🧀 : **[LCS 问题与 LIS 问题的相互关系,以及 LIS 问题的最优解证明](https://mp.weixin.qq.com/s?__biz=MzU4NDE3MTEyMA==&mid=2247487814&idx=1&sn=e33023c2d474ff75af83eda1c4d01892)**,里面详细讲解了 `LIS` 贪心解的正确性证明,以及 `LCS` 和 `LIS` 在特定条件下存在的内在联系。

代码:
```Java
class Solution {
    public int minFlipsMonoIncr(String s) {
        char[] cs = s.toCharArray();
        int n = cs.length, ans = 0;
        int[] g = new int[n + 10];
        Arrays.fill(g, n + 10);
        for (int i = 0; i < n; i++) {
            int t = s.charAt(i) - '0';
            int l = 1, r = i + 1;
            while (l < r) {
                int mid = l + r >> 1;
                if (g[mid] > t) r = mid;
                else l = mid + 1;
            }
            g[r] = t;
            ans = Math.max(ans, r);
        }
        return n - ans;
    }
}
```
* 时间复杂度:$O(n\log{n})$
* 空间复杂度:$O(n)$

---

### 前缀和 + 枚举

更进一步,利用 `s` 只存在 $0$ 和 $1$ 两种数值,我们知道最后的目标序列形如 `000...000`、`000...111` 或 `111...111` 的形式。

因此我们可以枚举目标序列的 $0$ 和 $1$ 分割点位置 $idx$(分割点是 $0$ 是 $1$ 都可以,不消耗改变次数)。

于是问题转换为:分割点 $idx$ 左边有多少个 $1$(目标序列中分割点左边均为 $0$,因此 $1$ 的个数为左边的改变次数),分割点 $idx$ 的右边有多少个 $0$(目标序列中分割点右边均为 $1$,因此 $0$ 的个数为右边的改变次数),两者之和即是分割点为 $idx$ 时的总变化次数,所有 $idx$ 的总变化次数最小值即是答案。

而求解某个点左边或者右边有多少 $1$ 和 $0$ 可通过「前缀和」进行优化。

代码:
```Java
class Solution {
    public int minFlipsMonoIncr(String s) {
        char[] cs = s.toCharArray();
        int n = cs.length, ans = n;
        int[] sum = new int[n + 10];
        for (int i = 1; i <= n; i++) sum[i] = sum[i - 1] + (cs[i - 1] - '0');
        for (int i = 1; i <= n; i++) {
            int l = sum[i - 1], r = (n - i) - (sum[n] - sum[i]);
            ans = Math.min(ans, l + r);
        }
        return ans;
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


Question:
给定一个由 `0` 和 `1` 组成的数组 `arr`,将数组分成  `3` 个非空的部分 ,使得所有这些部分表示相同的二进制值。

如果可以做到,请返回任何 `[i, j]`,其中 `i+1 < j`,这样一来:

* `arr[0], arr[1], ..., arr[i]` 为第一部分;
* `arr[i + 1], arr[i + 2], ..., arr[j - 1]` 为第二部分;
* `arr[j], arr[j + 1], ..., arr[arr.length - 1]` 为第三部分。
* 这三个部分所表示的二进制值相等。

如果无法做到,就返回 `[-1, -1]`。

注意,在考虑每个部分所表示的二进制时,应当将其看作一个整体。例如,`[1,1,0]` 表示十进制中的 `6`,而不会是 `3`。此外,前导零也是被允许的,所以 `[0,1,1]` 和 `[1,1]` 表示相同的值。

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

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

输出:[-1,-1]
```
示例 3:
```
输入:arr = [1,1,0,0,1]

输出:[0,2]
```

提示:
* $3 <= arr.length <= 3 \times 10^4$
* `arr[i]` 是 `0` 或 `1`

Solution:
### 模拟

心情不好,写的代码也不好。

就大概讲讲啥意思吧:

1. 先统计 `1` 的个数 `cnt`,若 `cnt = 0` 代表能够任意划分,直接返回 `[0, 2]`;
2. 若 `cnt` 不为 $3$ 的倍数,必然不能正常划分,返回无解 `[-1, -1]`,否则可知三段中必然每一段 `1` 的数量均为 $t = \frac{cnt}{3}$ 个;
3. 最后检查「三段中 `1` 的间隔位是否相同,后缀 `0` 个数是否相同」即可:
    1. 创建二维数组 `ins` 记录三段中,相邻 `1` 之间的间隔情况,若存在间隔 `1` 不同,返回无解 `[-1, -1]`;
    2. 预处理四个变量 `l1`、`l2`、`r1` 和 `r2`,分别代表能够划分出最左边 `t` 个 `1` 的左右端点,以及能够划分出最右边 `t` 个 `1` 的左右端点,同时统计最后一段的后缀 `0` 的数量 `d`,配合四个变量来检查能否划分出具有 `d` 个后缀 `0` 的前两段。

代码:
```Java
class Solution {
    public int[] threeEqualParts(int[] arr) {
        int[] fail = new int[]{-1, -1};
        // 检查总数
        int n = arr.length, cnt = 0;
        for (int i = 0; i < n; i++) cnt += arr[i];
        if (cnt == 0) return new int[]{0, 2};
        if (cnt % 3 != 0) return fail;
        // 检查间隔相对位
        int t = cnt / 3;
        int[][] ins = new int[3][t];
        for (int i = 0, j = -1, k = 0, p = 0, idx = 0; i < n; i++) {
            if (arr[i] == 0) continue;
            if (j != -1) ins[p][idx++] = i - j;
            if (++k == t) {
                j = -1; k = 0; p++; idx = 0;
            } else {
                j = i;
            }
        }
        for (int i = 0; i < t; i++) {
            if (ins[0][i] == ins[1][i] && ins[0][i] == ins[2][i] && ins[1][i] == ins[2][i]) continue;
            return fail;
        }
        // 构造答案(l1 和 l2 分别为能够划分出最左边 t 个 1 的 左右端点;r1 和 r2 分别为能够划分出最右边 t 个 1 的左右端点)
        int l1 = -1, l2 = -1, r1 = -1, r2 = -1;
        for (int i = 0, k = 0; i < n; i++) {
            k += arr[i];
            if (k == t) {
                if (l1 == -1) l1 = i;
            } else if (k == t + 1) {
                l2 = i - 1;
                break;
            }
        }
        for (int i = n - 1, k = 0; i >= 0; i--) {
            k += arr[i];
            if (k == t) {
                if (r2 == -1) r2 = i;
            } else if (k == t + 1) {
                r1 = i + 1;
                break;
            }
        }
        int d = 0; // d 为最右边一段的后缀 0 的数量
        for (int i = n - 1; i >= 0; i--) {
            if (arr[i] == 1) break;
            d++;
        }
        if (l1 + d > l2 || r1 + d > r2) return fail;
        return new int[]{l1 + d, r1 + d};
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


929. 独特的电子邮件地址

Question:
每个 有效电子邮件地址 都由一个 本地名 和一个 域名 组成,以 `'@'` 符号分隔。除小写字母之外,电子邮件地址还可以含有一个或多个 `'.'` 或 `'+'` 。

* 例如,在 `alice@leetcode.com` 中, `alice` 是 本地名 ,而 `leetcode.com` 是域名 。

如果在电子邮件地址的 本地名 部分中的某些字符之间添加句点('`.'`),则发往那里的邮件将会转发到本地名中没有点的同一地址。请注意,此规则 不适用于域名 。

* 例如,`"alice.z@leetcode.com”` 和 `“alicez@leetcode.com”` 会转发到同一电子邮件地址。

如果在 本地名 中添加加号(`'+'`),则会忽略第一个加号后面的所有内容。这允许过滤某些电子邮件。同样,此规则 不适用于域名 。

* 例如 `m.y+name@email.com` 将转发到 `my@email.com`。

可以同时使用这两个规则。

给你一个字符串数组 `emails`,我们会向每个 `emails[i]` 发送一封电子邮件。返回实际收到邮件的不同地址数目。

示例 1:
```
输入:emails = ["test.email+alex@leetcode.com","test.e.mail+bob.cathy@leetcode.com","testemail+david@lee.tcode.com"]

输出:2

解释:实际收到邮件的是 "testemail@leetcode.com" 和 "testemail@lee.tcode.com"。
```
示例 2:
```
输入:emails = ["a@leetcode.com","b@leetcode.com","c@leetcode.com"]

输出:3
```

提示:
* $1 <= emails.length <= 100$
* $1 <= emails[i].length <= 100$
* `emails[i]` 由小写英文字母、`'+'`、`'.'` 和 `'@'` 组成
* 每个 `emails[i]` 都包含有且仅有一个 `'@'` 字符
* 所有本地名和域名都不为空
* 本地名不会以 `'+'` 字符作为开头

Solution:
### 模拟

根据题意进行模拟。

代码:
```Java
class Solution {
    public int numUniqueEmails(String[] emails) {
        Set<String> set = new HashSet<>();
        for (String s : emails) {
            StringBuilder sb = new StringBuilder();
            int n = s.length(), i = 0;
            boolean ok = true;
            while (i < n) {
                char c = s.charAt(i);
                if (c == '@') break;
                if (c == '.' && ++i >= 0) continue;
                if (c == '+') ok = false;
                if (ok) sb.append(c);
                i++;
            }
            set.add(sb.append(s.substring(i)).toString());
        }
        return set.size();
    }
}
```
* 时间复杂度:$O(\sum_{i = 0}^{n - 1} len(emails[i]))$
* 空间复杂度:$O(\sum_{i = 0}^{n - 1} len(emails[i]))$

### 最后

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

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

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

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


930. 和相同的二元子数组

Question:
给你一个二元数组 nums ,和一个整数 goal ,请你统计并返回有多少个和为 goal 的 非空 子数组。

子数组 是数组的一段连续部分。

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

输出:4

解释:
如下面黑体所示,有 4 个满足题目要求的子数组:
[1,0,1,0,1]
[1,0,1,0,1]
[1,0,1,0,1]
[1,0,1,0,1]
```
示例 2:
```
输入:nums = [0,0,0,0,0], goal = 0

输出:15
```

提示:
* 1 <= nums.length <= 3 * $10^4$
* nums[i] 不是 0 就是 1
* 0 <= goal <= nums.length

Solution:
### 前缀和 + 哈希表

一个简单的想法是,先计算 $nums$ 的前缀和数组 $sum$,然后从前往后扫描 $nums$,对于右端点 $r$,通过前缀和数组可以在 $O(1)$ 复杂度内求得 $[0, r]$ 连续一段的和,根据容斥原理,我们还需要求得某个左端点 $l$,使得 $[0, r]$ 减去 $[0, l - 1]$ 和为 $t$,即满足 $sum[r] - sum[l - 1] = t$,这时候利用哈希表记录扫描过的 $sum[i]$ 的出现次数,可以实现 $O(1)$ 复杂度内求得满足要求的左端点个数。

该方法适用于 $nums[i]$ 值不固定为 $0$ 和 $1$ 的其他情况。

代码:
```Java
class Solution {
    public int numSubarraysWithSum(int[] nums, int t) {
        int n = nums.length;
        int[] sum = new int[n + 1];
        for (int i = 1; i <= n; i++) sum[i] = sum[i - 1] + nums[i - 1];
        Map<Integer, Integer> map = new HashMap<>();
        map.put(0, 1);
        int ans = 0;
        for (int i = 0; i < n; i++) {
            int r = sum[i + 1], l = r - t;
            ans += map.getOrDefault(l, 0);
            map.put(r, map.getOrDefault(r, 0) + 1);
        }
        return ans;
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n)$

---

### 双指针

另外一个通用性稍差一点的做法则是利用 $nums[i]$ 没有负权值。

**$nums[i]$ 没有负权值意味着前缀和数组必然具有(非严格)单调递增特性。**

不难证明,在给定 $t$ 的情况下,当我们右端点 $r$ 往右移动时,满足条件的左端点 $l$ 必然往右移动。

实现上,我们可以使用两个左端点 $l1$ 和 $l2$,代表在给定右端点 $r$ 的前提下满足要求的左端点集合,同时使用 $s1$ 和 $s2$ 分别代表两个端点到 $r$ 这一段的和。

代码:
```Java
class Solution {
    public int numSubarraysWithSum(int[] nums, int t) {
        int n = nums.length;
        int ans = 0;
        for (int r = 0, l1 = 0, l2 = 0, s1 = 0, s2 = 0; r < n; r++) {
            s1 += nums[r];
            s2 += nums[r];
            while (l1 <= r && s1 > t) s1 -= nums[l1++];
            while (l2 <= r && s2 >= t) s2 -= nums[l2++];
            ans += l2 - l1;
        }
        return ans;
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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