Question:
给你一个字符串 `s`,请你将 `s` 分割成一些子串,使每个子串都是回文。

返回符合要求的 最少分割次数 。

示例 1:
```
输入:s = "aab"
输出:1
解释:只需一次分割就可将 s 分割成 ["aa","b"] 这样两个回文子串。
```
示例 2:
```
输入:s = "a"
输出:0
```
示例 3:
```
输入:s = "ab"
输出:1
```

提示:
* $1 <= s.length <= 2000$
* `s` 仅由小写英文字母组成

Solution:
### 动态规划

如果在 [131. 分割回文串](https://mp.weixin.qq.com/s?__biz=MzU4NDE3MTEyMA==&mid=2247487047&idx=1&sn=117c48f20778868442fce44e100d2ea8&chksm=fd9ca558caeb2c4eb1bff4f0878ff796feabe523657c2aafea0b2d1c7026e1c0572ab1e6d205&token=635532356&lang=zh_CN#rd) 你有使用到 DP 进行预处理的话。

这道题就很简单了,就是一道常规的动态规划题。

为了方便,我们约定所有下标从 $1$ 开始。

即对于长度为 $n$ 的字符串,我们使用 $[1,n]$ 进行表示。估计不少看过三叶题解的同学都知道,这样做的目的是为了减少边界情况判断,这本身也是对于「哨兵」思想的运用。

* **递推「最小分割次数」思路**

我们定义 $f[r]$ 为将 $[1,r]$ 这一段字符分割为若干回文串的最小分割次数,那么最终答案为 $f[n]$。

不失一般性的考虑 $f[r]$ 如何转移:

1. 从「起点字符」到「第 $r$ 个字符」能形成回文串。那么最小分割次数为 0,此时有 $f[r] = 0$
2. 从「起点字符」到「第 $r$ 个字符」不能形成回文串。此时我们需要枚举左端点 $l$,如果 $[l,r]$ 这一段是回文串的话,那么有 $f[r] = f[l - 1] + 1$

在 $2$ 中满足回文要求的左端点位置 $l$ 可能有很多个,我们在所有方案中取一个 $\min$ 即可。

* **快速判断「任意一段子串是否回文」思路**

剩下的问题是,我们如何快速判断连续一段 $[l, r]$ 是否为回文串,做法和昨天的 [131. 分割回文串](https://mp.weixin.qq.com/s?__biz=MzU4NDE3MTEyMA==&mid=2247487047&idx=1&sn=117c48f20778868442fce44e100d2ea8&chksm=fd9ca558caeb2c4eb1bff4f0878ff796feabe523657c2aafea0b2d1c7026e1c0572ab1e6d205&token=635532356&lang=zh_CN#rd)  一模一样。

*PS. 在 [131. 分割回文串](https://mp.weixin.qq.com/s?__biz=MzU4NDE3MTEyMA==&mid=2247487047&idx=1&sn=117c48f20778868442fce44e100d2ea8&chksm=fd9ca558caeb2c4eb1bff4f0878ff796feabe523657c2aafea0b2d1c7026e1c0572ab1e6d205&token=635532356&lang=zh_CN#rd),数据范围只有 $16$,因此我们可以不使用 DP 进行预处理,而是使用双指针来判断是否回文也能过。但是该题数据范围为 $2000$(数量级为 $10^3$),使用朴素做法判断是否回文的话,复杂度会去到 $O(n^3)$(计算量为 $10^9$),必然超时。*

因此我们不可能每次都使用双指针去线性扫描一遍 $[l, r]$ 判断是否回文。

一个合理的做法是,我们先预处理出所有的 $g[l][r]$,$g[l][r]$ 代表 $[l,r]$ 这一段是否为回文串。

预处理  $g[l][r]$ 的过程可以用递推去做。

要想  $g[l][r] = true$ ,必须满足以下两个条件:

1. $g[l + 1][r - 1] = true$
2. $s[i] = s[j]$

由于状态 $f[l][r]$ 依赖于状态 $f[l + 1][r - 1]$,因此需要我们左端点 $l$ 是「从大到小」进行遍历;而右端点 $r$ 是「从小到大」进行遍历。

因此最终的遍历过程可以整理为:**右端点 $r$ 一直往右移动(从小到大),在 $r$  固定情况下,左端点 $l$ 在 $r$ 在左边开始,一直往左移动(从大到小)**

代码:
```Java
class Solution {
    public int minCut(String s) {
        int n = s.length();
        char[] cs = s.toCharArray();

        // g[l][r] 代表 [l,r] 这一段是否为回文串
        boolean[][] g = new boolean[n + 1][n + 1];
        for (int r = 1; r <= n; r++) {
            for (int l = r; l >= 1; l--) {
                // 如果只有一个字符,则[l,r]属于回文
                if (l == r) {
                    g[l][r] = true;
                } else {
                    // 在 l 和 r 字符相同的前提下
                    if (cs[l - 1] == cs[r - 1]) {
                        // 如果 l 和 r 长度只有 2;或者 [l+1,r-1] 这一段满足回文,则[l,r]属于回文
                        if (r - l == 1 || g[l + 1][r - 1]) {
                            g[l][r] = true;
                        }
                    }
                }
            }
        }

        // f[r] 代表将 [1,r] 这一段分割成若干回文子串所需要的最小分割次数
        int[] f = new int[n + 1];
        for (int r = 1; r <= n; r++) {
            // 如果 [1,r] 满足回文,不需要分割
            if (g[1][r]) {
                f[r] = 0;
            } else {
                // 先设定一个最大分割次数(r 个字符最多消耗 r - 1 次分割)
                f[r] = r - 1;
                // 在所有符合 [l,r] 回文的方案中取最小值
                for (int l = 1; l <= r; l++) {
                    if (g[l][r]) f[r] = Math.min(f[r], f[l - 1] + 1);
                }
            }
        }

        return f[n];
    }
}
```
* 时间复杂度:$O(n^2)$
* 空间复杂度:$O(n^2)$

---

### 关于「如何确定 DP 状态定义」的分享

有同学会对「如何确定 DP 的状态定义」有疑问,觉得自己总是定不下 DP 的状态定义。

首先,十分正常,不用担心。

DP 的状态定义,基本上是考经验的(猜的),猜对了 DP 的状态定义,基本上「状态转移方程」就是呼之欲出。

虽然大多数情况都是猜的,但也不是毫无规律,相当一部分是定义是与「结尾」和「答案」有所关联的。

**例如本题定义 $f[i]$ 为以下标为 $i$ 的字符作为结尾(结尾)的最小分割次数(答案)。**

因此对于那些你没见过的 DP 模型题,可以从这两方面去「猜」。

---

### Manacher 算法(非重要补充)

如果你还学有余力的话,可以看看下面这篇题解。

提供了「回文串」问题的究极答案:Manacher 算法。

由于 Manacher 算法较为局限,只能解决「回文串」问题,远不如 KMP 算法使用广泛,**不建议大家深究原理,而是直接当做「模板」背过。**

背过这样的算法的意义在于:相当于大脑里有了一个时间复杂度为 $O(n)$ 的 api 可以使用,这个 api 传入一个字符串,返回该字符串的最大回文子串。

[回文串问题的究极答案:Manacher 算法](https://leetcode-cn.com/problems/longest-palindromic-substring/solution/shua-chuan-lc-po-su-jie-fa-manacher-suan-i2px/)

**如果觉得自己背不下来,也没有问题。事实上我还没有见过必须使用 Manacher 算法才能过的回文串题。**

### 最后

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

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

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

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


Question:
在一条环路上有 `N` 个加油站,其中第 `i` 个加油站有汽油 `gas[i]` 升。

你有一辆油箱容量无限的的汽车,从第 `i` 个加油站开往第 `i+1` 个加油站需要消耗汽油 `cost[i]` 升。你从其中的一个加油站出发,开始时油箱为空。

如果你可以绕环路行驶一周,则返回出发时加油站的编号,否则返回 `-1`。

说明:
* 如果题目有解,该答案即为唯一答案。
* 输入数组均为非空数组,且长度相同。
* 输入数组中的元素均为非负数。

示例 1:

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

输出: 3

解释:
从 3 号加油站(索引为 3 处)出发,可获得 4 升汽油。此时油箱有 = 0 + 4 = 4 升汽油
开往 4 号加油站,此时油箱有 4 - 1 + 5 = 8 升汽油
开往 0 号加油站,此时油箱有 8 - 2 + 1 = 7 升汽油
开往 1 号加油站,此时油箱有 7 - 3 + 2 = 6 升汽油
开往 2 号加油站,此时油箱有 6 - 4 + 3 = 5 升汽油
开往 3 号加油站,你需要消耗 5 升汽油,正好足够你返回到 3 号加油站。
因此,3 可为起始索引。
```
示例 2:
```
输入:
gas  = [2,3,4]
cost = [3,4,3]

输出: -1

解释:
你不能从 0 号或 1 号加油站出发,因为没有足够的汽油可以让你行驶到下一个加油站。
我们从 2 号加油站出发,可以获得 4 升汽油。 此时油箱有 = 0 + 4 = 4 升汽油
开往 0 号加油站,此时油箱有 4 - 3 + 2 = 3 升汽油
开往 1 号加油站,此时油箱有 3 - 3 + 3 = 3 升汽油
你无法返回 2 号加油站,因为返程需要消耗 4 升汽油,但是你的油箱只有 3 升汽油。
因此,无论怎样,你都不可能绕环路行驶一周。
```

Solution:
### 基本分析/朴素解法

这是一道比较经典的题目。

估计在 LeetCode 也是有一段时间了,所以连数据范围都没有。

我这里直接规定一下数据范围为 $10^5$,这意味着我们不能使用 $O(n^2)$ 做法了。

但朴素做法往往是优化的出发点,所以我们还是先分析一下,朴素的做法是怎么样的:

* 题目要求「合法起点」的下标,因此我们可以枚举所有的「起点」

* 然后按照「油量 & 成本」模拟一遍,看是否能走完一圈

共有 $n$ 个「起点」,检查某个「起点」合法性的复杂度是 $O(n)$ 的。因此整体复杂度为 $O(n^2)$ 的。

代码:

```java
class Solution {
    public int canCompleteCircuit(int[] gas, int[] cost) {
        int n = gas.length;
        for (int start = 0; start < n; start++) {
            // 直接跳过第一步都不满足的起点
            if (gas[start] < cost[start]) continue;
            // 剩余油量
            int cur = gas[start] - cost[start];
            // 所在位置
            int idx = (start + 1) % n;
            while (idx != start) {
                cur += gas[idx] - cost[idx];
                // 如果剩余油量为负数,说明无法离开当前位置,走到下一位置
                if (cur < 0) break;
                idx = (idx + 1) % n;
            }
            if (idx == start) return start;
        }
        return -1;
    }
}
```

* 时间复杂度:$O(n^2)$
* 空间复杂度:$O(1)$

PS. 在 LeetCode 上提交了一下,是可以过的 🤣

---

### KMP

不考虑我们跳过那些第一步就不满足的起点的话,将上述解法中的两个主要逻辑“单独”拎出来看,都是无法优化的:

* 在没做任何操作之前,我们无法知道哪些起点是不合法的
* 没有比 $O(n)$ 更低的复杂度可以验证一个起点的合法性

因此只能使用 $O(n^2)$ 解法?

并不是。单独看无法优化,合在一起则可以:**随着某个起点「合法性验证」失败,我们可以否决掉一些「必然不合法」的方案。**

什么意思呢?

在朴素解法中,当我们验证了某个起点 $i$ 失败(无法走完一圈)之后,我们接下来会去尝试验证起点 $i + 1$。

**这时候验证 $i$ 失败过程中产生的“信息”,其实并没有贡献到之后的算法中。**

事实上,起点 $i$ 验证失败,其实是意味存在某个位置 $k$ 使得「当前油量」为负数。而这个位置 $k$ 就是验证起点 $i$ 这过程中产生的“信息”。

它可以产生的价值是:**在位置 $i$ 和位置 $k$ 之间的所有位置,都不可能是一个合法起点。也就是说,随着起点 $i$ 验证失败,我们可以否决掉方案不仅仅是位置 $i$,而是 $[i, k]$ 这些位置。**

我们可以证明为什么会有这样的性质:

首先,可以明确的是:**因为 `gas` 数组和 `cost` 数组是给定的,因此每个位置的「净消耗」是固定的,与从哪个「起点」出发无关。**

净消耗是指该位置提供的「油量」和「到达下一位置的油耗」,也就是 `gas[i]` 和 `cast[i]` 之间的差值。

证明过程如图:

因此,当有了这个优化之后,我们每个位置其实只会被遍历常数次:**当位置 $i$ 作为「起点」验证失败后,验证过程中遍历过的位置就不需要再作为「起点」被验证了。**

代码:

```Java
class Solution {
    public int canCompleteCircuit(int[] gas, int[] cost) {
        int n = gas.length;
        for (int start = 0; start < n; ) {
            if (gas[start] < cost[start]) {
                start++;
            } else {
                int cur = gas[start] - cost[start];
                int idx = start + 1;
                while (idx % n != start) {
                    cur += gas[idx % n] - cost[idx % n];
                    if (cur < 0) break;
                    idx++;
                }
                if (idx % n == start) return start;
                else start = idx;
            }
        }
        return -1;
    }
}
```

* 时间复杂度:$O(n)$
* 空间复杂度:$O(1)$

---

### 总结

看到这里,你可以已经理解了 $O(n)$ 解法是怎么来的了。

但可能还不清楚这个优化思路的本质是什么,其实这个优化的本质与 KMP 为什么可以做到线性匹配如出一辙。

**本质都是利用某次匹配(验证)过程产生的“信息”,来加速之后的匹配(验证)过程(否决掉一些必然合法的方案)。**

在我写过的 [KMP](https://mp.weixin.qq.com/s?__biz=MzU4NDE3MTEyMA==&mid=2247486317&idx=1&sn=9c2ff2fa5db427133cce9c875064e7a4&chksm=fd9ca072caeb29642bf1f5c151e4d5aaff4dc10ba408b23222ea1672cfc41204a584fede5c05&token=1279910339&lang=zh_CN#rd) 题解里,有这么一句原话:

> 当我们的原串指针从 `i` 位置后移到 `j` 位置,不仅仅代表着「原串」下标范围为 $[i,j)$ 的字符与「匹配串」匹配或者不匹配,更是在否决那些以「原串」下标范围为 $[i,j)$ 为「匹配发起点」的子集。

所以,从更本质的角度出发,这道题其实是一道「KMP」思想应用题,或者说广泛性的「DFA」题。

---

### 其他

在写「总结」部分的时候,我还特意去看了一下题解区,没有人提到过「KMP」和「DFA」,几乎所有题解都停留在题目标签「贪心算法」的角度去思考。

这是不对的,题目标签的拟定很大程度取决于「写这个标签的人的水平」和「ta 当时看这道题的思考角度」,是一个主观的结果。

**学习算法和数据结构,应该是去理解每个算法和数据结构的“某个操作”为什么能够带来优化效果,并将该优化效果的“底层思想”挖掘出来,应用到我们没见过的问题中,这才是真正的“学习”。**

### 最后

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

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

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

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


LeetCode/131-140/137. 只出现一次的数字 II（中等）.md

137. 只出现一次的数字 II

Question:
给你一个整数数组 `nums`,除某个元素仅出现一次外,其余每个元素都恰出现三次。请你找出并返回那个只出现了一次的元素。

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

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

输出:99
```

提示:
* $1 <= nums.length <= 3 \times 10^4$
* $-2^{31} <= nums[i] <= 2^{31} - 1$
* `nums` 中,除某个元素仅出现 一次 外,其余每个元素都恰出现 三次

进阶:你的算法应该具有线性时间复杂度。 你可以不使用额外空间来实现吗?

Solution:
### 哈希表

一个朴素的做法是使用「哈希表」进行计数,然后将计数为 $1$ 的数字进行输出。

哈希表以「数值 : 数值出现次数」形式进行存储。

代码:
```Java
class Solution {
    public int singleNumber(int[] nums) {
        Map<Integer, Integer> map = new HashMap<>();
        for (int x : nums) {
            map.put(x, map.getOrDefault(x, 0) + 1);
        }
        for (int x : map.keySet()) {
            if (map.get(x) == 1) return x;
        }
        return -1;
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n)$

---

### 位数统计

哈希表解法的空间复杂度是 $O(n)$ 的,而题目的【进阶】部分提到应当使用常数空间来做。

其中一个比较容易想到的做法,是利用 $int$ 类型固定为 $32$ 位。

**使用一个长度为 $32$ 的数组 $cnt[]$ 记录下所有数值的每一位共出现了多少次 $1$,再对 $cnt[]$ 数组的每一位进行 $mod$ $3$ 操作,重新拼凑出只出现一次的数值。**

举个 🌰,考虑样例 `[1,1,1,3]`,$1$ 和 $3$ 对应的二进制表示分别是 `00..001` 和 `00..011`,存入 $cnt[]$ 数组后得到 `[0,0,...,0,1,4]`。进行 $mod$ $3$ 操作后得到 `[0,0,...,0,1,1]`,再转为十进制数字即可得「只出现一次」的答案 $3$。

代码:
```Java
class Solution {
    public int singleNumber(int[] nums) {
        int[] cnt = new int[32];
        for (int x : nums) {
            for (int i = 0; i < 32; i++) {
                if (((x >> i) & 1) == 1) {
                    cnt[i]++;
                }
            }
        }
        int ans = 0;
        for (int i = 0; i < 32; i++) {
            if ((cnt[i] % 3 & 1) == 1) {
                ans += (1 << i);
            }
        }
        return ans;
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(1)$

---

### DFA

如果我们考虑「除了某个元素只出现一次以外,其余每个元素均出现两次」的情况,那么可以使用「异或」运算。

利用相同数异或为 0 的性质,可以帮助我们很好实现状态切换:

本题是考虑「除了某个元素只出现一次以外,其余每个元素均出现三次」的情况,那么对应了「出现 0 次」、「出现 1 次」和「出现 2 次」三种状态,意味着至少需要两位进行记录,且状态转换关系为:

那么如何将上述 DFA 用表达式表示出来呢?有以下几种方法:

1. 用「真值表」写出「逻辑函数表达式」可参考 [这里](https://wenku.baidu.com/view/e9460ad96729647d27284b73f242336c1eb930f0.html),化简过程可以参考 [卡诺图化简法](https://baike.baidu.com/item/%E5%8D%A1%E8%AF%BA%E5%9B%BE%E5%8C%96%E7%AE%80%E6%B3%95) 。

2. 把结论记住(这是一道经典的 DFA 入门题)。

3. 硬做,位运算也就那几种,不会「数字电路」也记不住「结论」,砸时间看着真值表不断调逻辑也是可以写出来的。

代码:
```Java
class Solution {
    public int singleNumber(int[] nums) {
        int one = 0, two = 0;
        for(int x : nums){
            one = one ^ x & ~two;
            two = two ^ x & ~one;
        }
        return one;
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


138. 复制带随机指针的链表

Question:
给你一个长度为 `n` 的链表,每个节点包含一个额外增加的随机指针 `random`,该指针可以指向链表中的任何节点或空节点。

构造这个链表的 深拷贝。 深拷贝应该正好由 `n` 个 全新 节点组成,其中每个新节点的值都设为其对应的原节点的值。新节点的 `next` 指针和 `random` 指针也都应指向复制链表中的新节点,并使原链表和复制链表中的这些指针能够表示相同的链表状态。复制链表中的指针都不应指向原链表中的节点 。

例如,如果原链表中有 `X` 和 `Y` 两个节点,其中 `X.random --> Y` 。那么在复制链表中对应的两个节点 `x` 和 `y` ,同样有 `x.random --> y` 。

返回复制链表的头节点。

用一个由 `n` 个节点组成的链表来表示输入/输出中的链表。每个节点用一个 `[val, random_index]` 表示:

* `val`:一个表示 `Node.val` 的整数。
* `random_index`:随机指针指向的节点索引(范围从 $0$ 到 $n-1$);如果不指向任何节点,则为  `null` 。

你的代码 只 接受原链表的头节点 `head` 作为传入参数。

示例 1:

```
输入:head = [[7,null],[13,0],[11,4],[10,2],[1,0]]

输出:[[7,null],[13,0],[11,4],[10,2],[1,0]]
```
示例 2:

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

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

```
输入:head = [[3,null],[3,0],[3,null]]

输出:[[3,null],[3,0],[3,null]]
```
示例 4:
```
输入:head = []

输出:[]

解释:给定的链表为空(空指针),因此返回 null。
```

提示:
* $0 <= n <= 1000$
* $-10000 <= Node.val <= 10000$

Solution:
### 模拟 + 哈希表

如果不考虑 `random` 指针的话,对一条链表进行拷贝,我们只需要使用两个指针:一个用于遍历原链表,一个用于构造新链表(始终指向新链表的尾部)即可。这一步操作可看做是「创建节点 + 构建 `next` 指针关系」。

现在在此基础上增加一个 `random` 指针,我们可以将 `next` 指针和 `random` 指针关系的构建拆开进行:

1. 先不考虑 `random` 指针,和原本的链表复制一样,创建新新节点,并构造 `next` 指针关系,同时使用「哈希表」记录原节点和新节点的映射关系;
2. 对原链表和新链表进行同时遍历,对于原链表的每个节点上的 `random` 都通过「哈希表」找到对应的新 `random` 节点,并在新链表上构造 `random` 关系。

代码:
```Java
class Solution {
    public Node copyRandomList(Node head) {
        Node t = head;
        Node dummy = new Node(-10010), cur = dummy;
        Map<Node, Node> map = new HashMap<>();
        while (head != null) {
            Node node = new Node(head.val);
            map.put(head, node);
            cur.next = node;
            cur = cur.next; head = head.next;
        }
        cur = dummy.next; head = t;
        while (head != null) {
            cur.random = map.get(head.random);
            cur = cur.next; head = head.next;
        }
        return dummy.next;
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n)$

---

### 模拟(原地算法)

显然时间复杂度上无法优化,考虑如何降低空间(不使用「哈希表」)。

我们使用「哈希表」的目的为了实现原节点和新节点的映射关系,更进一步的是为了快速找到某个节点 `random` 在新链表的位置。

那么我们可以利用原链表的 `next` 做一个临时中转,从而实现映射。

具体的,我们可以按照如下流程进行:

1. 对原链表的每个节点节点进行复制,并追加到原节点的后面;
2. 完成 $1$ 操作之后,链表的奇数位置代表了原链表节点,链表的偶数位置代表了新链表节点,且每个原节点的 `next` 指针执行了对应的新节点。这时候,我们需要构造新链表的 `random` 指针关系,可以利用 `link[i + 1].random = link[i].random.next`,$i$ 为奇数下标,含义为 **新链表节点的 `random` 指针指向旧链表对应节点的 `random` 指针的下一个值**;
3. 对链表进行拆分操作。

代码:
```Java
class Solution {
    public Node copyRandomList(Node head) {
        if (head == null) return head;
        Node t = head;
        while (head != null) {
            Node node = new Node(head.val);
            node.next = head.next;
            head.next = node;
            head = node.next;
        }
        head = t;
        while (head != null) {
            if (head.random != null) head.next.random = head.random.next;
            head = head.next.next;
        }
        head = t;
        Node ans = head.next;
        while (head != null) {
            Node ne = head.next;
            if (ne != null) head.next = ne.next;
            head = ne;
        }
        return ans;
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


Question:
给你一个字符串 `s` 和一个字符串列表 `wordDict` 作为字典。请你判断是否可以利用字典中出现的单词拼接出 `s` 。

注意:不要求字典中出现的单词全部都使用,并且字典中的单词可以重复使用。

示例 1:
```
输入: s = "leetcode", wordDict = ["leet", "code"]

输出: true

解释: 返回 true 因为 "leetcode" 可以由 "leet" 和 "code" 拼接成。
```
示例 2:
```
输入: s = "applepenapple", wordDict = ["apple", "pen"]

输出: true

解释: 返回 true 因为 "applepenapple" 可以由 "apple" "pen" "apple" 拼接成。
     注意,你可以重复使用字典中的单词。
```
示例 3:
```
输入: s = "catsandog", wordDict = ["cats", "dog", "sand", "and", "cat"]

输出: false
```

提示:
* $1 <= s.length <= 300$
* $1 <= wordDict.length <= 1000$
* $1 <= wordDict[i].length <= 20$
* `s` 和 `wordDict[i]` 仅有小写英文字母组成
* `wordDict` 中的所有字符串 互不相同

Solution:
### 序列 DP

将字符串 `s` 长度记为 $n$,`wordDict` 长度记为 $m$。为了方便,我们调整字符串 `s` 以及将要用到的动规数组的下标从 $1$ 开始。

定义 $f[i]$ 为考虑前 $i$ 个字符,能否使用 `wordDict` 拼凑出来:当 $f[i] = true$ 代表 $s[1...i]$ 能够使用 `wordDict` 所拼凑,反之则不能。

不失一般性考虑 $f[i]$ 该如何转移:由于 $f[i]$ 需要考虑 $s[1...i]$ 范围内的字符,若 $f[i]$ 为 `True` 说明整个 $s[1...i]$ 都能够使用 `wordDict` 拼凑,自然也包括最后一个字符 $s[i]$ 所在字符串 `sub`。

**我们可以枚举最后一个字符所在字符串的左端点 $j$,若 $sub = s[j...i]$ 在 `wordDict` 中出现过,并且 $f[j - 1] = True$,说明 $s[0...(j - 1)]$ 能够被拼凑,并且子串 `sub` 也在 `wordDict`,可得 `f[i] = True`。**

为了快速判断某个字符是否在 `wordDict` 中出现,我们可以使用 `Set` 结构对 $wordDict[i]$ 进行转存。

Java 代码:
```Java
class Solution {
    public boolean wordBreak(String s, List<String> wordDict) {
        Set<String> set = new HashSet<>();
        for (String word : wordDict) set.add(word);
        int n = s.length();
        boolean[] f = new boolean[n + 10];
        f[0] = true;
        for (int i = 1; i <= n; i++) {
            for (int j = 1; j <= i && !f[i]; j++) {
                String sub = s.substring(j - 1, i);
                if (set.contains(sub)) f[i] = f[j - 1];
            }
        }
        return f[n];
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    bool wordBreak(string s, vector<string>& wordDict) {
        unordered_set<string> set;
        for (const string& word : wordDict) {
            set.insert(word);
        }
        int n = s.length();
        vector<bool> f(n + 10, false);
        f[0] = true;
        for (int i = 1; i <= n; ++i) {
            for (int j = 1; j <= i && !f[i]; ++j) {
                string sub = s.substr(j - 1, i - j + 1);
                if (set.find(sub) != set.end()) f[i] = f[j - 1] || f[i];
            }
        }
        return f[n];
    }
};
```
Python 代码:
```Python
class Solution:
    def wordBreak(self, s: str, wordDict: List[str]) -> bool:
        ss = set(wordDict)
        n = len(s)
        f = [False] * (n + 10)
        f[0] = True
        for i in range(1, n + 1):
            j = i
            while j >= 1 and not f[i]:
                sub = s[j - 1:i]
                if sub in ss:
                    f[i] = f[j - 1]
                j -= 1
        return f[n]
```
TypeScript 代码:
```TypeScript
function wordBreak(s: string, wordDict: string[]): boolean {
    const ss = new Set<string>(wordDict)
    const n = s.length
    const f = new Array<boolean>(n + 10).fill(false)
    f[0] = true
    for (let i = 1; i <= n; i++) {
        for (let j = i; j >= 1 && !f[i]; j--) {
            const sub = s.substring(j - 1, i)
            if (ss.has(sub)) f[i] = f[j - 1]
        }
    }
    return f[n]
}
```
* 时间复杂度:将 `wordDict` 转存在 `Set` 复杂度为 $O(m)$;`DP` 过程复忽裁剪子串和查询 `Set` 结构的常数,复杂度为 $O(n^2)$
* 空间复杂度:$O(n + m)$

---

### 总结

这里简单说下「线性 DP」和「序列 DP」的区别。

线性 DP 通常强调「状态转移所依赖的前驱状态」是由给定数组所提供的,即拓扑序是由原数组直接给出。更大白话来说就是通常有 $f[i][...]$ 依赖于 $f[i - 1][...]$。

这就限定了线性 DP 的复杂度是简单由「状态数量(或者说是维度数)」所决定。

序列 DP 通常需要结合题意来寻找前驱状态,即需要自身寻找拓扑序关系(例如本题,需要自己通过枚举的方式来找左端点,从而找到可转移的前驱状态 $f[j - 1]$)。

这就限定了序列 DP 的复杂度是由「状态数 + 找前驱」的复杂度所共同决定。也直接导致了序列 DP 有很多玩法,往往能够结合其他知识点出题,来优化找前驱这一操作,通常是利用某些性质,或是利用数据结构进行优化。

### 最后

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

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

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

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


1331. 数组序号转换

Question:
给你一个整数数组 `arr`,请你将数组中的每个元素替换为它们排序后的序号。

序号代表了一个元素有多大。序号编号的规则如下:

* 序号从 $1$ 开始编号。
* 一个元素越大,那么序号越大。如果两个元素相等,那么它们的序号相同。
* 每个数字的序号都应该尽可能地小。

示例 1:
```
输入:arr = [40,10,20,30]

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

解释:40 是最大的元素。 10 是最小的元素。 20 是第二小的数字。 30 是第三小的数字。
```
示例 2:
```
输入:arr = [100,100,100]

输出:[1,1,1]

解释:所有元素有相同的序号。
```
示例 3:
```
输入:arr = [37,12,28,9,100,56,80,5,12]

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

提示:
* $0 <= arr.length <= 10^5$
* $-10^9 <= arr[i] <= 10^9$

Solution:
### 模拟

根据题意进行模拟即可。

对 `arr` 进行拷贝操作得到新数组 `clone`,对 `clone` 数组进行排序,再使用哈希表记录元素与序号的映射关系,最后根据映射关系构造答案。

Java 代码:
```Java
class Solution {
    public int[] arrayRankTransform(int[] arr) {
        int[] clone = arr.clone();
        Arrays.sort(clone);
        Map<Integer, Integer> map = new HashMap<>();
        int n = arr.length, idx = 0;
        for (int i : clone) {
            if (!map.containsKey(i)) map.put(i, ++idx);
        }
        int[] ans = new int[n];
        for (int i = 0; i < n; i++) ans[i] = map.get(arr[i]);
        return ans;
    }
}
```
TypeScript 代码:
```TypeScript
function arrayRankTransform(arr: number[]): number[] {
    let clone = new Array<number>()
    for (const i of arr) clone.push(i)
    clone.sort((a,b)=>a-b)
    let n = arr.length, idx = 0
    let map = new Map<number, number>()
    for (const i of clone) {
        if (!map.has(i)) map.set(i, ++idx)
    }
    let ans = new Array<number>()
    for (let i = 0; i < n; i++) ans.push(map.get(arr[i]))
    return ans
};
```
* 时间复杂度:$O(n\log{n})$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


1332. 删除回文子序列

Question:
给你一个字符串 `s`,它仅由字母 `'a'` 和 `'b'` 组成。每一次删除操作都可以从 `s` 中删除一个回文 **子序列**。

返回删除给定字符串中所有字符(字符串为空)的最小删除次数。

「子序列」定义:如果一个字符串可以通过删除原字符串某些字符而不改变原字符顺序得到,那么这个字符串就是原字符串的一个子序列。

「回文」定义:如果一个字符串向后和向前读是一致的,那么这个字符串就是一个回文。

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

输出:1

解释:字符串本身就是回文序列,只需要删除一次。
```
示例 2:
```
输入:s = "abb"

输出:2

解释:"abb" -> "bb" -> "".
先删除回文子序列 "a",然后再删除 "bb"。
```
示例 3:
```
输入:s = "baabb"

输出:2

解释:"baabb" -> "b" -> "".
先删除回文子序列 "baab",然后再删除 "b"。
```

提示:
* $1 <= s.length <= 1000$
* `s` 仅包含字母 `'a'`  和 `'b'`

Solution:
### 双指针

由于 `s` 只有字母 `a` 和 `b`,并且删除的是「子序列」,因此最大的删除次数为 $2$(先删除所有的 `a`,再删除所有的 `b`)。

同时 `s` 本身不为空串(不存在删除次数为 $0$ 的情况),因此如果我们不能一次删除的话(`s` 本身为回文),只能通过 $2$ 次进行删除。

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

### 最后

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

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

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

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


LeetCode/1331-1340/1334. 阈值距离内邻居最少的城市（中等）.md

1334. 阈值距离内邻居最少的城市

Question:
有 $n$ 个城市,按从 $0$ 到 $n-1$ 编号。

给你一个边数组 `edges`,其中 $edges[i] = [from_{i}, to_{i}, weight_{i}]$ 代表 $from_{i}$ 和 $to_{i}$ 两个城市之间的双向加权边,距离阈值是一个整数 `distanceThreshold`。

返回能通过某些路径到达其他城市数目最少、且路径距离最大为 `distanceThreshold` 的城市。如果有多个这样的城市,则返回编号最大的城市。

注意,连接城市 $i$ 和 $j$ 的路径的距离等于沿该路径的所有边的权重之和。

示例 1:

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

输出:3

解释:城市分布图如上。
每个城市阈值距离 distanceThreshold = 4 内的邻居城市分别是:
城市 0 -> [城市 1, 城市 2]
城市 1 -> [城市 0, 城市 2, 城市 3]
城市 2 -> [城市 0, 城市 1, 城市 3]
城市 3 -> [城市 1, 城市 2]
城市 0 和 3 在阈值距离 4 以内都有 2 个邻居城市,但是我们必须返回城市 3,因为它的编号最大。
```
示例 2:

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

输出:0

解释:城市分布图如上。
每个城市阈值距离 distanceThreshold = 2 内的邻居城市分别是:
城市 0 -> [城市 1]
城市 1 -> [城市 0, 城市 4]
城市 2 -> [城市 3, 城市 4]
城市 3 -> [城市 2, 城市 4]
城市 4 -> [城市 1, 城市 2, 城市 3]
城市 0 在阈值距离 2 以内只有 1 个邻居城市。
```

提示:
* $2 <= n <= 100$
* $1 <= edges.length <= \frac{n \times (n - 1)}{2}$
* $edges[i].length = 3$
* $0 <= from_{i} < to_{i} < n$
* $1 <= weight_{i}, distanceThreshold <= 10^4$
* 所有 $(from_{i}, to_{i})$ 都是不同的。

Solution:
### 基本分析

若能预处理图中任意两点 $i$ 和 $j$ 的最短距离 `dist`,那么统计每个点 $i$ 在图中有多少满足 $dist[j] \leq distanceThreshold$ 的点 $j$ 即为答案。

于是问题转换为:**如何求解给定图中,任意两点的最短距离**。

---

### 存图

在学习最短路之前,我们先搞懂众多图论问题的前置 🧀 :存图。

为了方便,我们约定 $n$ 为点数,$m$ 为边数。

根据点和边的数量级关系,可以将图分成如下两类:

* 稠密图:边数较多,边数接近于点数的平方,即 $m \approx n^2$
* 稀疏图:边数较少,边数接近于点数,即 $m \approx n$

同时,根据「稠密图」还是「稀疏图」,我们有如下几种存图方式:

##### 1. 邻接矩阵(稠密图)

这是一种使用二维矩阵来进行存图的方式。

```Java
// w[a][b] = c 代表从 a 到 b 有权重为 c 的边
int[][] g = new int[N][N];

// 加边操作
void add(int a, int b, int c) {
    g[a][b] = c;
}
```

##### 2. 邻接表(稀疏图)

邻接表又叫**链式前向星**,是另一种常见的存图方式,实现代码与「使用数组存储单链表」一致(头插法)。

```Java
int[] he = new int[N], e = new int[M], ne = new int[M], w = new int[M];

// 加边操作
void add(int a, int b, int c) {
    e[idx] = b;
    ne[idx] = he[a];
    w[idx] = c;
    he[a] = idx++;
}
```

首先 `idx` 是用来对边进行编号的,然后对存图用到的几个数组作简单解释:
* `he` 数组:存储是某个节点所对应的边的集合(链表)的头结点;
* `e`  数组:由于访问某一条边指向的节点;
* `ne` 数组:由于是以链表的形式进行存边,该数组就是用于找到下一条边;
* `w`  数组:用于记录某条边的权重为多少。

当我们想要遍历所有由 `a` 点发出的边时,可以使用如下方式:

```Java
for (int i = he[a]; i != -1; i = ne[i]) {
    int b = e[i], c = w[i]; // 存在由 a 指向 b 的边,权重为 c
}
```

##### 3. 类

这是最简单,但使用频率最低的存图方式。

只有当我们需要确保某个操作复杂度为严格 $O(m)$ 时,才会考虑使用。

具体的,建立一个类来记录有向边信息:

```Java
class Edge {
    // 代表从 a 到 b 有一条权重为 c 的边
    int a, b, c;
    Edge(int _a, int _b, int _c) {
        a = _a; b = _b; c = _c;
    }
}
```

随后,使用诸如 `List` 的容器,存起所有边对象。在需要遍历所有边时,对容器进行进行遍历:

```Java
List<Edge> es = new ArrayList<>();

...

for (Edge e : es) {
    ...
}
```

综上,第  $3$ 种方式,往往是 `OJ` 给我们边信息的方式,我们自己几乎不会用这种方式建图。

实际运用中,熟练掌握「如何根据点和边的数量级关系,来决定使用邻接矩阵(稠密图)还是邻接表(稀疏图)」即可。

---

### Floyd(邻接矩阵)

`Floyd` 算法作为「多源汇最短路」算法,对于本题尤其适合。

`Floyd` 算法基于「动态规划」,其原始三维状态定义为 $dist[p][i][j]$,表示「所有从点 $i$ 到点 $j$,且允许经过点集 $(1, ... , p)$ 的路径」中的最短距离。

状态转移方程:
$$
dist[p][i][j] = \min(dist[p - 1][i][j], dist[p - 1][i][p] + dist[p - 1][p][j])
$$
$dist[p - 1][i][j]$ 代表从 $i$ 到 $j$ 但必然不经过点 $p$ 的路径,$dist[p - 1][i][p] + dist[p - 1][p][j]$ 代表必然经过点 $p$ 的路径,两者中取较小值更新 $dist[p][i][j]$。

不难发现任意的 $dist[p][X][Y]$ 依赖于 $dist[p - 1][X][Y]$,可采用「滚动数组」的方式进行优化。

将 `dist` 声明为二维数组,$dist[i][j]$ 代表从点 $i$ 到点 $j$ 的最短距离,并采取 [枚举中转点 - 枚举起点 - 枚举终点] 三层循环的方式更新 $dist[i][j]$。

如此一来,跑一遍 `Floyd` 算法便可得出任意两点的最短距离。

通过上述推导,不难发现,我们并没提及边权的正负问题,因此 `Floyd` 算法对边权的正负没有限制要求(可处理正负权边的图),且能利用 `Floyd` 算法可能够对图中负环进行判定。

Java 代码:

```Java
class Solution {
    public int findTheCity(int n, int[][] edges, int distanceThreshold) {
        int[][] g = new int[n][n];
        // 初始化邻接矩阵
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < n; j++) {
                g[i][j] = i == j ? 0 : 0x3f3f3f3f;
            }
        }
        // 存图
        for (int[] e : edges) {
            int a = e[0], b = e[1], c = e[2];
            g[a][b] = g[b][a] = Math.min(g[a][b], c);
        }
        // 最短路
        floyd(g);
        // 统计答案
        int ans = -1, cnt = n + 10;
        for (int i = 0; i < n; i++) {
            int cur = 0;
            for (int j = 0; j < n; j++) {
                if (i != j && g[i][j] <= distanceThreshold) cur++;
            }
            if (cur <= cnt) {
                cnt = cur; ans = i;
            }
        }
        return ans;
    }
    void floyd(int[][] g) {
        int n = g.length;
        // floyd 基本流程为三层循环: [枚举中转点 - 枚举起点 - 枚举终点] => 松弛操作
        for (int p = 0; p < n; p++) {
            for (int i = 0; i < n; i++) {
                for (int j = 0; j < n; j++) {
                    g[i][j] = Math.min(g[i][j], g[i][p] + g[p][j]);
                }
            }
        }
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    int findTheCity(int n, vector<vector<int>>& edges, int distanceThreshold) {
        vector<vector<int>> g(n, vector<int>(n, 0x3f3f3f3f));
        // 初始化邻接矩阵
        for (int i = 0; i < n; i++) g[i][i] = 0;
        // 存图
        for (const auto& e : edges) {
            int a = e[0], b = e[1], c = e[2];
            g[a][b] = g[b][a] = min(g[a][b], c);
        }
        // 最短路
        floyd(g);
        // 统计答案
        int ans = -1, cnt = n + 10;
        for (int i = 0; i < n; i++) {
            int cur = 0;
            for (int j = 0; j < n; j++) {
                if (i != j && g[i][j] <= distanceThreshold) cur++;
            }
            if (cur <= cnt) {
                cnt = cur; ans = i;
            }
        }
        return ans;
    }
    void floyd(vector<vector<int>>& g) {
        int n = g.size();
        // floyd 基本流程为三层循环: [枚举中转点 - 枚举起点 - 枚举终点] => 松弛操作
        for (int p = 0; p < n; p++) {
            for (int i = 0; i < n; i++) {
                for (int j = 0; j < n; j++) {
                    g[i][j] = min(g[i][j], g[i][p] + g[p][j]);
                }
            }
        }
    }
};

```
Python 代码:
```Python
class Solution:
    def findTheCity(self, n: int, edges: List[List[int]], distanceThreshold: int) -> int:
        def floyd(g: List[List[int]]) -> None:
            n = len(g)
            # floyd 基本流程为三层循环: [枚举中转点 - 枚举起点 - 枚举终点] => 松弛操作
            for p in range(n):
                for i in range(n):
                    for j in range(n):
                        g[i][j] = min(g[i][j], g[i][p] + g[p][j])

        g = [[float('inf')] * n for _ in range(n)]
        # 初始化邻接矩阵
        for i in range(n):
            g[i][i] = 0
        # 存图
        for a, b, c in edges:
            g[a][b] = g[b][a] = min(g[a][b], c)
        # 最短路
        floyd(g)
        # 统计答案
        ans, cnt = -1, n + 10
        for i in range(n):
            cur = sum(1 for j in range(n) if i != j and g[i][j] <= distanceThreshold)
            if cur <= cnt:
                cnt, ans = cur, i
        return ans
```
TypeScript 代码:
```TypeScript
function findTheCity(n: number, edges: number[][], distanceThreshold: number): number {
    const floyd = function (g: number[][]): void {
        const n = g.length;
        // floyd 基本流程为三层循环: [枚举中转点 - 枚举起点 - 枚举终点] => 松弛操作
        for (let p = 0; p < n; p++) {
            for (let i = 0; i < n; i++) {
                for (let j = 0; j < n; j++) {
                    g[i][j] = Math.min(g[i][j], g[i][p] + g[p][j]);
                }
            }
        }
    }

    const g = Array.from({ length: n }, () => Array(n).fill(0x3f3f3f3f));
    // 初始化邻接矩阵
    for (let i = 0; i < n; i++) g[i][i] = 0;
    // 存图
    for (const [a, b, c] of edges) g[a][b] = g[b][a] = Math.min(g[a][b], c);
    // 最短路
    floyd(g);
    // 统计答案
    let ans = -1, cnt = n + 10;
    for (let i = 0; i < n; i++) {
        let cur = 0;
        for (let j = 0; j < n; j++) {
            if (i !== j && g[i][j] <= distanceThreshold) cur++;
        }
        if (cur <= cnt) {
            cnt = cur; ans = i;
        }
    }
    return ans;
};
```
* 时间复杂度:初始化邻接矩阵和建图复杂度为 $O(n^2 + m)$;`floyd` 算法复杂度为 $O(n^3)$;统计答案复杂度为 $O(n^2)$;整体复杂度为 $O(n^3 + m)$
* 空间复杂度:$O(n^2)$

---

### 朴素 Dijkstra(邻接矩阵)

最为经典的「单源最短路」算法,通常搭配「邻接矩阵」使用,应用在边数较多的“稠密图”上。

朴素 `Dijkstra` 算法基于「贪心」,通过维护一维的距离数组 `dist` 实现,$dist[i]$ 表示从源点出发到点 $i$ 的最短距离。

朴素 `Dijkstra` 算法在每一次迭代中,都选择 `dist` 中值最小的点进行松弛操作,逐渐扩展最短路径范围。

Java 代码:

```Java
class Solution {
    public int findTheCity(int n, int[][] edges, int distanceThreshold) {
        int[][] g = new int[n][n];
        // 初始化邻接矩阵
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < n; j++) {
                g[i][j] = i == j ? 0 : 0x3f3f3f3f;
            }
        }
        // 存图
        for (int[] e : edges) {
            int a = e[0], b = e[1], c = e[2];
            g[a][b] = g[b][a] = Math.min(g[a][b], c);
        }
        int ans = -1, cnt = n + 10;
        for (int i = 0; i < n; i++) {
            // 单源最短路
            int[] dist = dijkstra(g, i);
            int cur = 0;
            for (int j = 0; j < n; j++) {
                if (i != j && dist[j] <= distanceThreshold) cur++;
            }
            if (cur <= cnt) {
                cnt = cur; ans = i;
            }
        }
        return ans;
    }
    int[] dijkstra(int[][] g, int x) {
        int n = g.length;
        // 起始先将所有的点标记为「未更新」和「距离为正无穷」
        boolean[] vis = new boolean[n];
        int[] dist = new int[n];
        Arrays.fill(dist, 0x3f3f3f3f);
        // 只有起点最短距离为 0
        dist[x] = 0;
        // 有多少个点就迭代多少次
        for (int k = 0; k < n; k++) {
            // 每次找到「最短距离最小」且「未被更新」的点 t
            int t = -1;
            for (int i = 0; i < n; i++) {
                if (!vis[i] && (t == -1 || dist[i] < dist[t])) t = i;
            }
            // 标记点 t 为已更新
            vis[t] = true;
            // 用点 t 的「最小距离」更新其他点
            for (int i = 0; i < n; i++) dist[i] = Math.min(dist[i], dist[t] + g[t][i]);
        }
        return dist;
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    int findTheCity(int n, vector<vector<int>>& edges, int distanceThreshold) {
        vector<vector<int>> g(n, vector<int>(n, 0x3f3f3f3f));
        // 初始化邻接矩阵
        for (int i = 0; i < n; i++) g[i][i] = 0;
        // 存图
        for (const auto& e : edges) {
            int a = e[0], b = e[1], c = e[2];
            g[a][b] = g[b][a] = min(g[a][b], c);
        }
        int ans = -1, cnt = n + 10;
        for (int i = 0; i < n; i++) {
            // 单源最短路
            vector<int> dist = dijkstra(g, i);
            int cur = count_if(dist.begin(), dist.end(), [distanceThreshold](int d) { return d <= distanceThreshold; });
            if (cur <= cnt) {
                cnt = cur; ans = i;
            }
        }
        return ans;
    }
    vector<int> dijkstra(const vector<vector<int>>& g, int x) {
        int n = g.size();
        vector<bool> vis(n, false);
        vector<int> dist(n, 0x3f3f3f3f);
        // 只有起点最短距离为 0
        dist[x] = 0;
        // 有多少个点就迭代多少次
        for (int k = 0; k < n; k++) {
            // 每次找到「最短距离最小」且「未被更新」的点 t
            int t = -1;
            for (int i = 0; i < n; i++) {
                if (!vis[i] && (t == -1 || dist[i] < dist[t])) t = i;
            }
            // 标记点 t 为已更新
            vis[t] = true;
            // 用点 t 的「最小距离」更新其他点
            for (int i = 0; i < n; i++) dist[i] = min(dist[i], dist[t] + g[t][i]);
        }
        return dist;
    }
};
```
Python 代码:
```Python
class Solution:
    def findTheCity(self, n: int, edges: List[List[int]], distanceThreshold: int) -> int:
        def dijkstra(g, x):
            n = len(g)
            vis = [False] * n
            dist = [float('inf')] * n
            # 只有起点最短距离为 0
            dist[x] = 0
            # 有多少个点就迭代多少次
            for k in range(n):
                # 每次找到「最短距离最小」且「未被更新」的点 t
                t = min((i for i in range(n) if not vis[i]), key=lambda i: dist[i])
                # 标记点 t 为已更新
                vis[t] = True
                # 用点 t 的「最小距离」更新其他点
                for i in range(n):
                    dist[i] = min(dist[i], dist[t] + g[t][i])
            return dist

        g = [[float('inf')] * n for _ in range(n)]
        # 初始化邻接矩阵
        for i in range(n):
            g[i][i] = 0
        # 存图
        for a, b, c in edges:
            g[a][b] = g[b][a] = min(g[a][b], c)
        ans, cnt = -1, n + 10
        for i in range(n):
            # 单源最短路
            dist = dijkstra(g, i)
            cur = sum(1 for j in range(n) if i != j and dist[j] <= distanceThreshold)
            if cur <= cnt:
                cnt, ans = cur, i
        return ans
```
TypeScript 代码:
```TypeScript
function findTheCity(n: number, edges: number[][], distanceThreshold: number): number {
    const dijkstra = function (g: number[][], x: number): number[]  {
        const n = g.length;
        const vis = Array(n).fill(false), dist = Array(n).fill(0x3f3f3f3f);
        // 只有起点最短距离为 0
        dist[x] = 0;
        // 有多少个点就迭代多少次
        for (let k = 0; k < n; k++) {
            // 每次找到「最短距离最小」且「未被更新」的点 t
            let t = -1;
            for (let i = 0; i < n; i++) {
                if (!vis[i] && (t === -1 || dist[i] < dist[t])) t = i;
            }
            // 标记点 t 为已更新
            vis[t] = true;
            // 用点 t 的「最小距离」更新其他点
            for (let i = 0; i < n; i++) dist[i] = Math.min(dist[i], dist[t] + g[t][i]);
        }
        return dist;
    }

    const g = Array.from({ length: n }, () => Array(n).fill(0x3f3f3f3f));
    // 初始化邻接矩阵
    for (let i = 0; i < n; i++) g[i][i] = 0;
    // 存图
    for (const [a, b, c] of edges) g[a][b] = g[b][a] = Math.min(g[a][b], c);
    let ans = -1, cnt = n + 10;
    for (let i = 0; i < n; i++) {
        // 单源最短路
        const dist = dijkstra(g, i);
        const cur = dist.filter(d => d <= distanceThreshold).length;
        if (cur <= cnt) {
            cnt = cur; ans = i;
        }
    }
    return ans;
};
```
* 时间复杂度:初始化邻接矩阵和建图复杂度为 $O(n^2 + m)$;统计答案时,共执行 $n$ 次朴素 `dijkstra` 算法,朴素 `dijkstra` 复杂度为 $O(n^2)$,总复杂度为 $O(n^3)$。整体复杂度为 $O(n^3 + m)$
* 空间复杂度:$O(n^2)$

---

### 堆优化 Dijkstra(邻接表)

堆优化 `Dijkstra` 算法与朴素 `Dijkstra` 算法都是「单源最短路」算法。

堆优化 `Dijkstra` 算法通过数据结构「优先队列(堆)」来优化朴素 `Dijkstra` 中的“找 `dist` 中值最小的点”的过程。

相比于复杂度与边数无关的 $O(n^2)$ 朴素 `Dijkstra` 算法,复杂度与边数相关的 $O(m\log{n})$ 堆优化 `Dijkstra` 算法更适合边较少的“稀疏图”。

Java 代码:

```Java
class Solution {
    int N = 110, M = N * N, INF = 0x3f3f3f3f, idx, n;
    int[] he = new int[N], e = new int[M], ne = new int[M], w = new int[M];
    void add(int a, int b, int c) {
        e[idx] = b;
        ne[idx] = he[a];
        w[idx] = c;
        he[a] = idx++;
    }
    public int findTheCity(int _n, int[][] edges, int distanceThreshold) {
        n = _n;
        // 初始化链表头
        Arrays.fill(he, -1);
        // 存图
        for (int[] e : edges) {
            int a = e[0], b = e[1], c = e[2];
            add(a, b, c); add(b, a, c);
        }
        // 统计答案
        int ans = -1, cnt = n + 10;
        for (int i = 0; i < n; i++) {
            // 单源最短路
            int[] dist = dijkstra(i);
            int cur = 0;
            for (int j = 0; j < n; j++) {
                if (i != j && dist[j] <= distanceThreshold) cur++;
            }
            if (cur <= cnt) {
                cnt = cur; ans = i;
            }
        }
        return ans;
    }
    int[] dijkstra(int x) {
        // 起始先将所有的点标记为「未更新」和「距离为正无穷」
        int[] dist = new int[n];
        Arrays.fill(dist, 0x3f3f3f3f);
        boolean[] vis = new boolean[n];
        dist[x] = 0;
        // 使用「优先队列」存储所有可用于更新的点
        // 以 (点编号, 到起点的距离) 进行存储,优先弹出「最短距离」较小的点
        PriorityQueue<int[]> q = new PriorityQueue<>((a,b)->a[1]-b[1]);
        q.add(new int[]{x, 0});
        while (!q.isEmpty()) {
            // 每次从「优先队列」中弹出
            int[] poll = q.poll();
            int u = poll[0], step = poll[1];
            // 如果弹出的点被标记「已更新」,则跳过
            if (vis[u]) continue;
            // 标记该点「已更新」,并使用该点更新其他点的「最短距离」
            vis[u] = true;
            for (int i = he[u]; i != -1; i = ne[i]) {
                int j = e[i];
                if (dist[j] <= dist[u] + w[i]) continue;
                dist[j] = dist[u] + w[i];
                q.add(new int[]{j, dist[j]});
            }
        }
        return dist;
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    static const int N = 110, M = N * N;
    int he[N], e[M], ne[M], w[M], idx, n, INF = 0x3f3f3f3f;
    void add(int a, int b, int c) {
        e[idx] = b;
        ne[idx] = he[a];
        w[idx] = c;
        he[a] = idx++;
    }
    int findTheCity(int _n, vector<vector<int>>& edges, int distanceThreshold) {
        n = _n;
        // 初始化链表头
        fill(he, he + n, -1);
        // 存图
        for (const auto& e : edges) {
            int a = e[0], b = e[1], c = e[2];
            add(a, b, c); add(b, a, c);
        }
        // 统计答案
        int ans = -1, cnt = n + 10;
        for (int i = 0; i < n; i++) {
            // 单源最短路
            vector<int> dist = dijkstra(i);
            int cur = count_if(dist.begin(), dist.end(), [distanceThreshold](int d) { return d <= distanceThreshold; });
            if (cur <= cnt) {
                cnt = cur; ans = i;
            }
        }
        return ans;
    }
    vector<int> dijkstra(int x) {
        // 起始先将所有的点标记为「未更新」和「距离为正无穷」
        vector<int> dist(n, INF);
        vector<bool> vis(n, false);
        dist[x] = 0;
        // 使用「优先队列」存储所有可用于更新的点
        // 以 (到起点的距离, 点编号) 进行存储,优先弹出「最短距离」较小的点
        priority_queue<pair<int, int>, vector<pair<int, int>>, greater<pair<int, int>>> q;
        q.push({0, x});
        while (!q.empty()) {
            // 每次从「优先队列」中弹出
            auto [step, u] = q.top();
            q.pop();
            // 如果弹出的点被标记「已更新」,则跳过
            if (vis[u]) continue;
            // 标记该点「已更新」,并使用该点更新其他点的「最短距离」
            vis[u] = true;
            for (int i = he[u]; i != -1; i = ne[i]) {
                int j = e[i];
                if (dist[j] <= dist[u] + w[i]) continue;
                dist[j] = dist[u] + w[i];
                q.push({dist[j], j});
            }
        }
        return dist;
    }
};
```
Python 代码:
```Python
import heapq

class Solution:
    def findTheCity(self, n: int, edges: List[List[int]], distanceThreshold: int) -> int:
        N, M, INF, idx = 110, 110 * 110, float('inf'), 0
        he, e, ne, w = [-1] * N, [0] * M, [0] * M, [0] * M

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

        def dijkstra(x):
            # 起始先将所有的点标记为「未更新」和「距离为正无穷」
            dist = [float('inf')] * n
            vis = [False] * n
            dist[x] = 0
            # 使用「优先队列」存储所有可用于更新的点
            # 以 (点编号, 到起点的距离) 进行存储,优先弹出「最短距离」较小的点
            q = [(0, x)]
            heapq.heapify(q)
            while q:
                # 每次从「优先队列」中弹出
                step, u = heapq.heappop(q)
                # 如果弹出的点被标记「已更新」,则跳过
                if vis[u]: continue
                # 标记该点「已更新」,并使用该点更新其他点的「最短距离」
                vis[u] = True
                i = he[u]
                while i != -1:
                    j, c = e[i], w[i]
                    i = ne[i]
                    if dist[j] <= dist[u] + c: continue
                    dist[j] = dist[u] + c
                    heapq.heappush(q, (dist[j], j))
            return dist

        # 初始化链表头
        he = [-1] * N
        # 存图
        for a, b, c in edges:
            add(a, b, c)
            add(b, a, c)
        # 统计答案
        ans, cnt = -1, n + 10
        for i in range(n):
            # 单源最短路
            dist = dijkstra(i)
            cur = sum(1 for j in range(n) if i != j and dist[j] <= distanceThreshold)
            if cur <= cnt:
                cnt, ans = cur, i
        return ans
```
* 时间复杂度:初始化邻接表和建图复杂度为 $O(n + m)$;统计答案时,共执行 $n$ 次堆优化 `dijkstra` 算法,堆优化 `dijkstra` 复杂度为 $O(m\log{n})$,总复杂度为 $O(nm\log{n})$。整体复杂度为 $O(nm\log{n})$
* 空间复杂度:$O(n + m)$

---

### Bellman Ford(类)

虽然题目规定了不存在「负权边」,但我们仍然可以使用可以在「负权图中求最短路」的 `Bellman Ford` 进行求解,该算法也是「单源最短路」算法,复杂度为 $O(n \times m)$。

通常为了确保 $O(n \times m)$,可以单独建一个类代表边,将所有边存入集合中,在 $n$ 次松弛操作中直接对边集合进行遍历。

由于本题边数量级为 $n^2$,共对 $n$ 个点执行 `Bellman Ford` 算法,因此整体会去到 $O(n^4)$,有 `TLE` 风险。

Java 代码:

```Java
class Solution {
    int n;
    public int findTheCity(int _n, int[][] edges, int distanceThreshold) {
        n = _n;
        int ans = -1, cnt = n + 10;
        for (int i = 0; i < n; i++) {
            int[] dist = bf(edges, i);
            int cur = 0;
            for (int j = 0; j < n; j++) {
                if (i != j && dist[j] <= distanceThreshold) cur++;
            }
            if (cur <= cnt) {
                cnt = cur; ans = i;
            }
        }
        return ans;
    }
    int[] bf(int[][] edges, int x) {
        int[] dist = new int[n];
        // 起始先将所有的点标记为「距离为正无穷」, 只有起点最短距离为 0
        Arrays.fill(dist, 0x3f3f3f3f);
        dist[x] = 0;
        // 有多少个点就迭代多少次
        for (int k = 0; k < n; k++) {
            // 每次都使用上一次迭代的结果,执行松弛操作
            int[] prev = dist.clone();
            for (int[] e : edges) {
                int a = e[0], b = e[1], c = e[2];
                dist[b] = Math.min(dist[b], prev[a] + c);
                dist[a] = Math.min(dist[a], prev[b] + c);
            }
        }
        return dist;
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    int findTheCity(int n, vector<vector<int>>& edges, int distanceThreshold) {
        int ans = -1, cnt = n + 10;
        for (int i = 0; i < n; i++) {
            vector<int> dist = bf(edges, i, n);
            int cur = 0;
            for (int j = 0; j < n; j++) {
                if (i != j && dist[j] <= distanceThreshold) cur++;
            }
            if (cur <= cnt) {
                cnt = cur; ans = i;
            }
        }
        return ans;
    }
    vector<int> bf(vector<vector<int>>& edges, int x, int n) {
        // 起始先将所有的点标记为「距离为正无穷」, 只有起点最短距离为 0
        vector<int> dist(n, 0x3f3f3f3f);
        dist[x] = 0;
        // 有多少个点就迭代多少次
        for (int k = 0; k < n; k++) {
            // 每次都使用上一次迭代的结果,执行松弛操作
            vector<int> prev = dist;
            for (const auto& e : edges) {
                int a = e[0], b = e[1], c = e[2];
                dist[b] = min(dist[b], prev[a] + c);
                dist[a] = min(dist[a], prev[b] + c);
            }
        }
        return dist;
    }
};
```
Python 代码:
```Python
class Solution:
    def findTheCity(self, n: int, edges: List[List[int]], distanceThreshold: int) -> int:
        def bf(edges: List[List[int]], x: int, n: int) -> List[int]:
            # 起始先将所有的点标记为「距离为正无穷」, 只有起点最短距离为 0
            dist = [float('inf')] * n
            dist[x] = 0
            # 有多少个点就迭代多少次
            for k in range(n):
                # 每次都使用上一次迭代的结果,执行松弛操作
                prev = dist.copy()
                for a, b, c in edges:
                    dist[b] = min(dist[b], prev[a] + c)
                    dist[a] = min(dist[a], prev[b] + c)
            return dist

        ans, cnt = -1, n + 10
        for i in range(n):
            dist = bf(edges, i, n)
            cur = sum(1 for j in range(n) if i != j and dist[j] <= distanceThreshold)
            if cur <= cnt:
                cnt, ans = cur, i
        return ans
```
TypeScript 代码:
```TypeScript
function findTheCity(n: number, edges: number[][], distanceThreshold: number): number {
    const bf = function(x: number): number[] {
        // 起始先将所有的点标记为「距离为正无穷」, 只有起点最短距离为 0
        const dist = new Array(n).fill(0x3f3f3f3f);
        dist[x] = 0;
        // 有多少个点就迭代多少次
        for (let k = 0; k < n; k++) {
            // 每次都使用上一次迭代的结果,执行松弛操作
            const prev = dist.slice();
            for (const e of edges) {
                const a = e[0], b = e[1], c = e[2];
                dist[b] = Math.min(dist[b], prev[a] + c);
                dist[a] = Math.min(dist[a], prev[b] + c);
            }
        }
        return dist;
    }

    let ans = -1, cnt = n + 10;
    for (let i = 0; i < n; i++) {
        const dist = bf(i);
        let cur = 0;
        for (let j = 0; j < n; j++) {
            if (i !== j && dist[j] <= distanceThreshold) cur++;
        }
        if (cur <= cnt) {
            cnt = cur; ans = i;
        }
    }
    return ans;
};
```
* 时间复杂度:统计答案时,共执行 $n$ 次 `Bellman Ford` 算法, `Bellman Ford` 复杂度为 $O(nm)$,总复杂度为 $O(n^2 \times m)$。整体复杂度为 $O(n^2 \times m)$
* 空间复杂度:$O(n + m)$

---

### SPFA(邻接表)

`SPFA` 也是一类能够处理「负权边」的单源最短路算法。

最坏情况下,复杂度为 $O(n \times m)$,在特定情况下,其效率优于 `Dijkstra` 算法,近似 $O(m)$。

基本执行流程如下:

1. 用双端队列来维护待更新节点,初始将源点放入队列
2. 每次从队列头中取出一个节点,对其所有相邻节点执行松弛操作
   1. 若某个相邻节点的最短距离发生了更新,且该节点不在队列中,将它加入队列中

3. 重复以上步骤,直到队列为空

Java 代码:
```Java
class Solution {
    int N = 110, M = N * N, INF = 0x3f3f3f3f, idx, n;
    int[] he = new int[N], e = new int[M], ne = new int[M], w = new int[M];
    void add(int a, int b, int c) {
        e[idx] = b;
        ne[idx] = he[a];
        w[idx] = c;
        he[a] = idx++;
    }
    public int findTheCity(int _n, int[][] edges, int distanceThreshold) {
        n = _n;
        // 初始化链表头
        Arrays.fill(he, -1);
        // 存图
        for (int[] e : edges) {
            int a = e[0], b = e[1], c = e[2];
            add(a, b, c); add(b, a, c);
        }
        // 统计答案
        int ans = -1, cnt = n + 10;
        for (int i = 0; i < n; i++) {
            // 单源最短路
            int[] dist = spfa(i);
            int cur = 0;
            for (int j = 0; j < n; j++) {
                if (i != j && dist[j] <= distanceThreshold) cur++;
            }
            if (cur <= cnt) {
                cnt = cur; ans = i;
            }
        }
        return ans;
    }
    int[] spfa(int x) {
        int[] dist = new int[n];
        boolean[] vis = new boolean[n];
        // 起始先将所有的点标记为「未入队」和「距离为正无穷」
        Arrays.fill(dist, INF);
        // 只有起点最短距离为 0
        dist[x] = 0;
        // 使用「双端队列」存储,存储的是点编号
        Deque<Integer> d = new ArrayDeque<>();
        // 将「源点/起点」进行入队,并标记「已入队」
        d.addLast(x);
        vis[x] = true;
        while (!d.isEmpty()) {
            // 每次从「双端队列」中取出,并标记「未入队」
            int u = d.pollFirst();
            vis[u] = false;
            // 尝试使用该点,更新其他点的最短距离
            // 如果更新的点,本身「未入队」则加入队列中,并标记「已入队」
            for (int i = he[u]; i != -1; i = ne[i]) {
                int j = e[i];
                if (dist[j] <= dist[u] + w[i]) continue;
                dist[j] = dist[u] + w[i];
                if (vis[j]) continue;
                d.addLast(j);
                vis[j] = true;
            }
        }
        return dist;
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    static const int N = 110, M = N * N;
    int he[N], e[M], ne[M], w[M], idx, n, INF = 0x3f3f3f3f;
    void add(int a, int b, int c) {
        e[idx] = b;
        ne[idx] = he[a];
        w[idx] = c;
        he[a] = idx++;
    }
    int findTheCity(int _n, vector<vector<int>>& edges, int distanceThreshold) {
        n = _n;
        fill(he, he + N, -1);
        for (const auto& e : edges) {
            int a = e[0], b = e[1], c = e[2];
            add(a, b, c); add(b, a, c);
        }
        int ans = -1, cnt = n + 10;
        for (int i = 0; i < n; i++) {
            vector<int> dist = spfa(i);
            int cur = count_if(dist.begin(), dist.end(), [&](int d) { return d != INF && d <= distanceThreshold; });
            if (cur <= cnt) {
                cnt = cur; ans = i;
            }
        }
        return ans;
    }
    vector<int> spfa(int x) {
        // 起始先将所有的点标记为「未入队」和「距离为正无穷」
        vector<int> dist(n, INF);
        vector<bool> vis(n, false);
        // 只有起点最短距离为 0
        dist[x] = 0;
        // 使用「双端队列」存储,存储的是点编号
        deque<int> d;
        // 将「源点/起点」进行入队,并标记「已入队」
        d.push_back(x);
        vis[x] = true;
        while (!d.empty()) {
            // 每次从「双端队列」中取出,并标记「未入队」
            int u = d.front();
            d.pop_front();
            vis[u] = false;
            // 尝试使用该点,更新其他点的最短距离
            // 如果更新的点,本身「未入队」则加入队列中,并标记「已入队」
            for (int i = he[u]; i != -1; i = ne[i]) {
                int j = e[i];
                if (dist[j] <= dist[u] + w[i]) continue;
                dist[j] = dist[u] + w[i];
                if (vis[j]) continue;
                d.push_back(j);
                vis[j] = true;
            }
        }
        return dist;
    }
};
```
Python 代码:
```Python
class Solution:
    def findTheCity(self, n: int, edges: List[List[int]], distanceThreshold: int) -> int:
        m, INF, idx = n * n, 0x3f3f3f3f, 0
        he, e, ne, w = [-1] * n, [0] * m, [0] * m, [0] * m

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

        def spfa(x: int) -> List[int]:
            # 起始先将所有的点标记为「未入队」和「距离为正无穷」
            dist = [INF] * n
            vis = [False] * n
            # 只有起点最短距离为 0
            dist[x] = 0
            # 使用「双端队列」存储,存储的是点编号
            d = deque()
            # 将「源点/起点」进行入队,并标记「已入队」
            d.append(x)
            vis[x] = True
            while d:
                # 每次从「双端队列」中取出,并标记「未入队」
                u = d.popleft()
                vis[u] = False
                i = he[u]
                # 尝试使用该点,更新其他点的最短距离
                # 如果更新的点,本身「未入队」则加入队列中,并标记「已入队」
                while i != -1:
                    j, c = e[i], w[i]
                    i = ne[i]
                    if dist[j] <= dist[u] + c: continue
                    dist[j] = dist[u] + c
                    if vis[j]: continue
                    d.append(j)
                    vis[j] = True
            return dist

        for a, b, c in edges:
            add(a, b, c)
            add(b, a, c)

        ans, cnt = -1, n + 10
        for i in range(n):
            dist = spfa(i)
            cur = sum(1 for d in dist if d != INF and d <= distanceThreshold)
            if cur <= cnt:
                cnt, ans = cur, i
        return ans
```
* 时间复杂度:统计答案时,共执行 $n$ 次 `spfa` 算法, `spfa` 复杂度为 $O(nm)$,总复杂度为 $O(n^2 \times m)$。整体复杂度为 $O(n^2 \times m)$
* 空间复杂度:$O(n + m)$

### 最后

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

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

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

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


LeetCode/1331-1340/1337. 矩阵中战斗力最弱的 K 行（简单）.md

1337. 矩阵中战斗力最弱的 K 行

Question:
给你一个大小为 $m * n$ 的矩阵 $mat$,矩阵由若干军人和平民组成,分别用 $1$ 和 $0$ 表示。

请你返回矩阵中战斗力最弱的 $k$ 行的索引,按从最弱到最强排序。

如果第 $i$ 行的军人数量少于第 $j$ 行,或者两行军人数量相同但 $i$ 小于 $j$,那么我们认为第 $i$ 行的战斗力比第 $j$ 行弱。

军人 总是 排在一行中的靠前位置,也就是说 $1$ 总是出现在 $0$ 之前。

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

输出:[2,0,3]

解释:
每行中的军人数目:
行 0 -> 2
行 1 -> 4
行 2 -> 1
行 3 -> 2
行 4 -> 5
从最弱到最强对这些行排序后得到 [2,0,3,1,4]
```
示例 2:
```
输入:mat =
[[1,0,0,0],
 [1,1,1,1],
 [1,0,0,0],
 [1,0,0,0]],
k = 2

输出:[0,2]

解释:
每行中的军人数目:
行 0 -> 1
行 1 -> 4
行 2 -> 1
行 3 -> 1
从最弱到最强对这些行排序后得到 [0,2,3,1]
```

提示:
* m == mat.length
* n == mat[i].length
* 2 <= n, m <= 100
* 1 <= k <= m
* matrix[i][j] 不是 0 就是 1

Solution:
### 朴素解法

一个朴素的做法是对矩阵进行遍历,统计每一行的军人数量,并以二元组 $(cnt_i, idx_i)$ 的形式进行存储。

然后对所有行的战力进行排序,选出战力最小的 $k$ 个下标即是答案。

代码:
```Java
class Solution {
    public int[] kWeakestRows(int[][] mat, int k) {
        int m = mat.length, n = mat[0].length;
        int[][] all = new int[m][1];
        for (int i = 0; i < m; i++) {
            int cur = 0;
            for (int j = 0; j < n; j++) cur += mat[i][j];
            all[i] = new int[]{cur, i};
        }
        Arrays.sort(all, (a, b)->{
            if (a[0] != b[0]) return a[0] - b[0];
            return a[1] - b[1];
        });
        int[] ans = new int[k];
        for (int i = 0; i < k; i++) ans[i] = all[i][1];
        return ans;
    }
}
```
* 时间复杂度:遍历矩阵的复杂度为 $O(m * n)$;排序复杂度为 $O(m\log{m})$;构造答案复杂度为 $O(k)$。整体复杂度为 $O(\max(m * n, m\log{m}))$
* 空间复杂度:$O(m)$ 空间用于存储所有的行战力;$O(\log{m})$ 空间用于排序。整体复杂度为 $O(m + \log{m})$

---

### 二分 + 优先队列(堆)

根据「军人总是排在靠前位置」的特性,我们可以通过「二分」找到每一行最后一个军人的位置,从而在 $O(\log{n})$ 的复杂度内统计出每行的军人数量(战力情况)。

同时由于我们只需要「战力最弱」的 $k$ 行数据,这引导我们可以建立一个「战力大根堆」来做,「战力大根堆」存放着数量最多为 $k$ 的战力二元组 $(cnt_i, idx_i)$,堆顶元素为战力最大的数对。

每次通过「二分」得到当前行的战力值后,判断当前战力值与堆顶元素的战力大小关系:

* 如果当前战力值比堆顶的元素要大:直接丢弃当前战力值(不可能属于在第 $k$ 小的集合中);
* 如果当前战力值比堆顶的元素要小:将堆顶元素弹出,将当前行放入堆中。

> 一些细节:每次写二分都有同学会问,`check` 函数怎么写,可以重点看看 [34 题题解](https://leetcode-cn.com/problems/find-first-and-last-position-of-element-in-sorted-array/solution/gong-shui-san-xie-kao-cha-dui-er-fen-de-86bk0/)。由于考虑某行没有军人的情况,我们需要二分完检查一下分割点是否符合 `check` 来决定军人数量。
另外,从堆弹出和往堆存入,需要与当前堆元素的数量有所关联。

代码:
```Java
class Solution {
    public int[] kWeakestRows(int[][] mat, int k) {
        int m = mat.length, n = mat[0].length;
        PriorityQueue<int[]> q = new PriorityQueue<>((a,b)->{
            if (a[0] != b[0]) return b[0] - a[0];
            return b[1] - a[1];
        });
        for (int i = 0; i < m; i++) {
            int l = 0, r = n - 1;
            while (l < r) {
                int mid = l + r + 1 >> 1;
                if (mat[i][mid] >= 1) l = mid;
                else r = mid - 1;
            }
            int cur = mat[i][r] >= 1 ? r + 1 : r;
            if (q.size() == k && q.peek()[0] > cur) q.poll();
            if (q.size() < k) q.add(new int[]{cur, i});
        }
        int[] ans = new int[k];
        int idx = k - 1;
        while (!q.isEmpty()) ans[idx--] = q.poll()[1];
        return ans;
    }
}
```
* 时间复杂度:二分得到每行的战力情况,复杂度为 $O(m\log{n})$;堆中最多有 $k$ 个元素,将行信息存入堆中复杂度为 $O(m\log{k});$构造答案复杂度为 $O(k\log{k})$。整体复杂度为 $O(m * (\log{n} + \log{k}))$
* 空间复杂度:$O(k)$

### 最后

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

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

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

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


LeetCode/1341-1350/1342. 将数字变成 0 的操作次数（简单）.md

1342. 将数字变成 0 的操作次数

Question:
给你一个非负整数 `num` ,请你返回将它变成 $0$ 所需要的步数。 如果当前数字是偶数,你需要把它除以 $2$ ;否则,减去 $1$ 。

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

输出:6

解释:
步骤 1) 14 是偶数,除以 2 得到 7 。
步骤 2) 7 是奇数,减 1 得到 6 。
步骤 3) 6 是偶数,除以 2 得到 3 。
步骤 4) 3 是奇数,减 1 得到 2 。
步骤 5) 2 是偶数,除以 2 得到 1 。
步骤 6) 1 是奇数,减 1 得到 0 。
```
示例 2:
```
输入:num = 8

输出:4

解释:
步骤 1) 8 是偶数,除以 2 得到 4 。
步骤 2) 4 是偶数,除以 2 得到 2 。
步骤 3) 2 是偶数,除以 2 得到 1 。
步骤 4) 1 是奇数,减 1 得到 0 。
```
示例 3:
```
输入:num = 123

输出:12
```

提示:
* $0 <= num <= 10^6$

Solution:
### 模拟

根据题意进行模拟:**根据当前 $num$ 的奇偶性进行对应操作,直到 $num$ 为 $0$,返回操作次数。**

代码:
```Java
class Solution {
    public int numberOfSteps(int num) {
        int ans = 0;
        while (num != 0 && ++ans >= 0) num = num % 2 == 0 ? num / 2 : num - 1;
        return ans;
    }
}
```
* 时间复杂度:$O(\log{num})$
* 空间复杂度:$O(1)$

---

### 数学(二进制性质)

目的是使得最终结果的二进制表示均为 $0$,两种操作对二进制数的影响分别为「整体右移」和「消减最低位的 $1$」。

因此整个模拟过程其实是:**如果当前的 $num$ 最低位不为 $1$(偶数),则不断进行右移,直到最低位为 $1$(奇数),然后再对最低位的 $1$ 进行消减,直到二进制表示中的所有 $1$ 均被消减完(结果为 $0$),模拟过程结束。**

换句话说,总的操作次数为 = 右移次数 + 消减次数 :

* 右移次数:$num$ 中最高位 $1$ 的所在的位置;
* 消减次数:$num$ 中 $1$ 的个数。

代码:
```Java
class Solution {
    public int numberOfSteps(int num) {
        return Math.max(getLoc(num) + getCnt(num) - 1, 0);
    }
    int getLoc(int x) {
        for (int i = 31; i >= 0; i--) {
            if (((x >> i) & 1) == 1) return i + 1;
        }
        return -1; // never
    }
    int getCnt(int x) {
        int ans = 0;
        for (int i = 31; i >= 0; i--) {
            if (((x >> i) & 1) == 1) ans++;
        }
        return ans;
    }
}
```
-
```Java
class Solution {
    public int numberOfSteps(int num) {
        return Math.max(getLoc(num) + getCnt(num) - 1, 0);
    }
    int getLoc(int x) {
        for (int i = 31; i >= 0; i--) {
            if (((x >> i) & 1) == 1) return i + 1;
        }
        return -1; // never
    }
    int getCnt(int x) {
        int ans = 0;
        while (x != 0 && ++ans >= 0) x -= (x & -x); // lowbit
        return ans;
    }
}
```
* 时间复杂度:令 $C$ 为 $int$ 二进制表示中的最大长度,复杂度为 $O(C)$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


Question:
给你一个整数数组 `arr` ,你一开始在数组的第一个元素处(下标为 `0`)。

每一步,你可以从下标 i 跳到下标:
* `i + 1` 满足:`i + 1 < arr.length`
* `i - 1` 满足:`i - 1 >= 0`
* `j` 满足:`arr[i] == arr[j]` 且 `i != j`

请你返回到达数组最后一个元素的下标处所需的 **最少操作次数** 。

注意:任何时候你都不能跳到数组外面。

示例 1:
```
输入:arr = [100,-23,-23,404,100,23,23,23,3,404]

输出:3

解释:那你需要跳跃 3 次,下标依次为 0 --> 4 --> 3 --> 9 。下标 9 为数组的最后一个元素的下标。
```
示例 2:
```
输入:arr = [7]

输出:0

解释:一开始就在最后一个元素处,所以你不需要跳跃。
```
示例 3:
```
输入:arr = [7,6,9,6,9,6,9,7]

输出:1

解释:你可以直接从下标 0 处跳到下标 7 处,也就是数组的最后一个元素处。
```
示例 4:
```
输入:arr = [6,1,9]

输出:2
```
示例 5:
```
输入:arr = [11,22,7,7,7,7,7,7,7,22,13]

输出:3
```

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

Solution:
### 单向 BFS

根据跳跃规则,我们能够进行「前后跳」和「等值跳」,问题为到达结尾位置的最少步数,容易想到 `BFS`。

为了方便进行「等值跳」,我们可以先使用「哈希表」记录某个值有哪些下标。

在进行 `BFS` 时,假如当前走到的位置为 $t$,我们尝试将 $t - 1$、$t + 1$ 和与 $arr[t]$ 等值的位置进行入队,为了防止重复同队,我们可以使用 $dist$ 数组记录到达某个位置的最小步数(初始化为 `INF`),只有 $dist[ne]$ 为 `INF` 时,该点没有被遍历过,可以入队并更新最小步数。

但光使用 $dist$ 还不能确保复杂度为 $O(n)$,因为每次都需要遍历与 $arr[t]$ 等值的下标,为确保等值下标的遍历只会发生一次,我们需要在将等值下标添加到队列后,将 $arr[t]$ 从哈希表中移除。

容易证明每次将于 $arr[t]$ 的等值元素添加到队列后,将 $arr[t]$ 从哈希表中移除的正确性:

首次检索到 $arr[t]$ 值时,必然是最小步数,记为 $step$,此时 `BFS` 做法将其他等值下标距离更新为 $step + 1$:

* 若 $arr[t]$ 与结尾元素值相等,且 $t$ 为 $n - 1$,此时 $step$ 即是答案;
* 若 $arr[t]$ 与结尾元素值相等,但 $t$ 不为 $n - 1$,此时会再跳一步到达结尾位置,即 $step + 1$ 为答案。那么是否可能存在使用比 $step + 1$ 更小的步数,也能到达结尾的位置呢?
    答案是:**可能存在,但如果最后是通过「等值跳」到达结尾位置的话,不可能存在比 $step + 1$ 更小的步数。**
    由于我们每次加入等值时都会进行哈希表的移除,因此到达 $t$ 的方式不可能是「等值跳」,而只能是「前后跳」。

    * 假设是通过前跳到达位置 $t$,即点分布如图,步数满足等于 $step + 1$:

    * 假设是通过后跳到达位置 $t$,即点分布如图,步数满足「如果是等值跳到达结尾,步数为 $step + 1$」:

    **综上,如果 $n - 1$ 是经过「等值跳」加入队列的话,起所能达到的最小步数必然为发起点 $t$ 的最小步数 $+1$。**

    **也就是说,即使首次等值跳,加入队列后会将其从哈希表中进行移除,正确性也是可以保证的。**

基于此,我们可以额外增加一个 trick,就是在构建哈希表的时候,使用「倒序」的形式构建等值下标列表,这样可以确保如果最后位置是通过「等值跳」而来是,能够优先出队。

**代码(感谢 [@Benhao](/u/himymben/) 和 [@🍭可乐可乐吗](/u/littletime_cc/) 同学提供的其他语言版本):**
```Java
class Solution {
    int INF = 0x3f3f3f3f;
    public int minJumps(int[] arr) {
        int n = arr.length;
        Map<Integer, List<Integer>> map = new HashMap<>();
        // 倒序插入 list,相当于给 deque 增加一个同层「下标越大,优先出队」的作用
        for (int i = n - 1; i >= 0; i--) {
            List<Integer> list = map.getOrDefault(arr[i], new ArrayList<>());
            list.add(i);
            map.put(arr[i], list);
        }
        int[] dist = new int[n];
        Arrays.fill(dist, INF);
        Deque<Integer> d = new ArrayDeque<>();
        d.addLast(0);
        dist[0] = 0;
        while (!d.isEmpty()) {
            int t = d.pollFirst(), step = dist[t];
            if (t == n - 1) return step;
            if (t + 1 < n && dist[t + 1] == INF) {
                d.addLast(t + 1);
                dist[t + 1] = step + 1;
            }
            if (t - 1 >= 0 && dist[t - 1] == INF) {
                d.addLast(t - 1);
                dist[t - 1] = step + 1;
            }
            List<Integer> list = map.getOrDefault(arr[t], new ArrayList<>());
            for (int ne : list) {
                if (dist[ne] == INF) {
                    d.addLast(ne);
                    dist[ne] = step + 1;
                }
            }
            map.remove(arr[t]);
        }
        return -1; // never
    }
}
```
-
```C++
class Solution {
public:
    int minJumps(vector<int>& arr) {
        const int inf = 0x3f3f3f3f;
        int n = arr.size();
        unordered_map<int, vector<int>> map;
        for(int i = n - 1; ~i; i--) {
            map[arr[i]].push_back(i);
        }
        vector<int> dist(n, inf);
        queue<int> q;
        q.push(0);
        dist[0] = 0;
        while(q.size()) {
            auto t = q.front(), step = dist[t];
            q.pop();
            if(t == n - 1) return step;
            if(t + 1 < n and dist[t + 1] == inf) {
                q.push(t + 1);
                dist[t + 1] = step + 1;
            }
            if(t - 1 >= 0 and dist[t - 1] == inf) {
                q.push(t - 1);
                dist[t - 1] = step + 1;
            }
            const auto& list = map[arr[t]];
            for(auto ne :list) {
                if(dist[ne] == inf) {
                    q.push(ne);
                    dist[ne] = step + 1;
                }
            }
            map[arr[t]].clear(); //or map.erase(arr[t]);
        }
        return -1;
    }
};
```
-
```python
class Solution:
    def minJumps(self, arr: List[int]) -> int:
        n = len(arr)
        mp = defaultdict(list)
        for i, num in enumerate(arr):
            mp[num].append(i)
        dist = [inf] * n
        d = deque([0])
        dist[0] = 0
        while len(d) > 0:
            t = d.popleft()
            step = dist[t]
            if t == n - 1:
                return step
            for ne in mp[arr[t]]:
                if dist[ne] == inf:
                    d.append(ne)
                    dist[ne] = step + 1
            mp.pop(arr[t])
            if dist[t + 1] == inf:
                d.append(t + 1)
                dist[t + 1] = step + 1
            if t and dist[t - 1] == inf:
                d.append(t - 1)
                dist[t - 1] = step + 1
        return -1
```
-
```Go
const INF int = 0x3f3f3f3f
func minJumps(arr []int) int {
    n := len(arr)
    mp := map[int][]int{}
    dist := make([]int, len(arr))
    for i := 0; i < n; i++{
        list := mp[arr[i]]
        list = append(list, i)
        mp[arr[i]] = list
        dist[i] = INF
    }
    d := []int{0}
    dist[0] = 0
    for len(d) > 0{
        t := d[0]
        step := dist[t]
        if t == n - 1{
            return step
        }
        d = d[1:]
        list := mp[arr[t]]
        delete(mp, arr[t])
        list = append(list, t + 1)
        if t > 0 {
            list = append(list, t - 1)
        }
        for _, ne := range list {
            if dist[ne] == INF {
                dist[ne] = step + 1
                d = append(d, ne)
            }
        }
    }
    return -1
}
```
* 时间复杂度:预处理出 `map` 的复杂度为 $O(n)$;跑一遍 `BFS` 得到答案复杂度为 $O(n)$。整体复杂度为 $O(n)$
* 空间复杂度:$O(n)$

---

### 双向 BFS

自然也能够使用「双向 `BFS`」进行求解。

不了解「双向 `BFS`」的同学,可以先看前置🧀:[【图论搜索专题】如何使用「双向 BFS」解决搜索空间爆炸问题](https://mp.weixin.qq.com/s?__biz=MzU4NDE3MTEyMA==&mid=2247486981&idx=1&sn=045ea6c880080fea1ce807794ccff69b&chksm=fd9ca51acaeb2c0c83d13e3b2a5196895d1a1b44f8981cc3efad9d6a2af158267010646cc262&token=1446568490&lang=zh_CN#rd) & [【图论搜索专题】双向 BFS 模板题](https://mp.weixin.qq.com/s?__biz=MzU4NDE3MTEyMA==&mid=2247489502&idx=1&sn=dc863d4bc71c4739a4799b9a4558bd01&chksm=fd9cbcc1caeb35d749d0d72f485485527482c27b608c8f4062c29a997ede97a09ce598b58c7f&token=1446568490&lang=zh_CN#rd) 。

双向 `BFS` 能够有效解决搜索空间爆炸问题,本题使用双向 `BFS` 的话,可以不进行哈希表的 `remove` 操作。

代码:
```Java
class Solution {
    int[] arr;
    int INF = 0x3f3f3f3f;
    int n;
    Map<Integer, List<Integer>> map = new HashMap<>();
    public int minJumps(int[] _arr) {
        arr = _arr;
        n = arr.length;
        if (n == 1) return 0;
        for (int i = n - 1; i >= 0; i--) {
            List<Integer> list = map.getOrDefault(arr[i], new ArrayList<>());
            list.add(i);
            map.put(arr[i], list);
        }
        Deque<Integer> d1 = new ArrayDeque<>(), d2 = new ArrayDeque<>();
        int[] dist1 = new int[n], dist2 = new int[n];
        Arrays.fill(dist1, INF);
        Arrays.fill(dist2, INF);
        d1.addLast(0);
        dist1[0] = 0;
        d2.addLast(n - 1);
        dist2[n - 1] = 0;
        while (!d1.isEmpty() && !d2.isEmpty()) {
            int t = -1;
            if (d1.size() < d2.size()) t = update(d1, d2, dist1, dist2);
            else t = update(d2, d1, dist2, dist1);
            if (t != -1) return t;
        }
        return -1; // never
    }
    int update(Deque<Integer> d1, Deque<Integer> d2, int[] dist1, int[] dist2) {
        int m = d1.size();
        while (m-- > 0) {
            int t = d1.pollFirst(), step = dist1[t];
            if (t + 1 < n) {
                if (dist2[t + 1] != INF) return step + 1 + dist2[t + 1];
                if (dist1[t + 1] == INF) {
                    d1.addLast(t + 1);
                    dist1[t + 1] = step + 1;
                }
            }
            if (t - 1 >= 0) {
                if (dist2[t - 1] != INF) return step + 1 + dist2[t - 1];
                if (dist1[t - 1] == INF) {
                    d1.addLast(t - 1);
                    dist1[t - 1] = step + 1;
                }
            }
            List<Integer> list = map.getOrDefault(arr[t], new ArrayList<>());
            for (int ne : list) {
                if (dist2[ne] != INF) return step + 1 + dist2[ne];
                if (dist1[ne] == INF) {
                    d1.addLast(ne);
                    dist1[ne] = step + 1;
                }
            }
            map.remove(arr[t]);
        }
        return -1;
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n)$

---

### 其他「图论搜索 / 模拟」内容

题太简单?不如来学习热乎的 [简单图论搜索题](https://mp.weixin.qq.com/s?__biz=MzU4NDE3MTEyMA==&mid=2247490011&idx=1&sn=4c8cbd5ad858f571291d47fcef75e75b&chksm=fd9cb2c4caeb3bd2ac442b2d4d1417e8eb6d65b1feca8399179951ebfa132e8a97a3935e7498&token=252055586&lang=zh_CN#rd) 🍭🍭🍭

* [常规 BFS(二维转一维)](https://mp.weixin.qq.com/s?__biz=MzU4NDE3MTEyMA==&mid=2247489621&idx=1&sn=5d43fb97bc167a50a7aeb4ae2068571c&chksm=fd9cb34acaeb3a5c7e1e2e2a88d460ae2418a3cef615e1abf017b5d58aa1e7f490856d67f800&token=2136593799&lang=zh_CN#rd)
* [常规 BFS/迭代加深(结合二叉树)](https://mp.weixin.qq.com/s?__biz=MzU4NDE3MTEyMA==&mid=2247489746&idx=2&sn=9e80b33c12e96369c7a770382a97adbb&chksm=fd9cb3cdcaeb3adb35c708e548851e419b00e41801c98cae146ba29f5bdc49370a43cddf668d&token=252055586&lang=zh_CN#rd)
* [多源 BFS](https://mp.weixin.qq.com/s?__biz=MzU4NDE3MTEyMA==&mid=2247487179&idx=1&sn=e30a662c03fba3861254dbcf3fb9d6f2&chksm=fd9ca5d4caeb2cc205804fd17a2ce86b25d0408adc3417e73154f59d37e7cb17e02374f5122c&scene=178&cur_album_id=1917113998693449732#rd)
* [双向 BFS](https://mp.weixin.qq.com/s?__biz=MzU4NDE3MTEyMA==&mid=2247489502&idx=1&sn=dc863d4bc71c4739a4799b9a4558bd01&chksm=fd9cbcc1caeb35d749d0d72f485485527482c27b608c8f4062c29a997ede97a09ce598b58c7f&scene=178&cur_album_id=1917113998693449732#rd)
* [双向 BFS II](https://mp.weixin.qq.com/s?__biz=MzU4NDE3MTEyMA==&mid=2247486981&idx=1&sn=045ea6c880080fea1ce807794ccff69b&chksm=fd9ca51acaeb2c0c83d13e3b2a5196895d1a1b44f8981cc3efad9d6a2af158267010646cc262&scene=178&cur_album_id=1917113998693449732#rd)
* [双向 BFS III(结合并查集)](https://mp.weixin.qq.com/s?__biz=MzU4NDE3MTEyMA==&mid=2247489671&idx=1&sn=c0f64de1a5e4613675f73d2ae43d0708&chksm=fd9cb398caeb3a8eae334c89dee17711fca43a00d93cf63a623792f3aac0c8bf586b4be9cc47&token=2074150457&lang=zh_CN#rd)
* [灵活运用多种搜索方式(启发式)](https://mp.weixin.qq.com/s?__biz=MzU4NDE3MTEyMA==&mid=2247489560&idx=2&sn=bb966d868c18d656620a20d31a425b23&chksm=fd9cb307caeb3a11424428f0a88e7f0cb86bb53b3e5a2b9e28683a24bcb3ac151655d2b6419e&scene=178&cur_album_id=1917113998693449732#rd)
* [灵活运用多种搜索方式 II(启发式)](https://mp.weixin.qq.com/s?__biz=MzU4NDE3MTEyMA==&mid=2247489588&idx=1&sn=479e4c0627247ab7e20af7909f2a8b64&chksm=fd9cb32bcaeb3a3d4f0bd73f023a92a165edabf212af1db9672a55bed1af7d4e32e8af9964c3&scene=178&cur_album_id=1917113998693449732#rd)
* [灵活运用多种搜索方式 III(启发式 结合状态压缩)](https://mp.weixin.qq.com/s?__biz=MzU4NDE3MTEyMA==&mid=2247489985&idx=1&sn=e503ce6ece048062f1d9ebee2572838a&chksm=fd9cb2decaeb3bc8c635c4a6cf0e78d5973723bb6c89a64875828435dc5b90ef07874ef7a6ae&token=252055586&lang=zh_CN#rd)

### 最后

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

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

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

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


LeetCode/1371-1380/1374. 生成每种字符都是奇数个的字符串（简单）.md

1374. 生成每种字符都是奇数个的字符串

Question:
给你一个整数 `n`,请你返回一个含 `n` 个字符的字符串,其中每种字符在该字符串中都恰好出现 奇数次 。

返回的字符串必须只含小写英文字母。如果存在多个满足题目要求的字符串,则返回其中任意一个即可。

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

输出:"pppz"

解释:"pppz" 是一个满足题目要求的字符串,因为 'p' 出现 3 次,且 'z' 出现 1 次。当然,还有很多其他字符串也满足题目要求,比如:"ohhh" 和 "love"。
```
示例 2:
```
输入:n = 2

输出:"xy"

解释:"xy" 是一个满足题目要求的字符串,因为 'x' 和 'y' 各出现 1 次。当然,还有很多其他字符串也满足题目要求,比如:"ag" 和 "ur"。
```
示例 3:
```
输入:n = 7

输出:"holasss"
```

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

Solution:
### 模拟

题目仅规定所用到的字符出现次数均为奇数,并无规定单个字符的最大使用次数。

因此直接根据 $n$ 的奇偶性来做即可,若 $n$ 为奇数,则构造出由 $n$ 个 `b` 拼接的字符串;若 $n$ 为偶数,则构造出由 $1$ 个 `a` 和 $n - 1$ 个 `b` 的拼接字符串。

Java 代码:
```Java
class Solution {
    public String generateTheString(int n) {
        StringBuilder sb = new StringBuilder();
        if (n % 2 == 0 && --n >= 0) sb.append('a');
        while (n-- > 0) sb.append('b');
        return sb.toString();
    }
}
```
TypeScript 代码:
```TypeScript
function generateTheString(n: number): string {
    let ans = ""
    if (n % 2 == 0 && --n >= 0) ans += "a"
    while (n-- > 0) ans += "b"
    return ans
};
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


1380. 矩阵中的幸运数

Question:
给你一个 $m * n$ 的矩阵,矩阵中的数字 **各不相同** 。请你按 **任意** 顺序返回矩阵中的所有幸运数。

幸运数是指矩阵中满足同时下列两个条件的元素:

* 在同一行的所有元素中最小
* 在同一列的所有元素中最大

示例 1:
```
输入:matrix = [[3,7,8],[9,11,13],[15,16,17]]

输出:[15]

解释:15 是唯一的幸运数,因为它是其所在行中的最小值,也是所在列中的最大值。
```
示例 2:
```
输入:matrix = [[1,10,4,2],[9,3,8,7],[15,16,17,12]]

输出:[12]

解释:12 是唯一的幸运数,因为它是其所在行中的最小值,也是所在列中的最大值。
```
示例 3:
```
输入:matrix = [[7,8],[1,2]]

输出:[7]
```

提示:
* $m == mat.length$
* $n == mat[i].length$
* $1 <= n, m <= 50$
* $1 <= matrix[i][j] <= 10^5$
* 矩阵中的所有元素都是不同的

Solution:
### 模拟

根据题意进行模拟即可。

具体的,创建两个数组 `row` 和 `col` 用于进行预处理,$row[x]$ 含义为第 $x$ 行的最小值,$col[y]$ 为第 $y$ 列的最大值。

然后扫描棋盘取得符合条件的幸运值即可。

代码:
```Java
class Solution {
    int N = 55;
    int[] row = new int[N], col = new int[N];
    public List<Integer> luckyNumbers (int[][] mat) {
        int n = mat.length, m = mat[0].length;
        for (int i = 0; i < n; i++) {
            row[i] = 100001;
            for (int j = 0; j < m; j++) {
                row[i] = Math.min(row[i], mat[i][j]);
                col[j] = Math.max(col[j], mat[i][j]);
            }
        }
        List<Integer> ans = new ArrayList<>();
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < m; j++) {
                int t = mat[i][j];
                if (t == row[i] && t == col[j]) ans.add(t);
            }
        }
        return ans;
    }
}
```
* 时间复杂度:$O(n * m)$
* 空间复杂度:$O(n * m)$

### 最后

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

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

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

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


1395. 统计作战单位数

Question:
`n` 名士兵站成一排。每个士兵都有一个 独一无二 的评分 `rating` 。

每 $3$ 个士兵可以组成一个作战单位,分组规则如下:

* 从队伍中选出下标分别为 `i`、`j`、`k` 的 $3$ 名士兵,他们的评分分别为 $rating[i]$、$rating[j]$、$rating[k]$

* 作战单位需满足: $rating[i] < rating[j] < rating[k]$ 或者 $rating[i] > rating[j] > rating[k]$ ,其中  $0 <= i < j < k < n$

请你返回按上述条件可以组建的作战单位数量。每个士兵都可以是多个作战单位的一部分。

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

输出:3

解释:我们可以组建三个作战单位 (2,3,4)、(5,4,1)、(5,3,1) 。
```
示例 2:
```
输入:rating = [2,1,3]

输出:0

解释:根据题目条件,我们无法组建作战单位。
```
示例 3:
```
输入:rating = [1,2,3,4]

输出:4
```

提示:
* $n == rating.length$
* $3 <= n <= 1000$
* $1 <= rating[i] <= 10^5$
* `rating` 中的元素都是唯一的

Solution:
### 基本分析

为了方便,我们记 `rating` 为 `rs`。

题目本质是要我们统计所有满足「递增」或「递减」的三元组。换句话说,对于每个 $t = rs[i]$ 而言,我们需要统计比其 $t$ 大或比 $t$ 小的数的个数。

问题涉及「单点修改(更新数值 $t$ 的出现次数)」以及「区间查询(查询某段范围内数的个数)」,使用「树状数组」求解较为合适。

---

### 树状数组 - 枚举两端

一个朴素的想法是,对于三元组 $(i, j, k)$,我们枚举其两端 $i$ 和 $k$,根据 $rs[i]$ 和 $rs[k]$ 的大小关系,查询范围 $[i + 1, k - 1]$ 之间合法的数的个数。

在确定左端点 $i$ 时,我们从 $i + 1$ 开始「从小到大」枚举右端点 $k$,并将遍历过程中经过的 $rs[k]$ 添加到树状数组进行计数。

处理过程中根据 $a = rs[i]$ 和 $b = rs[k]$ 的大小关系进行分情况讨论:

* 当 $a < b$ 时,我们需要在范围 $[i + 1, k - 1]$ 中找「大于 $a$」同时「小于 $b$」的数的个数,即 `query(b - 1) - query(a)`
* 当 $a > b$ 时,我们需要在范围 $[i + 1, k - 1]$ 中找「小于 $a$」同时「大于 $b$」的数的个数,即 `query(a - 1) - query(b)`

一些细节:显然我们需要在枚举每个左端点 $i$ 时清空树状数组,但注意不能使用诸如 `Arrays.fill(tr, 0)` 的方式进行清空。

因为在没有离散化的情况下,树状数组的大小为 $m = 1e5$,即执行 `Arrays.fill` 操作的复杂度为 $O(m)$,这会导致我们计算量为至少为 $n \times m = 1e8$,会有 `TLE`  风险。

因此一个合适做法是:在 $[i + 1, n - 1]$ 范围内枚举完 $k$ 后(进行的是 `+1` 计数),再枚举一次 $[i + 1, n - 1]$ 进行一次 `-1` 的计数进行抵消。

代码:
```Java
class Solution {
    static int N = (int)1e5 + 10;
    static int[] tr = new int[N];
    int lowbit(int x) {
        return x & -x;
    }
    void update(int x, int v) {
        for (int i = x; i < N; i += lowbit(i)) tr[i] += v;
    }
    int query(int x) {
        int ans = 0;
        for (int i = x; i > 0; i -= lowbit(i)) ans += tr[i];
        return ans;
    }
    public int numTeams(int[] rs) {
        int n = rs.length, ans = 0;
        for (int i = 0; i < n; i++) {
            int a = rs[i];
            for (int j = i + 1; j < n; j++) {
                int b = rs[j];
                if (a < b) ans += query(b - 1) - query(a);
                else ans += query(a - 1) - query(b);
                update(b, 1);
            }
            for (int j = i + 1; j < n; j++) update(rs[j], -1);
        }
        return ans;
    }
}
```
* 时间复杂度:令 $m = 1e5$ 为值域大小,整体复杂度为 $O(n^2\log{m})$
* 空间复杂度:$O(m)$

---

### 双树状数组优化 - 枚举中点

我们考虑将 $n$ 的数据范围提升到 $1e4$ 该如何做。

上述解法的瓶颈在于我们枚举三元组中的左右端点,复杂度为 $O(n^2)$,而实际上利用三元组必然递增或递减的特性,我们可以调整为枚举终点 $j$,从而将「枚举点对」调整为「枚举中点」,复杂度为 $O(n)$。

假设当前枚举到的点为 $rs[i]$,问题转换为在 $[0, i - 1]$ 有多少比 $rs[i]$ 小/大 的数,在 $[i + 1, n - 1]$ 有多少比 $rs[i]$ 大/小 的数,然后集合「乘法」原理即可知道 $rs[i]$ 作为三元组中点的合法方案数。

统计 $rs[i]$ 左边的比 $rs[i]$ 大/小 的数很好做,只需要在「从小到大」枚举 $i$ 的过程中,将 $rs[i]$ 添加到树状数组 `tr1` 即可。

对于统计 $rs[i]$ 右边比 $rs[i]$ 小/大 的数,则需要通过「抵消计数」来做,起始我们先将所有 $rs[idx]$ 加入到另外一个树状数组 `tr2` 中(进行 `+1` 计数),然后在从前往后处理每个 $rs[i]$ 的时候,在 `tr2` 中进行 `-1` 抵消,从而确保我们处理每个 $rs[i]$ 时,`tr1` 存储左边的数,`tr2` 存储右边的数。

代码:
```Java
class Solution {
    static int N = (int)1e5 + 10;
    static int[] tr1 = new int[N], tr2 = new int[N];
    int lowbit(int x) {
        return x & -x;
    }
    void update(int[] tr, int x, int v) {
        for (int i = x; i < N; i += lowbit(i)) tr[i] += v;
    }
    int query(int[] tr, int x) {
        int ans = 0;
        for (int i = x; i > 0; i -= lowbit(i)) ans += tr[i];
        return ans;
    }
    public int numTeams(int[] rs) {
        int n = rs.length, ans = 0;
        Arrays.fill(tr1, 0);
        Arrays.fill(tr2, 0);
        for (int i : rs) update(tr2, i, 1);
        for (int i = 0; i < n; i++) {
            int t = rs[i];
            update(tr2, t, -1);
            ans += query(tr1, t - 1) * (query(tr2, N - 1) - query(tr2, t));
            ans += (query(tr1, N - 1) - query(tr1, t)) * query(tr2, t - 1);
            update(tr1, t, 1);
        }
        return ans;
    }
}
```
* 时间复杂度:令 $m = 1e5$ 为值域大小,整体复杂度为 $O(n\log{m})$
* 空间复杂度:$O(m)$

### 最后

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

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

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

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


LeetCode/1401-1410/1403. 非递增顺序的最小子序列（简单）.md

1403. 非递增顺序的最小子序列

Question:
给你一个数组 `nums`,请你从中抽取一个子序列,满足该子序列的元素之和严格大于未包含在该子序列中的各元素之和。

如果存在多个解决方案,只需返回长度最小的子序列。如果仍然有多个解决方案,则返回元素之和最大的子序列。

与子数组不同的地方在于,「数组的子序列」不强调元素在原数组中的连续性,也就是说,它可以通过从数组中分离一些(也可能不分离)元素得到。

注意,题目数据保证满足所有约束条件的解决方案是唯一的。同时,返回的答案应当按非递增顺序排列。

示例 1:
```
输入:nums = [4,3,10,9,8]

输出:[10,9]

解释:子序列 [10,9] 和 [10,8] 是最小的、满足元素之和大于其他各元素之和的子序列。但是 [10,9] 的元素之和最大。
```
示例 2:
```
输入:nums = [4,4,7,6,7]

输出:[7,7,6]

解释:子序列 [7,7] 的和为 14 ,不严格大于剩下的其他元素之和(14 = 4 + 4 + 6)。因此,[7,6,7] 是满足题意的最小子序列。注意,元素按非递增顺序返回。
```
示例 3:
```
输入:nums = [6]

输出:[6]
```

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

Solution:
### 贪心

根据题意,我们可以先对 `nums` 进行排序(升序),然后从后往前开始选择,当首次满足「选择元素之和 严格大于 未选择元素之和」时,必然满足所选元素个数最少,若存在其他同样个数的合法方案,也满足所选方案为元素和最大方案,同时满足答案输出按照非升序要求。

Java 代码:
```Java
class Solution {
    public List<Integer> minSubsequence(int[] nums) {
        Arrays.sort(nums);
        int sum = 0, cur = 0, idx = nums.length - 1;
        for (int i : nums) sum += i;
        List<Integer> ans = new ArrayList<>();
        while (cur <= sum) {
            sum -= nums[idx];
            cur += nums[idx];
            ans.add(nums[idx--]);
        }
        return ans;
    }
}
```
TypeScript 代码:
```TypeScript
function minSubsequence(nums: number[]): number[] {
    nums = nums.sort((a,b)=>a-b)
    let sum = 0, cur = 0, idx = nums.length - 1
    for (const i of nums) sum += i
    const ans = new Array<number>()
    while (cur <= sum) {
        sum -= nums[idx]
        cur += nums[idx]
        ans.push(nums[idx--])
    }
    return ans
};
```
* 时间复杂度:$O(n\log{n})$
* 空间复杂度:$O(\log{n})$

### 最后

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

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

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

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


1405. 最长快乐字符串

Question:
如果字符串中不含有任何 `'aaa'`,`'bbb'` 或 `'ccc'` 这样的字符串作为子串,那么该字符串就是一个「快乐字符串」。

给你三个整数 `a`,`b` ,`c`,请你返回 任意一个 满足下列全部条件的字符串 `s`:

* `s` 是一个尽可能长的快乐字符串。
* `s` 中 最多 有 `a` 个字母 `'a'`、`b` 个字母 `'b'`、`c` 个字母 `'c'` 。
* `s` 中只含有 `'a'`、`'b'` 、`'c'` 三种字母。

如果不存在这样的字符串 `s` ,请返回一个空字符串 `""`。

示例 1:
```
输入:a = 1, b = 1, c = 7

输出:"ccaccbcc"

解释:"ccbccacc" 也是一种正确答案。
```
示例 2:
```
输入:a = 2, b = 2, c = 1

输出:"aabbc"
```
示例 3:
```
输入:a = 7, b = 1, c = 0

输出:"aabaa"

解释:这是该测试用例的唯一正确答案。
```

提示:
* $0 <= a, b, c <= 100$
* $a + b + c > 0$

Solution:
### 贪心

容易想到:**每次都取当前剩余次数最多的字符来进行构造(前提是满足「不出现形如 `aaa` 字符串」的要求)**。

具体的,可以使用「优先队列(堆)」来实现上述过程,以 `(字符编号, 字符剩余数量)` 的二元组形式进行存储,构建以 `字符剩余数量` 排倒序的「大根堆」:

1. 起始先将 $(0, a)$、$(1, b)$ 和 $(2, c)$ 进行入堆(其中 $123$ 为字符编号,代指 `abc`,同时规定只有剩余数量大于 $0$ 才能入堆);
2. 每次取出堆顶元素(剩余数量最多的字符),尝试参与答案的构造:
    1. 不违反连续三个字符相同:则说明当前字符能够追加到当前答案尾部,若追加后还有字符剩余,则更新剩余数量重新入堆;
    2. 违反连续三个字符相同:说明该字符无法追加到当前答案尾部,此时尝试从堆中取出剩余次数次大的字符(若当前堆为空,说明没有任何合法字符能够追加,直接 break),若次大字符追加后还有字符剩余,则更新剩余数量重新入堆,同时将此前取的最大字符元祖也重新入堆;
3. 重复步骤 $2$,直到所有字符均被消耗,或循环提前结束。

该做法的正确性:**当 $a = b = c$ 时能够确保所有字符轮流参与构建,得到长度最大的快乐字符串,而该贪心策略(每次尽可能地进行大数消减)可以确保能够尽可能的凑成 $a = b = c$ 的局面,并且凑成该局面过程中不会从有解变为无解。**

代码:
```Java
class Solution {
    public String longestDiverseString(int a, int b, int c) {
        StringBuilder sb = new StringBuilder();
        PriorityQueue<int[]> q = new PriorityQueue<>((x,y)->y[1]-x[1]);
        if (a > 0) q.add(new int[]{0, a});
        if (b > 0) q.add(new int[]{1, b});
        if (c > 0) q.add(new int[]{2, c});
        while (!q.isEmpty()) {
            int[] cur = q.poll();
            int n = sb.length();
            if (n >= 2 && sb.charAt(n - 1) - 'a' == cur[0] && sb.charAt(n - 2) - 'a' == cur[0]) {
                if (q.isEmpty()) break;
                int[] next = q.poll();
                sb.append((char)(next[0] + 'a'));
                next[1]--;
                if (next[1] != 0) q.add(next);
                q.add(cur);
            } else {
                sb.append((char)(cur[0] + 'a'));
                cur[1]--;
                if (cur[1] != 0) q.add(cur);
            }
        }
        return sb.toString();
    }
}
```
* 时间复杂度:令答案最大长度为 $n = a + b + c$,优先队列中最多有 $C = 3$ 个元素,复杂度为 $O(n * k * \log{C})$,其中 $k$ 为构造答案字符串中每个字符所需要的平均「出队 + 入队」次数,$k$ 为一个范围在 $[2,4]$ 的数字
* 空间复杂度:$O(C)$

### 最后

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

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

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

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


LeetCode/1401-1410/1408. 数组中的字符串匹配（简单）.md

1408. 数组中的字符串匹配

Question:
给你一个字符串数组 `words`,数组中的每个字符串都可以看作是一个单词。

请你按任意顺序返回 `words` 中是其他单词的子字符串的所有单词。

如果你可以删除 `words[j]` 最左侧和/或最右侧的若干字符得到 `word[i]`,那么字符串 `words[i]` 就是 `words[j]` 的一个子字符串。

示例 1:
```
输入:words = ["mass","as","hero","superhero"]

输出:["as","hero"]

解释:"as" 是 "mass" 的子字符串,"hero" 是 "superhero" 的子字符串。
["hero","as"] 也是有效的答案。
```
示例 2:
```
输入:words = ["leetcode","et","code"]

输出:["et","code"]

解释:"et" 和 "code" 都是 "leetcode" 的子字符串。
```
示例 3:
```
输入:words = ["blue","green","bu"]

输出:[]
```

提示:
* $1 <= words.length <= 100$
* $1 <= words[i].length <= 30$
* `words[i]` 仅包含小写英文字母。
* 题目数据保证每个 `words[i]` 都是独一无二的。

Solution:
### 模拟

根据题意进行模拟即可。

Java 代码:
```Java
class Solution {
    public List<String> stringMatching(String[] ss) {
        List<String> ans = new ArrayList<>();
        int n = ss.length;
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < n; j++) {
                if (i == j) continue;
                if (ss[j].indexOf(ss[i]) >= 0) {
                    ans.add(ss[i]);
                    break;
                }
            }
        }
        return ans;
    }
}
```
TypeScript 代码:
```TypeScript
function stringMatching(ss: string[]): string[] {
    const ans = new Array<string>()
    const n = ss.length
    for (let i = 0; i < n; i++) {
        for (let j = 0; j < n; j++) {
            if (i == j) continue
            if (ss[j].indexOf(ss[i]) >= 0) {
                ans.push(ss[i])
                break
            }
        }
    }
    return ans
};
```
* 时间复杂度:$O(n^2 \times m^2)$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


Question:
「HTML 实体解析器」 是一种特殊的解析器,它将 HTML 代码作为输入,并用字符本身替换掉所有这些特殊的字符实体。

HTML 里这些特殊字符和它们对应的字符实体包括:

* 双引号:字符实体为 `"`,对应的字符是 `"`。
* 单引号:字符实体为 `'`,对应的字符是 `'` 。
* 与符号:字符实体为 `&`,对应对的字符是 `&` 。
* 大于号:字符实体为 `>`,对应的字符是 `>`。
* 小于号:字符实体为 `<`,对应的字符是 `<`。
* 斜线号:字符实体为 `⁄`,对应的字符是 `/`。

给你输入字符串 `text`,请你实现一个 HTML 实体解析器,返回解析器解析后的结果。

示例 1:
```
输入:text = "& is an HTML entity but &ambassador; is not."

输出:"& is an HTML entity but &ambassador; is not."

解释:解析器把字符实体 & 用 & 替换
```
示例 2:
```
输入:text = "and I quote: "...""

输出:"and I quote: \"...\""
```
示例 3:
```
输入:text = "Stay home! Practice on Leetcode :)"

输出:"Stay home! Practice on Leetcode :)"
```
示例 4:
```
输入:text = "x > y && x < y is always false"

输出:"x > y && x < y is always false"
```
示例 5:
```
输入:text = "leetcode.com⁄problemset⁄all"

输出:"leetcode.com/problemset/all"
```

提示:
* $1 <= text.length <= 10^5$
* 字符串可能包含 $256$ 个`ASCII` 字符中的任意字符。

Solution:
### 模拟

每个特殊字符均以 `&` 开头,最长一个特殊字符为 `⁄`。

从前往后处理 `text`,若遇到 `&` 则往后读取最多 $6$ 个字符(中途遇到结束字符 `;` 则终止),若读取子串为特殊字符,将使用替换字符进行拼接,否则使用原字符进行拼接。

Java 代码:
```Java
class Solution {
    public String entityParser(String text) {
        Map<String, String> map = new HashMap<>(){{
            put(""", "\"");
            put("'", "'");
            put("&", "&");
            put(">", ">");
            put("<", "<");
            put("⁄", "/");
        }};
        int n = text.length();
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < n; ) {
            if (text.charAt(i) == '&') {
                int j = i + 1;
                while (j < n && j - i < 6 && text.charAt(j) != ';') j++;
                String sub = text.substring(i, Math.min(j + 1, n));
                if (map.containsKey(sub)) {
                    sb.append(map.get(sub));
                    i = j + 1;
                    continue;
                }
            }
            sb.append(text.charAt(i++));
        }
        return sb.toString();
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    string entityParser(string text) {
        unordered_map<string, string> entityMap = {
            {""", "\""},
            {"'", "'"},
            {"&", "&"},
            {">", ">"},
            {"<", "<"},
            {"⁄", "/"}
        };
        int n = text.length();
        string ans = "";
        for (int i = 0; i < n; ) {
            if (text[i] == '&') {
                int j = i + 1;
                while (j < n && j - i < 6 && text[j] != ';') j++;
                string sub = text.substr(i, min(j + 1, n) - i);
                if (entityMap.find(sub) != entityMap.end()) {
                    ans += entityMap[sub];
                    i = j + 1;
                    continue;
                }
            }
            ans += text[i++];
        }
        return ans;
    }
};
```
Python 代码:
```Python
class Solution:
    def entityParser(self, text: str) -> str:
        entity_map = {
            """: "\"",
            "'": "'",
            "&": "&",
            ">": ">",
            "<": "<",
            "⁄": "/"
        }
        i, n = 0, len(text)
        ans = ""
        while i < n:
            if text[i] == '&':
                j = i + 1
                while j < n and j - i < 6 and text[j] != ';':
                    j += 1
                sub = text[i:min(j + 1, n)]
                if sub in entity_map:
                    ans += entity_map[sub]
                    i = j + 1
                    continue
            ans += text[i]
            i += 1
        return ans
```
TypeScript 代码:
```TypeScript
function entityParser(text: string): string {
    const entityMap: { [key: string]: string } = {
        """: "\"",
        "'": "'",
        "&": "&",
        ">": ">",
        "<": "<",
        "⁄": "/"
    };
    const n = text.length;
    let ans = "";
    for (let i = 0; i < n; ) {
        if (text[i] == '&') {
            let j = i + 1;
            while (j < n && j - i < 6 && text[j] != ';') j++;
            const sub = text.substring(i, Math.min(j + 1, n));
            if (entityMap[sub]) {
                ans += entityMap[sub];
                i = j + 1;
                continue;
            }
        }
        ans += text[i++];
    }
    return ans;
};
```
* 时间复杂度:$O(n \times K)$,其中 $K = 6$ 为最大特殊字符长度
* 空间复杂度:$O(C)$,一个固定大小的哈希表

### 最后

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

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

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

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


Question:
给你一个链表的头节点 `head`,判断链表中是否有环。

如果链表中有某个节点,可以通过连续跟踪 `next` 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 `pos` 来表示链表尾连接到链表中的位置(索引从 $0$ 开始)。注意:`pos` 不作为参数进行传递 。仅仅是为了标识链表的实际情况。

如果链表中存在环 ,则返回 true 。 否则,返回 false 。

示例 1:

```
输入:head = [3,2,0,-4], pos = 1

输出:true

解释:链表中有一个环,其尾部连接到第二个节点。
```
示例 2:

```
输入:head = [1,2], pos = 0

输出:true

解释:链表中有一个环,其尾部连接到第一个节点。
```
示例 3:

```
输入:head = [1], pos = -1

输出:false

解释:链表中没有环。
```

提示:
* 链表中节点的数目范围是 $[0, 10^4]$
* $-10^5 <= Node.val <= 10^5$
* `pos` 为 `-1` 或者链表中的一个 有效索引 。

进阶:你能用 $O(1)$(即,常量)内存解决此问题吗?

Solution:
### 快慢指针

这是一道「快慢指针」的模板题。

使用两变量 `slow` 和 `fast` 分别代表快慢指针,`slow` 每次往后走一步,而 `fast` 每次往后两步,两者初始化均为 `head`。

若链表无环,则 `fast` 必然会先走到结尾,算法正常结束;若链表有环,当 `slow` 来到环入口时,`fast` 必然已经在环中的某个位置,此时再继续进行,由于存在速度差,发生相遇必不可能是 `slow` 追上 `fast`,而只能是 `fast` 从后方追上 `slow`,由于速度差为 $1$,因此最多会消耗不超过环节点个数的回合,两者即会相遇。

代码:
```Java
public class Solution {
    public boolean hasCycle(ListNode head) {
        ListNode slow = head, fast = head;
        while (fast != null && fast.next != null) {
            slow = slow.next; fast = fast.next.next;
            if (slow == fast) return true;
        }
        return false;
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


Question:
给定一个链表的头节点  `head`,返回链表开始入环的第一个节点。 如果链表无环,则返回 `null`。

如果链表中有某个节点,可以通过连续跟踪 `next` 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 `pos` 来表示链表尾连接到链表中的位置(索引从 `0` 开始)。如果 `pos` 是 `-1`,则在该链表中没有环。

注意:`pos` 不作为参数进行传递,仅仅是为了标识链表的实际情况。

不允许修改 链表。

示例 1:

```
输入:head = [3,2,0,-4], pos = 1

输出:返回索引为 1 的链表节点

解释:链表中有一个环,其尾部连接到第二个节点。
```
示例 2:

```
输入:head = [1,2], pos = 0

输出:返回索引为 0 的链表节点

解释:链表中有一个环,其尾部连接到第一个节点。
```
示例 3:

```
输入:head = [1], pos = -1

输出:返回 null

解释:链表中没有环。
```

提示:
* 链表中节点的数目范围在范围 $[0, 10^4]$ 内
* $-10^5 <= Node.val <= 10^5$
* `pos` 的值为 `-1` 或者链表中的一个有效索引

进阶:你是否可以使用 $O(1)$ 空间解决此题?

Solution:
### 快慢指针

起始使用 `slow` 和 `fast` 作为慢快指针(`slow` 每次走一步,`fast` 每次走两步),起始均为 `head`。

若 `fast` 顺利走到结尾,说明链表无环,直接返回 `null`;

若两者成功相遇,说明链表有环。我们定义链表起点到环入口距离为 `x`,环内节点数量为 `y`。那么从链表起点到环入口的任意路径都能归纳成 $x + k \times y$(其中 $k$ 为大于等于 $0$ 的任意值)。

当两者首次相遇时,假设 `slow` 走的距离为 `d1`,而 `fast` 走的距离为 `d2`,根据速度差定义可知 $d2 = d1 \times 2$。同时根据 [141. 环形链表](https://leetcode.cn/problems/linked-list-cycle/solution/by-ac_oier-lfgr/) 结论,两者必然在环中相遇,且必然是 `fast` 在环内从后面追上 `slow`,因此 `d2` 相比于 `d1` 必然是多了 `y` 的整数倍,即有 $d2 = d1 + m \times y$(其中 $m$ 为圈数),即可推导出 $d1 = m \times y$。

同时根据链表起点到环入口的任意路径均表示为 $x + k \times y$,我们知道如果 `slow` 再走 `x` 步会到达环入口,同时链表起点到环入口也是 `x` 步,因此我们可以复用 `fast`,将其复位到链表起点,和 `slow` 一起每次往前一步,当两者再次相遇,必然是同时位于环入口。

同时该做法容易拓展成求 `x` 和求 `y` 等问题。

代码:
```Java
public class Solution {
    public ListNode detectCycle(ListNode head) {
        ListNode base = head;
        ListNode slow = head, fast = head;
        boolean ok = false;
        while (!ok && fast != null && fast.next != null) {
            slow = slow.next; fast = fast.next.next;
            if (slow == fast) ok = true;
        }
        if (!ok) return null;
        fast = head;
        while (slow != fast) {
            slow = slow.next; fast = fast.next;
        }
        return slow;
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


Question:
运用你所掌握的数据结构,设计和实现一个  `LRU` (最近最少使用) 缓存机制 。
实现 `LRUCache` 类:

* `LRUCache(int capacity)` 以正整数作为容量 `capacity` 初始化 `LRU` 缓存
* `int get(int key)` 如果关键字 `key` 存在于缓存中,则返回关键字的值,否则返回 $-1$
* `void put(int key, int value)` 如果关键字已经存在,则变更其数据值;如果关键字不存在,则插入该组「关键字-值」。当缓存容量达到上限时,它应该在写入新数据之前删除最久未使用的数据值,从而为新的数据值留出空间。

进阶:你是否可以在 $O(1)$ 时间复杂度内完成这两种操作?

示例:
```
输入
["LRUCache", "put", "put", "get", "put", "get", "put", "get", "get", "get"]
[[2], [1, 1], [2, 2], [1], [3, 3], [2], [4, 4], [1], [3], [4]]
输出
[null, null, null, 1, null, -1, null, -1, 3, 4]

解释
LRUCache lRUCache = new LRUCache(2);
lRUCache.put(1, 1); // 缓存是 {1=1}
lRUCache.put(2, 2); // 缓存是 {1=1, 2=2}
lRUCache.get(1);    // 返回 1
lRUCache.put(3, 3); // 该操作会使得关键字 2 作废,缓存是 {1=1, 3=3}
lRUCache.get(2);    // 返回 -1 (未找到)
lRUCache.put(4, 4); // 该操作会使得关键字 1 作废,缓存是 {4=4, 3=3}
lRUCache.get(1);    // 返回 -1 (未找到)
lRUCache.get(3);    // 返回 3
lRUCache.get(4);    // 返回 4
```

提示:
* $1 <= capacity <= 3000$
* $0 <= key <= 3000$
* $0 <= value <= 10^4$
* 最多调用 $3 * 10^4$ 次 `get` 和 `put`

Solution:
### 基本分析

LRU 是一种十分常见的页面置换算法。

将 LRU 翻译成大白话就是:**当不得不淘汰某些数据时(通常是容量已满),选择最久未被使用的数据进行淘汰。**

**题目让我们实现一个容量固定的 `LRUCache` 。如果插入数据时,发现容器已满时,则先按照 LRU 规则淘汰一个数据,再将新数据插入,其中「插入」和「查询」都算作一次“使用”。**

可以通过 🌰 来理解,假设我们有容量为 $2$ 的 `LRUCache` 和 测试键值对 `[1-1,2-2,3-3]` ,将其按照顺序进行插入 & 查询:
* 插入 `1-1`,此时最新的使用数据为 `1-1`
* 插入 `2-2`,此时最新使用数据变为 `2-2`
* 查询 `1-1`,此时最新使用数据为 `1-1`
* 插入 `3-3`,由于容器已经达到容量,需要先淘汰已有数据才能插入,这时候会淘汰 `2-2`,`3-3` 成为最新使用数据

键值对存储方面,我们可以使用「哈希表」来确保插入和查询的复杂度为 $O(1)$。

另外我们还需要额外维护一个「使用顺序」序列。

我们期望当「新数据被插入」或「发生键值对查询」时,能够将当前键值对放到序列头部,这样当触发 LRU 淘汰时,只需要从序列尾部进行数据删除即可。

**期望在 $O(1)$ 复杂度内调整某个节点在序列中的位置,很自然想到双向链表。**

----

### 双向链表

具体的,我们使用哈希表来存储「键值对」,键值对的键作为哈希表的 Key,而哈希表的 Value 则使用我们自己封装的 `Node` 类,`Node` 同时作为双向链表的节点。

* 插入:检查当前键值对是否已经存在于哈希表:

	* 如果存在,则更新键值对,并将当前键值对所对应的 `Node` 节点调整到链表头部(`refresh` 操作)

	* 如果不存在,则检查哈希表容量是否已经达到容量:
		* 没达到容量:插入哈希表,并将当前键值对所对应的 `Node` 节点调整到链表头部(`refresh` 操作)
		* 已达到容量:先从链表尾部找到待删除元素进行删除(`delete` 操作),然后再插入哈希表,并将当前键值对所对应的 `Node` 节点调整到链表头部(`refresh` 操作)

* 查询:如果没在哈希表中找到该 Key,直接返回 $-1$;如果存在该 Key,则将对应的值返回,并将当前键值对所对应的 `Node` 节点调整到链表头部(`refresh` 操作)

一些细节: 为了减少双向链表左右节点的「判空」操作,我们预先建立两个「哨兵」节点 `head` 和 `tail`。

代码:
```Java
class LRUCache {
    class Node {
        int k, v;
        Node l, r;
        Node(int _k, int _v) {
            k = _k;
            v = _v;
        }
    }
    int n;
    Node head, tail;
    Map<Integer, Node> map;
    public LRUCache(int capacity) {
        n = capacity;
        map = new HashMap<>();
        head = new Node(-1, -1);
        tail = new Node(-1, -1);
        head.r = tail;
        tail.l = head;
    }

    public int get(int key) {
        if (map.containsKey(key)) {
            Node node = map.get(key);
            refresh(node);
            return node.v;
        }
        return -1;
    }

    public void put(int key, int value) {
        Node node = null;
        if (map.containsKey(key)) {
            node = map.get(key);
            node.v = value;
        } else {
            if (map.size() == n) {
                Node del = tail.l;
                map.remove(del.k);
                delete(del);
            }
            node = new Node(key, value);
            map.put(key, node);
        }
        refresh(node);
    }

    // refresh 操作分两步:
    // 1. 先将当前节点从双向链表中删除(如果该节点本身存在于双向链表中的话)
    // 2. 将当前节点添加到双向链表头部
    void refresh(Node node) {
        delete(node);
        node.r = head.r;
        node.l = head;
        head.r.l = node;
        head.r = node;
    }

    // delete 操作:将当前节点从双向链表中移除
    // 由于我们预先建立 head 和 tail 两位哨兵,因此如果 node.l 不为空,则代表了 node 本身存在于双向链表(不是新节点)
    void delete(Node node) {
        if (node.l != null) {
            Node left = node.l;
            left.r = node.r;
            node.r.l = left;
        }
    }
}
```
* 时间复杂度:各操作均为 $O(1)$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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