149. 直线上最多的点数

Question:
给你一个数组 `points`,其中 $points[i] = [x_i, y_i]$ 表示 `X-Y` 平面上的一个点。求最多有多少个点在同一条直线上。

示例 1:

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

输出:3
```
示例 2:

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

输出:4
```

提示:
* $1 <= points.length <= 300$
* $points[i].length == 2$
* $-10^4 <= x_i, y_i <= 10^4$
* `points` 中的所有点 互不相同

Solution:
### 枚举直线 + 枚举统计

我们知道,两点可以确定一条线。

一个朴素的做法是先枚举两点(确定一条线),然后检查其余点是否落在该线中。

为避免除法精度问题,当我们枚举两个点 $x$ 和 $y$ 时,不直接计算其对应直线的 `斜率`和 `截距`。

而是通过判断 $x$ 和 $y$ 与第三个点 $p$ 形成的两条直线斜率是否相等,来得知点 $p$ 是否落在该直线上。

斜率相等的两条直线要么平行,要么重合。

平行需要 $4$ 个点来唯一确定,我们只有 $3$ 个点,因此直接判定两条直线是否重合即可。

详细说,当给定两个点 $(x_1, y_1)$ 和 $(x_2, y_2)$ 时,对应斜率 $\frac{y_2 - y_1}{x_2 - x_1}$。

为避免计算机除法的精度问题,我们将「判定 $\frac{a_y - b_y}{a_x - b_x} = \frac{b_y - c_y}{b_x - c_x}$ 是否成立」改为「判定 $(a_y - b_y) \times (b_x - c_x) = (a_x - b_x) \times (b_y - c_y)$ 是否成立」。

将存在精度问题的「除法判定」巧妙转为「乘法判定」。

Java 代码:
```Java
class Solution {
    public int maxPoints(int[][] points) {
        int n = points.length, ans = 1;
        for (int i = 0; i < n; i++) {
            int[] x = points[i];
            for (int j = i + 1; j < n; j++) {
                int[] y = points[j];
                // 枚举点对 (i,j) 并统计有多少点在该线上, 起始 cnt = 2 代表只有 i 和 j 两个点在此线上
                int cnt = 2;
                for (int k = j + 1; k < n; k++) {
                    int[] p = points[k];
                    int s1 = (y[1] - x[1]) * (p[0] - y[0]);
                    int s2 = (p[1] - y[1]) * (y[0] - x[0]);
                    if (s1 == s2) cnt++;
                }
                ans = Math.max(ans, cnt);
            }
        }
        return ans;
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    int maxPoints(vector<vector<int>>& points) {
        int n = points.size(), ans = 1;
        for (int i = 0; i < n; i++) {
            vector<int> x = points[i];
            for (int j = i + 1; j < n; j++) {
                vector<int> y = points[j];
                // 枚举点对 (i,j) 并统计有多少点在该线上, 起始 cnt = 2 代表只有 i 和 j 两个点在此线上
                int cnt = 2;
                for (int k = j + 1; k < n; k++) {
                    vector<int> p = points[k];
                    int s1 = (y[1] - x[1]) * (p[0] - y[0]);
                    int s2 = (p[1] - y[1]) * (y[0] - x[0]);
                    if (s1 == s2) cnt++;
                }
                ans = max(ans, cnt);
            }
        }
        return ans;
    }
};
```
Python 代码:
```Python
class Solution:
    def maxPoints(self, points: List[List[int]]) -> int:
        n, ans = len(points), 1
        for i, x in enumerate(points):
            for j in range(i + 1, n):
                y = points[j]
                # 枚举点对 (i,j) 并统计有多少点在该线上, 起始 cnt = 2 代表只有 i 和 j 两个点在此线上
                cnt = 2
                for k in range(j + 1, n):
                    p = points[k]
                    s1 = (y[1] - x[1]) * (p[0] - y[0])
                    s2 = (p[1] - y[1]) * (y[0] - x[0])
                    if s1 == s2: cnt += 1
                ans = max(ans, cnt)
        return ans
```
TypeScript 代码:
```TypeScript
function maxPoints(points: number[][]): number {
    let n = points.length, ans = 1;
    for (let i = 0; i < n; i++) {
        let x = points[i];
        for (let j = i + 1; j < n; j++) {
            // 枚举点对 (i,j) 并统计有多少点在该线上, 起始 cnt = 2 代表只有 i 和 j 两个点在此线上
            let y = points[j], cnt = 2;
            for (let k = j + 1; k < n; k++) {
                let p = points[k];
                let s1 = (y[1] - x[1]) * (p[0] - y[0]);
                let s2 = (p[1] - y[1]) * (y[0] - x[0]);
                if (s1 == s2) cnt++;
            }
            ans = Math.max(ans, cnt);
        }
    }
    return ans;
};
```
* 时间复杂度:$O(n^3)$
* 空间复杂度:$O(1)$

---

### 枚举直线 + 哈希表统计

根据「朴素解法」的思路,枚举所有直线的过程不可避免,但统计点数的过程可以优化。

具体的,我们可以先枚举所有可能出现的 `直线斜率`(根据两点确定一条直线,即枚举所有的「点对」),使用「哈希表」统计所有 `斜率` 对应的点的数量,在所有值中取个 $max$ 即是答案。

一些细节:在使用「哈希表」进行保存时,为了避免精度问题,我们直接使用字符串进行保存,同时需要将 `斜率` 约干净。

Java 代码:
```Java
class Solution {
    public int maxPoints(int[][] points) {
        int n = points.length, ans = 1;
        for (int i = 0; i < n; i++) {
            Map<String, Integer> map = new HashMap<>();
            // 由当前点 i 发出的直线所经过的最多点数量
            int max = 0;
            for (int j = i + 1; j < n; j++) {
                int x1 = points[i][0], y1 = points[i][1], x2 = points[j][0], y2 = points[j][1];
                int a = x1 - x2, b = y1 - y2;
                int k = gcd(a, b);
                String key = (a / k) + "_" + (b / k);
                map.put(key, map.getOrDefault(key, 0) + 1);
                max = Math.max(max, map.get(key));
            }
            ans = Math.max(ans, max + 1);
        }
        return ans;
    }
    int gcd(int a, int b) {
        return b == 0 ? a : gcd(b, a % b);
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    int maxPoints(vector<vector<int>>& points) {
        int n = points.size(), ans = 1;
        for (int i = 0; i < n; i++) {
            map<string, int> map;
            int maxv = 0;
            for (int j = i + 1; j < n; j++) {
                int x1 = points[i][0], y1 = points[i][1], x2 = points[j][0], y2 = points[j][1];
                int a = x1 - x2, b = y1 - y2;
                int k = gcd(a, b);
                string key = to_string(a / k) + "_" + to_string(b / k);
                map[key]++;
                maxv = max(maxv, map[key]);
            }
            ans = max(ans, maxv + 1);
        }
        return ans;
    }
    int gcd(int a, int b) {
        return b == 0 ? a : gcd(b, a % b);
    }
};
```
Python 代码:
```Python
class Solution:
    def maxPoints(self, points):
        def gcd(a, b):
            return a if b == 0 else gcd(b, a % b)

        n, ans = len(points), 1
        for i in range(n):
            mapping = {}
            maxv = 0
            for j in range(i + 1, n):
                x1, y1 = points[i]
                x2, y2 = points[j]
                a, b = x1 - x2, y1 - y2
                k = gcd(a, b)
                key = str(a // k) + "_" + str(b // k)
                mapping[key] = mapping.get(key, 0) + 1
                maxv = max(maxv, mapping[key])
            ans = max(ans, maxv + 1)
        return ans
```
TypeScript 代码:
```TypeScript
function maxPoints(points: number[][]): number {
    const gcd = function(a: number, b: number): number {
        return b == 0 ? a : gcd(b, a % b);
    }
    let n = points.length, ans = 1;
    for (let i = 0; i < n; i++) {
        let mapping = {}, maxv = 0;
        for (let j = i + 1; j < n; j++) {
            let x1 = points[i][0], y1 = points[i][1], x2 = points[j][0], y2 = points[j][1];
            let a = x1 - x2, b = y1 - y2;
            let k = gcd(a, b);
            let key = `${a / k}_${b / k}`;
            mapping[key] = mapping[key] ? mapping[key] + 1 : 1;
            maxv = Math.max(maxv, mapping[key]);
        }
        ans = Math.max(ans, maxv + 1);
    }
    return ans;
};
```
* 时间复杂度:枚举所有直线的复杂度为 $O(n^2)$;令坐标值的最大差值为 $m$,`gcd` 复杂度为 $O(\log{m})$。整体复杂度为 $O(n^2 \times \log{m})$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


150. 逆波兰表达式求值

Question:
根据 逆波兰表示法,求表达式的值。

有效的算符包括 +、-、*、/ 。每个运算对象可以是整数,也可以是另一个逆波兰表达式。

说明:
* 整数除法只保留整数部分。
* 给定逆波兰表达式总是有效的。换句话说,表达式总会得出有效数值且不存在除数为 0 的情况。

示例 1:
```
输入:tokens = ["2","1","+","3","*"]
输出:9
解释:该算式转化为常见的中缀算术表达式为:((2 + 1) * 3) = 9
```
示例 2:
```
输入:tokens = ["4","13","5","/","+"]
输出:6
解释:该算式转化为常见的中缀算术表达式为:(4 + (13 / 5)) = 6
```
示例 3:
```
输入:tokens = ["10","6","9","3","+","-11","*","/","*","17","+","5","+"]
输出:22
解释:
该算式转化为常见的中缀算术表达式为:
  ((10 * (6 / ((9 + 3) * -11))) + 17) + 5
= ((10 * (6 / (12 * -11))) + 17) + 5
= ((10 * (6 / -132)) + 17) + 5
= ((10 * 0) + 17) + 5
= (0 + 17) + 5
= 17 + 5
= 22
```

提示:
* 1 <= tokens.length <= $10^4$
* tokens[i] 要么是一个算符("+"、"-"、"*" 或 "/"),要么是一个在范围 [-200, 200] 内的整数

逆波兰表达式:

逆波兰表达式是一种后缀表达式,所谓后缀就是指算符写在后面。

* 平常使用的算式则是一种中缀表达式,如 ( 1 + 2 ) * ( 3 + 4 ) 。
* 该算式的逆波兰表达式写法为 ( ( 1 2 + ) ( 3 4 + ) * ) 。

逆波兰表达式主要有以下两个优点:

* 去掉括号后表达式无歧义,上式即便写成 1 2 + 3 4 + * 也可以依据次序计算出正确结果。
* 适合用栈操作运算:遇到数字则入栈;遇到算符则取出栈顶两个数字进行计算,并将结果压入栈中。

Solution:
### 基本思路

这是一道关于「表达式计算」的题目。

所有的「表达式计算」问题都离不开「栈」。

对于本题,我们可以建立一个「数字栈」,存放所有的数字,当遇到运算符时,从栈中取出两个数进行运算,并将结果放回栈内,整个过程结束后,栈顶元素就是最终结果。

而栈的实现通常有两种:使用数组继续模拟 & 使用系统自带的栈结构

***

### 数组模拟栈解法

代码:

```java []
class Solution {
    public int evalRPN(String[] ts) {
        int[] d = new int[ts.length];
        int hh = 0, tt = -1;
        for (String s : ts) {
            if ("+-*/".contains(s)) {
                int b = d[tt--], a = d[tt--];
                d[++tt] = calc(a, b, s);
            } else {
                d[++tt] = Integer.parseInt(s);
            }
        }
        return d[tt];
    }
    int calc(int a, int b, String op) {
        if (op.equals("+")) return a + b;
        else if (op.equals("-")) return a - b;
        else if (op.equals("*")) return a * b;
        else if (op.equals("/")) return a / b;
        else return -1;
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n)$

***

### 自带栈解法

代码:
```java
class Solution {
    public int evalRPN(String[] ts) {
        Deque<Integer> d = new ArrayDeque<>();
        for (String s : ts) {
            if ("+-*/".contains(s)) {
                int b = d.pollLast(), a = d.pollLast();
                d.addLast(calc(a, b, s));
            }  else {
                d.addLast(Integer.parseInt(s));
            }
        }
        return d.pollLast();
    }
    int calc(int a, int b, String op) {
        if (op.equals("+")) return a + b;
        else if (op.equals("-")) return a - b;
        else if (op.equals("*")) return a * b;
        else if (op.equals("/")) return a / b;
        else return -1;
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n)$

***

### 其他

关于「表达式计算」,类似的题目在上周的「每日一题」也出现过:

* [224. 基本计算器](https://leetcode-cn.com/problems/basic-calculator/solution/shuang-zhan-jie-jue-tong-yong-biao-da-sh-olym/):包含符号 `+ - ( )`

* [227. 基本计算器 II](https://leetcode-cn.com/problems/basic-calculator-ii/solution/shi-yong-shuang-zhan-jie-jue-jiu-ji-biao-c65k/) :包含符号 `+ - * /`

* [772. 基本计算器 III](https://leetcode-cn.com/problems/basic-calculator-iii/) :有锁题,包含符号 `+ - * / ( )`

* [770. 基本计算器 IV](https://leetcode-cn.com/problems/basic-calculator-iv/) : 包含自定义函数符号

### 最后

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

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

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

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


LeetCode/1411-1420/1413. 逐步求和得到正数的最小值（简单）.md

1413. 逐步求和得到正数的最小值

Question:
给你一个整数数组 `nums`。你可以选定任意的正数 `startValue` 作为初始值。

你需要从左到右遍历 `nums` 数组,并将 `startValue` 依次累加上 `nums` 数组中的值。

请你在确保累加和始终大于等于 $1$ 的前提下,选出一个最小的 正数 作为 `startValue`。

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

输出:5

解释:如果你选择 startValue = 4,在第三次累加时,和小于 1 。
                累加求和
                startValue = 4 | startValue = 5 | nums
                  (4 -3 ) = 1  | (5 -3 ) = 2    |  -3
                  (1 +2 ) = 3  | (2 +2 ) = 4    |   2
                  (3 -3 ) = 0  | (4 -3 ) = 1    |  -3
                  (0 +4 ) = 4  | (1 +4 ) = 5    |   4
                  (4 +2 ) = 6  | (5 +2 ) = 7    |   2
```
示例 2:
```
输入:nums = [1,2]

输出:1

解释:最小的 startValue 需要是正数。
```
示例 3:
```
输入:nums = [1,-2,-3]

输出:5
```

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

Solution:
### 模拟

由于 `startValue` 可设定的最小值为 $1$,我们可以先统计当 `startValue = 1` 时,累加过程中的最小值 `min`。

根据 `min` 是否符合规定(大于等于 $1$)来决定如何调整:

* 若 `min` 满足大于等于 $1$,则 `startValue` 可取最小值 $1$;
* 若 `min` 不满足大于等于 $1$,计算将 `min` 调整到 $1$ 所需要的值(即 `1 - min`),将其累加到起始值上,即得答案 `2 - min`。

Java 代码:
```Java
class Solution {
    public int minStartValue(int[] nums) {
        int n = nums.length, min = 0x3f3f3f3f;
        for (int i = 0, j = 1; i < n; i++) {
            j = j + nums[i];
            min = Math.min(min, j);
        }
        return min < 1 ? 2 - min : 1;
    }
}
```
TypeScript 代码:
```TypeScript
function minStartValue(nums: number[]): number {
    let n = nums.length, min = 0x3f3f3f3f
    for (let i = 0, j = 1; i < n; i++) {
        j = j + nums[i]
        min = Math.min(min, j)
    }
    return min < 1 ? 2 - min : 1
};
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


LeetCode/1411-1420/1414. 和为 K 的最少斐波那契数字数目（中等）.md

1414. 和为 K 的最少斐波那契数字数目

Question:
给你数字 `k` ,请你返回和为 `k` 的斐波那契数字的最少数目,其中,每个斐波那契数字都可以被使用多次。

斐波那契数字定义为:

* F1 = 1
* F2 = 1
* Fn = Fn-1 + Fn-2 , 其中 n > 2 。

数据保证对于给定的 `k` ,一定能找到可行解。

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

输出:2

解释:斐波那契数字为:1,1,2,3,5,8,13,......
对于 k = 7 ,我们可以得到 2 + 5 = 7 。
```
示例 2:
```
输入:k = 10

输出:2

解释:对于 k = 10 ,我们可以得到 2 + 8 = 10 。
```
示例 3:
```
输入:k = 19

输出:3

解释:对于 k = 19 ,我们可以得到 1 + 5 + 13 = 19 。
```

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

Solution:
### 打表 + 贪心 + 二分

利用 `k` 的数据范围为 $1 <= k <= 10^9$,可以先使用 `static` 进行打表,预处理出值不超过 $10^9$ 的斐波那契数,存入数组 `list` 中。

然后考虑如何使用 `list` 中的数字(可重复)凑成 `k`。

一个直观的想法是:**每次从 `list` 中找到不超过 `k` 的最大数,用于进行对 `k` 的消减,直到 `k` 被消减到 $0$ 为止,消减的次数即是答案。**

而「从 `list` 中找到不超过 `k` 的最大数」这一操作,可使用「二分」。

下面证明该做法的正确性:

假定该做法所得到的可行解序列为 `A`(序列长度为 $ans$),真实最优解序列为 `B`(序列长度为 $min$)。

假设两者长度不等(只能是 $ans > min$),这意味着我们将 `A` 和 `B` 之间的相同的元素去掉后,剩余元素序列有如下结论:

* 「`A` 序列剩余元素之和」等于「`B` 序列剩余元素之和」;
* 「`A` 序列剩余元素个数」大于「`B` 序列剩余元素个数」。

`A'` 为 `A` 的剩余元素序列,`B'` 为 `B` 的剩余元素序列。

我们知道 `A'` 中的最大数必然大于 `B'` 中的最大数。不可能是等于关系(相等元素均被去掉),也不可能是小于关系,否则在构造 `A` 序列的时候就会按照生成 `B` 序列的方向进行构造。

**但要只靠该性质,要证明不存在满足上述结论的斐波那契数组合仍是困难。**

**我们可以从「斐波那契数」性质出发,挖掘可行解 `A` 的某些性质,然后证明不存在不满足该性质的方案比 `A` 更优即可。**

对于可行解 `A` 而言,由于我们「每次都选择不超过当前 `k` 的最大数」,因此必然**可行解中必然不存在两项相邻的斐波那契数**,否则会在选择 $f_i$ 和 $f_{i+1}$ 之前先因为「每次都选择不超过当前 `k` 的最大数」而先选择 $f_{i+2}$。

同时,由于 $f_i=f_{i-1}+f_{i-2}$、$f_{i+1}=f_{i}+f_{i-1}$ 和 $f_{i+2}=f_{i+1}+f_{i}$ 可得 $2 * f_{i}=f_{i+1}+f_{i-2}$。

也就是说**可行解 `A` 中必然不会出现相同值**,否则会在选择 $f_i$ 之前先因为「每次都选择不超过当前 `k` 的最大数」而先选择 $f_{i+1}$。

该推理对于边界特殊值 $1$ 同样成立,如果可行解 `A` 中存在多个 $1$,则必然会先因为「每次都选择不超过当前 `k` 的最大数」选择 $f_3=2$。

**再根据「斐波那契数奇偶数项求和」可知:$a_1 + a_3 + ... + a_{2n - 1} = a_{2n}$ 和 $a_2 + a_4 + ... + a_{2n} = a_{2n+1} - 1$。**

**综上,可以证明不存在比可行解 `A` 更短的最优解。**

代码:
```Java
class Solution {
    static List<Integer> list = new ArrayList<>();
    static {
        list.add(1);
        int a = 1, b = 1;
        while (b <= (int)1e9) {
            int c = a + b;
            a = b; b = c;
            list.add(c);
        }
    }
    public int findMinFibonacciNumbers(int k) {
        int ans = 0;
        while (k != 0) {
            int l = 0, r = list.size() - 1;
            while (l < r) {
                int mid = l + r + 1 >> 1;
                if (list.get(mid) <= k) l = mid;
                else r = mid - 1;
            }
            k -= list.get(r);
            ans++;
        }
        return ans;
    }
}
```
* 时间复杂度:令值不超过 $10^9$ 的斐波那契数的数量为 `C`,复杂度为 $O(\log{k} * \log{C})$
* 空间复杂度:$O(C)$

---

### 贪心

上述做法,由于预处理了所有值不超过 $10^9$ 的斐波那契数,导致无法直接取得「值不超过 `k` 的最大数」,需要再套一层「二分」。

不采用预处理的方式,利用斐波那契数列的递推性质,以及凑成 `k` 的最优解中不包含重复数字的结论,我们可以做到 $O(\log{k})$ 时间复杂度和 $O(1)$ 空间复杂度。

代码:
```Java
class Solution {
    public int findMinFibonacciNumbers(int k) {
        int a = 1, b = 1;
        while (b <= k) {
            int c = a + b;
            a = b; b = c;
        }
        int ans = 0;
        while (k != 0) {
            if (k >= b) {
                k -= b; ans++;
            }
            int c = b - a;
            b = a; a = c;
        }
        return ans;
    }
}
```
* 时间复杂度:$O(\log{k})$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


LeetCode/1411-1420/1417. 重新格式化字符串（简单）.md

1417. 重新格式化字符串

Question:
给你一个混合了数字和字母的字符串 `s`,其中的字母均为小写英文字母。

请你将该字符串重新格式化,使得任意两个相邻字符的类型都不同。也就是说,字母后面应该跟着数字,而数字后面应该跟着字母。

请你返回重新格式化后的字符串;如果无法按要求重新格式化,则返回一个空字符串。

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

输出:"0a1b2c"

解释:"0a1b2c" 中任意两个相邻字符的类型都不同。 "a0b1c2", "0a1b2c", "0c2a1b" 也是满足题目要求的答案。
```
示例 2:
```
输入:s = "leetcode"

输出:""

解释:"leetcode" 中只有字母,所以无法满足重新格式化的条件。
```
示例 3:
```
输入:s = "1229857369"

输出:""

解释:"1229857369" 中只有数字,所以无法满足重新格式化的条件。
```
示例 4:
```
输入:s = "covid2019"

输出:"c2o0v1i9d"
```
示例 5:
```
输入:s = "ab123"

输出:"1a2b3"
```

提示:
* $1 <= s.length <= 500$
* `s` 仅由小写英文字母和/或数字组成。

Solution:
### 模拟

起始先对字符串 `s` 进行遍历,将其分成字母串和数字串,若两者长度差值超过 $1$,说明无法构造成间隔字符串(返回空串),否则对两字符串使用双指针算法进行构造。

Java 代码:
```Java
class Solution {
    public String reformat(String s) {
        StringBuilder a = new StringBuilder(), b = new StringBuilder();
        for (char c : s.toCharArray()) {
            if (c >= 'a') a.append(c);
            else b.append(c);
        }
        int n = a.length(), m = b.length(), tot = n + m;
        if (Math.abs(n - m) > 1) return "";
        StringBuilder sb = new StringBuilder();
        while (sb.length() != tot) {
            if (n > m) sb.append(a.charAt(--n));
            else if (n < m) sb.append(b.charAt(--m));
            else {
                if (!sb.isEmpty() && sb.charAt(sb.length() - 1) >= 'a') sb.append(b.charAt(--m));
                else sb.append(a.charAt(--n));
            }
        }
        return sb.toString();
    }
}
```
TypeScript 代码:
```TypeScript
function reformat(s: string): string {
    let a = "", b = ""
    for (let i = 0; i < s.length; i++) {
        if (s[i] >= 'a') a += s[i]
        else b += s[i]
    }
    let n = a.length, m = b.length, tot = n + m
    if (Math.abs(n - m) > 1) return ""
    let ans = ""
    while (ans.length < tot) {
        if (n < m) ans += b[--m]
        else if (n > m) ans += a[--n]
        else {
            if (ans.length != 0 && ans[ans.length - 1] >= 'a') ans += b[--m]
            else ans += a[--n]
        }
    }
    return ans
};
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


Question:
给你一个数组 orders,表示客户在餐厅中完成的订单,确切地说, `orders[i]=[customerNamei,tableNumberi,foodItemi] `,其中 `customerNamei `是客户的姓名,`tableNumberi `是客户所在餐桌的桌号,而 `foodItemi `是客户点的餐品名称。

请你返回该餐厅的 点菜展示表 。在这张表中,表中第一行为标题,其第一列为餐桌桌号 “Table” ,后面每一列都是按字母顺序排列的餐品名称。接下来每一行中的项则表示每张餐桌订购的相应餐品数量,第一列应当填对应的桌号,后面依次填写下单的餐品数量。

注意:客户姓名不是点菜展示表的一部分。此外,表中的数据行应该按餐桌桌号升序排列。

示例 1:
```
输入:orders = [["David","3","Ceviche"],["Corina","10","Beef Burrito"],["David","3","Fried Chicken"],["Carla","5","Water"],["Carla","5","Ceviche"],["Rous","3","Ceviche"]]

输出:[["Table","Beef Burrito","Ceviche","Fried Chicken","Water"],["3","0","2","1","0"],["5","0","1","0","1"],["10","1","0","0","0"]]

解释:
点菜展示表如下所示:
Table,Beef Burrito,Ceviche,Fried Chicken,Water
3    ,0           ,2      ,1            ,0
5    ,0           ,1      ,0            ,1
10   ,1           ,0      ,0            ,0
对于餐桌 3:David 点了 "Ceviche" 和 "Fried Chicken",而 Rous 点了 "Ceviche"
而餐桌 5:Carla 点了 "Water" 和 "Ceviche"
餐桌 10:Corina 点了 "Beef Burrito"
```
示例 2:
```
输入:orders = [["James","12","Fried Chicken"],["Ratesh","12","Fried Chicken"],["Amadeus","12","Fried Chicken"],["Adam","1","Canadian Waffles"],["Brianna","1","Canadian Waffles"]]

输出:[["Table","Canadian Waffles","Fried Chicken"],["1","2","0"],["12","0","3"]]

解释:
对于餐桌 1:Adam 和 Brianna 都点了 "Canadian Waffles"
而餐桌 12:James, Ratesh 和 Amadeus 都点了 "Fried Chicken"
```
示例 3:
```
输入:orders = [["Laura","2","Bean Burrito"],["Jhon","2","Beef Burrito"],["Melissa","2","Soda"]]

输出:[["Table","Bean Burrito","Beef Burrito","Soda"],["2","1","1","1"]]
```

提示:
* 1 <= orders.length <= 5 * $10^4$
* orders[i].length == 3
* 1 <= customerNamei.length, foodItemi.length <= 20
* customerNamei 和 foodItemi 由大小写英文字母及空格字符 ' ' 组成。
* tableNumberi 是 1 到 500 范围内的整数。

Solution:
### 基本分析

这是一道考虑「数据结构运用」与「简单设计」的模拟题。

我们可以根据最终的 “结果” 反推数据结构存储格式。

最终 “结果” 包含两部分:

1. `title` : 由 **"Table" + 排序去重的餐品** 组成
2. 内容 : 由 **桌号 + 每件餐品对应的数量** 组成

基于此,不难设计出使用 `Set` 存储 `title` 相关内容,使用 `Map` 存储内容相关部分。

去重 `Map` 的部分 `Key` 为桌号,同时为了快速索引当前桌号「某个餐品的数量」,需要再套一层 `Map`。即最终存储格式为 `桌号 : {餐品 : 个数}`。

---

### HashSet & HashMap

有了基本分析,我们可以使用最常规的 `HashSet` 和 `HashMap` 进行实现。

由于 `HashSet` 是基于 `HashMap`,而 `HashMap` 的底层数据结构实现是 **哈希表**,因此我们需要在构造答案时手动排个序。

代码:
```Java
class Solution {
    public List<List<String>> displayTable(List<List<String>> os) {
        List<List<String>> ans = new ArrayList<>();
        // 桌号 : {餐品 : 个数}(用于构造内容)
        Map<Integer, Map<String, Integer>> tm = new HashMap<>();
        // 餐品(用于构造 title)
        Set<String> ts = new HashSet<>();
        for (List<String> o : os) {
            String c = o.get(0), t = o.get(1), f = o.get(2);
            Integer tidx = Integer.parseInt(t);
            ts.add(f);
            Map<String, Integer> map = tm.getOrDefault(tidx, new HashMap<>());
            map.put(f, map.getOrDefault(f, 0) + 1);
            tm.put(tidx, map);
        }
        int n = tm.size() + 1, m = ts.size() + 1;
        // 构造 title & 手动排序
        List<String> foods = new ArrayList<>(ts);
        Collections.sort(foods);
        List<String> title = new ArrayList<>();
        title.add("Table");
        title.addAll(foods);
        ans.add(title);
        // 构造内容 & 手动排序
        List<Integer> tables = new ArrayList<>(tm.keySet());
        Collections.sort(tables);
        for (int tidx : tables) {
            Map<String, Integer> map = tm.get(tidx);
            List<String> cur = new ArrayList<>();
            cur.add(tidx + "");
            for (String food : foods) {
                cur.add(map.getOrDefault(food, 0) + "");
            }
            ans.add(cur);
        }
        return ans;
    }
}
```
* 时间复杂度:`HashSet` 和 `HashMap` 的基本操作都是 $O(1)$。预处理所有的订单复杂度为 $O(n)$;去重后的桌数为 $r$,餐品数量为 $c$,对两者排序的复杂度分别为 $O(r\log{r})$ 和 $O(c\log{c})$;构造答案复杂度为 $O(r * c)$;最终复杂度为 $O(\max(n, r\log{r}, c\log{c}, r * c))$
* 空间复杂度:$O(r + c + r * c)$

---

### TreeSet & TreeMap

与 `HashSet` 和 `HashMap` 的关系类似,`TreeSet` 是基于 `TreeMap` 实现的,而 `TreeMap` 底层数据结构实现是 **红黑树**。

**得益于 Java 的「面向接口编程(IOP)」设计,我们可以毫不费力的将解法一中的 `HashSet` 替换成 `TreeSet`、将 `HashMap` 替换成 `TreeMap`,并删除手动排序相关代码,得到我们的解法二。**

利用 `TreeMap` 的默认排序规则(数值升序、非数值字典序升序)来简化我们的实现。

但需要注意的是,利用 `TreeMap` 的内部有序特性,调整操作可能会发生在每一次插入操作中,而解法一则是利用 `Collections.sort` 进行一次性的排序,对于非自定义类 `Collections.sort` 是基于 `Arrays.sort` 实现的,会根据「数组大小」、「数组本身是否大致有序」等因素综合决定最终的排序方案,在数据完全随机的情况下,执行效率很大程度要优于 `TreeMap` 的多次调整,但两者复杂度都是 $O(n\log{n})$。

因此在所有数据都提前给定的「离线」情况下,其实更推荐使用解法一。

代码:
```Java
class Solution {
    public List<List<String>> displayTable(List<List<String>> os) {
        List<List<String>> ans = new ArrayList<>();
        // 桌号 : {餐品 : 个数}(用于构造内容)
        Map<Integer, Map<String, Integer>> tm = new TreeMap<>();
        // 餐品(用于构造 title)
        Set<String> ts = new TreeSet<>();
        for (List<String> o : os) {
            String c = o.get(0), t = o.get(1), f = o.get(2);
            Integer tidx = Integer.parseInt(t);
            ts.add(f);
            Map<String, Integer> map = tm.getOrDefault(tidx, new HashMap<>());
            map.put(f, map.getOrDefault(f, 0) + 1);
            tm.put(tidx, map);
        }
        int n = tm.size() + 1, m = ts.size() + 1;
        // 构造 title
        List<String> title = new ArrayList<>();
        title.add("Table");
        title.addAll(ts);
        ans.add(title);
        // 构造内容
        for (int tidx : tm.keySet()) {
            Map<String, Integer> map = tm.get(tidx);
            List<String> cur = new ArrayList<>();
            cur.add(tidx + "");
            for (String food : ts) {
                cur.add(map.getOrDefault(food, 0) + "");
            }
            ans.add(cur);
        }
        return ans;
    }
}
```
* 时间复杂度:`TreeSet` 和 `TreeMap` 的基本操作都是 $O(log{k})$。预处理所有的订单复杂度为 $O(n\log{n})$;去重后的桌数为 $r$,餐品数量为 $c$,构造答案复杂度为 $O(r\log{r} * c\log{c})$;最终复杂度为 $O(\max(n\log{n}, r\log{r} * c\log{c}))$
* 空间复杂度:$O(r + c + r * c)$

### 最后

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

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

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

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


LeetCode/1421-1430/1422. 分割字符串的最大得分（简单）.md

1422. 分割字符串的最大得分

Question:
给你一个由若干 `0` 和 `1` 组成的字符串 `s` ,请你计算并返回将该字符串分割成两个非空子字符串(即左子字符串和右子字符串)所能获得的最大得分。

「分割字符串的得分」为左子字符串中 `0` 的数量加上右子字符串中 `1` 的数量。

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

输出:5

解释:
将字符串 s 划分为两个非空子字符串的可行方案有:
左子字符串 = "0" 且 右子字符串 = "11101",得分 = 1 + 4 = 5
左子字符串 = "01" 且 右子字符串 = "1101",得分 = 1 + 3 = 4
左子字符串 = "011" 且 右子字符串 = "101",得分 = 1 + 2 = 3
左子字符串 = "0111" 且 右子字符串 = "01",得分 = 1 + 1 = 2
左子字符串 = "01110" 且 右子字符串 = "1",得分 = 2 + 1 = 3
```
示例 2:
```
输入:s = "00111"

输出:5

解释:当 左子字符串 = "00" 且 右子字符串 = "111" 时,我们得到最大得分 = 2 + 3 = 5
```
示例 3:
```
输入:s = "1111"

输出:3
```

提示:
* $2 <= s.length <= 500$
* 字符串 `s` 仅由字符 `'0'` 和 `'1'` 组成。

Solution:
### 前缀和

构建前缀和数组来记录每个前缀中 $1$ 个个数,复杂度为 $O(n)$,枚举每个分割点,搭配前缀和数组计算左串中 $0$ 的数量和右串中 $1$ 的数量,取所有得分的最大值即是答案。

Java 代码:
```Java
class Solution {
    public int maxScore(String s) {
        int n = s.length(), ans = 0;
        int[] sum = new int[n + 10];
        for (int i = 1; i <= n; i++) sum[i] = sum[i - 1] + (s.charAt(i - 1) - '0');
        for (int i = 1; i <= n - 1; i++) {
            int a = i - sum[i], b = sum[n] - sum[i];
            ans = Math.max(ans, a + b);
        }
        return ans;
    }
}
```
Typescript 代码:
```Typescript
function maxScore(s: string): number {
    let n = s.length, ans = 0
    const sum = new Array<number>(n + 10).fill(0)
    for (let i = 1; i <= n; i++) sum[i] = sum[i - 1] + (s[i - 1] == '1' ? 1 : 0)
    for (let i = 1; i <= n - 1; i++) {
        const a = i - sum[i], b = sum[n] - sum[i]
        ans = Math.max(ans, a + b)
    }
    return ans
};
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n)$

---

### 模拟

更进一步,利用 `s` 中有只有 $0$ 和 $1$,我们可以遍遍历 `s` 边计算得分(而无须预处理前缀和数组),起始分割点为 `s[0]`,此时得分为 `s[0]` 中 $0$ 的个数加上 `s[1...(n-1)]` 中 $1$ 的个数。

然后继续往后处理 `s`,当 $s[i] = 0$,说明有一个 $0$ 从右串中移到了左串,并且 $0$ 在右串中不得分,在左串中得分,因此总得分加一;而当 $s[i] = 1$,说明有一个 $1$ 从右串中移到了左串,而 $1$ 在右串中得分,在左串中不得分,因此总得分减一。在所有得分中取最大值即是答案。

Java 代码:
```Java
class Solution {
    public int maxScore(String s) {
        int n = s.length(), cur = s.charAt(0) == '0' ? 1 : 0;
        for (int i = 1; i < n; i++) cur += s.charAt(i) - '0';
        int ans = cur;
        for (int i = 1; i < n - 1; i++) {
            cur += s.charAt(i) == '0' ? 1 : -1;
            ans = Math.max(ans, cur);
        }
        return ans;
    }
}
```
TypeScript 代码:
```TypeScript
function maxScore(s: string): number {
    let n = s.length, cur = s[0] == '0' ? 1 : 0
    for (let i = 1; i < n; i++) cur += s[i] == '1' ? 1 : 0
    let ans = cur
    for (let i = 1; i < n - 1; i++) {
        cur += s[i] == '0' ? 1 : -1
        ans = Math.max(ans, cur)
    }
    return ans
};
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


LeetCode/1421-1430/1423. 可获得的最大点数（中等）.md

1423. 可获得的最大点数

Question:
几张卡牌排成一行,每张卡牌都有一个对应的点数,点数由整数数组 `cardPoints` 给出。

每次行动,你可以从行的开头或者末尾拿一张卡牌,最终你必须正好拿 `k` 张卡牌。

你的点数就是你拿到手中的所有卡牌的点数之和。

给你一个整数数组 `cardPoints` 和整数 `k`,请你返回可以获得的最大点数。

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

输出:12

解释:第一次行动,不管拿哪张牌,你的点数总是 1 。但是,先拿最右边的卡牌将会最大化你的可获得点数。最优策略是拿右边的三张牌,最终点数为 1 + 6 + 5 = 12 。
```
示例 2:
```
输入:cardPoints = [2,2,2], k = 2

输出:4

解释:无论你拿起哪两张卡牌,可获得的点数总是 4 。
```
示例 3:
```
输入:cardPoints = [9,7,7,9,7,7,9], k = 7

输出:55

解释:你必须拿起所有卡牌,可以获得的点数为所有卡牌的点数之和。
```
示例 4:
```
输入:cardPoints = [1,1000,1], k = 1

输出:1

解释:你无法拿到中间那张卡牌,所以可以获得的最大点数为 1 。
```
示例 5:
```
输入:cardPoints = [1,79,80,1,1,1,200,1], k = 3

输出:202
```

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

Solution:
### 滑动窗口

从两边选卡片,选 `k` 张,卡片总数量为 `n` 张,即有 `n - k` 张不被选择。

所有卡片总和 `sum` 固定,要使选择的 `k` 张的总和最大,反过来就是要让不被选择的 `n - k` 张总和最小。

原问题等价为:**从 `cardPoints` 中找长度为 `n - k` 的连续段,使其总和最小。**

具体的,用变量 `sum` 代指 `cardPoints` 总和,`cur` 代表长度固定为 `n - k` 的当前窗口总和,`minv` 代表所有长度为 `n - k` 的窗口中总和最小的值。

起始先将滑动窗口压满,取得第一个滑动窗口的目标值 `cur`(同时更新为 `minv`),随后往后继续处理 `cardPoints`,每往前滑动一位,需要删除一个和添加一个元素,并不断更新 `minv`,最终 `sum - minv` 即是答案。

Java 代码:
```Java
class Solution {
    public int maxScore(int[] cardPoints, int k) {
        int n = cardPoints.length, len = n - k;
        int sum = 0, cur = 0;
        for (int i = 0; i < n; i++) sum += cardPoints[i];
        for (int i = 0; i < len; i++) cur += cardPoints[i];
        int minv = cur;
        for (int i = len; i < n; i++) {
            cur = cur + cardPoints[i] - cardPoints[i - len];
            minv = Math.min(minv, cur);
        }
        return sum - minv;
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    int maxScore(vector<int>& cardPoints, int k) {
        int n = cardPoints.size(), len = n - k;
        int sum = 0, cur = 0;
        for (int i = 0; i < n; i++) sum += cardPoints[i];
        for (int i = 0; i < len; i++) cur += cardPoints[i];
        int minv = cur;
        for (int i = len; i < n; i++) {
            cur = cur + cardPoints[i] - cardPoints[i - len];
            minv = min(minv, cur);
        }
        return sum - minv;
    }
};
```
Python 代码:
```Python
class Solution:
    def maxScore(self, cardPoints: List[int], k: int) -> int:
        n, m = len(cardPoints), len(cardPoints) - k
        total, cur = sum(cardPoints), sum(cardPoints[:m])
        minv = cur
        for i in range(m, n):
            cur = cur + cardPoints[i] - cardPoints[i - m]
            minv = min(minv, cur)
        return total - minv
```
TypeScript 代码:
```TypeScript
function maxScore(cardPoints: number[], k: number): number {
    const n = cardPoints.length, m = n - k;
    let tot = 0, cur = 0;
    for (let i = 0; i < n; i++) tot += cardPoints[i];
    for (let i = 0; i < m; i++) cur += cardPoints[i];
    let minv = cur;
    for (let i = m; i < n; i++) {
        cur = cur + cardPoints[i] - cardPoints[i - m];
        minv = Math.min(minv, cur);
    }
    return tot - minv;
};
```
* 时间复杂度:每个元素最多滑入和滑出窗口一次,复杂度为 $O(n)$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


Question:
给你一份旅游线路图,该线路图中的旅行线路用数组 paths 表示,其中 paths[i] = [cityAi, cityBi] 表示该线路将会从 cityAi 直接前往 cityBi 。请你找出这次旅行的终点站,即没有任何可以通往其他城市的线路的城市。

题目数据保证线路图会形成一条不存在循环的线路,因此恰有一个旅行终点站。

示例 1:
```
输入:paths = [["London","New York"],["New York","Lima"],["Lima","Sao Paulo"]]

输出:"Sao Paulo"

解释:从 "London" 出发,最后抵达终点站 "Sao Paulo" 。本次旅行的路线是 "London" -> "New York" -> "Lima" -> "Sao Paulo" 。
```
示例 2:
```
输入:paths = [["B","C"],["D","B"],["C","A"]]

输出:"A"

解释:所有可能的线路是:
"D" -> "B" -> "C" -> "A".
"B" -> "C" -> "A".
"C" -> "A".
"A".
显然,旅行终点站是 "A" 。
```
示例 3:
```
输入:paths = [["A","Z"]]

输出:"Z"
```

提示:
* 1 <= paths.length <= 100
* paths[i].length == 2
* 1 <= cityAi.length, cityBi.length <= 10
* cityAi != cityBi
* 所有字符串均由大小写英文字母和空格字符组成。

Solution:
### 模拟 + 哈希表

根据题意,我们可以取一个任意城市作为起点,然后使用 $paths$ 中的路线信息开始搜索,直到当前城市无法到达下一个城市,即是答案。

实现上,为了可以快速找到某个城市所能到达的城市,可以先使用哈希表对 $paths$ 中的路线信息进行预处理。

代码:
```Java
class Solution {
    public String destCity(List<List<String>> ps) {
        Map<String, String> map = new HashMap<>();
        for (List<String> p : ps) map.put(p.get(0), p.get(1));
        String ans = ps.get(0).get(0);
        while (map.containsKey(ans)) ans = map.get(ans);
        return ans;
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


LeetCode/1431-1440/1438. 绝对差不超过限制的最长连续子数组（中等）.md

1438. 绝对差不超过限制的最长连续子数组

Question:
给你一个整数数组 nums ,和一个表示限制的整数 limit,请你返回最长连续子数组的长度,该子数组中的任意两个元素之间的绝对差必须小于或者等于 limit 。

如果不存在满足条件的子数组,则返回 0 。

示例 1:
```
输入:nums = [8,2,4,7], limit = 4
输出:2
解释:所有子数组如下:
[8] 最大绝对差 |8-8| = 0 <= 4.
[8,2] 最大绝对差 |8-2| = 6 > 4.
[8,2,4] 最大绝对差 |8-2| = 6 > 4.
[8,2,4,7] 最大绝对差 |8-2| = 6 > 4.
[2] 最大绝对差 |2-2| = 0 <= 4.
[2,4] 最大绝对差 |2-4| = 2 <= 4.
[2,4,7] 最大绝对差 |2-7| = 5 > 4.
[4] 最大绝对差 |4-4| = 0 <= 4.
[4,7] 最大绝对差 |4-7| = 3 <= 4.
[7] 最大绝对差 |7-7| = 0 <= 4.
因此,满足题意的最长子数组的长度为 2 。
```
示例 2:
```
输入:nums = [10,1,2,4,7,2], limit = 5
输出:4
解释:满足题意的最长子数组是 [2,4,7,2],其最大绝对差 |2-7| = 5 <= 5 。
```
示例 3:
```
输入:nums = [4,2,2,2,4,4,2,2], limit = 0
输出:3
```

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

Solution:
### 二分 + 滑动窗口

数据范围是 $10^5$,因此只能考虑「对数解法」和「线性解法」。

对数解法很容易想到「二分」。

在给定 `limit` 的情况下,倘若有「恰好」满足条件的区间长度为 `len`,必然存在满足条件且长度小于等于 `len` 的区间,同时必然不存在长度大于 `len` 且满足条件的区间。

因此长度 `len` 在数轴中具有「二段性」。

**问题转化为「如何判断 `nums` 中是否有长度 `len` 的区间满足绝对值不超过 `limit`」**

我们可以枚举区间的右端点 `r`,那么对应的左端点为 `r - len + 1`,然后使用「单调队列」来保存区间的最大值和最小值。

```java
class Solution {
    public int longestSubarray(int[] nums, int limit) {
        int n = nums.length;
        int l = 1, r = n;
        while (l < r) {
            int mid = l + r + 1 >> 1;
            if (check(nums, mid, limit)) {
                l = mid;
            } else {
                r = mid - 1;
            }
        }
        return r;
    }
    boolean check(int[] nums, int len, int limit) {
        int n = nums.length;
        Deque<Integer> max = new ArrayDeque<>(), min = new ArrayDeque<>();
        for (int r = 0, l = r - len + 1; r < n; r++, l = r - len + 1) {
            if (!max.isEmpty() && max.peekFirst() < l) max.pollFirst();
            while (!max.isEmpty() && nums[r] >= nums[max.peekLast()]) max.pollLast();
            max.addLast(r);
            if (!min.isEmpty() && min.peekFirst() < l) min.pollFirst();
            while (!min.isEmpty() && nums[r] <= nums[min.peekLast()]) min.pollLast();
            min.addLast(r);
            if (l >= 0 && Math.abs(nums[max.peekFirst()] - nums[min.peekFirst()]) <= limit) return true;
        }
        return false;
    }
}
```
* 时间复杂度:枚举长度的复杂度为 $O(\log{n})$,对于每次 `check` 而言,每个元素最多入队和出队常数次,复杂度为 $O(n)$。整体复杂度为 $O(n\log{n})$
* 空间复杂度:$O(n)$

***

### 双指针

上述解法我们是在对 `len` 进行二分,而事实上我们可以直接使用「双指针」解法找到最大值。

始终让右端点 `r` 右移,当不满足条件时让 `l` 进行右移。

同时,还是使用「单调队列」保存我们的区间最值,这样我们只需要对数组进行一次扫描即可得到答案。

```Java
class Solution {
    public int longestSubarray(int[] nums, int limit) {
        int n = nums.length;
        int ans = 0;
        Deque<Integer> max = new ArrayDeque<>(), min = new ArrayDeque<>();
        for (int r = 0, l = 0; r < n; r++) {
            while (!max.isEmpty() && nums[r] >= nums[max.peekLast()]) max.pollLast();
            while (!min.isEmpty() && nums[r] <= nums[min.peekLast()]) min.pollLast();
            max.addLast(r);
            min.addLast(r);
            while (Math.abs(nums[max.peekFirst()] - nums[min.peekFirst()]) > limit) {
                l++;
                if (max.peekFirst() < l) max.pollFirst();
                if (min.peekFirst() < l) min.pollFirst();
            }
            ans = Math.max(ans, r - l + 1);
        }
        return ans;
    }
}
```
* 时间复杂度:每个元素最多入队和出队常数次,复杂度为 $O(n)$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


LeetCode/1441-1450/1441. 用栈操作构建数组（中等）.md

1441. 用栈操作构建数组

Question:
给你一个数组 `target` 和一个整数 `n`。

每次迭代,需要从  `list = { 1 , 2 , 3 ..., n }` 中依次读取一个数字。

请使用下述操作来构建目标数组 `target` :

* `"Push"`:从 `list` 中读取一个新元素, 并将其推入数组中。
* `"Pop"`:删除数组中的最后一个元素。

如果目标数组构建完成,就停止读取更多元素。

题目数据保证目标数组严格递增,并且只包含 `1` 到 `n` 之间的数字。

请返回构建目标数组所用的操作序列。如果存在多个可行方案,返回任一即可。

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

输出:["Push","Push","Pop","Push"]

解释:
读取 1 并自动推入数组 -> [1]
读取 2 并自动推入数组,然后删除它 -> [1]
读取 3 并自动推入数组 -> [1,3]
```
示例 2:
```
输入:target = [1,2,3], n = 3

输出:["Push","Push","Push"]
```
示例 3:
```
输入:target = [1,2], n = 4

输出:["Push","Push"]

解释:只需要读取前 2 个数字就可以停止。
```

提示:
* $1 <= target.length <= 100$
* $1 <= n <= 100$
* $1 <= target[i] <= n$
* `target` 严格递增

Solution:
### 模拟

根据题意进行模拟即可。

每次我们将当前处理到 `i` 压入栈中(往答案添加一个 `Push`),然后判断当前处理到的 `i` 是否最新的栈顶元素 $target[j]$ 是否相同。

若不相同则丢弃元素(往答案添加一个 `Pop`),若存在则将指针 `j` 后移,直到构建出目标答案。

Java 代码:
```Java
class Solution {
    public List<String> buildArray(int[] target, int n) {
        List<String> ans = new ArrayList<>();
        int m = target.length;
        for (int i = 1, j = 0; i <= n && j < m; i++) {
            ans.add("Push");
            if (target[j] != i) ans.add("Pop");
            else j++;
        }
        return ans;
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    vector<string> buildArray(vector<int>& target, int n) {
        int m = target.size();
        vector<string> ans;
        for (int i = 1, j = 0; i <= n && j < m; i++) {
            ans.push_back("Push");
            if (target[j] != i) ans.push_back("Pop");
            else j++;
        }
        return ans;
    }
};
```
Python3 代码:
```Python
class Solution:
    def buildArray(self, target: List[int], n: int) -> List[str]:
        ans = []
        m, i, j = len(target), 1, 0
        while i <= n and j < m:
            ans.append("Push")
            if target[j] != i:
                ans.append("Pop")
            else:
                j += 1
            i += 1
        return ans
```
TypeScript 代码:
```TypeScript
function buildArray(target: number[], n: number): string[] {
    const ans = new Array<string>()
    const m = target.length
    for (let i = 1, j = 0; i <= n && j < m; i++) {
        ans.push("Push")
        if (target[j] != i) ans.push("Pop")
        else j++
    }
    return ans
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


LeetCode/1441-1450/1442. 形成两个异或相等数组的三元组数目（中等）.md

1442. 形成两个异或相等数组的三元组数目

Question:
给你一个整数数组 arr 。

现需要从数组中取三个下标 i、j 和 k ,其中 (0 <= i < j <= k < arr.length) 。

a 和 b 定义如下:
* a = arr[i] ^ arr[i + 1] ^ ... ^ arr[j - 1]
* b = arr[j] ^ arr[j + 1] ^ ... ^ arr[k]
注意:^ 表示 按位异或 操作。

请返回能够令 a == b 成立的三元组 (i, j , k) 的数目。

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

输出:4

解释:满足题意的三元组分别是 (0,1,2), (0,2,2), (2,3,4) 以及 (2,4,4)
```
示例 2:
```
输入:arr = [1,1,1,1,1]

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

输出:0
```
示例 4:
```
输入:arr = [1,3,5,7,9]

输出:3
```
示例 5:
```
输入:arr = [7,11,12,9,5,2,7,17,22]

输出:8
```

提示:
* 1 <= arr.length <= 300
* 1 <= arr[i] <= $10^8$

Solution:
### 基本分析

数据范围是 $10^2$,三元组包含 $i$、$j$ 和 $k$ 三个下标,因此通过「枚举下标」并「每次循环计算异或结果」的 $O(n^4)$ 朴素做法不用考虑了。

相信做过 [1310. 子数组异或查询](https://leetcode-cn.com/problems/xor-queries-of-a-subarray/solution/gong-shui-san-xie-yi-ti-shuang-jie-shu-z-rcgu/) 的同学不难想到可以使用「树状数组」或者「前缀异或」来优化我们「每次循环计算异或结果」的过程。

由于不涉及修改操作,我们优先使用「前缀异或」。经过这样优化之后的复杂度是 $O(n^3)$,可以过。

---

### 前缀异或

预处理出「前缀异或」数组,并枚举三元组的下标。

**本质上是利用集合(区间结果)的容斥原理。只不过前缀和需要利用「减法(逆运算)」做容斥,而前缀异或是利用「相同数值进行异或结果为 $0$(偶数次的异或结果为 $0$)」的特性实现容斥。**

代码:
```Java
class Solution {
    public int countTriplets(int[] arr) {
        int n = arr.length;
        int[] sum = new int[n + 1];
        for (int i = 1; i <= n; i++) sum[i] = sum[i - 1] ^ arr[i - 1];
        int ans = 0;
        for (int i = 1; i <= n; i++) {
            for (int j = i + 1; j <= n; j++) {
                for (int k = j; k <= n; k++) {
                    int a = sum[j - 1] ^ sum[i - 1];
                    int b = sum[k] ^ sum[j - 1];
                    if (a == b) ans++;
                }
            }
        }
        return ans;
    }
}
```
* 时间复杂度:$O(n^3)$
* 空间复杂度:$O(n)$

---

### 前缀异或 & 哈希表

我们重新审视一下这道题。

题目其实是要我们 **取得连续的一段区间 $[i, k]$,并在这一段中找到分割点 $j$,使得区间内分割点左边的异或结果为 $a$,分割点右边的异或结果为 $b$。并最终让 $a$ 和 $b$ 相等。**

由 $a$ 与 $b$ 相等,我们可以推导出 $a ⊕ b = 0$,再结合 $a$ 和 $b$ 的由来,**可以推导出 $[i, k]$ 连续一段的异或结果为  $0$**。

再结合我们预处理的「前缀异或」数组,可得:

$$Xor(i, k) = sum[k] ⊕ sum[i - 1] = 0$$

根据公式和「相同数值异或结果为 $0$」特性,我们可以知道 $sum[k]$ 和 $sum[i - 1]$ 数值相等,因此我们可以使用「哈希表」记录每个出现过的异或结果对应的下标集合,从而实现在确定 $k$ 的情况下,通过 $O(1)$ 的复杂度找到所有符合条件的 $i$。

需要注意的是,因为我们「前缀异或」数组的下标是从 $1$ 开始,所以我们需要先往「哈希表」存入一个哨兵 $0$ 作为边界,当然这一步不需要特殊操作,只需要让 $k$ 从 $0$ 开始执行循环即可(利用「前缀异或」数组中下标 $0$ 的值本身为 $0$)。

代码:
```Java
class Solution {
    public int countTriplets(int[] arr) {
        int n = arr.length;
        // 预处理前缀异或数组
        int[] sum = new int[n + 1];
        for (int i = 1; i <= n; i++) sum[i] = sum[i - 1] ^ arr[i - 1];
        int ans = 0;
        // 记录出现过的异或结果,存储格式:{ 异或结果 : [下标1, 下标2 ...] }
        Map<Integer, List<Integer>> map = new HashMap<>();
        for (int k = 0; k <= n; k++) {
            List<Integer> list = map.getOrDefault(sum[k], new ArrayList<>());
            for (int idx : list) {
                int i = idx + 1;
                ans += k - i;
            }
            list.add(k);
            map.put(sum[k], list);
        }
        return ans;
    }
}
```
```Java
class Solution {
    public int countTriplets(int[] arr) {
        int n = arr.length;
        // 事实上,甚至可以不预处理「前缀异或数组」,使用一个变量 xor 边遍历边计算即可
        int xor = 0, ans = 0;
        Map<Integer, List<Integer>> map = new HashMap<>();
        for (int k = 0; k <= n; k++) {
            if (k >= 1) xor ^= arr[k - 1];
            List<Integer> list = map.getOrDefault(xor, new ArrayList<>());
            for (int idx : list) {
                int i = idx + 1;
                ans += k - i;
            }
            list.add(k);
            map.put(xor, list);
        }
        return ans;
    }
}
```
* 时间复杂度:$O(n^2)$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


Question:
给你一个字符串 `s` ,字符串的「能量」定义为:只包含一种字符的最长非空子字符串的长度。

请你返回字符串的能量。

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

输出:2

解释:子字符串 "ee" 长度为 2 ,只包含字符 'e' 。
```
示例 2:
```
输入:s = "abbcccddddeeeeedcba"

输出:5

解释:子字符串 "eeeee" 长度为 5 ,只包含字符 'e' 。
```
示例 3:
```
输入:s = "triplepillooooow"

输出:5
```
示例 4:
```
输入:s = "hooraaaaaaaaaaay"

输出:11
```
示例 5:
```
输入:s = "tourist"

输出:1
```

提示:
* $1 <= s.length <= 500$
* $s$ 只包含小写英文字母。

Solution:
### 双指针

根据题意,使用「双指针」进行扫描计数即可。

代码:
```Java
class Solution {
    public int maxPower(String s) {
        int n = s.length(), ans = 1;
        for (int i = 0; i < n; ) {
            int j = i;
            while (j < n && s.charAt(j) == s.charAt(i)) j++;
            ans = Math.max(ans, j - i);
            i = j;
        }
        return ans;
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


Question:
给你一个整数 `n` ,请你返回所有 $0$ 到 $1$ 之间(不包括 $0$ 和 $1$)满足分母小于等于  `n` 的 最简 分数 。分数可以以 **任意** 顺序返回。

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

输出:["1/2"]

解释:"1/2" 是唯一一个分母小于等于 2 的最简分数。
```
示例 2:
```
输入:n = 3

输出:["1/2","1/3","2/3"]
```
示例 3:
```
输入:n = 4

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

解释:"2/4" 不是最简分数,因为它可以化简为 "1/2" 。
```
示例 4:
```
输入:n = 1

输出:[]
```

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

Solution:
### 数论

数据范围为 $100$ 且数值大小在 $(0, 1)$ 之间,因此枚举「分子 + 分母」的 $O(n^2)$ 做法是可接受的。

于是问题转化为:**如何快速判断两个数组成的分数是否为最简(即判断两个数的最大公约数是否为 $1$)。**

快速求得 $a$ 和 $b$ 的最大公约数的主要方式有两种 :「更相减损法」和「欧几里得算法」,其中「欧几里得算法」的递归实现最为好写,复杂度为 $O(\log{(a + b)})$,在绝大多数的情况下适用,只有在需要实现高精度时,才会考虑使用「更相减损法」。

而 stein 算法则是没有必要掌握的。

代码:
```Java
class Solution {
    int gcd(int a, int b) { // 欧几里得算法
        return b == 0 ? a : gcd(b, a % b);
    }
    public List<String> simplifiedFractions(int n) {
        List<String> ans = new ArrayList<>();
        for (int i = 1; i < n; i++) {
            for (int j = i + 1; j <= n; j++) {
                if (gcd(i, j) == 1) ans.add(i + "/" + j);
            }
        }
        return ans;
    }
}
```
-
```Java
class Solution {
    int gcd(int a, int b) { // 更相减损法
        while (true) {
            if (a > b) a -= b;
            else if (a < b) b -= a;
            else return a;
        }
    }
    public List<String> simplifiedFractions(int n) {
        List<String> ans = new ArrayList<>();
        for (int i = 1; i < n; i++) {
            for (int j = i + 1; j <= n; j++) {
                if (gcd(i, j) == 1) ans.add(i + "/" + j);
            }
        }
        return ans;
    }
}
```
-
```Java
class Solution {
    int gcd(int a, int b) { // stein
        if (a == 0 || b == 0) return Math.max(a, b);
        if (a % 2 == 0 && b % 2 == 0) return 2 * gcd(a >> 1, b >> 1);
        else if (a % 2 == 0) return gcd(a >> 1, b);
        else if (b % 2 == 0) return gcd(a, b >> 1);
        else return gcd(Math.abs(a - b), Math.min(a, b));
    }
    public List<String> simplifiedFractions(int n) {
        List<String> ans = new ArrayList<>();
        for (int i = 1; i < n; i++) {
            for (int j = i + 1; j <= n; j++) {
                if (gcd(i, j) == 1) ans.add(i + "/" + j);
            }
        }
        return ans;
    }
}
```
* 时间复杂度:枚举分子分母的复杂度为 $O(n^2)$;判断两数是否能凑成最简分数复杂度为 $O(\log{n})$。整体复杂度为 $O(n^2 * \log{n})$
* 空间复杂度:忽略递归带来的额外空间开销,复杂度为 $O(1)$

### 最后

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

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

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

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


LeetCode/1441-1450/1449. 数位成本和为目标值的最大数字（困难）.md

1449. 数位成本和为目标值的最大数字

Question:
给你一个整数数组 cost 和一个整数 target 。请你返回满足如下规则可以得到的 最大 整数:

给当前结果添加一个数位(i + 1)的成本为 cost[i] (cost 数组下标从 0 开始)。
总成本必须恰好等于 target 。
添加的数位中没有数字 0 。
由于答案可能会很大,请你以字符串形式返回。

如果按照上述要求无法得到任何整数,请你返回 "0" 。

示例 1:
```
输入:cost = [4,3,2,5,6,7,2,5,5], target = 9
输出:"7772"
解释:添加数位 '7' 的成本为 2 ,添加数位 '2' 的成本为 3 。所以 "7772" 的代价为 2*3+ 3*1 = 9 。 "977" 也是满足要求的数字,但 "7772" 是较大的数字。
 数字     成本
  1  ->   4
  2  ->   3
  3  ->   2
  4  ->   5
  5  ->   6
  6  ->   7
  7  ->   2
  8  ->   5
  9  ->   5
```
示例 2:
```
输入:cost = [7,6,5,5,5,6,8,7,8], target = 12
输出:"85"
解释:添加数位 '8' 的成本是 7 ,添加数位 '5' 的成本是 5 。"85" 的成本为 7 + 5 = 12 。
```
示例 3:
```
输入:cost = [2,4,6,2,4,6,4,4,4], target = 5
输出:"0"
解释:总成本是 target 的条件下,无法生成任何整数。
```
示例 4:
```
输入:cost = [6,10,15,40,40,40,40,40,40], target = 47
输出:"32211"
```

提示:
* cost.length == 9
* 1 <= cost[i] <= 5000
* 1 <= target <= 5000

Solution:
### 基本分析

根据题意:给定 $1$~$9$ 几个数字,每个数字都有选择成本,求给定费用情况下,凑成的最大数字是多少。

通常我们会如何比较两数大小关系?

首先我们 **根据长度进行比较,长度较长数字较大**;再者,对于长度相等的数值,**从高度往低位进行比较,找到第一位不同,不同位值大的数值较大。**

其中规则一的比较优先级要高于规则二。

基于此,我们可以将构造分两步进行。

---

### 动态规划 + 贪心

具体的,先考虑「数值长度」问题,每个数字有相应选择成本,所能提供的长度均为 $1$。

问题转换为:**有若干物品,求给定费用的前提下,花光所有费用所能选择的最大价值(物品个数)为多少。**

每个数字可以被选择多次,属于完全背包模型。

当求得最大「数值长度」后,考虑如何构造答案。

根据规则二,**应该尽可能让高位的数值越大越好**,因此我们可以从数值 $9$ 开始往数值 $1$ 遍历,如果状态能够由该数值转移而来,则选择该数值。

*PS. 写了几天两维版本了,大家应该都掌握了叭,今天赶着出门,直接写一维。*

代码:
```Java
class Solution {
    public String largestNumber(int[] cost, int t) {
        int[] f = new int[t + 1];
        Arrays.fill(f, Integer.MIN_VALUE);
        f[0] = 0;
        for (int i = 1; i <= 9; i++) {
            int u = cost[i - 1];
            for (int j = u; j <= t; j++) {
                f[j] = Math.max(f[j], f[j - u] + 1);
            }
        }
        if (f[t] < 0) return "0";
        String ans = "";
        for (int i = 9, j = t; i >= 1; i--) {
            int u = cost[i - 1];
            while (j >= u && f[j] == f[j - u] + 1) {
                ans += String.valueOf(i);
                j -= u;
            }
        }
        return ans;
    }
}
```
* 时间复杂度:$O(n * t)$
* 空间复杂度:$O(t)$

---

### 思考 & 进阶

懂得分两步考虑的话,这道题还是挺简单。虽然是「DP」+「贪心」,但两部分都不难。

其实这道题改改条件/思路,也能衍生出几个版本:

0. **【思考】如何彻底转化为「01 背包」或者「多重背包」来处理?**

    完全背包经过一维优化后时间复杂度为 $O(N * C)$。是否可以在不超过此复杂度的前提下,通过预处理物品将问题转换为另外两种传统背包?

    * 对于「多重背包」答案是可以的。由于给定的最终费用 $t$,我们可以明确算出每个物品最多被选择的次数,可以在 $O(N)$ 的复杂度内预处理额外的 $s[]$ 数组。然后配合「单调队列优化」,做到 $O(N * C)$ 复杂度,整体复杂度不会因此变得更差。
        但转换增加了「预处理」的计算量。为了让转换变成“更有意义”,我们可以在「预处理」时顺便做一个小优化:**对于相同成本的数字,只保留数值大的数字**。不难证明,当成本相同时,选择更大的数字不会让结果变差。

    * 对于「01 背包」答案是不可以。原因与「多重背包」单纯转换为「01 背包」不会降低复杂度一致。因此本题转换成「01 背包」会使得 $N$ 发生非常数级别的增大。

1. **【进阶】不再是给定数值 $1$~$9$(取消 $cost$ 数组),转为给定 $nums$ 数组(代表所能选择的数字,不包含 $0$),和相应 $price$ 数组(长度与 $nums$ 一致,代表选择 $nums[i]$ 所消耗的成本为 $price[i]$)。现有做法是否会失效?**

    此时 $nums$ 中不再是只有长度为 $1$ 的数值了。但我们「判断数值大小」的两条规则不变。因此「第一步」不需要做出调整,但在进行「第二步」开始前,我们要先对物品进行「自定义规则」的排序,确保「贪心」构造答案过程是正确的。规则与证明都不难请自行思考。

2. **【进阶】在进阶 $1$ 的前提下,允许 $nums$ 出现 $0$,且确保答案有解(不会返回答案 $0$),该如何求解?**

    增加数值 $0$ 其实只会对最高位数字的决策产生影响。

    我们可以**通过预处理转换为「分组 & 树形」背包问题**:将 $nums$ 中的非 $0$ 作为一组「主件」(分组背包部分:必须选择一个主件),所有数值作为「附属件」(树形背包部分:能选择若干个,选择附属件必须同时选择主件)。

### 最后

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

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

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

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


LeetCode/1441-1450/1450. 在既定时间做作业的学生人数（简单）.md

1450. 在既定时间做作业的学生人数

Question:
给你两个整数数组 `startTime`(开始时间)和 `endTime`(结束时间),并指定一个整数 `queryTime` 作为查询时间。

已知,第 `i` 名学生在 `startTime[i]` 时开始写作业并于 `endTime[i]` 时完成作业。

请返回在查询时间 `queryTime` 时正在做作业的学生人数。形式上,返回能够使 `queryTime` 处于区间 `[startTime[i], endTime[i]]`(含)的学生人数。

示例 1:
```
输入:startTime = [1,2,3], endTime = [3,2,7], queryTime = 4

输出:1

解释:一共有 3 名学生。
第一名学生在时间 1 开始写作业,并于时间 3 完成作业,在时间 4 没有处于做作业的状态。
第二名学生在时间 2 开始写作业,并于时间 2 完成作业,在时间 4 没有处于做作业的状态。
第三名学生在时间 3 开始写作业,预计于时间 7 完成作业,这是是唯一一名在时间 4 时正在做作业的学生。
```
示例 2:
```
输入:startTime = [4], endTime = [4], queryTime = 4

输出:1

解释:在查询时间只有一名学生在做作业。
```
示例 3:
```
输入:startTime = [4], endTime = [4], queryTime = 5

输出:0
```
示例 4:
```
输入:startTime = [1,1,1,1], endTime = [1,3,2,4], queryTime = 7

输出:0
```
示例 5:
```
输入:startTime = [9,8,7,6,5,4,3,2,1], endTime = [10,10,10,10,10,10,10,10,10], queryTime = 5

输出:5
```

提示:
* $startTime.length == endTime.length$
* $1 <= startTime.length <= 100$
* $1 <= startTime[i] <= endTime[i] <= 1000$
* $1 <= queryTime <= 1000$

Solution:
### 差分

为了方便,我们令 `startTime` 为 `st`,令 `endTime` 为 `et`,令 `queryTime` 为 `t`。

问题涉及「区间修改」+「单点查询」,是一道关于「差分」的模板题。

> **对差分不熟悉的同学,可以查看前置  🧀 : [差分入门](https://mp.weixin.qq.com/s?__biz=MzU4NDE3MTEyMA==&mid=2247490329&idx=1&sn=6d448a53cd722bbd990fda82bd262857)**

利用时间点数据范围为 $1e3$,我们建立一个 $1e3$ 大小的差分数组 `c`,对于时间段 $[st[i], et[i]]$ 而言,我们只需在差分数组 $c[st[i]]$ 上进行自增,在 $c[et[i] + 1]$ 进行自减,即可实现 $O(1)$ 复杂度对范围 $[st[i], et[i]]$ 的标记。

当处理完后,只需要对 `c` 执行「前缀和」操作,即可查询每个时间点被标记的次数(由于我们只需要求时间点 `t` 的覆盖次数,因此我们只需求 $[0, t]$ 范围的前缀和即可)。

Java 代码:
```Java
class Solution {
    public int busyStudent(int[] st, int[] et, int t) {
        int[] c = new int[1010];
        for (int i = 0; i < st.length; i++) {
            c[st[i]]++; c[et[i] + 1]--;
        }
        for (int i = 1; i <= t; i++) c[i] += c[i - 1];
        return c[t];
    }
}
```
TypeScript 代码:
```TypeScript
function busyStudent(st: number[], et: number[], t: number): number {
    const c = new Array<number>(1010).fill(0)
    for (let i = 0; i < st.length; i++) {
        c[st[i]]++; c[et[i] + 1]--;
    }
    for (let i = 1; i <= t; i++) c[i] += c[i - 1];
    return c[t]
};
```
* 时间复杂度:创建差分数组复杂度为 $O(C)$,其中 $C = 1e3$ 为值域大小,随后使用差分数组进行区间标记,复杂度为 $O(n)$,构建前缀和数组用于查询复杂度为 $O(t)$,整体复杂度为 $O(C)$
* 空间复杂度:$O(C)$

### 最后

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

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

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

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


LeetCode/1451-1460/1455. 检查单词是否为句中其他单词的前缀（简单）.md

1455. 检查单词是否为句中其他单词的前缀

Question:
给你一个字符串 `sentence` 作为句子并指定检索词为 `searchWord`,其中句子由若干用 单个空格 分隔的单词组成。请你检查检索词 `searchWord` 是否为句子 `sentence` 中任意单词的前缀。

如果 `searchWord` 是某一个单词的前缀,则返回句子 `sentence` 中该单词所对应的下标(下标从 $1$ 开始)。如果 `searchWord` 是多个单词的前缀,则返回匹配的第一个单词的下标(最小下标)。如果 `searchWord` 不是任何单词的前缀,则返回 $-1$ 。

字符串 `s` 的前缀是 `s` 的任何前导连续子字符串。

示例 1:
```
输入:sentence = "i love eating burger", searchWord = "burg"

输出:4

解释:"burg" 是 "burger" 的前缀,而 "burger" 是句子中第 4 个单词。
```
示例 2:
```
输入:sentence = "this problem is an easy problem", searchWord = "pro"

输出:2

解释:"pro" 是 "problem" 的前缀,而 "problem" 是句子中第 2 个也是第 6 个单词,但是应该返回最小下标 2 。
```
示例 3:
```
输入:sentence = "i am tired", searchWord = "you"

输出:-1

解释:"you" 不是句子中任何单词的前缀。
```

提示:
* $1 <= sentence.length <= 100$
* $1 <= searchWord.length <= 10$
* `sentence` 由小写英文字母和空格组成。
* `searchWord` 由小写英文字母组成。

Solution:
### 模拟

根据题意进行模拟即可。

Java 代码:
```Java
class Solution {
    public int isPrefixOfWord(String s, String t) {
        String[] ss = s.split(" ");
        int n = ss.length, m = t.length();
        for (int i = 0; i < n; i++) {
            if (ss[i].length() < m) continue;
            boolean ok = true;
            for (int j = 0; j < m && ok; j++) {
                if (ss[i].charAt(j) != t.charAt(j)) ok = false;
            }
            if (ok) return i + 1;
        }
        return -1;
    }
}
```
Typescript 代码:
```Typescript
function isPrefixOfWord(s: string, t: string): number {
    const ss = s.split(" ")
    const n = ss.length, m = t.length
    for (let i = 0; i < n; i++) {
        if (ss[i].length < m) continue
        let ok = true
        for (let j = 0; j < m && ok; j++) {
            if (ss[i][j] != t[j]) ok = false
        }
        if (ok) return i + 1
    }
    return -1
};
```
* 时间复杂度:$O(n \times m)$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


LeetCode/1451-1460/1457. 二叉树中的伪回文路径（中等）.md

1457. 二叉树中的伪回文路径

Question:
给你一棵二叉树,每个节点的值为 `1` 到 `9` 。

我们称二叉树中的一条路径是 「伪回文」的,当它满足:路径经过的所有节点值的排列中,存在一个回文序列。

请你返回从根到叶子节点的所有路径中伪回文路径的数目。

示例 1:

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

输出:2

解释:上图为给定的二叉树。总共有 3 条从根到叶子的路径:红色路径 [2,3,3] ,绿色路径 [2,1,1] 和路径 [2,3,1] 。
     在这些路径中,只有红色和绿色的路径是伪回文路径,因为红色路径 [2,3,3] 存在回文排列 [3,2,3] ,绿色路径 [2,1,1] 存在回文排列 [1,2,1] 。
```
示例 2:

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

输出:1

解释:上图为给定二叉树。总共有 3 条从根到叶子的路径:绿色路径 [2,1,1] ,路径 [2,1,3,1] 和路径 [2,1] 。
     这些路径中只有绿色路径是伪回文路径,因为 [2,1,1] 存在回文排列 [1,2,1] 。
```
示例 3:
```
输入:root = [9]

输出:1
```

提示:
* 给定二叉树的节点数目在范围 $[1, 10^5]$ 内
* $1 <= Node.val <= 9$

Solution:
### DFS + 位运算

“伪回文”是指能够通过重新排列变成“真回文”,真正的回文串只有两种情况:

* 长度为偶数,即出现次数为奇数的字符个数为 $0$ 个
* 长度为奇数,即出现次数为奇数的字符个数为 $1$ 个(位于中间)

因此,**我们只关心路径中各个字符(数字 `0-9`)出现次数的奇偶性,若路径中所有字符出现次数均为偶数,或仅有一个字符出现次数为奇数,那么该路径满足要求**。

节点值范围为 $[1, 9]$,除了使用固定大小的数组进行词频统计以外,还可以使用一个 `int` 类型的变量 `cnt` 来统计各数值的出现次数奇偶性:若 $cnt$ 的第 $k$ 位为 $1$,说明数值 $k$ 的出现次数为奇数,否则说明数值 $k$ 出现次数为偶数或没出现过,两者是等价的。

例如 $cnt = (0001010)_2$ 代表数值 $1$ 和数值 $3$ 出现次数为奇数次,其余数值没出现过或出现次数为偶数次。

翻转一个二进制数字中的某一位可使用「异或」操作,具体操作位 `cnt ^= 1 << k`。

判断是否最多只有一个字符出现奇数次的操作,也就是判断一个二进制数字是为全为 $0$ 或仅有一位 $1$,可配合 `lowbit` 来做,若 `cnt` 与 `lowbit(cnt) = cnt & -cnt` 相等,说明满足要求。

考虑到对 `lowbit(x) = x & -x` 不熟悉的同学,这里再做简单介绍:*`lowbit(x)` 表示 `x` 的二进制表示中最低位的 $1$ 所在的位置对应的值*,即仅保留从最低位起的第一个 $1$,其余位均以 $0$ 填充:
* `x = 6`,其二进制表示为 $(110)_2$,那么 $lowbit(6) = (010)_2 = 2$
* `x = 12`,其二进制表示为 $(1100)_2$,那么 $lowbit(12) = (100)_2 = 4$

Java 代码:

```Java
class Solution {
    int ans = 0;
    public int pseudoPalindromicPaths (TreeNode root) {
        dfs(root, 0);
        return ans;
    }
    void dfs(TreeNode root, int cnt) {
        if (root.left == null && root.right == null) {
            cnt ^= 1 << root.val;
            if (cnt == (cnt & -cnt)) ans++;
            return ;
        }
        if (root.left != null) dfs(root.left, cnt ^ (1 << root.val));
        if (root.right != null) dfs(root.right, cnt ^ (1 << root.val));
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    int ans;
    int pseudoPalindromicPaths(TreeNode* root) {
        dfs(root, 0);
        return ans;
    }
    void dfs(TreeNode* root, int cnt) {
        if (!root->left && !root->right) {
            cnt ^= 1 << root->val;
            if (cnt == (cnt & -cnt)) ans++;
            return;
        }
        if (root->left) dfs(root->left, cnt ^ (1 << root->val));
        if (root->right) dfs(root->right, cnt ^ (1 << root->val));
    }
};
```
Python 代码:
```Python
class Solution:
    def pseudoPalindromicPaths (self, root: Optional[TreeNode]) -> int:
        ans = 0
        def dfs(root, cnt):
            nonlocal ans
            if not root.left and not root.right:
                cnt ^= 1 << root.val
                ans += 1 if cnt == (cnt & -cnt) else 0
                return
            if root.left:
                dfs(root.left, cnt ^ (1 << root.val))
            if root.right:
                dfs(root.right, cnt ^ (1 << root.val))
        dfs(root, 0)
        return ans
```
TypeScript 代码:
```TypeScript
function pseudoPalindromicPaths (root: TreeNode | null): number {
    let ans = 0;
    const dfs = function (root: TreeNode, cnt: number): void {
        if (root.left == null && root.right == null) {
            cnt ^= 1 << root.val;
            if (cnt == (cnt & -cnt)) ans++;
            return ;
        }
        if (root.left) dfs(root.left, cnt ^ (1 << root.val));
        if (root.right) dfs(root.right, cnt ^ (1 << root.val));
    }
    dfs(root, 0);
    return ans;
};
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(\log{n})$

### 最后

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

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

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

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


LeetCode/1451-1460/1460. 通过翻转子数组使两个数组相等（简单）.md

1460. 通过翻转子数组使两个数组相等

Question:
给你两个长度相同的整数数组 `target` 和 `arr` 。每一步中,你可以选择 `arr` 的任意 非空子数组 并将它翻转。你可以执行此过程任意次。

如果你能让 `arr` 变得与 `target` 相同,返回 `True`;否则,返回 `False` 。

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

输出:true

解释:你可以按照如下步骤使 arr 变成 target:
1- 翻转子数组 [2,4,1] ,arr 变成 [1,4,2,3]
2- 翻转子数组 [4,2] ,arr 变成 [1,2,4,3]
3- 翻转子数组 [4,3] ,arr 变成 [1,2,3,4]
上述方法并不是唯一的,还存在多种将 arr 变成 target 的方法。
```
示例 2:
```
输入:target = [7], arr = [7]

输出:true

解释:arr 不需要做任何翻转已经与 target 相等。
```
示例 3:
```
输入:target = [3,7,9], arr = [3,7,11]

输出:false

解释:arr 没有数字 9 ,所以无论如何也无法变成 target 。
```

提示:
* $target.length = arr.length$
* $1 <= target.length <= 1000$
* $1 <= target[i] <= 1000$
* $1 <= arr[i] <= 1000$

Solution:
### 模拟

当两数组词频相同,且翻转次数不受限制时,我们至少能通过「逐个调整」将一数组变为另一数组(以当前需要调整的位置作为待翻转子数组的左端点,目标数值所在位置作为右端点)。

Java 代码:
```Java
class Solution {
    public boolean canBeEqual(int[] target, int[] arr) {
        int n = arr.length, tot = 0;
        int[] cnt = new int[1010];
        for (int i = 0; i < n; i++) {
            if (++cnt[target[i]] == 1) tot++;
            if (--cnt[arr[i]] == 0) tot--;
        }
        return tot == 0;
    }
}
```
TypeScript 代码:
```TypeScript
function canBeEqual(target: number[], arr: number[]): boolean {
    let n = target.length, tot = 0
    const cnt = new Array<number>(1010).fill(0)
    for (let i = 0; i < n; i++) {
        if (++cnt[target[i]] == 1) tot++
        if (--cnt[arr[i]] == 0) tot--
    }
    return tot == 0
};
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(C)$,其中 $C = 1010$ 为值域大小

### 最后

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

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

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

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


LeetCode/1461-1470/1464. 数组中两元素的最大乘积（简单）.md

1464. 数组中两元素的最大乘积

Question:
给你一个整数数组 `nums`,请你选择数组的两个不同下标 `i` 和 `j`,使 `(nums[i]-1)*(nums[j]-1)` 取得最大值。

请你计算并返回该式的最大值。

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

输出:12

解释:如果选择下标 i=1 和 j=2(下标从 0 开始),则可以获得最大值,(nums[1]-1)*(nums[2]-1) = (4-1)*(5-1) = 3*4 = 12 。
```
示例 2:
```
输入:nums = [1,5,4,5]

输出:16

解释:选择下标 i=1 和 j=3(下标从 0 开始),则可以获得最大值 (5-1)*(5-1) = 16 。
```
示例 3:
```
输入:nums = [3,7]

输出:12
```

提示:
* $2 <= nums.length <= 500$
* $1 <= nums[i] <= 10^3$

Solution:
### 模拟

根据题意,使用两个变量 `a` 和 `b` 记录最大值和次大值。

Java 代码:
```Java
class Solution {
    public int maxProduct(int[] nums) {
        int a = -1, b = -1;
        for (int x : nums) {
            if (x > a) {
                b = a; a = x;
            } else if (x > b) {
                b = x;
            }
        }
        return (a - 1) * (b - 1);
    }
}
```
TypeScript 代码:
```TypeScript
function maxProduct(nums: number[]): number {
    let a = -1, b = -1
    for (const x of nums) {
        if (x > a) {
            b = a; a = x
        } else if (x > b) {
            b = x
        }
    }
    return (a - 1) * (b - 1)
};
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


LeetCode/1461-1470/1465. 切割后面积最大的蛋糕（中等）.md

1465. 切割后面积最大的蛋糕

Question:
矩形蛋糕的高度为 `h` 且宽度为 `w`,给你两个整数数组 `hs` 和 `vs`,其中:

* `hs[i]` 是从矩形蛋糕顶部到第  `i` 个水平切口的距离
* `vs[j]` 是从矩形蛋糕的左侧到第 `j` 个竖直切口的距离

请你按数组 `hs` 和 `vs` 中提供的水平和竖直位置切割后,请你找出**面积最大**的那份蛋糕,并返回其**面积**。

由于答案可能是一个很大的数字,因此需要将结果 对 $10^9 + 7$ 取余 后返回。

示例 1:

```
输入:h = 5, w = 4, hs = [1,2,4], vs = [1,3]

输出:4

解释:上图所示的矩阵蛋糕中,红色线表示水平和竖直方向上的切口。切割蛋糕后,绿色的那份蛋糕面积最大。
```
示例 2:

```
输入:h = 5, w = 4, hs = [3,1], vs = [1]

输出:6

解释:上图所示的矩阵蛋糕中,红色线表示水平和竖直方向上的切口。切割蛋糕后,绿色和黄色的两份蛋糕面积最大。
```
示例 3:
```
输入:h = 5, w = 4, hs = [3], vs = [3]

输出:9
```

提示:
* $2 <= h, w <= 10^9$
* $1 <= hs.length <= \min(h - 1, 10^5)$
* $1 <= vs.length <= \min(w - 1, 10^5)$
* $1 <= hs[i] < h$
* $1 <= vs[i] < w$
* 题目数据保证 `hs` 中的所有元素各不相同
* 题目数据保证 `vs` 中的所有元素各不相同

Solution:
### 贪心

由于求是“最大”蛋糕面积,**水平/垂直方向的连续段必然由「同方向且相邻」的切割位置(或是蛋糕边界)所构成**。

这点可通过反证法证明:如果最终蛋糕的(左右或上下)边缘不是由「相邻」的切割位置(或是蛋糕边界)决定,而是由跨越某些切割点的位置所决定的话,那么这个蛋糕必不是“完整”一块。

用题面的示例 $1$ 来举个 🌰,我们仅分析水平方向(`hs = [1, 2, 4]`):

最终蛋糕的在该方向的连续段大小,只能是 `[上边缘, 切割点 1]`、`[切割点 1, 切割点 2]`、`[切割点 2, 切割点 4]` 和 `[切割点 4, 下边缘]` 四个之一,而不可能由非相邻切割点所组成,例如 `[切割点 1, 切割点 4]`(会被 `切割点 2` 所一分为二,不再完整)。

因此,我们可先对 `hs` 和 `vs` 进行排序,从而确保我们在遍历 `hs` 和 `vs` 过程中,处理到的均为「同反向且相邻」的切割位置。

随后,利用俩方向相互独立,分别求出俩方向连续段的最大长度,两者乘积即是答案。

Java 代码:

```Java
class Solution {
    int MOD = (int)1e9+7;
    public int maxArea(int h, int w, int[] hs, int[] vs) {
        Arrays.sort(hs);
        Arrays.sort(vs);
        int n = hs.length, m = vs.length;
        int mh = Math.max(hs[0], h - hs[n - 1]), mv = Math.max(vs[0], w - vs[m - 1]);
        for (int i = 1; i < n; i++) mh = Math.max(mh, hs[i] - hs[i - 1]);
        for (int i = 1; i < m; i++) mv = Math.max(mv, vs[i] - vs[i - 1]);
        return (int)((mh * 1L * mv) % MOD);
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    int maxArea(int h, int w, vector<int>& hs, vector<int>& vs) {
        int MOD = 1e9+7;
        sort(hs.begin(), hs.end());
        sort(vs.begin(), vs.end());
        int n = hs.size(), m = vs.size();
        int mh = max(hs[0], h - hs[n - 1]), mv = max(vs[0], w - vs[m - 1]);
        for (int i = 1; i < n; i++) mh = max(mh, hs[i] - hs[i - 1]);
        for (int i = 1; i < m; i++) mv = max(mv, vs[i] - vs[i - 1]);
        return (mh * 1L * mv) % MOD;
    }
};
```
Python 代码:
```Python
class Solution:
    def maxArea(self, h: int, w: int, hs: List[int], vs: List[int]) -> int:
        MOD = 10 ** 9 + 7
        hs.sort()
        vs.sort()
        n, m = len(hs), len(vs)
        mh, mv = max(hs[0], h - hs[n - 1]), max(vs[0], w - vs[m - 1])
        for i in range(1, n):
            mh = max(mh, hs[i] - hs[i - 1])
        for i in range(1, m):
            mv = max(mv, vs[i] - vs[i - 1])
        return (mh * mv) % MOD
```
TypeScript 代码:
```TypeScript
function maxArea(h: number, w: number, hs: number[], vs: number[]): number {
    const MOD = BigInt(1e9 + 7);
    hs.sort((a,b)=>a-b);
    vs.sort((a,b)=>a-b);
    const n = hs.length, m = vs.length;
    let mh = Math.max(hs[0], h - hs[n - 1]), mv = Math.max(vs[0], w - vs[m - 1]);
    for (let i = 1; i < n; i++) mh = Math.max(mh, hs[i] - hs[i - 1]);
    for (let i = 1; i < m; i++) mv = Math.max(mv, vs[i] - vs[i - 1]);
    return Number((BigInt(mh) * BigInt(mv)) % MOD);
};
```
* 时间复杂度:$O(m\log{m} + n\log{n})$
* 空间复杂度:$O(\max(\log{m}, \log{n})$

### 最后

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

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

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

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


1466. 重新规划路线

Question:
`n` 座城市,从 `0` 到 `n-1` 编号,其间共有 `n-1` 条路线。

因此,要想在两座不同城市之间旅行只有唯一一条路线可供选择(路线网形成一颗树)。

去年,交通运输部决定重新规划路线,以改变交通拥堵的状况。

路线用 `connections` 表示,其中 $connections[i] = [a, b]$ 表示从城市 `a` 到 `b` 的一条有向路线。

今年,城市 `0` 将会举办一场大型比赛,很多游客都想前往城市 `0` 。

请你帮助重新规划路线方向,使每个城市都可以访问城市 `0`。返回需要变更方向的最小路线数。

题目数据保证每个城市在重新规划路线方向后都能到达城市 `0` 。

示例 1:

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

输出:3

解释:更改以红色显示的路线的方向,使每个城市都可以到达城市 0 。
```
示例 2:

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

输出:2

解释:更改以红色显示的路线的方向,使每个城市都可以到达城市 0 。
```
示例 3:
```
输入:n = 3, connections = [[1,0],[2,0]]

输出:0
```

提示:
* $2 <= n <= 5 \times 10^4$
* $connections.length = n-1$
* $connections[i].length = 2$
* $0 <= connections[i][0], connections[i][1] <= n-1$
* $connections[i][0]$ `!=` $connections[i][1]$

Solution:
### DFS

核心等价:原图上从所有点能够访问到 `0`,等价于反图中从 `0` 出发能到任何点。

因此,可用 `connections` 创建双向图(原边权重为 $1$,反向边权重为 $0$),然后从 `0` 出发访问整张图,统计访问过程中的权重之和,即是原图中需要反向的边的数量。

Java 代码:
```Java
class Solution {
    int N = 50010, M = N * 2, idx = 0;
    int[] he = new int[N], e = new int[M], ne = new int[M], w = new int[M];
    void add(int a, int b, int c) {
        e[idx] = b;
        ne[idx] = he[a];
        w[idx] = c;
        he[a] = idx++;
    }
    public int minReorder(int n, int[][] connections) {
        Arrays.fill(he, -1);
        for (int[] info : connections) {
            int a = info[0], b = info[1];
            add(a, b, 1); add(b, a, 0);
        }
        return dfs(0, -1);
    }
    int dfs(int u, int fa) {
        int ans = 0;
        for (int i = he[u]; i != -1; i = ne[i]) {
            int j = e[i];
            if (j == fa) continue;
            ans += w[i] + dfs(j, u);
        }
        return ans;
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    int N = 50010, M = N * 2, idx = 0;
    int he[50010], e[100020], ne[100020], w[100020];
    void add(int a, int b, int c) {
        e[idx] = b;
        ne[idx] = he[a];
        w[idx] = c;
        he[a] = idx++;
    }
    int minReorder(int n, vector<vector<int>>& connections) {
        memset(he, -1, sizeof(he));
        for (auto& info : connections) {
            int a = info[0], b = info[1];
            add(a, b, 1); add(b, a, 0);
        }
        return dfs(0, -1);
    }
    int dfs(int u, int fa) {
        int ans = 0;
        for (int i = he[u]; i != -1; i = ne[i]) {
            int j = e[i];
            if (j == fa) continue;
            ans += w[i] + dfs(j, u);
        }
        return ans;
    }
};
```
Python 代码:
```Python
class Solution:
    def minReorder(self, n: int, connections: List[List[int]]) -> int:
        N, M, idx = n + 10, (n + 10) * 2, 0
        he, e, ne, w = [-1] * N, [0] * M, [0] * M, [0] * M

        def add(a, b, c):
            nonlocal idx
            e[idx], ne[idx], w[idx], he[a] = b, he[a], c, idx
            idx += 1

        def dfs(u, fa):
            ans = 0
            i = he[u]
            while i != -1:
                j, c = e[i], w[i]
                i = ne[i]
                if j == fa: continue
                ans += c + dfs(j, u)
            return ans

        for a, b in connections:
            add(a, b, 1)
            add(b, a, 0)
        return dfs(0, -1)
```
TypeScript 代码:
```TypeScript
function minReorder(n: number, connections: number[][]): number {
    let N = n + 10, M = N * 2, idx = 0;
    const he = Array(N).fill(-1), e = Array(M).fill(0), ne = Array(M).fill(0), w = Array(M).fill(0);
    const add = function(a: number, b: number, c: number): void {
        e[idx] = b;
        ne[idx] = he[a];
        w[idx] = c;
        he[a] = idx++;
    };
    const dfs = function(u: number, fa: number): number {
        let ans = 0;
        for (let i = he[u]; i != -1; i = ne[i]) {
            const j = e[i];
            if (j == fa) continue;
            ans += w[i] + dfs(j, u);
        }
        return ans;
    };
    for (let [a, b] of connections) {
        add(a, b, 1); add(b, a, 0);
    }
    return dfs(0, -1);
};
```
* 时间复杂度:建图复杂度为 $O(n)$;从 `0` 点开始遍历,访问所有点并统计权重之和的复杂度为 $O(n)$。整体复杂度为 $O(n)$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


1470. 重新排列数组

Question:
给你一个数组 `nums`,数组中有 `2n` 个元素,按 $[x_1,x_2,...,x_n,y_1,y_2,...,y_n]$ 的格式排列。

请你将数组按 $[x_1,y_1,x_2,y_2,...,x_n,y_n]$ 格式重新排列,返回重排后的数组。

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

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

解释:由于 x1=2, x2=5, x3=1, y1=3, y2=4, y3=7 ,所以答案为 [2,3,5,4,1,7]
```
示例 2:
```
输入:nums = [1,2,3,4,4,3,2,1], n = 4

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

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

提示:
* $1 <= n <= 500$
* $nums.length = 2 \times n$
* $1 <= nums[i] <= 10^3$

Solution:
### 模拟

根据题意进行模拟即可。

Java 代码:
```Java
class Solution {
    public int[] shuffle(int[] nums, int n) {
        int[] ans = new int[2 * n];
        for (int i = 0, j = n, k = 0; k < 2 * n; k++) {
            ans[k] = k % 2 == 0 ? nums[i++] : nums[j++];
        }
        return ans;
    }
}
```
Typescript 代码:
```Typescript
function shuffle(nums: number[], n: number): number[] {
    const ans = new Array<number>()
    for (let i = 0, j = n, k = 0; k < 2 * n; k++) {
        ans.push(k % 2 == 0 ? nums[i++] : nums[j++])
    }
    return ans
};
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n)$

---

### 加餐

**加餐一道其他题目 [关于 RMQ 的若干解法](https://mp.weixin.qq.com/s?__biz=MzU4NDE3MTEyMA==&mid=2247493262&idx=1&sn=2d8e192a5767b49b9a13a6192ab3b833) 🎉🎉**

### 最后

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

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

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

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


Question:
在一个小城市里,有 `m` 个房子排成一排,你需要给每个房子涂上 `n` 种颜色之一(颜色编号为 `1` 到 `n` )。

有的房子去年夏天已经涂过颜色了,所以这些房子不可以被重新涂色。

我们将连续相同颜色尽可能多的房子称为一个街区。

(比方说 `houses = [1,2,2,3,3,2,1,1]`,它包含 5 个街区  `[{1}, {2,2}, {3,3}, {2}, {1,1}]` 。)

给你一个数组 `houses`,一个 `m * n` 的矩阵 `cost` 和一个整数 `target`,其中:

* `houses[i]`:是第 `i` 个房子的颜色,`0` 表示这个房子还没有被涂色。
* `cost[i][j]`:是将第 `i` 个房子涂成颜色 `j + 1` 的花费。

请你返回房子涂色方案的最小总花费,使得每个房子都被涂色后,恰好组成 `target` 个街区。

如果没有可用的涂色方案,请返回 `-1` 。

示例 1:
```
输入:houses = [0,0,0,0,0], cost = [[1,10],[10,1],[10,1],[1,10],[5,1]], m = 5, n = 2, target = 3

输出:9

解释:房子涂色方案为 [1,2,2,1,1]
此方案包含 target = 3 个街区,分别是 [{1}, {2,2}, {1,1}]。
涂色的总花费为 (1 + 1 + 1 + 1 + 5) = 9。
```
示例 2:
```
输入:houses = [0,2,1,2,0], cost = [[1,10],[10,1],[10,1],[1,10],[5,1]], m = 5, n = 2, target = 3

输出:11

解释:有的房子已经被涂色了,在此基础上涂色方案为 [2,2,1,2,2]
此方案包含 target = 3 个街区,分别是 [{2,2}, {1}, {2,2}]。
给第一个和最后一个房子涂色的花费为 (10 + 1) = 11。
```
示例 3:
```
输入:houses = [0,0,0,0,0], cost = [[1,10],[10,1],[1,10],[10,1],[1,10]], m = 5, n = 2, target = 5

输出:5
```
示例 4:
```
输入:houses = [3,1,2,3], cost = [[1,1,1],[1,1,1],[1,1,1],[1,1,1]], m = 4, n = 3, target = 3

输出:-1

解释:房子已经被涂色并组成了 4 个街区,分别是 [{3},{1},{2},{3}] ,无法形成 target = 3 个街区。
```

提示:
* $m = houses.length = cost.length$
* $n = cost[i].length$
* $1 <= m <= 100$
* $1 <= n <= 20$
* $1 <= target <= m$
* $0 <= houses[i] <= n$
* $1 <= cost[i][j] <= 10^4$

Solution:
### 动态规划

**定义 $f[i][j][k]$ 为考虑前 $i$ 间房子,且第 $i$ 间房子的颜色编号为 $j$,前 $i$ 间房子形成的分区数量为 $k$ 的所有方案中的「最小上色成本」。**

我们不失一般性的考虑 $f[i][j][k]$ 该如何转移,由于某些房子本身就已经上色,上色的房子是不允许被粉刷的。

我们可以根据第 $i$ 间房子是否已经被上色,进行分情况讨论:

* 第 $i$ 间房子已经被上色,即 $houses[i] != 0$,此时只有满足 $j == houses[i]$ 的状态才是有意义的,其余状态均为 `INF`。
    同时根据「第 $i$ 间房子的颜色 $j$」与「第 $i - 1$ 间房子的颜色 $p$」是否相同,会决定第 $i$ 间房子是否形成一个新的分区。这同样需要进行分情况讨论。
    整理后的转移方程为:

    $$
    f[i][j][k]=\begin{cases}
    min(f[i - 1][j][k], f[i - 1][p][k - 1]) &j == houses[i], p != j\\
    INF & j != houses[i]
    \end{cases}
    $$

* 第 $i$ 间房子尚未被上色,即 $houses[i] == 0$,此时房子可以被粉刷成任意颜色。不会有无效状态的情况。
  同样,根据「第 $i$ 间房子的颜色 $j$」与「第 $i - 1$ 间房子的颜色 $p$」是否相同,会决定第 $i$ 间房子是否形成一个新的分区。
    转移方程为:

  $$
  f[i][j][k] = min(f[i - 1][j][k], f[i - 1][p][k - 1]) + cost[i][j - 1], p != j
  $$

一些编码细节:

* 下标转换:这是个人习惯,无论做什么题,我都喜欢将下标转换为从 $1$ 开始,目的是为了「节省负值下标的分情况讨论」、「将无效状态限制在 $0$ 下标内」或者「充当哨兵」等等。
* 将 `0x3f3f3f3f` 作为 `INF`:因为目标是求最小值,我们应当使用一个较大值充当正无穷,来关联无效状态。同时为了确保不会出现「在正无穷基础上累加导致丢失正无穷含义」的歧义情况,我们可以使用一个有「累加空间」的值作为「正无穷」(这个问题刚好最近在 [这里](https://mp.weixin.qq.com/s?__biz=MzU4NDE3MTEyMA==&mid=2247486435&idx=1&sn=2464b01e9c0fb8d81fcea6dd4ed0ec92&chksm=fd9ca0fccaeb29ea9947456290099143c5bc9bdea0ccfb2408f5fd63f5e0fc13fcd8060d4251&token=515378286&lang=zh_CN#rd) 专门讲过)。

Java 代码:
```Java
class Solution {
    int INF = 0x3f3f3f3f;
    public int minCost(int[] hs, int[][] cost, int m, int n, int t) {
        int[][][] f = new int[m + 1][n + 1][t + 1];

        // 不存在分区数量为 0 的状态
        for (int i = 0; i <= m; i++) {
            for (int j = 0; j <= n; j++) {
                f[i][j][0] = INF;
            }
        }

        for (int i = 1; i <= m; i++) {
            int color = hs[i - 1];
            for (int j = 1; j <= n; j++) {
                for (int k = 1; k <= t; k++) {
                    // 形成分区数量大于房子数量,状态无效
                    if (k > i) {
                        f[i][j][k] = INF;
                        continue;
                    }

                    // 第 i 间房间已经上色
                    if (color != 0) {
                        if (j == color) { // 只有与「本来的颜色」相同的状态才允许被转移
                            int tmp = INF;
                            // 先从所有「第 i 间房形成新分区」方案中选最优(即与上一房间颜色不同)
                            for (int p = 1; p <= n; p++) {
                                if (p != j) {
                                    tmp = Math.min(tmp, f[i - 1][p][k - 1]);
                                }
                            }
                            // 再结合「第 i 间房不形成新分区」方案中选最优(即与上一房间颜色相同)
                            f[i][j][k] = Math.min(f[i - 1][j][k], tmp);

                        } else { // 其余状态无效
                            f[i][j][k] = INF;
                        }

                    // 第 i 间房间尚未上色
                    } else {
                        int u = cost[i - 1][j - 1];
                        int tmp = INF;
                        // 先从所有「第 i 间房形成新分区」方案中选最优(即与上一房间颜色不同)
                        for (int p = 1; p <= n; p++) {
                            if (p != j) {
                                tmp = Math.min(tmp, f[i - 1][p][k - 1]);
                            }
                        }
                        // 再结合「第 i 间房不形成新分区」方案中选最优(即与上一房间颜色相同)
                        // 并将「上色成本」添加进去
                        f[i][j][k] = Math.min(tmp, f[i - 1][j][k]) + u;
                    }
                }
            }
        }

        // 从「考虑所有房间,并且形成分区数量为 t」的所有方案中找答案
        int ans = INF;
        for (int i = 1; i <= n; i++) {
            ans = Math.min(ans, f[m][i][t]);
        }
        return ans == INF ? -1 : ans;
    }
}
```
* 时间复杂度:共有 $m \times n \times t$ 个状态需要被转移,每次转移需要枚举「所依赖的状态」的颜色,复杂度为 $O(n)$。整体复杂度为 $O(m \times n^2 \times t)$
* 空间复杂度:$O(m \times n \times t)$

---

### 状态定义的由来

对于有一定 DP 刷题量的同学来说,上述的「状态定义」应该很好理解。

**根据经验,我们可以很容易确定「房间编号维度 $i$」和「分区数量维度 $k$」需要纳入考虑,同时为了在转移过程中,我们能够清楚知道从哪些状态转移过来需要增加「分区数量」,哪些状态转移过来不需要增加,因此需要多引入「最后一间房间颜色 $j$」维度。**

至于对 DP 不熟悉的同学,可以从写「爆搜」开始入手。

**这里的“写”不一定真的要动手去实现一个完整的「爆搜」方案,只需要合理设计出来 `DFS` 函数签名即可。**

但为了更好理解,我写了一个完整版的供你参考。

代码:
```Java
class Solution {
    int INF = 0x3f3f3f3f;
    int ans = INF;
    int[] hs;
    int[][] cost;
    int m, n, t;
    public int minCost(int[] _hs, int[][] _cost, int _m, int _n, int _t) {
        m = _m; n = _n; t = _t;
        hs = _hs;
        cost = _cost;
        dfs(0, -1, 0, 0);
        return ans == INF ? -1 : ans;
    }
    // u : 当前处理到的房间编号
    // last : 当前处理的房间颜色
    // cnt : 当前形成的分区数量
    // sum : 当前的上色成本
    void dfs(int u, int last, int cnt, int sum) {
        if (sum >= ans || cnt > t) return;
        if (u == m) {
            if (cnt == t) {
                ans = Math.min(ans, sum);
            }
            return;
        }
        int color = hs[u];
        if (color == 0) {
            for (int i = 1; i <= n; i++) {
                int nCnt = u - 1 < 0 ? 1 : last == i ? cnt : cnt + 1;
                dfs(u + 1, i, nCnt, sum + cost[u][i - 1]);
            }
        } else {
            int nCnt = u - 1 < 0 ? 1 : last == color ? cnt : cnt + 1;
            dfs(u + 1, color, nCnt, sum);
        }
    }
}
```
* 时间复杂度:`n` 为颜色数量,`m` 为房间数量。不考虑剪枝效果,每个房间都可以粉刷 `n` 种颜色,复杂度为指数级别的 $O(n^m)$
* 空间复杂度:忽略递归带来的额外空间开销。复杂度为 $O(1)$

可以发现,`DFS` 的可变参数有四个,其中 `sum` 是用于更新最终答案 `ans` 的,其应该作为动规值,其余三个参数,作为动规数组的三个维度。

至此,我们可以确定动态规划的「状态定义」,关于如何利用这种「技巧」来得到一个可靠的「状态定义」最早在 [这里](https://mp.weixin.qq.com/s?__biz=MzU4NDE3MTEyMA==&mid=2247485319&idx=1&sn=95a3dc9c97ca57185de792ca70924afe&chksm=fd9cac98caeb258ebea466f59378670a90af1cb3015ae70922e1d04ac711a5b8d8d853ac5e7d&token=515378286&lang=zh_CN#rd) 讲过。

---

### 记忆化搜索

看到评论区有同学贴了「记忆化搜索」的版本,那么就作为补充增加到题解吧 ~

注意记忆化容器应当与我们的「动规数组」结构保存一致。

代码:
```Java
class Solution {
    int INF = 0x3f3f3f3f;
    int m, n, t;
    int[] hs;
    int[][] cost;
    boolean[][][] vis;
    int[][][] cache;
    public int minCost(int[] _hs, int[][] _cost, int _m, int _n, int _t) {
        m = _m; n = _n; t = _t;
        hs = _hs;
        cost = _cost;
        vis = new boolean[m + 1][n + 1][t + 1];
        cache = new int[m + 1][n + 1][t + 1];
        int ans = dfs(0, 0, 0, 0);
        return ans == INF ? -1 : ans;
    }
    int dfs(int u, int last, int cnt, int sum){
        if(cnt > t) return INF;
        if(vis[u][last][cnt]) return cache[u][last][cnt];
        if (u == m) return cnt == t ? 0 : INF;
        int ans = INF;
        int color = hs[u];
        if(color == 0){
            for(int i = 1; i <= n; i++){
                int nCnt = u == 0 ? 1 : last == i ? cnt : cnt + 1;
                int cur = dfs(u + 1, i, nCnt, sum + cost[u][i - 1]);
                ans = Math.min(ans, cur + cost[u][i - 1]);
            }
        } else{
            int nCnt = u == 0 ? 1 : last == color ? cnt : cnt + 1;
            int cur = dfs(u + 1, color, nCnt, sum);
            ans = Math.min(ans, cur);
        }
        vis[u][last][cnt] = true;
        cache[u][last][cnt] = ans;
        return ans;
    }
}
```
* 时间复杂度:共有 $m \times n \times t$ 个状态需要被转移,每次转移需要枚举「所依赖的状态」的颜色,复杂度为 $O(n)$。整体复杂度为 $O(m \times n^2 \times t)$
* 空间复杂度:$O(m \times n \times t)$

### 最后

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

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

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

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


LeetCode/1471-1480/1475. 商品折扣后的最终价格（简单）.md

1475. 商品折扣后的最终价格

Question:
给你一个数组 `prices`,其中 `prices[i]` 是商店里第 `i` 件商品的价格。

商店里正在进行促销活动,如果你要买第 `i` 件商品,那么你可以得到与 `prices[j]` 相等的折扣,其中 `j` 是满足 `j > i` 且 `prices[j] <= prices[i]` 的 最小下标 ,如果没有满足条件的 `j` ,你将没有任何折扣。

请你返回一个数组,数组中第 `i` 个元素是折扣后你购买商品 `i` 最终需要支付的价格。

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

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

解释:
商品 0 的价格为 price[0]=8 ,你将得到 prices[1]=4 的折扣,所以最终价格为 8 - 4 = 4 。
商品 1 的价格为 price[1]=4 ,你将得到 prices[3]=2 的折扣,所以最终价格为 4 - 2 = 2 。
商品 2 的价格为 price[2]=6 ,你将得到 prices[3]=2 的折扣,所以最终价格为 6 - 2 = 4 。
商品 3 和 4 都没有折扣。
```
示例 2:
```
输入:prices = [1,2,3,4,5]

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

解释:在这个例子中,所有商品都没有折扣。
```
示例 3:
```
输入:prices = [10,1,1,6]

输出:[9,0,1,6]
```

提示:
* $1 <= prices.length <= 500$
* $1 <= prices[i] <= 10^3$

Solution:
### 模拟

数据范围为 $10^3$,可直接模拟。

Java 代码:
```Java
class Solution {
    public int[] finalPrices(int[] ps) {
        int n = ps.length;
        int[] ans = new int[n];
        for (int i = 0; i < n; i++) {
            int k = 0;
            for (int j = i + 1; j < n && k == 0; j++) {
                if (ps[j] <= ps[i]) k = ps[j];
            }
            ans[i] = ps[i] - k;
        }
        return ans;
    }
}
```
Typescript 代码:
```Typescript
function finalPrices(ps: number[]): number[] {
    const n = ps.length
    const ans = new Array<number>(n).fill(0)
    for (let i = 0; i < n; i++) {
        let k = 0
        for (let j = i + 1; j < n && k == 0; j++) {
            if (ps[j] <= ps[i]) k = ps[j]
        }
        ans[i] = ps[i] - k
    }
    return ans
};
```
* 时间复杂度:$O(n^2)$
* 空间复杂度:$O(n)$

---

### 单调栈

根据题意,我们知道若 $nums[i]$ 存在折扣,必然是被其右边最近一个满足「$nums[j] <= nums[i]$」的 $nums[j]$ 所更新。反过来说,给定一个 $nums[j]$,其所能更新的是其左边最近一个满足「$nums[i] >= nums[j]$」的 $nums[i]$。

因此我们可以使用「单调栈」来进行求解。

当然,我们也能不依靠经验,而从问题的本身出发,逐步分析出该做法。

具体的,我们可以从前往后处理所有的 $nums[i]$,使用某类容器装载我们所有的「待打折」的商品下标。假设当前处理到的是 $nums[i]$:
* 若其比容器内的任意商品价格要高,其必然不能更新任何一个待打折商品的价格,将其也加入容器尾部(此时我们发现,若有一个新的商品加入容器,其必然是当前所有待打折商品中的价格最高的,即容器内的商品价格单调递增);
* 若其价格不高于容器内的商品价格,其能够更新容器内待打折的商品价格,并且由于我们容器满足单调递增特性,我们必然能够从尾部开始取出待打折商品来进行更新,直到处理完成或遇到第一个无法更新价格的商品。

由于我们需要往尾部添加和取出元素,因此容器可使用「栈」。

**至此,我们分析了单调栈做法的两大重点:为何单调 & 为何用栈。若对「单调栈」仍不了解的同学,可以看前置 🧀 : [关于 RMQ 的若干解法](https://mp.weixin.qq.com/s?__biz=MzU4NDE3MTEyMA==&mid=2247493262&idx=1&sn=2d8e192a5767b49b9a13a6192ab3b833) 中的第四种解法 🎉🎉🎉。**

Java 代码:
```Java
class Solution {
    public int[] finalPrices(int[] ps) {
        int n = ps.length;
        int[] ans = new int[n];
        Deque<Integer> d = new ArrayDeque<>();
        for (int i = 0; i < n; i++) {
            while (!d.isEmpty() && ps[d.peekLast()] >= ps[i]) {
                int idx = d.pollLast();
                ans[idx] = ps[idx] - ps[i];
            }
            d.addLast(i); ans[i] = ps[i];
        }
        return ans;
    }
}
```
TypeScript 代码:
```TypeScript
function finalPrices(ps: number[]): number[] {
    let n = ps.length, he = 0, ta = 0
    const ans = new Array<number>(n).fill(0)
    const stk = new Array<number>(n).fill(0)
    for (let i = 0; i < n; i++) {
        while (he < ta && ps[stk[ta - 1]] >= ps[i]) {
            const idx = stk[--ta]
            ans[idx] = ps[idx] - ps[i]
        }
        stk[ta++] = i; ans[i] = ps[i]
    }
    return ans
};
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


LeetCode/1471-1480/1480. 一维数组的动态和（简单）.md

1480. 一维数组的动态和

Question:
给你一个数组 nums 。数组「动态和」的计算公式为:runningSum[i] = sum(nums[0]...nums[i]) 。

请返回 nums 的动态和。

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

输出:[1,3,6,10]

解释:动态和计算过程为 [1, 1+2, 1+2+3, 1+2+3+4] 。
```
示例 2:
```
输入:nums = [1,1,1,1,1]

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

解释:动态和计算过程为 [1, 1+1, 1+1+1, 1+1+1+1, 1+1+1+1+1] 。
```
示例 3:
```
输入:nums = [3,1,2,10,1]

输出:[3,4,6,16,17]
```

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

Solution:
### 一维前缀和

根据题意,创建答案数组,从头到尾模拟一遍,每次都将当前的前缀之和赋值给 $ans[i]$ 即可。

代码:
```Java
class Solution {
    public int[] runningSum(int[] nums) {
        int n = nums.length;
        int[] ans = new int[n];
        for (int i = 0, s = 0; i < n; i++) {
            ans[i] = s = s + nums[i];
        }
        return ans;
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


LeetCode/1481-1490/1482. 制作 m 束花所需的最少天数（中等）.md

1482. 制作 m 束花所需的最少天数

Question:
给你一个整数数组 bloomDay,以及两个整数 m 和 k 。

现需要制作 m 束花。制作花束时,需要使用花园中 相邻的 k 朵花 。

花园中有 n 朵花,第 i 朵花会在 bloomDay[i] 时盛开,恰好 可以用于 一束 花中。

请你返回从花园中摘 m 束花需要等待的最少的天数。如果不能摘到 m 束花则返回 -1 。

示例 1:
```
输入:bloomDay = [1,10,3,10,2], m = 3, k = 1

输出:3

解释:让我们一起观察这三天的花开过程,x 表示花开,而 _ 表示花还未开。
现在需要制作 3 束花,每束只需要 1 朵。
1 天后:[x, _, _, _, _]   // 只能制作 1 束花
2 天后:[x, _, _, _, x]   // 只能制作 2 束花
3 天后:[x, _, x, _, x]   // 可以制作 3 束花,答案为 3
```
示例 2:
```
输入:bloomDay = [1,10,3,10,2], m = 3, k = 2

输出:-1

解释:要制作 3 束花,每束需要 2 朵花,也就是一共需要 6 朵花。而花园中只有 5 朵花,无法满足制作要求,返回 -1 。
```
示例 3:
```
输入:bloomDay = [7,7,7,7,12,7,7], m = 2, k = 3

输出:12

解释:要制作 2 束花,每束需要 3 朵。
花园在 7 天后和 12 天后的情况如下:
7 天后:[x, x, x, x, _, x, x]
可以用前 3 朵盛开的花制作第一束花。但不能使用后 3 朵盛开的花,因为它们不相邻。
12 天后:[x, x, x, x, x, x, x]
显然,我们可以用不同的方式制作两束花。
```
示例 4:
```
输入:bloomDay = [1000000000,1000000000], m = 1, k = 1

输出:1000000000

解释:需要等 1000000000 天才能采到花来制作花束
```
示例 5:
```
输入:bloomDay = [1,10,2,9,3,8,4,7,5,6], m = 4, k = 2

输出:9
```

提示:
* bloomDay.length == n
* 1 <= n <= $10^5$
* 1 <= bloomDay[i] <= $10^9$
* 1 <= m <= $10^6$
* 1 <= k <= n

Solution:
### 二分查找

题目需要求得「所需的最少天数」。

假设「所需的最少天数」为 `ans` ,那么以 `ans` 为分割点的数轴具有「二段性」:

* 天数范围落在 $[0, ans)$ 无法制作完成
* 天数范围在 $[ans, +∞)$ 可以制作完成

因此可以通过「二分」来找到分割点 `ans`。

接下来我们需要确定「二分范围」,一个及格的「二分范围」只需要确保答案落在范围即可。

显然范围的左边界为 $0$(代表尚未有花绽放),范围的右边界为 $max(bloomDay[i])$(最后一朵花的开放时间,代表所有花都开完)。

我们既可以通过遍历 $bloomDay[]$ 数组来取得「精确右边界」,也可以直接根据数据范围 `1 <= bloomDay[i] <= 10^9` 来确定「粗略右边界」。

由于二分查找本身具有“折半”效率,因此两者不会有太大效率差距,我这里采用「粗略右边界」的方式。

代码:
```Java
class Solution {
    int n, m, k;
    boolean[] fl;
    public int minDays(int[] nums, int _m, int _k) {
        n = nums.length;
        m = _m; k = _k;

        if (n < m * k) return -1;

        fl = new boolean[n];

        int l = 0, r = (int)1e9;
        while (l < r) {
            int mid = l + r >> 1;
            if (check(nums, mid)) {
                r = mid;
            } else {
                l = mid + 1;
            }
        }
        return check(nums, r) ? r : -1;
    }
    boolean check(int[] nums, int mid) {
        for (int i = 0; i < n; i++) {
            fl[i] = nums[i] <= mid;
        }
        int cnt = 0;
        for (int i = 0; i < n && cnt < m; ) {
            if (fl[i]) {
                int cur = 1, j = i;
                while (cur < k && j + 1 < n && fl[j + 1]) {
                    j++;
                    cur++;
                }
                if (cur == k) cnt++;
                i = j + 1;
            } else {
                i++;
            }
        }
        return cnt >= m;
    }
}
```
* 时间复杂度:`check` 函数的复杂度为 $O(n)$。整体复杂度为 $O(n\log{1e9})$
* 空间复杂度:$O(n)$

---

### 优化 check 函数

不难发现,上述 `check` 函数每次都先将所有已开的花预处理出来。复杂度是严格 $O(n)$。

其实这个过程也能下放到统计逻辑去做,这样能够让 `check` 函数的复杂度从严格 $O(n)$ 变为最坏情况 $O(n)$,同时省去 $fl[]$ 数组,将空间优化至 $O(1)$。

代码:
```Java
class Solution {
    int n, m, k;
    public int minDays(int[] nums, int _m, int _k) {
        n = nums.length;
        m = _m; k = _k;

        if (n < m * k) return -1;

        int l = 0, r = (int)1e9;
        while (l < r) {
            int mid = l + r >> 1;
            if (check(nums, mid)) {
                r = mid;
            } else {
                l = mid + 1;
            }
        }
        return check(nums, r) ? r : -1;
    }
    boolean check(int[] nums, int mid) {
        int cnt = 0;
        for (int i = 0; i < n && cnt < m; ) {
            int cur = nums[i] <= mid ? 1 : 0, j = i;
            if (cur > 0) {
                while (cur < k && j + 1 < n && nums[j + 1] <= mid) {
                    j++;
                    cur++;
                }
                if (cur == k) cnt++;
                i = j + 1;
            } else {
                i++;
            }
        }
        return cnt >= m;
    }
}
```
* 时间复杂度:`check` 函数的复杂度为 $O(n)$。整体复杂度为 $O(n\log{1e9})$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


1486. 数组异或操作

Question:
给你两个整数,n 和 start 。

数组 nums 定义为:nums[i] = start + 2*i(下标从 0 开始)且 n == nums.length 。

请返回 nums 中所有元素按位异或(XOR)后得到的结果。

示例 1:
```
输入:n = 5, start = 0

输出:8

解释:数组 nums 为 [0, 2, 4, 6, 8],其中 (0 ^ 2 ^ 4 ^ 6 ^ 8) = 8 。
     "^" 为按位异或 XOR 运算符。
```
示例 2:
```
输入:n = 4, start = 3

输出:8

解释:数组 nums 为 [3, 5, 7, 9],其中 (3 ^ 5 ^ 7 ^ 9) = 8.
```
示例 3:
```
输入:n = 1, start = 7

输出:7
```
示例 4:
```
输入:n = 10, start = 5

输出:2
```

提示:
* 1 <= n <= 1000
* 0 <= start <= 1000
* n == nums.length

Solution:
### 模拟

数据范围只有 $10^3$,按照题目要求从头模拟一遍即可。

代码:
```Java
class Solution {
    public int xorOperation(int n, int start) {
        int ans = start;
        for (int i = 1; i < n; i++) {
            int x = start + 2 * i;
            ans ^= x;
        }
        return ans;
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(1)$

---

### 数学

上述解法数据范围出到 $10^8$ 大概率会发生 TLE。

如果数据范围出到 $10^8$ 的话,本题难度应该会归为「中等」或「困难」。

事实上,本题存在「数学规律」解法。

原式子为 $start ⊕ (start + 2) ⊕ (start + 4) ⊕ ... ⊕ (start + 2 * (n - 1))$ 。

我们发现原式子中只有数值 $2$ 是固定系数(由题目给定),考虑将其进行提出。

得到新式子 $s ⊕ (s + 1) ⊕ (s + 2) ⊕ ... ⊕ (s + (n - 1)) * 2$,其中 $s = start / 2$。

之所以进行这样的转换操作,是因为我们想要利用 $1 ⊕ 2 ⊕ 3 = 0$ 的异或性质。

**但是转换到了这一步,我们发现「新式子」与「原式子」其实并不相等。**

我们需要考虑两者之间的差值关系:

**不难发现,将「原式」转化成「新式」的集体除以 $2$ 的操作相当于将每个 $item$ 的进行「右移一位」,同时「异或运算」是每位独立计算的,因此「右移一位」不会影响移动部分的计算结果。**

**本质上,「原式」转化成「新式」是将最终答案 `ans` 进了「右移」一位的操作。因此如果要重新得到 `ans`,我们需要将其重新「左移」一位,将最后一位异或结果补回。**

即 `原式结果 = 新式结果 << 1 | e`,$e$ 为最后一位异或结果(只能是 $0$ 或者 $1$,其余高位为 $0$)。

我们重新观察「原式」发现式子中每个 $item$ 奇偶性相同,这意味着其二进制的最低位相同。

根据 `n` 和 `start` 的奇偶数搭配,不难得最后一位 `e = n & start & 1`。

剩下的问题在于如何在不遍历的情况下计算「新式」结果,前面说到转化的目的是为了利用 $1 ⊕ 2 ⊕ 3 = 0$ 异或特性。

事实上,这个式子存在一般性的推广结论:$4i ⊕ (4i + 1) ⊕ (4i + 2) ⊕ (4i + 3) = 0$。

因此只需要对最后一项进行 `%4` 讨论即可,这部分属于「结论」,详见代码的 `calc` 部分。

**总结一下,假设我们最终的答案为 `ans`。整个处理过程其实就是把原式中的每个 $item$ 右移一位(除以 $2$),计算 `ans` 中除了最低一位以外的结果;然后再将 `ans` 进行一位左移(重新乘以 $2$),将原本丢失的最后一位结果重新补上。补上则是利用了 `n` 和 `start` 的「奇偶性」的讨论。**

代码:
```Java
class Solution {
    int calc(int x) {
        if (x % 4 == 0) return x;
        else if (x % 4 == 1) return 1;
        else if (x % 4 == 2) return x + 1;
        else return 0;
    }
    public int xorOperation(int n, int start) {
        // 整体除以 2,利用 %4 结论计算 ans 中除「最低一位」的结果
        int s = start >> 1;
        int prefix = calc(s - 1) ^ calc(s + n - 1);
        // 利用「奇偶性」计算 ans 中的「最低一位」结果
        int last = n & start & 1;
        int ans = prefix << 1 | last;
        return ans;
    }
}
```
* 时间复杂度:$O(1)$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


LeetCode/1501-1510/1503. 所有蚂蚁掉下来前的最后一刻（中等）.md

1503. 所有蚂蚁掉下来前的最后一刻

Question:
有一块木板,长度为 `n` 个 单位 。一些蚂蚁在木板上移动,每只蚂蚁都以每秒一个单位的速度移动。其中,一部分蚂蚁向左移动,其他蚂蚁向右移动。

当两只向 不同 方向移动的蚂蚁在某个点相遇时,它们会同时改变移动方向并继续移动。假设更改方向不会花费任何额外时间。

而当蚂蚁在某一时刻 `t` 到达木板的一端时,它立即从木板上掉下来。

给你一个整数 `n` 和两个整数数组 `left` 以及 `right`。两个数组分别标识向左或者向右移动的蚂蚁在 `t = 0` 时的位置。请你返回最后一只蚂蚁从木板上掉下来的时刻。

示例 1:

```
输入:n = 4, left = [4,3], right = [0,1]

输出:4

解释:如上图所示:
-下标 0 处的蚂蚁命名为 A 并向右移动。
-下标 1 处的蚂蚁命名为 B 并向右移动。
-下标 3 处的蚂蚁命名为 C 并向左移动。
-下标 4 处的蚂蚁命名为 D 并向左移动。
请注意,蚂蚁在木板上的最后时刻是 t = 4 秒,之后蚂蚁立即从木板上掉下来。(也就是说在 t = 4.0000000001 时,木板上没有蚂蚁)。
```
示例 2:

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

输出:7

解释:所有蚂蚁都向右移动,下标为 0 的蚂蚁需要 7 秒才能从木板上掉落。
```
示例 3:

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

输出:7

解释:所有蚂蚁都向左移动,下标为 7 的蚂蚁需要 7 秒才能从木板上掉落。
```

提示:
* $1 <= n <= 10^4$
* $0 <= left.length <= n + 1$
* $0 <= left[i] <= n$
* $0 <= right.length <= n + 1$
* $0 <= right[i] <= n$
* $1 <= left.length + right.length <= n + 1$
* `left` 和 `right` 中的所有值都是唯一的,并且每个值 只能出现在二者之一 中。

Solution:
### 模拟

当一只蚂蚁起始位置为 `x`,方向往左,且往左移动过程不受其他影响时,其到达左边界所需时间为 `x`;若方向往右,且往右移动过程不受其他影响时,其到达右边界所需时间为 `n - x`。

蚂蚁之间的相互影响实际上并不会影响它们到达木板边缘的时间。

两只相遇的蚂蚁只是简单地交换了彼此的移动方向,并没有影响到它们的移动步数,即两只蚂蚁相遇可视为身份互换。

因此,我们只需要分别计算每只蚂蚁单独掉落到木板边缘所需的时间,然后取最大值即可得到所有蚂蚁掉落的最后时刻。

Java 代码:
```Java
class Solution {
    public int getLastMoment(int n, int[] left, int[] right) {
        int ans = 0;
        for (int x : left) ans = Math.max(ans, x);
        for (int x : right) ans = Math.max(ans, n - x);
        return ans;
    }
}
```
C++  代码:
```C++
class Solution {
public:
    int getLastMoment(int n, vector<int>& left, vector<int>& right) {
        int ans = 0;
        for (int x : left) ans = max(ans, x);
        for (int x : right) ans = max(ans, n - x);
        return ans;
    }
};
```
Python 代码:
```Python
class Solution:
    def getLastMoment(self, n: int, left: List[int], right: List[int]) -> int:
        return max(left + [n - x for x in right])
```
TypeScript 代码:
```TypeScript
function getLastMoment(n: number, left: number[], right: number[]): number {
    let ans = 0;
    for (const x of left) ans = Math.max(ans, x);
    for (const x of right) ans = Math.max(ans, n - x);
    return ans;
};
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


LeetCode/151-160/153. 寻找旋转排序数组中的最小值（中等）.md

153. 寻找旋转排序数组中的最小值

Question:
已知一个长度为 n 的数组,预先按照升序排列,经由 1 到 n 次 旋转 后,得到输入数组。

例如,原数组 nums = [0,1,2,4,5,6,7] 在变化后可能得到:

* 若旋转 4 次,则可以得到 [4,5,6,7,0,1,2]
* 若旋转 7 次,则可以得到 [0,1,2,4,5,6,7]

注意,数组 [a[0], a[1], a[2], ..., a[n-1]] 旋转一次 的结果为数组 [a[n-1], a[0], a[1], a[2], ..., a[n-2]] 。

给你一个元素值 互不相同 的数组 nums ,它原来是一个升序排列的数组,并按上述情形进行了多次旋转。请你找出并返回数组中的 最小元素 。

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

输出:1

解释:原数组为 [1,2,3,4,5] ,旋转 3 次得到输入数组。
```
示例 2:
```
输入:nums = [4,5,6,7,0,1,2]

输出:0

解释:原数组为 [0,1,2,4,5,6,7] ,旋转 4 次得到输入数组。
```
示例 3:
```
输入:nums = [11,13,15,17]

输出:11

解释:原数组为 [11,13,15,17] ,旋转 4 次得到输入数组。
```

提示:
* n == nums.length
* 1 <= n <= 5000
* -5000 <= nums[i] <= 5000
* nums 中的所有整数 互不相同
* nums 原来是一个升序排序的数组,并进行了 1 至 n 次旋转

Solution:
### 二分查找

今天这道题和昨天的 [81. 搜索旋转排序数组 II](https://leetcode-cn.com/problems/search-in-rotated-sorted-array-ii/) 相比,有了限制条件「所有整数互不相同」。

因此我们不需要进行「恢复二段性」的预处理,是可以做到严格 $O(log{n})$ 的复杂度。

我们仍然从「二分」的本质「二段性」进行出发分析:

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

代码:
```java []
class Solution {
    public int findMin(int[] nums) {
        int n = nums.length;
        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;
            }
        }
        return r + 1 < n ? nums[r + 1] : nums[0];
    }
}
```
* 时间复杂度:$O(\log{n})$
* 空间复杂度:$O(1)$

***

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

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

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

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

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

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

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

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

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

### 最后

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

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

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

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