Question:
这里有 $n$ 门不同的在线课程,按从 $1$ 到 $n$ 编号。给你一个数组 $courses$ ,其中 $courses[i] = [durationi, lastDayi]$ 表示第 $i$ 门课将会 持续 上 $duration_i$ 天课,并且必须在不晚于 $lastDay_i$ 的时候完成。

你的学期从第 $1$ 天开始。且不能同时修读两门及两门以上的课程。

返回你最多可以修读的课程数目。

示例 1:
```
输入:courses = [[100, 200], [200, 1300], [1000, 1250], [2000, 3200]]

输出:3

解释:
这里一共有 4 门课程,但是你最多可以修 3 门:
首先,修第 1 门课,耗费 100 天,在第 100 天完成,在第 101 天开始下门课。
第二,修第 3 门课,耗费 1000 天,在第 1100 天完成,在第 1101 天开始下门课程。
第三,修第 2 门课,耗时 200 天,在第 1300 天完成。
第 4 门课现在不能修,因为将会在第 3300 天完成它,这已经超出了关闭日期。
```
示例 2:
```
输入:courses = [[1,2]]

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

输出:0
```

提示:
* $1 <= courses.length <= 10^4$
* $1 <= durationi, lastDayi <= 10^4$

Solution:
### 贪心 + 优先队列(堆)

这是一道很好的题目。

题目是要我们构造出一种可行的排列,排列中每个课程的实际结束时间满足「最晚完成时间」要求,求可行排序的最大长度(每个课程对答案的贡献都是 $1$)。

这容易引导我们往「泛化背包」方面进行思考:简单来说,对于某个物品(课程)而言,在不同条件下成本不同,在时间轴 $[1, courses[i][1] - courses[i][0]]$ 上该物品可被选,成本为其持续时间,在比该范围大的数轴上无法被选,成本为正无穷。因此某一段特定的时间轴上,问题可抽象成有条件限制的组合优化问题。

**由于数据范围为 $10^4$,泛化背包做法需要记录的维度大于一维,不予考虑。**

再然后容易想到「二分」,显然在以最大选择数量 $ans$ 为分割点的数组上具有「二段性」:

* 使用数量小于等于 $ans$ 的课程能够构造出合法排序(考虑在最长合法序列上做减法即可);
* 使用数量大于 $ans$ 的课程无法构造出合法排列。

此时二分范围为 $[0, n]$,问题转化为:如何在 $O(n)$ 检查是否可构造出某个长度 $len$ 的合法排列(实现 `check` 方法)。

**常规的线性扫描做法无法确定是否存在某个长度的合法排列,因此二分不予考虑。**

我们需要运用「贪心」思维考虑可能的方案。

具体的,我们先根据「结束时间」对 $courses$ 排升序,从前往后考虑每个课程,处理过程中维护一个总时长 $sum$,对于某个课程 $courses[i]$ 而言,根据如果学习该课程,是否满足「最晚完成时间」要求进行分情况讨论:

* 学习该课程后,满足「最晚完成时间」要求,即 $sum + courses[i][0] <= courses[i][1]$,则进行学习;

* 学习该课程后,不满足「最晚完成时间」要求,此时从过往学习的课程中找出「持续时间」最长的课程进行「回退」操作(这个持续时长最长的课程有可能是当前课程)。

其中「记录当前已选课程」和「从过往学习的课程中找出持续时间最长的课程」操作可以使用优先队列(大根堆)实现。

可用「归纳法 + 反证法」证明该做法能够取到最优排列之一,定义最优排列为「总课程数最大,且总耗时最少」的合法排列。

1. 在课程数量相同的前提下,该做法得到的排列总耗时最少

    这点可通过「反证法」来证明:当不满足「最后完成时间」时,我们总是弹出「持续时间」最长的课程来进行回退,因此在所有课程对答案的贡献都是 $1$ 的前提下,该做法能够确保总耗时最少。即当堆中元素数量被调整为 $x$ 时,必然是由元素数量为 $x + 1$ 时,将持续时间最长的课程弹出所得来。

2. 该做法能够确保取得最大课程数量

    在得证第 $1$ 点后,可用「归纳法」进行证明第 $2$ 点:只考虑一个课程的情况下(假定 $courses[0][0] < courses[0][1]$),选该课程会比不选更好。
    将 $courses[0]$ 的最优解排列记为 $s[0]$,当确定了 $s[0]$ 后再考虑如何处理 $courses[1]$ 来得到 $s[1]$。
    首先可知 $s[1]$ 只有三种情况:
    * $s[1] = s[0]$:意味着 $courses[1]$ 不参与到最优解排列当中;
    * $s[1] \neq s[0]$,但两个最优解长度相同:意味着 $s[1]$ 是由 $courses[1]$ 替换了 $s[0]$ 中的某个课程而来(此时只有第 $0$ 个课程),且基于证明 $1$ 可以得证,被替换的课程持续时间比 $courses[1]$ 要长;
    * $s[1] \neq s[0]$,且 $s[1]$ 长度比 $s[0]$ 大 $1$:意味着 $s[1]$ 是由 $courses[1]$ 最接追加到 $s[0]$ 而来。

    综上,我们证明了,如果已知某个边界情况的最优解,那么由边界的最优解可推导出在此基础上多考虑一个课程时的最优解。即以上分析可以推广到任意的 $s[i - 1]$ 和 $s[i]$。

    **需要注意,在推广到任意的 $s[i - 1]$ 和 $s[i]$ 时,还需要证明在已知 $s[i - 1]$ 时,多考虑一个 $courses[i]$ 不会出现 $s[i]$ 的和 $s[i - 1]$ 的长度差超过 $1$ 的情况。**
    这个基于已得证的第 $1$ 点,再用反证法可证:如果在 $s[i - 1]$ 的基础上多考虑一个 $courses[i]$ 能够使得总长度增加超过 $1$,说明存在一个「之前没有被选的课程」+「课程 $courses[i]$」的持续时间比「被替换的课程」短。
    那么使用这个「之前没有被选的课程」直接替换「被替换的课程」可得到长度与 $s[i - 1]$ 相同,且总耗时更短的排列方案,这与 $s[i - 1]$ 本身是最优排列冲突。

代码:
```Java
class Solution {
    public int scheduleCourse(int[][] courses) {
        Arrays.sort(courses, (a,b)->a[1]-b[1]);
        PriorityQueue<Integer> q = new PriorityQueue<>((a,b)->b-a);
        int sum = 0;
        for (int[] c : courses) {
            int d = c[0], e = c[1];
            sum += d;
            q.add(d);
            if (sum > e) sum -= q.poll();
        }
        return q.size();
    }
}
```
* 时间复杂度:$O(n\log{n})$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


Question:
给定一个非负整数 c ,你要判断是否存在两个整数 a 和 b,使得 $a^2$ + $b^2$ = c 。

示例 1:
```
输入:c = 5

输出:true

解释:1 * 1 + 2 * 2 = 5
```
示例 2:
```
输入:c = 3

输出:false
```
示例 3:
```
输入:c = 4

输出:true
```
示例 4:
```
输入:c = 2

输出:true
```
示例 5:
```
输入:c = 1

输出:true
```

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

Solution:
### 基本分析

**根据等式 $a^2 + b^2 = c$,可得知 `a` 和 `b` 的范围均为 $[0,\sqrt{c}]$。**

基于此我们会有以下几种做法。

---

### 枚举

我们可以枚举 `a` ,边枚举边检查是否存在 `b` 使得等式成立。

这样做的复杂度为 $O(\sqrt{c})$。

代码:
```Java
class Solution {
    public boolean judgeSquareSum(int c) {
        int max = (int)Math.sqrt(c);
        for (int a = 0; a <= max; a++) {
            int b = (int)Math.sqrt(c - a * a);
            if (a * a + b * b == c) return true;
        }
        return false;
    }
}
```
* 时间复杂度:$O(\sqrt{c})$
* 空间复杂度:$O(1)$

---

### 双指针

由于 `a` 和 `b` 的范围均为 $[0,\sqrt{c}]$,因此我们可以使用「双指针」在 $[0,\sqrt{c}]$ 范围进行扫描:

* $a^2 + b^2 == c$ : 找到符合条件的 `a` 和 `b`,返回 $true$
* $a^2 + b^2 < c$ : 当前值比目标值要小,`a++`
* $a^2 + b^2 > c$ : 当前值比目标值要大,`b--`

代码:
```Java
class Solution {
    public boolean judgeSquareSum(int c) {
        int a = 0, b = (int)Math.sqrt(c);
        while (a <= b) {
            int cur = a * a + b * b;
            if (cur == c) {
                return true;
            } else if (cur > c) {
                b--;
            } else {
                a++;
            }
        }
        return false;
    }
}
```
* 时间复杂度:$O(\sqrt{c})$
* 空间复杂度:$O(1)$

---

### 费马平方和

**费马平方和 : 奇质数能表示为两个平方数之和的充分必要条件是该质数被 4 除余 1 。**

翻译过来就是:**当且仅当一个自然数的质因数分解中,满足 `4k+3` 形式的质数次方数均为偶数时,该自然数才能被表示为两个平方数之和。**

因此我们对 `c` 进行质因数分解,再判断满足 `4k+3` 形式的质因子的次方数是否均为偶数即可。

代码:
```Java
public class Solution {
    public boolean judgeSquareSum(int c) {
        for (int i = 2, cnt = 0; i * i <= c; i++, cnt = 0) {
            while (c % i == 0 && ++cnt > 0) c /= i;
            if (i % 4 == 3 && cnt % 2 != 0) return false;
        }
        return c % 4 != 3;
    }
}
```
* 时间复杂度:$O(\sqrt{c})$
* 空间复杂度:$O(1)$

---

### 我猜你问

* 三种解法复杂度都一样,哪个才是最优解呀?

前两套解法是需要「真正掌握」的,而「费马平方和」更多的是作为一种拓展。

你会发现从复杂度上来说,其实「费马平方和」并没有比前两种解法更好,但由于存在对 `c` 除质因数操作,导致「费马平方和」实际表现效果要优于同样复杂度的其他做法。但这仍然不成为我们必须掌握「费马平方和」的理由。

三者从复杂度上来说,都是 $O(\sqrt{c})$ 算法,不存在最不最优的问题。

* 是否有关于「费马平方和」的证明呢?

想要看 莱昂哈德·欧拉 对于「费马平方和」的证明在 [这里](https://zh.wikipedia.org/wiki/%E8%B4%B9%E9%A9%AC%E5%B9%B3%E6%96%B9%E5%92%8C%E5%AE%9A%E7%90%86),我这里直接引用 费马 本人的证明:

> 我确实发现了一个美妙的证明,但这里空白太小写不下。

* 我就是要学「费马平方和」,有没有可读性更高的代码?

有的,在这里。喜欢的话可以考虑背过:

```Java
public class Solution {
    public boolean judgeSquareSum(int c) {
        for (int i = 2; i * i <= c; i++) {
            int cnt = 0;
            while (c % i == 0) {
                cnt++;
                c /= i;
            }
            if (i % 4 == 3 && cnt % 2 != 0) return false;
        }
        return c % 4 != 3;
    }
}
```

### 最后

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

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

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

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


636. 函数的独占时间

Question:
有一个单线程 `CPU` 正在运行一个含有 `n` 道函数的程序。

每道函数都有一个位于  `0` 和 `n-1` 之间的唯一标识符。

函数调用存储在一个调用栈上 :当一个函数调用开始时,它的标识符将会推入栈中。而当一个函数调用结束时,它的标识符将会从栈中弹出。标识符位于栈顶的函数是当前正在执行的函数。每当一个函数开始或者结束时,将会记录一条日志,包括函数标识符、是开始还是结束、以及相应的时间戳。

给你一个由日志组成的列表 `logs` ,其中 `logs[i]` 表示第 `i` 条日志消息,该消息是一个按 `"{function_id}:{"start" | "end"}:{timestamp}"` 进行格式化的字符串。

例如,`"0:start:3"` 意味着标识符为 `0` 的函数调用在时间戳 `3` 的 起始开始执行 ;而 `"1:end:2"` 意味着标识符为 `1` 的函数调用在时间戳 `2` 的末尾结束执行。

注意,函数可以调用多次,可能存在递归调用 。

函数的「独占时间」定义是在这个函数在程序所有函数调用中执行时间的总和,调用其他函数花费的时间不算该函数的独占时间。

例如,如果一个函数被调用两次,一次调用执行 `2` 单位时间,另一次调用执行 `1` 单位时间,那么该函数的独占时间为 `2 + 1 = 3` 。

以数组形式返回每个函数的独占时间,其中第 `i` 个下标对应的值表示标识符 `i` 的函数的独占时间。

示例 1:

```
输入:n = 2, logs = ["0:start:0","1:start:2","1:end:5","0:end:6"]

输出:[3,4]

解释:
函数 0 在时间戳 0 的起始开始执行,执行 2 个单位时间,于时间戳 1 的末尾结束执行。
函数 1 在时间戳 2 的起始开始执行,执行 4 个单位时间,于时间戳 5 的末尾结束执行。
函数 0 在时间戳 6 的开始恢复执行,执行 1 个单位时间。
所以函数 0 总共执行 2 + 1 = 3 个单位时间,函数 1 总共执行 4 个单位时间。
```
示例 2:
```
输入:n = 1, logs = ["0:start:0","0:start:2","0:end:5","0:start:6","0:end:6","0:end:7"]

输出:[8]

解释:
函数 0 在时间戳 0 的起始开始执行,执行 2 个单位时间,并递归调用它自身。
函数 0(递归调用)在时间戳 2 的起始开始执行,执行 4 个单位时间。
函数 0(初始调用)恢复执行,并立刻再次调用它自身。
函数 0(第二次递归调用)在时间戳 6 的起始开始执行,执行 1 个单位时间。
函数 0(初始调用)在时间戳 7 的起始恢复执行,执行 1 个单位时间。
所以函数 0 总共执行 2 + 4 + 1 + 1 = 8 个单位时间。
```
示例 3:
```
输入:n = 2, logs = ["0:start:0","0:start:2","0:end:5","1:start:6","1:end:6","0:end:7"]

输出:[7,1]

解释:
函数 0 在时间戳 0 的起始开始执行,执行 2 个单位时间,并递归调用它自身。
函数 0(递归调用)在时间戳 2 的起始开始执行,执行 4 个单位时间。
函数 0(初始调用)恢复执行,并立刻调用函数 1 。
函数 1在时间戳 6 的起始开始执行,执行 1 个单位时间,于时间戳 6 的末尾结束执行。
函数 0(初始调用)在时间戳 7 的起始恢复执行,执行 1 个单位时间,于时间戳 7 的末尾结束执行。
所以函数 0 总共执行 2 + 4 + 1 = 7 个单位时间,函数 1 总共执行 1 个单位时间。
```
示例 4:
```
输入:n = 2, logs = ["0:start:0","0:start:2","0:end:5","1:start:7","1:end:7","0:end:8"]

输出:[8,1]
```
示例 5:
```
输入:n = 1, logs = ["0:start:0","0:end:0"]

输出:[1]
```

提示:
* $1 <= n <= 100$
* $1 <= logs.length <= 500$
* $0 <= function_id < n$
* $0 <= timestamp <= 10^9$
* 两个开始事件不会在同一时间戳发生
* 两个结束事件不会在同一时间戳发生
* 每道函数都有一个对应 `"start"` 日志的 `"end"` 日志

Solution:
### 模拟

我们使用「栈」来模拟执行过程:当一个函数被调用(`op = start`)时,压入栈内,当函数调用完成(`op = end`)时,从栈顶弹出(此时栈顶元素必然是该结束函数的入栈记录),使用变量 `cur` 记录当前时间。

从前往后处理所有的 $log[i]$,根据 $log[i]$ 是属于函数调用还是函数结束进行分情况讨论:

* 当 $log[i]$ 为函数调用:此时从该函数的调用发起时间 `ts` 到上一次记录的当前时间,都是前一函数的执行时间,因此可以将 `ts - cur` 累加到栈帧中的前一函数。即若栈不为空,则将该时间累加到栈顶对应的函数上,然后将 $log[i]$ 入栈,同时更新当前时间;
* 当 $log[i]$ 为函数结束:此时栈顶元素必然是该函数的调用记录,此时 $log[i]$ 的结束时间与上一次记录的当前时间的时长 `ts - cur + 1`,必然是该函数的执行时间,将其累加到当前函数中,并更新当前时间。

Java 代码:
```Java
class Solution {
    public int[] exclusiveTime(int n, List<String> logs) {
        int[] ans = new int[n];
        Deque<Integer> d = new ArrayDeque<>();
        int cur = -1;
        for (String log : logs) {
            String[] ss = log.split(":");
            int idx = Integer.parseInt(ss[0]), ts = Integer.parseInt(ss[2]);
            if (ss[1].equals("start")) {
                if (!d.isEmpty()) ans[d.peekLast()] += ts - cur;
                d.addLast(idx);
                cur = ts;
            } else {
                int func = d.pollLast();
                ans[func] += ts - cur + 1;
                cur = ts + 1;
            }
        }
        return ans;
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    vector<int> exclusiveTime(int n, vector<string>& logs) {
        vector<int> ans(n, 0);
        stack<int> s;
        int cur = -1;
        for (auto log : logs) {
            istringstream ss(log);
            string buf;
            vector<string> tokens;
            while (getline(ss, buf, ':')) {
                tokens.push_back(buf);
            }
            int idx = stoi(tokens[0]), ts = stoi(tokens[2]);
            if (tokens[1] == "start") {
                if (!s.empty()) ans[s.top()] += ts - cur;
                s.push(idx);
                cur = ts;
            } else {
                ans[s.top()] += ts - cur + 1;
                s.pop();
                cur = ts + 1;
            }
        }
        return ans;
    }
};
```
Python 代码:
```Python
class Solution:
    def exclusiveTime(self, n: int, logs: List[str]) -> List[int]:
        ans = [0] * n
        s = []
        cur = -1
        for log in logs:
            ss = log.split(':')
            idx, ts = int(ss[0]), int(ss[2])
            if ss[1] == "start":
                if s: ans[s[-1]] += ts - cur
                s.append(idx)
                cur = ts
            else:
                ans[s.pop()] += ts - cur + 1
                cur = ts + 1
        return ans
```
TypeScript 代码:
```TypeScript
function exclusiveTime(n: number, logs: string[]): number[] {
    const ans = new Array<number>(n).fill(0)
    const stk = new Array<number>()
    let he = 0, ta = 0, cur = -1
    for (let log of logs) {
        const ss = log.split(":")
        const idx = Number(ss[0]), ts = Number(ss[2])
        if (ss[1] == "start") {
            if (he < ta) ans[stk[ta - 1]] += ts - cur
            stk[ta++] = idx
            cur = ts
        } else {
            const func = stk[--ta]
            ans[func] += ts - cur + 1
            cur = ts + 1
        }
    }
    return ans
};
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


Question:
在 LeetCode 商店中, 有 `n` 件在售的物品。每件物品都有对应的价格。然而,也有一些大礼包,每个大礼包以优惠的价格捆绑销售一组物品。

给你一个整数数组 `price` 表示物品价格,其中 `price[i]` 是第 `i` 件物品的价格。另有一个整数数组 `needs` 表示购物清单,其中 `needs[i]` 是需要购买第 `i` 件物品的数量。

还有一个数组 special 表示大礼包,`special[i]` 的长度为 `n + 1` ,其中 `special[i][j]` 表示第 `i` 个大礼包中内含第 `j` 件物品的数量,且 `special[i][n]` (也就是数组中的最后一个整数)为第 `i` 个大礼包的价格。

返回 确切 满足购物清单所需花费的最低价格,你可以充分利用大礼包的优惠活动。你不能购买超出购物清单指定数量的物品,即使那样会降低整体价格。任意大礼包可无限次购买。

示例 1:
```
输入:price = [2,5], special = [[3,0,5],[1,2,10]], needs = [3,2]

输出:14

解释:有 A 和 B 两种物品,价格分别为 ¥2 和 ¥5 。
大礼包 1 ,你可以以 ¥5 的价格购买 3A 和 0B 。
大礼包 2 ,你可以以 ¥10 的价格购买 1A 和 2B 。
需要购买 3 个 A 和 2 个 B , 所以付 ¥10 购买 1A 和 2B(大礼包 2),以及 ¥4 购买 2A 。
```
示例 2:
```
输入:price = [2,3,4], special = [[1,1,0,4],[2,2,1,9]], needs = [1,2,1]

输出:11

解释:A ,B ,C 的价格分别为 ¥2 ,¥3 ,¥4 。
可以用 ¥4 购买 1A 和 1B ,也可以用 ¥9 购买 2A ,2B 和 1C 。
需要买 1A ,2B 和 1C ,所以付 ¥4 买 1A 和 1B(大礼包 1),以及 ¥3 购买 1B , ¥4 购买 1C 。
不可以购买超出待购清单的物品,尽管购买大礼包 2 更加便宜。
```

提示:
* n == price.length
* n == needs.length
* 1 <= n <= 6
* 0 <= price[i] <= 10
* 0 <= needs[i] <= 10
* 1 <= special.length <= 100
* special[i].length == n + 1
* 0 <= special[i][j] <= 50

Solution:
### 转换 DFS(转换为礼包处理)

对于某个 $need[i]$ 而言,既可以「单买」也可以使用「礼包形式购买」,同时两种购买方式都存在对「份数」的决策(单买多少份/买多少个相应的礼包)。

利用物品数量和礼包数量数据范围都较少,我们可以先对「单买」情况进行预处理,将其转换为「礼包」形式。若 $price[0] = 100$,则使用礼包 $[1, 0, 0, ...,0, 100]$ 来代指。

然后再预处理每个礼包最多选多少个,并使用哈希表进行存储。

最后使用 `DFS` 对每个「礼包」如何选择进行爆搜即可。

代码:
```Java
class Solution {
    int ans = 0x3f3f3f3f;
    List<Integer> price, needs;
    List<List<Integer>> special;
    Map<Integer, Integer> map = new HashMap<>();
    int n, m;
    public int shoppingOffers(List<Integer> _price, List<List<Integer>> _special, List<Integer> _needs) {
        price = _price; special = _special; needs = _needs;
        n = price.size();
        List<Integer> temp = new ArrayList<>();
        for (int i = 0; i < n; i++) temp.add(0);
        for (int i = 0; i < n; i++) {
            List<Integer> clone = new ArrayList<>(temp);
            clone.set(i, 1);
            clone.add(price.get(i));
            special.add(clone);
        }
        m = special.size();
        for (int i = 0; i < m; i++) {
            List<Integer> x = special.get(i);
            int max = 0;
            for (int j = 0; j < n; j++) {
                int a = x.get(j), b = needs.get(j);
                if (a == 0 || b == 0) continue;
                max = Math.max(max, (int)Math.ceil(b / a));
            }
            map.put(i, max);
        }
        dfs(0, needs, 0);
        return ans;
    }
    void dfs(int u, List<Integer> list, int cur) {
        if (cur >= ans) return ;
        if (u == m) {
            for (int i = 0; i < n; i++) {
                if (list.get(i) != 0) return ;
            }
            ans = Math.min(ans, cur);
            return ;
        }
        List<Integer> x = special.get(u);
        out:for (int k = 0; k <= map.get(u); k++) {
            List<Integer> clist = new ArrayList<>(list);
            for (int i = 0; i < n; i++) {
                int a = x.get(i), b = clist.get(i);
                if (a * k > b) break out;
                clist.set(i, b - a * k);
            }
            dfs(u + 1, clist, cur + k * x.get(n));
        }
    }
}
```
* 时间复杂度:令物品数量为 $n$,原礼包数量为 $m$。将「单买」预处理成「礼包」,共有 $n$ 中「单买」情况需要转换,同时每个转换需要对数组进行复制,复杂度为 $O(n^2)$,预处理完后,共有 $n + m$ 个礼包;预处理每个礼包所能选的最大数量,复杂度为 $O((n * m) * n)$;对礼包的选择方案进行决策,一个礼包最多选择 $k = \max(needs[i]) = 10$ 个,复杂度为 $O(k ^ {n + m})$
* 空间复杂度:$O(k ^ {n + m})$

---

### 完全背包

这还是一道很有意思的「完全背包」问题。

不了解「完全背包」的同学,可以先看前置🧀:[完全背包](https://mp.weixin.qq.com/s?__biz=MzU4NDE3MTEyMA==&mid=2247486107&idx=1&sn=e5fa523008fc5588737b7ed801caf4c3&chksm=fd9ca184caeb28926959c0987208a3932ed9c965267ed366b5b82a6fc16d42f1ff40c29db5f1&scene=178&cur_album_id=1751702161341628417#rd)。目前[「背包问题」专题](https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzU4NDE3MTEyMA==&action=getalbum&album_id=1751702161341628417&scene=173&from_msgid=2247486107&from_itemidx=1&count=3&nolastread=1#wechat_redirect) 已经讲了 $21$ 篇,大概还有 $2$ - $4$ 篇彻底讲完,完全覆盖了所有的「背包问题」。

背包问题难点在于对「成本」和「价值」的抽象。

对于本题,我们可以定义 **$f[i, j_0, j_1, j_2, ... , j_{n - 1}]$ 为考虑前 $i$ 个大礼包,购买 $j_0$ 件物品 $0$,购买 $j_1$ 件物品 $1$,....,购买 $j_{n - 1}$ 件物品 $n - 1$ 时的最小花费。**

同时,我们有初始化条件 $f[0, 0, 0, ... , 0] = 0$(其余 $f[0, x_0, x_1, x_2, ..., x_n]$ 为正无穷)的初始化条件,最终答案为 $f[m - 1, need[0], need[1], ..., need[n - 1]]$。

这样的朴素完全背包做法复杂度过高,根据我们的前置🧀 [完全背包](https://mp.weixin.qq.com/s?__biz=MzU4NDE3MTEyMA==&mid=2247486107&idx=1&sn=e5fa523008fc5588737b7ed801caf4c3&chksm=fd9ca184caeb28926959c0987208a3932ed9c965267ed366b5b82a6fc16d42f1ff40c29db5f1&scene=178&cur_album_id=1751702161341628417#rd) 中的数学推导分析,我们发现完全背包的一维空间优化,是具有优化复杂度的意义。

因此,我们可以对礼包维度进行优化,使用 $f[need[0], need[1], ... , need[n - 1]]$ 来作为状态表示。

不失一般性的考虑 $f[j_0, j_1, ... , j_{n - 1}]$ 该如何转移(以物品 $0$ 为例进行分析,其他同理):

1. 不选择任何大礼包,只进行单买:$f[j_0, j_1, ... , j_{n - 1}] = min(f[j_0, j_1, ... , j_{n - 1}], f[j_0 - 1, j_1, ..., j_{n - 1}] + price[0]$;
2. 购买大礼包:$f[j_0, j_1, ... , j_{n - 1}] = min(f[j_0, j_1, ... , j_{n - 1}], f[j_0 - special[i][0], j_1 - special[i][1],, ..., j_{n - 1} - special[i][n - 1]] + special[i][n]$

最终的 $f[j_0, j_1, ... , j_{n - 1}]$ 为上述所有方案中的最小值。

>一些细节:实现时,为了防止过多的维度,以及可能存在的 MLE 风险,我们可以对维度进行压缩处理,而最简单的方式可以通过与排列数建立映射关系。

代码:
```Java
class Solution {
    public int shoppingOffers(List<Integer> price, List<List<Integer>> special, List<Integer> needs) {
        int n = price.size();
        int[] g = new int[n + 1];
        g[0] = 1;
        for (int i = 1; i <= n; i++) {
            g[i] = g[i - 1] * (needs.get(i - 1) + 1);
        }
        int mask = g[n];
        int[] f = new int[mask];
        int[] cnt = new int[n];
        for (int state = 1; state < mask; state++) {
            f[state] = 0x3f3f3f3f;
            Arrays.fill(cnt, 0);
            for (int i = 0; i < n; i++) {
                cnt[i] = state % g[i + 1] / g[i];
            }
            for (int i = 0; i < n; i++) {
                if (cnt[i] > 0) f[state] = Math.min(f[state], f[state - g[i]] + price.get(i));
            }
            out:for (List<Integer> x : special) {
                int cur = state;
                for (int i = 0; i < n; i++) {
                    if (cnt[i] < x.get(i)) continue out;
                    cur -= x.get(i) * g[i];
                }
                f[state] = Math.min(f[state], f[cur] + x.get(n));
            }
        }
        return f[mask - 1];
    }
}
```
* 时间复杂度:令物品数量为 $n$,原礼包数量为 $m$。每个物品最多需要 $k = \max(needs[i]) = 10$ 个,共有 $k^n$ 个状态需要转移,转移时需要考虑「单买」和「礼包」决策,复杂度分别为 $O(n)$ 和 $O(m * n)$。整体复杂度为 $O(k^n * (m * n))$
* 空间复杂度:$O(k^n * (m * n))$

### 最后

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

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

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

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


Question:
一条包含字母 A-Z 的消息通过以下的方式进行了编码:
```
'A' -> 1
'B' -> 2
...
'Z' -> 26
```
要 解码 一条已编码的消息,所有的数字都必须分组,然后按原来的编码方案反向映射回字母(可能存在多种方式)。例如,"11106" 可以映射为:
* "AAJF" 对应分组 (1 1 10 6)
* "KJF" 对应分组 (11 10 6)

注意,像 (1 11 06) 这样的分组是无效的,因为 "06" 不可以映射为 'F' ,因为 "6" 与 "06" 不同。

除了 上面描述的数字字母映射方案,编码消息中可能包含 `'*'` 字符,可以表示从 '1' 到 '9' 的任一数字(不包括 '0')。例如,编码字符串 `"1*"` 可以表示 "11"、"12"、"13"、"14"、"15"、"16"、"17"、"18" 或 "19" 中的任意一条消息。对 "1*" 进行解码,相当于解码该字符串可以表示的任何编码消息。

给你一个字符串 s ,由数字和 '*' 字符组成,返回 解码 该字符串的方法 数目 。

由于答案数目可能非常大,返回对 $10^9 + 7$ 取余 的结果。

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

输出:9

解释:这一条编码消息可以表示 "1"、"2"、"3"、"4"、"5"、"6"、"7"、"8" 或 "9" 中的任意一条。
可以分别解码成字符串 "A"、"B"、"C"、"D"、"E"、"F"、"G"、"H" 和 "I" 。
因此,"*" 总共有 9 种解码方法。
```
示例 2:
```
输入:s = "1*"

输出:18

解释:这一条编码消息可以表示 "11"、"12"、"13"、"14"、"15"、"16"、"17"、"18" 或 "19" 中的任意一条。
每种消息都可以由 2 种方法解码(例如,"11" 可以解码成 "AA" 或 "K")。
因此,"1*" 共有 9 * 2 = 18 种解码方法。
```
示例 3:
```
输入:s = "2*"

输出:15

解释:这一条编码消息可以表示 "21"、"22"、"23"、"24"、"25"、"26"、"27"、"28" 或 "29" 中的任意一条。
"21"、"22"、"23"、"24"、"25" 和 "26" 由 2 种解码方法,但 "27"、"28" 和 "29" 仅有 1 种解码方法。
因此,"2*" 共有 (6 * 2) + (3 * 1) = 12 + 3 = 15 种解码方法。
```

提示:
* $1$ <= s.length <= $10^5$
* s[i] 是 `0-9` 中的一位数字或字符 '*'

Solution:
### 分情况讨论 DP

这是一道普通的线性 DP 题(之所以说普通,是因为状态定义比较容易想到),也是 [(题解)91. 解码方法](https://leetcode-cn.com/problems/decode-ways/solution/gong-shui-san-xie-gen-ju-shu-ju-fan-wei-ug3dd/) 的进阶题。

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

**定义 $f[i]$ 为考虑以 $s[i]$ 为结尾的字符串,共有多少种解码方案。**

那么最终答案为 $f[n - 1]$,同时我们有显而易见的起始状态 `f[0] = s[0] == '*' ? 9 : (s[0] != '0' ? 1 : 0)`.

不失一般性考虑 $f[i]$ 该如何转移,$s[i]$ 要么是 `*`,要么是数字,对应一个分情况讨论过程:

* 当 $s[i]$ 为 `*`:此时考虑 $s[i]$ 是单独作为一个 `item`,还是与上一个字符共同作为一个 `item`:
    * $s[i]$ 单独作为一个 `item`:由于 `*` 可以代指数字 `1-9`,因此有 $f[i] = f[i - 1] * 9$;
    * $s[i]$ 与上一个字符共同作为一个 `item`:此时需要对上一个字符 $s[j]$ 进行讨论:
        * $s[j]$ 为数字 `1`:此时 $s[i]$ 可以代指 `1-9`,对应了 `item` 为 `11-19` 这 $9$ 种情况,此时有 $f[i] = f[i - 2] * 9$(**如果 $f[i - 2]$ 取不到,则使用 $1$ 代指,下面同理**);
        * $s[j]$ 为数字 `2`:此时 $s[i]$ 可以代指 `1-6`,对应了 `item` 为 `21-26` 这 $6$ 种情况,此时有 $f[i] = f[i - 2] * 6$;
        *  $s[j]$ 为字符 `*`:此时两个 `*` 对应了合法方案为 $11-19$ 和 $21-26$ 共 $15$ 种方案,此时有 $f[i] = f[i - 2] * 15$;

* 当 $s[i]$ 为数字:此时可以从「前一字符 $s[j]$ 为何种字符」和「当前 $s[i]$ 是否为 $0$」出发进行讨论:
    *  $s[j]$ 为字符 `*`,根据当前 $s[i]$ 是否为 $0$ 讨论:
        * $s[i]$ 为数字 $0$:此时 $s[i]$ 无法独自作为一个 `item`,只能与 $s[j]$ 组合,对应了 $10$ 和 $20$ 两种情况,此时有 $f[i] = f[i - 2] * 2$;
        * $s[i]$ 为数字 `1-9`,此时首先有 $s[i]$ 可以作为一个独立 `item` 的情况,即有 $f[i] = f[i - 1]$,然后对 $s[i]$ 的数值大小进一步分情况讨论:
            * $s[i]$ 为数字 `1-6`,此时 $s[j]$ 可以代指 $1$ 和 $2$,对应了方案 $1x$ 和 $2x$,此时有 $f[i] = f[i - 2] * 2$;
            * $s[i]$ 为数字 `7-9`,此时 $s[j]$ 可以代指 $1$,对应方案 $1x$,此时有 $f[i] = f[i - 2]$;
    * $s[j]$ 为数字类型,此时从「当前 $s[i]$ 是否为 $0$」出发进行讨论:
        * $s[i]$ 为数字 $0$:此时 $s[j]$ 只有为 $1$ 和 $2$ 时,才是合法方案,则有 $f[i] = f[i - 2]$;
        * $s[i]$ 为数字 `1-9`:此时首先有 $s[i]$ 可以作为一个独立 `item` 的情况,即有 $f[i] = f[i - 1]$,然后再考虑能够与 $s[j]$ 组成合法 `item` 的情况:
            * $s[j]$ 为数值 $1$:此时有 $f[i] = f[i - 2]$;
            * $s[j]$ 为数值 $2$,且 $s[i]$ 为数值 `1-6`:此时有 $f[i] = f[i - 2]$。

由于是求方案数,因此最终的 $f[i]$ 为上述所有的合法的分情况讨论的累加值,并对 $1e9+ 7$ 取模。

> 一些细节:实现上了避免大量对 $f[i - 2]$ 是否可以取得的讨论,我们可以对 `s` 前追加一个空格作为哨兵(无须真正插入),以简化代码,同时由于 $f[i]$ 只依赖于 $f[i - 1]$ 和 $f[i - 1]$,可以使用「滚动数组」的形式进行空间空间优化(见 $P2$)。

> 另外,对于「滚动数组」的空间优化方式,还需要说明两点:转移前先使用变量保存 `(i-1)%3` 和 `(i-2)%3` 的计算结果,防止大量的重复计算;不能再偷懒使用 `toCharArray`,只能使用 `charAt`,因为 Java 为了遵循字符串不变的原则,会在调用 `toCharArray` 时返回新数组,这样复杂度就还是 $O(n)$ 的。
诸如此类的「滚动数组」优化方式,最早在 [这里](https://mp.weixin.qq.com/s?__biz=MzU4NDE3MTEyMA==&mid=2247485638&idx=1&sn=d7468955c5b121664031fd5c1b5a6f10&chksm=fd9ca3d9caeb2acf2dd4e9f512b2d4dc820107489a094749ffe7cae646f0cc60de06f2d18898&scene=178&cur_album_id=1751702161341628417#rd) 讲过。

代码:
```Java
class Solution {
    int mod = (int)1e9+7;
    public int numDecodings(String s) {
        char[] cs = s.toCharArray();
        int n = cs.length;
        long[] f = new long[n];
        f[0] = cs[0] == '*' ? 9 : (cs[0] != '0' ? 1 : 0);
        for (int i = 1; i < n; i++) {
            char c = cs[i], prev = cs[i - 1];
            if (c == '*') {
                // cs[i] 单独作为一个 item
                f[i] += f[i - 1] * 9;
                // cs[i] 与前一个字符共同作为一个 item
                if (prev == '*') {
                    // 11 - 19 & 21 - 26
                    f[i] += (i - 2 >= 0 ? f[i - 2] : 1) * 15;
                } else {
                    int u = (int)(prev - '0');
                    if (u == 1) {
                        f[i] += (i - 2 >= 0 ? f[i - 2] : 1) * 9;
                    } else if (u == 2) {
                        f[i] += (i - 2 >= 0 ? f[i - 2] : 1) * 6;
                    }
                }
            } else {
                int t = (int)(c - '0');
                if (prev == '*') {
                    if (t == 0) {
                        f[i] += (i - 2 >= 0 ? f[i - 2] : 1) * 2;
                    } else {
                        // cs[i] 单独作为一个 item
                        f[i] += f[i - 1];
                        // cs[i] 与前一个字符共同作为一个 item
                        if (t <= 6) {
                            f[i] += (i - 2 >= 0 ? f[i - 2] : 1) * 2;
                        } else {
                            f[i] += i - 2 >= 0 ? f[i - 2] : 1;
                        }
                    }
                } else {
                    int u = (int)(prev - '0');
                    if (t == 0) {
                        if (u == 1 || u == 2) {
                            f[i] += i - 2 >= 0 ? f[i - 2] : 1;
                        }
                    } else {
                        // cs[i] 单独作为一个 item
                        f[i] += (f[i - 1]);
                        // cs[i] 与前一个字符共同作为一个 item
                        if (u == 1) {
                            f[i] += i - 2 >= 0 ? f[i - 2] : 1;
                        } else if (u == 2 && t <= 6) {
                            f[i] += i - 2 >= 0 ? f[i - 2] : 1;
                        }
                    }
                }
            }
            f[i] %= mod;
        }
        return (int)(f[n - 1]);
    }
}
```

```Java
class Solution {
    int mod = (int)1e9+7;
    public int numDecodings(String s) {
        int n = s.length() + 1;
        long[] f = new long[3];
        f[0] = 1;
        f[1] = s.charAt(0) == '*' ? 9 : (s.charAt(0) != '0' ? 1 : 0);
        for (int i = 2; i < n; i++) {
            char c = s.charAt(i - 1), prev = s.charAt(i - 2);
            int p1 = (i - 1) % 3, p2 = (i - 2) % 3;
            long cnt = 0;
            if (c == '*') {
                // cs[i] 单独作为一个 item
                cnt += f[p1] * 9;
                // cs[i] 与前一个字符共同作为一个 item
                if (prev == '*') {
                    cnt += f[p2] * 15;
                } else {
                    int u = (int)(prev - '0');
                    if (u == 1) cnt += f[p2] * 9;
                    else if (u == 2) cnt += f[p2] * 6;
                }
            } else {
                int t = (int)(c - '0');
                if (prev == '*') {
                    if (t == 0) {
                        cnt += f[p2]* 2;
                    } else {
                        // cs[i] 单独作为一个 item
                        cnt += f[p1];
                        // cs[i] 与前一个字符共同作为一个 item
                        if (t <= 6) cnt += f[p2] * 2;
                        else cnt += f[p2];
                    }
                } else {
                    int u = (int)(prev - '0');
                    if (t == 0) {
                        if (u == 1 || u == 2) cnt += f[p2];
                    } else {
                        // cs[i] 单独作为一个 item
                        cnt += f[p1];
                        // cs[i] 与前一个字符共同作为一个 item
                        if (u == 1) cnt += f[p2];
                        else if (u == 2 && t <= 6) cnt += f[p2];
                    }
                }
            }
            f[i % 3] = cnt % mod;
        }
        return (int)(f[(n - 1) % 3]);
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:使用「滚动数组」进行优化,复杂度为 $O(1)$,否则为 $O(n)$

---

### 枚举 DP

上述解法之所以复杂,是因为不仅仅要对当前字符 $s[i]$ 分情况讨论,还需要对上一个字符 $s[j]$ 分情况讨论。

事实上,我们可以利用解码对象只有 `A-Z` 来进行枚举。

在从前往后处理字符串 `s` 时,枚举 $s[i]$ 参与构成的解码内容 `item` 是字母 `A-Z` 中哪一个,从而将分情况讨论转变成对应位的字符对比。

代码:
```Java
class Solution {
    int mod = (int)1e9+7;
    public int numDecodings(String s) {
        int n = s.length();
        long[] f = new long[3];
        f[0] = 1;
        for (int i = 1; i <= n; i++) {
            char c = s.charAt(i - 1);
            int t = c - '0';
            long cnt = 0;
            int p1 = (i - 1) % 3, p2 = (i - 2) % 3;
            // 枚举组成什么 item(A -> 1; B -> 2 ...)
            for (int item = 1; item <= 26; item++) {
                if (item < 10) { // 该 item 由一个字符组成
                    if (c == '*' || t == item) cnt += f[p1];
                } else { // 该 item 由两个字符组成
                    if (i - 2 < 0) break;
                    char prev = s.charAt(i - 2);
                    int u = prev - '0';
                    int a = item / 10, b = item % 10;
                    if ((prev == '*' || u == a) && (t == b || (c == '*' && b != 0))) cnt += f[p2];
                }
            }
            f[i % 3] = cnt % mod;
        }
        return (int)(f[n % 3]);
    }
}
```
* 时间复杂度:$O(n * C)$,其中 $C$ 为解码内容字符集大小,固定为 $26$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


Question:
求解一个给定的方程,将 `x` 以字符串 `"x=#value"` 的形式返回。该方程仅包含 `'+'` , `'-'` 操作,变量 `x` 和其对应系数。

如果方程没有解,请返回 `"No solution"`。如果方程有无限解,则返回 `"Infinite solutions"`。

题目保证,如果方程中只有一个解,则 'x' 的值是一个整数。

示例 1:
```
输入: equation = "x+5-3+x=6+x-2"

输出: "x=2"
```
示例 2:
```
输入: equation = "x=x"

输出: "Infinite solutions"
```
示例 3:
```
输入: equation = "2x=x"

输出: "x=0"
```

提示:
* $3 <= equation.length <= 1000$
* `equation` 只有一个 `'='`.
* `equation` 方程由整数组成,其绝对值在 $[0, 100]$ 范围内,不含前导零和变量 `'x'` 。

Solution:
### 模拟

为了方便,我们令 `equation` 为 `s`。

由于运算符只有 `+` 和 `-`,因此无须考虑运算优先级,可在遍历过程中进行计算。

使用变量 `x` 和 `num` 分别代指当前运算结果中 $x$ 的系数以及数值部分,从前往后处理 `s` 的每个字符,根据字符类型进行分情况讨论,假设当前处理到的数值为 $s[i]$:

* 若 $s[i] =$ `+/-`:此时影响的是下一个运算数值的正负,修改对应的 `op` 标识;
* 若 $s[i] =$ `数值`:此时将完整的运算值进行取出(运算值可能是关于 $x$ 的描述,可能是纯数值),假设连续段 $s[i:j - 1]$ 之间为当前运算值,根据 $s[j - 1]$ 是否为字符 `x` 可知,是要将 $s[i:j - 2]$ 的数值累加到变量 `x`,还是将 $s[i:j - 1]$ 的数值累加到变量 `num`;
* 若 $s[i] =$ `=`:此时代表方程的左边已处理完,将变量 `x` 和 `num` 进行翻转(含义为将左边的运算结果移项到右边),并继续往后处理。

当整个字符串 `s` 处理完后,我们得到最终关于 $x$ 的系数 `x`,以及数值大小 `num`。

根据 `x` 是否为 $0$ 可知答案:

* 若 `x` 为 $0$:此时根据 `num` 是否为 $0$ 可知是 `Infinite solutions`(对应 `num` 为 $0$) 还是 `No solution`(对应 `num` 不为 $0$)
* 若 `x` 不为 $0$:对 `x` 和 `num` 进行约分后,返回对应答案。

Java 代码:
```Java
class Solution {
    public String solveEquation(String s) {
        int x = 0, num = 0, n = s.length();
        char[] cs = s.toCharArray();
        for (int i = 0, op = 1; i < n; ) {
            if (cs[i] == '+') {
                op = 1; i++;
            } else if (cs[i] == '-') {
                op = -1; i++;
            } else if (cs[i] == '=') {
                x *= -1; num *= -1; op = 1; i++;
            } else {
                int j = i;
                while (j < n && cs[j] != '+' && cs[j] != '-' && cs[j] != '=') j++;
                if (cs[j - 1] == 'x') x += (i < j - 1 ? Integer.parseInt(s.substring(i, j - 1)) : 1) * op;
                else num += Integer.parseInt(s.substring(i, j)) * op;
                i = j;
            }
        }
        if (x == 0) return num == 0 ? "Infinite solutions" : "No solution";
        else return "x=" + (num / -x);
    }
}
```
TypeScript 代码:
```TypeScript
function solveEquation(s: string): string {
    let x = 0, num = 0, n = s.length
    for (let i = 0, op = 1; i < n; ) {
        if (s[i] == '+') {
            op = 1; i++;
        } else if (s[i] == '-') {
            op = -1; i++
        } else if (s[i] == '=') {
            x *= -1; num *= -1; op = 1; i++;
        } else {
            let j = i
            while (j < n && s[j] != '+' && s[j] != '-' && s[j] != '=') j++
            if (s[j - 1] == 'x') x += (i < j - 1 ? Number(s.substring(i, j - 1)) : 1) * op
            else num += Number(s.substring(i, j)) * op
            i = j
        }
    }
    if (x == 0) return num == 0 ? "Infinite solutions" : "No solution"
    else return "x=" + (num / -x)
};
```
* 时间复杂度:$O(n)$
* 空间复杂度:使用 `charAt` 替换 `toCharArray`。复杂度为 $O(1)$

### 最后

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

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

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

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


641. 设计循环双端队列

Question:
设计实现双端队列。

实现 `MyCircularDeque` 类:

* `MyCircularDeque(int k)` :构造函数,双端队列最大为 $k$ 。
* `boolean insertFront()`:将一个元素添加到双端队列头部。 如果操作成功返回 `true` ,否则返回 `false` 。
* `boolean insertLast()` :将一个元素添加到双端队列尾部。如果操作成功返回 `true` ,否则返回 `false` 。
* `boolean deleteFront()` :从双端队列头部删除一个元素。 如果操作成功返回 `true` ,否则返回 `false` 。
* `boolean deleteLast()` :从双端队列尾部删除一个元素。如果操作成功返回 `true` ,否则返回 `false` 。
* `int getFront()` :从双端队列头部获得一个元素。如果双端队列为空,返回 `-1` 。
* `int getRear()` :获得双端队列的最后一个元素。 如果双端队列为空,返回 `-1` 。
* `boolean isEmpty()` :若双端队列为空,则返回 `true` ,否则返回 `false`  。
* `boolean isFull()` :若双端队列满了,则返回 `true` ,否则返回 `false` 。

示例 1:
```
输入
["MyCircularDeque", "insertLast", "insertLast", "insertFront", "insertFront", "getRear", "isFull", "deleteLast", "insertFront", "getFront"]
[[3], [1], [2], [3], [4], [], [], [], [4], []]

输出
[null, true, true, true, false, 2, true, true, true, 4]

解释
MyCircularDeque circularDeque = new MycircularDeque(3); // 设置容量大小为3
circularDeque.insertLast(1);			        // 返回 true
circularDeque.insertLast(2);			        // 返回 true
circularDeque.insertFront(3);			        // 返回 true
circularDeque.insertFront(4);			        // 已经满了,返回 false
circularDeque.getRear();  				// 返回 2
circularDeque.isFull();				        // 返回 true
circularDeque.deleteLast();			        // 返回 true
circularDeque.insertFront(4);			        // 返回 true
circularDeque.getFront();				// 返回 4
```

提示:
* $1 <= k <= 1000$
* $0 <= value <= 1000$
* `insertFront`, `insertLast`, `deleteFront`, `deleteLast`, `getFront`, `getRear`, `isEmpty`, `isFull`  调用次数不大于 `2000` 次

Solution:
### 基本分析

一个基本的实现,需要满足除构造函数以外复杂度为 $O(k)$ 以外,其余操作均为 $O(1)$。

常规实现包含两种方式:**数组实现** 与 **链表实现**。

其中数组实现可以利用调用次数较小,开成调用次数 $3$ 倍大小,然后从中间开始往两边存储,这样做就不用考虑下标边的界问题;而更为常规的解法是构造一个与限定空间 $k$ 等大的数组,使用两下标并配合坐标转换来做,对于下标自增操作而言,只需要进行「加一取模」即可,而对于下标自减操作,由于考虑负值问题,需要进行「增加限定空间偏移后,进行减一再取模」。

而链表实现则无须考虑额外的下标转换问题,但需要额外定义类。

---

### 数组

使用两坐标 `he` 和 `ta` 分别代表队列头和尾(初始值均为 $0$),使用 `cnt` 记录当前队列元素大小,使用 `k` 记录初始化时指定的空间大小。

对各类操作进行逻辑控制:

* `insertFront` 操作:需要对 `he` 进行自减操作,即 `he = (he + k - 1) % k` 为目标位置,同时对 `cnt` 进行自增;
* `insertLast` 操作:需要对 `ta` 进行自增操作,但 `ta` 起始指向是待插入位置,因此 `ta` 为目标位置,随后更新 `ta = (ta + 1) % k`,同时对 `cnt` 进行自增;
* `deleteFront` 操作:需要对 `he` 进行自增操作,直接更新 `he = (he + 1) % k`,同时更新 `cnt` 进行自减;
* `deleteLast` 操作:需要对 `ta` 进行自减操作,更新 `ta = (ta + k - 1) % k`,同时更新 `cnt` 进行自减;
* `getFront` 操作:返回 `nums[he]` 即可, 若 `isFull` 为 `True`,返回 `-1`;
* `getRear` 操作:返回 `nums[ta - 1]`,由于存在负值问题,需要转换为返回 `nums[(ta + k - 1) % k]` 若 `isFull` 为 `True`,返回 `-1`;
* `isEmpty` 操作:根据 `cnt` 与 `k` 的关系进行返回;
* `isFull` 操作:根据 `cnt` 与 `k` 的关系进行返回;

Java 代码:
```Java
class MyCircularDeque {
    int[] nums;
    int he, ta, cnt, k;
    public MyCircularDeque(int _k) {
        k = _k;
        nums = new int[k];
    }
    public boolean insertFront(int value) {
        if (isFull()) return false;
        he = (he + k - 1) % k;
        nums[he] = value; cnt++;
        return true;
    }
    public boolean insertLast(int value) {
        if (isFull()) return false;
        nums[ta++] = value; cnt++;
        ta %= k;
        return true;
    }
    public boolean deleteFront() {
        if (isEmpty()) return false;
        he = (he + 1) % k; cnt--;
        return true;
    }
    public boolean deleteLast() {
        if (isEmpty()) return false;
        ta = (ta + k - 1) % k; cnt--;
        return true;
    }
    public int getFront() {
        return isEmpty() ? -1 : nums[he];
    }
    public int getRear() {
        return isEmpty() ? -1 : nums[(ta + k - 1) % k];
    }
    public boolean isEmpty() {
        return cnt == 0;
    }
    public boolean isFull() {
        return cnt == k;
    }
}
```
TypeScript 代码:
```TypeScript
class MyCircularDeque {
    he = 0; ta = 0; cnt = 0; k = 0;
    nums: number[];
    constructor(_k: number) {
        this.k = _k
        this.he = this.ta = this.cnt = 0
        this.nums = new Array<number>(this.k)
    }
    insertFront(value: number): boolean {
        if (this.isFull()) return false
        this.he = (this.he + this.k - 1) % this.k
        this.nums[this.he] = value
        this.cnt++
        return true
    }
    insertLast(value: number): boolean {
        if (this.isFull()) return false
        this.nums[this.ta++] = value
        this.ta %= this.k
        this.cnt++
        return true
    }
    deleteFront(): boolean {
        if (this.isEmpty()) return false
        this.he = (this.he + 1) % this.k
        this.cnt--
        return true
    }
    deleteLast(): boolean {
        if (this.isEmpty()) return false
        this.ta = (this.ta + this.k - 1) % this.k
        this.cnt--
        return true
    }
    getFront(): number {
        return this.isEmpty() ? -1 : this.nums[this.he]
    }
    getRear(): number {
        return this.isEmpty() ? -1 : this.nums[(this.ta + this.k - 1) % this.k]
    }
    isEmpty(): boolean {
        return this.cnt == 0
    }
    isFull(): boolean {
        return this.cnt == this.k
    }
}
```
* 时间复杂度:除在初始化函数中构造容器复杂度为 $O(k)$ 以外,其余操作复杂度均为 $O(1)$
* 空间复杂度:$O(n)$

---

### 链表

创建 `Node` 代指每个操作的元素,使用双向链表来构造循环队列。

各类操作均对应基本的链表操作,不再赘述。

Java 代码:
```Java
class MyCircularDeque {
    class Node {
        Node prev, next;
        int val;
        Node (int _val) {
            val = _val;
        }
    }
    int cnt, k;
    Node he, ta;
    public MyCircularDeque(int _k) {
        k = _k;
        he = new Node(-1); ta = new Node(-1);
        he.next = ta; ta.prev = he;
    }
    public boolean insertFront(int value) {
        if (isFull()) return false;
        Node node = new Node(value);
        node.next = he.next;
        node.prev = he;
        he.next.prev = node;
        he.next = node;
        cnt++;
        return true;
    }
    public boolean insertLast(int value) {
        if (isFull()) return false;
        Node node = new Node(value);
        node.next = ta;
        node.prev = ta.prev;
        ta.prev.next = node;
        ta.prev = node;
        cnt++;
        return true;
    }
    public boolean deleteFront() {
        if (isEmpty()) return false;
        he.next.next.prev = he;
        he.next = he.next.next;
        cnt--;
        return true;
    }
    public boolean deleteLast() {
        if (isEmpty()) return false;
        ta.prev.prev.next = ta;
        ta.prev = ta.prev.prev;
        cnt--;
        return true;
    }
    public int getFront() {
        return isEmpty() ? -1 : he.next.val;
    }
    public int getRear() {
        return isEmpty() ? -1 : ta.prev.val;
    }
    public boolean isEmpty() {
        return cnt == 0;
    }
    public boolean isFull() {
        return cnt == k;
    }
}
```
TypeScript 代码:
```TypeScript
class TNode {
    prev: TNode = null; next: TNode = null;
    val: number = 0;
    constructor(_val: number) {
        this.val = _val
    }
}
class MyCircularDeque {
    he = null; ta = null;
    cnt = 0; k = 0;
    constructor(_k: number) {
        this.cnt = 0; this.k = _k;
        this.he = new TNode(-1); this.ta = new TNode(-1);
        this.he.next = this.ta
        this.ta.prev = this.he
    }
    insertFront(value: number): boolean {
        if (this.isFull()) return false
        const node = new TNode(value)
        node.next = this.he.next
        node.prev = this.he
        this.he.next.prev = node
        this.he.next = node
        this.cnt++
        return true
    }
    insertLast(value: number): boolean {
        if (this.isFull()) return false
        const node = new TNode(value)
        node.next = this.ta
        node.prev = this.ta.prev
        this.ta.prev.next = node
        this.ta.prev = node
        this.cnt++
        return true
    }
    deleteFront(): boolean {
        if (this.isEmpty()) return false
        this.he.next.next.prev = this.he
        this.he.next = this.he.next.next
        this.cnt--
        return true
    }
    deleteLast(): boolean {
        if (this.isEmpty()) return false
        this.ta.prev.prev.next = this.ta
        this.ta.prev = this.ta.prev.prev
        this.cnt--
        return true
    }
    getFront(): number {
        return this.isEmpty() ? -1 : this.he.next.val
    }
    getRear(): number {
        return this.isEmpty() ? -1 : this.ta.prev.val
    }
    isEmpty(): boolean {
        return this.cnt == 0
    }
    isFull(): boolean {
        return this.cnt == this.k
    }
}
```
* 时间复杂度:所有操作复杂度均为 $O(1)$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


643. 子数组最大平均数 I

Question:
给定 n 个整数,找出平均数最大且长度为 k 的连续子数组,并输出该最大平均数。

示例:
```
输入:[1,12,-5,-6,50,3], k = 4

输出:12.75

解释:最大平均数 (12-5-6+50)/4 = 51/4 = 12.75
```

提示:
* 1 <= k <= n <= 30,000。
* 所给数据范围 [-10,000,10,000]。

Solution:
### 滑动窗口

这是一道道滑动窗口裸题。

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

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

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

代码:
```Java
class Solution {
    public double findMaxAverage(int[] nums, int k) {
        double ans = 0, sum = 0;
        for (int i = 0; i < k; i++) sum += nums[i];
        ans = sum / k;
        for (int i = k; i < nums.length; i++) {
            sum = sum + nums[i] - nums[i - k]; // int add = nums[i], del = nums[i - k];
            ans = Math.max(ans, sum / k);
        }
        return ans;
    }
}
```
* 时间复杂度:每个元素最多滑入和滑出窗口一次。复杂度为 $O(n)$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


Question:
集合 `s` 包含从 `1` 到 `n` 的整数。不幸的是,因为数据错误,导致集合里面某一个数字复制了成了集合里面的另外一个数字的值,导致集合 丢失了一个数字 并且 有一个数字重复 。

给定一个数组 `nums` 代表了集合 `S` 发生错误后的结果。

请你找出重复出现的整数,再找到丢失的整数,将它们以数组的形式返回。

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

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

Solution:
### 计数

一个朴素的做法是,使用「哈希表」统计每个元素出现次数,然后在 $[1, n]$ 查询每个元素的出现次数。

在「哈希表」中出现 $2$ 次的为重复元素,未在「哈希表」中出现的元素为缺失元素。

由于这里数的范围确定为 $[1, n]$,我们可以使用数组来充当「哈希表」,以减少「哈希表」的哈希函数执行和冲突扩容的时间开销。

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

---

### 数学

我们还可以利用数值范围为 $[1, n]$,只有一个数重复和只有一个缺失的特性,进行「作差」求解。

* 令 $[1, n]$ 的求和为 $tot$,这部分可以使用「等差数列求和公式」直接得出:$tot = \frac{n (1 + n)}{2}$ ;
* 令数组 $nums$ 的求和值为 $sum$,由循环累加可得;
* 令数组 $sums$ 去重求和值为 $set$,由循环配合「哈希表/数组」累加可得。

最终答案为 `(重复元素, 缺失元素) = (sum-set, tot-set)` 。

代码;
```Java
class Solution {
    public int[] findErrorNums(int[] nums) {
        int n = nums.length;
        int[] cnts = new int[n + 1];
        int tot = (1 + n) * n / 2;
        int sum = 0, set = 0;
        for (int x : nums) {
            sum += x;
            if (cnts[x] == 0) set += x;
            cnts[x] = 1;
        }
        return new int[]{sum - set, tot - set};
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n)$

---

### 桶排序

因为值的范围在 $[1, n]$,我们可以运用「桶排序」的思路,根据 $nums[i] = i + 1$ 的对应关系使用 $O(n)$ 的复杂度将每个数放在其应该落在的位置里。

然后线性扫描一遍排好序的数组,找到不符合 $nums[i] = i + 1$ 对应关系的位置,从而确定重复元素和缺失元素是哪个值。

代码:
```Java
class Solution {
    public int[] findErrorNums(int[] nums) {
        int n = nums.length;
        for (int i = 0; i < n; i++) {
            while (nums[i] != i + 1 && nums[nums[i] - 1] != nums[i]) {
                swap(nums, i, nums[i] - 1);
            }
        }
        int a = -1, b = -1;
        for (int i = 0; i < n; i++) {
            if (nums[i] != i + 1) {
                a = nums[i];
                b = i == 0 ? 1 : nums[i - 1] + 1;
            }
        }
        return new int[]{a, b};
    }
    void swap(int[] nums, int i, int j) {
        int tmp = nums[i];
        nums[i] = nums[j];
        nums[j] = tmp;
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


Question:
给出 `n` 个数对。 在每一个数对中,第一个数字总是比第二个数字小。

现在,我们定义一种跟随关系,当且仅当 `b < c` 时,数对 $(c, d)$ 才可以跟在 $(a, b)$ 后面。我们用这种形式来构造一个数对链。

给定一个数对集合,找出能够形成的最长数对链的长度。你不需要用到所有的数对,你可以以任何顺序选择其中的一些数对来构造。

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

输出:2

解释:最长的数对链是 [1,2] -> [3,4]
```

提示:
* 给出数对的个数在 $[1, 1000]$ 范围内。

Solution:
### 排序 + 贪心 DP

起始先将 `pairs` 根据第一维排升序(或直接双关键字排升序)。

考虑定义 $f[i]$ 为以 $pairs[i]$ 为结尾的最长数对链长度,所有 $f[i]$ 中的最大值为答案。

不失一般性考虑 $f[i]$ 该如何转移:不难发现 $f[i]$ 为所有满足「下标范围在 $[0, i - 1]$,且 $pairs[j][1] < pairs[i][0]$」条件的 $f[j] + 1$ 的最大值。

但实际上,我们只需要从 $j = i - 1$ 开始往回找,找到第一个满足 $pairs[j][1] < pairs[i][0]$ 的位置 $j$ 即可。

容易证明该做法的正确性:**假设贪心解(该做法)找到的位置 $j$ 不是最优位置,即存在比 $j$ 更小的合法下标 $j'$ 满足 $f[j'] > f[j]$。根据我们的排序规则必然有 $pairs[j'][0] <= pairs[j][0]$ 的性质,则可知 $pairs[j]$ 必然可以代替 $pairs[j']$ 接在原本以 $pairs[j']$ 为结尾的最优数链上(最优数链长度不变,结果不会变差),则至少有 $f[j'] = f[j]$。**

代码:
```Java
class Solution {
    public int findLongestChain(int[][] pairs) {
        Arrays.sort(pairs, (a,b)->a[0]-b[0]);
        int n = pairs.length, ans = 1;
        int[] f = new int[n];
        for (int i = 0; i < n; i++) {
            f[i] = 1;
            for (int j = i - 1; j >= 0 && f[i] == 1; j--) {
                if (pairs[j][1] < pairs[i][0]) f[i] = f[j] + 1;
            }
            ans = Math.max(ans, f[i]);
        }
        return ans;
    }
}
```
* 时间复杂度:排序的复杂度为 $O(n\log{n})$;不考虑剪枝效果 `DP` 复杂度为 $O(n^2)$。整体复杂度为 $O(n^2)$
* 空间复杂度:$O(n)$

---

### 排序 + 贪心 DP(优化转移)

根据上述分析,我们知道对于一个特定的 $pairs[i]$ 而言,其所有合法(满足条件 $pairs[j][1] < pairs[i][0]$)的前驱状态 $f[j]$ 必然是非单调递增的。

根据 `LIS` 问题的贪心解的思路,我们可以额外使用一个数组记录下特定长度数链的最小结尾值,从而实现二分找前驱状态。

具体的,创建 $g$ 数组,其中 $g[len] = x$ 代表数链长度为 $len$ 时结尾元素的第二维最小值为 $x$。

如此一来,当我们要找 $f[i]$ 的前驱状态时,等价于在 $g$ 数组中找满足「小于 $pairs[i][0]$」的最大下标。同时,我们不再需要显式维护 $f$ 数组,只需要边转移变更新答案即可。

> **不了解 `LIS` 问题的同学可以看前置 🧀 : [LCS 问题与 LIS 问题的相互关系,以及 LIS 问题的最优解证明](https://mp.weixin.qq.com/s?__biz=MzU4NDE3MTEyMA==&mid=2247487814&idx=1&sn=e33023c2d474ff75af83eda1c4d01892) 🎉🎉🎉**

代码:
```Java
class Solution {
    public int findLongestChain(int[][] pairs) {
        Arrays.sort(pairs, (a,b)->a[0]-b[0]);
        int n = pairs.length, ans = 1;
        int[] g = new int[n + 10];
        Arrays.fill(g, 0x3f3f3f3f);
        for (int i = 0; i < n; i++) {
            int l = 1, r = i + 1;
            while (l < r) {
                int mid = l + r >> 1;
                if (g[mid] >= pairs[i][0]) r = mid;
                else l = mid + 1;
            }
            g[r] = Math.min(g[r], pairs[i][1]);
            ans = Math.max(ans, r);
        }
        return ans;
    }
}
```
* 时间复杂度:排序的复杂度为 $O(n\log{n})$;`DP` 复杂度为 $O(n\log{n})$。整体复杂度为 $O(n\log{n})$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


Question:
在英语中,我们有一个叫做 词根(`root`) 的概念,可以词根后面添加其他一些词组成另一个较长的单词——我们称这个词为 继承词(`successor`)。例如,词根`an`,跟随着单词 `other`(其他),可以形成新的单词 `another`(另一个)。

现在,给定一个由许多词根组成的词典 `dictionary` 和一个用空格分隔单词形成的句子 `sentence`。你需要将句子中的所有继承词用词根替换掉。如果继承词有许多可以形成它的词根,则用最短的词根替换它。

你需要输出替换之后的句子。

示例 1:
```
输入:dictionary = ["cat","bat","rat"], sentence = "the cattle was rattled by the battery"

输出:"the cat was rat by the bat"
```
示例 2:
```
输入:dictionary = ["a","b","c"], sentence = "aadsfasf absbs bbab cadsfafs"

输出:"a a b c"
```

提示:
* $1 <= dictionary.length <= 1000$
* $1 <= dictionary[i].length <= 100$
* `dictionary[i]` 仅由小写字母组成。
* $1 <= sentence.length <= 10^6$
* `sentence` 仅由小写字母和空格组成。
* `sentence` 中单词的总量在范围 $[1, 1000]$ 内。
* `sentence` 中每个单词的长度在范围 $[1, 1000]$ 内。
* `sentence` 中单词之间由一个空格隔开。
* `sentence` 没有前导或尾随空格。

Solution:
### 基本分析

这是一道 `Trie` 的模板题,还不了解 `Trie` 的同学可以先看前置 🧀:[【设计数据结构】实现 Trie (前缀树)](https://mp.weixin.qq.com/s?__biz=MzU4NDE3MTEyMA==&mid=2247488490&idx=1&sn=db2998cb0e5f08684ee1b6009b974089)

前置 🧀 通过图解形式讲解了 `Trie` 的结构与原理,以及提供了两种实现 `Trie` 的方式。

回到本题,为了方便,我们令 `ds` 为 `dictionary`,令 `s` 为 `sentence`。

---

### 二维数组

一个比较习惯的做法,是使用「二维数组」来实现 `Trie`,配合 `static` 优化,可以有效控制 `new` 的次数,耗时相对稳定。

考虑两个 `Trie` 的基本操作:

* `add` 操作:变量入参字符串 `s`,将字符串中的每位字符映射到 $[0, 25]$,同时为了能够方便查询某个字符串(而不只是某个前缀)是否曾经存入过 `Trie` 中,额外使用一个布尔数组 `isEnd` 记录某个位置是否为单词结尾。
* `query` 操作:对变量入参字符串 `s` 进行遍历,如果在字典树不存在该字符串的任何前缀,直接返回 `s`,否则返回首个出现(最短)的前缀。

至于二维数组的大小估算,可以直接开成 $N \times C$,其中 $N$ 为要插入到 `Trie` 中的字符总数,$C$ 为对应的字符集大小。在 $N \times C$ 没有 `MLE` 风险时,可以直接开这么多;而当 $N \times C$ 较大(超过 $1e7$,甚至 $1e8$ 时),可以适当将 $N \times C$ 中的 $N$ 减少,使得总空间在 $1e7$ 左右,因为实际上由于二维数组中的某些行中会存储一个字符以上,实际上我们用不到这么多行。

代码(不使用 `static` 优化,耗时增加十倍):
```Java
class Solution {
    static int N = 100000, M = 26;
    static int[][] tr = new int[N][M];
    static boolean[] isEnd = new boolean[N * M];
    static int idx;
    void add(String s) {
        int p = 0;
        for (int i = 0; i < s.length(); i++) {
            int u = s.charAt(i) - 'a';
            if (tr[p][u] == 0) tr[p][u] = ++idx;
            p = tr[p][u];
        }
        isEnd[p] = true;
    }
    String query(String s) {
        for (int i = 0, p = 0; i < s.length(); i++) {
            int u = s.charAt(i) - 'a';
            if (tr[p][u] == 0) break;
            if (isEnd[tr[p][u]]) return s.substring(0, i + 1);
            p = tr[p][u];
        }
        return s;
    }
    public String replaceWords(List<String> ds, String s) {
        for (int i = 0; i <= idx; i++) {
            Arrays.fill(tr[i], 0);
            isEnd[i] = false;
        }
        for (String d : ds) add(d);
        StringBuilder sb = new StringBuilder();
        for (String str : s.split(" ")) sb.append(query(str)).append(" ");
        return sb.substring(0, sb.length() - 1);
    }
}
```
* 时间复杂度:令 $n = \sum_{i = 0}^{ds.length - 1} ds[i].length$,$m$ 为 `s` 长度,复杂度为 $O(n + m)$
* 空间复杂度:$O(n \times C)$,其中 $C = 26$ 为字符集大小

---

### TrieNode

另外一个能够有效避免「估数组大小」操作的方式,是使用 `TrieNode` 的方式实现 `Trie`:每次使用到新的格子再触发 `new` 操作。

至于为什么有了 `TrieNode` 的方式,我还是会采用「二维数组」优先的做法,在 [知乎](https://zhuanlan.zhihu.com/p/531180364) 上有同学问过我类似的问题,只不过原问题是「为什么有了动态开点线段树,直接 `build` 出 $4n$ 空间的做法仍有意义」,这对应到本题使用「二维数组」还是「TrieNode」是一样的道理:

除非某些语言在启动时,采用虚拟机的方式,并且预先分配了足够的内存,否则所有的 `new` 操作都需要反映到 os 上,而在 `linux` 分配时需要遍历红黑树,因此即使是总空间一样,一次性的 `new` 要比多次小空间的 `new` 更省时间,同时集中性的 `new` 也比分散性的 `new` 操作要更快,这也就是为什么我们不无脑使用 `TrieNode` 的原因。

代码:
```Java
class Solution {
    class Node {
        boolean isEnd;
        Node[] tns = new Node[26];
    }
    Node root = new Node();
    void add(String s) {
        Node p = root;
        for (int i = 0; i < s.length(); i++) {
            int u = s.charAt(i) - 'a';
            if (p.tns[u] == null) p.tns[u] = new Node();
            p = p.tns[u];
        }
        p.isEnd = true;
    }
    String query(String s) {
        Node p = root;
        for (int i = 0; i < s.length(); i++) {
            int u = s.charAt(i) - 'a';
            if (p.tns[u] == null) break;
            if (p.tns[u].isEnd) return s.substring(0, i + 1);
            p = p.tns[u];
        }
        return s;
    }
    public String replaceWords(List<String> ds, String s) {
        for (String str : ds) add(str);
        StringBuilder sb = new StringBuilder();
        for (String str : s.split(" ")) sb.append(query(str)).append(" ");
        return sb.substring(0, sb.length() - 1);
    }
}
```
* 时间复杂度:令 $n = \sum_{i = 0}^{ds.length - 1} ds[i].length$,$m$ 为 `s` 长度,复杂度为 $O(n + m)$
* 空间复杂度:$O(n \times C)$,其中 $C = 26$ 为字符集大小

---

### 加餐

**今天额外增加一道更贴合笔试面试的加餐题 : [结合 DFS 的 Trie 运用题](https://mp.weixin.qq.com/s?__biz=MzU4NDE3MTEyMA==&mid=2247492188&idx=1&sn=a1436d1ffe2b8200a36c3196ca1c7ed1) 🎉🎉**

### 最后

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

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

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

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


Question:
Dota2 的世界里有两个阵营:`Radiant`(天辉)和 `Dire`(夜魇)。

Dota2 参议院由来自两派的参议员组成,现在参议院希望对一个 Dota2 游戏里的改变作出决定,他们以一个基于轮为过程的投票进行。

在每一轮中,每一位参议员都可以行使两项权利中的一项:

* 禁止一名参议员的权利:参议员可以让另一位参议员在这一轮和随后的几轮中丧失所有的权利 。
* 宣布胜利:如果参议员发现有权利投票的参议员都是同一个阵营的,他可以宣布胜利并决定在游戏中的有关变化。

给你一个字符串 `s` 代表每个参议员的阵营。字母 `'R'` 和 `'D'`分别代表了 `Radiant`(天辉)和 `Dire`(夜魇)。

然后,如果有 `n` 个参议员,给定字符串的大小将是 `n`。

以轮为基础的过程从给定顺序的第一个参议员开始到最后一个参议员结束,这一过程将持续到投票结束,所有失去权利的参议员将在过程中被跳过。

假设每一位参议员都足够聪明,会为自己的政党做出最好的策略,你需要预测哪一方最终会宣布胜利并在 Dota2 游戏中决定改变。

输出应该是 `"Radiant"` 或 `"Dire"`。

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

输出:"Radiant"

解释:
第 1 轮时,第一个参议员来自 Radiant 阵营,他可以使用第一项权利让第二个参议员失去所有权利。
这一轮中,第二个参议员将会被跳过,因为他的权利被禁止了。
第 2 轮时,第一个参议员可以宣布胜利,因为他是唯一一个有投票权的人。
```
示例 2:
```
输入:s = "RDD"

输出:"Dire"

解释:
第 1 轮时,第一个来自 Radiant 阵营的参议员可以使用第一项权利禁止第二个参议员的权利。
这一轮中,第二个来自 Dire 阵营的参议员会将被跳过,因为他的权利被禁止了。
这一轮中,第三个来自 Dire 阵营的参议员可以使用他的第一项权利禁止第一个参议员的权利。
因此在第二轮只剩下第三个参议员拥有投票的权利,于是他可以宣布胜利
```

提示:
* $n = s.length$
* $1 <= n <= 10^4$
* `senate[i]` 为 `'R'` 或 `'D'`

Solution:
### 基本分析

整理题意:每次选择对方的一个成员进行消除,直到所有剩余成员均为我方时,宣布胜利。

由于每个对方成员执行权利都意味着我方损失一名成员,因此最佳方式是 **尽量让对方的权利不执行,或延迟执行(意味着我方有更多执行权利的机会)**。因此,最优决策为 **每次都选择对方的下一个行权成员进行消除**。

---

### 贪心

这个找“最近一个”对方行权成员的操作既可以使用「有序集合」来做,也可以使用「循环队列」来做。

先说使用「有序集合」的做法。

起始我们先将 `s` 中的 `Radiant` 和 `Dire` 分别存入有序集合 `rs` 和 `ds` 当中,然后从前往后模拟消除过程,过程中使用 `idx` 代表当前处理到的成员下标,使用 `set` 记录当前哪些成员已被消除。

当轮到 `s[idx]` 行权时(若 `s[idx]` 已被消除,则跳过本轮),从对方的有序队列中取出 **首个大于等于 `idx` 的下标**(取出代表移除);若对方的有序序列不存在该下标,而行权过程又是循环进行的,说明此时下一个行权的对方成员是 **首个大于等于 $0$** 的下标,我们对其进行取出消除,随后往后继续决策。

Java 代码:
```Java
class Solution {
    public String predictPartyVictory(String s) {
        TreeSet<Integer> rs = new TreeSet<>(), ds = new TreeSet<>();
        int n = s.length();
        for (int i = 0; i < n; i++) {
            if (s.charAt(i) == 'R') rs.add(i);
            else ds.add(i);
        }
        Set<Integer> set = new HashSet<>();
        int idx = 0;
        while (rs.size() != 0 && ds.size() != 0) {
            if (!set.contains(idx)) {
                TreeSet<Integer> temp = null;
                if (s.charAt(idx) == 'R') temp = ds;
                else temp = rs;
                Integer t = temp.ceiling(idx);
                if (t == null) t = temp.ceiling(0);
                set.add(t);
                temp.remove(t);
            }
            idx = (idx + 1) % n;
        }
        return rs.size() != 0 ? "Radiant" : "Dire";
    }
}
```
不难发现,虽然将所有成员存入有序集合和取出的复杂度均为 $O(n\log{n})$,但整体复杂度仍是大于 $O(n\log{n})$。

因为我们在每一轮的消除中,从 `idx` 位置找到下一个决策者,总是需要遍历那些已被消除的位置,而该无效遍历操作可使用「循环队列」优化。

---

### 优化

使用循环队列 `rd` 和 `dd` 来取代有序集合 `rs` 和 `ds`。起始将各个成员分类依次入队,每次从两队列队头中取出成员,假设从 `rs` 中取出成员下标为 `a`,从 `dd` 中取出成员下标为 `b`,对两者进行比较:

* 若有 `a < b`,说明 `a` 先行权,且其消除对象为 `b`,`a` 行权后需等到下一轮,对其进行大小为 $n$ 的偏移后重新添加到 `rd` 尾部(含义为本次行权后需要等到下一轮);
* 若有 `b < a`,同理,将 `a` 消除后,对 `b` 进行大小为 $n$ 的偏移后重新添加到 `dd` 尾部。

Java 代码:
```Java
class Solution {
    public String predictPartyVictory(String s) {
        Deque<Integer> rd = new ArrayDeque<>(), dd = new ArrayDeque<>();
        int n = s.length();
        for (int i = 0; i < n; i++) {
            if (s.charAt(i) == 'R') rd.addLast(i);
            else dd.addLast(i);
        }
        while (rd.size() != 0 && dd.size() != 0) {
            int a = rd.pollFirst(), b = dd.pollFirst();
            if (a < b) rd.addLast(a + n);
            else dd.addLast(b + n);
        }
        return rd.size() != 0 ? "Radiant" : "Dire";
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    string predictPartyVictory(string s) {
        deque<int> rd, dd;
        int n = s.length();
        for (int i = 0; i < n; i++) {
            if (s[i] == 'R') rd.push_back(i);
            else dd.push_back(i);
        }
        while (!rd.empty() && !dd.empty()) {
            int a = rd.front(), b = dd.front();
            rd.pop_front();
            dd.pop_front();
            if (a < b) rd.push_back(a + n);
            else dd.push_back(b + n);
        }
        return !rd.empty() ? "Radiant" : "Dire";
    }
};
```
Python 代码:
```Python
from collections import deque

class Solution:
    def predictPartyVictory(self, s: str) -> str:
        rd, dd = deque(), deque()
        n = len(s)
        for i in range(n):
            if s[i] == 'R':
                rd.append(i)
            else:
                dd.append(i)
        while rd and dd:
            a, b = rd.popleft(), dd.popleft()
            if a < b:
                rd.append(a + n)
            else:
                dd.append(b + n)
        return "Radiant" if rd else "Dire"
```
TypeScript 代码:
```TypeScript
function predictPartyVictory(s: string): string {
    let rd: Array<number> = [], dd: Array<number> = [];
    let n: number = s.length;
    for (let i = 0; i < n; i++) {
        if (s.charAt(i) == 'R') rd.push(i);
        else dd.push(i);
    }
    while (rd.length != 0 && dd.length != 0) {
        let a: number = rd.shift()!, b: number = dd.shift()!;
        if (a < b) rd.push(a + n);
        else dd.push(b + n);
    }
    return rd.length != 0 ? "Radiant" : "Dire";
};
```
* 时间复杂度:将所有成员进行分队,复杂度为 $O(n)$;每个回合会有一名成员被消除,最多有 $n$ 个成员,复杂度为 $O(n)$。整体复杂度为 $O(n)$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


650. 只有两个键的键盘

Question:
最初记事本上只有一个字符 `'A'` 。你每次可以对这个记事本进行两种操作:

* `Copy All(复制全部)`:复制这个记事本中的所有字符(不允许仅复制部分字符)。
* `Paste(粘贴)`:粘贴 上一次 复制的字符。

给你一个数字 `n` ,你需要使用最少的操作次数,在记事本上输出 恰好 `n` 个 `'A'` 。返回能够打印出 `n` 个 `'A'` 的最少操作次数。

示例 1:
```
输入:3

输出:3

解释:
最初, 只有一个字符 'A'。
第 1 步, 使用 Copy All 操作。
第 2 步, 使用 Paste 操作来获得 'AA'。
第 3 步, 使用 Paste 操作来获得 'AAA'。
```
示例 2:
```
输入:n = 1

输出:0
```

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

Solution:
### 动态规划

**定义 $f[i][j]$ 为经过最后一次操作后,当前记事本上有 $i$ 个字符,粘贴板上有 $j$ 个字符的最小操作次数。**

由于我们粘贴板的字符必然是经过 `Copy All` 操作而来,因此对于一个合法的 $f[i][j]$ 而言,必然有 $j <= i$。

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

* 最后一次操作是 `Paste` 操作:此时粘贴板的字符数量不会发生变化,即有 $f[i][j] = f[i - j][j] + 1$;

* 最后一次操作是 `Copy All` 操作:那么此时的粘贴板的字符数与记事本上的字符数相等(满足 $i = j$),此时的 $f[i][j] = \min(f[i][x] + 1), 0 \leq x < i$。

我们发现最后一个合法的 $f[i][j]$(满足 $i = j$)依赖与前面 $f[i][j]$(满足 $j < i$)。

因此实现上,我们可以使用一个变量 $min$ 保存前面转移的最小值,用来更新最后的 $f[i][j]$。

再进一步,我们发现如果 $f[i][j]$ 的最后一次操作是由 `Paste` 而来,原来粘贴板的字符数不会超过 $i / 2$,因此在转移 $f[i][j]$(满足 $j < i$)时,其实只需要枚举 $[0, i/2]$ 即可。

代码:
```Java
class Solution {
    int INF = 0x3f3f3f3f;
    public int minSteps(int n) {
        int[][] f = new int[n + 1][n + 1];
        for (int i = 0; i <= n; i++) {
            for (int j = 0; j <= n; j++) {
                f[i][j] = INF;
            }
        }
        f[1][0] = 0; f[1][1] = 1;
        for (int i = 2; i <= n; i++) {
            int min = INF;
            for (int j = 0; j <= i / 2; j++) {
                f[i][j] = f[i - j][j] + 1;
                min = Math.min(min, f[i][j]);
            }
            f[i][i] = min + 1;
        }
        int ans = INF;
        for (int i = 0; i <= n; i++) ans = Math.min(ans, f[n][i]);
        return ans;
    }
}
```
* 时间复杂度:$O(n^2)$
* 空间复杂度:$O(n^2)$

---

### 数学

如果我们将「$1$ 次 `Copy All` + $x$ 次 `Paste`」看做一次“动作”的话。

那么 **一次“动作”所产生的效果就是将原来的字符串变为原来的 $x + 1$ 倍**。

最终的最小操作次数方案可以等价以下操作流程:

1. 起始对长度为 $1$ 的记事本字符进行 $1$ 次 `Copy All` + $k_1 - 1$ 次 `Paste` 操作(消耗次数为 $k_1$,得到长度为 $k_1$ 的记事本长度);
2. 对长度为为 $k_1$ 的记事本字符进行 $1$ 次 `Copy All` + $k_2 - 1$ 次 `Paste` 操作(消耗次数为 $k_1 + k_2$,得到长度为 $k_1 * k_2$ 的记事本长度)
...

最终经过 $k$ 次“动作”之后,得到长度为 $n$ 的记事本长度,即有:

$$
n = k_1 * k_2 * ... * k_x
$$

问题转化为:如何对 $n$ 进行拆分,可以使得 $k_1 + k_2 + ... + k_x$ 最小。

对于任意一个 $k_i$(合数)而言,根据定理 $a * b >= a + b$ 可知进一步的拆分必然不会导致结果变差。

因此,我们只需要使用「试除法」对 $n$ 执行分解质因数操作,累加所有的操作次数,即可得到答案。

代码:
```Java
class Solution {
    public int minSteps(int n) {
        int ans = 0;
        for (int i = 2; i * i <= n; i++) {
            while (n % i == 0) {
                ans += i;
                n /= i;
            }
        }
        if (n != 1) ans += n;
        return ans;
    }
}
```
* 时间复杂度:$O(\sqrt{n})$
* 空间复杂度:$O(1)$

---

### 打表

我们发现,对于某个 $minSteps(i)$ 而言为定值,且数据范围只有 $1000$,因此考虑使用打表来做。

代码:
```Java
class Solution {
    static int N = 1010;
    static int[] g = new int[N];
    static {
        for (int k = 2; k < N; k++) {
            int cnt = 0, n = k;
            for (int i = 2; i * i <= n; i++) {
                while (n % i == 0) {
                    cnt += i;
                    n /= i;
                }
            }
            if (n != 1) cnt += n;
            g[k] = cnt;
        }
        // System.out.println(Arrays.toString(g)); // 输出打表结果
    }
    public int minSteps(int n) {
        return g[n];
    }
}
```

```Java
class Solution {
    static int[] g = new int[]{0, 0, 2, 3, 4, 5, 5, 7, 6, 6, 7, 11, 7, 13, 9, 8, 8, 17, 8, 19, 9, 10, 13, 23, 9, 10, 15, 9, 11, 29, 10, 31, 10, 14, 19, 12, 10, 37, 21, 16, 11, 41, 12, 43, 15, 11, 25, 47, 11, 14, 12, 20, 17, 53, 11, 16, 13, 22, 31, 59, 12, 61, 33, 13, 12, 18, 16, 67, 21, 26, 14, 71, 12, 73, 39, 13, 23, 18, 18, 79, 13, 12, 43, 83, 14, 22, 45, 32, 17, 89, 13, 20, 27, 34, 49, 24, 13, 97, 16, 17, 14, 101, 22, 103, 19, 15, 55, 107, 13, 109, 18, 40, 15, 113, 24, 28, 33, 19, 61, 24, 14, 22, 63, 44, 35, 15, 15, 127, 14, 46, 20, 131, 18, 26, 69, 14, 23, 137, 28, 139, 16, 50, 73, 24, 14, 34, 75, 17, 41, 149, 15, 151, 25, 23, 20, 36, 20, 157, 81, 56, 15, 30, 14, 163, 45, 19, 85, 167, 16, 26, 24, 25, 47, 173, 34, 17, 19, 62, 91, 179, 15, 181, 22, 64, 29, 42, 36, 28, 51, 16, 26, 191, 15, 193, 99, 21, 18, 197, 19, 199, 16, 70, 103, 36, 24, 46, 105, 29, 21, 30, 17, 211, 57, 74, 109, 48, 15, 38, 111, 76, 20, 30, 42, 223, 17, 16, 115, 227, 26, 229, 30, 21, 35, 233, 21, 52, 63, 82, 26, 239, 16, 241, 24, 15, 65, 19, 46, 32, 37, 86, 17, 251, 17, 34, 129, 25, 16, 257, 48, 44, 22, 35, 133, 263, 20, 58, 28, 92, 71, 269, 16, 271, 25, 23, 139, 21, 30, 277, 141, 37, 18, 281, 52, 283, 75, 27, 26, 48, 16, 34, 36, 100, 77, 293, 19, 64, 43, 20, 151, 36, 17, 50, 153, 104, 27, 66, 25, 307, 22, 106, 38, 311, 22, 313, 159, 18, 83, 317, 58, 40, 17, 110, 32, 36, 16, 23, 165, 112, 47, 54, 21, 331, 87, 43, 169, 72, 18, 337, 28, 116, 26, 42, 27, 21, 49, 31, 175, 347, 36, 349, 19, 22, 21, 353, 64, 76, 93, 27, 181, 359, 17, 38, 183, 25, 24, 78, 66, 367, 31, 47, 44, 60, 38, 373, 30, 18, 53, 42, 18, 379, 28, 130, 193, 383, 17, 23, 195, 49, 101, 389, 23, 40, 20, 134, 199, 84, 21, 397, 201, 29, 18, 401, 72, 44, 105, 17, 38, 48, 26, 409, 48, 140, 107, 66, 31, 88, 23, 142, 32, 419, 19, 421, 213, 53, 59, 27, 76, 68, 111, 27, 50, 431, 17, 433, 40, 37, 113, 42, 78, 439, 22, 20, 32, 443, 44, 94, 225, 152, 19, 449, 18, 52, 117, 154, 229, 25, 28, 457, 231, 26, 32, 461, 23, 463, 37, 39, 235, 467, 23, 74, 54, 160, 65, 54, 84, 29, 28, 59, 241, 479, 18, 50, 243, 33, 26, 102, 17, 487, 67, 166, 21, 491, 48, 46, 34, 22, 39, 78, 88, 499, 19, 170, 253, 503, 19, 106, 36, 29, 131, 509, 27, 80, 18, 28, 259, 108, 50, 58, 46, 176, 24, 521, 37, 523, 135, 20, 265, 48, 22, 46, 60, 65, 30, 54, 94, 112, 73, 182, 271, 25, 18, 541, 273, 184, 27, 114, 25, 547, 141, 67, 23, 48, 32, 86, 279, 45, 143, 557, 39, 56, 20, 31, 283, 563, 54, 118, 285, 19, 77, 569, 29, 571, 28, 194, 50, 33, 18, 577, 36, 196, 38, 90, 102, 64, 79, 24, 295, 587, 21, 50, 66, 200, 45, 593, 22, 29, 153, 202, 38, 599, 19, 601, 52, 73, 155, 27, 106, 607, 29, 39, 68, 60, 27, 613, 309, 49, 24, 617, 108, 619, 40, 32, 313, 96, 24, 20, 315, 33, 161, 54, 20, 631, 85, 214, 319, 132, 60, 27, 42, 77, 19, 641, 112, 643, 34, 51, 38, 647, 18, 70, 25, 41, 167, 653, 114, 136, 49, 79, 56, 659, 23, 661, 333, 33, 89, 31, 45, 52, 171, 226, 74, 72, 20, 673, 339, 19, 30, 677, 118, 104, 28, 230, 44, 683, 29, 142, 23, 232, 51, 66, 33, 691, 177, 24, 349, 144, 38, 58, 351, 236, 21, 701, 24, 56, 23, 55, 355, 108, 66, 709, 78, 85, 95, 54, 29, 29, 183, 242, 361, 719, 19, 110, 40, 244, 185, 39, 27, 727, 26, 18, 80, 60, 68, 733, 369, 22, 33, 78, 49, 739, 46, 35, 62, 743, 40, 154, 375, 89, 32, 114, 20, 751, 55, 254, 44, 156, 20, 757, 381, 37, 30, 761, 132, 116, 195, 28, 385, 72, 19, 769, 25, 260, 197, 773, 51, 41, 103, 47, 391, 60, 25, 82, 42, 38, 22, 162, 136, 787, 201, 266, 86, 120, 23, 74, 399, 61, 203, 797, 31, 64, 20, 95, 403, 84, 74, 35, 46, 272, 107, 809, 19, 811, 40, 274, 50, 168, 28, 62, 411, 26, 50, 821, 142, 823, 109, 24, 68, 827, 33, 829, 90, 280, 25, 31, 144, 172, 34, 40, 421, 839, 21, 58, 423, 284, 215, 31, 55, 29, 61, 286, 29, 60, 78, 853, 70, 30, 113, 857, 29, 859, 52, 51, 433, 863, 19, 178, 435, 37, 42, 90, 39, 80, 115, 103, 44, 22, 80, 877, 441, 296, 24, 881, 22, 883, 34, 67, 445, 887, 46, 134, 96, 23, 227, 66, 154, 184, 21, 39, 451, 60, 20, 70, 54, 53, 119, 186, 156, 907, 231, 107, 27, 911, 30, 94, 459, 69, 233, 138, 28, 919, 34, 310, 463, 84, 25, 47, 465, 109, 39, 929, 41, 33, 237, 314, 469, 33, 25, 937, 76, 316, 56, 941, 162, 64, 67, 21, 56, 947, 86, 86, 31, 320, 30, 953, 61, 196, 243, 43, 481, 144, 20, 62, 52, 113, 245, 198, 35, 967, 28, 39, 104, 971, 19, 146, 489, 26, 69, 977, 168, 100, 23, 115, 493, 983, 50, 202, 48, 57, 36, 66, 24, 991, 41, 334, 80, 204, 90, 997, 501, 46, 21, 31, 172, 76, 255, 75, 505, 72, 21, 1009};
    public int minSteps(int n) {
        return g[n];
    }
}
```
* 时间复杂度:将打表逻辑配合 `static` 交给 OJ 执行,复杂度为 $O(C * \sqrt{C})$,$C$ 为常数,固定为 $1010$;将打表逻辑放到本地执行,复杂度为 $O(1)$
* 空间复杂度:$O(C)$

### 最后

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

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

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

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


652. 寻找重复的子树

Question:
给定一棵二叉树 `root`,返回所有重复的子树。

对于同一类的重复子树,你只需要返回其中任意一棵的根结点即可。

如果两棵树具有相同的结构和相同的结点值,则它们是重复的。

示例 1:

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

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

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

输出:[[1]]
```
示例 3:

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

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

提示:
* 树中的结点数在 $[1,10^4]$ 范围内。
* $-200 <= Node.val <= 200$

Solution:
### DFS + 哈希表

设计递归函数 `String dfs(TreeNode root)`,含义为返回以传入参数 `root` 为根节点的子树所对应的指纹标识。

对于标识的设计只需使用 `"_"` 分割不同的节点值,同时对空节点进行保留(定义为空串 `" "`)即可。

使用哈希表记录每个标识(子树)出现次数,当出现次数为 $2$(首次判定为重复出现)时,将该节点加入答案。

Java 代码:
```Java
class Solution {
    Map<String, Integer> map = new HashMap<>();
    List<TreeNode> ans = new ArrayList<>();
    public List<TreeNode> findDuplicateSubtrees(TreeNode root) {
        dfs(root);
        return ans;
    }
    String dfs(TreeNode root) {
        if (root == null) return " ";
        StringBuilder sb = new StringBuilder();
        sb.append(root.val).append("_");
        sb.append(dfs(root.left)).append(dfs(root.right));
        String key = sb.toString();
        map.put(key, map.getOrDefault(key, 0) + 1);
        if (map.get(key) == 2) ans.add(root);
        return key;
    }
}
```
Typescript 代码:
```Typescript
let ans: Array<TreeNode>
let map: Map<string, number>
function findDuplicateSubtrees(root: TreeNode | null): Array<TreeNode | null> {
    ans = new Array<TreeNode>()
    map = new Map<string, number>()
    dfs(root)
    return ans
};
function dfs(root: TreeNode | null): string {
    if (root == null) return " "
    let key = root.val + "_"
    key += dfs(root.left)
    key += dfs(root.right)
    if (!map.has(key)) map.set(key, 1)
    else map.set(key, map.get(key) + 1)
    if (map.get(key) == 2) ans.push(root)
    return key
}
```
* 时间复杂度:`DFS` 过程复杂度为 $O(n)$,对于每个子树需要构造出与子树同等规模的字符串,复杂度为 $O(n)$。整体复杂度为 $O(n^2)$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


LeetCode/651-660/653. 两数之和 IV - 输入 BST（简单）.md

650. 只有两个键的键盘

Question:
给定一个二叉搜索树 `root` 和一个目标结果 `k`,如果 `BST` 中存在两个元素且它们的和等于给定的目标结果,则返回 `true`。

示例 1:

```
输入: root = [5,3,6,2,4,null,7], k = 9

输出: true
```
示例 2:

```
输入: root = [5,3,6,2,4,null,7], k = 28

输出: false
```

提示:
* 二叉树的节点个数的范围是  $[1, 10^4]$
* $-10^4 <= Node.val <= 10^4$
* `root` 为二叉搜索树
* $-10^5 <= k <= 10^5$

Solution:
### 哈希表 + 树的搜索

在递归搜索过程中记录下相应的节点值(使用 `Set` 集合),如果在遍历某个节点 $x$ 时发现集合中存在 $k - x.val$,说明存在两个节点之和等于 $k$,返回 `True`,若搜索完整棵树都没有则返回 `False`。

代码:
```Java
class Solution {
    Set<Integer> set = new HashSet<>();
    public boolean findTarget(TreeNode root, int k) {
        if (root == null) return false;
        if (set.contains(k - root.val)) return true;
        set.add(root.val);
        return findTarget(root.left, k) | findTarget(root.right, k);
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n)$

---

### 双指针 + BST 中序遍历

解法一中没有利用 `BST` 特性,利用 `BST` 中序遍历有序的特性,我们可以实现类似「双指针」的效果。

起始先让 BST 的最左链和最右链完全入栈,此时栈顶元素为 BST 中的最小值和最大值,分别使用 `l` 和 `r` 充当指针代指,根据两指针指向的节点值之和与 $k$ 的大小关系来指导如何让 `l` 和 `r` 移动,`l` 的移动过程其实就是找下一个比 `l.val` 更大的值,而 `r` 的移动过程其实就是找下一个比 `r.val` 更小的值。

代码:
```Java
class Solution {
    public boolean findTarget(TreeNode root, int k) {
        Deque<TreeNode> ld = new ArrayDeque<>(), rd = new ArrayDeque<>();
        TreeNode temp = root;
        while (temp != null) {
            ld.addLast(temp);
            temp = temp.left;
        }
        temp = root;
        while (temp != null) {
            rd.addLast(temp);
            temp = temp.right;
        }
        TreeNode l = ld.peekLast(), r = rd.peekLast();
        while (l.val < r.val) {
            int t = l.val + r.val;
            if (t == k) return true;
            else if (t < k) l = getNext(ld, true);
            else r = getNext(rd, false);
        }
        return false;
    }
    TreeNode getNext(Deque<TreeNode> d, boolean isLeft) {
        TreeNode cur = d.pollLast();
        TreeNode node = isLeft ? cur.right : cur.left;
        while (node != null) {
            d.addLast(node);
            node = isLeft ? node.left : node.right;
        }
        return d.peekLast();
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


Question:
给定一个不重复的整数数组 `nums`。

最大二叉树 可以用下面的算法从 `nums` 递归地构建:

1. 创建一个根节点,其值为 `nums` 中的最大值。
2. 递归地在最大值 左边 的 子数组前缀上 构建左子树。
3. 递归地在最大值 右边 的 子数组后缀上 构建右子树。

返回 `nums` 构建的最大二叉树。

示例 1:

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

输出:[6,3,5,null,2,0,null,null,1]

解释:递归调用如下所示:
- [3,2,1,6,0,5] 中的最大值是 6 ,左边部分是 [3,2,1] ,右边部分是 [0,5] 。
    - [3,2,1] 中的最大值是 3 ,左边部分是 [] ,右边部分是 [2,1] 。
        - 空数组,无子节点。
        - [2,1] 中的最大值是 2 ,左边部分是 [] ,右边部分是 [1] 。
            - 空数组,无子节点。
            - 只有一个元素,所以子节点是一个值为 1 的节点。
    - [0,5] 中的最大值是 5 ,左边部分是 [0] ,右边部分是 [] 。
        - 只有一个元素,所以子节点是一个值为 0 的节点。
        - 空数组,无子节点。
```
示例 2:

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

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

提示:
* $1 <= nums.length <= 1000$
* $0 <= nums[i] <= 1000$
* `nums` 中的所有整数 互不相同

Solution:
### 基本分析

根据题目描述,可知该问题本质是「区间求最值」问题(RMQ)。

而求解 RMQ 有多种方式:递归分治、有序集合/ST/线段树 和 单调栈。

其中递归分治做法复杂度为 $O(n^2)$,对本题来说可过;而其余诸如线段树的方式需要 $O(n\log{n})$ 的建树和单次 $O(\log{n})$ 的查询,整体复杂度为 $O(n\log{n})$;单调栈解法则是整体复杂度为 $O(n)$。

---

### 递归分治

设置递归函数 `TreeNode build(int[] nums, int l, int r)` 含义为从 `nums` 中的 $[l, r]$ 下标范围进行构建,返回构建后的头结点。

当 $l > r$ 时,返回空节点,否则在 $[l, r]$ 中进行扫描,找到最大值对应的下标 `idx` 并创建对应的头结点,递归构建 $[l, idx - 1]$ 和 $[idx + 1, r]$ 作为头节点的左右子树。

Java 代码:
```Java
class Solution {
    public TreeNode constructMaximumBinaryTree(int[] nums) {
        return build(nums, 0, nums.length - 1);
    }
    TreeNode build(int[] nums, int l, int r) {
        if (l > r) return null;
        int idx = l;
        for (int i = l; i <= r; i++) {
            if (nums[i] > nums[idx]) idx = i;
        }
        TreeNode ans = new TreeNode(nums[idx]);
        ans.left = build(nums, l, idx - 1);
        ans.right = build(nums, idx + 1, r);
        return ans;
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    TreeNode* constructMaximumBinaryTree(vector<int>& nums) {
        return build(nums, 0, nums.size() - 1);
    }
    TreeNode* build(vector<int>& nums, int l, int r) {
        if (l > r) return nullptr;
        int idx = l;
        for (int i = l; i <= r; ++i) {
            if (nums[i] > nums[idx]) idx = i;
        }
        TreeNode* ans = new TreeNode(nums[idx]);
        ans->left = build(nums, l, idx - 1);
        ans->right = build(nums, idx + 1, r);
        return ans;
    }
};
```
Python 代码:
```Python
class Solution:
    def constructMaximumBinaryTree(self, nums: List[int]) -> Optional[TreeNode]:
        return self.build(nums, 0, len(nums) - 1)

    def build(self, nums, l, r):
        if l > r:
            return None
        idx = l
        for i in range(l, r + 1):
            if nums[i] > nums[idx]:
                idx = i
        node = TreeNode(nums[idx])
        node.left = self.build(nums, l, idx - 1)
        node.right = self.build(nums, idx + 1, r)
        return node
```
TypeScript 代码:
```TypeScript
function constructMaximumBinaryTree(nums: number[]): TreeNode | null {
    return build(nums, 0, nums.length - 1)
};
function build(nums: number[], l: number, r: number): TreeNode | null {
    if (l > r) return null
    let idx = l
    for (let i = l; i <= r; i++) {
        if (nums[i] > nums[idx]) idx = i
    }
    const ans = new TreeNode(nums[idx])
    ans.left = build(nums, l, idx - 1)
    ans.right = build(nums, idx + 1, r)
    return ans
}
```
* 时间复杂度:$O(n^2)$
* 空间复杂度:忽略递归带来的额外空间开销,复杂度为 $O(1)$

---

### 线段树

抽象成区间求和问题后,涉及「单点修改」和「区间查询」,再结合节点数量为 $1e3$,可使用 `build` $4n$ 空间不带懒标记的线段树进行求解。

设计线段树节点 `Node` 包含属性:左节点下标 `l`、右节点下标 `r` 和当前区间 $[l, r]$ 所对应的最值 $val$。

构建线段树的过程为基本的线段树模板内容,而构建答案树的过程与递归分治过程类型(将线性找最值过程用线段树优化)。

Java 代码:
```Java
class Solution {
    class Node {
        int l, r, val;
        Node (int _l, int _r) {
            l = _l; r = _r;
        }
    }
    void build(int u, int l, int r) {
        tr[u] = new Node(l, r);
        if (l == r) return ;
        int mid = l + r >> 1;
        build(u << 1, l, mid);
        build(u << 1 | 1, mid + 1, r);
    }
    void update(int u, int x, int v) {
        if (tr[u].l == x && tr[u].r == x) {
            tr[u].val = Math.max(tr[u].val, v);
            return ;
        }
        int mid = tr[u].l + tr[u].r >> 1;
        if (x <= mid) update(u << 1, x, v);
        else update(u << 1 | 1, x, v);
        pushup(u);
    }
    int query(int u, int l, int r) {
        if (l <= tr[u].l && tr[u].r <= r) return tr[u].val;
        int mid = tr[u].l + tr[u].r >> 1, ans = 0;
        if (l <= mid) ans = query(u << 1, l, r);
        if (r > mid) ans = Math.max(ans, query(u << 1 | 1, l, r));
        return ans;
    }
    void pushup(int u) {
        tr[u].val = Math.max(tr[u << 1].val, tr[u << 1 | 1].val);
    }
    Node[] tr = new Node[4010];
    int[] hash = new int[1010];
    public TreeNode constructMaximumBinaryTree(int[] nums) {
        int n = nums.length;
        build(1, 1, n);
        for (int i = 0; i < n; i++) {
            hash[nums[i]] = i + 1;
            update(1, i + 1, nums[i]);
        }
        return dfs(nums, 1, n);
    }
    TreeNode dfs(int[] nums, int l, int r) {
        if (l > r) return null;
        int val = query(1, l, r), idx = hash[val];
        TreeNode ans = new TreeNode(val);
        ans.left = dfs(nums, l, idx - 1);
        ans.right = dfs(nums, idx + 1, r);
        return ans;
    }
}
```
C++ 代码:
```C++
struct Node {
    int l, r, val;
    Node(int _l, int _r) : l(_l), r(_r), val(0) {}
    Node() : l(0), r(0), val(0) {}
};
class Solution {
public:
    vector<Node> tr;
    unordered_map<int, int> hash;
    TreeNode* constructMaximumBinaryTree(vector<int>& nums) {
        int n = nums.size();
        tr.resize(4010);
        build(1, 1, n);
        for (int i = 0; i < n; ++i) {
            hash[nums[i]] = i + 1;
            update(1, i + 1, nums[i]);
        }
        return dfs(nums, 1, n);
    }
    void build(int u, int l, int r) {
        tr[u] = Node(l, r);
        if (l == r) return;
        int mid = (l + r) >> 1;
        build((u << 1), l, mid);
        build((u << 1) + 1, mid + 1, r);
    }
    void update(int u, int x, int v) {
        if (tr[u].l == x && tr[u].r == x) {
            tr[u].val = max(tr[u].val, v);
            return;
        }
        int mid = (tr[u].l + tr[u].r) >> 1;
        if (x <= mid) update((u << 1), x, v);
        else update((u << 1) + 1, x, v);
        pushup(u);
    }
    int query(int u, int l, int r) {
        if (l <= tr[u].l && tr[u].r <= r) return tr[u].val;
        int mid = (tr[u].l + tr[u].r) >> 1, ans = 0;
        if (l <= mid) ans = max(ans, query((u << 1), l, r));
        if (r > mid) ans = max(ans, query((u << 1) + 1, l, r));
        return ans;
    }
    void pushup(int u) {
        tr[u].val = max(tr[(u << 1)].val, tr[(u << 1) + 1].val);
    }
    TreeNode* dfs(vector<int>& nums, int l, int r) {
        if (l > r) return NULL;
        int val = query(1, l, r), idx = hash[val];
        TreeNode* ans = new TreeNode(val);
        ans -> left = dfs(nums, l, idx - 1);
        ans -> right = dfs(nums, idx + 1, r);
        return ans;
    }
};
```
Python 代码:
```Python
class Node:
    def __init__(self, l=0, r=0, val=0):
        self.l = l
        self.r = r
        self.val = val

class Solution:
    def __init__(self):
        self.tr = [Node() for _ in range(4010)]
        self.hash = {}

    def constructMaximumBinaryTree(self, nums):
        n = len(nums)
        self.build(1, 1, n)
        for i, num in enumerate(nums):
            self.hash[num] = i + 1
            self.update(1, i + 1, num)
        return self.dfs(nums, 1, n)

    def build(self, u, l, r):
        self.tr[u] = Node(l, r)
        if l == r: return
        mid = (l + r) >> 1
        self.build(u << 1, l, mid)
        self.build(u << 1 | 1, mid + 1, r)

    def update(self, u, x, v):
        if self.tr[u].l == x and self.tr[u].r == x:
            self.tr[u].val = max(self.tr[u].val, v)
            return
        mid = (self.tr[u].l + self.tr[u].r) >> 1
        if x <= mid: self.update(u << 1, x, v)
        else: self.update(u << 1 | 1, x, v)
        self.pushup(u)

    def query(self, u, l, r):
        if l <= self.tr[u].l and self.tr[u].r <= r: return self.tr[u].val
        mid = (self.tr[u].l + self.tr[u].r) >> 1
        ans = 0
        if l <= mid: ans = max(ans, self.query(u << 1, l, r))
        if r > mid: ans = max(ans, self.query(u << 1 | 1, l, r))
        return ans

    def pushup(self, u):
        self.tr[u].val = max(self.tr[u << 1].val, self.tr[u << 1 | 1].val)

    def dfs(self, nums, l, r):
        if l > r: return None
        val = self.query(1, l, r)
        idx = self.hash[val]
        node = TreeNode(val)
        node.left = self.dfs(nums, l, idx - 1)
        node.right = self.dfs(nums, idx + 1, r)
        return node
```
TypeScript 代码:
```TypeScript
class TNode {
    l = 0; r = 0; val = 0;
    constructor (_l: number, _r: number) {
        this.l = _l; this.r = _r;
    }
}
const tr: TNode[] = new Array<TNode>(4010)
const hash: number[] = new Array<number>(1010)
function constructMaximumBinaryTree(nums: number[]): TreeNode | null {
    const n = nums.length
    build(1, 1, n)
    for (let i = 0; i < n; i++) {
        hash[nums[i]] = i + 1
        update(1, i + 1, nums[i])
    }
    return dfs(nums, 1, n)
};
function build(u: number, l: number, r: number): void {
    tr[u] = new TNode(l, r)
    if (l == r) return
    const mid = l + r >> 1
    build(u << 1, l, mid)
    build(u << 1 | 1, mid + 1, r)
}
function update(u: number, x: number, v: number): void {
    if (tr[u].l == x && tr[u].r == x) {
        tr[u].val = Math.max(tr[u].val, v)
        return
    }
    const mid = tr[u].l + tr[u].r >> 1
    if (x <= mid) update(u << 1, x, v)
    else update(u << 1 | 1, x, v)
    pushup(u)
}
function query(u: number, l: number, r: number): number {
    if (l <= tr[u].l && tr[u].r <= r) return tr[u].val
    let mid = tr[u].l + tr[u].r >> 1, ans = 0
    if (l <= mid) ans = query(u << 1, l, r)
    if (r > mid) ans = Math.max(ans, query(u << 1 | 1, l, r))
    return ans
}
function pushup(u: number): void {
    tr[u].val = Math.max(tr[u << 1].val, tr[u << 1 | 1].val)
}
function dfs(nums: number[], l: number, r: number): TreeNode {
    if (l > r) return null
    let val = query(1, l, r), idx = hash[val]
    const ans = new TreeNode(val)
    ans.left = dfs(nums, l, idx - 1)
    ans.right = dfs(nums, idx + 1, r)
    return ans
}

```
* 时间复杂度:构建线段树复杂度为 $O(n\log{n})$;构造答案树复杂度为 $O(n\log{n})$。整体复杂度为 $O(n\log{n})$
* 空间复杂度:$O(n)$

---

### 单调栈

更进一步,根据题目对树的构建的描述可知,`nums` 中的任二节点所在构建树的水平截面上的位置仅由下标大小决定。

不难想到可抽象为找最近元素问题,可使用单调栈求解。

具体的,我们可以从前往后处理所有的 $nums[i]$,若存在栈顶元素并且栈顶元素的值比当前值要小,根据我们从前往后处理的逻辑,可确定栈顶元素可作为当前 $nums[i]$ 对应节点的左节点,同时为了确保最终 $nums[i]$ 的左节点为 $[0, i - 1]$ 范围的最大值,我们需要确保在构建 $nums[i]$ 节点与其左节点的关系时,$[0, i - 1]$ 中的最大值最后出队,此时可知容器栈具有「单调递减」特性。基于此,我们可以分析出,当处理完 $nums[i]$ 节点与其左节点关系后,可明确 $nums[i]$ 可作为未出栈的栈顶元素的右节点。

> 一些细节:`Java` 容易使用 `ArrayDeque` 充当容器,但为与 `TS` 保存一致,两者均使用数组充当容器。

Java 代码:
```Java
class Solution {
    static TreeNode[] stk = new TreeNode[1010];
    public TreeNode constructMaximumBinaryTree(int[] nums) {
        int he = 0, ta = 0;
        for (int x : nums) {
            TreeNode node = new TreeNode(x);
            while (he < ta && stk[ta - 1].val < x) node.left = stk[--ta];
            if (he < ta) stk[ta - 1].right = node;
            stk[ta++] = node;
        }
        return stk[0];
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    TreeNode* stk[1010];
    TreeNode* constructMaximumBinaryTree(vector<int>& nums) {
        int he = 0, ta = 0;
        for (int x : nums) {
            TreeNode* node = new TreeNode(x);
            while (he < ta && stk[ta - 1]->val < x) node->left = stk[--ta];
            if (he < ta) stk[ta - 1]->right = node;
            stk[ta++] = node;
        }
        return stk[0];
    }
};
```
Python 代码:
```Python
class Solution:
    def constructMaximumBinaryTree(self, nums: List[int]) -> Optional[TreeNode]:
        stk = [None] * 1010
        he, ta = 0, 0
        for x in nums:
            node = TreeNode(x)
            while he < ta and stk[ta - 1].val < x:
                node.left = stk[ta - 1]
                ta -= 1
            if he < ta:
                stk[ta - 1].right = node
            stk[ta] = node
            ta += 1
        return stk[0]
```
TypeScript 代码:
```TypeScript
const stk = new Array<TreeNode>(1010)
function constructMaximumBinaryTree(nums: number[]): TreeNode | null {
    let he = 0, ta = 0
    for (const x of nums) {
        const node = new TreeNode(x)
        while (he < ta && stk[ta - 1].val < x) node.left = stk[--ta]
        if (he < ta) stk[ta - 1].right = node
        stk[ta++] = node
    }
    return stk[0]
};
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


Question:
给你一棵二叉树的根节点 root ,请你构造一个下标从 0 开始、大小为 m x n 的字符串矩阵 res ,用以表示树的 格式化布局 。构造此格式化布局矩阵需要遵循以下规则:

* 树的 高度 为 $height$,矩阵的行数 $m$ 应该等于 $height + 1$。
* 矩阵的列数 $n$ 应该等于 $2^{height+1 - 1}$ 。
* 根节点 需要放置在 顶行 的 正中间 ,对应位置为 $res[0][(n-1)/2]$ 。
* 对于放置在矩阵中的每个节点,设对应位置为 $res[r][c]$ ,将其左子节点放置在 $res[r+1][c-2^{height-r-1}]$ ,右子节点放置在 $res[r+1][c+2^{height-r-1}]$ 。
* 继续这一过程,直到树中的所有节点都妥善放置。
* 任意空单元格都应该包含空字符串 `""` 。

返回构造得到的矩阵 `res` 。

示例 1:

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

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

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

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

提示:
* 树中节点数在范围 $[1, 2^{10}]$ 内
* $-99 <= Node.val <= 99$
* 树的深度在范围 $[1, 10]$ 内

Solution:
### DFS

根据题意,我们可以先设计 `dfs1` 递归函数得到树的高度 `h`,以及与其相关的矩阵行列大小(并初始化矩阵)。

随后根据填充规则,设计 `dfs2` 递归函数往矩阵进行填充。

Java 代码:
```Java
class Solution {
    int h, m, n;
    List<List<String>> ans;
    public List<List<String>> printTree(TreeNode root) {
        dfs1(root, 0);
        m = h + 1; n = (1 << (h + 1)) - 1;
        ans = new ArrayList<>();
        for (int i = 0; i < m; i++) {
            List<String> cur = new ArrayList<>();
            for (int j = 0; j < n; j++) cur.add("");
            ans.add(cur);
        }
        dfs2(root, 0, (n - 1) / 2);
        return ans;
    }
    void dfs1(TreeNode root, int depth) {
        if (root == null) return ;
        h = Math.max(h, depth);
        dfs1(root.left, depth + 1);
        dfs1(root.right, depth + 1);
    }
    void dfs2(TreeNode root, int x, int y) {
        if (root == null) return ;
        ans.get(x).set(y, String.valueOf(root.val));
        dfs2(root.left, x + 1, y - (1 << (h - x - 1)));
        dfs2(root.right, x + 1, y + (1 << (h - x - 1)));
    }
}
```
Typescript 代码:
```Typescript
let h: number, m: number, n: number;
let ans: string[][];
function printTree(root: TreeNode | null): string[][] {
    h = 0
    dfs1(root, 0)
    m = h + 1; n = (1 << (h + 1)) - 1
    ans = new Array<Array<string>>()
    for (let i = 0; i < m; i++) {
        ans[i] = new Array<string>(n).fill("")
    }
    dfs2(root, 0, (n - 1) / 2)
    return ans
};
function dfs1(root: TreeNode | null, depth: number): void {
    if (root == null) return
    h = Math.max(h, depth)
    dfs1(root.left, depth + 1)
    dfs1(root.right, depth + 1)
}
function dfs2(root: TreeNode | null, x: number, y: number): void {
    if (root == null) return
    ans[x][y] = root.val + "";
    dfs2(root.left, x + 1, y - (1 << (h - x - 1)))
    dfs2(root.right, x + 1, y + (1 << (h - x - 1)))
}
```
* 时间复杂度:$O(n \times m)$
* 空间复杂度:$O(n \times m)$

### 最后

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

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

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

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


LeetCode/651-660/658. 找到 K 个最接近的元素（中等）.md

658. 找到 K 个最接近的元素

Question:
给定一个 排序好 的数组 `arr`,两个整数 `k` 和 `x` ,从数组中找到最靠近 `x`(两数之差最小)的 `k` 个数。返回的结果必须要是按升序排好的。

整数 `a` 比整数 `b` 更接近 `x` 需要满足:

* `|a - x| < |b - x|` 或者
* `|a - x| == |b - x|` 且 `a < b`

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

输出:[1,2,3,4]
```
示例 2:
```
输入:arr = [1,2,3,4,5], k = 4, x = -1

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

提示:
* $1 <= k <= arr.length$
* $1 <= arr.length <= 10^4$
* `arr` 按升序排列
* $-10^4 <= arr[i], x <= 10^4$

Solution:
### 二分 + 双指针

容易想到先通过「二分」找到与 `x` 差值最小的位置 `idx`,然后从 `idx` 开始使用「双指针」往两边进行拓展(初始化左端点 $i = idx - 1$,右端点 $j = idx + 1$),含义为 $[i + 1, j - 1]$ 范围内子数组为候选区间,不断根据两边界与 `x` 的差值关系进行扩充,直到候选区间包含 $k$ 个数。

Java 代码:
```Java
class Solution {
    public List<Integer> findClosestElements(int[] arr, int k, int x) {
        int n = arr.length, l = 0, r = n - 1;
        while (l < r) {
            int mid = l + r + 1 >> 1;
            if (arr[mid] <= x) l = mid;
            else r = mid - 1;
        }
        r = r + 1 < n && Math.abs(arr[r + 1] - x) < Math.abs(arr[r] - x) ? r + 1 : r;
        int i = r - 1, j = r + 1;
        while (j - i - 1 < k) {
            if (i >= 0 && j < n) {
                if (Math.abs(arr[j] - x) < Math.abs(arr[i] - x)) j++;
                else i--;
            } else if (i >= 0) {
                i--;
            } else {
                j++;
            }
        }
        List<Integer> ans = new ArrayList<>();
        for (int p = i + 1; p <= j - 1; p++) ans.add(arr[p]);
        return ans;
    }
}
```
TypeScript 代码:
```TypeScript
function findClosestElements(arr: number[], k: number, x: number): number[] {
    let n = arr.length, l = 0, r = n - 1
    while (l < r) {
        const mid = l + r + 1 >> 1
        if (arr[mid] <= x) l = mid
        else r = mid - 1
    }
    r = r + 1 < n && Math.abs(arr[r + 1] - x) < Math.abs(arr[r] - x) ? r + 1 : r
    let i = r - 1, j = r + 1
    while (j - i - 1 < k) {
        if (i >= 0 && j < n) {
            if (Math.abs(arr[j] - x) < Math.abs(arr[i] - x)) j++
            else i--
        } else if (i >= 0) {
            i--
        } else {
            j++
        }
    }
    return arr.slice(i + 1, j);
};
```
* 时间复杂度:查找分割点复杂度为 $O(\log{n})$;从分割点往两边拓展复杂度为 $O(k)$。整体复杂度为 $O(\log{n} + k)$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


Question:
图像平滑器 是大小为 $3 * 3$ 的过滤器,用于对图像的每个单元格平滑处理,平滑处理后单元格的值为该单元格的平均灰度。

每个单元格的  平均灰度 定义为:该单元格自身及其周围的 $8$ 个单元格的平均值,结果需向下取整。(即,需要计算蓝色平滑器中 $9$ 个单元格的平均值)。

如果一个单元格周围存在单元格缺失的情况,则计算平均灰度时不考虑缺失的单元格(即,需要计算红色平滑器中 $4$ 个单元格的平均值)。

给你一个表示图像灰度的 $m * n$ 整数矩阵 `img` ,返回对图像的每个单元格平滑处理后的图像 。

示例 1:

```
输入:img = [[1,1,1],[1,0,1],[1,1,1]]

输出:[[0, 0, 0],[0, 0, 0], [0, 0, 0]]

解释:
对于点 (0,0), (0,2), (2,0), (2,2): 平均(3/4) = 平均(0.75) = 0
对于点 (0,1), (1,0), (1,2), (2,1): 平均(5/6) = 平均(0.83333333) = 0
对于点 (1,1): 平均(8/9) = 平均(0.88888889) = 0
```
示例 2:

```
输入: img = [[100,200,100],[200,50,200],[100,200,100]]

输出: [[137,141,137],[141,138,141],[137,141,137]]

解释:
对于点 (0,0), (0,2), (2,0), (2,2): floor((100+200+200+50)/4) = floor(137.5) = 137
对于点 (0,1), (1,0), (1,2), (2,1): floor((200+200+50+200+100+100)/6) = floor(141.666667) = 141
对于点 (1,1): floor((50+200+200+200+200+100+100+100+100)/9) = floor(138.888889) = 138
```

提示:
* $m == img.length$
* $n == img[i].length$
* $1 <= m, n <= 200$
* $0 <= img[i][j] <= 255$

Solution:
### 朴素解法

为了方便,我们称每个单元格及其八连通方向单元格所组成的连通块为一个 `item`。

数据范围只有 $200$,我们可以直接对每个 `item` 进行遍历模拟。

代码:
```Java
class Solution {
    public int[][] imageSmoother(int[][] img) {
        int m = img.length, n = img[0].length;
        int[][] ans = new int[m][n];
        int[][] dirs = new int[][]{{0,0},{1,0},{-1,0},{0,1},{0,-1},{-1,-1},{-1,1},{1,-1},{1,1}};
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                int tot = 0, cnt = 0;
                for (int[] di : dirs) {
                    int nx = i + di[0], ny = j + di[1];
                    if (nx < 0 || nx >= m || ny < 0 || ny >= n) continue;
                    tot += img[nx][ny]; cnt++;
                }
                ans[i][j] = tot / cnt;
            }
        }
        return ans;
    }
}
```
---
```Python
dirs = list(product(*[[-1,0,1]] * 2))
class Solution:
    def imageSmoother(self, img: List[List[int]]) -> List[List[int]]:
        m, n = len(img), len(img[0])
        ans = [[0] * n for _ in range(m)]
        for i in range(m):
            for j in range(n):
                tot, cnt = 0, 0
                for di in dirs:
                    if 0 <= (nx := i + di[0]) < m and 0 <= (ny := j + di[1]) < n:
                        tot += img[nx][ny]
                        cnt += 1
                ans[i][j] = tot // cnt
        return ans
```
* 时间复杂度:$O(m \times n \times C)$,其中 $C$ 为灰度单位所包含的单元格数量,固定为 $9$
* 空间复杂度:$O(m \times n)$

---

### 前缀和

在朴素解法中,对于每个 $ans[i][j]$ 我们都不可避免的遍历 $8$ 联通方向,而利用「前缀和」我们可以对该操作进行优化。

对于某个 $ans[i][j]$ 而言,我们可以直接计算出其所在 `item` 的左上角 $(a, b) = (i - 1, j - 1)$ 以及其右下角 $(c, d) = (i + 1, j + 1)$,同时为了防止超出原矩阵,我们需要将 $(a, b)$ 与 $(c, d)$ 对边界分别取 `max` 和 `min`。

当有了合法的 $(a, b)$ 和 $(c, d)$ 后,我们可以直接计算出 `item` 的单元格数量(所包含的行列乘积)及 `item`  的单元格之和(前缀和查询),从而算得 $ans[i][j]$。

代码:
```Java
class Solution {
    public int[][] imageSmoother(int[][] img) {
        int m = img.length, n = img[0].length;
        int[][] sum = new int[m + 10][n + 10];
        for (int i = 1; i <= m; i++) {
            for (int j = 1; j <= n; j++) {
                sum[i][j] = sum[i - 1][j] + sum[i][j - 1] - sum[i - 1][j - 1] + img[i - 1][j - 1];
            }
        }
        int[][] ans = new int[m][n];
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                int a = Math.max(0, i - 1), b = Math.max(0, j - 1);
                int c = Math.min(m - 1, i + 1), d = Math.min(n - 1, j + 1);
                int cnt = (c - a + 1) * (d - b + 1);
                int tot = sum[c + 1][d + 1] - sum[a][d + 1] - sum[c + 1][b] + sum[a][b];
                ans[i][j] = tot / cnt;
            }
        }
        return ans;
    }
}
```
---
```Python
class Solution:
    def imageSmoother(self, img: List[List[int]]) -> List[List[int]]:
        m, n = len(img), len(img[0])
        sum = [[0] * (n + 10) for _ in range(m + 10)]
        for i in range(1, m + 1):
            for j in range(1, n + 1):
                sum[i][j] = sum[i - 1][j] + sum[i][j - 1] - sum[i - 1][j - 1] + img[i - 1][j - 1]
        ans = [[0] * n for _ in range(m)]
        for i in range(m):
            for j in range(n):
                a, b = max(0, i - 1), max(0, j - 1)
                c, d = min(m - 1, i + 1), min(n - 1, j + 1)
                cnt = (c - a + 1) * (d - b + 1)
                tot = sum[c + 1][d + 1] - sum[a][d + 1] - sum[c + 1][b] + sum[a][b]
                ans[i][j] = tot // cnt
        return ans
```
* 时间复杂度:$O(m \times n)$
* 空间复杂度:$O(m \times n)$

### 最后

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

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

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

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


662. 二叉树最大宽度

Question:
给你一棵二叉树的根节点 `root`,返回树的最大宽度。

树的「最大宽度」是所有层中最大的宽度 。

每一层的宽度被定义为该层最左和最右的非空节点(即两个端点)之间的长度。

将这个二叉树视作与满二叉树结构相同,两端点间会出现一些延伸到这一层的 `null` 节点,这些 `null` 节点也计入长度。

题目数据保证答案将会在  `32` 位 带符号整数范围内。

示例 1:

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

输出:4

解释:最大宽度出现在树的第 3 层,宽度为 4 (5,3,null,9) 。
```
示例 2:

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

输出:7

解释:最大宽度出现在树的第 4 层,宽度为 7 (6,null,null,null,null,null,7) 。
```
示例 3:

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

输出:2

解释:最大宽度出现在树的第 2 层,宽度为 2 (3,2) 。
```

提示:
* 树中节点的数目范围是 $[1, 3000]$
* $-100 <= Node.val <= 100$

Solution:
### DFS

根据满二叉树的节点编号规则:若根节点编号为 `u`,则其左子节点编号为 `u << 1`,其右节点编号为 `u << 1 | 1`。

一个朴素的想法是:我们在 `DFS`过程中使用两个哈希表分别记录每层深度中的最小节点编号和最大节点编号,两者距离即是当前层的宽度,最终所有层数中的最大宽度即是答案。

而实现上,我们可以利用先 `DFS` 左节点,再 `DFS` 右节点的性质可知,每层的最左节点必然是最先被遍历到,因此我们只需要记录当前层最先被遍历到点编号(即当前层最小节点编号),并在 `DFS` 过程中计算宽度,更新答案即可。

> 看到评论区有同学讨论关于编号溢出问题,之所以溢出仍能 `AC` 是因为测试数组中没有同层内「宽度」左端点不溢出,右端点溢出,同时该层就是最大宽度的数据点。
我们可以通过 `u = u - map.get(depth) + 1` 操作来对同层内的节点进行重新编号(使得同层最靠左的非空节点编号为 $1$)。
通过重编号操作 我们可以消除由于深度加深带来的编号溢出问题,同时 `TS` 代码不再需要使用 `bigint`。

Java 代码:
```Java
class Solution {
    Map<Integer, Integer> map = new HashMap<>();
    int ans;
    public int widthOfBinaryTree(TreeNode root) {
        dfs(root, 1, 0);
        return ans;
    }
    void dfs(TreeNode root, int u, int depth) {
        if (root == null) return ;
        if (!map.containsKey(depth)) map.put(depth, u);
        ans = Math.max(ans, u - map.get(depth) + 1);
        u = u - map.get(depth) + 1;
        dfs(root.left, u << 1, depth + 1);
        dfs(root.right, u << 1 | 1, depth + 1);
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    unordered_map<int, int> map;
    int ans = 0;
    int widthOfBinaryTree(TreeNode* root) {
        dfs(root, 1, 0);
        return ans;
    }
    void dfs(TreeNode* root, int u, int depth) {
        if (!root) return;
        if (!map.count(depth)) map[depth] = u;
        ans = max(ans, u - map[depth] + 1);
        u = u - map[depth] + 1;
        dfs(root->left, u << 1, depth + 1);
        dfs(root->right, u << 1 | 1, depth + 1);
    }
};
```
Python 代码:
```Python
class Solution:
    def widthOfBinaryTree(self, root: Optional[TreeNode]) -> int:
        mapping = {}
        ans = 0
        def dfs(root, u, depth):
            nonlocal ans
            if not root:
                return
            if depth not in mapping:
                mapping[depth] = u
            ans = max(ans, u - mapping[depth] + 1)
            u = u - mapping[depth] + 1
            dfs(root.left, u << 1, depth + 1)
            dfs(root.right, u << 1 | 1, depth + 1)
        dfs(root, 1, 0)
        return ans
```
TypeScript 代码:
```TypeScript
let map = new Map<number, number>()
let ans = 0
function widthOfBinaryTree(root: TreeNode | null): number {
    map.clear()
    ans = 0
    dfs(root, 1, 0)
    return ans
};
function dfs(root: TreeNode | null, u: number, depth: number): void {
    if (root == null) return
    if (!map.has(depth)) map.set(depth, u)
    ans = Math.max(ans, u - map.get(depth) + 1)
    u = u - map.get(depth) + 1
    dfs(root.left, u << 1, depth + 1)
    dfs(root.right, u << 1 | 1, depth + 1)
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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