Question:
给定一个字符串 `queryIP`。如果是有效的 `IPv4` 地址,返回 `"IPv4"` ;如果是有效的 `IPv6` 地址,返回 `"IPv6"` ;如果不是上述类型的 `IP` 地址,返回 `"Neither"` 。

有效的 `IPv4` 地址 是 `“x1.x2.x3.x4”` 形式的 `IP` 地址。 其中$ 0 <= x_i <= 255$ 且 $x_i$ 不能包含 前导零。

例如: `“192.168.1.1”` 、 `“192.168.1.0”` 为有效 `IPv4` 地址, `“192.168.01.1”` 为无效 `IPv4` 地址; `“192.168.1.00”` 、 `“192.168@1.1”` 为无效 `IPv4` 地址。

一个有效的 `IPv6` 地址 是一个格式为 `“x1:x2:x3:x4:x5:x6:x7:x8”` 的 `IP` 地址,其中:

* $1 <= x_i.length <= 4$
* $x_i$ 是一个 十六进制字符串 ,可以包含数字、小写英文字母( `'a'` 到 `'f'` )和大写英文字母( `'A'` 到 `'F'` )。
* 在 $x_i$ 中允许前导零。

例如 `"2001:0db8:85a3:0000:0000:8a2e:0370:7334"` 和 `"2001:db8:85a3:0:0:8A2E:0370:7334"` 是有效的 `IPv6` 地址,而 `"2001:0db8:85a3::8A2E:037j:7334"` 和 `"02001:0db8:85a3:0000:0000:8a2e:0370:7334"` 是无效的 `IPv6` 地址。

示例 1:
```
输入:queryIP = "172.16.254.1"

输出:"IPv4"

解释:有效的 IPv4 地址,返回 "IPv4"
```
示例 2:
```
输入:queryIP = "2001:0db8:85a3:0:0:8A2E:0370:7334"

输出:"IPv6"

解释:有效的 IPv6 地址,返回 "IPv6"
```
示例 3:
```
输入:queryIP = "256.256.256.256"

输出:"Neither"

解释:既不是 IPv4 地址,又不是 IPv6 地址
```

提示:
* `queryIP` 仅由英文字母,数字,字符 `'.'` 和 `':'` 组成。

Solution:
### 模拟

为了方便,我们称合法 `IPv4`/`IPv6` 中由 `.`/`:` 分割的部分称为 `item`。

无论是 `IPv4` 还是 `IPv6`,我们都只需将连续段的 `item` 取出,并结合题意判断即可,一个较为简单的方式使用 `split` 操作来得到所有的 `item`,考虑到某些语言并不内置 `split`,这里采取双指针的方式来做。

为方便大家理解,今天将题解文字说明写到注释中。

代码:
```Java
class Solution {
    public String validIPAddress(String ip) {
        if (ip.indexOf(".") >= 0 && check4(ip)) return "IPv4";
        if (ip.indexOf(":") >= 0 && check6(ip)) return "IPv6";
        return "Neither";
    }
    boolean check4(String ip) {
        int n = ip.length(), cnt = 0;
        char[] cs = ip.toCharArray();
        for (int i = 0; i < n && cnt <= 3; ) {
            // 找到连续数字段,以 x 存取
            int j = i, x = 0;
            while (j < n && cs[j] >= '0' && cs[j] <= '9' && x <= 255) x = x * 10 + (cs[j++] - '0');
            // 非 item 字符之间没有 item
            if (i == j) return false;
            // 含前导零 或 数值大于 255
            if ((j - i > 1 && cs[i] == '0') || (x > 255)) return false;
            i = j + 1;
            if (j == n) continue;
            // 存在除 . 以外的其他非数字字符
            if (cs[j] != '.') return false;
            cnt++;
        }
        // 恰好存在 3 个不位于两端的 .
        return cnt == 3 && cs[0] != '.' && cs[n - 1] != '.';
    }
    boolean check6(String ip) {
        int n = ip.length(), cnt = 0;
        char[] cs = ip.toCharArray();
        for (int i = 0; i < n && cnt <= 7; ) {
            int j = i;
            while (j < n && ((cs[j] >= 'a' && cs[j] <= 'f') || (cs[j] >= 'A' && cs[j] <= 'F') || (cs[j] >= '0' && cs[j] <= '9'))) j++;
            // 非 item 字符之间没有 item 或 长度超过 4
            if (i == j || j - i > 4) return false;
            i = j + 1;
            if (j == n) continue;
            // 存在除 : 以外的其他非数字字符
            if (cs[j] != ':') return false;
            cnt++;
        }
        // 恰好存在 7 个不位于两段的 :
        return cnt == 7 && cs[0] != ':' && cs[n - 1] != ':';
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:使用 `toCharArray` 操作会产生新数组,复杂度为 $O(n)$,使用 `charAt` 操作代替复杂度为 $O(1)$

### 最后

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

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

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

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


LeetCode/461-470/470. 用 Rand7() 实现 Rand10()（中等）.md

Question:
已有方法 rand7 可生成 1 到 7 范围内的均匀随机整数,试写一个方法 rand10 生成 1 到 10 范围内的均匀随机整数。

不要使用系统的 Math.random() 方法。

示例 1:
```
输入: 1

输出: [7]
```
示例 2:
```
输入: 2

输出: [8,4]
```
示例 3:
```
输入: 3

输出: [8,1,10]
```

提示:
1. rand7 已定义。
2. 传入参数: n 表示 rand10 的调用次数。

进阶:
* rand7()调用次数的 期望值 是多少 ?
* 你能否尽量少调用 rand7() ?

Solution:
### 基本分析

给定一个随机生成 $1$ ~ $7$ 的函数,要求实现等概率返回 $1$ ~ $10$ 的函数。

首先需要知道,在输出域上进行定量整体偏移,仍然满足等概率,即要实现 $0$ ~ $6$ 随机器,只需要在 `rand7` 的返回值上进行 $-1$ 操作即可。

但输出域的 拼接/叠加 并不满足等概率。例如 `rand7() + rand7()` 会产生 $[2, 14]$ 范围内的数,但每个数并非等概率:

* 产生 $2$ 的概率为:$\frac{1}{7} * \frac{1}{7} = \frac{1}{49}$
* 产生 $4$ 的概率为:$\frac{1}{7} * \frac{1}{7} + \frac{1}{7} * \frac{1}{7} + \frac{1}{7} * \frac{1}{7} = \frac{3}{49}$

在 $[2, 14]$ 这 $13$ 个数里面,等概率的数值不足 $10$ 个。

**因此,你应该知道「执行两次 `rand7()` 相加,将 $[1, 10]$ 范围内的数进行返回,否则一直重试」的做法是错误的。**

---

### $k$ 进制诸位生成 + 拒绝采样

上述做法出现概率分布不均的情况,是因为两次随机值的不同组合「相加」的会出现相同的结果($(1, 3)$、$(2, 2)$、$(3, 1)$ 最终结果均为 $4$)。

结合每次执行 `rand7` 都可以看作一次独立事件。我们可以将两次 `rand7` 的结果看作生成 $7$ 进制的两位。**从而实现每个数值都唯一对应了一种随机值的组合(等概率),反之亦然。**

举个🌰,设随机执行两次 `rand7` 得到的结果分别是 $4$(第一次)、$7$(第二次),由于我们是要 $7$ 进制的数,因此可以先对 `rand7` 的执行结果进行 $-1$ 操作,将输出域偏移到 $[0, 6]$(仍为等概率),即得到 $3$(第一次)和 $6$(第二次),最终得到的是数值 $(63)_7$,数值 $(63)_7$ 唯一对应了我们的随机值组合方案,反过来随机值组合方案也唯一对应一个 $7$ 进制的数值。

**那么根据「进制转换」的相关知识,如果我们存在一个 `randK` 的函数,对其执行 $n$ 次,我们能够等概率产生 $[0, K^n - 1]$ 范围内的数值。**

回到本题,执行一次 `rand7` 只能产生 $[0, 6]$ 范围内的数值,不足 $10$ 个;而执行 $2$ 次 `rand7` 的话则能产生 $[0, 48]$ 范围内的数值,足够 $10$ 个,且等概率。

我们只需要判定生成的值是否为题意的 $[1, 10]$ 即可,如果是的话直接返回,否则一直重试。

代码:

```Java
class Solution extends SolBase {
    public int rand10() {
        while (true) {
            int ans = (rand7() - 1) * 7 + (rand7() - 1); // 进制转换
            if (1 <= ans && ans <= 10) return ans;
        }
    }
}
```
* 时间复杂度:期望复杂度为 $O(1)$,最坏情况下为 $O(\infty)$
* 空间复杂度:$O(1)$

---

### 进阶

1. 降低对 `rand7` 的调用次数

我们发现,在上述解法中,范围 $[0, 48]$ 中,只有 $[1, 10]$ 范围内的数据会被接受返回,其余情况均被拒绝重试。

为了尽可能少的调用 `rand7` 方法,我们可以从 $[0, 48]$ 中取与 $[1, 10]$ 成倍数关系的数,来进行转换。

我们可以取 $[0, 48]$ 中的 $[1, 40]$ 范围内的数来代指 $[1, 10]$。

首先在 $[0, 48]$ 中取 $[1, 40]$ 仍为等概率,其次形如 $x1$ 的数值有 $4$ 个($1$、$11$、$21$、$31$),形如 $x2$ 的数值有 $4$ 个($2$、$12$、$22$、$32$)... 因此最终结果仍为等概率。

代码:
```Java
class Solution extends SolBase {
    public int rand10() {
        while (true) {
            int ans = (rand7() - 1) * 7 + (rand7() - 1); // 进制转换
            if (1 <= ans && ans <= 40) return ans % 10 + 1;
        }
    }
}
```
* 时间复杂度:期望复杂度为 $O(1)$,最坏情况下为 $O(\infty)$
* 空间复杂度:$O(1)$

2.  计算 `rand7` 的期望调用次数

在 $[0, 48]$ 中我们采纳了 $[1, 40]$ 范围内的数值,即以调用两次为基本单位的话,有 $\frac{40}{49}$ 的概率被接受返回(成功)。

成功的概率为 $\frac{40}{49}$,那么需要触发成功所需次数(期望次数)为其倒数 $\frac{49}{40} = 1.225$,每次会调用两次 `rand7`,因而总的期望调用次数为 $1.225 * 2 = 2.45$ 。

### 最后

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

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

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

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


Question:
给你一个 不含重复 单词的字符串数组 `words` ,请你找出并返回 `words` 中的所有 连接词 。

连接词 定义为:一个完全由给定数组中的至少两个较短单词组成的字符串。

示例 1:
```
输入:words = ["cat","cats","catsdogcats","dog","dogcatsdog","hippopotamuses","rat","ratcatdogcat"]

输出:["catsdogcats","dogcatsdog","ratcatdogcat"]

解释:"catsdogcats" 由 "cats", "dog" 和 "cats" 组成;
     "dogcatsdog" 由 "dog", "cats" 和 "dog" 组成;
     "ratcatdogcat" 由 "rat", "cat", "dog" 和 "cat" 组成。
```
示例 2:
```
输入:words = ["cat","dog","catdog"]

输出:["catdog"]
```

提示:
* $1 <= words.length <= 10^4$
* $0 <= words[i].length <= 1000$
* $words[i]$ 仅由小写字母组成
* $0 <= sum(words[i].length) <= 10^5$

Solution:
### 序列 DP + 字符串哈希

给定数组 $words$,先考虑如何判断某个 $s = words[i]$ 是否为「连接词」。

为了方便,我们称组成 `s` 的每个连接部分为 `item`。

举个 🌰,例如 `s = abc`,其可能的 `item` 组合为 `a` 和 `bc`。

判断单个字符串是否为连接词可使用动态规划求解:**定义 $f[i]$ 为考虑 `s` 的前 $i$ 个字符(令下标从 $1$ 开始),能够切分出的最大 `item` 数的个数。**

这里之所以采用「记录 $f[i]$ 为最大分割 `item` 数(`int` 类型动规数组)」,而不是「记录 $f[i]$ 为是否可由多个 `item` 组成(`bool` 类型动规数组)」,是因为每个 $s = words[i]$ 至少可由自身组成,采用 `bool` 记录状态的话,最终 $f[n]$ 必然为 `True`,需要额外处理最后一个状态,干脆记录最大分割数量好了。此时如果 `s` 为「连接词」必然有 $f[n] > 1$。

不失一般性的考虑 $f[i]$ 该如何转移:**假设 $f[i]$ 可由 $f[j]$ 转移而来(其中 $j < i$),那么能够转移的充要条件为 $f[j] != 0$ 且子串 $s[(j + 1)..i]$ 在 $words$ 出现过**。

其中枚举 $i$ 和 $j$ 的复杂度已经去到 $O(n^2)$ 了,如果常规通过 `HashMap` 等数据结构判断某个字符串是否存在,执行哈希函数时需要对字符进行遍历,整体复杂度去到了 $O(n^3)$,会 `TLE`。

**我们通过「字符串哈希」方式来优化判断某个子串是否存在于 $words$ 中。**

具体的,在判断每个 $s = words[i]$ 是否为为连接词前,先对 $words$ 进行遍历,预处理每个 $words[i]$ 的哈希值,并存入 `HashSet` 中,这样我们将「判断某个子串是否存在于 $words$」的问题转化为「判断某个数值是否存在于 `Set` 当中」。

又由于 **我们在计算某个子串 `s` 的哈希值时,是从前往后处理每一位的 $s[i]$,因此在转移 $f[i]$ 时,我们期望能够从前往后处理子串,这是常规的从 $[0, i - 1]$ 范围内找可转移点 $f[j]$ 无法做到的**。

所以 **我们调整转移逻辑为:从 $f[i]$ 出发,枚举范围 $[i + 1, n]$,找到可由 $f[i]$ 所能更新的状态 $f[j]$,并尝试使用 $f[i]$ 来更新 $f[j]$。转移方程为:**

$$
f[j] = \max(f[j], f[i] + 1)
$$

当然,能够转移的前提条件为 $f[i]$ 为有效值,且子串 $s[(i + 1), j]$ 在 $words$ 出现过。

> 一些细节:为了方便,我们定义 $f[i] = -1$ 为无效状态;
另外由于字符串哈希会产生哈希碰撞,这里在计算哈希值的时候,修改了一下哈希计算方式(额外增加了一个 `OFFSET`),当时的目的是想在电脑没电前 `AC`,而另一个更加稳妥的方式是使用双哈希,或是干脆记录某个哈希值对应了哪些字符串。

代码:
```Java
class Solution {
    Set<Long> set = new HashSet<>();
    int P = 131, OFFSET = 128;
    public List<String> findAllConcatenatedWordsInADict(String[] words) {
        for (String s : words) {
            long hash = 0;
            for (char c : s.toCharArray()) hash = hash * P + (c - 'a') + OFFSET;
            set.add(hash);
        }
        List<String> ans = new ArrayList<>();
        for (String s : words) {
            if (check(s)) ans.add(s);
        }
        return ans;
    }
    boolean check(String s) {
        int n = s.length();
        int[] f = new int[n + 1];
        Arrays.fill(f, -1);
        f[0] = 0;
        for (int i = 0; i <= n; i++) {
            if (f[i] == -1) continue;
            long cur = 0;
            for (int j = i + 1; j <= n; j++) {
                cur = cur * P + (s.charAt(j - 1) - 'a') + OFFSET;
                if (set.contains(cur)) f[j] = Math.max(f[j], f[i] + 1);
            }
            if (f[n] > 1) return true;
        }
        return false;
    }
}
```
* 时间复杂度:令 $n$ 为 $words$ 数组长度,$N = \sum_{i = 0}^{n - 1}words[i].length$,根据数据范围 $N$ 最大为 $1e5$。预处理出 `Set` 的复杂度为 $O(N)$;会对所有 $words[i]$ 执行 `check` 操作,复杂度为 $O((words[i].length)^2)$,总的计算量最大值为 $O(N^2)$,由于存在剪枝,实际上达不到该计算量
* 空间复杂度:$O(n + \max(words[i].length))$

### 最后

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

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

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

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


Question:
你将得到一个整数数组 `matchsticks`,其中 $matchsticks[i]$ 是第 `i` 个火柴棒的长度。你要用 所有的火柴棍 拼成一个正方形。

你 不能折断 任何一根火柴棒,但你可以把它们连在一起,而且每根火柴棒必须 使用一次 。

如果你能使这个正方形,则返回 `true`,否则返回 `false`。

示例 1:

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

输出: true

解释: 能拼成一个边长为2的正方形,每边两根火柴。
```
示例 2:
```
输入: matchsticks = [3,3,3,3,4]

输出: false

解释: 不能用所有火柴拼成一个正方形。
```

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

Solution:
> 祝大家儿童节快乐,永葆童心,一直善良 😊 ~

### DFS 剪枝

为了方便,我们称 `matchsticks` 为 `ms`。

数据范围为 $n = 15$,朴素的 `DFS` 爆搜(搜索过程中维护一个大小为 $4$ 的数组 `cur`,数组中的每一位代表正方形一条边长所使用到的火柴总长度,若最终数组中每一位均等于 $t = \frac{\sum_{i = 0}^{n - 1}ms[i]}{4}$,代表存在合法方案)复杂度为 $4^n$,会 `TLE`。

我们考虑如何进行「剪枝」。

首先一个较为明显的剪枝操作是进行「可行性剪枝」:我们在决策 $ms[idx]$ 时,如果将其累加到某个 $cur[i]$ 之后,会导致 $cur[i] > t$,则说明必然不会是合法方案,该分支不再往后搜索。

另外一个较为 trick 的剪枝是通过「调整搜索顺序/检查当前桶的总长度」来进行「重复性剪枝」:我们可以先对 `ms` 排倒序,进行「从大到小」的爆搜。本质上,我们是将一些小火柴重复放到某几个桶的搜索路径(其实对应的是相同的分配方案),放到了最后处理;同时,当我们要放置 $ms[idx]$ 的火柴时,如果存在多个桶总长度相等,例如 $cur[i] = cur[j]$,将 $ms[idx]$ 放置在 $cur[i]$ 或 $cur[j]$ 也是等价的。

代码:
```Java
class Solution {
    int[] ms;
    int t;
    public boolean makesquare(int[] _ms) {
        ms = _ms;
        int sum = 0;
        for (int i : ms) sum += i;
        t = sum / 4;
        if (t * 4 != sum) return false;
        Arrays.sort(ms);
        return dfs(ms.length - 1, new int[4]);
    }
    boolean dfs(int idx, int[] cur) {
        if (idx == -1) return true;
        out:for (int i = 0; i < 4; i++) {
            for (int j = 0; j < i; j++) {
                if (cur[j] == cur[i]) continue out;
            }
            int u = ms[idx];
            if (cur[i] + u > t) continue;
            cur[i] += u;
            if (dfs(idx - 1, cur)) return true;
            cur[i] -= u;
        }
        return false;
    }
}
```
* 时间复杂度:$O(4^n)$
* 空间复杂度:排序的复杂度为 $O(\log{n})$,忽略递归带来的额外空间开销,复杂度为 $O(\log{n})$

---

### 模拟退火

事实上,这道题还能使用「模拟退火」进行求解。

**因为将 $n$ 个数划分为 $4$ 份,等效于用 $n$ 个数构造出一个「特定排列」,然后对「特定排列」进行固定模式的构造逻辑,就能实现「答案」与「最优排列」的对应关系。**

基于此,我们可以使用「模拟退火」进行求解。

单次迭代的基本流程:

1. 随机选择两个下标,计算「交换下标元素前对应序列的得分」&「交换下标元素后对应序列的得分」
2. 如果温度下降(交换后的序列更优),进入下一次迭代
3. 如果温度上升(交换前的序列更优),以「一定的概率」恢复现场(再交换回来)

值得一提的是,这道题造数据的人十分有水平,在最后一个样例中放了个卡 `SA` 的数据:
```
[403,636,824,973,815,318,881,506,863,21,834,211,316,772,803]
```

但我不知道为什么 LC 的提交结果会这么奇怪,已通过 `184/183` 样例?是总样例数哪个地方更新漏了,还是缓存没刷新:

这个数据点优秀在于起始排序可以导致我们固定的 `calc` 逻辑最终落入局部最优。针对这种情况,也很好解决,只需要在执行 `SA`  之前,先对原数组进行一次随机化打乱即可。

代码(`2022/06/01` 可通过):
```Java
class Solution {
    int[] ms;
    int n, k;
    Random random = new Random(20220601);
    double hi = 1e9, lo = 1e-4, fa = 0.95;
    int N = 400;
    boolean ans = false;
    int calc() {
        int diff = 0;
        for (int i = 0, j = 0; i < 4; i++) {
            int cnt = 0;
            while (j < n && cnt < k) cnt += ms[j++];
            diff += Math.abs(k - cnt);
        }
        if (diff == 0) ans = true;
        return diff;
    }
    void sa() {
        shuffle(ms);
        for (double t = hi; t > lo && !ans; t *= fa) {
            int a = random.nextInt(n), b = random.nextInt(n);
            if (a == b) continue;
            int prev = calc();
            swap(ms, a, b);
            int cur = calc();
            int diff = cur - prev;
            if (Math.log(diff / t) > random.nextDouble()) swap(ms, a, b);
        }
    }
    public boolean makesquare(int[] _ms) {
        ms = _ms;
        n = ms.length;
        int sum = 0;
        for (int i : ms) sum += i;
        k = sum / 4;
        if (k * 4 != sum) return false;
        while (!ans && N-- > 0) sa();
        return ans;
    }
    void shuffle(int[] nums) {
        for (int i = n; i > 0; i--) {
            int idx = random.nextInt(i);
            swap(nums, idx, i - 1);
        }
    }
    void swap(int[] nums, int a, int b) {
        int c = nums[a];
        nums[a] = nums[b];
        nums[b] = c;
    }
}
```

---

### 我猜你问

**Q0. 模拟退火有何风险?**
随机算法,会面临 `WA` 和 `TLE` 风险。

**Q1. 模拟退火中的参数如何敲定的?**
根据经验猜的,然后提交。根据结果是 `WA` 还是 `TLE` 来决定之后的调参方向。如果是 `WA` 说明部分数据落到了「局部最优」或者尚未达到「全局最优」。

**Q2. 参数如何调整?**
如果是 `WA` 了,一般我是优先调大 fa 参数,使降温变慢,来变相增加迭代次数;如果是 `TLE` 了,一般是优先调小 fa 参数,使降温变快,减小迭代次数。总迭代参数 `N` 也是同理。

可以简单理解调大 fa 代表将「大步」改为「baby step」,防止越过全局最优,同时增加总执行步数。

**Q3. 关于「模拟退火」正确性?**

随机种子不变,测试数据不变,迭代参数不变,那么退火的过程就是恒定的,必然都能找到这些测试样例的「全局最优」。

### 最后

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

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

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

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


Question:
给你一个二进制字符串数组 strs 和两个整数 m 和 n 。

请你找出并返回 strs 的最大子集的大小,该子集中 最多 有 m 个 0 和 n 个 1 。

如果 x 的所有元素也是 y 的元素,集合 x 是集合 y 的 子集 。

示例 1:
```
输入:strs = ["10", "0001", "111001", "1", "0"], m = 5, n = 3
输出:4
解释:最多有 5 个 0 和 3 个 1 的最大子集是 {"10","0001","1","0"} ,因此答案是 4 。
其他满足题意但较小的子集包括 {"0001","1"} 和 {"10","1","0"} 。{"111001"} 不满足题意,因为它含 4 个 1 ,大于 n 的值 3 。
```
示例 2:
```
输入:strs = ["10", "0", "1"], m = 1, n = 1
输出:2
解释:最大的子集是 {"0", "1"} ,所以答案是 2 。
```

提示:
* 1 <= strs.length <= 600
* 1 <= strs[i].length <= 100
* strs[i] 仅由 '0' 和 '1' 组成
* 1 <= m, n <= 100

Solution:
### (多维)01 背包

通常与「背包问题」相关的题考察的是 **将原问题转换为「背包问题」的能力**。

要将原问题转换为「背包问题」,往往需要从题目中抽象出「价值」与「成本」的概念。

这道题如果抽象成「背包问题」的话,应该是:

**每个字符串的价值都是 1(对答案的贡献都是 1),选择的成本是该字符串中 1 的数量和 0 的数量。**

问我们在 1 的数量不超过 $m$,0 的数量不超过 $n$ 的条件下,最大价值是多少。

由于每个字符串只能被选一次,且每个字符串的选与否对应了「价值」和「成本」,求解的问题也是「最大价值」是多少。

因此可以直接套用 01 背包的「状态定义」来做:

**$f[k][i][j]$ 代表考虑前 k 件物品,在数字 1 容量不超过 $i$,数字 0 容量不超过 $j$ 的条件下的「最大价值」(每个字符串的价值均为 1)。**

有了「状态定义」之后,「转移方程」也很好推导:

$$f[k][i][j] = \max(f[k - 1][i][j], f[k - 1][i - cnt[k][0]][j - cnt[k][1]] + 1)$$

其中 $cnt$ 数组记录的是字符串中出现的 $01$ 数量。

代码(为了方便理解,$P1$ 将第一件物品的处理单独抽了出来,也可以不抽出来,只需要将让物品下标从 $1$ 开始即可,见 $P2$):
```Java
class Solution {
    public int findMaxForm(String[] strs, int m, int n) {
        int len = strs.length;
        // 预处理每一个字符包含 0 和 1 的数量
        int[][] cnt = new int[len][2];
        for (int i = 0; i < len; i++) {
            String str = strs[i];
            int zero = 0, one = 0;
            for (char c : str.toCharArray()) {
                if (c == '0') {
                    zero++;
                } else {
                    one++;
                }
            }
            cnt[i] = new int[]{zero, one};
        }

        // 处理只考虑第一件物品的情况
        int[][][] f = new int[len][m + 1][n + 1];
        for (int i = 0; i <= m; i++) {
            for (int j = 0; j <= n; j++) {
                f[0][i][j] = (i >= cnt[0][0] && j >= cnt[0][1]) ? 1 : 0;
            }
        }

        // 处理考虑其余物品的情况
        for (int k = 1; k < len; k++) {
            int zero = cnt[k][0], one = cnt[k][1];
            for (int i = 0; i <= m; i++) {
                for (int j = 0; j <= n; j++) {
                    // 不选择第 k 件物品
                    int a = f[k-1][i][j];
                    // 选择第 k 件物品(前提是有足够的 m 和 n 额度可使用)
                    int b = (i >= zero && j >= one) ? f[k-1][i-zero][j-one] + 1 : 0;
                    f[k][i][j] = Math.max(a, b);
                }
            }
        }
        return f[len-1][m][n];
    }
}
```
```Java
class Solution {
    public int findMaxForm(String[] strs, int m, int n) {
        int len = strs.length;
        int[][] cnt = new int[len][2];
        for (int i = 0; i < len; i++) {
            String str = strs[i];
            int zero = 0, one = 0;
            for (char c : str.toCharArray()) {
                if (c == '0') zero++;
                else one++;

            }
            cnt[i] = new int[]{zero, one};
        }
        int[][][] f = new int[len + 1][m + 1][n + 1];
        for (int k = 1; k <= len; k++) {
            int zero = cnt[k - 1][0], one = cnt[k - 1][1];
            for (int i = 0; i <= m; i++) {
                for (int j = 0; j <= n; j++) {
                    int a = f[k - 1][i][j];
                    int b = (i >= zero && j >= one) ? f[k - 1][i - zero][j - one] + 1 : 0;
                    f[k][i][j] = Math.max(a, b);
                }
            }
        }
        return f[len][m][n];
    }
}
```
* 时间复杂度:预处理字符串的复杂度为 $O(\sum_{i = 0}^{k - 1}len(strs[i]))$,处理状态转移的 $O(k * m * n)$。整体复杂度为:$O(k * m * n + \sum_{i = 0}^{k - 1}len(strs[i]))$
* 空间复杂度:$O(k * m * n)$

---

### 滚动数组

根据「状态转移」可知,更新某个物品的状态时,只依赖于上一个物品的状态。

因此,可以使用「滚动数组」的方式进行空间优化。

代码(为了方便理解,$P1$ 将第一件物品的处理单独抽了出来,也可以不抽出来,只需要将让物品下标从 $1$ 开始即可,见 $P2$):
```Java
class Solution {
    public int findMaxForm(String[] strs, int m, int n) {
        int len = strs.length;
        // 预处理每一个字符包含 0 和 1 的数量
        int[][] cnt = new int[len][2];
        for (int i = 0; i < len; i++) {
            String str = strs[i];
            int zero = 0, one = 0;
            for (char c : str.toCharArray()) {
                if (c == '0') {
                    zero++;
                } else {
                    one++;
                }
            }
            cnt[i] = new int[]{zero, one};
        }

        // 处理只考虑第一件物品的情况
        // 「物品维度」修改为 2
        int[][][] f = new int[2][m + 1][n + 1];
        for (int i = 0; i <= m; i++) {
            for (int j = 0; j <= n; j++) {
                f[0][i][j] = (i >= cnt[0][0] && j >= cnt[0][1]) ? 1 : 0;
            }
        }

        // 处理考虑其余物品的情况
        for (int k = 1; k < len; k++) {
            int zero = cnt[k][0], one = cnt[k][1];
            for (int i = 0; i <= m; i++) {
                for (int j = 0; j <= n; j++) {
                    // 不选择第 k 件物品
                    // 将 k-1 修改为 (k-1)&1
                    int a = f[(k-1)&1][i][j];
                    // 选择第 k 件物品(前提是有足够的 m 和 n 额度可使用)
                    // 将 k-1 修改为 (k-1)&1
                    int b = (i >= zero && j >= one) ? f[(k-1)&1][i-zero][j-one] + 1 : 0;
                    f[k&1][i][j] = Math.max(a, b);
                }
            }
        }
        // 将 len-1 修改为 (len-1)&1
        return f[(len-1)&1][m][n];
    }
}
```
```Java
class Solution {
    public int findMaxForm(String[] strs, int m, int n) {
        int len = strs.length;
        int[][] cnt = new int[len][2];
        for (int i = 0; i < len; i++) {
            String str = strs[i];
            int zero = 0, one = 0;
            for (char c : str.toCharArray()) {
                if (c == '0') zero++;
                else one++;
            }
            cnt[i] = new int[]{zero, one};
        }
        int[][][] f = new int[2][m + 1][n + 1];
        for (int k = 1; k <= len; k++) {
            int zero = cnt[k - 1][0], one = cnt[k - 1][1];
            for (int i = 0; i <= m; i++) {
                for (int j = 0; j <= n; j++) {
                    int a = f[(k-1) & 1][i][j];
                    int b = (i >= zero && j >= one) ? f[(k-1) & 1][i - zero][j - one] + 1 : 0;
                    f[k&1][i][j] = Math.max(a, b);
                }
            }
        }
        return f[len&1][m][n];
    }
}
```
* 时间复杂度:预处理字符串的复杂度为 $O(\sum_{i = 0}^{k - 1}len(strs[i]))$,处理状态转移的 $O(k * m * n)$。整体复杂度为:$O(k * m * n + \sum_{i = 0}^{k - 1}len(strs[i]))$
* 空间复杂度:$O(m * n)$

---

### 一维空间优化

事实上,我们还能继续进行空间优化。

再次观察我们的「状态转移方程」发现:**$f[k][i][j]$ 不仅仅依赖于上一行,还明确依赖于比 $i$ 小和比 $j$ 小的状态。**

即可只依赖于「上一行」中「正上方」的格子,和「正上方左边」的格子。

对应到「朴素的 01 背包问题」依赖关系如图:

因此可直接参考「01 背包的空间优化」方式:取消掉「物品维度」,然后调整容量的遍历顺序。

代码:
```Java
class Solution {
    public int findMaxForm(String[] strs, int m, int n) {
        int len = strs.length;
        int[][] cnt = new int[len][2];
        for (int i = 0; i < len; i++) {
            int zero = 0, one = 0;
            for (char c : strs[i].toCharArray()) {
                if (c == '0') zero++;
                else one++;
            }
            cnt[i] = new int[]{zero, one};
        }
        int[][] f = new int[m + 1][n + 1];
        for (int k = 0; k < len; k++) {
            int zero = cnt[k][0], one = cnt[k][1];
            for (int i = m; i >= zero; i--) {
                for (int j = n; j >= one; j--) {
                    f[i][j] = Math.max(f[i][j], f[i - zero][j - one] + 1);
                }
            }
        }
        return f[m][n];
    }
}
```
* 时间复杂度:预处理字符串的复杂度为 $O(\sum_{i = 0}^{k - 1}len(strs[i]))$,处理状态转移的 $O(k * m * n)$。整体复杂度为:$O(k * m * n + \sum_{i = 0}^{k - 1}len(strs[i]))$
* 空间复杂度:$O(m * n)$

### 最后

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

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

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

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


Question:
冬季已经来临。 你的任务是设计一个有固定加热半径的供暖器向所有房屋供暖。

在加热器的加热半径范围内的每个房屋都可以获得供暖。

现在,给出位于一条水平线上的房屋 `houses` 和供暖器 `heaters` 的位置,请你找出并返回可以覆盖所有房屋的最小加热半径。

说明:所有供暖器都遵循你的半径标准,加热的半径也一样。

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

输出: 1

解释: 仅在位置2上有一个供暖器。如果我们将加热半径设为1,那么所有房屋就都能得到供暖。
```
示例 2:
```
输入: houses = [1,2,3,4], heaters = [1,4]

输出: 1

解释: 在位置1, 4上有两个供暖器。我们需要将加热半径设为1,这样所有房屋就都能得到供暖。
```
示例 3:
```
输入:houses = [1,5], heaters = [2]

输出:3
```

提示:
* $1 <= houses.length, heaters.length <= 3 \times 10^4$
* $1 <= houses[i], heaters[i] <= 10^9$

Solution:
### 二分 + 双指针

需要求得最小加热半径 $ans$,使得所有的 $houses[i]$ 均被覆盖。

在以 $ans$ 为分割点的数轴上具有「二段性」:

* 数值小于 $ans$ 的半径无法覆盖所有的房子;
* 数值大于等于 $ans$ 的半径可以覆盖所有房子。

因此可直接「二分答案」,考虑应该在什么范围内进行「二分」。

可以从数据范围入手,使用 $1e9$ 为二分上界,该做法能确保答案在二分范围内。

考虑如何实现 `check` 函数。

先对 $houses$ 和 $heaters$ 进行排序,使用 $i$ 指向当前处理到的 $houses[i]$;$j$ 指向 **可能** 覆盖到 $houses[i]$ 的最小下标 $heaters[j]$;$x$ 代表当前需要 `check` 的半径。

当且仅当 $heaters[j] + x < houses[i]$ 时,$houses[i]$ 必然不能被 $heaters[j]$ 所覆盖,此时让 $j$ 自增。

找到合适的 $j$ 之后,再检查 $heaters[j] - x <= houses[i] <= heaters[j] + x$ 是否满足,即可知道 $houses[i]$ 的覆盖情况。

代码:
```Java
class Solution {
    public int findRadius(int[] houses, int[] heaters) {
        Arrays.sort(houses);
        Arrays.sort(heaters);
        int l = 0, r = (int) 1e9;
        while (l < r) {
            int mid = l + r >> 1;
            if (check(houses, heaters, mid)) r = mid;
            else l = mid + 1;
        }
        return r;
    }
    boolean check(int[] houses, int[] heaters, int x) {
        int n = houses.length, m = heaters.length;
        for (int i = 0, j = 0; i < n; i++) {
            while (j < m && houses[i] > heaters[j] + x) j++;
            if (j < m && heaters[j] - x <= houses[i] && houses[i] <= heaters[j] + x) continue;
            return false;
        }
        return true;
    }
}
```
* 时间复杂度:令 $n$ 和 $m$ 分别为 `houses` 和 `heaters` 长度,$L = 1e9$ 为最大长度,对其进行排序复杂度为 $O(n\log{n} + m\log{m})$,在 $[0, L]$ 范围进行二分,单次 `check` 会使用「双指针」判断是否每个 $houses[i]$ 是否被覆盖,复杂度为 $O(\max(n, m) \times \log{L})$。整体复杂度为 $O(\max(n, m) \times \log{L})$
* 空间复杂度:排序所需要消耗的空间。复杂度为 $O(\log{n} + \log{m})$

### 最后

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

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

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

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


Question:
对整数的二进制表示取反(`0` 变 `1` ,`1` 变 `0`)后,再转换为十进制表示,可以得到这个整数的补数。

* 例如,整数 `5` 的二进制表示是 `"101"` ,取反后得到 `"010"` ,再转回十进制表示得到补数 2 。
给你一个整数 `num`,输出它的补数。

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

输出:2

解释:5 的二进制表示为 101(没有前导零位),其补数为 010。所以你需要输出 2 。
```
示例 2:
```
输入:num = 1

输出:0

解释:1 的二进制表示为 1(没有前导零位),其补数为 0。所以你需要输出 0 。
```

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

Solution:
### 模拟(遍历)

返回对 $num$ 的二进制表示取反的数,注意 $num$ 的二进制表示是不包含前导零的。

因此主要问题求得 $num$ 最高位 $1$ 的位置。

一个简单的做法是:先对 $num$ 进行「从高到低」的检查,找到最高位 $1$ 的位置 $s$,然后再对 $num$ 进行遍历,将低位到 $s$ 位的位置执行逐位取反操作。

代码:
```Java
class Solution {
    public int findComplement(int num) {
        int s = -1;
        for (int i = 31; i >= 0; i--) {
            if (((num >> i) & 1) != 0) {
                s = i;
                break;
            }
        }
        int ans = 0;
        for (int i = 0; i < s; i++) {
            if (((num >> i) & 1) == 0) ans |= (1 << i);
        }
        return ans;
    }
}
```
* 时间复杂度:$O(\log{num})$
* 空间复杂度:$O(1)$

---

### 模拟(lowbit)

通过解法一我们发现,如果 $num$ 的二进制表示中最高位 $1$ 的位置为 $s$ 的话,那么实际上我们只需要对 $num$ 的前 $s - 1$ 位进行取反即是答案(第 $s$ 位的取反结果始终为 $0$)。

因此我们可以先使用 `lowbit` 操作来得到 $num$ 二进制表示中最高位 $1$ 的位置为 $1$,其余位为 $0$ 时所代表的数字 $x$。

然后 $x - 1$ 即是二进制表示中前 $s - 1$ 位均为 $1$,其余位为 $0$ 的数字,将其与 $num$ 的取反数执行「按位与」操作,即可达到「仅对 $num$ 的前 $s - 1$ 位进行取反」的效果。

代码:
```Java
class Solution {
    public int findComplement(int num) {
        int x = 0;
        for (int i = num; i != 0; i -= i & -i) x = i;
        return ~num & (x - 1);
    }
}
```
* 时间复杂度:$O(\log{num})$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


Question:
两个整数的 汉明距离 指的是这两个数字的二进制数对应位不同的数量。

给你一个整数数组 nums,请你计算并返回 nums 中任意两个数之间汉明距离的总和。

示例 1:
```
输入:nums = [4,14,2]
输出:6
解释:在二进制表示中,4 表示为 0100 ,14 表示为 1110 ,2表示为 0010 。(这样表示是为了体现后四位之间关系)
所以答案为:
HammingDistance(4, 14) + HammingDistance(4, 2) + HammingDistance(14, 2) = 2 + 2 + 2 = 6
```
示例 2:
```
输入:nums = [4,14,4]
输出:4
```

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

Solution:
### 按位统计

我们知道,汉明距离为两数二进制表示中不同位的个数,同时每位的统计是相互独立的。

即最终的答案为 $\sum_{x = 0}^{31} calc(x)$,其中 $calc$ 函数为求得所有数二进制表示中的某一位 $x$ 所产生的不同位的个数。

我们考虑某个 $cacl(x)$ 如何求得:

事实上,对于某个 `nums[i]` 我们只关心在 `nums` 中有多少数的第 $x$ 位的与其不同,而不关心具体是哪些数与其不同,同时二进制表示中非 $0$ 即 $1$。

**这指导我们可以建立两个集合 $s0$ 和 $s1$,分别统计出 `nums` 中所有数的第 $x$ 位中 $0$ 的个数和 $1$ 的个数,集合中的每次计数代表了 `nums` 中的某一元素,根据所在集合的不同代表了其第 $x$ 位的值。那么要找到在 `nums` 中有多少数与某一个数的第 $x$ 位不同,只需要读取另外一个集合的元素个数即可,变成了 $O(1)$ 操作。那么要求得「第 $x$ 位所有不同数」的对数的个数,只需要应用乘法原理,将两者元素个数相乘即可。**

前面说到每位的统计是相对独立的,因此只要对「每一位」都应用上述操作,并把「每一位」的结果累加即是最终答案。

代码:
```Java
class Solution {
    public int totalHammingDistance(int[] nums) {
        int ans = 0;
        for (int x = 31; x >= 0; x--) {
            int s0 = 0, s1 = 0;
            for (int u : nums) {
                if (((u >> x) & 1) == 1) {
                    s1++;
                } else {
                    s0++;
                }
            }
            ans += s0 * s1;
        }
        return ans;
    }
}
```
* 时间复杂度:$O(C * n)$,$C$ 固定为 $32$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


478. 在圆内随机生成点

Question:
给定圆的半径和圆心的位置,实现函数 `randPoint`,在圆中产生均匀随机点。

实现 `Solution` 类:
* `Solution(double radius, double x_center, double y_center)` 用圆的半径 `radius` 和圆心的位置 $(x_center, y_center)$ 初始化对象
* `randPoint()` 返回圆内的一个随机点。圆周上的一点被认为在圆内。答案作为数组返回 $[x, y]$ 。

示例 1:
```
输入:
["Solution","randPoint","randPoint","randPoint"]
[[1.0, 0.0, 0.0], [], [], []]
输出: [null, [-0.02493, -0.38077], [0.82314, 0.38945], [0.36572, 0.17248]]

解释:
Solution solution = new Solution(1.0, 0.0, 0.0);
solution.randPoint ();//返回[-0.02493,-0.38077]
solution.randPoint ();//返回[0.82314,0.38945]
solution.randPoint ();//返回[0.36572,0.17248
```

提示:
* $0 < radius <= 10^8$
* $-10^7 <= x_center, y_center <= 10^7$
* `randPoint` 最多被调用 $3 \times 10^4$ 次

Solution:
### 等概率随机采样

为了方便,我们称圆心为 $(x, y)$,半径为 $r$。

对给定圆内的点进行等概率随机采样,容易想到随机化两个信息:一个是距离圆心的距离 `len`(在范围 $[0, r]$ 中进行随机),另外一个是夹角 `ang`(在范围 $[0, 2\pi]$ 中随机,随便找个参考线即可,例如以往 $x$ 轴正方向的射线为参考)。

然后根据 `len` 和 `ang` 直接计算对应的点的坐标,这样 **可以确保随机出来的点一定在圆内,但并非「等概率」。**

在不考虑夹角的情况下,我们本质是在 $[0, r]$ 范围内随机,这在「一维」上「等概率」是成立的,因为满足「任意连续段中点被抽到的次数与总次数的比例」与「该连续段长度与总长度的比例」。

但在圆中并非如此,不考虑夹角时,「任意连续段 `len` 与总长度 `r` 的比例」和「`len` 对应面积与总面积比例」并不相等。例如 `len` 有 $\frac{1}{2}$ 的概率取到小于等于 $\frac{r}{2}$ 的值,而半径为 $\frac{r}{2}$ 扫过的面积仅为总面积的 $\frac{1}{4}$,因此我们的 `len` 不能直接在 $[0, r]$ 范围内随机,为了消除这种一维转圆导致的「等概率」失效,我们可以从 $[0, r^2]$ 内随机再开平方,从而确保距离与面积比例一致。

代码:
```Java
class Solution {
    double r, x, y;
    Random random = new Random();
    public Solution(double _r, double _x, double _y) {
        r = _r; x = _x; y = _y;
    }
    public double[] randPoint() {
        double len = Math.sqrt(random.nextDouble(r * r)), ang = random.nextDouble(2 * Math.PI);
        double nx = x + len * Math.cos(ang), ny = y + len * Math.sin(ang);
        return new double[]{nx, ny};
    }
}
```
* 时间复杂度:$O(1)$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


479. 最大回文数乘积

Question:
给定一个整数 $n$ ,返回 可表示为两个 $n$ 位整数乘积的 最大回文整数 。

因为答案可能非常大,所以返回它对 $1337$ 取余 。

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

输出:987

解释:99 x 91 = 9009, 9009 % 1337 = 987
```
示例 2:
```
输入: n = 1

输出: 9
```

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

Solution:
### 枚举 + 数学

对于数位为 $n$ 的两个数而言,其乘积的位数要么是 $2 * n$,要么是 $2 * n - 1$。

当数位 $n > 1$ 时,我们总能在数位为 $2 * n$ 中找到答案。

利用回文串的特性,我们只需枚举回文串的前半部分即可(后半部分唯一确定),我们只要在枚举前半部分时按照「从大到小」进行,即可确保找到的第一个合法值为最大数,对于一个数位为 $n$ 的最大数为 $10^n - 1$。

具体的,当枚举到回文串的前半部分 $i$ 时,我们利用回文串特性构造出具实际的回文数值 $nums$,随后检查 $nums$ 能否分解成数位为 $n$ 的数对 $(a, b)$,利用乘法具有交换律,我们只需要枚举数对中的较大数即可。

代码:
```Java
class Solution {
    public int largestPalindrome(int n) {
        if (n == 1) return 9;
        int max = (int) Math.pow(10, n) - 1;
        for (int i = max; i >= 0; i--) {
            long num = i, t = i;
            while (t != 0) {
                num = num * 10 + (t % 10);
                t /= 10;
            }
            for (long j = max; j * j >= num; j--) {
                if (num % j == 0) return (int)(num % 1337);
            }
        }
        return -1;
    }
}
```
* 时间复杂度:枚举回文串的前半部分复杂度为 $O(10^n)$;检查回文串能否被分解复杂度为 $O(10^n)$。整体复杂度为 $O(10^{2n})$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


480. 滑动窗口中位数

Question:
中位数是有序序列最中间的那个数。如果序列的长度是偶数,则没有最中间的数;此时中位数是最中间的两个数的平均数。

例如:
* $[2,3,4]$,中位数是 $3$
* $[2,3]$,中位数是 $(2 + 3) / 2 = 2.5$

给你一个数组 $nums$,有一个长度为 $k$ 的窗口从最左端滑动到最右端。

窗口中有  $k$ 个数,每次窗口向右移动 $1$ 位。

你的任务是找出每次窗口移动后得到的新窗口中元素的中位数,并输出由它们组成的数组。

示例:
```
给出 nums = [1,3,-1,-3,5,3,6,7],以及 k = 3。

窗口位置                      中位数
---------------               -----
[1  3  -1] -3  5  3  6  7       1
 1 [3  -1  -3] 5  3  6  7      -1
 1  3 [-1  -3  5] 3  6  7      -1
 1  3  -1 [-3  5  3] 6  7       3
 1  3  -1  -3 [5  3  6] 7       5
 1  3  -1  -3  5 [3  6  7]      6

 因此,返回该滑动窗口的中位数数组 [1,-1,-1,3,5,6]。
```

提示:
* 你可以假设 $k$ 始终有效,即:$k$ 始终小于等于输入的非空数组的元素个数。
* 与真实值误差在 $10 ^ {-5}$ 以内的答案将被视作正确答案。

Solution:
### 朴素解法

一个直观的做法是:对每个滑动窗口的数进行排序,获取排序好的数组中的第 $\frac{k}{2}$ 和 $\frac{k - 1}{2}$ 个数(避免奇偶数讨论),计算中位数。

我们大概分析就知道这个做法至少 $O(n * k)$ 的,算上排序的话应该是 $O(n * (k + k\log{k}))$。

比较无奈的是,这道题的中文说明中没有给出数据范围。我们无法根据判断这样的做法会不会超时。

朴素做法通常是优化的开始,所以还是提供一下朴素做法的代码。

代码:
```Java
class Solution {
    public double[] medianSlidingWindow(int[] nums, int k) {
        int n = nums.length;
        int cnt = n - k + 1;
        double[] ans = new double[cnt];
        int[] t = new int[k];
        for (int l = 0, r = l + k - 1; r < n; l++, r++) {
            for (int i = l; i <= r; i++) t[i - l] = nums[i];
            Arrays.sort(t);
            ans[l] = (t[k / 2] / 2.0) + (t[(k - 1) / 2] / 2.0);
        }
        return ans;
    }
}
```
* 时间复杂度:最多有 `n` 个窗口需要滑动计算。每个窗口,需要先插入数据,复杂度为 $O(k)$,插入后需要排序,复杂度为 $O(k\log{k})$。整体复杂度为 $O(n * (k + k\log{k}))$
* 空间复杂度:使用了长度为 `k` 的临时数组。复杂度为 $O(k)$

---

### 优先队列(堆)

从朴素解法中我们可以发现,其实我们需要的就是滑动窗口中的第 $\frac{k}{2}$ 小的值和第 $\frac{k - 1}{2}$ 小的值。

我们知道滑动窗口求最值的问题,可以使用优先队列来做。

但这里我们求的是第 $k$ 小的数,而且是需要两个值。还能不能使用优先队列来做呢?

我们可以维护两个堆:

* 一个大根堆维护着滑动窗口中一半较小的值(此时堆顶元素为滑动窗口中的第 $\frac{k - 1}{2}$ 小的值)
* 一个小根堆维护着滑动窗口中一半较大的值(此时堆顶元素为滑动窗口中的第 $\frac{k}{2}$ 小的值)

滑动窗口的中位数就是两个堆的堆顶元素的平均值。

实现细节:

1. 初始化时,先让 `k` 个元素直接入 `right`,再从 `right` 中倒出 $\frac{k}{2}$ 个到 `left` 中。这时候可以根据 `left` 和 `right` 得到第一个滑动窗口的中位值。

2. 开始滑动窗口,每次滑动都有一个待添加和待移除的数:

    2.1 根据与右堆的堆顶元素比较,决定是插入哪个堆和从哪个堆移除

    2.2 之后调整两堆的大小(确保只会出现 `left.size() == right.size()` 或 `right.size() - left.size() == 1`,对应了窗口长度为偶数或者奇数的情况)

    2.3 根据 `left` 堆 和 `right` 堆得到当前滑动窗口的中位值

代码:
```Java
class Solution {
    public double[] medianSlidingWindow(int[] nums, int k) {
        int n = nums.length;
        int cnt = n - k + 1;
        double[] ans = new double[cnt];
        // 如果是奇数滑动窗口,让 right 的数量比 left 多一个
        PriorityQueue<Integer> left  = new PriorityQueue<>((a,b)->Integer.compare(b,a)); // 滑动窗口的左半部分
        PriorityQueue<Integer> right = new PriorityQueue<>((a,b)->Integer.compare(a,b)); // 滑动窗口的右半部分
        for (int i = 0; i < k; i++) right.add(nums[i]);
        for (int i = 0; i < k / 2; i++) left.add(right.poll());
        ans[0] = getMid(left, right);
        for (int i = k; i < n; i++) {
            // 人为确保了 right 会比 left 多,因此,删除和添加都与 right 比较(left 可能为空)
            int add = nums[i], del = nums[i - k];
            if (add >= right.peek()) {
                right.add(add);
            } else {
                left.add(add);
            }
            if (del >= right.peek()) {
                right.remove(del);
            } else {
                left.remove(del);
            }
            adjust(left, right);
            ans[i - k + 1] = getMid(left, right);
        }
        return ans;
    }
    void adjust(PriorityQueue<Integer> left, PriorityQueue<Integer> right) {
        while (left.size() > right.size()) right.add(left.poll());
        while (right.size() - left.size() > 1) left.add(right.poll());
    }
    double getMid(PriorityQueue<Integer> left, PriorityQueue<Integer> right) {
        if (left.size() == right.size()) {
            return (left.peek() / 2.0) + (right.peek() / 2.0);
        } else {
            return right.peek() * 1.0;
        }
    }
}
```
* 时间复杂度:调整过程中堆大小最大为 `k`,堆操作中的指定元素删除复杂度为 $O(k)$;窗口数量最多为 `n`。整体复杂度为 $O(n * k)$
* 空间复杂度:最多有 `n` 个元素在堆内。复杂度为 $O(n)$

---

### 答疑

以下是针对一些具有代表性的问题进行的集中答疑:

* 为什么 `new PriorityQueue<>((x,y)->(y-x))` 的写法会有某些案例无法通过?和 `new PriorityQueue<>((x,y)->Integer.compare(y,x))` 写法有何区别?

  `(x,y)->(y-x)` 的写法逻辑没有错,AC 不了是因为 int 溢出。

  在 Java 中 Integer.compare 的实现是 `(x < y) ? -1 : ((x == y) ? 0 : 1)`。只是单纯的比较,不涉及运算,所以不存在溢出风险。

  而直接使用 `y - x`,当 `y = Integer.MAX_VALUE`, `x = Integer.MIN_VALUE` 时,到导致溢出,返回的是 **负数** ,而不是逻辑期望的 **正数**

同样具有溢出问题的还有计算第 $\frac{k}{2}$ 小的数和第 $\frac{k - 1}{2}$ 小的数的平均值时。

因此题解中使用的是  `(a / 2.0) + (b / 2.0)` 的形式,而不是采用 `(a + b) / 2.0` 的形式。后者有相加溢出的风险。

---

### 复杂度说明

JDK 中 `PriorityQueue` 的 `remove(Object o)` 实现是先调用 `indexOf(Object o)` 方法进行线性扫描找到下标(复杂度为 $O(n)$),之后再调用 `removeAt(int i)` 进行删除(复杂度为 $O(\log{n})$)。

对于本题而言,如果需要实现 $O(\log{n})$ 的 `remove(Object o)`, 只能通过引入其他数据结构(如哈希表)来实现快速查找元素在对堆数组中的下标。

对于本题,可以使用元素在原数组中的下标作为 key,在堆数组中的真实下标作为 val。

通过哈希表可以 $O(1)$ 的复杂度找到下标,之后的删除只需要算堆调整的复杂度即可(最多 down 一遍,up 一遍,复杂度为 $O(\log{n})$)。

至于 JDK 没有这样做的原因,猜测是因为基本类型的包装类型存在小数缓存机制,导致无法很好的使用哈希表来对应一个插入元素的下标。

举个🌰,我们调用三次 `add(10)`, 会有 $3$ 个 $10$ 在堆内,但是由于小数(默认范围为 $[-128,127]$)包装类型存在缓存机制,使用哈希表继续记录的话,只会有 { Integer.valueOf(10) : 移动过程中最后一次访问的数组下标 } 这样一条记录(`add` 进去的 $10$ 均为同一对象)。这时候删除一个 $10$ 之后,哈希表无法正确指导我们找到下一个 $10$ 的位置。

### 最后

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

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

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

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


Question:
神奇字符串 `s` 仅由 `'1'` 和 `'2'` 组成,并需要遵守下面的规则:

* 神奇字符串 `s` 的神奇之处在于,串联字符串中 `'1'` 和 `'2'` 的连续出现次数可以生成该字符串。

`s` 的前几个元素是 `s = "1221121221221121122......"` 。如果将 `s` 中连续的若干 `1` 和 `2` 进行分组,可以得到 `"1 22 11 2 1 22 1 22 11 2 11 22 ......"` 。

每组中 `1` 或者 `2` 的出现次数分别是 `"1 2 2 1 1 2 1 2 2 1 2 2 ......"` 。上面的出现次数正是 `s` 自身。

给你一个整数 `n` ,返回在神奇字符串 `s` 的前 `n` 个数字中 `1` 的数目。

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

输出:3

解释:神奇字符串 s 的前 6 个元素是 “122112”,它包含三个 1,因此返回 3 。
```
示例 2:
```
输入:n = 1

输出:1
```

提示:
* $1 <= n <= 10^5$

Solution:
### 双指针 + 构造 + 打表

我们将相关的字符串分为三类:**题目描述的神奇字符串 `s` 称为“原串”,对 `s` 进行连续段划分所得的串叫“划分串”,对划分串进行计数的串叫“计数串”**。

解题的核心思路:**由于划分串是对原串的划分,同时计数串又与原串相同,因此可得三类串均只有 `1` 和 `2` 两种数值。即可知划分串的每段长度只能是「长度为 `1`」或「长度为 `2`」,利用划分串的每段构造长度有限,我们可以通过「简单分情况讨论」的方式进行构造**。

具体的,我们需要利用「原串和计数串的相同的性质」对 `s` 进行构造:不难发现计数串总是不长于原串,因此我们可以使用变量 `i` 来记录当前构造到原串位置,使用变量 `j` 来记录计数串对应到的实际位置。

不失一般性假设当前构造到 `s` 中的某一位为 `last`,而计数串对应的实际位置为 `t`,由于两者均只有 `1` 和 `2` 两种可能,我们可以对其进行简单的分情况讨论(可见代码注释)。

> 一些细节:由于神奇字符串起始字符固定,构造逻辑固定,因此神奇字符串唯一固定。
我们可以采取 `static` 代码块的方式进行打表预处理(`Java` 中的 `static` 代码块只会在类加载的过程执行一次,而 `LC` 的测评机制是实例化多个 `Solution` 对象来跑多个样例,但 `Solution` 类仍只会被加载一次,即 `static` 在多个样例测评中只会被执行一次。

Java 代码:
```Java
class Solution {
    static int N = 100010;
    static int[] f = new int[N];
    static {
        StringBuilder sb = new StringBuilder();
        sb.append("01"); // 首位多加一个 0 作为哨兵
        for (int i = 1, j = 1, cnt = 0; i < N; j++) {
            int last = sb.charAt(sb.length() - 1) - '0', t = sb.charAt(j) - '0';
            if (last == 1) {
                if (t == 1) {
                    // 当原串当前字符是 1,而计数串当前字符为 1
                    // 往后构造形成的原串只能是 12,原串指针后移一位
                    sb.append("2");
                    f[i] = ++cnt; i++;
                } else {
                    // 当原串当前字符是 1,而计数串当前字符为 2
                    // 往后构造形成的原串只能是 112,此时同步更新 f[i + 1],原串指针后移两位
                    sb.append("12");
                    f[i] = ++cnt; f[i + 1] = ++cnt; i += 2;
                }
            } else {
                if (t == 1) {
                    // 当原串当前字符是 2,而计数串当前字符为 1
                    // 往后构造形成的原串只能是 21,原串指针后移一位
                    sb.append("1");
                    f[i] = cnt; i++;
                } else {
                    // 当原串当前字符是 2,而计数串当前字符为 2
                    // 往后构造形成的原串只能是 221,原串指针后移两位
                    sb.append("21");
                    f[i] = f[i + 1] = cnt; i += 2;
                }
            }
        }
    }
    public int magicalString(int n) {
        return f[n];
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    static const int N = 100010;
    static vector<int> f;
    Solution() {
        if(!f.empty()) return;
        f.resize(N);
        string sb = "01"; // 首位多加一个 0 作为哨兵
        for (int i = 1, j = 1, cnt = 0; i < N; j++) {
            int last = sb[sb.size() - 1] - '0', t = sb[j] - '0';
            if (last == 1) {
                if (t == 1) {
                    sb += '2';
                    f[i++] = ++cnt;
                } else {
                    sb += "12";
                    f[i++] = ++cnt; f[i++] = ++cnt;
                }
            } else {
                if (t == 1) {
                    sb += '1';
                    f[i++] = cnt;
                } else {
                    sb += "21";
                    f[i++] = f[i++] = cnt;
                }
            }
        }
    }
    int magicalString(int n) {
        return f[n];
    }
};
vector<int> Solution::f = {};
```
Python 代码:
```Python
class Solution:
    def magicalString(self, n: int) -> int:
        ss = '01' # 首位多加一个 0 作为哨兵
        i, j, cnt = 1, 1, 0
        f = [0] * (n + 10)
        while i <= n:
            last, t = ss[i], ss[j]
            if last == '1':
                if t == '1':
                    # 当原串当前字符是 1,而计数串当前字符为 1
                    # 往后构造形成的原串只能是 12,原串指针后移一位
                    ss += '2'
                    f[i], cnt, i = cnt + 1, cnt + 1, i + 1
                else:
                    # 当原串当前字符是 1,而计数串当前字符为 2
                    # 往后构造形成的原串只能是 112,此时同步更新 f[i + 1],原串指针后移两位
                    ss += '12'
                    f[i], f[i + 1], cnt, i = cnt + 1, cnt + 2, cnt + 2, i + 2
            else:
                if t == '1':
                    # 当原串当前字符是 2,而计数串当前字符为 1
                    # 往后构造形成的原串只能是 21,原串指针后移一位
                    ss += '1'
                    f[i], i = cnt, i + 1
                else:
                    # 当原串当前字符是 2,而计数串当前字符为 2
                    # 往后构造形成的原串只能是 221,原串指针后移两位
                    ss += '21'
                    f[i], f[i + 1], i = cnt, cnt, i + 2
            j += 1
        return f[n]
```
TypeScript 代码:
```TypeScript
function magicalString(n: number): number {
    let str = '01' // 首位多加一个 0 作为哨兵
    const f = new Array<number>(n + 10).fill(0)
    for (let i = 1, j = 1, cnt = 0; i <= n; j++) {
        const last = str[str.length - 1], t = str[j]
        if (last == '1') {
            if (t == '1') {
                // 当原串当前字符是 1,而计数串当前字符为 1
                // 往后构造形成的原串只能是 12,原串指针后移一位
                str += '2'
                f[i] = ++cnt; i++
            } else {
                // 当原串当前字符是 1,而计数串当前字符为 2
                // 往后构造形成的原串只能是 112,此时同步更新 f[i + 1],原串指针后移两位
                str += '12'
                f[i] = ++cnt; f[i + 1] = ++cnt; i += 2
            }
        } else {
            if (t == '1') {
                // 当原串当前字符是 2,而计数串当前字符为 1
                // 往后构造形成的原串只能是 21,原串指针后移一位
                str += '1'
                f[i] = cnt; i++
            } else {
                // 当原串当前字符是 2,而计数串当前字符为 2
                // 往后构造形成的原串只能是 221,原串指针后移两位
                str += '21'
                f[i] = f[i + 1] = cnt; i += 2
            }
        }
    }
    return f[n]
}
```
* 时间复杂度:$O(n)$,若将 `static` 打表逻辑放到本地进行,能够减少构造的计算量,但仍会有创建答案数组的 $O(n)$ 开销,因此为均摊 $O(1)$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


Question:
有一个密钥字符串 `S`,只包含字母,数字以及 `'-'`(破折号)。其中,`N` 个 `'-'` 将字符串分成了 `N+1` 组。

给你一个数字 `K`,请你重新格式化字符串,使每个分组恰好包含 `K` 个字符。特别地,第一个分组包含的字符个数必须小于等于 `K`,但至少要包含 `1` 个字符。两个分组之间需要用 `'-'`(破折号)隔开,并且将所有的小写字母转换为大写字母。

给定非空字符串 `S` 和数字 `K`,按照上面描述的规则进行格式化。

示例 1:
```
输入:S = "5F3Z-2e-9-w", K = 4

输出:"5F3Z-2E9W"

解释:字符串 S 被分成了两个部分,每部分 4 个字符;
     注意,两个额外的破折号需要删掉。
```
示例 2:
```
输入:S = "2-5g-3-J", K = 2

输出:"2-5G-3J"

解释:字符串 S 被分成了 3 个部分,按照前面的规则描述,第一部分的字符可以少于给定的数量,其余部分皆为 2 个字符。
```

提示:
* S 的长度可能很长,请按需分配大小。K 为正整数。
* S 只包含字母数字(a-z,A-Z,0-9)以及破折号'-'
* S 非空

Solution:
### 模拟

简单字符串模拟,从后往前处理,避免对首个分区的分情况讨论和取余操作。

代码:
```Java
class Solution {
    public String licenseKeyFormatting(String s, int k) {
        StringBuilder sb = new StringBuilder();
        for (int i = s.length() - 1, cnt = 0; i >= 0; i--) {
            if (s.charAt(i) == '-') continue;
            if (cnt == k && (cnt = 0) >= 0) sb.append("-");
            sb.append(s.charAt(i));
            cnt++;
        }
        return sb.reverse().toString().toUpperCase();
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


Question:
对于给定的整数 n, 如果n的k(k>=2)进制数的所有数位全为1,则称 k(k>=2)是 n 的一个好进制。

以字符串的形式给出 n, 以字符串的形式返回 n 的最小好进制。

示例 1:
```
输入:"13"
输出:"3"
解释:13 的 3 进制是 111。
```
示例 2:
```
输入:"4681"
输出:"8"
解释:4681 的 8 进制是 11111。
```
示例 3:
```
输入:"1000000000000000000"
输出:"999999999999999999"
解释:1000000000000000000 的 999999999999999999 进制是 11。
```

提示:
* n的取值范围是 [3, $10^{18}$]。
* 输入总是有效且没有前导 0。

Solution:
### 基本分析

设 $(n)_{10}$ 的 $k$ 进制表示共有 $Len$ 位,那么根据「进制转换」相关知识,必然有如下等式:

$$
(n)_{10} = (11...11)_{k} = k^0 + k^1 + k^2 + ... + k^{Len - 1}
$$

当 $n$ 给定的情况下,$k$ 随着 $Len$ 减小而增大,由此我们可以分析出 $k$ 的上界:

* 当 $Len$ 取 $1$ 的时候:$k^0 = n$,即 $n = 1$,与题目给定的数据范围冲突,不可取;
* 当 $Len$ 取 $2$ 的时候:$k^0 + k^1 = n$,即 $k = n - 1$,为合法值。

**因此 $k$ 的上界为 $n - 1$,同时我们知道长度 $Len$ 满足等式 $Len \geq 2$。**

然后我们再分析一下 $Len$ 的上界。

**根据 $k$ 与 $Len$ 大小变化关系,同时已知 $k \geq 2$,不难分析当 $k$ 取最小值 $2$ 的时候,$Len$ 可得最大值(同时 $n$ 的最大值为 $10^{18}$),可分析出 $Len \leq \lceil \log_2{n} \rceil$,$\log_2{10^{18}}$ 不超过 $60$。**

因此可以采取枚举 $Len$ 的做法。

---

### 枚举 $Len$

根据分析,我们可以在 $[2, 60]$ 范围内从大到小枚举 $Len$,当取得第一位合法的 $Len$ 时,$k$ 为最小合法值。

剩下的问题在于如何在给定 $Len$ 的情况下,求得 $k$ 为多少。

前面分析到 $Len \geq 2$,令 $s = Len - 1$,再根据 [二项式定理](https://baike.baidu.com/item/%E4%BA%8C%E9%A1%B9%E5%BC%8F%E5%AE%9A%E7%90%86) 可得:

$$
n = k^0 + k^1 + k^2 + ... + k^s < C_{s}^{0} * k^0 + C_{s}^{1} * k^1 + ... + C_{s}^{s} * k^s = (k + 1)^{s}
$$

同时结合 $n > k^ s$,可得 $k^s < n < (k + 1)^s$,整理后可得:

$$
k < n^{\frac{1}{s}} < k + 1
$$

因此,对于任意的 $s = Len - 1$ 都有唯一的解为 $n^{\frac{1}{s}}$(正整数),我们只需要验证 $n^{\frac{1}{s}}$ 是否为正整数即可。

一些细节:实现上为了方便,不处理 $k = n - 1$ 的边界问题,我们可以调整枚举下界为 $3$,当枚举不出合法 $k$ 时,直接返回 $n - 1$ 作为答案。

代码:
```Java
class Solution {
    public String smallestGoodBase(String n) {
        long m = Long.parseLong(n);
        int max = (int)(Math.log(m) / Math.log(2) + 1);
        for (int len = max; len >= 3; len--) {
            long k = (long)Math.pow(m, 1.0 / (len - 1));
            long res = 0;
            for (int i = 0; i < len; i++) res = res * k + 1;
            if (res == m) return String.valueOf(k);
        }
        return String.valueOf(m - 1);
    }
}
```
* 时间复杂度:$O(\log^2{n})$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


485. 最大连续 1 的个数

Question:
给定一个二进制数组, 计算其中最大连续 1 的个数。

示例:
```
输入:[1,1,0,1,1,1]
输出:3
解释:开头的两位和最后的三位都是连续 1 ,所以最大连续 1 的个数是 3.
```

提示:
* 输入的数组只包含 0 和 1 。
* 输入数组的长度是正整数,且不超过 10,000。

Solution:
### 双指针解法

使用 `i` 和 `j` 分别代表连续 1 的左右边界。

起始状态 `i == j`,当 `i` 到达第一个 1 的位置时,让 `j` 不断右移直到右边界。

更新 `ans`

```java
class Solution {
    public int findMaxConsecutiveOnes(int[] nums) {
        int n = nums.length;
        int ans = 0;
        for (int i = 0, j = 0; i < n; j = i) {
            if (nums[i] == 1) {
                while (j + 1 < n && nums[j + 1] == 1) j++;
                ans = Math.max(ans, j - i + 1);
                i = j + 1;
            } else {
                i++;
            }
        }
        return ans;
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


Question:
你正在参与祖玛游戏的一个变种。

在这个祖玛游戏变体中,桌面上有 一排 彩球,每个球的颜色可能是:红色 `'R'`、黄色 `'Y'`、蓝色 `'B'`、绿色 `'G'` 或白色 `'W'` 。你的手中也有一些彩球。

你的目标是 清空 桌面上所有的球。每一回合:

* 从你手上的彩球中选出 任意一颗 ,然后将其插入桌面上那一排球中:两球之间或这一排球的任一端。
* 接着,如果有出现 三个或者三个以上 且 颜色相同 的球相连的话,就把它们移除掉。
	* 如果这种移除操作同样导致出现三个或者三个以上且颜色相同的球相连,则可以继续移除这些球,直到不再满足移除条件。
* 如果桌面上所有球都被移除,则认为你赢得本场游戏。
* 重复这个过程,直到你赢了游戏或者手中没有更多的球。

给你一个字符串 board ,表示桌面上最开始的那排球。另给你一个字符串 hand ,表示手里的彩球。请你按上述操作步骤移除掉桌上所有球,计算并返回所需的 最少 球数。如果不能移除桌上所有的球,返回 -1 。

示例 1:
```
输入:board = "WRRBBW", hand = "RB"

输出:-1

解释:无法移除桌面上的所有球。可以得到的最好局面是:
- 插入一个 'R' ,使桌面变为 WRRRBBW 。WRRRBBW -> WBBW
- 插入一个 'B' ,使桌面变为 WBBBW 。WBBBW -> WW
桌面上还剩着球,没有其他球可以插入。
```
示例 2:
```
输入:board = "WWRRBBWW", hand = "WRBRW"

输出:2

解释:要想清空桌面上的球,可以按下述步骤:
- 插入一个 'R' ,使桌面变为 WWRRRBBWW 。WWRRRBBWW -> WWBBWW
- 插入一个 'B' ,使桌面变为 WWBBBWW 。WWBBBWW -> WWWW -> empty
只需从手中出 2 个球就可以清空桌面。
```
示例 3:
```
输入:board = "G", hand = "GGGGG"

输出:2

解释:要想清空桌面上的球,可以按下述步骤:
- 插入一个 'G' ,使桌面变为 GG 。
- 插入一个 'G' ,使桌面变为 GGG 。GGG -> empty
只需从手中出 2 个球就可以清空桌面。
```
示例 4:
```
输入:board = "RBYYBBRRB", hand = "YRBGB"

输出:3

解释:要想清空桌面上的球,可以按下述步骤:
- 插入一个 'Y' ,使桌面变为 RBYYYBBRRB 。RBYYYBBRRB -> RBBBRRB -> RRRB -> B
- 插入一个 'B' ,使桌面变为 BB 。
- 插入一个 'B' ,使桌面变为 BBB 。BBB -> empty
只需从手中出 3 个球就可以清空桌面。
```

提示:
* 1 <= board.length <= 16
* 1 <= hand.length <= 5
* board 和 hand 由字符 `'R'`、`'Y'`、`'B'`、`'G'` 和 `'W'` 组成
* 桌面上一开始的球中,不会有三个及三个以上颜色相同且连着的球

Solution:
### 搜索 + 剪枝

数据范围 $1 <= board.length <= 16$ 和 $1 <= hand.length <= 5$。

为了方便,我们使用 $a$ 和 $b$ 来代指 $board$ 和 $hand$。

但在爆搜过程中同时维持两个字符串构造会超时,考虑使用一个 `int` 来记录 $hand$ 的使用情况。

代码:
```Java
class Solution {
    int INF = 0x3f3f3f3f;
    String b;
    int m;
    Map<String, Integer> map = new HashMap<>();
    public int findMinStep(String a, String _b) {
        b = _b;
        m = b.length();
        int ans = dfs(a, 1 << m);
        return ans == INF ? -1 : ans;
    }
    int dfs(String a, int cur) {
        if (a.length() == 0) return 0;
        String hashKey = a + "_" + cur;
        if (map.containsKey(hashKey)) return map.get(hashKey);
        int ans = INF;
        int n = a.length();
        for (int i = 0; i < m; i++) {
            if (((cur >> i) & 1) == 1) continue;
            int next = (1 << i) | cur;
            for (int j = 0; j <= n; j++) {
                boolean ok = false;
                if (j > 0 && j < n && a.charAt(j) == a.charAt(j - 1) && a.charAt(j - 1) != b.charAt(i)) ok = true;
                if (j < n && a.charAt(j) == b.charAt(i)) ok = true;
                if (!ok) continue;
                StringBuilder sb = new StringBuilder();
                sb.append(a.substring(0, j)).append(b.substring(i, i + 1));
                if (j != n) sb.append(a.substring(j));
                int k = j;
                while (0 <= k && k < sb.length()) {
                    char c = sb.charAt(k);
                    int l = k, r = k;
                    while (l >= 0 && sb.charAt(l) == c) l--;
                    while (r < sb.length() && sb.charAt(r) == c) r++;
                    if (r - l - 1 >= 3) {
                        sb.delete(l + 1, r);
                        k = l >= 0 ? l : r;
                    } else {
                        break;
                    }
                }
                ans = Math.min(ans, dfs(sb.toString(), next) + 1);
            }
        }
        map.put(hashKey, ans);
        return ans;
    }
}
```
* 时间复杂度:略。「爆搜」同时还得考虑「剪枝」的复杂度分析意义不大。
* 空间复杂度:略

---

### AStar 算法

我们建立一个类 `Node` 来代指当前搜索局面。

```Java
class Node {
    // 当前的棋盘状况
    String a;
    // cur 代表当前 hand 的使用情况(若 cur 二进制表示中的第 k 位为 1,代表 hand 的第 k 个彩球已被使用)
    // val 代表「当前棋盘为 a」和「hand 使用情况为 cur」的情况下,至少还需要多少步才能将 a 全部消掉(启发式估算值)
    // step 代表当前局面是经过多少步而来
    int cur, val, step;
    Node (String _a, int _c, int _v, int _s) {
        a = _a;
        cur = _c; val = _v; step = _s;
    }
}
```

显然,直接对此进行 `BFS`,会 TLE。

我们考虑将优化 `BFS` 中使用到的队列改为优先队列:**更接近答案的局面先出队进行局面延展。**

然后我们考虑如何设计 AStar 的启发式函数。

**首先,一个合格的 AStar 启发式函数应当能够确保「估值不会小于理论最小距离」。同时由于启发式的估值函数是针对于最终状态进行估算,因此只确保最终状态的第一次出队时为最短路,其余中间状态的首次出队不一定是最短路,为此我们需要使用哈希表来记录中间状态的距离变化,如果某个局面的最短距离被更新,我们应当将其再次入队。**

基于此,我们设计如下的 AStar 的启发式函数:使用哈希表来统计「当前的棋盘 $a$ 的彩球数量」&「当前手上拥有的彩球数量」,对「无解情况」和「理论最小次数」进行分析:

 * 对于某个彩球 $c$ 而言,如果当前棋盘的数量 + 手上的数量 都不足 $3$ 个,那么该局面往下搜索也必然无解,该局面无须入队;
 * 对于某个彩球 $c$ 而言,如果当前棋盘数量少于 $3$ 个,那么至少需要补充至 $3$ 个才能被消除,而缺少的个数则是「从手上彩球放入棋盘内」的次数,即对于彩球 $c$,我们理论上至少需要消耗 $3 - cnt$ 次($cnt$ 为当前棋盘拥有的彩球 $c$ 的数量)。

需要注意的是:对于某个局面 $node$ 而言,最终的距离是由「已确定距离」+「估值距离」两部分组成,我们应当根据这两部分之和进行出队,才能确保算法的正确性。

代码:
```Java
class Solution {
    class Node {
        String a;
        int cur, val, step;
        Node (String _a, int _c, int _v, int _s) {
            a = _a;
            cur = _c; val = _v; step = _s;
        }
    }
    int f(String a, int k) {
        Map<Character, Integer> m1 = new HashMap<>(), m2 =  new HashMap<>();
        for (int i = 0; i < a.length(); i++) {
            m1.put(a.charAt(i), m1.getOrDefault(a.charAt(i), 0) + 1);
        }
        for (int i = 0; i < m; i++) {
            if (((k >> i) & 1) == 0) m2.put(b.charAt(i), m2.getOrDefault(b.charAt(i), 0) + 1);
        }
        int ans = 0;
        for (char c : m1.keySet()) {
            int c1 = m1.get(c), c2 = m2.getOrDefault(c, 0);
            if (c1 + c2 < 3) return INF;
            if (c1 < 3) ans += (3 - c1);
        }
        return ans;
    }

    int INF = 0x3f3f3f3f;
    String b;
    int m;
    Map<String, Integer> map = new HashMap<>();
    public int findMinStep(String _a, String _b) {
        b = _b;
        m = b.length();
        PriorityQueue<Node> q = new PriorityQueue<>((o1,o2)->(o1.val+o1.step)-(o2.val+o2.step));
        q.add(new Node(_a, 1 << m, f(_a, 1 << m), 0));
        map.put(_a + "_" + (1 << m), 0);
        while (!q.isEmpty()) {
            Node poll = q.poll();
            String a = poll.a;
            int cur = poll.cur;
            int step = poll.step;
            int n = a.length();
            for (int i = 0; i < m; i++) {
                if (((cur >> i) & 1) == 1) continue;
                int next = (1 << i) | cur;
                for (int j = 0; j <= n; j++) {
                    boolean ok = false;
                    if (j > 0 && j < n && a.charAt(j) == a.charAt(j - 1) && a.charAt(j - 1) != b.charAt(i)) ok = true;
                    if (j < n && a.charAt(j) == b.charAt(i)) ok = true;
                    if (!ok) continue;
                    StringBuilder sb = new StringBuilder();
                    sb.append(a.substring(0, j)).append(b.substring(i, i + 1));
                    if (j != n) sb.append(a.substring(j));
                    int k = j;
                    while (0 <= k && k < sb.length()) {
                        char c = sb.charAt(k);
                        int l = k, r = k;
                        while (l >= 0 && sb.charAt(l) == c) l--;
                        while (r < sb.length() && sb.charAt(r) == c) r++;
                        if (r - l - 1 >= 3) {
                            sb.delete(l + 1, r);
                            k = l >= 0 ? l : r;
                        } else {
                            break;
                        }
                    }
                    String nextStr = sb.toString();
                    if (nextStr.length() == 0) return step + 1;
                    if (f(nextStr, next) == INF) continue;
                    String hashKey = nextStr + "_" + next;
                    if (!map.containsKey(hashKey) || map.get(hashKey) > step + 1) {
                        map.put(hashKey, step + 1);
                        q.add(new Node(nextStr, next, f(nextStr, next), step + 1));
                    }
                }
            }
        }
        return -1;
    }
}
```
* 时间复杂度:略。「爆搜」同时还得考虑「启发式加速」的复杂度分析意义不大。
* 空间复杂度:略

### 最后

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

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

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

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


Question:
作为一位web开发者, 懂得怎样去规划一个页面的尺寸是很重要的。 现给定一个具体的矩形页面面积,你的任务是设计一个长度为 L 和宽度为 W 且满足以下要求的矩形的页面。要求:

1. 你设计的矩形页面必须等于给定的目标面积。

2. 宽度 W 不应大于长度 L,换言之,要求 L >= W 。

3. 长度 L 和宽度 W 之间的差距应当尽可能小。

你需要按顺序输出你设计的页面的长度 L 和宽度 W。

示例:
```
输入: 4

输出: [2, 2]

解释: 目标面积是 4, 所有可能的构造方案有 [1,4], [2,2], [4,1]。
但是根据要求2,[1,4] 不符合要求; 根据要求3,[2,2] 比 [4,1] 更能符合要求. 所以输出长度 L 为 2, 宽度 W 为 2。
```

说明:
1. 给定的面积不大于 10,000,000 且为正整数。
2. 你设计的页面的长度和宽度必须都是正整数。

Solution:
### 模拟

根据题意,从 $\sqrt{area}$ 开始往后模拟,遇到第一个能够被整除的数值,则返回该答案。

代码:
```Java
class Solution {
    public int[] constructRectangle(int area) {
        for (int i = (int)(Math.sqrt(area)); ;i--) {
            if (area % i == 0) return new int[]{area / i, i};
        }
    }
}
```
* 时间复杂度:$O(\sqrt{n})$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


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

向数组中的每个整数前添加 '+' 或 '-' ,然后串联起所有整数,可以构造一个 表达式 :

* 例如,nums = [2, 1] ,可以在 2 之前添加 '+' ,在 1 之前添加 '-' ,然后串联起来得到表达式 "+2-1" 。
返回可以通过上述方法构造的、运算结果等于 target 的不同 表达式 的数目。

示例 1:
```
输入:nums = [1,1,1,1,1], target = 3
输出:5
解释:一共有 5 种方法让最终目标和为 3 。
-1 + 1 + 1 + 1 + 1 = 3
+1 - 1 + 1 + 1 + 1 = 3
+1 + 1 - 1 + 1 + 1 = 3
+1 + 1 + 1 - 1 + 1 = 3
+1 + 1 + 1 + 1 - 1 = 3
```
示例 2:
```
输入:nums = [1], target = 1
输出:1
```

提示:
* 1 <= nums.length <= 20
* 0 <= nums[i] <= 1000
* 0 <= sum(nums[i]) <= 100
* -1000 <= target <= 100

Solution:
### DFS

数据范围只有 $20$,而且每个数据只有 $+/-$ 两种选择,因此可以直接使用 DFS 进行「爆搜」。

而 DFS 有「使用全局变量维护」和「接收返回值处理」两种形式。

代码:
```Java
class Solution {
    public int findTargetSumWays(int[] nums, int t) {
        return dfs(nums, t, 0, 0);
    }
    int dfs(int[] nums, int t, int u, int cur) {
        if (u == nums.length) {
            return cur == t ? 1 : 0;
        }
        int left = dfs(nums, t, u + 1, cur + nums[u]);
        int right = dfs(nums, t, u + 1, cur - nums[u]);
        return left + right;
    }
}
```

```Java
class Solution {
    int ans = 0;
    public int findTargetSumWays(int[] nums, int t) {
        dfs(nums, t, 0, 0);
        return ans;
    }
    void dfs(int[] nums, int t, int u, int cur) {
        if (u == nums.length) {
            ans += cur == t ? 1 : 0;
            return;
        }
        dfs(nums, t, u + 1, cur + nums[u]);
        dfs(nums, t, u + 1, cur - nums[u]);
    }
}
```
* 时间复杂度:$O(2^n)$
* 空间复杂度:忽略递归带来的额外空间消耗。复杂度为 $O(1)$

---

### 记忆化搜索

不难发现,在 DFS 的函数签名中只有「数值下标 `u`」和「当前结算结果 `cur`」为可变参数,考虑将其作为记忆化容器的两个维度,返回值作为记忆化容器的记录值。

由于 `cur` 存在负权值,为了方便,我们这里不设计成静态数组,而是使用「哈希表」进行记录。

以上分析都在 [(题解)403. 青蛙过河](https://leetcode-cn.com/problems/frog-jump/solution/gong-shui-san-xie-yi-ti-duo-jie-jiang-di-74fw/) 完整讲过。

代码:
```Java
class Solution {
    public int findTargetSumWays(int[] nums, int t) {
        return dfs(nums, t, 0, 0);
    }
    Map<String, Integer> cache = new HashMap<>();
    int dfs(int[] nums, int t, int u, int cur) {
        String key = u + "_" + cur;
        if (cache.containsKey(key)) return cache.get(key);
        if (u == nums.length) {
            cache.put(key, cur == t ? 1 : 0);
            return cache.get(key);
        }
        int left = dfs(nums, t, u + 1, cur + nums[u]);
        int right = dfs(nums, t, u + 1, cur - nums[u]);
        cache.put(key, left + right);
        return cache.get(key);
    }
}
```
* 时间复杂度:$O(n * \sum_{i = 0}^{n - 1} abs(nums[i]))$
* 空间复杂度:忽略递归带来的额外空间消耗。复杂度为 $O(n * \sum_{i = 0}^{n - 1} abs(nums[i]))$

---

### 动态规划

能够以「递归」的形式实现动态规划(记忆化搜索),自然也能使用「递推」的方式进行实现。

根据记忆化搜索的分析,我们可以定义:

**$f[i][j]$ 代表考虑前 $i$ 个数,当前计算结果为 $j$ 的方案数,令 `nums` 下标从 $1$ 开始。**

那么 $f[n][target]$ 为最终答案,$f[0][0] = 1$ 为初始条件:代表不考虑任何数,凑出计算结果为 $0$ 的方案数为 $1$ 种。

根据每个数值只能搭配 $+/-$ 使用,可得状态转移方程:

$$f[i][j] = f[i - 1][j - nums[i - 1]] + f[i - 1][j + nums[i - 1]]$$

到这里,既有了「状态定义」和「转移方程」,又有了可以滚动下去的「有效值」(起始条件)。

距离我们完成所有分析还差最后一步。

当使用递推形式时,我们通常会使用「静态数组」来存储动规值,因此还需要考虑维度范围的:

* 第一维为物品数量:范围为 `nums` 数组长度
* 第二维为中间结果:令 `s` 为所有 `nums` 元素的总和(题目给定了 `nums[i]` 为非负数的条件,否则需要对 `nums[i]` 取绝对值再累加),那么中间结果的范围为 $[-s, s]$

因此,我们可以确定动规数组的大小。**同时在转移时,对第二维度的使用做一个 `s` 的右偏移,以确保「负权值」也能够被合理计算/存储。**

代码:
```Java
class Solution {
    public int findTargetSumWays(int[] nums, int t) {
        int n = nums.length;
        int s = 0;
        for (int i : nums) s += Math.abs(i);
        if (Math.abs(t) > s) return 0;
        int[][] f = new int[n + 1][2 * s + 1];
        f[0][0 + s] = 1;
        for (int i = 1; i <= n; i++) {
            int x = nums[i - 1];
            for (int j = -s; j <= s; j++) {
                if ((j - x) + s >= 0) f[i][j + s] += f[i - 1][(j - x) + s];
                if ((j + x) + s <= 2 * s) f[i][j + s] += f[i - 1][(j + x) + s];
            }
        }
        return f[n][t + s];
    }
}
```
* 时间复杂度:$O(n * \sum_{i = 0}^{n - 1} abs(nums[i]))$
* 空间复杂度:$O(n * \sum_{i = 0}^{n - 1} abs(nums[i]))$

---

### 动态规划(优化)

**在上述「动态规划」分析中,我们总是尝试将所有的状态值都计算出来,当中包含很多对「目标状态」不可达的“额外”状态值。**

即达成某些状态后,不可能再回到我们的「目标状态」。

例如当我们的 $target$ 不为 $-s$ 和 $s$ 时,$-s$ 和 $s$ 就是两个对「目标状态」不可达的“额外”状态值,到达 $-s$ 或 $s$ 已经使用所有数值,对 $target$ 不可达。

那么我们如何规避掉这些“额外”状态值呢?

我们可以从哪些数值使用哪种符号来分析,即划分为「负值部分」&「非负值部分」,令「负值部分」的绝对值总和为 $m$,即可得:

$$(s - m) - m = s - 2 * m = target$$

变形得:

$$m = \frac{s - target}{2}$$

问题转换为:**只使用 $+$ 运算符,从 `nums` 凑出 $m$ 的方案数。**

**这样「原问题的具体方案」和「转换问题的具体方案」具有一一对应关系:「转换问题」中凑出来的数值部分在实际计算中应用 $-$,剩余部分应用 $+$,从而实现凑出来原问题的 $target$ 值。**

另外,由于 `nums` 均为非负整数,因此我们需要确保 $s - target$ 能够被 $2$ 整除。

同时,由于问题转换为 **从 `nums` 中凑出 $m$ 的方案数,因此「状态定义」和「状态转移」都需要进行调整(01 背包求方案数):**

**定义 $f[i][j]$ 为从 `nums` 凑出总和「恰好」为 $j$ 的方案数。**

最终答案为 $f[n][m]$,$f[0][0] = 1$ 为起始条件:代表不考虑任何数,凑出计算结果为 $0$ 的方案数为 $1$ 种。

每个数值有「选」和「不选」两种决策,转移方程为:

$$f[i][j] = f[i - 1][j] + f[i - 1][j - nums[i - 1]]$$

代码:
```Java
class Solution {
    public int findTargetSumWays(int[] nums, int t) {
        int n = nums.length;
        int s = 0;
        for (int i : nums) s += Math.abs(i);
        if (t > s || (s - t) % 2 != 0) return 0;
        int m = (s - t) / 2;
        int[][] f = new int[n + 1][m + 1];
        f[0][0] = 1;
        for (int i = 1; i <= n; i++) {
            int x = nums[i - 1];
            for (int j = 0; j <= m; j++) {
                f[i][j] += f[i - 1][j];
                if (j >= x) f[i][j] += f[i - 1][j - x];
            }
        }
        return f[n][m];
    }
}
```
* 时间复杂度:$O(n * (\sum_{i = 0}^{n - 1} abs(nums[i]) - target))$
* 空间复杂度:$O(n * (\sum_{i = 0}^{n - 1} abs(nums[i]) - target))$

### 最后

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

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

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

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


Question:
在《英雄联盟》的世界中,有一个叫 “提莫” 的英雄。他的攻击可以让敌方英雄艾希(编者注:寒冰射手)进入中毒状态。

当提莫攻击艾希,艾希的中毒状态正好持续 `duration` 秒。

正式地讲,提莫在 `t` 发起发起攻击意味着艾希在时间区间 `[t, t + duration - 1]`(含 `t` 和 `t + duration - 1`)处于中毒状态。如果提莫在中毒影响结束 前 再次攻击,中毒状态计时器将会 重置 ,在新的攻击之后,中毒影响将会在 `duration` 秒后结束。

给你一个 非递减 的整数数组 `timeSeries`,其中 `timeSeries[i]` 表示提莫在 `timeSeries[i]` 秒时对艾希发起攻击,以及一个表示中毒持续时间的整数 `duration`。

返回艾希处于中毒状态的  **总** 秒数。

示例 1:
```
输入:timeSeries = [1,4], duration = 2

输出:4

解释:提莫攻击对艾希的影响如下:
- 第 1 秒,提莫攻击艾希并使其立即中毒。中毒状态会维持 2 秒,即第 1 秒和第 2 秒。
- 第 4 秒,提莫再次攻击艾希,艾希中毒状态又持续 2 秒,即第 4 秒和第 5 秒。
艾希在第 1、2、4、5 秒处于中毒状态,所以总中毒秒数是 4 。
```
示例 2:
```
输入:timeSeries = [1,2], duration = 2

输出:3

解释:提莫攻击对艾希的影响如下:
- 第 1 秒,提莫攻击艾希并使其立即中毒。中毒状态会维持 2 秒,即第 1 秒和第 2 秒。
- 第 2 秒,提莫再次攻击艾希,并重置中毒计时器,艾希中毒状态需要持续 2 秒,即第 2 秒和第 3 秒。
艾希在第 1、2、3 秒处于中毒状态,所以总中毒秒数是 3 。
```

提示:
* $1 <= timeSeries.length <= 10^4$
* $0 <= timeSeries[i], duration <= 10^7$
* timeSeries 按 非递减 顺序排列

Solution:
### 模拟

题目已确保 $timeSeries$ 为非递减排序,按照顺序进行遍历处理即可。

我们使用 $ans$ 统计答案,使用 $last$ 记录上一次攻击的结束点,对于任意的 $timeSeries[i]$ 而言,假设其发起点为 $s = timeSeries[i]$,结束点为 $e = s + duration - 1$,针对 $last$ 和 $s$ 进行分情况讨论即可:

* $last < s$ :两次攻击不重合,则有 $ans += duration; last = e;$
* $last >= s$ :两次攻击重合,则有 $ans += e - last; last = e$

>注意:$last$ 作为上次的结束点,在处理 $timeSeries[i]$ 时,$last$ 是一个「已被统计」的存在,因此我们需要将其初始化为 $-1$(使用一个比 $0$ 小的数值作为哨兵),以确保当 $timeSeries[0] = 0$ 时,第 $0$ 秒能够被计数。

代码:
```Java
class Solution {
    public int findPoisonedDuration(int[] timeSeries, int duration) {
        int ans = 0, last = -1;
        for (int s : timeSeries) {
            int e = s + duration - 1;
            ans += last < s ? duration : e - last;
            last = e;
        }
        return ans;
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


496. 下一个更大元素 I

Question:
给你两个 没有重复元素 的数组 `nums1` 和 `nums2`,其中 `nums1` 是 `nums2` 的子集。

请你找出 `nums1` 中每个元素在 `nums2` 中的下一个比其大的值。

`nums1` 中数字 `x` 的下一个更大元素是指 `x` 在 `nums2` 中对应位置的右边的第一个比 `x` 大的元素。如果不存在,对应位置输出 $-1$ 。

示例 1:
```
输入: nums1 = [4,1,2], nums2 = [1,3,4,2].

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

解释:
对于 num1 中的数字 4 ,你无法在第二个数组中找到下一个更大的数字,因此输出 -1 。
对于 num1 中的数字 1 ,第二个数组中数字1右边的下一个较大数字是 3 。
对于 num1 中的数字 2 ,第二个数组中没有下一个更大的数字,因此输出 -1 。
```
示例 2:
```
输入: nums1 = [2,4], nums2 = [1,2,3,4].

输出: [3,-1]

解释:
对于 num1 中的数字 2 ,第二个数组中的下一个较大数字是 3 。
对于 num1 中的数字 4 ,第二个数组中没有下一个更大的数字,因此输出 -1 。
```

提示:
* 1 <= nums1.length <= nums2.length <= 1000
* 0 <= nums1[i], nums2[i] <= $10^4$
* nums1和nums2中所有整数 互不相同
* nums1 中的所有整数同样出现在 nums2 中

Solution:
### 模拟

一个朴素的做法是直接根据题意进行模拟,对于每个 $ans[i]$ 而言,先找到 $nums1[i]$ 在 $nums2$ 的位置 $j$,然后接着往后找到最近一个比其大的数,如果 $j$ 走到结尾尚未出现合法的 $ans[i]$,则是 $-1$。

代码:
```Java
class Solution {
    public int[] nextGreaterElement(int[] nums1, int[] nums2) {
        int n = nums1.length, m = nums2.length;
        int[] ans = new int[n];
        for (int i = 0; i < n; i++) {
            int j = 0;
            while (j < m && nums1[i] != nums2[j]) j++;
            while (j < m && nums1[i] >= nums2[j]) j++;
            ans[i] = j < m ? nums2[j] : -1;
        }
        return ans;
    }
}
```
* 时间复杂度:$O(n * m)$
* 空间复杂度:$O(n)$

---

### 单调栈

当题目出现「找到最近一个比其大的元素」的字眼时,自然会想到「单调栈」。

具体的,由于我们目标是找到某个数其在 $nums2$ 的右边中第一个比其大的数,因此我们可以对 $nums2$ 进行逆序遍历。

我们在遍历 $nums2$ 时,实时维护一个单调栈,当我们遍历到元素 $nums2[i]$ 时,可以先将栈顶中比 $nums2[i]$ 小的元素出栈,最终结果有两种可能:

1. 栈为空,说明 $nums2[i]$ 之前(右边)没有比其大的数;

2. 栈不为空, 此时栈顶元素为 $nums2[i]$ 在 $nums2$ 中(右边)最近的比其大的数。

再利用数组中数值各不相同,在遍历 $nums2$ 的同时,使用哈希表记录每个 $nums2[i]$ 对应目标值是多少即可。

代码:
```Java
class Solution {
    public int[] nextGreaterElement(int[] nums1, int[] nums2) {
        int n = nums1.length, m = nums2.length;
        Deque<Integer> d = new ArrayDeque<>();
        Map<Integer, Integer> map = new HashMap<>();
        for (int i = m - 1; i >= 0; i--) {
            int x = nums2[i];
            while (!d.isEmpty() && d.peekLast() <= x) d.pollLast();
            map.put(x, d.isEmpty() ? -1 : d.peekLast());
            d.addLast(x);
        }
        int[] ans = new int[n];
        for (int i = 0; i < n; i++) ans[i] = map.get(nums1[i]);
        return ans;
    }
}
```
* 时间复杂度:维护单调栈,每个元素最多入栈出栈一次,复杂度为 $O(m)$;构造答案复杂度为 $O(n)$。整体复杂度为 $O(n + m)$
* 空间复杂度:$O(m)$

### 最后

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

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

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

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


497. 非重叠矩形中的随机点

Question:
给定一个由非重叠的轴对齐矩形的数组 `rects`,其中 $rects[i] = [a_i, b_i, x_i, y_i]$ 表示 $(a_i, b_i)$ 是第 $i$ 个矩形的左下角点,$(x_i, y_i)$ 是第 $i$ 个矩形的右上角点。

设计一个算法来随机挑选一个被某一矩形覆盖的整数点。矩形周长上的点也算做是被矩形覆盖。所有满足要求的点必须等概率被返回。

在给定的矩形覆盖的空间内的任何整数点都有可能被返回。

请注意 ,整数点是具有整数坐标的点。

实现 `Solution` 类:
* `Solution(int[][] rects)` 用给定的矩形数组 `rects` 初始化对象。
* `int[] pick()` 返回一个随机的整数点 $[u, v]$ 在给定的矩形所覆盖的空间内。

示例 1:

```
输入:
["Solution", "pick", "pick", "pick", "pick", "pick"]
[[[[-2, -2, 1, 1], [2, 2, 4, 6]]], [], [], [], [], []]

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

解释:
Solution solution = new Solution([[-2, -2, 1, 1], [2, 2, 4, 6]]);
solution.pick(); // 返回 [1, -2]
solution.pick(); // 返回 [1, -1]
solution.pick(); // 返回 [-1, -2]
solution.pick(); // 返回 [-2, -2]
solution.pick(); // 返回 [0, 0]
```

提示:
* $1 <= rects.length <= 100$
* $rects[i].length == 4$
* $-10^9 <= a_i < x_i <= 10^9$
* $-10^9 <= b_i < y_i <= 10^9$
* $x_i - a_i <= 2000$
* $y_i - b_i <= 2000$
* 所有的矩形不重叠。
* `pick` 最多被调用 $10^4$ 次。

Solution:
### 前缀和 + 二分

为了方便,我们使用 `rs` 来代指 `rects`,定义某个矩阵内整数点的数量为「面积」。

一个朴素的想法是「先随机使用哪个矩形,再随机该矩形内的点」,其中后者是极其容易的,根据矩形特质,只需在该矩形的 `XY` 坐标范围内随机即可确保等概率,而前者(随机使用哪个矩形)为了确保是等概率,我们不能简单随机坐标,而需要结合面积来做。

具体的,我们可以预处理前缀和数组 `sum`(前缀和数组下标默认从 $1$ 开始),其中 $sum[i]$ 代表前 $i$ 个矩形的面积之和(即下标范围 $[0, i - 1]$ 的面积总和),最终 $sum[n]$ 为所有矩形的总面积,我们可以在 $[1, sum[n]]$ 范围内随机,假定随机到的值为 $val$,然后利用 `sum` 数组的具有单调性,进行「二分」,找到 $val$ 所在的矩形(每个矩形均会贡献面积,可看做是每个矩形在数轴 $[1, sum[n]]$ 内贡献一段长度为面积的连续段,我们二分是为了找到点 $val$ 所在的连续段是由哪个矩形所贡献),然后在该矩形中进行随机,得到最终的随机点。

代码:
```Java
class Solution {
    int[][] rs;
    int[] sum;
    int n;
    Random random = new Random();
    public Solution(int[][] rects) {
        rs = rects;
        n = rs.length;
        sum = new int[n + 1];
        for (int i = 1; i <= n; i++) sum[i] = sum[i - 1] + (rs[i - 1][2] - rs[i - 1][0] + 1) * (rs[i - 1][3] - rs[i - 1][1] + 1);
    }
    public int[] pick() {
        int val = random.nextInt(sum[n]) + 1;
        int l = 0, r = n;
        while (l < r) {
            int mid = l + r >> 1;
            if (sum[mid] >= val) r = mid;
            else l = mid + 1;
        }
        int[] cur = rs[r - 1];
        int x = random.nextInt(cur[2] - cur[0] + 1) + cur[0], y = random.nextInt(cur[3] - cur[1] + 1) + cur[1];
        return new int[]{x, y};
    }
}
```
* 时间复杂度:令 $n$ 为给定的 `rs` 数组长度。初始化 `Solution` 时需要预处理前缀和数组,复杂度为 $O(n)$;每次 `pick` 时需要在矩形个数 $n$ 范围内进行二分,复杂度为 $O(\log{n})$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


Question:
给你一个大小为 `m x n` 的矩阵 `mat`,请以对角线遍历的顺序,用一个数组返回这个矩阵中的所有元素。

示例 1:

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

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

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

提示:
* $m == mat.length$
* $n == mat[i].length$
* $1 <= m, n <= 10^4$
* $1 <= m \times n <= 10^4$
* $-10^5 <= mat[i][j] <= 10^5$

Solution:
### 模拟

根据题意进行模拟即可。

为了方便,令 `mat` 为 `g`,记 `g` 的行和宽分别为 $n$ 和 $m$。当前所在位置为 $(x, y)$,遍历方向使用 $dir$ 代指(当 $dir = 1$ 代表往右上方进行遍历,当 $dir = -1$ 代表往左下方进行遍历),使用 $idx$ 记录当前处理到的答案下标。

每次除了将当前格子放入答案(`ans[idx++]=g[x][y]`)以外,还需要结合 $dir$ 找到当前位置的右上方格子 $(x - 1, y + 1)$ 或是左下方格子 $(x + 1, y - 1)$,若下一目标位置「越界」并且还没搜索完整个矩阵,我们需要根据优先级来找「下一个发起点」的位置,并且翻转遍历方向。

具体的找「下一个发起点」的优先级为:

* 若当前遍历方向为往右上角,即 $dir = 1$,优先找 $(x, y + 1)$ 作为下一发起点,若越界,则找 $(x + 1, y)$ 作为下一发起点;
* 若当前遍历方向为往左下角,即 $dir = -1$,优先找 $(x + 1, y)$ 作为下一发起点,若越界,则找 $(x, y + 1)$ 作为下一发起点。

代码:
```Java
class Solution {
    public int[] findDiagonalOrder(int[][] g) {
        int n = g.length, m = g[0].length, cnt = n * m;
        int[] ans = new int[cnt];
        int x = 0, y = 0, dir = 1, idx = 0;
        while (idx != cnt) {
            ans[idx++] = g[x][y];
            int nx = x, ny = y;
            if (dir == 1) {
                nx = x - 1; ny = y + 1;
            } else {
                nx = x + 1; ny = y - 1;
            }
            if (idx < cnt && (nx < 0 || nx >= n || ny < 0 || ny >= m)) {
                if (dir == 1) {
                    nx = y + 1 < m ? x : x + 1;
                    ny = y + 1 < m ? y + 1 : y;
                } else {
                    nx = x + 1 < n ? x + 1 : x;
                    ny = x + 1 < n ? y : y + 1;
                }
                dir *= -1;
            }
            x = nx; y = ny;
        }
        return ans;
    }
}
```
* 时间复杂度:$O(n \times m)$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


Question:
给你一个字符串数组 `words`,只返回可以使用在 美式键盘 同一行的字母打印出来的单词。键盘如下图所示。

美式键盘 中:

* 第一行由字符 `"qwertyuiop"` 组成。
* 第二行由字符 `"asdfghjkl"` 组成。
* 第三行由字符 `"zxcvbnm"` 组成。

示例 1:
```
输入:words = ["Hello","Alaska","Dad","Peace"]

输出:["Alaska","Dad"]
```
示例 2:
```
输入:words = ["omk"]

输出:[]
```
示例 3:
```
输入:words = ["adsdf","sfd"]

输出:["adsdf","sfd"]
```

提示:
* 1 <= words.length <= 20
* 1 <= words[i].length <= 100
* words[i] 由英文字母(小写和大写字母)组成

Solution:
### 模拟

根据题意,进行模拟即可。

先将键盘上的三行字母进行打表分类,依次检查 $words$ 中的单词中的每个字符是否都属于同一编号,若属于同一编号,则将其单词加入答案。

代码:
```Java
class Solution {
    static String[] ss = new String[]{"qwertyuiop", "asdfghjkl", "zxcvbnm"};
    static int[] hash = new int[26];
    static {
        for (int i = 0; i < ss.length; i++) {
            for (char c : ss[i].toCharArray()) hash[c - 'a'] = i;
        }
    }
    public String[] findWords(String[] words) {
        List<String> list = new ArrayList<>();
        out:for (String w : words) {
            int t = -1;
            for (char c : w.toCharArray()) {
                c = Character.toLowerCase(c);
                if (t == -1) t = hash[c - 'a'];
                else if (t != hash[c - 'a']) continue out;
            }
            list.add(w);
        }
        return list.toArray(new String[list.size()]);
    }
}
```
* 时间复杂度:$O(\sum_{i = 0}^{n - 1} words[i].length)$
* 空间复杂度:`toCharArray` 会拷贝新数组,不使用 `toCharArray`,使用 `charAt` 的话,复杂度为 $O(C)$,$C$ 为常数,固定为 $26$;否则复杂度为 $O(\sum_{i = 0}^{n - 1} words[i].length)$

### 最后

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

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

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

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


Question:
假设 力扣(LeetCode)即将开始 IPO 。

为了以更高的价格将股票卖给风险投资公司,力扣 希望在 IPO 之前开展一些项目以增加其资本。

由于资源有限,它只能在 IPO 之前完成最多 k 个不同的项目。

帮助 力扣 设计完成最多 k 个不同项目后得到最大总资本的方式。

给你 n 个项目。对于每个项目 i ,它都有一个纯利润 profits[i] ,和启动该项目需要的最小资本 capital[i] 。

最初,你的资本为 w 。当你完成一个项目时,你将获得纯利润,且利润将被添加到你的总资本中。

总而言之,从给定项目中选择 最多 k 个不同项目的列表,以 最大化最终资本 ,并输出最终可获得的最多资本。

答案保证在 32 位有符号整数范围内。

示例 1:
```
输入:k = 2, w = 0, profits = [1,2,3], capital = [0,1,1]

输出:4

解释:
由于你的初始资本为 0,你仅可以从 0 号项目开始。
在完成后,你将获得 1 的利润,你的总资本将变为 1。
此时你可以选择开始 1 号或 2 号项目。
由于你最多可以选择两个项目,所以你需要完成 2 号项目以获得最大的资本。
因此,输出最后最大化的资本,为 0 + 1 + 3 = 4。
```
示例 2:
```
输入:k = 3, w = 0, profits = [1,2,3], capital = [0,1,2]

输出:6
```

提示:
* 1 <= k <= $10^5$
* 0 <= w <= $10^9$
* n == profits.length
* n == capital.length
* 1 <= n <= $10^5$
* 0 <= profits[i] <= $10^4$
* 0 <= capital[i] <= $10^9$

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

由于每完成一个任务都会使得总资金 `w` 增加或不变。因此对于所选的第 $i$ 个任务而言,应该在所有「未被选择」且启动资金不超过 `w` 的所有任务里面选利润最大的。

**可通过「归纳法」证明每次都在所有候选中选择利润最大的任务,可使得总资金最大。**

对于第 $i$ 次选择而言(当前所有的资金为 $w$),如果选择的任务利润为 $cur$,而实际可选的最大任务利润为 $max$( $cur <= max$ )。

将「选择 $cur$」调整为「选择 $max$」,结果不会变差:

1. 根据传递性,由 $cur <= max$ 可得 $w + cur <= w + max$,可推导出调整后的总资金不会变少;
2. 利用推论 $1$,由于总资金相比调整前没有变少,因此后面可选择的任务集合也不会变少。这意味着 **至少可以维持** 第 $i$ 次选择之后的所有原有选择。

至此,我们证明了将每次的选择调整为选择最大利润的任务,结果不会变差。

**当知道了「每次都应该在所有可选择的任务里选利润最大」的推论之后,再看看算法的具体流程。**

由于每完成一个任务总资金都会 增大/不变,因此所能覆盖的任务集合数量也随之 增加/不变 。

因此算法核心为「**每次决策前,将启动资金不超过当前总资金的任务加入集合,再在里面取利润最大的任务**」。

**「取最大」的过程可以使用优先队列(根据利润排序的大根堆),而「将启动资金不超过当前总资金的任务加入集合」的操作,可以利用总资金在整个处理过程递增,而先对所有任务进行预处理排序来实现。**

具体的,我们可以按照如下流程求解:

1. 根据 `profits` 和 `capital` 预处理出总的任务集合二元组,并根据「启动资金」进行升序排序;

2. 每次决策前,将所有的启动资金不超过 $w$ 的任务加入优先队列(根据利润排序的大根堆),然后从优先队列(根据利润排序的大根堆),将利润累加到 $w$;

3. 循环步骤 $2$,直到达到 $k$ 个任务,或者队列为空(当前资金不足以选任何任务)。

代码:

```Java
class Solution {
    public int findMaximizedCapital(int k, int w, int[] profits, int[] capital) {
        int n = profits.length;
        List<int[]> list = new ArrayList<>();
        for (int i = 0; i < n; i++) {
            list.add(new int[]{capital[i], profits[i]});
        }
        Collections.sort(list, (a,b)->a[0]-b[0]);
        PriorityQueue<Integer> q = new PriorityQueue<>((a,b)->b-a);
        int i = 0;
        while (k-- > 0) {
            while (i < n && list.get(i)[0] <= w) q.add(list.get(i++)[1]);
            if (q.isEmpty()) break;
            w += q.poll();
        }
        return w;
    }
}
```
* 时间复杂度:构造出二元组数组并排序的复杂度为 $O(n\log{n})$;大根堆最多有 $n$ 个元素,使用大根堆计算答案的复杂度为 $O(k\log{n})$。整体复杂度为 $O(\max(n\log{n}, k\log{n}))$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


503. 下一个更大元素 II

Question:
给定一个循环数组(最后一个元素的下一个元素是数组的第一个元素),输出每个元素的下一个更大元素。数字 x 的下一个更大的元素是按数组遍历顺序,这个数字之后的第一个比它更大的数,这意味着你应该循环地搜索它的下一个更大的数。如果不存在,则输出 -1。

示例 1:
```
输入: [1,2,1]
输出: [2,-1,2]
解释: 第一个 1 的下一个更大的数是 2;
数字 2 找不到下一个更大的数;
第二个 1 的下一个最大的数需要循环搜索,结果也是 2。
```

Solution:
### 单调栈解法

对于「找最近一个比当前值大/小」的问题,都可以使用单调栈来解决。

单调栈就是在栈的基础上维护一个栈内元素单调。

在理解单调栈之前,我们先回想一下「朴素解法」是如何解决这个问题的。

对于每个数而言,我们需要遍历其右边的数,直到找到比自身大的数,这是一个 $O(n^2)$ 的做法。

**之所以是 $O(n^2)$,是因为每次找下一个最大值,我们是通过「主动」遍历来实现的。**

而如果使用的是单调栈的话,可以做到 $O(n)$ 的复杂度,我们**将当前还没得到答案的下标暂存于栈内,从而实现「被动」更新答案。**

也就是说,栈内存放的永远是还没更新答案的下标。

具体的做法是:

每次将当前遍历到的下标存入栈内,**将当前下标存入栈内前,检查一下当前值是否能够作为栈内位置的答案(即成为栈内位置的「下一个更大的元素」),如果可以,则将栈内下标弹出。**

如此一来,我们便实现了「被动」更新答案,同时由于我们的弹栈和出栈逻辑,决定了我们**整个过程中栈内元素单调**。

还有一些编码细节,由于我们要找每一个元素的下一个更大的值,因此我们需要对原数组遍历两次,对遍历下标进行取余转换。

以及因为栈内存放的是还没更新答案的下标,可能会有位置会一直留在栈内(最大值的位置),因此我们要在处理前预设答案为 -1。而从实现那些没有下一个更大元素(不出栈)的位置的答案是 -1。

代码:
```java
class Solution {
    public int[] nextGreaterElements(int[] nums) {
        int n = nums.length;
        int[] ans = new int[n];
        Arrays.fill(ans, -1);
        Deque<Integer> d = new ArrayDeque<>();
        for (int i = 0; i < n * 2; i++) {
            while (!d.isEmpty() && nums[i % n] > nums[d.peekLast()]) {
                int u = d.pollLast();
                ans[u] = nums[i % n];
            }
            d.addLast(i % n);
        }
        return ans;
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n)$

***

### 卡常小技巧

本题不需要用到这个技巧,但是还是介绍一下,可作为拓展。

我们可以使用静态数组来模拟栈,这样我们的代码将会更快一点:

```java
class Solution {
    public int[] nextGreaterElements(int[] nums) {
        int n = nums.length;
        int[] ans = new int[n];
        Arrays.fill(ans, -1);
        // 使用数组模拟栈,hh 代表栈底,tt 代表栈顶
        int[] d = new int[n * 2];
        int hh = 0, tt = -1;
        for (int i = 0; i < n * 2; i++) {
            while (hh <= tt && nums[i % n] > nums[d[tt]]) {
                int u = d[tt--];
                ans[u] = nums[i % n];
            }
            d[++tt] = i % n;
        }
        return ans;
    }
}
```

***

### 总结

要从逻辑上去理解为什么能用「单调栈」解决问题:

1. 我们希望将 $O(n^2)$ 算法优化为 $O(n)$ 算法,因此需要将「主动」获取答案转换为「被动」更新
2. 我们需要使用数据结构保持那些「尚未更新」的位置下标,由于题目要求的是找「下一个更大的元素」,因此使用栈来保存
3. 「被动」更新答案的逻辑导致了我们栈内元素单调

### 最后

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

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

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

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


Question:
给定一个整数 `num`,将其转化为 $7$ 进制,并以字符串形式输出。

示例 1:
```
输入: num = 100

输出: "202"
```
示例 2:
```
输入: num = -7

输出: "-10"
```

提示:
* $-10^7 <= num <= 10^7$

Solution:
### 模拟

今天是个特别的日子,可惜是个简单题 QWQ

按照通用的进制转换方式进行模拟即可。

代码:
```Java
class Solution {
    public String convertToBase7(int n) {
        boolean flag = n < 0;
        if (flag) n = -n;
        StringBuilder sb = new StringBuilder();
        do {
            sb.append(n % 7);
            n /= 7;
        } while (n != 0);
        sb.reverse();
        return flag ? "-" + sb.toString() : sb.toString();
    }
}
```
* 时间复杂度:$O(\log{|n|})$
* 空间复杂度:$O(\log{|n|})$

### 最后

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

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

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

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


Question:
给你一个长度为 `n` 的整数数组 `score`,其中 `score[i]` 是第 $i$ 位运动员在比赛中的得分。所有得分都**互不相同**。

运动员将根据得分**决定名次**,其中名次第 `1` 的运动员得分最高,名次第 `2` 的运动员得分第 `2` 高,依此类推。运动员的名次决定了他们的获奖情况:
* 名次第 `1` 的运动员获金牌 `"Gold Medal"` 。
* 名次第 `2` 的运动员获银牌 `"Silver Medal"`。
* 名次第 `3` 的运动员获铜牌 `"Bronze Medal"`。
* 从名次第 `4` 到第 `n` 的运动员,只能获得他们的名次编号(即,名次第 x 的运动员获得编号 `"x"`)。

使用长度为 `n` 的数组 `answer` 返回获奖,其中 `answer[i]` 是第 `i` 位运动员的获奖情况。

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

输出:["Gold Medal","Silver Medal","Bronze Medal","4","5"]

解释:名次为 [1st, 2nd, 3rd, 4th, 5th] 。
```
示例 2:
```
输入:score = [10,3,8,9,4]

输出:["Gold Medal","5","Bronze Medal","Silver Medal","4"]

解释:名次为 [1st, 5th, 3rd, 2nd, 4th] 。
```

提示:
* $n == score.length$
* $1 <= n <= 10^4$
* $0 <= score[i] <= 10^6$
* $score$ 中的所有值 互不相同

Solution:
### 模拟

根据题意,我们可以先对 `score` 数组进行拷贝并排序,利用分数各不相同,对排序数组中分值进行名次编号(存入哈希表),再利用名次编号构造答案。

代码:
```Java
class Solution {
    String[] ss = new String[]{"Gold Medal", "Silver Medal", "Bronze Medal"};
    public String[] findRelativeRanks(int[] score) {
        int n = score.length;
        String[] ans = new String[n];
        int[] clone = score.clone();
        Arrays.sort(clone);
        Map<Integer, Integer> map = new HashMap<>();
        for (int i = n - 1; i >= 0; i--) map.put(clone[i], n - 1 - i);
        for (int i = 0; i < n; i++) {
            int rank = map.get(score[i]);
            ans[i] = rank < 3 ? ss[rank] : String.valueOf(rank + 1);
        }
        return ans;
    }
}
```
* 时间复杂度:拷贝 `score` 数组的复杂度为 $O(n)$;对拷贝数组进行排序的复杂度为 $O(n\log{n})$;构造哈希表的复杂度为 $O(n)$;利用哈希表构造答案复杂度为 $O(n)$。整体复杂度为 $O(n\log{n})$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


Question:
对于一个 正整数,如果它和除了它自身以外的所有 **正因子** 之和相等,我们称它为 「完美数」。

给定一个 整数 `n`, 如果是完美数,返回 `true`,否则返回 `false`。

示例 1:
```
输入:num = 28

输出:true

解释:28 = 1 + 2 + 4 + 7 + 14
1, 2, 4, 7, 和 14 是 28 的所有正因子。
```
示例 2:
```
输入:num = 6

输出:true
```
示例 3:
```
输入:num = 496

输出:true
```
示例 4:
```
输入:num = 8128

输出:true
```
示例 5:
```
输入:num = 2

输出:false
```

提示:
* $1 <= num <= 10^8$

Solution:
### 数学

我们知道正因数总是成对的出现,因此我们可以仅枚举每对正因数的较小数,即从 $[1, \sqrt{num}]$ 范围内进行枚举(其中 $nums > 1$)。

同时为避免使用 `sqrt` 库函数和溢出,使用 $i <= \frac{num}{i}$ 作为上界判断。

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

### 最后

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

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

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

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


LeetCode/501-510/508. 出现次数最多的子树元素和（中等）.md

508. 出现次数最多的子树元素和

Question:
给你一个二叉树的根结点 `root`,请返回出现次数最多的子树元素和。如果有多个元素出现的次数相同,返回所有出现次数最多的子树元素和(不限顺序)。

一个结点的 「子树元素和」 定义为以该结点为根的二叉树上所有结点的元素之和(包括结点本身)。

示例 1:

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

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

```
输入: root = [5,2,-5]

输出: [2]
```

提示:
* 节点数在 $[1, 10^4]$ 范围内
* $-10^5 <= Node.val <= 10^5$

Solution:
### DFS

使用 `DFS` 对树进行遍历,同时将每棵子树的总和值存入哈希表(以元素和为 `key`,出现次数为 `value`),并实时维护最大子树和,使用变量 `max` 存储。

当遍历完整棵树后,将哈希表中值为 `max` 的键存入答案。

代码:
```Java
class Solution {
    Map<Integer, Integer> map = new HashMap<>();
    int max = 0;
    public int[] findFrequentTreeSum(TreeNode root) {
        dfs(root);
        List<Integer> list = new ArrayList<>();
        for (int k : map.keySet()) {
            if (map.get(k) == max) list.add(k);
        }
        int n = list.size();
        int[] ans = new int[n];
        for (int i = 0; i < n; i++) ans[i] = list.get(i);
        return ans;
    }
    int dfs(TreeNode root) {
        if (root == null) return 0;
        int cur = root.val + dfs(root.left) + dfs(root.right);
        map.put(cur, map.getOrDefault(cur, 0) + 1);
        max = Math.max(max, map.get(cur));
        return cur;
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


Question:
给你一个整数数组 `nums`,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。

子数组是数组中的一个连续部分。

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

输出:6

解释:连续子数组 [4,-1,2,1] 的和最大,为 6 。
```
示例 2:
```
输入:nums = [1]

输出:1
```
示例 3:
```
输入:nums = [5,4,-1,7,8]

输出:23
```

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

进阶:如果你已经实现复杂度为 $O(n)$ 的解法,尝试使用更为精妙的分治法求解。

Solution:
### 前缀和 or 线性 DP

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

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

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

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

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

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

学习完一维前缀和后,回到本题。

先用 `nums` 预处理出前缀和数组 `sum`,然后在遍历子数组右端点 `j` 的过程中,通过变量 `m` 动态记录已访问的左端点 `i` 的前缀和最小值。最终,在所有 `sum[j] - m` 的取值中选取最大值作为答案。

代码实现上,我们无需明确计算前缀和数组 `sum`,而是使用变量 `s` 表示当前累计的前缀和(充当右端点),并利用变量 `m` 记录已访问的前缀和的最小值(充当左端点)即可。

**本题除了将其看作为「前缀和裸题用有限变量进行空间优化」以外,还能以「线性 DP」角度进行理解。**

定义 $f[i]$ 为考虑前 $i$ 个元素,且第 $nums[i]$ 必选的情况下,形成子数组的最大和。

不难发现,仅考虑前 $i$ 个元素,且 $nums[i]$ 必然参与的子数组中。要么是 $nums[i]$ 自己一个成为子数组,要么与前面的元素共同组成子数组。

因此,状态转移方程:
$$
f[i] = \max(f[i - 1] + nums[i], nums[i])
$$

由于 $f[i]$ 仅依赖于 $f[i - 1]$ 进行转移,可使用有限变量进行优化,因此写出来的代码也是和上述前缀和角度分析的类似。

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

---

### 分治

“分治法”的核心思路是将大问题拆分成更小且相似的子问题,通过递归解决这些子问题,最终合并子问题的解来得到原问题的解。

实现分治,关键在于对“递归函数”的设计(入参 & 返回值)。

在涉及数组的分治题中,左右下标 `l` 和 `r` 必然会作为函数入参,因为它能用于表示当前所处理的区间,即小问题的范围。

对于本题,仅将最大子数组和(答案)作为返回值并不足够,因为单纯从小区间的解无法直接推导出大区间的解,我们需要一些额外信息来辅助求解。

具体的,我们可以将返回值设计成四元组,分别代表 `区间和`,`前缀最大值`,`后缀最大值` 和 `最大子数组和`,用 `[sum, lm, rm, max]` 表示。

有了完整的函数签名 `int[] dfs(int[] nums, int l, int r)`,考虑如何实现分治:

1. 根据当前区间 $[l, r]$ 的长度进行分情况讨论:
   1. 若 $l = r$,只有一个元素,区间和为 $nums[l]$,而 最大子数组和、前缀最大值 和 后缀最大值 由于允许“空数组”,因此均为 $\max(nums[l], 0)$
   2. 否则,将当前问题划分为两个子问题,通常会划分为两个相同大小的子问题,划分为 $[l, mid]$ 和 $[mid + 1, r]$ 两份,递归求解,其中 $mid = \left \lfloor \frac{l + r}2{} \right \rfloor$

随后考虑如何用“子问题”的解合并成“原问题”的解:

1. **合并区间和 (`sum`):** 当前问题的区间和等于左右两个子问题的区间和之和,即 `sum = left[0] + right[0]`。
2. **合并前缀最大值 (`lm`):** 当前问题的前缀最大值可以是左子问题的前缀最大值,或者左子问题的区间和加上右子问题的前缀最大值。即 `lm = max(left[1], left[0] + right[1])`。
3. **合并后缀最大值 (`rm`):** 当前问题的后缀最大值可以是右子问题的后缀最大值,或者右子问题的区间和加上左子问题的后缀最大值。即 `rm = max(right[2], right[0] + left[2])`。
4. **合并最大子数组和 (`max`):** 当前问题的最大子数组和可能出现在左子问题、右子问题,或者跨越左右两个子问题的边界。因此,`max` 可以通过 `max(left[3], right[3], left[2] + right[1])` 来得到。

一些细节:由于我们在计算 `lm`、`rm` 和 `max` 的时候允许数组为空,而答案对子数组的要求是至少包含一个元素。因此对于 `nums` 全为负数的情况,我们会错误得出最大子数组和为 `0` 的答案。针对该情况,需特殊处理,遍历一遍 `nums`,若最大值为负数,直接返回最大值。

Java 代码:

```Java
class Solution {
    // 返回值: [sum, lm, rm, max] = [区间和, 前缀最大值, 后缀最大值, 最大子数组和]
    int[] dfs(int[] nums, int l, int r) {
        if (l == r) {
            int t = Math.max(nums[l], 0);
            return new int[]{nums[l], t, t, t};
        }
        // 划分成两个子区间,分别求解
        int mid = l + r >> 1;
        int[] left = dfs(nums, l, mid), right = dfs(nums, mid + 1, r);
        // 组合左右子区间的信息,得到当前区间的信息
        int[] ans = new int[4];
        ans[0] = left[0] + right[0]; // 当前区间和
        ans[1] = Math.max(left[1], left[0] + right[1]); // 当前区间前缀最大值
        ans[2] = Math.max(right[2], right[0] + left[2]); // 当前区间后缀最大值
        ans[3] = Math.max(Math.max(left[3], right[3]), left[2] + right[1]); // 最大子数组和
        return ans;
    }
    public int maxSubArray(int[] nums) {
        int m = nums[0];
        for (int x : nums) m = Math.max(m, x);
        if (m <= 0) return m;
        return dfs(nums, 0, nums.length - 1)[3];
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    // 返回值: [sum, lm, rm, max] = [区间和, 前缀最大值, 后缀最大值, 最大子数组和]
    vector<int> dfs(vector<int>& nums, int l, int r) {
        if (l == r) {
            int t = max(nums[l], 0);
            return {nums[l], t, t, t};
        }
        // 划分成两个子区间,分别求解
        int mid = l + r >> 1;
        auto left = dfs(nums, l, mid), right = dfs(nums, mid + 1, r);
        // 组合左右子区间的信息,得到当前区间的信息
        vector<int> ans(4);
        ans[0] = left[0] + right[0]; // 当前区间和
        ans[1] = max(left[1], left[0] + right[1]); // 当前区间前缀最大值
        ans[2] = max(right[2], right[0] + left[2]); // 当前区间后缀最大值
        ans[3] = max({left[3], right[3], left[2] + right[1]}); // 最大子数组和
        return ans;
    }
    int maxSubArray(vector<int>& nums) {
        int m = nums[0];
        for (int x : nums) m = max(m, x);
        if (m <= 0) return m;
        return dfs(nums, 0, nums.size() - 1)[3];
    }
};
```
Python 代码:
```Python
class Solution:
    def maxSubArray(self, nums: List[int]) -> int:
        def dfs(l, r):
            if l == r:
                t = max(nums[l], 0)
                return [nums[l], t, t, t]
            # 划分成两个子区间,分别求解
            mid = (l + r) // 2
            left, right = dfs(l, mid), dfs(mid + 1, r)
            # 组合左右子区间的信息,得到当前区间的信息
            ans = [0] * 4
            ans[0] = left[0] + right[0] # 当前区间和
            ans[1] = max(left[1], left[0] + right[1]) # 当前区间前缀最大值
            ans[2] = max(right[2], right[0] + left[2]) # 当前区间后缀最大值
            ans[3] = max(left[3], right[3], left[2] + right[1]) # 最大子数组和
            return ans

        m = max(nums)
        if m <= 0:
            return m
        return dfs(0, len(nums) - 1)[3]
```
TypeScript 代码:
```TypeScript
function maxSubArray(nums: number[]): number {
    const dfs = function (l: number, r: number): number[] {
        if (l == r) {
            const t = Math.max(nums[l], 0);
            return [nums[l], t, t, t];
        }
        // 划分成两个子区间,分别求解
        const mid = (l + r) >> 1;
        const left = dfs(l, mid), right = dfs(mid + 1, r);
        // 组合左右子区间的信息,得到当前区间的信息
        const ans = Array(4).fill(0);
        ans[0] = left[0] + right[0]; // 当前区间和
        ans[1] = Math.max(left[1], left[0] + right[1]); // 当前区间前缀最大值
        ans[2] = Math.max(right[2], right[0] + left[2]); // 当前区间后缀最大值
        ans[3] = Math.max(left[3], right[3], left[2] + right[1]); // 最大子数组和
        return ans;
    }

    const m = Math.max(...nums);
    if (m <= 0) return m;
    return dfs(0, nums.length - 1)[3];
};
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(\log{n})$

### 最后

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

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

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

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


Question:
给你一个 $m$ 行 $n$ 列的矩阵 `matrix`,请按照顺时针螺旋顺序,返回矩阵中的所有元素。

示例 1:

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

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

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

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

提示:
* $m == matrix.length$
* $n == matrix[i].length$
* $1 <= m, n <= 10$
* $-100 <= matrix[i][j] <= 100$

Solution:
### 按照「形状」进行模拟

我们可以按「圈」进行划分打印。

使用「左上角」$(x1,y1)$ &「右下角」$(x2,y2)$ 来确定某个「圈」,进行打印。

完成后,令「左上角」&「右下角」往里收,分别得到 $(x1 + 1, y1 + 1)$ 和 $(x2 - 1, y2 - 1)$,执行相同的打印规则。

代码 :
```Java
class Solution {
    public List<Integer> spiralOrder(int[][] mat) {
        List<Integer> ans = new ArrayList<>();
        int m = mat.length, n = mat[0].length;
        circle(mat, 0, 0, m - 1, n - 1, ans);
        return ans;
    }

    // 遍历 以 (x1, y1) 作为左上角,(x2, y2) 作为右下角形成的「圈」
    void circle(int[][] mat, int x1, int y1, int x2, int y2, List<Integer> ans) {
        if (x2 < x1 || y2 < y1) return;
        // 只有一行时,按「行」遍历
        if (x1 == x2) {
            for (int i = y1; i <= y2; i++) ans.add(mat[x1][i]);
            return;
        }
        // 只有一列时,按「列」遍历
        if (y1 == y2) {
            for (int i = x1; i <= x2; i++) ans.add(mat[i][y2]);
            return;
        }
        // 遍历当前「圈」
        for (int i = y1; i < y2; i++) ans.add(mat[x1][i]);
        for (int i = x1; i < x2; i++) ans.add(mat[i][y2]);
        for (int i = y2; i > y1; i--) ans.add(mat[x2][i]);
        for (int i = x2; i > x1; i--) ans.add(mat[i][y1]);
        // 往里收一圈,继续遍历
        circle(mat, x1 + 1, y1 + 1, x2 - 1, y2 - 1, ans);
    }
}
```
* 时间复杂度:$O(n * m)$
* 空间复杂度:$O(1)$

---

### 按照「方向」进行模拟

事实上,我们还可以根据「方向」进行模拟。

因为每一圈的打印输出都是按照特定的「四个方向」进行的。

这种解法更为简洁。而触发方向转换的时机:

1. 下一步发生位置溢出
2. 回到了本圈的起点

代码:
```Java
class Solution {
    int INF = 101;
    public List<Integer> spiralOrder(int[][] mat) {
        List<Integer> ans = new ArrayList<>();
        int m = mat.length, n = mat[0].length;
        // 定义四个方向
        int[][] dirs = new int[][]{{0,1},{1,0},{0,-1},{-1,0}};
        for (int x = 0, y = 0, d = 0, i = 0; i < m * n; i++) {
            ans.add(mat[x][y]);
            mat[x][y] = INF;
            // 下一步要到达的位置
            int nx = x + dirs[d][0], ny = y + dirs[d][1];
            // 如果下一步发生「溢出」或者已经访问过(说明四个方向已经走过一次)
            if (nx < 0 || nx >= m || ny < 0 || ny >= n || mat[nx][ny] == INF) {
                d = (d + 1) % 4;
                nx = x + dirs[d][0]; ny = y + dirs[d][1];
            }
            x = nx; y = ny;
        }
        return ans;
    }
}
```

-

```C++
class Solution {
public:
    const int dir[4][2] = {{0,1}, {1,0},{0,-1},{-1,0}};
    vector<int> spiralOrder(vector<vector<int>>& matrix) {
        int idx = 0, m = matrix.size(), n = matrix[0].size();
        vector<int> res;
        for(int x = 0, y = 0, d = 0; idx < m * n; idx++){
            res.push_back(matrix[x][y]);
            matrix[x][y] = 101;
            int t_x = x + dir[d][0], t_y = y + dir[d][1];
            if(t_x < 0 or t_x >= m or t_y < 0 or t_y >= n or matrix[t_x][t_y] == 101){
                d = (d + 1) % 4;
                t_x = x + dir[d][0],t_y = y + dir[d][1];
            }
            x = t_x, y = t_y;
        }
        return res;
    }
};
```
* 时间复杂度:$O(n * m)$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


58. 最后一个单词的长度

Question:
给你一个字符串 `s`,由若干单词组成,单词前后用一些空格字符隔开。返回字符串中最后一个单词的长度。

**单词**是指仅由字母组成、不包含任何空格字符的最大子字符串。

示例 1:
```
输入:s = "Hello World"

输出:5
```
示例 2:
```
输入:s = "   fly me   to   the moon  "

输出:4
```
示例 3:
```
输入:s = "luffy is still joyboy"

输出:6
```

提示:
* 1 <= s.length <= $10^4$
* s 仅有英文字母和空格 ' ' 组成
* s 中至少存在一个单词

Solution:
### 模拟

根据题意,我们可以「从后往前」对字符串进行遍历,使用两个指针 $i$ 和 $j$ 来代指最后一个单词的范围($i$ 最终会指向目标单词首个字符的前一个字符,$j$ 最终会指向目标单词的最后一个字符),最终 $j - i$ 即是答案。

代码:
```Java
class Solution {
    public int lengthOfLastWord(String s) {
        int n = s.length();
        int j = n - 1;
        while (j >= 0 && s.charAt(j) == ' ') j--;
        int i = j;
        while (i >= 0 && s.charAt(i) != ' ') i--;
        return j - i;
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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