301. 删除无效的括号

Question:
给你一个由若干括号和字母组成的字符串 `s`,删除最小数量的无效括号,使得输入的字符串有效。

返回所有可能的结果。答案可以按 任意顺序 返回。

示例 1:
```
输入: "()())()"
输出: ["()()()", "(())()"]
```
示例 2:
```
输入: "(a)())()"
输出: ["(a)()()", "(a())()"]
```
示例 3:
```
输入: ")("
输出: [""]
```

提示:
* $1 <= s.length <= 25$
* `s` 由小写英文字母以及括号 `'('` 和 `')'` 组成
* `s` 中至多含 `20` 个括号

Solution:
### 搜索 + 剪枝

由于题目要求我们将所有(最长)合法方案输出,因此不可能有别的优化,只能进行「爆搜」。

我们可以使用 `DFS` 实现回溯搜索。

基本思路:

我们知道所有的合法方案,必然有左括号的数量与右括号数量相等。

首先我们令左括号的得分为 $1$;右括号的得分为 $-1$。则会有如下性质:

1. 对于一个合法的方案而言,必然有最终得分为 $0$;
2. 搜索过程中不会出现得分值为 **负数** 的情况(当且仅当子串中某个前缀中「右括号的数量」大于「左括号的数量」时,会出现负数,此时不是合法方案)。

同时我们可以预处理出「爆搜」过程的最大得分: `max = min(左括号的数量, 右括号的数量)`

**PS.「爆搜」过程的最大得分必然是:合法左括号先全部出现在左边,之后使用最多的合法右括号进行匹配。**

枚举过程中出现字符分三种情况:

* 左括号:如果增加当前 `(` 后,仍为合法子串(即 $score + 1 <= max$) 时,我们可以选择添加该左括号,也能选择不添加;
* 右括号:如果增加当前 `)` 后,仍为合法子串(即 $score - 1 >= 0$) 时,我们可以选择添加该右括号,也能选择不添加;
* 普通字符:直接添加。

使用 `Set` 进行方案去重,$len$ 记录「爆搜」过程中的最大子串,然后只保留长度等于 $len$ 的子串。

代码:
```Java
class Solution {
    Set<String> set = new HashSet<>();
    int n, max, len;
    String s;
    public List<String> removeInvalidParentheses(String _s) {
        s = _s;
        n = s.length();
        int l = 0, r = 0;
        for (char c : s.toCharArray()) {
            if (c == '(') l++;
            else if (c == ')') r++;
        }
        max = Math.min(l, r);
        dfs(0, "", 0);
        return new ArrayList<>(set);
    }
    void dfs(int u, String cur, int score) {
        if (score < 0 || score > max) return ;
        if (u == n) {
            if (score == 0 && cur.length() >= len) {
                if (cur.length() > len) set.clear();
                len = cur.length();
                set.add(cur);
            }
            return ;
        }
        char c = s.charAt(u);
        if (c == '(') {
            dfs(u + 1, cur + String.valueOf(c), score + 1);
            dfs(u + 1, cur, score);
        } else if (c == ')') {
            dfs(u + 1, cur + String.valueOf(c), score - 1);
            dfs(u + 1, cur, score);
        } else {
            dfs(u + 1, cur + String.valueOf(c), score);
        }
    }
}
```
* 时间复杂度:预处理 $max$ 的复杂度为 $O(n)$;不考虑 $score$ 带来的剪枝效果,最坏情况下,每个位置都有两种选择,搜索所有方案的复杂度为 $O(2^n)$;同时搜索过程中会产生的新字符串(最终递归树中叶子节点的字符串长度最大为 $n$,使用 `StringBuilder` 也是同理),复杂度为 $O(n)$。整体复杂度为 $O(n \times 2^n)$
* 空间复杂度:最大合法方案数与字符串长度呈线性关系。复杂度为 $O(n)$

---

### 搜索 + 剪枝

在解法一,我们是在搜索过程中去更新最后的 $len$。

但事实上,我们可以通过预处理,得到最后的「应该删除的左括号数量」和「应该删掉的右括号数量」,来直接得到最终的 $len$。

因此在此基础上,我们可以考虑多增加一层剪枝。

代码:
```Java
class Solution {
    Set<String> set = new HashSet<>();
    int n, max, len;
    String s;
    public List<String> removeInvalidParentheses(String _s) {
        s = _s;
        n = s.length();

        int l = 0, r = 0;
        for (char c : s.toCharArray()) {
            if (c == '(') {
                l++;
            } else if (c == ')') {
                if (l != 0) l--;
                else r++;
            }
        }
        len = n - l - r;

        int c1 = 0, c2 = 0;
        for (char c : s.toCharArray()) {
            if (c == '(') c1++;
            else if (c == ')') c2++;
        }
        max = Math.min(c1, c2);

        dfs(0, "", l, r, 0);
        return new ArrayList<>(set);
    }
    void dfs(int u, String cur, int l, int r, int score) {
        if (l < 0 || r < 0 || score < 0 || score > max) return ;
        if (l == 0 && r == 0) {
            if (cur.length() == len) set.add(cur);
        }
        if (u == n) return ;
        char c = s.charAt(u);
        if (c == '(') {
            dfs(u + 1, cur + String.valueOf(c), l, r, score + 1);
            dfs(u + 1, cur, l - 1, r, score);
        } else if (c == ')') {
            dfs(u + 1, cur + String.valueOf(c), l, r, score - 1);
            dfs(u + 1, cur, l, r - 1, score);
        } else {
            dfs(u + 1, cur + String.valueOf(c), l, r, score);
        }
    }
}
```
* 时间复杂度:预处理 $max$ 和 $len$ 的复杂度为 $O(n)$;不考虑 $score$ 带来的剪枝效果,最坏情况下,每个位置都有两种选择,搜索所有方案的复杂度为 $O(2^n)$;同时搜索过程中会产生的新字符串(最终递归树中叶子节点的字符串长度最大为 $n$,使用 `StringBuilder` 也是同理),复杂度为 $O(n)$。整体复杂度为 $O(n \times 2^n)$
* 空间复杂度:最大合法方案数与字符串长度呈线性关系。复杂度为 $O(n)$

### 最后

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

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

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

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


LeetCode/301-310/303. 区域和检索 - 数组不可变（简单）.md

303. 区域和检索 - 数组不可变

Question:
给定一个整数数组 `nums`,求出数组从索引 `i` 到 `j`(`i ≤ j`)范围内元素的总和,包含 `i`、`j` 两点。

实现 `NumArray` 类:
* `NumArray(int[] nums)` 使用数组 `nums` 初始化对象
* `int sumRange(int i, int j)` 返回数组 `nums` 从索引 `i` 到 `j`(`i ≤ j`)范围内元素的总和,包含 i、j 两点(也就是 `sum(nums[i], nums[i + 1], ... , nums[j])`)

示例:
```
输入:
["NumArray", "sumRange", "sumRange", "sumRange"]
[[[-2, 0, 3, -5, 2, -1]], [0, 2], [2, 5], [0, 5]]
输出:
[null, 1, -1, -3]

解释:
NumArray numArray = new NumArray([-2, 0, 3, -5, 2, -1]);
numArray.sumRange(0, 2); // return 1 ((-2) + 0 + 3)
numArray.sumRange(2, 5); // return -1 (3 + (-5) + 2 + (-1))
numArray.sumRange(0, 5); // return -3 ((-2) + 0 + 3 + (-5) + 2 + (-1))
```

提示:
* $0 <= nums.length <= 10^4$
* $-10^5 <= nums[i] <= 10^5$
* $0 <= i <= j < nums.length$
* 最多调用 $10^4$ 次 `sumRange` 方法

Solution:
### 前缀和(一维)

这是一道前缀和裸题。

当要我们求「连续段」区域和的时候,要很自然的想到「前缀和」。

所谓前缀和,是指对原数组“累计和”的描述,通常是指一个与原数组等长的数组。

设前缀和数组为 `sum`,**`sum` 的每一位记录的是从「起始位置」到「当前位置」的元素和**。例如 $sum[x]$ 是指原数组中“起始位置”到“位置 `x`”这一连续段的元素和。

有了前缀和数组 `sum`,当我们求连续段 $[i, j]$ 的区域和时,利用「容斥原理」,便可进行快速求解。

通用公式:`ans = sum[j] - sum[i - 1]`。

由于涉及 `-1` 操作,为减少边界处理,我们可让前缀和数组下标从 $1$ 开始。在进行快速求和时,再根据原数组下标是否从 $1$ 开始,决定是否进行相应的下标偏移。

Java 代码:
```Java
class NumArray {
    int[] sum;
    public NumArray(int[] nums) {
        int n = nums.length;
        // 前缀和数组下标从 1 开始,因此设定长度为 n + 1(模板部分)
        sum = new int[n + 1];
        // 预处理除前缀和数组(模板部分)
        for (int i = 1; i <= n; i++) sum[i] = sum[i - 1] + nums[i - 1];
    }
    public int sumRange(int i, int j) {
        // 求某一段区域和 [i, j] 的模板是 sum[j] - sum[i - 1](模板部分)
        // 但由于我们源数组下标从 0 开始,因此要在模板的基础上进行 + 1
        i++; j++;
        return sum[j] - sum[i - 1];
    }
}
```
C++ 代码:
```C++
class NumArray {
public:
    vector<int> sum;
    NumArray(std::vector<int>& nums) {
        int n = nums.size();
        // 前缀和数组下标从 1 开始,因此设定长度为 n + 1(模板部分)
        sum.resize(n + 1, 0);
        // 预处理除前缀和数组(模板部分)
        for (int i = 1; i <= n; i++) sum[i] = sum[i - 1] + nums[i - 1];
    }
    int sumRange(int i, int j) {
        // 求某一段区域和 [i, j] 的模板是 sum[j] - sum[i - 1](模板部分)
        // 但由于我们源数组下标从 0 开始,因此要在模板的基础上进行 + 1
        i++; j++;
        return sum[j] - sum[i - 1];
    }
};
```
Python 代码:
```Python
class NumArray:
    def __init__(self, nums: List[int]):
        n = len(nums)
        # 前缀和数组下标从 1 开始,因此设定长度为 n + 1(模板部分)
        self.sum = [0] * (n + 1)
        # 预处理除前缀和数组(模板部分)
        for i in range(1, n + 1):
            self.sum[i] = self.sum[i - 1] + nums[i - 1]

    def sumRange(self, i: int, j: int) -> int:
        # 求某一段区域和 [i, j] 的模板是 sum[j] - sum[i - 1](模板部分)
        # 但由于我们源数组下标从 0 开始,因此要在模板的基础上进行 + 1
        i, j = i + 1, j + 1
        return self.sum[j] - self.sum[i - 1]
```
TypeScript 代码:
```TypeScript
class NumArray {
    private sum: number[];
    constructor(nums: number[]) {
        const n: number = nums.length;
        // 前缀和数组下标从 1 开始,因此设定长度为 n + 1(模板部分)
        this.sum = new Array(n + 1).fill(0);
        // 预处理除前缀和数组(模板部分)
        for (let i = 1; i <= n; i++) this.sum[i] = this.sum[i - 1] + nums[i - 1];
    }
    sumRange(i: number, j: number): number {
        // 求某一段区域和 [i, j] 的模板是 sum[j] - sum[i - 1](模板部分)
        // 但由于我们源数组下标从 0 开始,因此要在模板的基础上进行 + 1
        i++; j++;
        return this.sum[j] - this.sum[i - 1];
    }
}
```
* 时间复杂度:预处理前缀和数组需要对原数组进行线性扫描,复杂度为 $O(n)$,计算结果复杂度为 $O(1)$。整体复杂度为 $O(n)$
* 空间复杂度:$O(n)$

---

### 一维前缀和模板

我们再来整理下一维「前缀和」的模板代码(`Java` 为例):

```java
// 预处理前缀和数组
{
    sum = new int[n + 1];
    for (int i = 1; i <= n; i++) sum[i] = sum[i - 1] + nums[i - 1];
}

// 计算 [i, j] 结果
{
    i++; j++;
    ans = sum[j] - sum[i - 1];
}
```

---

### 总结

最后我们看看「前缀和」与其他知识点的联系。

为啥「前缀和」能大幅度的降低区间求和的复杂度?其实本质是利用数学进行求值:某一段的区间和 = 起点到区间右端点的和(含右端点)- 起点到区间左端点的和(不含左端点)

而求解前缀和数组的过程,则是基于动态规划思想:由于前缀和的每一位都是求「当前位置到起点位置区间的和」。因此当求解某一位的前缀和时,需要「前一位置的前缀和」和「当前位置的原数组值」(而与前一位置的前缀和是如何计算出来无关)。其过程类似于 dp 入门题 [509. 斐波那契数](https://leetcode-cn.com/problems/fibonacci-number/) 的求解过程。

### 最后

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

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

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

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


LeetCode/301-310/304. 二维区域和检索 - 矩阵不可变（中等）.md

304. 二维区域和检索 - 矩阵不可变

Question:
给定一个二维矩阵,计算其子矩形范围内元素的总和,该子矩阵的左上角为 (row1, col1) ,右下角为 (row2, col2) 。

上图子矩阵左上角 (row1, col1) = (2, 1) ,右下角(row2, col2) = (4, 3),该子矩形内元素的总和为 8。

示例:
```
给定 matrix = [
  [3, 0, 1, 4, 2],
  [5, 6, 3, 2, 1],
  [1, 2, 0, 1, 5],
  [4, 1, 0, 1, 7],
  [1, 0, 3, 0, 5]
]

sumRegion(2, 1, 4, 3) -> 8
sumRegion(1, 1, 2, 2) -> 11
sumRegion(1, 2, 2, 4) -> 12
```

提示:
* 你可以假设矩阵不可变。
* 会多次调用 sumRegion 方法。
* 你可以假设 row1 ≤ row2 且 col1 ≤ col2 。

Solution:
### 前缀和解法(二维)

真就是今天的「每日一题」呗 ~

这是一道「二维前缀和」的裸题。

「二维前缀和」解决的是二维矩阵中的矩形区域求和问题。

**二维前缀和数组中的每一个格子记录的是「以当前位置为区域的右下角(区域左上角恒定为原数组的左上角)的区域和」**

贴一张官解示意图,我觉得很清晰:

**如果觉得不清晰,请将将 f[i][j] 理解成是以 (i, j) 为右下角,(0, 0) 为左上角的区域和。**

因此当我们要求 (x1, y1) 作为左上角,(x2, y2) 作为右下角 的区域和的时候,可以直接利用前缀和数组快速求解:

`sum[x2][y2] - sum[x1 - 1][y2] - sum[x2][y1 - 1] + sum[x1 - 1][y1 - 1]`

再贴官解示意图(画图困难户在学做图了,别骂😢):

(x1, y1) == (row1, col1),(x2, y2) == (row2, col2)

代码:
```Java
class NumMatrix {
    int[][] sum;
    public NumMatrix(int[][] matrix) {
        int n = matrix.length, m = n == 0 ? 0 : matrix[0].length;
        // 与「一维前缀和」一样,前缀和数组下标从 1 开始,因此设定矩阵形状为 [n + 1][m + 1](模板部分)
        sum = new int[n + 1][m + 1];
        // 预处理除前缀和数组(模板部分)
        for (int i = 1; i <= n; i++) {
            for (int j = 1; j <= m; j++) {
                sum[i][j] = sum[i - 1][j] + sum[i][j - 1] - sum[i - 1][j - 1] + matrix[i - 1][j - 1];
            }
        }
    }

    public int sumRegion(int x1, int y1, int x2, int y2) {
        // 求某一段区域和 [i, j] 的模板是 sum[x2][y2] - sum[x1 - 1][y2] - sum[x2][y1 - 1] + sum[x1 - 1][y1 - 1];(模板部分)
        // 但由于我们源数组下标从 0 开始,因此要在模板的基础上进行 + 1
        x1++; y1++; x2++; y2++;
        return sum[x2][y2] - sum[x1 - 1][y2] - sum[x2][y1 - 1] + sum[x1 - 1][y1 - 1];
    }
}
```
* 时间复杂度:预处理前缀和数组需要对原数组进行线性扫描,复杂度为 $O(n * m)$,计算结果复杂度为 $O(1)$。整体复杂度为 $O(n * m)$
* 空间复杂度:$O(n * m)$

***

### 二维前缀和模板【重点】

下面重点分享下前缀和模板该如何记忆,其实很多模板都可以按照类似方式进行记忆。

虽然「二维前缀和」的模板相比「一维前缀和」的模板要长,但是逻辑是清晰的,可以现场推导。

当然也可以**在理解的基础上**,使用以下方式进行记忆:

```Java
// 预处理前缀和数组
{
    sum = new int[n + 1][m + 1];
    for (int i = 1; i <= n; i++) {
        for (int j = 1; j <= m; j++) {
            // 当前格子(和) = 上方的格子(和) + 左边的格子(和) - 左上角的格子(和) + 当前格子(值)【和是指对应的前缀和,值是指原数组中的值】
            sum[i][j] = sum[i - 1][j] + sum[i][j - 1] - sum[i - 1][j - 1] + matrix[i - 1][j - 1];
        }
    }
}

// 首先我们要令左上角为 (x1, y1) 右下角为 (x2, y2)
// 计算 (x1, y1, x2, y2) 的结果
{
    // 前缀和是从 1 开始,原数组是从 0 开始,上来先将原数组坐标全部 +1,转换为前缀和坐标
    x1++; y1++; x2++; y2++;
    // 记作 22 - 12 - 21 + 11,然后 不减,减第一位,减第二位,减两位
    // 也可以记作 22 - 12(x - 1) - 21(y - 1) + 11(x y 都 - 1)
    ans = sum[x2][y2] - sum[x1 - 1][y2] - sum[x2][y1 - 1] + sum[x1 - 1][y1 - 1];
}
```

### 最后

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

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

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

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


Question:
累加数 是一个字符串,组成它的数字可以形成累加序列。

一个有效的 累加序列 必须 至少 包含 `3` 个数。除了最开始的两个数以外,字符串中的其他数都等于它之前两个数相加的和。

给你一个只包含数字 `'0'-'9'` 的字符串,编写一个算法来判断给定输入是否是 累加数 。如果是,返回 `true` ;否则,返回 `false` 。

说明:累加序列里的数 不会 以 `0` 开头,所以不会出现 `1`, `2`, `03` 或者 `1`, `02`, `3` 的情况。

示例 1:
```
输入:"112358"

输出:true

解释:累加序列为: 1, 1, 2, 3, 5, 8 。1 + 1 = 2, 1 + 2 = 3, 2 + 3 = 5, 3 + 5 = 8
```
示例 2:
```
输入:"199100199"

输出:true

解释:累加序列为: 1, 99, 100, 199。1 + 99 = 100, 99 + 100 = 199
```

提示:
* $1 <= num.length <= 35$
* `num` 仅由数字(`0` - `9`)组成

进阶:你计划如何处理由过大的整数输入导致的溢出?

Solution:
### 回溯 + 高精度加法

给定的 $nums$ 的长度只有 $35$,且要求序列的第三个数开始由前两个数相加而来。

容易想到通过 `DFS` 爆搜每个数的分割点,同时利用累加数的特性(第三个数起,每个数均由为前两数之和)进行剪枝。

具体的,我们可以实现一个 `boolean dfs(int u)` 函数,入参为当前决策到 $num$ 的哪一位,返回值为决策结果(序列)是否为累加数序列,爆搜过程中的分割数序列存放到 $list$ 中。

由于是 **从位置 $u$ 作为开始位置决策如何分割出当前数 $x$,我们可以枚举当前数的结束位置,范围为 $[u, n - 1]$,但需要注意分割数不能包含前导零,即如果 $num[u] = 0$,则当前数只能为 $0$**。

同时,一个合法的分割数必然满足「其值大小为前两数之和」,因此当前数 $x$ 能够被添加到 $list$ 的充要条件为:

1. $list$ 长度不足 $2$,即 $x$ 为序列中的前两数,不存在值大小的约束问题,$x$ 可以被直接到 $list$ 并继续爆搜;
2. $list$ 长度大于等于 $2$,即 $x$ 需要满足「其值大小为前两数之和」要求,以此条件作为剪枝,满足要求的 $x$ 才能追加到 $list$ 中并继续爆搜。

最后,在整个 `DFS` 过程中我们需要监测「当前数」与「前两数之和」是否相等,而分割数长度最大为 $35$,存在溢出风险,我们需要实现「高精度加法」,实现一个 `check` 函数,用于检查 `a + b` 是否为 `c`,其中 `a`、`b` 和 `c` 均为使用「逆序」存储数值的数组(最高位对应个位,举个 🌰,$a = 35$,则有 `[5, 3]`)。

若爆搜过程能顺利结束(得到长度至少为 $3$ 的序列),则说明能够拆分出累加数序列,返回 `True`,否则返回 `False`。

至此,我们解决了本题,并通过引入「高精度」来回答了「进阶」部分的问题。

代码:
```Java
class Solution {
    String num;
    int n;
    List<List<Integer>> list = new ArrayList<>();
    public boolean isAdditiveNumber(String _num) {
        num = _num;
        n = num.length();
        return dfs(0);
    }
    boolean dfs(int u) {
        int m = list.size();
        if (u == n) return m >= 3;
        int max = num.charAt(u) == '0' ? u + 1 : n;
        List<Integer> cur = new ArrayList<>();
        for (int i = u; i < max; i++) {
            cur.add(0, num.charAt(i) - '0');
            if (m < 2 || check(list.get(m - 2), list.get(m - 1), cur)) {
                list.add(cur);
                if (dfs(i + 1)) return true;
                list.remove(list.size() - 1);
            }
        }
        return false;
    }
    boolean check(List<Integer> a, List<Integer> b, List<Integer> c) {
        List<Integer> ans = new ArrayList<>();
        int t = 0;
        for (int i = 0; i < a.size() || i < b.size(); i++) {
            if (i < a.size()) t += a.get(i);
            if (i < b.size()) t += b.get(i);
            ans.add(t % 10);
            t /= 10;
        }
        if (t > 0) ans.add(t);
        boolean ok = c.size() == ans.size();
        for (int i = 0; i < c.size() && ok; i++) {
            if (c.get(i) != ans.get(i)) ok = false;
        }
        return ok;
    }
}
```
* 时间复杂度:爆搜的复杂度为指数级别,且存在剪枝操作,分析时空复杂度意义不大
* 空间复杂度:爆搜的复杂度为指数级别,且存在剪枝操作,分析时空复杂度意义不大

### 最后

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

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

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

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


LeetCode/301-310/307. 区域和检索 - 数组可修改（中等）.md

307. 区域和检索 - 数组可修改

Question:
给你一个数组 `nums`,请你完成两类查询,其中一类查询要求更新数组下标对应的值,另一类查询要求返回数组中某个范围内元素的总和。

实现 `NumArray` 类:
* `NumArray(int[] nums)` 用整数数组 `nums` 初始化对象
* `void update(int index, int val)` 将 `nums[index]` 的值更新为 `val`
* `int sumRange(int left, int right)` 返回子数组 `nums[left, right]` 的总和(即,`nums[left] + nums[left + 1], ..., nums[right]`)

示例:
```
输入:
["NumArray", "sumRange", "update", "sumRange"]
[[[1, 3, 5]], [0, 2], [1, 2], [0, 2]]

输出:
[null, 9, null, 8]

解释:
NumArray numArray = new NumArray([1, 3, 5]);
numArray.sumRange(0, 2); // 返回 9 ,sum([1,3,5]) = 9
numArray.update(1, 2);   // nums = [1,2,5]
numArray.sumRange(0, 2); // 返回 8 ,sum([1,2,5]) = 8
```

提示:
* $1 <= nums.length <= 3 * 10^4$
* $-100 <= nums[i] <= 100$
* $0 <= index < nums.length$
* $-100 <= val <= 100$
* $0 <= left <= right < nums.length$
* 最多调用 $3 * 10^4$ 次 `update` 和 `sumRange` 方法

Solution:
### 解题思路

这是一道很经典的题目,通常还能拓展出一大类问题。

针对不同的题目,我们有不同的方案可以选择(假设我们有一个数组):

1. 数组不变,求区间和:「前缀和」、「树状数组」、「线段树」
2. 多次修改某个数(单点),求区间和:「树状数组」、「线段树」
3. 多次修改某个区间,输出最终结果:「差分」
4. 多次修改某个区间,求区间和:「线段树」、「树状数组」(看修改区间范围大小)
5. 多次将某个区间变成同一个数,求区间和:「线段树」、「树状数组」(看修改区间范围大小)

这样看来,「线段树」能解决的问题是最多的,那我们是不是无论什么情况都写「线段树」呢?

答案并不是,而且恰好相反,只有在我们遇到第 4/5 类问题,不得不写「线段树」的时候,我们才考虑线段树。

因为「线段树」代码很长,而且常数很大,实际表现不算很好。我们只有在不得不用的时候才考虑「线段树」。

总结一下,我们应该按这样的优先级进行考虑:

1. 简单求区间和,用「前缀和」
2. 多次将某个区间变成同一个数,用「线段树」
3. 其他情况,用「树状数组」

---

### 树状数组

本题只涉及「单点修改」和「区间求和」,属于「树状数组」的经典应用。

「树状数组」本身是一个很简单的数据结构,但是要搞懂其为什么可以这样「查询」&「更新」还是比较困难的(特别是为什么可以这样更新),往往需要从「二进制分解」进行出发理解。

树状数组涉及的操作有两个,复杂度均为 $O(\log{n})$:

* `void add(int x, int u)`:含义为在 $x$ 的位置增加 $u$(注意位置下标从 $1$ 开始);
* `int query(int x)`:含义为查询从 $[1, x]$ 区间的和为多少(配合容斥原理,可实现任意区间查询)。

代码:
```Java
class NumArray {
    int[] tr;
    int lowbit(int x) {
        return x & -x;
    }
    void add(int x, int u) {
        for (int i = x; i <= n; i += lowbit(i)) tr[i] += u;
    }
    int query(int x) {
        int ans = 0;
        for (int i = x; i > 0; i -= lowbit(i)) ans += tr[i];
        return ans;
    }

    int[] nums;
    int n;
    public NumArray(int[] _nums) {
        nums = _nums;
        n = nums.length;
        tr = new int[n + 10];
        for (int i = 0; i < n; i++) add(i + 1, nums[i]);
    }
    public void update(int index, int val) {
        add(index + 1, val - nums[index]);
        nums[index] = val;
    }
    public int sumRange(int left, int right) {
        return query(right + 1) - query(left);
    }
}
```
* 时间复杂度:插入和查询复杂度均为 $O(\log{n})$
* 空间复杂度:$O(n)$

---

### 线段树

相比「树状数组」,另外一个更为进阶且通用的做法是使用「线段树」。

线段树的所有操作同样为 $O(\log{n}),$由于本题不涉及「区间修改」操作,因此我们的线段树只需要实现 `pushup` 操作(子节点往上更新父节点),而不需要实现用于懒标记的 `pushdown` 操作(父节点往下传递「更新」的操作)。

关于线段树设计的几种操作:

* `void build(int u, int l, int r)`:含义为从编号为 $u$ 的节点开始,构造范围为 $[l,r]$ 的树节点;
* `void update(int u, int x, int v)`:含义为从编号为 $u$ 的节点开始,在 $x$ 位置增加 $v$;
    * 更具一般性(涉及区间修改)的操作应该为 `void update(int u, int l, int r, int v)`,代表在 $[l, r]$ 范围增加 $v$;
* `int query(int u, int l, int r)`:含义为从编号为 $u$ 的节点开始,查询 $[l, r]$ 区间和为多少。

> 注意:对于编号为 `u` 的节点而言,其左子节点的编号为 `u << 1`,其右节点的编号为 `u << 1 | 1`。

代码(考虑为线段树增加 `static` 优化的代码见 $P2$,样例个数较少,优化不明显):
```Java
class NumArray {
    Node[] tr;
    class Node {
        int l, r, v;
        Node(int _l, int _r) {
            l = _l; r = _r;
        }
    }
    void build(int u, int l, int r) {
        tr[u] = new Node(l, r);
        if (l == r) return;
        int mid = l + r >> 1;
        build(u << 1, l, mid);
        build(u << 1 | 1, mid + 1, r);
    }
    void update(int u, int x, int v) {
        if (tr[u].l == x && tr[u].r == x) {
            tr[u].v += v;
            return ;
        }
        int mid = tr[u].l + tr[u].r >> 1;
        if (x <= mid) update(u << 1, x, v);
        else update(u << 1 | 1, x, v);
        pushup(u);
    }
    int query(int u, int l, int r) {
        if (l <= tr[u].l && tr[u].r <= r) return tr[u].v;
        int mid = tr[u].l + tr[u].r >> 1;
        int ans = 0;
        if (l <= mid) ans += query(u << 1, l, r);
        if (r > mid) ans += query(u << 1 | 1, l, r);
        return ans;
    }
    void pushup(int u) {
        tr[u].v = tr[u << 1].v + tr[u << 1 | 1].v;
    }

    int[] nums;
    public NumArray(int[] _nums) {
        nums = _nums;
        int n = nums.length;
        tr = new Node[n * 4];
        build(1, 1, n);
        for (int i = 0; i < n; i++) update(1, i + 1, nums[i]);
    }
    public void update(int index, int val) {
        update(1, index + 1, val - nums[index]);
        nums[index] = val;
    }
    public int sumRange(int left, int right) {
        return query(1, left + 1, right + 1);
    }
}
```

-

```Java
class NumArray {
    static int N = 30010;
    static Node[] tr = new Node[N * 4];
    class Node {
        int l, r, v;
        Node(int _l, int _r) {
            l = _l; r = _r;
        }
    }
    void build(int u, int l, int r) {
        if (tr[u] == null) {
            tr[u] = new Node(l, r);
        } else {
            tr[u].l = l; tr[u].r = r; tr[u].v = 0;
        }
        if (l == r) return ;
        int mid = l + r >> 1;
        build(u << 1, l, mid);
        build(u << 1 | 1, mid + 1, r);
    }
    void update(int u, int x, int v) {
        if (tr[u].l == x && tr[u].r == x) {
            tr[u].v += v;
            return ;
        }
        int mid = tr[u].l + tr[u].r >> 1;
        if (x <= mid) update(u << 1, x, v);
        else update(u << 1 | 1, x, v);
        pushup(u);
    }
    int query(int u, int l, int r) {
        if (l <= tr[u].l && tr[u].r <= r) return tr[u].v;
        int mid = tr[u].l + tr[u].r >> 1;
        int ans = 0;
        if (l <= mid) ans += query(u << 1, l, r);
        if (r > mid) ans += query(u << 1 | 1, l, r);
        return ans;
    }
    void pushup(int u) {
        tr[u].v = tr[u << 1].v + tr[u << 1 | 1].v;
    }

    int[] nums;
    public NumArray(int[] _nums) {
        nums = _nums;
        int n = nums.length;
        build(1, 1, n);
        for (int i = 0; i < n; i++) update(1, i + 1, nums[i]);
    }
    public void update(int index, int val) {
        update(1, index + 1, val - nums[index]);
        nums[index] = val;
    }
    public int sumRange(int left, int right) {
        return query(1, left + 1, right + 1);
    }
}
```
* 时间复杂度:插入和查询复杂度均为 $O(\log{n})$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


Question:
树是一个无向图,其中任何两个顶点只通过一条路径连接。 换句话说,一个任何没有简单环路的连通图都是一棵树。

给你一棵包含 $n$ 个节点的树,标记为 $0$ 到 $n - 1$ 。给定数字 $n$ 和一个有 $n - 1$ 条无向边的 `edges` 列表(每一个边都是一对标签),其中 $edges[i] = [a_i, b_i]$ 表示树中节点 $a_i$ 和 $b_i$ 之间存在一条无向边。

可选择树中任何一个节点作为根。当选择节点 $x$ 作为根节点时,设结果树的高度为 $h$ 。在所有可能的树中,具有最小高度的树(即,`min(h)`)被称为 最小高度树 。

请你找到所有的 **最小高度树** 并按 **任意顺序** 返回它们的根节点标签列表。

树的 **高度** 是指根节点和叶子节点之间最长向下路径上边的数量。

示例 1:

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

输出:[1]

解释:如图所示,当根是标签为 1 的节点时,树的高度是 1 ,这是唯一的最小高度树。
```
示例 2:

```
输入:n = 6, edges = [[3,0],[3,1],[3,2],[3,4],[5,4]]

输出:[3,4]
```

提示:
* $1 <= n <= 2 \times 10^4$
* $edges.length = n - 1$
* $0 <= ai, bi < n$
* $ai != bi$
* 所有 $(ai, bi)$ 互不相同
* 给定的输入保证是一棵树,并且不会有重复的边

Solution:
### 树形 DP

这是一道树形 DP 模板题。

当确定以某个点为根节点时,整棵树的形态唯一固定,不妨以编号为 $0$ 的节点作为根节点进行分析。

假设当前处理到的节点为 `u`,其是从父节点 `fa` 遍历而来,且将要遍历的子节点为 `j`。

即树的形态如图所示(一些可能有的出边用虚线表示):

**树形 DP 问题通常将问题根据「方向」进行划分**。

对于当前处理到的节点 `u` 而言,我们根据是否考虑「从 `fa` 到 `u` 的出边」将其分为「往上」和「往下」两个方向。

假设我们可以通过 `DFS` 预处理出 $f$ 数组和 $g$ 数组:

* $f[u]$ 代表在以 $0$ 号点为根节点的树中,以 `u` 节点为子树根节点时,往下的最大高度
* $g[u]$ 代表在以 $0$ 号点为根节点的树中,以 `u` 节点为子节点时,往上的最大高度

那么最终以 `u` 为根节点的最大高度为 $\max(f[u], g[u])$。

$f[u]$ 只需要简单的 `DFS` 即可处理出来。对于 $g[u]$ 而言,其同样包含「往上」和「往下」两部分:

* 对于经过 `fa` 后接着往上的部分有 $g[fa] + 1$
* 对于经过 `fa` 后转而往下的部分,我们需要考虑「`fa` 节点往下的最大值 $f[fa]$」是否由 `u` 节点参与而来进行分情况讨论:
  * 如果 $f[fa]$ 本身不由 `u` 参与,那么 $g[u]$ 应当是 `fa` 节点往下的最大值 $+1$ 而来($+1$ 代表加上 `fa` 到 `u` 的边)
  * 如果本身 `fa` 往下的最大值由 `u` 节点参与,此时应当使用 `fa` 往下的次大值 $+1$ 来更新 $g[u]$

因此我们需要对 $f$ 数组进行拆分,拆分为记录「最大值的 $f1$ 数组」和记录「次大值的 $f2$ 数组(注意这里的次大值是非严格的次大值)」,同时使用 $p$ 数组记录下取得 $f1[u]$ 时 `u` 的子节点 `j` 为何值。

实现上,在处理「往上」方向的 `DFS` 时,为避免对 `fa` 节点为空的处理,我们可以将「用 `fa` 来更新 `u`」调整为「用 `u` 来更新 `j`」。

代码:
```Java
class Solution {
    int N = 20010, M = N * 2, idx = 0;
    int[] he = new int[N], e = new int[M], ne = new int[M];
    int[] f1 = new int[N], f2 = new int[N], g = new int[N], p = new int[N];
    void add(int a, int b) {
        e[idx] = b;
        ne[idx] = he[a];
        he[a] = idx++;
    }
    public List<Integer> findMinHeightTrees(int n, int[][] edges) {
        Arrays.fill(he, -1);
        for (int[] e : edges) {
            int a = e[0], b = e[1];
            add(a, b); add(b, a);
        }
        dfs1(0, -1);
        dfs2(0, -1);
        List<Integer> ans = new ArrayList<>();
        int min = n;
        for (int i = 0; i < n; i++) {
            int cur = Math.max(f1[i], g[i]);
            if (cur < min) {
                min = cur;
                ans.clear();
                ans.add(i);
            } else if (cur == min) {
                ans.add(i);
            }
        }
        return ans;
    }
    int dfs1(int u, int fa) {
        for (int i = he[u]; i != -1; i = ne[i]) {
            int j = e[i];
            if (j == fa) continue;
            int sub = dfs1(j, u) + 1;
            if (sub > f1[u]) {
                f2[u] = f1[u];
                f1[u] = sub;
                p[u] = j;
            } else if (sub > f2[u]) {
                f2[u] = sub;
            }
        }
        return f1[u];
    }
    void dfs2(int u, int fa) {
        for (int i = he[u]; i != -1; i = ne[i]) {
            int j = e[i];
            if (j == fa) continue;
            if (p[u] != j) g[j] = Math.max(g[j], f1[u] + 1);
            else g[j] = Math.max(g[j], f2[u] + 1);
            g[j] = Math.max(g[j], g[u] + 1);
            dfs2(j, u);
        }
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n)$

---

### 补充

可能会初次接触「树形 DP」的同学不太理解,这里再补充说明一下。

归根结底,以  `u` 为根节点的最大深度,必然是下面三种情况之一(往下、往上 和 往上再往下)。

其中对 $f$ 数组的拆分(变为 $f1$ 和 $f2$)以及记录取得 $f1$ 对应的子节点 $p[i]$,目的都是为了能够正确统计「往上再往下」的情况(统计该情况时,不能考虑从 `fa` 经过 `u` 的路径,因此需要记录一个非严格的次大值 $f2$)。

### 最后

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

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

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

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


Question:
实现获**下一个排列**的函数,算法需要将给定数字序列重新排列成字典序中下一个更大的排列。

如果不存在下一个更大的排列,则将数字重新排列成最小的排列(即升序排列)。

必须「原地」修改,只允许使用额外常数空间。

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

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

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

输出:[1,5,1]
```
示例 4:
```
输入:nums = [1]

输出:[1]
```

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

Solution:
### 模拟

找下一个排列的数,大家可以先想想大脑来是怎么完成这个找数的过程的。

**我们会尽可能的将低位的数字变大,这样才符合「下一个排列」的定义。**

也就是从低位往高位检查,观察某一位在「下一个排列」中是否可以被更大的数代替。

那么如何判断某一位能够被更大的数代替呢?

**其实就是将 `k` 位到低位的所有数作为候选,判断是否有更大的数可以填入 `k` 位中。**

假设当前我们检查到 `k` 位,要分析第 `k` 位在「下一个排列」中是否能被更大的数代替。

我们会先假定高位到 `k` 位的数不变,在 `k` 位到低位中是否有比 `k` 位上的数更大的数,如果有说明 `k` 在「下一个排列」中变大。

换句话说,我们要找的第 `k` 位其实就是从低位到高位的第一个下降的数。

...

为了更好理解,我们结合样例来分析,假设样例为 `[1,3,5,4,1]`:

1. **从后往前找,找到第一个下降的位置,记为 `k`。注意`k` 以后的位置是降序的。** 在样例中就是找到 3

2. **从 k 往后找,找到最小的比 k 要大的数。** 找到 4

3. **将两者交换。注意此时 `k` 以后的位置仍然是降序的。**

4. **直接将 k 以后的部分翻转(变为升序)。**

注意:如果在步骤 1 中找到头部还没找到,说明该序列已经是字典序最大的排列。按照题意,我们要将数组重新排列成最小的排列。

代码:
```Java
class Solution {
    public void nextPermutation(int[] nums) {
        int n = nums.length, k = n - 1;
        while (k - 1 >= 0 && nums[k - 1] >= nums[k]) k--;
        if (k == 0) {
            reverse(nums, 0, n - 1);
        } else {
            int u = k;
            while (u + 1 < n && nums[u + 1] > nums[k - 1]) u++;
            swap(nums, k - 1, u);
            reverse(nums, k, n - 1);
        }
    }
    void reverse(int[] nums, int a, int b) {
        int l = a, r = b;
        while (l < r) swap(nums, l++, r--);
    }
    void swap(int[] nums, int a, int b) {
        int c = nums[a];
        nums[a] = nums[b];
        nums[b] = c;
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


Question:
给你一个只包含 `'('` 和 `')'` 的字符串,找出最长有效(格式正确且连续)括号子串的长度。

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

输出:2

解释:最长有效括号子串是 "()"
```
示例 2:
```
输入:s = ")()())"

输出:4

解释:最长有效括号子串是 "()()"
```
示例 3:
```
输入:s = ""

输出:0
```

提示:
* $0 <= s.length <= 3 \times 10^4$
* $s[i]$ 为 `'('` 或 `')'`

Solution:
### 栈

从前往后扫描字符串 `s`。

用 `i` 来记录当前遍历到的位置,用 `j` 来记录最近的最长有效括号的开始位置的「前一个位置」。

只对 `'('` 进行入栈(入栈的是对应的下标),当遍历到 `')'` 的时候,由于栈中只有 `'('`,所以可以直接弹出一个 `'('` 与之匹配(如果有的话)。

再检查栈中是否还有 `'('`,如果有使用栈顶元素的下标来计算长度,否则使用 `j` 下标来计算长度。

该做法的本质:**栈里只存放待匹配的 `(` 符号,虽然这些 `(` 在原字符串中的下标不一定连续,但 `(` 之间一定为有效括号,因此可以使用栈顶元素作为有效括号的左边界计算长度**。

举个 🌰,对于 `s` = `((())((` 的情况,当整个字符串处理完,栈内剩下的不是所有的 `(` 符号(不是 $5$ 个),而是剩余待匹配的 `(` 符号(而是 $3$ 个,即从左往右第 $1$、$4$ 和 $5$ 个 `(`),虽然第 $1$ 和第 $4$ 个 `(` 符号在 `s` 中不连续,但其之间必然是有效括号。

Java 代码:
```Java
class Solution {
    public int longestValidParentheses(String s) {
        Deque<Integer> d = new ArrayDeque<>();
        int n = s.length(), ans = 0;
        for (int i = 0, j = -1; i < n; i++) {
            if (s.charAt(i) == '(') {
                d.addLast(i);
            } else {
                if (!d.isEmpty()) {
                    d.pollLast();
                    int top = j;
                    if (!d.isEmpty()) top = d.peekLast();
                    ans = Math.max(ans, i - top);
                } else {
                    j = i;
                }
            }
        }
        return ans;
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    int longestValidParentheses(string s) {
        stack<int> d;
        int n = s.length(), ans = 0;
        for (int i = 0, j = -1; i < n; i++) {
            if (s[i] == '(') {
                d.push(i);
            } else {
                if (!d.empty()) {
                    d.pop();
                    int top = j;
                    if (!d.empty()) top = d.top();
                    ans = max(ans, i - top);
                } else {
                    j = i;
                }
            }
        }
        return ans;
    }
};
```
Python 代码:
```Python
from collections import deque

class Solution:
    def longestValidParentheses(self, s: str) -> int:
        d = deque()
        n, ans, j = len(s), 0, -1
        for i in range(n):
            if s[i] == '(':
                d.append(i)
            else:
                if d:
                    d.pop()
                    top = j
                    if d: top = d[-1]
                    ans = max(ans, i - top)
                else:
                    j = i
        return ans
```
TypeScript 代码:
```TypeScript
function longestValidParentheses(s: string): number {
    const d: number[] = [];
    let n = s.length, ans = 0;
    for (let i = 0, j = -1; i < n; i++) {
        if (s[i] === '(') {
            d.push(i);
        } else {
            if (d.length) {
                d.pop();
                let top = j;
                if (d.length) top = d[d.length - 1]
                ans = Math.max(ans, i - top);
            } else {
                j = i;
            }
        }
    }
    return ans;
};
```
* 时间复杂度:每个字符最多进栈和出栈一次,复杂度为 $O(n)$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


33. 搜索旋转排序数组

Question:
整数数组 `nums` 按升序排列,数组中的值互不相同。

在传递给函数之前,`nums` 在预先未知的某个下标 $k$ ($0 <= k < nums.length$)上进行了**旋转**。

使数组变为 `[nums[k], nums[k+1], ..., nums[n-1], nums[0], nums[1], ..., nums[k-1]]`(下标 从 $0$ 开始计数)。

例如, `[0,1,2,4,5,6,7]` 在下标 $3$ 处经旋转后可能变为 `[4,5,6,7,0,1,2] `。

给你**旋转后**的数组 `nums` 和一个整数 `target` 。

如果 `nums` 中存在这个目标值 `target` ,则返回它的下标,否则返回 `-1` 。

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

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

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

输出:-1
```

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

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

Solution:
### 先找旋转点后二分

但凡是从有序序列中找某个数,我们第一反应应该是「二分」。

这道题是一个原本有序的数组在某个点上进行了旋转,其实就是将原本一段升序的数组分为了两段。

我们可以先找到旋转点 `idx`,然后对 `idx` 前后进行「二分」。

Java 代码:
```Java
class Solution {
    public int search(int[] nums, int target) {
        int n = nums.length, idx = -1;
        for (int i = 0; i < n - 1 && idx == -1; i++) {
            if (nums[i] > nums[i + 1]) idx = i;
        }
        int ans = find(nums, 0, idx, target);
        if (ans != -1) return ans;
        return idx + 1 < n ? find(nums, idx + 1, n - 1, target) : ans;
    }
    int find(int[] nums, int l, int r, int target) {
        while (l < r) {
            int mid = l + r >> 1;
            if (nums[mid] >= target) r = mid;
            else l = mid + 1;
        }
        return nums[l] == target ? l : -1;
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    int search(vector<int>& nums, int target) {
        int n = nums.size(), idx = -1;
        for (int i = 0; i < n - 1 && idx == -1; i++) {
            if (nums[i] > nums[i + 1]) idx = i;
        }
        int ans = find(nums, 0, idx, target);
        if (ans != -1) return ans;
        return idx + 1 < n ? find(nums, idx + 1, n - 1, target) : ans;
    }
    int find(vector<int>& nums, int l, int r, int target) {
        while (l < r) {
            int mid = l + r >> 1;
            if (nums[mid] >= target) r = mid;
            else l = mid + 1;
        }
        return nums[l] == target ? l : -1;
    }
};
```
Python 代码:
```Python
class Solution:
    def search(self, nums: List[int], target: int) -> int:
        n, idx = len(nums), -1
        for i in range(0, n - 1):
            if nums[i] > nums[i + 1]:
                idx = i
                break

        def find(L, R):
            l, r = L, R
            while l < r:
                mid = l + r >> 1
                if nums[mid] >= target: r = mid
                else: l = mid + 1
            return l if nums[l] == target else -1

        ans = find(0, idx)
        if ans != -1: return ans
        return find(idx + 1, n - 1) if idx + 1 < n else ans
```
TypeScript 代码:
```TypeScript
function search(nums: number[], target: number): number {
    let n = nums.length, idx = -1;
    for (let i = 0; i < n - 1 && idx == -1; i++) {
        if (nums[i] > nums[i + 1]) idx = i;
    }
    const find = function(l: number, r: number): number {
        while (l < r) {
            let mid = l + r >> 1;
            if (nums[mid] >= target) r = mid;
            else l = mid + 1;
        }
        return nums[l] == target ? l : -1;
    }
    let ans = find(0, idx);
    if (ans!= -1) return ans;
    return idx + 1 < n ? find(idx + 1, n - 1) : ans;
};
```
* 时间复杂度:先对数组进行一次遍历,找到 `idx`,复杂度为 $O(n)$,对 `idx` 前后进行二分查找,复杂度为 $O(\log{n})$。整体为 $O(n)$
* 空间复杂度:$O(1)$

---

### 两次二分

不难发现,虽然在朴素解法中我们应用了「二分」查找。

但理论复杂度为 $O(n)$,实际复杂度也远达不到 $O(\log{n})$,执行效率取决于旋转点 `idx` 所在数组的下标位置。

那么我们如何实现 $O(\log{n})$ 的解法呢?

这道题其实是要我们明确「二分」的本质是什么。

「二分」不是单纯指从有序数组中快速找某个数,这只是「二分」的一个应用。

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

经过旋转的数组,显然前半段满足 `>= nums[0]`,而后半段不满足 `>= nums[0]`。我们可以以此作为依据,通过「二分」找到旋转点。

找到旋转点之后,再通过比较 `target` 和 `nums[0]` 的大小,确定 `target` 落在旋转点的左边还是右边。

Java 代码:
```Java
class Solution {
    public int search(int[] nums, int target) {
        int n = nums.length;

        // 第一次「二分」:从中间开始找,找到满足 >=nums[0] 的分割点(旋转点)
        int l = 0, r = n - 1;
        while (l < r) {
            int mid = l + r + 1 >> 1;
            if (nums[mid] >= nums[0]) l = mid;
            else r = mid - 1;
        }

        // 第二次「二分」:通过和 nums[0] 进行比较,得知 target 是在旋转点的左边还是右边
        if (target >= nums[0]) {
            l = 0;
        } else {
            l = l + 1; r = n - 1;
        }
        while (l < r) {
            int mid = l + r >> 1;
            if (nums[mid] >= target) r = mid;
            else l = mid + 1;
        }

        return nums[r] == target ? r : -1;
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    int search(vector<int>& nums, int target) {
        int n = nums.size();

        // 第一次「二分」:从中间开始找,找到满足 >=nums[0] 的分割点(旋转点)
        int l = 0, r = n - 1;
        while (l < r) {
            int mid = l + r + 1 >> 1;
            if (nums[mid] >= nums[0]) l = mid;
            else r = mid - 1;
        }

        // 第二次「二分」:通过和 nums[0] 进行比较,得知 target 是在旋转点的左边还是右边
        if (target >= nums[0]) {
            l = 0;
        } else {
            l = l + 1; r = n - 1;
        }
        while (l < r) {
            int mid = l + r >> 1;
            if (nums[mid] >= target) r = mid;
            else l = mid + 1;
        }

        return nums[r] == target ? r : -1;
    }
};
```
Python 代码:
```Python
class Solution:
    def search(self, nums: List[int], target: int) -> int:
        n = len(nums)

        # 第一次「二分」:从中间开始找,找到满足 >=nums[0] 的分割点(旋转点)
        l, r = 0, n - 1
        while l < r:
            mid = l + r + 1 >> 1
            if nums[mid] >= nums[0]: l = mid
            else: r = mid - 1

        # 第二次「二分」:通过和 nums[0] 进行比较,得知 target 是在旋转点的左边还是右边
        if target >= nums[0]:
            l = 0
        else:
            l, r = l + 1, n - 1
        while l < r:
            mid = l + r >> 1
            if nums[mid] >= target: r = mid
            else: l = mid + 1

        return r if nums[r] == target else -1
```
TypeScript 代码:
```TypeScript
function search(nums: number[], target: number): number {
    const n = nums.length;

    // 第一次「二分」:从中间开始找,找到满足 >=nums[0] 的分割点(旋转点)
    let l = 0, r = n - 1;
    while (l < r) {
        let mid = l + r + 1 >> 1;
        if (nums[mid] >= nums[0]) l = mid;
        else r = mid - 1;
    }

    // 第二次「二分」:通过和 nums[0] 进行比较,得知 target 是在旋转点的左边还是右边
    if (target >= nums[0]) {
        l = 0;
    } else {
        l = l + 1; r = n - 1;
    }
    while (l < r) {
        let mid = l + r >> 1;
        if (nums[mid] >= target) r = mid;
        else l = mid + 1;
    }

    return nums[r] == target ? r : -1;
};
```
* 时间复杂度:$O(\log{n})$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


LeetCode/31-40/34. 在排序数组中查找元素的第一个和最后一个位置（中等）.md

34. 在排序数组中查找元素的第一个和最后一个位置

Question:
给定一个按照升序排列的整数数组 `nums`,和一个目标值 `target`。

找出给定目标值在数组中的开始位置和结束位置。

如果数组中不存在目标值 `target`,返回 $[-1, -1]$。

**进阶:**

* 你可以设计并实现时间复杂度为 $O(\log{n})$ 的算法解决此问题吗?

示例 1:
```
输入:nums = [5,7,7,8,8,10], target = 8

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

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

输出:[-1,-1]
```

提示:
* $0 <= nums.length <= 10^5$
* $-10^9 <= nums[i] <= 10^9$
* `nums` 是一个非递减数组
* $-10^9 <= target <= 10^9$

Solution:
### 二分解法

这是一道「二分查找」的裸题。

「二分」有一个比较容易混淆的点是:当需要找目标值第一次出现的下标时,条件应该写成 `nums[mid] >= target` 还是 `nums[mid] <= target`。

其实有一个很好理解的方法:

**由于二分是从中间开始找起的,所以找的必然是条件区间中靠近中心的的边界值。**

文字不好理解,我们结合图片来看:

代码:
```Java
class Solution {
    public int[] searchRange(int[] nums, int t) {
        int[] ans = new int[]{-1, -1};
        int n = nums.length;
        if (n == 0) return ans;
        int l = 0, r = n - 1;
        while (l < r) {
            int mid = l + r >> 1;
            if (nums[mid] >= t) r = mid;
            else l = mid + 1;
        }
        if (nums[r] != t) return ans;
        ans[0] = r;
        l = 0; r = n - 1;
        while (l < r) {
            int mid = l + r + 1 >> 1;
            if (nums[mid] <= t) l = mid;
            else r = mid - 1;
        }
        ans[1] = r;
        return ans;
    }
}
```
* 时间复杂度:$O(\log{n})$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


Question:
给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。

如果目标值不存在于数组中,返回它将会被按顺序插入的位置。

你可以假设数组中无重复元素。

示例 1:
```
输入: [1,3,5,6], 5

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

输出: 1
```
示例 3:
```
输入: [1,3,5,6], 7

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

输出: 0
```

Solution:
### 朴素做法

一个朴素的做法,从前往后进行处理,直到遇到符合条件的位置。

代码:
```Java
class Solution {
    public int searchInsert(int[] nums, int t) {
        for (int i = 0; i < nums.length; i++) {
            if (nums[i] == t || nums[i] > t) return i;
        }
        return nums.length;
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(1)$

---

### 二分解法

利用数组本身有序,我们可以通过二分找插入位置。

具体的,通过二分找到符合 `nums[mid] >= t` 的分割点。注意二分完后需要再次检查是否位置是否符合条件,如果不符合,代表插入元素应该被添加到数组结尾。

代码:
```Java
class Solution {
    public int searchInsert(int[] nums, int t) {
        int n = nums.length;
        int l = 0, r = n - 1;
        while (l < r) {
            int mid = l + r >> 1;
            if (nums[mid] >= t) {
                r = mid;
            } else {
                l = mid + 1;
            }
        }
        return nums[r] >= t ? r : r + 1;
    }
}
```
* 时间复杂度:$O(\log{n})$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


Question:
请你判断一个 `9 x 9` 的数独是否有效。只需要 根据以下规则 ,验证已经填入的数字是否有效即可。

1. 数字 `1-9` 在每一行只能出现一次。
2. 数字 `1-9` 在每一列只能出现一次。
3. 数字 `1-9` 在每一个以粗实线分隔的 `3 x 3` 宫内只能出现一次。(请参考示例图)

数独部分空格内已填入了数字,空白格用 `'.'` 表示。

注意:
* 一个有效的数独(部分已被填充)不一定是可解的。
* 只需要根据以上规则,验证已经填入的数字是否有效即可。

示例 1:
```
输入:board =
[["5","3",".",".","7",".",".",".","."]
,["6",".",".","1","9","5",".",".","."]
,[".","9","8",".",".",".",".","6","."]
,["8",".",".",".","6",".",".",".","3"]
,["4",".",".","8",".","3",".",".","1"]
,["7",".",".",".","2",".",".",".","6"]
,[".","6",".",".",".",".","2","8","."]
,[".",".",".","4","1","9",".",".","5"]
,[".",".",".",".","8",".",".","7","9"]]

输出:true
```
示例 2:
```
输入:board =
[["8","3",".",".","7",".",".",".","."]
,["6",".",".","1","9","5",".",".","."]
,[".","9","8",".",".",".",".","6","."]
,["8",".",".",".","6",".",".",".","3"]
,["4",".",".","8",".","3",".",".","1"]
,["7",".",".",".","2",".",".",".","6"]
,[".","6",".",".",".",".","2","8","."]
,[".",".",".","4","1","9",".",".","5"]
,[".",".",".",".","8",".",".","7","9"]]

输出:false

解释:除了第一行的第一个数字从 5 改为 8 以外,空格内其他数字均与 示例1 相同。 但由于位于左上角的 3x3 宫内有两个 8 存在, 因此这个数独是无效的。
```

提示:
* $board.length == 9$
* $board[i].length == 9$
* `board[i][j]` 是一位数字或者 `'.'`

Solution:
### 哈希表

由于只要我们判断是否为有效的数独。

所以我们只需要对 `board` 中出现的数进行判断,如果 `board` 中有数违反了数独的规则,返回 `false`,否则返回 `true`。

直观上,我们很容易想到使用 **哈希表** 来记录某行/某列/某个小方块出现过哪些数字,来帮助我们判断是否符合「有效数独」的定义。

这道题唯一的难点可能是在于如何确定某个数落在哪个小方块中,我们可以去小方块进行编号:

然后推导出小方块编号和行列的关系为: $idx = \left \lfloor i / 3 \right \rfloor * 3 + \left \lfloor j / 3 \right \rfloor$。

代码:
```Java
class Solution {
    public boolean isValidSudoku(char[][] board) {
        Map<Integer, Set<Integer>> row  = new HashMap<>(), col = new HashMap<>(), area = new HashMap<>();
        for (int i = 0; i < 9; i++) {
            row.put(i, new HashSet<>());
            col.put(i, new HashSet<>());
            area.put(i, new HashSet<>());
        }
        for (int i = 0; i < 9; i++) {
            for (int j = 0; j < 9; j++) {
                char c = board[i][j];
                if (c == '.') continue;
                int u = c - '0';
                int idx = i / 3 * 3 + j / 3;
                if (row.get(i).contains(u) || col.get(j).contains(u) || area.get(idx).contains(u)) return false;
                row.get(i).add(u);
                col.get(j).add(u);
                area.get(idx).add(u);
            }
        }
        return true;
    }
}
```
* 时间复杂度:在固定 $9 \times 9$ 的问题里,计算量不随数据变化而变化。复杂度为 $O(1)$
* 空间复杂度:在固定 $9 \times 9$ 的问题里,存储空间不随数据变化而变化。复杂度为 $O(1)$

---

### 数组

大多数的哈希表计数问题,都能转换为使用数组解决。

虽然时间复杂度一样,但哈希表的更新和查询复杂度为均摊 $O(1)$,而定长数组的的更新和查询复杂度则是严格 $O(1)$。

因此从执行效率上来说,数组要比哈希表快上不少。

代码:
```Java
class Solution {
    public boolean isValidSudoku(char[][] board) {
        boolean[][] row = new boolean[10][10], col = new boolean[10][10], area = new boolean[10][10];
        for (int i = 0; i < 9; i++) {
            for (int j = 0; j < 9; j++) {
                int c = board[i][j];
                if (c == '.') continue;
                int u = c - '0';
                int idx = i / 3 * 3 + j / 3;
                if (row[i][u] || col[j][u] || area[idx][u]) return false;
                row[i][u] = col[j][u] = area[idx][u] = true;
            }
        }
        return true;
    }
}
```
* 时间复杂度:在固定 $9*9$ 的问题里,计算量不随数据变化而变化。复杂度为 $O(1)$
* 空间复杂度:在固定 $9*9$ 的问题里,存储空间不随数据变化而变化。复杂度为 $O(1)$

---

### 位运算

更进一步,我们可以使用一个 $int$ 来记录 某行/某列/某个小方块 的数值填入情况:使用从低位开始的 $[1, 9]$ 位来记录该数值是否已被填入。

例如 $(... 111000111 .)_2$ 代表数值 $[1, 3]$ 和 $[7, 9]$ 均被填入。

代码:
```Java
class Solution {
    public boolean isValidSudoku(char[][] board) {
        int[] row = new int[10], col = new int[10], area = new int[10];
        for (int i = 0; i < 9; i++) {
            for (int j = 0; j < 9; j++) {
                char c = board[i][j];
                if (c == '.') continue;
                int u = c - '0';
                int idx = i / 3 * 3 + j / 3;
                if ((((row[i] >> u) & 1) == 1) || (((col[j] >> u) & 1) == 1) || (((area[idx] >> u) & 1) == 1)) return false;
                row[i] |= (1 << u);
                col[j] |= (1 << u);
                area[idx] |= (1 << u);
            }
        }
        return true;
    }
}
```
* 时间复杂度:在固定 $9 \times 9$ 的问题里,计算量不随数据变化而变化。复杂度为 $O(1)$
* 空间复杂度:在固定 $9 \times 9$ 的问题里,存储空间不随数据变化而变化。复杂度为 $O(1)$

### 最后

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

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

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

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


Question:
编写一个程序,通过填充空格来解决数独问题。

数独的解法需 遵循如下规则:
1. 数字 `1-9` 在每一行只能出现一次。
2. 数字 `1-9` 在每一列只能出现一次。
3. 数字 `1-9` 在每一个以粗实线分隔的 `3 x 3` 宫内只能出现一次。(请参考示例图)

数独部分空格内已填入了数字,空白格用 '.' 表示。

示例:

```
输入:board =
[["5","3",".",".","7",".",".",".","."],
["6",".",".","1","9","5",".",".","."],
[".","9","8",".",".",".",".","6","."],
["8",".",".",".","6",".",".",".","3"],
["4",".",".","8",".","3",".",".","1"],
["7",".",".",".","2",".",".",".","6"],
[".","6",".",".",".",".","2","8","."],
[".",".",".","4","1","9",".",".","5"],
[".",".",".",".","8",".",".","7","9"]]

输出:[["5","3","4","6","7","8","9","1","2"],["6","7","2","1","9","5","3","4","8"],
["1","9","8","3","4","2","5","6","7"],
["8","5","9","7","6","1","4","2","3"],
["4","2","6","8","5","3","7","9","1"],
["7","1","3","9","2","4","8","5","6"],
["9","6","1","5","3","7","2","8","4"],
["2","8","7","4","1","9","6","3","5"],
["3","4","5","2","8","6","1","7","9"]]

解释:输入的数独如上图所示,唯一有效的解决方案如下所示:
```

提示:
* $board.length == 9$
* $board[i].length == 9$
* `board[i][j]` 是一位数字或者 `'.'`
* 题目数据 保证 输入数独仅有一个解

Solution:
### 回溯解法

和 N 皇后一样,是一道回溯解法裸题。

上一题「36. 有效的数独(中等)」是让我们判断给定的 `borad` 是否为有效数独。

这题让我们对给定 `board` 求数独,由于 `board` 固定是 `9 * 9` 的大小,我们可以使用回溯算法去做。

这一类题和 N 皇后一样,属于经典的回溯算法裸题。

这类题都有一个明显的特征,就是数据范围不会很大,如该题限制了范围为 `9 * 9`,而 N 皇后的 N 一般不会超过 13。

对每一个需要填入数字的位置进行填入,如果发现填入某个数会导致数独解不下去,则进行回溯。

代码:
```Java
class Solution {
    boolean[][] row = new boolean[9][9];
    boolean[][] col = new boolean[9][9];
    boolean[][][] cell = new boolean[3][3][9];
    public void solveSudoku(char[][] board) {
        for (int i = 0; i < 9; i++) {
            for (int j = 0; j < 9; j++) {
                if (board[i][j] != '.') {
                    int t = board[i][j] - '1';
                    row[i][t] = col[j][t] = cell[i / 3][j / 3][t] = true;
                }
            }
        }
        dfs(board, 0, 0);
    }
    boolean dfs(char[][] board, int x, int y) {
        if (y == 9) return dfs(board, x + 1, 0);
        if (x == 9) return true;
        if (board[x][y] != '.') return dfs(board, x, y + 1);
        for (int i = 0; i < 9; i++) {
            if (!row[x][i] && !col[y][i] && !cell[x / 3][y / 3][i]) {
                board[x][y] = (char)(i + '1');
                row[x][i] = col[y][i] = cell[x / 3][y / 3][i] = true;
                if (dfs(board, x, y + 1)) {
                    break;
                } else {
                    board[x][y] = '.';
                    row[x][i] = col[y][i] = cell[x / 3][y / 3][i] = false;
                }
            }
        }
        return board[x][y] != '.';
    }
}
```
* 时间复杂度:在固定 `9*9` 的棋盘里,具有一个枚举方案的最大值(极端情况,假设我们的棋盘刚开始是空的,这时候每一个格子都要枚举,每个格子都有可能从 $1$ 枚举到 $9$,所以枚举次数为 $9 \times 9 \times 9 = 729$),即复杂度不随数据变化而变化。复杂度为 $O(1)$
* 空间复杂度:在固定 `9*9` 的棋盘里,复杂度不随数据变化而变化。复杂度为 $O(1)$

### 最后

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

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

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

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


Question:
给定一个正整数 n ,输出外观数列的第 n 项。

「外观数列」是一个整数序列,从数字 1 开始,序列中的每一项都是对前一项的描述。

你可以将其视作是由递归公式定义的数字字符串序列:
* countAndSay(1) = "1"
* countAndSay(n) 是对 countAndSay(n-1) 的描述,然后转换成另一个数字字符串。

前五项如下:
```
1.     1
2.     11
3.     21
4.     1211
5.     111221
第一项是数字 1
描述前一项,这个数是 1 即 “ 一 个 1 ”,记作 "11"
描述前一项,这个数是 11 即 “ 二 个 1 ” ,记作 "21"
描述前一项,这个数是 21 即 “ 一 个 2 + 一 个 1 ” ,记作 "1211"
描述前一项,这个数是 1211 即 “ 一 个 1 + 一 个 2 + 二 个 1 ” ,记作 "111221"
```
要**描述**一个数字字符串,首先要将字符串分割为 最小 数量的组,每个组都由连续的最多 相同字符 组成。然后对于每个组,先描述字符的数量,然后描述字符,形成一个描述组。要将描述转换为数字字符串,先将每组中的字符数量用数字替换,再将所有描述组连接起来。

例如,数字字符串 "3322251" 的描述如下图:

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

输出:"1"

解释:这是一个基本样例。
```
示例 2:
```
输入:n = 4

输出:"1211"

解释:
countAndSay(1) = "1"
countAndSay(2) = 读 "1" = 一 个 1 = "11"
countAndSay(3) = 读 "11" = 二 个 1 = "21"
countAndSay(4) = 读 "21" = 一 个 2 + 一 个 1 = "12" + "11" = "1211"
```

提示:
* 1 <= n <= 30

Solution:
### 模拟

一个朴素的想法是:根据题意进行模拟,从起始条件 $k = 1$ 时 `ans = "1"` 出发,逐步递推到 $k = n$ 的情况,对于第 $k$ 项而言,其实就是对第 $k - 1$ 项的「连续段」的描述,而求「连续段」长度,可以使用双指针实现。

代码:
```Java
class Solution {
    public String countAndSay(int n) {
        String ans = "1";
        for (int i = 2; i <= n; i++) {
            String cur = "";
            int m = ans.length();
            for (int j = 0; j < m; ) {
                int k = j + 1;
                while (k < m && ans.charAt(j) == ans.charAt(k)) k++;
                int cnt = k - j;
                cur += cnt + "" + ans.charAt(j);
                j = k;
            }
            ans = cur;
        }
        return ans;
    }
}
```
* 时间复杂度:$O(n^2)$
* 空间复杂度:$O(n)$

---

### 打表

利用数据范围只有 $30$,我们可以使用 `static` 进行打表操作,从而节省掉不同样例之间的「重复」部分的计算量。

例如对于 $n = 5$ 和 $n = 6$ 都存在先计算前五项的公共部分,打表可以确保这部分只会被计算一次,同时能够应用到后面项中。

代码:
```Java
class Solution {
    static String[] f = new String[35];
    static {
        f[1] = "1";
        for (int i = 2; i < 35; i++) {
            String prev = f[i - 1], cur = "";
            int m = prev.length();
            for (int j = 0; j < m; ) {
                int k = j + 1;
                while (k < m && prev.charAt(j) == prev.charAt(k)) k++;
                int cnt = k - j;
                cur += cnt + "" + prev.charAt(j);
                j = k;
            }
            f[i] = cur;
        }
    }
    public String countAndSay(int n) {
        return f[n];
    }
}
```
* 时间复杂度:将打表逻辑到放本地执行,复杂度为 $O(1)$;放到 $OJ$ 执行则为 $O(n^2)$
* 空间复杂度:$O(C)$

### 最后

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

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

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

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


Question:
给定一个无重复元素的数组 `candidates` 和一个目标数 `target` ,找出 `candidates` 中所有可以使数字和为 `target` 的组合。

`candidates` 中的数字可以无限制重复被选取。

说明:
* 所有数字(包括 `target`)都是正整数。
* 解集不能包含重复的组合。

示例 1:
```
输入:candidates = [2,3,6,7], target = 7,

所求解集为:
[
  [7],
  [2,2,3]
]
```
示例 2:
```
输入:candidates = [2,3,5], target = 8,

所求解集为:
[
  [2,2,2,2],
  [2,3,3],
  [3,5]
]
```

提示:
* $1 <= candidates.length <= 30$
* $1 <= candidates[i] <= 200$
* `candidate` 中的每个元素都是独一无二的。
* $1 <= target <= 500$

Solution:
### DFS

这道题很明显就是在考察回溯算法。

之前跟你分享过的 [37. 解数独(困难)](https://mp.weixin.qq.com/s/0y4lGAH43U3w5loTgaeyoQ) 里面有提到我们应该如何快速判断一道题是否应该使用 DFS + 回溯算法来爆搜。

总的来说,你可以从两个方面来考虑:

  * **1. 求的是所有的方案,而不是方案数。** 由于求的是所有方案,不可能有什么特别的优化,我们只能进行枚举。这时候可能的解法有动态规划、记忆化搜索、DFS + 回溯算法。

  * **2. 通常数据范围不会太大,只有几十。** 如果是动态规划或是记忆化搜索的题的话,由于它们的特点在于低重复/不重复枚举,所以一般数据范围可以出到 $10^4$ 到 $10^5$,而 DFS + 回溯的话,通常会限制在 $30$ 以内。

这道题数据范围是 $30$ 以内,而且是求所有方案,因此我们使用 DFS + 回溯来求解。

代码:
```Java
class Solution {
    public List<List<Integer>> combinationSum(int[] cs, int t) {
        List<List<Integer>> ans = new ArrayList<>();
        List<Integer> cur = new ArrayList<>();
        dfs(cs, t, 0, ans, cur);
        return ans;
    }
    /**
     * cs: 原数组,从该数组进行选数
     * t: 还剩多少值需要凑成。起始值为 target ,代表还没选择任何数;当 t = 0,代表选择的数凑成了 target
     * u: 当前决策到 cs[] 中的第几位
     * ans: 最终结果集
     * cur: 当前结果集
     */
    void dfs(int[] cs, int t, int u, List<List<Integer>> ans, List<Integer> cur) {
        if (t == 0) {
            ans.add(new ArrayList<>(cur));
            return;
        }
        if (u == cs.length || t < 0) return;

        // 枚举 cs[u] 的使用次数
        for (int i = 0; cs[u] * i <= t; i++) {
            dfs(cs, t - cs[u] * i, u + 1, ans, cur);
            cur.add(cs[u]);
        }
        // 进行回溯。注意回溯总是将数组的最后一位弹出
        for (int i = 0; cs[u] * i <= t; i++) {
            cur.remove(cur.size() - 1);
        }
    }
}
```
* 时间复杂度:爆搜通常是指数级别的复杂度
* 空间复杂度:爆搜通常是指数级别的复杂度
*

### 最后

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

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

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

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


Question:
给定一个数组 `candidates` 和一个目标数 `target` ,找出 `candidates` 中所有可以使数字和为 `target` 的组合。

`candidates` 中的每个数字在每个组合中只能使用一次。

说明:
* 所有数字(包括目标数)都是正整数。
* 解集不能包含重复的组合。

示例 1:

```
输入: candidates = [10,1,2,7,6,1,5], target = 8,

所求解集为:
[
  [1, 7],
  [1, 2, 5],
  [2, 6],
  [1, 1, 6]
]
```
示例 2:
```
输入: candidates = [2,5,2,1,2], target = 5,

所求解集为:
[
  [1,2,2],
  [5]
]
```

Solution:
### DFS + 回溯

这道题和「39. 组合总和(中等)」几乎一样。

唯一的不同是这题每个数只能使用一次,而「39. 组合总和(中等)」中可以使用无限次。

我们再来回顾一下应该如何快速判断一道题是否应该使用 DFS + 回溯算法来爆搜。

这个判断方法,最早三叶在 [37. 解数独(困难)](https://mp.weixin.qq.com/s/0y4lGAH43U3w5loTgaeyoQ) 讲过。

总的来说,你可以从两个方面来考虑:

  * **1. 求的是所有的方案,而不是方案数。** 由于求的是所有方案,不可能有什么特别的优化,我们只能进行枚举。这时候可能的解法有动态规划、记忆化搜索、DFS + 回溯算法。

  * **2. 通常数据范围不会太大,只有几十。** 如果是动态规划或是记忆化搜索的题的话,由于它们的特点在于低重复/不重复枚举,所以一般数据范围可以出到 $10^5$ 到 $10^7$,而 DFS + 回溯的话,通常会限制在 30 以内。

这道题数据范围是 30 以内,而且是求所有方案。因此我们使用 DFS + 回溯来求解。

我们可以接着 [39. 组合总和(中等)](https://mp.weixin.qq.com/s/5Ee6jbc3lDlWFEDzTM_LkA) 的思路来修改:

1. 由于每个数字只能使用一次,我们可以直接在 DFS 中决策某个数是用还是不用。

2. 由于不允许重复答案,可以使用 set 来保存所有合法方案,最终再转为 list 进行返回。当然我们需要先对 cs 进行排序,确保得到的合法方案中数值都是从小到大的。这样 set 才能起到去重的作用。对于 `[1,2,1]` 和 `[1,1,2]`,set 不会认为是相同的数组。

代码:
```Java
class Solution {
    public List<List<Integer>> combinationSum2(int[] cs, int t) {
        Arrays.sort(cs);
        Set<List<Integer>> ans = new HashSet<>();
        List<Integer> cur = new ArrayList<>();
        dfs(cs, t, 0, ans, cur);
        return new ArrayList<>(ans);
    }

    /**
     * cs: 原数组,从该数组进行选数
     * t: 还剩多少值需要凑成。起始值为 target ,代表还没选择任何数;当 t = 0,代表选择的数凑成了 target
     * u: 当前决策到 cs[] 中的第几位
     * ans: 最终结果集
     * cur: 当前结果集
     */
    void dfs(int[] cs, int t, int u, Set<List<Integer>> ans, List<Integer> cur) {
        if (t == 0) {
            ans.add(new ArrayList<>(cur));
            return;
        }
        if (u == cs.length || t < 0) return;

        // 使用 cs[u]
        cur.add(cs[u]);
        dfs(cs, t - cs[u], u + 1, ans, cur);

        // 进行回溯
        cur.remove(cur.size() - 1);
        // 不使用 cs[u]
        dfs(cs, t, u + 1, ans, cur);
    }
}
```
* 时间复杂度: DFS 回溯算法通常是指数级别的复杂度(因此数据范围通常为 30 以内)。这里暂定 $O(n \times 2^n)$
* 空间复杂度:同上。复杂度为 $O(n \times 2^n)$

### 最后

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

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

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

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


Question:
有 `n` 个气球,编号为 `0` 到 `n - 1`,每个气球上都标有一个数字,这些数字存在数组 `nums` 中。

现在要求你戳破所有的气球。

戳破第 `i` 个气球,你可以获得 `nums[i - 1] * nums[i] * nums[i + 1]` 枚硬币。 这里的 `i - 1` 和 `i + 1` 代表和 `i` 相邻的两个气球的序号。

如果 `i - 1` 或 `i + 1` 超出了数组的边界,那么就当它是一个数字为 `1` 的气球。

求所能获得硬币的最大数量。

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

输出:167

解释:
nums = [3,1,5,8] --> [3,5,8] --> [3,8] --> [8] --> []
coins =  3*1*5    +   3*5*8   +  1*3*8  + 1*8*1 = 167
```
示例 2:
```
输入:nums = [1,5]

输出:10
```

提示:
* $n = nums.length$
* $1 <= n <= 300$
* $0 <= nums[i] <= 100$

Solution:
### 区间 DP

定义 $f[l][r]$ 为考虑将 $(l, r)$ 范围内(不包含 `l` 和 `r` 边界)的气球消耗掉,所能取得的最大价值。

根据题意,我们可以对 `nums` 进行扩充,将其从长度为 $n$ 的 `nums` 变为长度 $n + 2$ 的 `arr`,其中 $arr[1...n]$ 对应了原数组 `nums`,而 $arr[0] = arr[n + 1] = 1$。

此时易知 $f[0][n + 1]$ 即是答案,不失一般性考虑 $f[l][r]$ 该如何转移,假设在 $(l, r)$ 范围内最后剩下的气球的编号为 $k$,此时的 $f[l][r]$ 由「以 $k$ 为分割点的两端所产生的价值」和「消耗 $k$ 本身带来的价值」两部分组成:

$$
f[l][r] = \max(f[l][k] + f[k][r] + arr[l] \times arr[k] \times arr[r]), k \in (l, r)
$$

为了确保转移能够顺利进行,我们需要确保在计算 $f[l][r]$ 的时候,区间长度比其小的 $f[l][k]$ 和 $f[k][r]$ 均被计算。

因此我们可以采用先枚举区间长度 `len`,然后枚举区间左端点 `l`(同时直接算得区间右端点 `r`)的方式来做。

Java 代码:
```Java
class Solution {
    public int maxCoins(int[] nums) {
        int n = nums.length;
        int[] arr = new int[n + 2];
        arr[0] = arr[n + 1] = 1;
        for (int i = 1; i <= n; i++) arr[i] = nums[i - 1];
        int[][] f = new int[n + 2][n + 2];
        for (int len = 3; len <= n + 2; len++) {
            for (int l = 0; l + len - 1 <= n + 1; l++) {
                int r = l + len - 1;
                for (int k = l + 1; k <= r - 1; k++) {
                    f[l][r] = Math.max(f[l][r], f[l][k] + f[k][r] + arr[l] * arr[k] * arr[r]);
                }
            }
        }
        return f[0][n + 1];
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    int maxCoins(vector<int>& nums) {
        int n = nums.size();
        int arr[n + 2];
        arr[0] = arr[n + 1] = 1;
        for (int i = 1; i <= n; i++) arr[i] = nums[i - 1];
        int f[n + 2][n + 2];
        memset(f, 0, sizeof f);
        for (int len = 3; len <= n + 2; len++) {
            for (int l = 0; l + len - 1 <= n + 1; l++) {
                int r = l + len - 1;
                for (int k = l + 1; k <= r - 1; k++) {
                    f[l][r] = max(f[l][r], f[l][k] + f[k][r] + arr[l] * arr[k] * arr[r]);
                }
            }
        }
        return f[0][n + 1];
    }
};
```
Python 代码:
```Python
class Solution:
    def maxCoins(self, nums: List[int]) -> int:
        n = len(nums)
        arr = [1] * (n + 2)
        arr[0] = arr[n + 1] = 1
        for i in range(1, n + 1):
            arr[i] = nums[i - 1]
        f = [[0] * (n + 2) for _ in range(n + 2)]
        for clen in range(3, n + 3):
            for l in range(0, n + 2 - clen + 1):
                r = l + clen - 1
                for k in range(l + 1, r):
                    f[l][r] = max(f[l][r], f[l][k] + f[k][r] + arr[l] * arr[k] * arr[r])
        return f[0][n + 1]
```
TypeScript 代码:
```TypeScript
function maxCoins(nums: number[]): number {
    const n = nums.length
    const arr = new Array<number>(n + 2).fill(1)
    for (let i = 1; i <= n; i++) arr[i] = nums[i - 1]
    const f = new Array<Array<number>>(n + 2)
    for (let i = 0; i < n + 2; i++) f[i] = new Array<number>(n + 2).fill(0)
    for (let len = 3; len <= n + 2; len++) {
        for (let l = 0; l + len - 1 <= n + 1; l++) {
            const r = l + len - 1
            for (let k = l + 1; k <= r - 1; k++) {
                f[l][r] = Math.max(f[l][r], f[l][k] + f[k][r] + arr[l] * arr[k] * arr[r])
            }
        }
    }
    return f[0][n + 1]
}
```
* 时间复杂度:$O(n^3)$
* 空间复杂度:$O(n^2)$

### 最后

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

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

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

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


Question:
超级丑数 是一个正整数,并满足其所有质因数都出现在质数数组 primes 中。

给你一个整数 n 和一个整数数组 primes ,返回第 n 个 超级丑数 。

题目数据保证第 n 个 超级丑数 在 32-bit 带符号整数范围内。

示例 1:
```
输入:n = 12, primes = [2,7,13,19]

输出:32

解释:给定长度为 4 的质数数组 primes = [2,7,13,19],前 12 个超级丑数序列为:[1,2,4,7,8,13,14,16,19,26,28,32] 。
```
示例 2:
```
输入:n = 1, primes = [2,3,5]

输出:1

解释:1 不含质因数,因此它的所有质因数都在质数数组 primes = [2,3,5] 中。
```

提示:
* 1 <= n <= $10^6$
* 1 <= primes.length <= 100
* 2 <= primes[i] <= 1000
* 题目数据 保证 primes[i] 是一个质数
* primes 中的所有值都 互不相同 ,且按 递增顺序 排列

Solution:
### 基本分析

类似的题目在之前的每日一题也出现过。

本题做法与 [264. 丑数 II](https://leetcode-cn.com/problems/ugly-number-ii/) 类似,相关题解在 [这里](https://leetcode-cn.com/problems/ugly-number-ii/solution/gong-shui-san-xie-yi-ti-shuang-jie-you-x-3nvs/)。

回到本题,根据丑数的定义,我们有如下结论:

* $1$ 是最小的丑数。
* 对于任意一个丑数 $x$,其与任意给定的质因数 $primes[i]$ 相乘,结果仍为丑数。

---

### 优先队列(堆)

有了基本的分析思路,一个简单的解法是使用优先队列:

1. 起始先将最小丑数 $1$ 放入队列
2. 每次从队列取出最小值 $x$,然后将 $x$ 所对应的丑数 $x * primes[i]$ 进行入队。
3. 对步骤 $2$ 循环多次,第 $n$ 次出队的值即是答案。

为了防止同一丑数多次进队,我们可以使用数据结构 $Set$ 来记录入过队列的丑数,但该做法常数较大,容易被卡。

利用题目规定的答案为 $int$ 范围,以及丑数性质,我们可以直接在入队的时候做控制。

代码:
```Java
class Solution {
    public int nthSuperUglyNumber(int n, int[] primes) {
        PriorityQueue<Integer> q = new PriorityQueue<>();
        q.add(1);
        while (n-- > 0) {
            int x = q.poll();
            if (n == 0) return x;
            for (int k : primes) {
                if (k <= Integer.MAX_VALUE / x) q.add(k * x);
                if (x % k == 0) break;
            }
        }
        return -1; // never
    }
}
```
* 时间复杂度:令 $primes$ 长度为 $m$,需要从优先队列(堆)中弹出 $n$ 个元素,每次弹出最多需要放入 $m$ 个元素,堆中最多有 $n * m$ 个元素。复杂度为 $O(n * m \log{(n * m)})$
* 空间复杂度:$O(n * m)$

---

### 多路归并

从解法一中不难发现,我们「往后产生的丑数」都是基于「已有丑数」而来(使用「已有丑数」乘上「给定质因数」$primes[i]$)。

因此,如果我们所有丑数的有序序列为 $a1,a2,a3,...,an$ 的话,序列中的每一个数都必然能够被以下三个序列(中的至少一个)覆盖(这里假设 $primes = [2,3,5]$):

* 由丑数 * $2$ 所得的有序序列:$1 * 2$、$2 * 2$、$3 * 2$、$4 * 2$、$5 * 2$、$6 * 2$、$8 * 2$ ...
* 由丑数 * $3$ 所得的有序序列:$1 * 3$、$2 * 3$、$3 * 3$、$4 * 3$、$5 * 3$、$6 * 3$、$8 * 3$ ...
* 由丑数 * $5$ 所得的有序序列:$1 * 5$、$2 * 5$、$3 * 5$、$4 * 5$、$5 * 5$、$6 * 5$、$8 * 5$ ...

我们令这些有序序列为 $arr$,最终的丑数序列为 $ans$。

如果 $primes$ 的长度为 $m$ 的话,我们可以使用 $m$ 个指针来指向这 $m$ 个有序序列 $arr$ 的当前下标。

显然,我们需要每次取 $m$ 个指针中值最小的一个,然后让指针后移(即将当前序列的下一个值放入堆中),不断重复这个过程,直到我们找到第 $n$ 个丑数。

当然,实现上,我们并不需要构造出这 $m$ 个有序序列。

我们可以构造一个存储三元组的小根堆,三元组信息为 $(val, i, idx)$:

* $val$ :为当前列表指针指向具体值;
* $i$ :代表这是由 $primes[i]$ 构造出来的有序序列;
* $idx$:代表丑数下标,存在关系 $val = ans[idx] * primes[i]$。

起始时,我们将所有的 $(primes[i], i, 0)$ 加入优先队列(堆)中,每次从堆中取出最小元素,那么下一个该放入的元素为 $(ans[idx + 1] * primes[i], i, idx + 1)$。

另外,由于我们每个 $arr$ 的指针移动和 $ans$ 的构造,都是单调递增,因此我们可以通过与当前最后一位构造的 $ans[x]$ 进行比较来实现去重,而无须引用常数较大的 `Set` 结构。

代码:
```Java
class Solution {
    public int nthSuperUglyNumber(int n, int[] primes) {
        int m = primes.length;
        PriorityQueue<int[]> q = new PriorityQueue<>((a,b)->a[0]-b[0]);
        for (int i = 0; i < m; i++) {
            q.add(new int[]{primes[i], i, 0});
        }
        int[] ans = new int[n];
        ans[0] = 1;
        for (int j = 1; j < n; ) {
            int[] poll = q.poll();
            int val = poll[0], i = poll[1], idx = poll[2];
            if (val != ans[j - 1]) ans[j++] = val;
            q.add(new int[]{ans[idx + 1] * primes[i], i, idx + 1});
        }
        return ans[n - 1];
    }
}
```
* 时间复杂度:需要构造长度为 $n$ 的答案,每次构造需要往堆中取出和放入元素,堆中有 $m$ 个元素,起始时,需要对 $primes$ 进行遍历,复杂度为 $O(m)$。整体复杂度为 $O(\max(m, n\log{m}))$
* 空间复杂度:存储 $n$ 个答案,堆中有 $m$ 个元素,复杂度为 $O(n + m)$

### 最后

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

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

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

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


318. 最大单词长度乘积

Question:
给定一个字符串数组 `words`,找到 $length(word[i]) * length(word[j])$ 的最大值,并且这两个单词不含有公共字母。你可以认为每个单词只包含小写字母。如果不存在这样的两个单词,返回 $0$。

 示例 1:
```
输入: ["abcw","baz","foo","bar","xtfn","abcdef"]

输出: 16

解释: 这两个单词为 "abcw", "xtfn"。
```
示例 2:
```
输入: ["a","ab","abc","d","cd","bcd","abcd"]

输出: 4

解释: 这两个单词为 "ab", "cd"。
```
示例 3:
```
输入: ["a","aa","aaa","aaaa"]

输出: 0

解释: 不存在这样的两个单词。
```

提示:
* $2 <= words.length <= 1000$
* $1 <= words[i].length <= 1000$
* $words[i]$ 仅包含小写字母

Solution:
### 模拟

根据题意进行模拟即可,利用每个 $words[i]$ 只有小写字母,且只需要区分两字符是否有字母重复。

我们可以使用一个 `int` 来代指某个 $word[i]$:低 $26$ 来代指字母 `a-z` 是否出现过。

然后对每个「字符对」所对应的两个 `int` 值执行 `&` 操作(若两字符无重复字符,则结果为 $0$),并得出最终答案。

代码:
```Java
class Solution {
    public int maxProduct(String[] words) {
        int n = words.length, idx = 0;
        int[] masks = new int[n];
        for (String w : words) {
            int t = 0;
            for (int i = 0; i < w.length(); i++) {
                int u = w.charAt(i) - 'a';
                t |= (1 << u);
            }
            masks[idx++] = t;
        }
        int ans = 0;
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < i; j++) {
                if ((masks[i] & masks[j]) == 0) ans = Math.max(ans, words[i].length() * words[j].length());
            }
        }
        return ans;
    }
}
```
* 时间复杂度:令 $n$ 为 $words$ 数组的长度,转换出 $masks$ 的复杂度为 $O(\sum_{i = 0}^{i = n - 1}words[i].length)$;得到答案的复杂度为 $O(n^2)$。整体复杂度为 $O(\max(\sum_{i = 0}^{i = n - 1}words[i].length, n^2))$
* 空间复杂度:$O(n)$

---

### 优化

不难发现,对于词频相同($mask$ 值相等)的两字符,只需要保留字符长度大的即可,因此我们可以使用「哈希表」代替 $masks$ 数组。

代码:
```Java
class Solution {
    public int maxProduct(String[] words) {
        Map<Integer, Integer> map = new HashMap<>();
        for (String w : words) {
            int t = 0, m = w.length();
            for (int i = 0; i < m; i++) {
                int u = w.charAt(i) - 'a';
                t |= (1 << u);
            }
            if (!map.containsKey(t) || map.get(t) < m) map.put(t, m);
        }
        int ans = 0;
        for (int a : map.keySet()) {
            for (int b : map.keySet()) {
                if ((a & b) == 0) ans = Math.max(ans, map.get(a) * map.get(b));
            }
        }
        return ans;
    }
}
```
* 时间复杂度:令 $n$ 为 $words$ 数组的长度,得到 $map$ 的复杂度为 $O(\sum_{i = 0}^{i = n - 1}words[i].length)$;得到答案的复杂度为 $O(n^2)$。整体复杂度为 $O(\max(\sum_{i = 0}^{i = n - 1}words[i].length, n^2))$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


Question:
初始时有 `n` 个灯泡处于关闭状态。第一轮,你将会打开所有灯泡。接下来的第二轮,你将会每两个灯泡关闭一个。

第三轮,你每三个灯泡就切换一个灯泡的开关(即,打开变关闭,关闭变打开)。

第 `i` 轮,你每 `i` 个灯泡就切换一个灯泡的开关。直到第 `n` 轮,你只需要切换最后一个灯泡的开关。

找出并返回 `n` 轮后有多少个亮着的灯泡。

示例 1:

```
输入:n = 3

输出:1

解释:
初始时, 灯泡状态 [关闭, 关闭, 关闭].
第一轮后, 灯泡状态 [开启, 开启, 开启].
第二轮后, 灯泡状态 [开启, 关闭, 开启].
第三轮后, 灯泡状态 [开启, 关闭, 关闭].

你应该返回 1,因为只有一个灯泡还亮着。
```
示例 2:
```
输入:n = 0

输出:0
```
示例 3:
```
输入:n = 1

输出:1
```

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

Solution:
### 数学

这是一道经典的数论题。

整理一下题意:**第 $i$ 轮改变所有编号为 $i$ 的倍数的灯泡的状态(其中灯泡编号从 $1$ 开始)。**

**一个编号为 $x$ 的灯泡经过 $n$ 轮后处于打开状态的充要条件为「该灯泡被切换状态次数为奇数次」。**

同时,一个灯泡切换状态的次数为其约数的个数(去重)。

于是问题转换为:**在 $[1,n]$ 内有多少个数,其约数的个数为奇数**。这些约数个数为奇数的灯泡就是最后亮着的灯泡。

又根据「约数」的定义,我们知道如果某个数 $k$ 为 $x$ 的约数,那么 $\frac{x}{k}$ 亦为 $x$ 的约数,即「约数」总是成对出现,那么某个数的约数个数为奇数,意味着某个约数在分解过程中出现了 $2$ 次,且必然重复出现在同一次拆解中,即 $k = \frac{x}{k}$,即有 $x$ 为完全平方数(反之亦然)。

问题最终转换为:**在 $[1,n]$ 中完全平方数的个数为多少。**

根据数论推论,$[1,n]$ 中完全平方数的个数为 $\left \lfloor \sqrt{n} \right \rfloor$,即最后亮着的灯泡数量为 $\left \lfloor \sqrt{n} \right \rfloor$。

代码:
```Java
class Solution {
    public int bulbSwitch(int n) {
        return (int)Math.sqrt(n);
    }
}
```
* 时间复杂度:$O(1)$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


Question:
给定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1。

你可以认为每种硬币的数量是无限的。

示例 1:
```
输入:coins = [1, 2, 5], amount = 11
输出:3
解释:11 = 5 + 5 + 1
```
示例 2:
```
输入:coins = [2], amount = 3
输出:-1
```
示例 3:
```
输入:coins = [1], amount = 0
输出:0
```
示例 4:
```
输入:coins = [1], amount = 1
输出:1
```
示例 5:
```
输入:coins = [1], amount = 2
输出:2
```

提示:
* 1 <= coins.length <= 12
* 1 <= coins[i] <= $2^{31}$ - 1
* 0 <= amount <= $10^4$

Solution:
### 完全背包(朴素解法)

硬币相当于我们的物品,每种硬币可以选择「无限次」,我们应该很自然的想到「完全背包」。

如果不能,那么从现在开始就要培养这样的习惯:

**当看到题目是给定一些「物品」,让我们从中进行选择,以达到「最大价值」或者「特定价值」时,我们应该联想到「背包问题」。**

**这本质上其实是一个组合问题:被选物品之间不需要满足特定关系,只需要选择物品,以达到「全局最优」或者「特定状态」即可。**

再根据物品的选择次数限制来判断是何种背包问题。

本题每种硬币可以被选择「无限次」,我们可以直接套用「完全背包」的状态定义进行微调:

**定义 $f[i][j]$ 为考虑前 $i$ 件物品,凑成总和为 $j$ 所需要的最少硬币数量。**

为了方便初始化,我们一般让 $f[0][x]$ 代表不考虑任何物品的情况。

因此我们有显而易见的初始化条件:$f[0][0] = 0$,其余 $f[0][x] = INF$。

代表当没有任何硬币的时候,存在凑成总和为 0 的方案,方案所使用的硬币为 0;凑成其他总和的方案不存在。

由于我们要求的是「最少」硬币数量,因此我们不希望「无效值」参与转移,因此可设 $INF = INT\_MAX$。

当「状态定义」与「基本初始化」有了之后,我们不失一般性的考虑 $f[i][j]$ 该如何转移。

对于第 $i$ 个硬币我们有两种决策方案:

* 不使用该硬币:
$$
f[i - 1][j]
$$

* 使用该硬币,由于每个硬币可以被选择多次(容量允许的情况下),因此最优解应当是所有方案中的最小值:
$$
min(f[i-1][j-k*coin] + k)
$$

代码:
```Java
class Solution {
    int INF = Integer.MAX_VALUE;
    public int coinChange(int[] cs, int cnt) {
        int n = cs.length;
        int[][] f = new int[n + 1][cnt + 1];

        // 初始化(没有任何硬币的情况):只有 f[0][0] = 0;其余情况均为无效值。
        // 这是由「状态定义」决定的,当不考虑任何硬币的时候,只能凑出总和为 0 的方案,所使用的硬币数量为 0
        for (int i = 1; i <= cnt; i++) f[0][i] = INF;

        // 有硬币的情况
        for (int i = 1; i <= n; i++) {
            int val = cs[i - 1];
            for (int j = 0; j <= cnt; j++) {
                // 不考虑当前硬币的情况
                f[i][j] = f[i - 1][j];

                // 考虑当前硬币的情况(可选当前硬币个数基于当前容量大小)
                for (int k = 1; k * val <= j; k++) {
                    if (f[i - 1][j - k * val] != INF) {
                        f[i][j] = Math.min(f[i][j], f[i-1][j-k*val] + k);
                    }
                }
            }
        }
        return f[n][cnt] == INF ? -1 : f[n][cnt];
    }
}
```
* 时间复杂度:共有 $n * cnt$ 个状态需要转移,每个状态转移最多遍历 $cnt$ 次。整体复杂度为 $O(n * cnt^2)$。
* 空间复杂度:$O(n * cnt)$。

***

### 无效状态的定义问题

**借这个问题,刚好说一下,我们初始化时,对于无效状态应该如何定义。**

可以看到上述解法,将 `INF` 定义为 `INT_MAX`。

这是因为我们转移时取的是较小值,我们希望无效值不要被转移,所以将 `INF` 定义为较大的数,以代表数学上的 $+\infty$ (正无穷)。

这很合理,但是我们需要注意,如果我们在 `INF` 的基础上进行累加的话,常规的语言会将其变成负数最小值。

也就是在正无穷基础上进行累加,会丢失其正无穷的含义,这与数学上的正无穷概念冲突。

因此,我们才有先判断再使用的习惯:

```Java
if (f[i-1][j] != INF) {
    f[i][j] = Math.min(f[i][j], f[i-1][j]);
}
```

但事实上,如果每次使用都需要有前置检查的话,是很麻烦的。

于是我们有另外一个技巧,不直接使用 `INT_MAX` 作为 `INF`,而是使用一个比 `INT_MAX` 小的较大数来代表 `INF`。

相当于预留了一些「累加空间」给 `INF`。

比如使用 `0x3f3f3f3f` 作为最大值,这样我们使用 `INF` 做状态转移的时候,就不需要先判断再使用了。

代码:
```Java
class Solution {
    int INF = 0x3f3f3f3f;
    public int coinChange(int[] cs, int cnt) {
        int n = cs.length;
        int[][] f = new int[n + 1][cnt + 1];
        for (int i = 1; i <= cnt; i++) f[0][i] = INF;
        for (int i = 1; i <= n; i++) {
            int val = cs[i - 1];
            for (int j = 0; j <= cnt; j++) {
                f[i][j] = f[i-1][j];
                for (int k = 0; k * val <= j; k++) {
                        f[i][j] = Math.min(f[i][j], f[i-1][j-k*val] + k);
                }
            }
        }
        return f[n][cnt] == INF ? -1 : f[n][cnt];
    }
}
```

***

### 完全背包(一维优化)

显然朴素版的完全背包进行求解复杂度有点高。

在[「学习完全背包」](https://mp.weixin.qq.com/s/nke1OjkhKACaONx1opk8AA)和[「上一讲练习」](https://mp.weixin.qq.com/s/zWh9zyIGMd-6fzz-KIQGDw)中,我们从最朴素背包转移方程出发,从数学的角度去推导一维优化是如何来的。

这十分科学,而绝对严谨。

但每次都这样推导是十分耗时的。

因此,我们这次站在一个「更高」的角度去看「完全背包」问题。

我们知道传统的「完全背包」二维状态转移方程是:

$$
f[i][j] = max(f[i - 1][j], f[i - 1][j - k * w[i]] + k* v[i])
$$

经过一维空间优化后的状态转移方程是(同时容量维度遍历顺序为「从小到大」):

$$
f[j] = max(f[j], f[j - w[i]] + v[i])
$$

这是我们在 [学习完全背包](https://mp.weixin.qq.com/s/nke1OjkhKACaONx1opk8AA) 时推导的,是经过严格证明的,具有一般性的。

然后我们只需要对「成本」&「价值」进行抽象,并结合「换元法」即可得到任意背包问题的一维优化状态转移方程。

拿我们本题的状态转移方程来分析,本题的朴素状态转移方程为:

$$
f[i][j] = min(f[i - 1][j], f[i-1][j-k*coin] + k)
$$

我们将硬币的面值抽象为「成本」,硬币的数量抽象「价值」,再对物品维度进行消除,即可得:

$$
f[j] = min(f[j], f[j-coin] + 1)
$$

**如果还不理解,可以将上述四个状态转移方程「两两成对」结合来看。**

代码:
```java []
class Solution {
    int INF = 0x3f3f3f3f;
    public int coinChange(int[] cs, int cnt) {
        int n = cs.length;
        int[] f = new int[cnt + 1];
        for (int i = 1; i <= cnt; i++) f[i] = INF;
        for (int i = 1; i <= n; i++) {
            int val = cs[i - 1];
            for (int j = val; j <= cnt; j++) {
                f[j] = Math.min(f[j], f[j - val] + 1);
            }
        }
        return f[cnt] == INF ? -1 : f[cnt];
    }
}
```
* 时间复杂度:共有 $n * cnt$ 个状态需要转移,整体复杂度为 $O(n * cnt)$。
* 空间复杂度:$O(cnt)$。

***

### 总结

本节,我们先是从朴素「完全背包」的角度分析并解决了问题。

而在考虑「一维优化」的时候,由于已经有前两节「数学推导优化思路」的基础,我们这次站在了「更高」的角度去看待一维优化。

**从抽象「成本」&「价值」,结合「换元法」的角度去理解一维优化过程。**

这可以大大节省我们分析推导的时间。

建议大家加强理解 ~

下一节练习篇,我们会继续强化这个过程

### 最后

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

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

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

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


Question:
给你一个整数数组 `nums`,将它重新排列成 `nums[0] < nums[1] > nums[2] < nums[3]...` 的顺序。

你可以假设所有输入数组都可以得到满足题目要求的结果。

示例 1:
```
输入:nums = [1,5,1,1,6,4]

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

解释:[1,4,1,5,1,6] 同样是符合题目要求的结果,可以被判题程序接受。
```
示例 2:
```
输入:nums = [1,3,2,2,3,1]

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

提示:
* $1 <= nums.length <= 5 \times 10^4$
* $0 <= nums[i] <= 5000$
* 题目数据保证,对于给定的输入 `nums`,总能产生满足题目要求的结果

进阶:你能用 $O(n)$ 时间复杂度和 / 或原地 $O(1)$ 额外空间来实现吗?

Solution:
### 构造(快选 + 三数排序)

这道题即使不考虑空间 $O(1)$ 的进阶要求,只要求做到 $O(n)$ 时间的话,在 LC 上也属于难题了。如果大家是第一次做,并且希望在有限时间(不超过 $20$ 分钟)内做出来,可以说是难上加难。

本质上,题目要我们实现一种构造方法,能够将 `nums` 调整为满足「摆动」要求。

具体的构造方法:

1. 找到 `nums` 的中位数,这一步可以通过「快速选择」算法来做,时间复杂度为 $O(n)$,空间复杂度为 $O(\log{n})$,假设找到的中位数为 `x`;

2. 根据 $nums[i]$ 与 `x` 的大小关系,将 $nums[i]$ 分为三类(小于/等于/大于),划分三类的操作可以采用「三数排序」的做法,复杂度为 $O(n)$。

   这一步做完之后,我们的数组调整为:$[a_1, a_2, a_3, ... , b_1, b_2, b_3, ... , c_1, c_2, c_3]$ ,即分成「小于 `x` / 等于 `x` / 大于 `x`」三段。

3. 构造:先放「奇数」下标,再放「偶数」下标,放置方向都是「从左到右」(即可下标从小到大进行放置),放置的值是则是「从大到小」。

   到这一步之前,我们使用到的空间上界是 $O(\log{n})$,如果对空间上界没有要求的话,我们可以简单对 `nums` 进行拷贝,然后按照对应逻辑进行放置即可,但这样最终的空间复杂度为 $O(n)$(代码见 $P2$);如果不希望影响到原有的空间上界,我们需要额外通过「找规律/数学」的方式,找到原下标和目标下标的映射关系(函数 `getIdx` 中)。

容易证明该构造过程的正确性(即该构造过程必然能顺利进行):由于我们是按照值「从大到小」进行放置,如果构造出来的方案不合法,必然是相邻的两个值为相等(“应当递增实际递减”或者“应当递减实际递增”的情况已被「从大到小」进行放置所否决),而当相邻位置放置了相同的值,即存在某个奇数下标,以及其相邻的偶数下标都放置了相同的值,这等价于该值出现次数超过总个数的一半,这与「题目本身保证数据能够构造出摆动数组」所冲突。

代码:
```Java
class Solution {
    int[] nums;
    int n;
    int qselect(int l, int r, int k) {
        if (l == r) return nums[l];
        int x = nums[l + r >> 1], i = l - 1, j = r + 1;
        while (i < j) {
            do i++; while (nums[i] < x);
            do j--; while (nums[j] > x);
            if (i < j) swap(i, j);
        }
        int cnt = j - l + 1;
        if (k <= cnt) return qselect(l, j, k);
        else return qselect(j + 1, r, k - cnt);
    }
    void swap(int a, int b) {
        int c = nums[a];
        nums[a] = nums[b];
        nums[b] = c;
    }
    int getIdx(int x) {
        return (2 * x + 1) % (n | 1);
    }
    public void wiggleSort(int[] _nums) {
        nums = _nums;
        n = nums.length;
        int x = qselect(0, n - 1, n + 1 >> 1);
        int l = 0, r = n - 1, loc = 0;
        while (loc <= r) {
            if (nums[getIdx(loc)] > x) swap(getIdx(loc++), getIdx(l++));
            else if (nums[getIdx(loc)] < x) swap(getIdx(loc), getIdx(r--));
            else loc++;
        }
    }
}
```

-

```Java
class Solution {
    int[] nums;
    int n;
    int qselect(int l, int r, int k) {
        if (l == r) return nums[l];
        int x = nums[l + r >> 1], i = l - 1, j = r + 1;
        while (i < j) {
            do i++; while (nums[i] < x);
            do j--; while (nums[j] > x);
            if (i < j) swap(i, j);
        }
        int cnt = j - l + 1;
        if (k <= cnt) return qselect(l, j, k);
        else return qselect(j + 1, r, k - cnt);
    }
    void swap(int a, int b) {
        int c = nums[a];
        nums[a] = nums[b];
        nums[b] = c;
    }
    public void wiggleSort(int[] _nums) {
        nums = _nums;
        n = nums.length;
        int x = qselect(0, n - 1, n + 1 >> 1);
        int l = 0, r = n - 1, loc = 0;
        while (loc <= r) {
            if (nums[loc] < x) swap(loc++, l++);
            else if (nums[loc] > x) swap(loc, r--);
            else loc++;
        }
        int[] clone = nums.clone();
        int idx = 1; loc = n - 1;
        while (idx < n) {
            nums[idx] = clone[loc--];
            idx += 2;
        }
        idx = 0;
        while (idx < n) {
            nums[idx] = clone[loc--];
            idx += 2;
        }
    }
}
```
* 时间复杂度:快选的时间复杂度为 $O(n)$;三数排序复杂度为 $O(n)$。整体复杂度为 $O(n)$
* 空间复杂度:我的习惯是不算递归带来的额外空间消耗的,但如果是题目指定 $O(1)$ 空间的话,显然是不能按照习惯来,快选的空间复杂度为 $O(\log{n})$。整体复杂度为 $O(\log{n})$

### 最后

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

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

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

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


Question:
给定一个整数,写一个函数来判断它是否是 $3$ 的幂次方。如果是,返回 $true$ ;否则,返回 $false$ 。

整数 $n$ 是 $3$ 的幂次方需满足:存在整数 $x$ 使得 $n == 3^x$

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

输出:true
```
示例 2:
```
输入:n = 0

输出:false
```
示例 3:
```
输入:n = 9

输出:true
```
示例 4:
```
输入:n = 45

输出:false
```

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

Solution:
### 数学

一个不能再朴素的做法是将 $n$ 对 $3$ 进行试除,直到 $n$ 不再与 $3$ 呈倍数关系,最后判断 $n$ 是否为 $3^0 = 1$ 即可。

代码:
```Java
class Solution {
    public boolean isPowerOfThree(int n) {
        if (n <= 0) return false;
        while (n % 3 == 0) n /= 3;
        return n == 1;
    }
}
```
* 时间复杂度:$O(\log_{3}n)$
* 空间复杂度:$O(1)$

---

### 倍数 & 约数

题目要求不能使用循环或递归来做,而传参 $n$ 的数据类型为 `int`,这引导我们首先分析出 `int` 范围内的最大 $3$ 次幂是多少,约为 $3^{19} = 1162261467$。

如果 $n$ 为 $3$ 的幂的话,那么必然满足 $n * 3^k = 1162261467$,即 $n$ 与 $1162261467$ 存在倍数关系。

因此,我们只需要判断 $n$ 是否为 $1162261467$ 的约数即可。

> 注意:这并不是快速判断 $x$ 的幂的通用做法,当且仅当 $x$ 为质数可用。

代码:
```Java
class Solution {
    public boolean isPowerOfThree(int n) {
        return n > 0 && 1162261467 % n == 0;
    }
}
```
* 时间复杂度:$O(1)$
* 空间复杂度:$O(1)$

---

### 打表

另外一个更容易想到的「不使用循环/递归」的做法是进行打表预处理。

使用 `static` 代码块,预处理出不超过 `int` 数据范围的所有 $3$ 的幂,这样我们在跑测试样例时,就不需要使用「循环/递归」来实现逻辑,可直接 $O(1)$ 查表返回。

代码:
```Java
class Solution {
    static Set<Integer> set = new HashSet<>();
    static {
        int cur = 1;
        set.add(cur);
        while (cur <= Integer.MAX_VALUE / 3) {
            cur *= 3;
            set.add(cur);
        }
    }
    public boolean isPowerOfThree(int n) {
        return n > 0 && set.contains(n);
    }
}
```
* 时间复杂度:将打表逻辑交给 $OJ$ 执行的话,复杂度为 $O(\log_3{C})$,$C$ 固定为 $2147483647$;将打表逻辑放到本地执行,复杂度为 $O(1)$
* 空间复杂度:$O(\log_3{n})$

### 最后

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

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

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

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


Question:
给你一个整数数组 `nums` 以及两个整数 `lower` 和 `upper` 。求数组中,值位于范围 $[lower, upper]$ (包含 `lower` 和 `upper`)之内的 区间和的个数 。

区间和 $S(i, j)$ 表示在 `nums` 中,位置从 $i$ 到 $j$ 的元素之和,包含 $i$ 和 $j$ (`i ≤ j`)。

示例 1:
```
输入:nums = [-2,5,-1], lower = -2, upper = 2

输出:3

解释:存在三个区间:[0,0]、[2,2] 和 [0,2] ,对应的区间和分别是:-2 、-1 、2 。
```
示例 2:
```
输入:nums = [0], lower = 0, upper = 0

输出:1
```

提示:
* $1 <= nums.length <= 10^5$
* $-2^{31} <= nums[i] <= 2^{31} - 1$
* $-10^5 <= lower <= upper <= 10^5$
* 题目数据保证答案是一个 $32$ 位 的整数

Solution:
### 树状数组(离散化)

由于区间和的定义是子数组的元素和,容易想到「前缀和」来快速求解。

对于每个 $nums[i]$ 而言,我们需要统计以每个 $nums[i]$ 为右端点的合法子数组个数(合法子数组是指区间和值范围为 $[lower, upper]$ 的子数组)。

我们可以从前往后处理 $nums$,假设当前我们处理到位置 $k$,同时下标 $[0, k]$ 的前缀和为 $s$,那么以 $nums[k]$ 为右端点的合法子数组个数,等价于在下标 $[0, k - 1]$ 中前缀和范围在 $[s - upper, s - lower]$ 的数的个数。

我们需要使用一个数据结构来维护「遍历过程中的前缀和」,每遍历 $nums[i]$ 需要往数据结构加一个数,同时每次需要查询值在某个范围内的数的个数。涉及的操作包括「单点修改」和「区间查询」,容易想到使用树状数组进行求解。

但值域的范围是巨大的(同时还有负数域),我们可以利用 $nums$ 的长度为 $10^5$ 来做离散化。我们需要考虑用到的数组都有哪些:

1. 首先前缀和数组中的每一位 $s$ 都需要被用到(添加到树状数组中);
2. 同时对于每一位 $nums[i]$(假设对应的前缀和为 $s$),我们都需要查询以其为右端点的合法子数组个数,即查询前缀和范围在 $[s - upper, s - lower]$ 的数的个数。

因此对于前缀和数组中的每一位  $s$,我们用到的数有 $s$、$s - upper$ 和 $s - lower$ 三个数字,共有 $1e5$ 个 $s$,即最多共有 $3 \times 10^5$ 个不同数字被使用,我们可以对所有用到的数组进行排序编号(离散化),从而将值域大小控制在 $3 \times 10^5$ 范围内。

 代码:
```Java
class Solution {
    int m;
    int[] tr = new int[100010 * 3];
    int lowbit(int x) {
        return x & -x;
    }
    void add(int x, int v) {
        for (int i = x; i <= m; i += lowbit(i)) tr[i] += v;
    }
    int query(int x) {
        int ans = 0;
        for (int i = x; i > 0; i -= lowbit(i)) ans += tr[i];
        return ans;
    }
    public int countRangeSum(int[] nums, int lower, int upper) {
        Set<Long> set = new HashSet<>();
        long s = 0;
        set.add(s);
        for (int i : nums) {
            s += i;
            set.add(s);
            set.add(s - lower);
            set.add(s - upper);
        }
        List<Long> list = new ArrayList<>(set);
        Collections.sort(list);
        Map<Long, Integer> map = new HashMap<>();
        for (long x : list) map.put(x, ++m);
        s = 0;
        int ans = 0;
        add(map.get(s), 1);
        for (int i : nums) {
            s += i;
            int a = map.get(s - lower), b = map.get(s - upper) - 1;
            ans += query(a) - query(b);
            add(map.get(s), 1);
        }
        return ans;
    }
}
```
* 时间复杂度:去重离散化的复杂度为 $O(n\log{n})$;统计答案的复杂度为 $O(n\log{n})$
* 空间复杂度:$O(n)$

---

### 线段树(动态开点 + STL)

值域范围爆炸,但是数组长度(查询次数)有限,容易想到「线段树(动态开点)」。

但如果按照我们 [729. 我的日程安排表 I](https://sharingsource.github.io/2022/05/04/729.%20%E6%88%91%E7%9A%84%E6%97%A5%E7%A8%8B%E5%AE%89%E6%8E%92%E8%A1%A8%20I%EF%BC%88%E4%B8%AD%E7%AD%89%EF%BC%89/)、[731. 我的日程安排表 II](https://sharingsource.github.io/2022/05/04/731.%20%E6%88%91%E7%9A%84%E6%97%A5%E7%A8%8B%E5%AE%89%E6%8E%92%E8%A1%A8%20II%EF%BC%88%E4%B8%AD%E7%AD%89%EF%BC%89/) 和 [732. 我的日程安排表 III](https://sharingsource.github.io/2022/05/04/732.%20%E6%88%91%E7%9A%84%E6%97%A5%E7%A8%8B%E5%AE%89%E6%8E%92%E8%A1%A8%20III%EF%BC%88%E5%9B%B0%E9%9A%BE%EF%BC%89/) 系列题解的求解方式「估点 + 静态数组 + 动态 `new`」,仍无法解决 `MLE`  问题。

究其原因,我们在 [日程安排表] 系列中使用到的方式存在「固定双边开点(一旦要对 $u$ 的子节点开点,会同时将左右子节点都开出来)」以及「查询时也会触发开点」的问题,导致我们最多处理值域范围在 $1e9$ 的情况。

对于本题的值域范围远超 $1e9$,我们需要考虑修「静态数组」和「开点方式」来解决 `MLE`  问题:

1. 由于值域太大,采用估点方式来开精挑数组会直接导致 `MLE`,我们使用支持扩容的 `STL` 容器;
2. 由于同时开点和查询也会触发开点,会导致我们不必要的空间浪费,我们直接将开点操作放在 `update` 实现中,并且只有当需要查询到左子节点才对左子节点进行开点,当查询到右子节点才对右子节点进行开点。

 代码:
```Java
class Solution {
    class Node {
        int ls = -1, rs = -1, val = 0;
    }
    List<Node> tr = new ArrayList<>();
    void update(int u, long lc, long rc, long x, int v) {
        Node node = tr.get(u);
        node.val += v;
        if (lc == rc) return ;
        long mid = lc + rc >> 1;
        if (x <= mid) {
            if (node.ls == -1) {
                tr.add(new Node());
                node.ls = tr.size() - 1;
            }
            update(node.ls, lc, mid, x, v);
        } else {
            if (node.rs == -1) {
                tr.add(new Node());
                node.rs = tr.size() - 1;
            }
            update(node.rs, mid + 1, rc, x, v);
        }
    }
    int query(int u, long lc, long rc, long l, long r) {
        if (u == -1) return 0;
        if (r < lc || l > rc) return 0;
        Node node = tr.get(u);
        if (l <= lc && rc <= r) return node.val;
        long mid = lc + rc >> 1;
        return query(node.ls, lc, mid, l, r) + query(node.rs, mid + 1, rc, l, r);
    }
    public int countRangeSum(int[] nums, int lower, int upper) {
        long L = 0, R = 0, s = 0;
        for (int i : nums) {
            s += i;
            L = Math.min(Math.min(L, s), Math.min(s - lower, s - upper));
            R = Math.max(Math.max(R, s), Math.max(s - lower, s - upper));
        }
        s = 0;
        int ans = 0;
        tr.add(new Node());
        update(0, L, R, 0, 1);
        for (int i : nums) {
            s += i;
            long a = s - upper, b = s - lower;
            ans += query(0, L, R, a, b);
            update(0, L, R, s, 1);
        }
        return ans;
    }
}
```
* 时间复杂度:令 $n$ 为数组长度,$m$ 为值域大小,复杂度为 $O(n\log{m})$
* 空间复杂度:$O(n\log{m})$

---

### 线段树(动态指针)

更进一步,我们可以连 `STL` 都不使用,直接在 `Node` 的定义时,将左右子节点的引用存放起来。

虽然这样不会带来空间上的优化,但可以有效避免 `STL` 的创建、访问和扩容(实际运行相比解法二,用时少一半)。

代码:
```Java
class Solution {
    class Node {
        Node ls, rs;
        int val = 0;
    }
    void update(Node node, long lc, long rc, long x, int v) {
        node.val += v;
        if (lc == rc) return ;
        long mid = lc + rc >> 1;
        if (x <= mid) {
            if (node.ls == null) node.ls = new Node();
            update(node.ls, lc, mid, x, v);
        } else {
            if (node.rs == null) node.rs = new Node();
            update(node.rs, mid + 1, rc, x, v);
        }
    }
    int query(Node node, long lc, long rc, long l, long r) {
        if (node == null) return 0;
        if (r < lc || l > rc) return 0;
        if (l <= lc && rc <= r) return node.val;
        long mid = lc + rc >> 1;
        return query(node.ls, lc, mid, l, r) + query(node.rs, mid + 1, rc, l, r);
    }
    public int countRangeSum(int[] nums, int lower, int upper) {
        long L = 0, R = 0, s = 0;
        for (int i : nums) {
            s += i;
            L = Math.min(Math.min(L, s), Math.min(s - lower, s - upper));
            R = Math.max(Math.max(R, s), Math.max(s - lower, s - upper));
        }
        s = 0;
        int ans = 0;
        Node root = new Node();
        update(root, L, R, 0, 1);
        for (int i : nums) {
            s += i;
            long a = s - upper, b = s - lower;
            ans += query(root, L, R, a, b);
            update(root, L, R, s, 1);
        }
        return ans;
    }
}
```
* 时间复杂度:令 $n$ 为数组长度,$m$ 为值域大小,复杂度为 $O(n\log{m})$
* 空间复杂度:$O(n\log{m})$

### 最后

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

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

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

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


LeetCode/331-340/331. 验证二叉树的前序序列化（中等）.md

331. 验证二叉树的前序序列化

Question:
序列化二叉树的一种方法是使用前序遍历。当我们遇到一个非空节点时,我们可以记录下这个节点的值。如果它是一个空节点,我们可以使用一个标记值记录,例如 #。
```
     _9_
    /   \
   3     2
  / \   / \
 4   1  #  6
/ \ / \   / \
# # # #   # #
```
例如,上面的二叉树可以被序列化为字符串 "9,3,4,#,#,1,#,#,2,#,6,#,#",其中 # 代表一个空节点。

给定一串以逗号分隔的序列,验证它是否是正确的二叉树的前序序列化。编写一个在不重构树的条件下的可行算法。

每个以逗号分隔的字符或为一个整数或为一个表示 null 指针的 '#' 。

你可以认为输入格式总是有效的,例如它永远不会包含两个连续的逗号,比如 "1,,3" 。

示例 1:
```
输入: "9,3,4,#,#,1,#,#,2,#,6,#,#"
输出: true
```
示例 2:
```
输入: "1,#"
输出: false
```
示例 3:
```
输入: "9,#,#,1"
输出: false
```

Solution:
### 二叉树规律解法

事实上,我们能利用「二叉树」的特性来做。

由于**每一个非空节点都对应了 2 个出度,空节点都对应了 0 个出度;除了根节点,每个节点都有一个入度。**

我们可以使用 `in` 和 `out` 来分别记录「入度」和「出度」的数量;`m` 和 `n` 分别代表「非空节点数量」和「空节点数量」。

同时,一颗合格的二叉树最终结果必然满足 `in == out`。

**但我们又不能只利用最终 `in == out` 来判断是否合法,这很容易可以举出反例:考虑将一个合法序列的空节点全部提前,这样最终结果仍然满足 `in == out`,但这样的二叉树是不存在的。**

**我们还需要一些额外的特性,支持我们在遍历过程中提前知道一颗二叉树不合法。**

**例如,我们可以从合格二叉树的前提出发,挖掘遍历过程中 `in` 和 `out` 与 `n` 和 `m` 的关系。**

***

### 证明 1(利用不等式)

我们令非空节点数量为 $m$,空节点数量为 $n$,入度和出度仍然使用 $in$ 和 $out$ 代表。

找一下 $in$ 和 $out$ 与 $n$ 和 $m$ 之间的关系。

一颗合格二叉树 $m$ 和 $n$ 的最小的比例关系是 $1 : 2$,也就是对应了这么一个形状:

```
 4
/ \
# #
```

而遍历过程中 $m$ 和 $n$ 的最小的比例关系则是 $1 : 0$,**这其实对应了二叉树空节点总是跟在非空节点的后面这一性质。**

换句话说,在没到最后一个节点之前,我们是不会遇到 空节点数量 > 非空节点数量 的情况的。

**`非空节点数量 >= 空节点数量` 在遍历没结束前恒成立:$m>=n$**

然后再结合「**每一个非空节点都对应了 $2$ 个出度,空节点都对应了 $0$ 个出度;除了根节点,每个节点都有一个入度**」特性。

在遍历尚未结束前,我们有以下关系:

1. $m >= n$
2. $in <= m + n - 1$
3. $out <= 2 * m$

简单的变形可得:

* 由 $2$ 变形可得:$m >= in + 1 - n$
* 由 $3$ 变形可得:$m >= out / 2$

即有:
1. $m >= n$
2. $m >= in + 1 - n$
3. $m >= out / 2$

再将 $1$ 和 $2$ 相加,抵消 $n$:$2m >= in + 1$
1. $2m >= in + 1$ => $in <= 2m - 1$
2. $m >= out / 2$ => $out <= 2m$

因此,在遍历尚未完成时,$in$ 和 $out$ 始终满足上述关系(与空节点数量 $n$ 无关)。

如果不从合格二叉树的前提($m>=n$)出发,我们是无法得到上述关系式的。

**因此,我们可以一边遍历一边统计「严格出度」和「严格入度」,然后写一个 `check` 函数去判定 $in$、$out$ 和 $m$ 三者关系是否符合要求,如果不符合则说明二叉树不合法。**

代码:
```Java
class Solution {
  public boolean isValidSerialization(String s) {
    String[] ss = s.split(",");
    int n = ss.length;
    int in = 0, out = 0;
    for (int i = 0, m = 0; i < n; i++) {
      // 统计「严格出度」和「严格入度」...
      if (i != n - 1 && !check(m, in, out)) return false;
    }
    return in == out;
  }
  boolean check(int m, int in, int out) {
    boolean a = (in <= 2 * m - 1), b = (out <= 2 * m);
    return a && b;
  }
}
```

**注意:因为我们这里的证明使用到的是不等式。因此统计的必须是「严格出度」&「严格入度」,不能假定一个「非空节点(非根)」必然对应两个「出度」和一个「入度」。**

**要想统计出「严格出度」&「严格入度」在编码上还是有一定难度的。那么是否可以推导出更加简单性质来使用呢?**

**请看「证明 2」。**

***

### 证明 2(利用技巧转换为等式)

我们令非空节点数量为 $m$,空节点数量为 $n$,入度和出度仍然使用 $in$ 和 $out$ 代表。

找一下 $in$ 和 $out$ 与 $n$ 和 $m$ 之间的关系。

一颗合格二叉树 $m$ 和 $n$ 的最小的比例关系是 $1 : 2$,也就是对应了这么一个形状:
```
 4
/ \
# #
```
而遍历过程中 $m$ 和 $n$ 的最小的比例关系则是 $1 : 0$,这**其实对应了二叉树空节点总是跟在非空节点的后面这一性质。**

换句话说,在没到最后一个节点之前,我们是不会遇到 `空节点数量 > 非空节点数量` 的情况的。

**`非空节点数量 >= 空节点数量` 在遍历没结束前恒成立:$m>=n$**

之后我们再采用一个技巧,就是**遍历过程中每遇到一个「非空节点」就增加两个「出度」和一个「入度」,每遇到一个「空节点」只增加一个「入度」。而不管每个「非空节点」是否真实对应两个子节点。**

那么我们的起始条件变成:

1. $m >= n$
2. $in = m + n - 1$
3. $out = 2 * m$

从第 $2$ 个等式出发,结合第 $1$ 个等式:

$in = m + n - 1 <= m + m - 1 = 2m - 1 = out - 1$

即可得 $in + 1 <= out$ ,也就是 $in < out$ 恒成立。

代码:
```Java
class Solution {
    public boolean isValidSerialization(String s) {
        String[] ss = s.split(",");
        int n = ss.length;
        int in = 0, out = 0;
        for (int i = 0; i < n; i++) {
            if (!ss[i].equals("#")) out += 2;
            if (i != 0) in++;
            if (i != n - 1 && out <= in) return false;
        }
        return in == out;
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


334. 递增的三元子序列

Question:
给你一个整数数组 `nums`,判断这个数组中是否存在长度为 $3$ 的递增子序列。

如果存在这样的三元组下标 `(i, j, k)` 且满足 `i < j < k` ,使得 `nums[i] < nums[j] < nums[k]` ,返回 `true` ;否则,返回 `false`。

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

输出:true

解释:任何 i < j < k 的三元组都满足题意
```
示例 2:
```
输入:nums = [5,4,3,2,1]

输出:false

解释:不存在满足题意的三元组
```
示例 3:
```
输入:nums = [2,1,5,0,4,6]

输出:true

解释:三元组 (3, 4, 5) 满足题意,因为 nums[3] == 0 < nums[4] == 4 < nums[5] == 6
```

提示:
* $1 <= nums.length <= 5 * 10^5$
* $-2^{31} <= nums[i] <= 2^{31} - 1$

进阶:你能实现时间复杂度为 $O(n$) ,空间复杂度为 $O(1)$ 的解决方案吗?

Solution:
### 最长上升子序列(贪心 + 二分)

题目要我们判断是否存在长度为 $3$ 的上升子序列,问题可以转换为求 $nums$ 的最长上升子序列(`LIS` 问题)的长度。

如果 $nums$ 的最长上升子序列长度大于等于 $3$,那么原问题答案为 `True`,否则为 `False`。

而求最长上升子序列的最优解是有基于「贪心 + 二分」的 $O(n\log{n})$ 做法,对此不了解的同学,可以先看前置 🧀 :[LCS 问题与 LIS 问题的相互关系,以及 LIS 问题的最优解证明](https://mp.weixin.qq.com/s?__biz=MzU4NDE3MTEyMA==&mid=2247487814&idx=1&sn=e33023c2d474ff75af83eda1c4d01892&chksm=fd9cba59caeb334f1fbfa1aefd3d9b2ab6abfccfcab8cb1dbff93191ae9b787e1b4681bbbde3&token=252055586&lang=zh_CN#rd),里面详细讲解了 `LIS` 的贪心解证明,以及与最长公共子序列(`LCS` 问题)的相互关系,本次不再赘述。

**简单来说,就是在遍历每个数 $nums[i]$ 的同时,维护一个具有单调性的 $f[]$ 数组,其中 $f[len] = x$ 代表长度为 $len$ 的最长上升子序列最小结尾元素为 $x$,可以证明 $f$ 数组具有单调性(看 [前置🧀](https://mp.weixin.qq.com/s?__biz=MzU4NDE3MTEyMA==&mid=2247487814&idx=1&sn=e33023c2d474ff75af83eda1c4d01892&chksm=fd9cba59caeb334f1fbfa1aefd3d9b2ab6abfccfcab8cb1dbff93191ae9b787e1b4681bbbde3&token=252055586&lang=zh_CN#rd)),因此每次可以通过二分找到小于 $nums[i]$ 的最大下标,来作为 $nums[i]$ 的前一个数。**

综上,我们使用 `LIS` 的「贪心 + 二分」做法求得最长上升子序列的最大长度,然后和 $3$ 比较即可得出答案。

**代码(感谢 [@🍭可乐可乐吗QAQ](/u/littletime_cc/) 同学提供的其他语言版本):**
```Java
class Solution {
    public boolean increasingTriplet(int[] nums) {
        int n = nums.length, ans = 1;
        int[] f = new int[n + 1];
        Arrays.fill(f, 0x3f3f3f3f);
        for (int i = 0; i < n; i++) {
            int t = nums[i];
            int l = 1, r = i + 1;
            while (l < r) {
                int mid = l + r >> 1;
                if (f[mid] >= t) r = mid;
                else l = mid + 1;
            }
            f[r] = t;
            ans = Math.max(ans, r);
        }
        return ans >= 3;
    }
}
```
-
```C++
class Solution {
public:
    bool increasingTriplet(vector<int>& nums) {
        int n = nums.size(), ans = 1;
        vector<int> f(n + 1, INT_MAX);
        for(int i = 0; i < n; i++){
            int t = nums[i];
            int L = 1, R = i + 1;
            while(L < R){
                int mid = L + R >> 1;
                if(f[mid] >= t) R = mid;
                else L = mid + 1;
            }
            f[R] = t;
            ans = max(ans, R);
        }
        return ans >= 3;
    }
};
```
* 时间复杂度:$O(n\log{n})$
* 空间复杂度:$O(n)$

---

### 优化 : 定长上升子序列(贪心)

利用本题只需要我们判定是否存在长度为 $3$ 的上升子序列,而不需要回答 `LIS` 最大长度。

我们可以对 $f$ 数组进行优化:**使用有限变量进行替换(将 $f$ 数组的长度压缩为 $2$),数组含义不变,$f[1] = x$ 代表长度为 $1$ 的上升子序列最小结尾元素为 $x$,$f[2] = y$ 代表长度为 $2$ 的上升子序列的最小结尾元素为 $y$。**

**从前往后扫描每个 $nums[i]$,每次将 $nums[i]$ 分别与 $f[1]$ 和 $f[2]$ 进行比较,如果能够满足 $nums[i] > f[2]$,代表 $nums[i]$ 能够接在长度为 $2$ 的上升子序列的后面,直接返回 `True`,否则尝试使用 $nums[i]$ 来更新 $f[1]$ 和 $f[2]。$**

这样,我们只使用了有限变量,每次处理 $nums[i]$ 只需要和有限变量进行比较,时间复杂度为 $O(n)$,空间复杂度为 $O(1)$。

**代码(感谢 [@🍭可乐可乐吗QAQ](/u/littletime_cc/) 和 [@Benhao](/u/himymben/) 同学提供的其他语言版本):**
```Java
class Solution {
    public boolean increasingTriplet(int[] nums) {
        int n = nums.length;
        long[] f = new long[3];
        f[1] = f[2] = (int)1e19;
        for (int i = 0; i < n; i++) {
            int t = nums[i];
            if (f[2] < t) return true;
            else if (f[1] < t && t < f[2]) f[2] = t;
            else if (f[1] > t) f[1] = t;
        }
        return false;
    }
}
```
-
```Python
class Solution:
    def increasingTriplet(self, nums: List[int]) -> bool:
        n = len(nums)
        f = [inf] * 3
        for i in range(n):
            t = nums[i]
            if f[2] < t:
                return True
            elif f[1] < t < f[2]:
                f[2] = t
            elif f[1] > t:
                f[1] = t
        return False
```
-
```Go
func increasingTriplet(nums []int) bool {
    n := len(nums)
    f := []int{math.MaxInt32,math.MaxInt32,math.MaxInt32}
    for i := 0; i < n; i++ {
        t := nums[i]
        if f[2] < t{
            return true
        } else if f[1] < t && t < f[2]{
            f[2] = t
        } else if f[1] > t {
            f[1] = t
        }
    }
    return false
}
```
-
```C++
class Solution {
public:
    bool increasingTriplet(vector<int>& nums) {
        int n = nums.size();
        vector<int> f(3, INT_MAX);
        for(int i = 0; i < n; i++){
            int t = nums[i];
            if(f[2] < t) return true;
            else if(f[1] < t and t < f[2]) f[2] = t;
            else if(f[1] > t) f[1] = t;
        }
        return false;
    }
};
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


Question:
给你一个整数数组 `distance` 。

从 `X-Y` 平面上的点 $(0,0)$ 开始,先向北移动 `distance[0]` 米,然后向西移动 `distance[1]` 米,向南移动 `distance[2]` 米,向东移动 `distance[3]` 米,持续移动。也就是说,每次移动后你的方位会发生逆时针变化。

判断你所经过的路径是否相交。如果相交,返回 `true` ;否则,返回 `false`。

示例 1:

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

输出:true
```
示例 2:

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

输出:false
```
示例 3:

```
输入:distance = [1,1,1,1]

输出:true
```

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

Solution:
### 找规律 + 分情况讨论

这是一道画图找规律的题目。

**首先显然的是,至少需要 $4$ 条边才 可能 存在相交路径,如果 $distance$ 的长度小于 $4$,可直接返回 `False`,同时这引导我们如果某条边 $distance[i]$ 发生相交,不可能是与 $distance[i - 1]$ 或 $distance[i - 2]$ 之间发生的(基于条件 $1 <= distance[i] <= 10^5$)。**

为了方便,我们分别使用 $a$、$b$、$c$ 和 $d$ 来分别代指「往上」、「往左」、「往下」和「往右」的四类方向边。

然后对可能相交情况进行分情况讨论,假设当前枚举到的边为 $distance[i]$(下面使用 $d[i]$ 代指 $distance[i]$):

1. $d[i]$ 与 $d[i - 3]$ 发生相交:此时满足 $d[i] >= d[i - 2]$,同时 $d[i - 1] <= d[i - 3]$;

> **需要注意的时候 $d[i]$ 不一定是 $d$ 类边,而可以是 $abcd$ 任意类的边,此时只是矩形发生旋转,并不会影响路径相交的事实,其余分情况讨论也是同理。**

2. $d[i]$ 与 $d[i - 4]$ 发生相交:此时满足 $d[i - 1] = d[i - 3]$,同时 $d[i] + d[i - 4] >= d[i - 2]$;

3. $d[i]$ 与 $d[i - 5]$ 发生相交:此时满足$d[i - 1] <= d[i - 3]$,同时 $d[i - 2] > d[i - 4]$,同时 $d[i] + d[i - 4] >= d[i - 2]$,同时 $d[i - 1] + d[i - 5] >= d[i - 3]$。

**综上,$d[i]$ 不会与 $d[i - 1]$ 和 $d[i - 2]$ 发生相交,而 $d[i]$ 与 $d[i - 3]$、$d[i - 4]$ 和 $d[i - 5]$ 的相交条件如上述讨论。并且 $d[i]$ 不会与 $d[i - x]$ $(x > 5)$ 发生相交的同时,不与 $d[i - y]$ $(3 <= y <= 5)$ 发生相交。即 $d[i]$ 与 $d[i - x]$ $(x > 5)$ 发生相交前,必然与 $d[i - y]$ $(3 <= y <= 5)$ 发生相交。**

代码:
```Java
class Solution {
    public boolean isSelfCrossing(int[] d) {
        int n = d.length;
        if (n < 4) return false;
        for (int i = 3; i < n; i++) {
            if (d[i] >= d[i - 2] && d[i - 1] <= d[i - 3]) return true;
            if (i >= 4 && d[i - 1] == d[i - 3] && d[i] + d[i - 4] >= d[i - 2]) return true;
            if (i >= 5 && d[i - 1] <= d[i - 3] && d[i - 2] > d[i - 4] && d[i] + d[i - 4] >= d[i - 2] && d[i - 1] + d[i - 5] >= d[i - 3]) return true;
        }
        return false;
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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