LeetCode/2041-2050/2045. 到达目的地的第二短时间（困难）.md

2045. 到达目的地的第二短时间

Question:
城市用一个 双向连通 图表示,图中有 $n$ 个节点,从 $1$ 到 $n$ 编号(包含 $1$ 和 $n$)。图中的边用一个二维整数数组 $edges$ 表示,其中每个 $edges[i] = [u_i, v_i]$ 表示一条节点 $u_i$ 和节点 $v_i$ 之间的双向连通边。每组节点对由 最多一条 边连通,顶点不存在连接到自身的边。穿过任意一条边的时间是 $time$ 分钟。

每个节点都有一个交通信号灯,每 $change$ 分钟改变一次,从绿色变成红色,再由红色变成绿色,循环往复。所有信号灯都 同时 改变。你可以在 任何时候 进入某个节点,但是 只能 在节点 信号灯是绿色时 才能离开。如果信号灯是  绿色 ,你 不能 在节点等待,必须离开。

第二小的值 是 严格大于 最小值的所有值中最小的值。

* 例如,`[2, 3, 4]` 中第二小的值是 $3$ ,而 `[2, 2, 4]` 中第二小的值是 $4$ 。
给你 $n$、$edges$、$time$ 和 $change$ ,返回从节点 $1$ 到节点 $n$ 需要的 第二短时间 。

注意:

* 你可以 任意次 穿过任意顶点,包括 1 和 n 。
* 你可以假设在 启程时 ,所有信号灯刚刚变成 绿色 。

示例 1:

```
输入:n = 5, edges = [[1,2],[1,3],[1,4],[3,4],[4,5]], time = 3, change = 5

输出:13

解释:
上面的左图展现了给出的城市交通图。
右图中的蓝色路径是最短时间路径。
花费的时间是:
- 从节点 1 开始,总花费时间=0
- 1 -> 4:3 分钟,总花费时间=3
- 4 -> 5:3 分钟,总花费时间=6
因此需要的最小时间是 6 分钟。

右图中的红色路径是第二短时间路径。
- 从节点 1 开始,总花费时间=0
- 1 -> 3:3 分钟,总花费时间=3
- 3 -> 4:3 分钟,总花费时间=6
- 在节点 4 等待 4 分钟,总花费时间=10
- 4 -> 5:3 分钟,总花费时间=13
因此第二短时间是 13 分钟。
```

示例 2:

```
输入:n = 2, edges = [[1,2]], time = 3, change = 2

输出:11

解释:
最短时间路径是 1 -> 2 ,总花费时间 = 3 分钟
最短时间路径是 1 -> 2 -> 1 -> 2 ,总花费时间 = 11 分钟
```

提示:
* $2 <= n <= 10^4$
* $n - 1 <= edges.length <= min(2 * 10^4, n * (n - 1) / 2)$
* $edges[i].length == 2$
* $1 <= u_i, v_i <= n$
* $u_i != v_i$
* 不含重复边
* 每个节点都可以从其他节点直接或者间接到达
* $1 <= time, change <= 10^3$

Solution:
### 堆优化 Dijkstra

整体题意:在一张正权无向图上求严格次短路,该图无重边与自环。

同时根据提示 $edges.length <= \min(2 * 10^4, n * (n - 1) / 2)$ 可知,该图为「稀疏图」,容易想到「堆优化 Dijkstra」做法。

对「堆优化 Dijkstra」或者「其他最短路算法」不熟悉的同学,可以看前置 🧀 :[【最短路/必背模板】涵盖所有的「存图方式」与「最短路算法(详尽注释)」](https://mp.weixin.qq.com/s?__biz=MzU4NDE3MTEyMA==&mid=2247488007&idx=1&sn=9d0dcfdf475168d26a5a4bd6fcd3505d&chksm=fd9cb918caeb300e1c8844583db5c5318a89e60d8d552747ff8c2256910d32acd9013c93058f&token=211780171&lang=zh_CN#rd)。内容如题,首次接触的话,建议每个模板先敲十遍。

回到本题,与常规的「求最短路」不同,本题需要求得「严格次短路」,我们可以在原来的最短路算法基础上($dist1[]$ 数组用于记录最短路)多引入一个 $dist2[]$ 数组,$dist2[x]$ 用于记录从节点 $1$ 到节点 $x$ 的严格次短路。

维护次短路是容易的,基本思路为:

* 若当前距离 $dist$ 小于 $dist1[x]$,原本的最短路 $dist1[x]$ 沦为次短路 $dist2[x]$,即先用 $dist1[x]$ 更新 $dist2[x]$ 后,再用 $dist$ 更新 $dist1[x]$;
* 若当前距离 $dist$ 等于 $dist1[x]$,不符合「严格次短路」,忽略;
* 若当前距离 $dist$ 大于 $dist1[x]$,且 $dist$ 小于 $dist2[x]$,则使用 $dist$ 更新 $dist2[x]$。

同时,由于处理「严格次短路包含重复边」的情况,我们无须使用 $vis[]$ 数组记录处理过的点,而要确保每次「最短路」或者「次短路」被更新时,都进行入堆操作。

然后考虑「红绿灯」切换问题,这本质是引入动态边权,假设我们当前经过 $step$ 步到达节点 $i$,根据其与 $change$ 的关系分情况讨论即可:

* $\left \lfloor \frac{step}{change} \right \rfloor$ 为偶数:当前处于绿灯,动态边权为 $0$;
* $\left \lfloor \frac{step}{change} \right \rfloor$ 为奇数:当前处于红灯,需要增加动态边权(等待时间),增加的动态边权为 `change - (step % change)`。

最后,为了避免每个样例都 `new` 大数组,我们可以使用 `static` 修饰需要用到的数组,并在执行逻辑前进行重置工作。

代码:
```Java
class Solution {
    static int N = 10010, M = 4 * N, INF = 0x3f3f3f3f, idx = 0;
    static int[] he = new int[N], e = new int[M], ne = new int[M];
    static int[] dist1 = new int[N], dist2 = new int[N];
    void add(int a, int b) {
        e[idx] = b;
        ne[idx] = he[a];
        he[a] = idx;
        idx++;
    }
    public int secondMinimum(int n, int[][] edges, int time, int change) {
        Arrays.fill(dist1, INF);
        Arrays.fill(dist2, INF);
        Arrays.fill(he, -1);
        idx = 0;
        for (int[] e : edges) {
            int u = e[0], v = e[1];
            add(u, v); add(v, u);
        }
        PriorityQueue<int[]> q = new PriorityQueue<>((a,b)->a[1]-b[1]);
        q.add(new int[]{1, 0});
        dist1[1] = 0;
        while (!q.isEmpty()) {
            int[] poll = q.poll();
            int u = poll[0], step = poll[1];
            for (int i = he[u]; i != -1; i = ne[i]) {
                int j = e[i];
                int a = step / change, b = step % change;
                int wait = a % 2 == 0 ? 0 : change - b;
                int dist = step + time + wait;
                if (dist1[j] > dist) {
                    dist2[j] = dist1[j];
                    dist1[j] = dist;
                    q.add(new int[]{j, dist1[j]});
                    q.add(new int[]{j, dist2[j]});
                } else if (dist1[j] < dist && dist < dist2[j]) {
                    dist2[j] = dist;
                    q.add(new int[]{j, dist2[j]});
                }
            }
        }
        return dist2[n];
    }
}
```
* 时间复杂度:令 $n$ 为点数,$m$ 为边数,堆优化 Dijkstra 的复杂度为 $O(m\log{n})$
* 空间复杂度:$O(n + m)$

---

### BFS

更进一步,原图边权虽然由「固定部分 $time$」和「动态部分 $change$ 相关」所组成,但在路径固定的前提下,其实边权之和完全确定。

因此我们可以先将原图等价为边权为 $1$ 的新图,通过 `BFS` 求出最短路 $dist1[x]$ 和严格次短路 $dist2[x]$,然后利用此时的 $dist2[n]$ 其实是严格次短路的边数,计算原图上的边权之和。

代码:
```Java
class Solution {
    static int N = 10010, M = 4 * N, INF = 0x3f3f3f3f, idx = 0;
    static int[] he = new int[N], e = new int[M], ne = new int[M];
    void add(int a, int b) {
        e[idx] = b;
        ne[idx] = he[a];
        he[a] = idx;
        idx++;
    }
    public int secondMinimum(int n, int[][] edges, int time, int change) {
        Arrays.fill(he, -1);
        idx = 0;
        for (int[] e : edges) {
            int u = e[0], v = e[1];
            add(u, v); add(v, u);
        }
        Deque<int[]> d = new ArrayDeque<>();
        int[] dist1 = new int[n + 10], dist2 = new int[n + 10];
        Arrays.fill(dist1, INF);
        Arrays.fill(dist2, INF);
        d.addLast(new int[]{1, 0});
        dist1[1] = 0;
        while (!d.isEmpty()) {
            int[] poll = d.pollFirst();
            int u = poll[0], dist = poll[1];
            for (int i = he[u]; i != -1; i = ne[i]) {
                int j = e[i];
                if (dist1[j] > dist + 1) {
                    dist2[j] = dist1[j];
                    dist1[j] = dist + 1;
                    d.addLast(new int[]{j, dist1[j]});
                    d.addLast(new int[]{j, dist2[j]});
                } else if (dist1[j] < dist + 1 && dist + 1 < dist2[j]) {
                    dist2[j] = dist + 1;
                    d.addLast(new int[]{j, dist2[j]});
                }
            }
        }
        int ans = 0;
        for (int i = 0; i < dist2[n]; i++) {
            int a = ans / change, b = ans % change;
            int wait = a % 2 == 0 ? 0 : change - b;
            ans += time + wait;
        }
        return ans;
    }
}
```
* 时间复杂度:令 $n$ 为点数,$m$ 为边数,BFS 的复杂度为 $O(n + m)$
* 空间复杂度:$O(n + m)$

---

### AStar 算法

在 `BFS` 的基础上再进一步,我们可以将问题拓展为「使用 AStar 算法来找第 `K` 短路」(修改代码的 `K` 即可),利用终点的第 $k$ 次出队必然是第 $k$ 短路(注意要求的是严格的第 $k$ 短路)。

代码:
```Java
class Solution {
    static int N = 10010, M = 4 * N, INF = 0x3f3f3f3f, K = 2, idx = 0, n = 0;
    static int[] he = new int[N], e = new int[M], ne = new int[M];
    static int[] dist = new int[N];
    void add(int a, int b) {
        e[idx] = b;
        ne[idx] = he[a];
        he[a] = idx;
        idx++;
    }
    void bfs() {
        Arrays.fill(dist, INF);
        Deque<Integer> d = new ArrayDeque<>();
        d.addLast(n);
        dist[n] = 0;
        while (!d.isEmpty()) {
            int x = d.pollFirst(), step = dist[x];
            for (int i = he[x]; i != -1; i = ne[i]) {
                int j = e[i];
                if (dist[j] != INF) continue;
                dist[j] = step + 1;
                d.addLast(j);
            }
        }
    }
    int astar() {
        int k = K, min = -1;
        PriorityQueue<int[]> q = new PriorityQueue<>((a,b)->a[2]-b[2]);
        q.add(new int[]{1, 0, dist[1]});
        while (!q.isEmpty()) {
            int[] poll = q.poll();
            int x = poll[0], step = poll[1];
            if (x == n && min != step) {
                min = step;
                if (--k == 0) return min;
            }
            for (int i = he[x]; i != -1; i = ne[i]) {
                int j = e[i];
                q.add(new int[]{j, step + 1, step + 1 + dist[j]});
            }
        }
        return -1; // never
    }
    public int secondMinimum(int _n, int[][] edges, int time, int change) {
        n = _n; idx = 0;
        Arrays.fill(he, -1);
        for (int[] e : edges) {
            int u = e[0], v = e[1];
            add(u, v); add(v, u);
        }
        bfs();
        int cnt = astar(), ans = 0;
        for (int i = 0; i < cnt; i++) {
            int a = ans / change, b = ans % change;
            int wait = a % 2 == 0 ? 0 : change - b;
            ans += time + wait;
        }
        return ans;
    }
}
```
* 时间复杂度:启发式搜索不讨论时空复杂度
* 空间复杂度:启发式搜索不讨论时空复杂度

### 最后

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

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

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

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


LeetCode/2041-2050/2047. 句子中的有效单词数（简单）.md

2047. 句子中的有效单词数

Question:
句子仅由小写字母(`'a'` 到 `'z'`)、数字('0' 到 '9')、连字符(`'-'`)、标点符号(`'!'`、`'.'` 和 `','`)以及空格(`' '`)组成。每个句子可以根据空格分解成 一个或者多个 `token` ,这些 `token` 之间由一个或者多个空格 `' '` 分隔。

如果一个 `token` 同时满足下述条件,则认为这个 `token` 是一个有效单词:

* 仅由小写字母、连字符和/或标点(不含数字)。
* 至多一个 连字符 `'-'` 。如果存在,连字符两侧应当都存在小写字母(`"a-b"` 是一个有效单词,但 `"-ab"` 和 `"ab-"` 不是有效单词)。
* 至多一个 标点符号。如果存在,标点符号应当位于 `token` 的 末尾 。
这里给出几个有效单词的例子:`"a-b."`、`"afad"`、`"ba-c"`、`"a!"` 和 `"!"` 。

给你一个字符串 `sentence` ,请你找出并返回 `sentence` 中 有效单词的数目 。

示例 1:
```
输入:sentence = "cat and  dog"

输出:3

解释:句子中的有效单词是 "cat"、"and" 和 "dog"
```
示例 2:
```
输入:sentence = "!this  1-s b8d!"

输出:0

解释:句子中没有有效单词
"!this" 不是有效单词,因为它以一个标点开头
"1-s" 和 "b8d" 也不是有效单词,因为它们都包含数字
```
示例 3:
```
输入:sentence = "alice and  bob are playing stone-game10"

输出:5

解释:句子中的有效单词是 "alice"、"and"、"bob"、"are" 和 "playing"
"stone-game10" 不是有效单词,因为它含有数字
```
示例 4:
```
输入:sentence = "he bought 2 pencils, 3 erasers, and 1  pencil-sharpener."

输出:6

解释:句子中的有效单词是 "he"、"bought"、"pencils,"、"erasers,"、"and" 和 "pencil-sharpener."
```

提示:
* $1 <= sentence.length <= 1000$
* `sentence` 由小写英文字母、数字(`0-9`)、以及字符(`' '`、`'-'`、`'!'`、`'.'` 和 ',')组成
* 句子中至少有 $1$ 个 `token`

Solution:
### 模拟

根据题意进行模拟即可,先将 `sentence` 按照空格进行分割,得到多个 `item`,再对每个 `item` 进行合法性检查,最后统计合法的 `item` 个数即为答案。

在对 `item` 进行合法性检查时,分别使用 $c1$ 和 $c2$ 代表「连字符」和「标点符号」的出现次数。

> 特别说明:Java 的 `split` 操作基于正则,复杂度为线性,其他语言可能需要使用「双指针」手写 `split` 操作

代码( 双指针实现 `split` 的代码见 $P2$ ):
```Java
class Solution {
    public int countValidWords(String sentence) {
        String[] ss = sentence.split(" ");
        int ans = 0;
        for (String s : ss) if (check(s)) ans++;
        return ans;
    }
    boolean check(String s) {
        int n = s.length();
        if (n == 0) return false;
        for (int i = 0, c1 = 0, c2 = 0; i < n; i++) {
            char c = s.charAt(i);
            if (Character.isDigit(c)) return false;
            if (c == ' ') return false;
            if (c == '-' && ++c1 >= 0) {
                if (c1 > 1 || (i == 0 || i == n - 1)) return false;
                if (!Character.isLetter(s.charAt(i - 1)) || !Character.isLetter(s.charAt(i + 1))) return false;
            }
            if ((c == '!' || c == '.' || c == ',') && ++c2 >= 0) {
                if (c2 > 1 || (i != n - 1)) return false;
            }
        }
        return true;
    }
}
```
-
```Java
class Solution {
    public int countValidWords(String sentence) {
        int n = sentence.length(), ans = 0;
        for (int i = 0; i < n; ) {
            if (sentence.charAt(i) == ' ' && ++i >= 0) continue;
            int j = i;
            while (j < n && sentence.charAt(j) != ' ') j++;
            if (check(sentence.substring(i, j))) ans++;
            i = j + 1;
        }
        return ans;
    }
    boolean check(String s) {
        int n = s.length();
        if (n == 0) return false;
        for (int i = 0, c1 = 0, c2 = 0; i < n; i++) {
            char c = s.charAt(i);
            if (Character.isDigit(c)) return false;
            if (c == ' ') return false;
            if (c == '-' && ++c1 >= 0) {
                if (c1 > 1 || (i == 0 || i == n - 1)) return false;
                if (!Character.isLetter(s.charAt(i - 1)) || !Character.isLetter(s.charAt(i + 1))) return false;
            }
            if ((c == '!' || c == '.' || c == ',') && ++c2 >= 0) {
                if (c2 > 1 || (i != n - 1)) return false;
            }
        }
        return true;
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:执行过程会产生若干子串,子串总长度与 `s` 相同。复杂度为 $O(n)$

### 最后

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

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

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

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


LeetCode/2041-2050/2049. 统计最高分的节点数目（中等）.md

2049. 统计最高分的节点数目

Question:
给你一棵根节点为 $0$ 的 二叉树 ,它总共有 $n$ 个节点,节点编号为 $0$ 到 $n - 1$ 。

同时给你一个下标从 $0$ 开始的整数数组 $parents$ 表示这棵树,其中 $parents[i]$ 是节点 $i$ 的父节点。由于节点 $0$ 是根,所以 $parents[0] == -1$ 。

一个子树的 **大小** 为这个子树内节点的数目。每个节点都有一个与之关联的 **分数** 。求出某个节点分数的方法是,将这个节点和与它相连的边全部 **删除** ,剩余部分是若干个 **非空** 子树,这个节点的 **分数** 为所有这些子树 大小的乘积 。

请你返回有 **最高得分** 节点的 数目 。

示例 1:

```
输入:parents = [-1,2,0,2,0]

输出:3

解释:
- 节点 0 的分数为:3 * 1 = 3
- 节点 1 的分数为:4 = 4
- 节点 2 的分数为:1 * 1 * 2 = 2
- 节点 3 的分数为:4 = 4
- 节点 4 的分数为:4 = 4
最高得分为 4 ,有三个节点得分为 4 (分别是节点 1,3 和 4 )。
```
示例 2:

```
输入:parents = [-1,2,0]

输出:2

解释:
- 节点 0 的分数为:2 = 2
- 节点 1 的分数为:2 = 2
- 节点 2 的分数为:1 * 1 = 1
最高分数为 2 ,有两个节点分数为 2 (分别为节点 0 和 1 )。
```

提示:
* $n == parents.length$
* $2 <= n <= 10^5$
* $parents[0] == -1$
* 对于 $i != 0$ ,有 $0 <= parents[i] <= n - 1$
* $parents$ 表示一棵二叉树。

Solution:
### 建图 + DFS + 统计答案

为了更具有一般性,我们假定该树为多叉树。

由于题目给定的 `parents` 数组仅支持我们快速查找某个节点的父节点,为了方便遍历整棵树,我们先使用「邻接表」将图(树)建起来。

还不熟悉「邻接表」存图方式的同学可以看看前置 🧀 : [涵盖所有的「存图方式」模板](https%3A//mp.weixin.qq.com/s?__biz%3DMzU4NDE3MTEyMA%3D%3D%26mid%3D2247488007%26idx%3D1%26sn%3D9d0dcfdf475168d26a5a4bd6fcd3505d%26chksm%3Dfd9cb918caeb300e1c8844583db5c5318a89e60d8d552747ff8c2256910d32acd9013c93058f%26mpshare%3D1%26scene%3D23%26srcid%3D0311tjKy74JijYzXhHo8Qob7%26sharer_sharetime%3D1646964421353%26sharer_shareid%3D1221771780968b30ef07c3f22cd356ed%2523rd)。

然后使用 `DFS` 预处理出 `f` 数组,其中 $f[i]$ 代表以节点 $i$ 为根节点的子树所包含的节点数量。

考虑如何计算「删除某个节点 $x$ 后,剩余连通块的数量,以及每个连通块的节点数量」,根据节点 $x$ 是否为根节点进行分情况讨论:

* 若 $x$ 为根节点,删除后的连通块的数量为「$x$ 的出边数量」,假定共有 $k$ 条出边,根据题目定义,对应的 **大小** 为各个连通块的节点数量乘积:
$$
f[u_1] \times f[u_2] \times ... \times f[u_k]
$$

* 若 $x$ 不是根节点,删除后的连通块的数量为「$x$ 的出边数量 + $1$」,其中 $1$ 代指「以 $x$ 节点的父节点所在的整体连通块」。

    假定节点 $x$ 共有 $k$ 条出边,根据题目定义,对应的 **大小** 为「(各个连通块的节点数量乘积) $\times$ ($x$ 节点的父节点所在的整体连通块大小)」,而「$x$ 节点的父节点所在的整体连通块大小」,利用容斥原理可知为 $f[root] - f[u] = n - f[u]$,含义为「从原树中减掉以节点 $x$ 为根节点的子树」的部分,即最终 **大小** 为:
$$
(f[u_1] \times f[u_2] \times ... \times f[u_k]) \times (n - f[x])
$$

代码:
```Java
class Solution {
    static int N = 100010, M = N * 2;
    static int[] he = new int[N], e = new int[M], ne = new int[M];
    static int[] f = new int[N];
    int idx;
    void add(int a, int b) {
        e[idx] = b;
        ne[idx] = he[a];
        he[a] = idx++;
    }
    public int countHighestScoreNodes(int[] parents) {
        Arrays.fill(he, -1);
        int n = parents.length;
        for (int i = 1; i < n; i++) add(parents[i], i);
        dfs(0);
        long max = 0;
        int ans = 0;
        for (int x = 0; x < n; x++) {
            long cur = 1;
            for (int i = he[x]; i != -1; i = ne[i]) cur *= f[e[i]];
            if (x != 0) cur *= n - f[x];
            if (cur > max) {
                max = cur; ans = 1;
            } else if (cur == max) {
                ans++;
            }
        }
        return ans;
    }
    int dfs(int u) {
        int ans = 1;
        for (int i = he[u]; i != -1; i = ne[i]) ans += dfs(e[i]);
        f[u] = ans;
        return ans;
    }
}
```
* 时间复杂度:建图复杂度为 $O(n)$;通过 `DFS` 预处理 `f` 数组复杂度为 $O(n + m)$,其中 $m$ 为边数,对于本题(二叉树)而言,点边数量级相等,因此 `DFS` 预处理的复杂度为 $O(n)$;模拟删除任意点并统计答案的复杂度为 $O(n + m)$,对于本题(二叉树)而言,数量级为 $O(n)$。整体复杂度为 $O(n)$
* 空间复杂度:$O(n)$

---

### 拓扑排序 + 统计答案

通过对「待删除节点是否为根节点」的分情况讨论可知:若要算得删除某个节点后的得分,重点需要知道「当前节点的左右子树(如果有)所包含的节点数量」以及「当前节点的父节点所在连通块的节点数量」。

我们解法 $1$ 的建图目的就是为了得到某个节点的子树情况,`DFS` 预处理 `f` 数组的目的就是知道以某个节点为根的子树所包含的总节点数量。

而这个「建图 + `DFS`」过程可由「拓扑排序」所代替。

具体的,我们可以先对 `parents`进行遍历, 统计所有节点的出度 $out[i]$,然后将所有出度为 $0$ 的节点(叶子节点)进行入队。跑一遍求拓扑排序的 `BFS`,每次某个节点 $t$ 出队,我们就对节点 $t$ 的父节点 $fa = parents[t]$ 进行出度减一操作(若出度减至 $0$,则将 $fa$ 进行入队,注意 $fa$ 不能为根节点,因为 $fa$ 入队没有意义,其入队不能更新其他点的出度),并在求拓扑序的过程中预处理出 `l` 和 `r` 数组,$l[i]$ 和 $r[i]$ 分别代表节点 $i$ 的左子树的节点数和右节点的节点数。

跑完拓扑排序后,我们得到的 `l` 和 `r` 数组就足够我们统计答案,仍然是对删除节点 $x$ 是否为根节点的分情况讨论:

* 若 $x$ 不是根节点,得分为 $\max(l[x], 1) \times \max( r[x], 1)$
* 若 $x$ 为根节点,得分为 $\max(l[x], 1) \times \max( r[x], 1) \times (n - (l[x] + r[x] + 1)$

代码:
```Java
class Solution {
    public int countHighestScoreNodes(int[] parents) {
        int n = parents.length;
        int[] out = new int[n];
        for (int i = 1; i < n; i++) out[parents[i]]++;
        Deque<Integer> d = new ArrayDeque<>();
        for (int i = 0; i < n; i++) {
            if (out[i] == 0) d.addLast(i);
        }
        // l[i] 和 r[i] 分别代表节点 i 的左子树的节点数和右节点的节点数
        int[] l = new int[n], r = new int[n];
        while (!d.isEmpty()) {
            int t = d.pollFirst(), fa = parents[t];
            out[fa]--;
            if (l[fa] == 0) l[fa] = l[t] + r[t] + 1;
            else r[fa] = l[t] + r[t] + 1;
            if (out[fa] == 0 && fa != 0) d.addLast(fa);
        }
        long max = 0;
        int ans = 0;
        for (int i = 0; i < n; i++) {
            long cur = Math.max(l[i], 1) * Math.max(r[i], 1);
            if (i != 0) cur *= n - (l[i] + r[i] + 1);
            if (cur > max) {
                max = cur; ans = 1;
            } else if (cur == max) {
                ans++;
            }
        }
        return ans;
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


2055. 蜡烛之间的盘子

Question:
给你一个长桌子,桌子上盘子和蜡烛排成一列。给你一个下标从 $0$ 开始的字符串 `s` ,它只包含字符 `'*'` 和 `'|'` ,其中 `'*'` 表示一个 盘子 ,`'|'` 表示一支 蜡烛 。

同时给你一个下标从 $0$ 开始的二维整数数组 `queries` ,其中 $queries[i] = [lefti, righti]$ 表示 子字符串 $s[left_i...right_i]$ (包含左右端点的字符)。对于每个查询,你需要找到 子字符串中 在 两支蜡烛之间 的盘子的 数目 。如果一个盘子在 子字符串中 左边和右边 都 至少有一支蜡烛,那么这个盘子满足在 两支蜡烛之间 。

比方说,`s = "||**||**|*"` ,查询 $[3, 8]$ ,表示的是子字符串 `"*||**|"` 。子字符串中在两支蜡烛之间的盘子数目为 $2$ ,子字符串中右边两个盘子在它们左边和右边 都 至少有一支蜡烛。
请你返回一个整数数组 $answer$ ,其中 $answer[i]$ 是第 $i$ 个查询的答案。

示例 1:

```
输入:s = "**|**|***|", queries = [[2,5],[5,9]]

输出:[2,3]

解释:
- queries[0] 有两个盘子在蜡烛之间。
- queries[1] 有三个盘子在蜡烛之间。
```
示例 2:

```
输入:s = "***|**|*****|**||**|*", queries = [[1,17],[4,5],[14,17],[5,11],[15,16]]

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

解释:
- queries[0] 有 9 个盘子在蜡烛之间。
- 另一个查询没有盘子在蜡烛之间。
```

提示:
* $3 <= s.length <= 10^5$
* `s` 只包含字符 `'*'` 和 `'|'` 。
* $1 <= queries.length <= 10^5$
* $queries[i].length == 2$
* $0 <= lefti <= righti < s.length$

Solution:
### 二分 + 前缀和

对于任意一个查询 $[a, b]$ 而言,我们需要统计所有左边和右边能够找到蜡烛的盘子数量。

一个自然的想法是:**找到区间 $[a, b]$ 两边缘的蜡烛,分别记为 $c$ 和 $d$,显然区间 $[c, d]$ 内的盘子都是需要被统计的。**

因此,我们可以对字符串 `s` 进行从前往后的扫描,将所有的蜡烛下标添加到数组(数组严格递增),并预处理出盘子的「前缀和」数组。

然后处理询问时的「找区间 $[a, b]$ 边缘蜡烛」操作可通过对数组「二分」来做,而「查询区间 $[c, d]$ 内的盘子数量」操作可直接查询「前缀和」数组。

Java 代码:
```Java
class Solution {
    public int[] platesBetweenCandles(String s, int[][] qs) {
        char[] cs = s.toCharArray();
        int n = cs.length, m = qs.length;
        int[] ans = new int[m], sum = new int[n + 1];
        List<Integer> list = new ArrayList<>();
        for (int i = 0; i < n; i++) {
            if (cs[i] == '|') list.add(i);
            sum[i + 1] = sum[i] + (cs[i] == '*' ? 1 : 0);
        }
        if (list.size() == 0) return ans;
        for (int i = 0; i < m; i++) {
            int a = qs[i][0], b = qs[i][1];
            int c = -1, d = -1;
            // 找到 a 右边最近的蜡烛
            int l = 0, r = list.size() - 1;
            while (l < r) {
                int mid = l + r >> 1;
                if (list.get(mid) >= a) r = mid;
                else l = mid + 1;
            }
            if (list.get(r) >= a) c = list.get(r);
            else continue;
            // 找到 b 左边最近的蜡烛
            l = 0; r = list.size() - 1;
            while (l < r) {
                int mid = l + r + 1 >> 1;
                if (list.get(mid) <= b) l = mid;
                else r = mid - 1;
            }
            if (list.get(r) <= b) d = list.get(r);
            else continue;
            if (c <= d) ans[i] = sum[d + 1] - sum[c];
        }
        return ans;
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    vector<int> platesBetweenCandles(string s, vector<vector<int>>& qs) {
        vector<int> ans(qs.size(), 0);
        vector<int> sum(s.length() + 1, 0);
        vector<int> list;
        for (int i = 0; i < s.length(); i++) {
            if (s[i] == '|') list.push_back(i);
            sum[i + 1] = sum[i] + (s[i] == '*' ? 1 : 0);
        }
        if (list.empty()) return ans;
        for (int i = 0; i < qs.size(); i++) {
            int a = qs[i][0], b = qs[i][1];
            int c = -1, d = -1;
            // 找到 a 右边最近的蜡烛
            int l = 0, r = list.size() - 1;
            while (l < r) {
                int mid = l + r >> 1;
                if (list[mid] >= a) r = mid;
                else l = mid + 1;
            }
            if (list[r] >= a) c = list[r];
            else continue;
            // 找到 b 左边最近的蜡烛
            l = 0; r = list.size() - 1;
            while (l < r) {
                int mid = l + r + 1 >> 1;
                if (list[mid] <= b) l = mid;
                else r = mid - 1;
            }
            if (list[r] <= b) d = list[r];
            else continue;
            if (c <= d) ans[i] = sum[d + 1] - sum[c];
        }
        return ans;
    }
};
```
Python 代码:
```Python
class Solution:
    def platesBetweenCandles(self, s: str, qs: List[List[int]]) -> List[int]:
        ans = [0] * len(qs)
        sum_val = [0] * (len(s) + 1)
        candle_positions = [i for i, char in enumerate(s) if char == '|']
        for i in range(len(s)):
            sum_val[i + 1] = sum_val[i] + (s[i] == '*')
        if not candle_positions:
            return ans
        for i in range(len(qs)):
            a, b = qs[i]
            c, d = -1, -1
            # 找到 a 右边最近的蜡烛
            l, r = 0, len(candle_positions) - 1
            while l < r:
                mid = l + r >> 1
                if candle_positions[mid] >= a:
                    r = mid
                else:
                    l = mid + 1
            if candle_positions[r] >= a:
                c = candle_positions[r]
            else:
                continue
            # 找到 b 左边最近的蜡烛
            l, r = 0, len(candle_positions) - 1
            while l < r:
                mid = l + r + 1 >> 1
                if candle_positions[mid] <= b:
                    l = mid
                else:
                    r = mid - 1
            if candle_positions[r] <= b:
                d = candle_positions[r]
            else:
                continue
            if c <= d:
                ans[i] = sum_val[d + 1] - sum_val[c]
        return ans
```
TypeScript 代码:
```TypeScript
function platesBetweenCandles(s: string, qs: number[][]): number[] {
    const ans = new Array(qs.length).fill(0);
    const sum = new Array(s.length + 1).fill(0);
    const list = [];
    for (let i = 0; i < s.length; i++) {
        if (s[i] == '|') list.push(i);
        sum[i + 1] = sum[i] + (s[i] === '*' ? 1 : 0);
    }
    if (list.length == 0) return ans;
    for (let i = 0; i < qs.length; i++) {
        const a = qs[i][0], b = qs[i][1];
        let c = -1, d = -1;
        // 找到 a 右边最近的蜡烛
        let l = 0, r = list.length - 1;
        while (l < r) {
            const mid = l + r >> 1;
            if (list[mid] >= a) r = mid;
            else l = mid + 1;
        }
        if (list[r] >= a) c = list[r];
        else continue;
        // 找到 b 左边最近的蜡烛
        l = 0; r = list.length - 1;
        while (l < r) {
            const mid = l + r + 1 >> 1;
            if (list[mid] <= b) l = mid;
            else r = mid - 1;
        }
        if (list[r] <= b) d = list[r];
        else continue;
        if (c <= d) ans[i] = sum[d + 1] - sum[c];
    }
    return ans;
};
```
* 时间复杂度:令 `s` 的长度为 $n$,`qs` 长度为 $m$。统计所有的蜡烛并存入 `list` 的复杂度为 $O(n)$;预处理前缀和数组复杂度为 $O(n)$;处理单个询问需要进行两次二分(蜡烛数量最多为 $n$),复杂度为 $O(\log{n})$,共有 $m$ 个询问需要处理,复杂度为 $O(m \times \log{n})$。整体复杂度为 $O(n + m\log{n})$
* 空间复杂度:$O(n)$

---

### 前缀和

更进一步,在给定 `s` 的前提下,每个位置其左边和右边最近的蜡烛唯一确定。

我们可以在预处理前缀和的同时,预处理每个位置左右最近的蜡烛下标,从而省去每次二分。

Java 代码:
```Java
class Solution {
    public int[] platesBetweenCandles(String s, int[][] qs) {
        char[] cs = s.toCharArray();
        int n = cs.length, m = qs.length;
        int[] l = new int[n], r = new int[n];
        int[] sum = new int[n + 1];
        for (int i = 0, j = n - 1, p = -1, q = -1; i < n; i++, j--) {
            if (cs[i] == '|') p = i;
            if (cs[j] == '|') q = j;
            l[i] = p; r[j] = q;
            sum[i + 1] = sum[i] + (cs[i] == '*' ? 1 : 0);
        }
        int[] ans = new int[m];
        for (int i = 0; i < m; i++) {
            int a = qs[i][0], b = qs[i][1];
            int c = r[a], d = l[b];
            if (c != -1 && c <= d) ans[i] = sum[d + 1] - sum[c];
        }
        return ans;
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    vector<int> platesBetweenCandles(string s, vector<vector<int>>& qs) {
        vector<int> l(s.length(), 0), r(s.length(), 0);
        vector<int> sum(s.length() + 1, 0);
        for (int i = 0, j = s.length() - 1, p = -1, q = -1; i < s.length(); i++, j--) {
            if (s[i] == '|') p = i;
            if (s[j] == '|') q = j;
            l[i] = p; r[j] = q;
            sum[i + 1] = sum[i] + (s[i] == '*' ? 1 : 0);
        }
        vector<int> ans(qs.size(), 0);
        for (int i = 0; i < qs.size(); i++) {
            int a = qs[i][0], b = qs[i][1];
            int c = r[a], d = l[b];
            if (c != -1 && c <= d) ans[i] = sum[d + 1] - sum[c];
        }
        return ans;
    }
};
```
* 时间复杂度:$O(n + m)$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


LeetCode/2051-2060/2059. 转化数字的最小运算数（中等）.md

2059. 转化数字的最小运算数

Question:
给你一个下标从 `0` 开始的整数数组 `nums`,该数组由 互不相同 的数字组成。

另给你两个整数 `start` 和 `goal` 。

整数 `x` 的值最开始设为 `start` ,你打算执行一些运算使 `x` 转化为 `goal` 。

你可以对数字 `x` 重复执行下述运算:

如果 `0 <= x <= 1000`,那么,对于数组中的任一下标 `i` `(0 <= i < nums.length)`,可以将 `x` 设为下述任一值:
* `x + nums[i]`
* `x - nums[i]`
* `x ^ nums[i](按位异或 XOR)`

注意,你可以按任意顺序使用每个 `nums[i]` 任意次。使 `x` 越过 `0 <= x <= 1000` 范围的运算同样可以生效,但该该运算执行后将不能执行其他运算。

返回将 `x = start` 转化为 `goal` 的最小操作数;如果无法完成转化,则返回 `-1` 。

示例 1:
```
输入:nums = [1,3], start = 6, goal = 4

输出:2

解释:
可以按 6 → 7 → 4 的转化路径进行,只需执行下述 2 次运算:
- 6 ^ 1 = 7
- 7 ^ 3 = 4
```
示例 2:
```
输入:nums = [2,4,12], start = 2, goal = 12

输出:2

解释:
可以按 2 → 14 → 12 的转化路径进行,只需执行下述 2 次运算:
- 2 + 12 = 14
- 14 - 2 = 12
```
示例 3:
```
输入:nums = [3,5,7], start = 0, goal = -4

输出:2

解释:
可以按 0 → 3 → -4 的转化路径进行,只需执行下述 2 次运算:
- 0 + 3 = 3
- 3 - 7 = -4
注意,最后一步运算使 x 超过范围 0 <= x <= 1000 ,但该运算仍然可以生效。
```
示例 4:
```
输入:nums = [2,8,16], start = 0, goal = 1

输出:-1

解释:
无法将 0 转化为 1
```
示例 5:
```
输入:nums = [1], start = 0, goal = 3

输出:3

解释:
可以按 0 → 1 → 2 → 3 的转化路径进行,只需执行下述 3 次运算:
- 0 + 1 = 1
- 1 + 1 = 2
- 2 + 1 = 3
```

提示:
* $1 <= nums.length <= 1000$
* $-10^9 <= nums[i], goal <= 10^9$
* $0 <= start <= 1000$
* start != goal
* $nums$ 中的所有整数互不相同

Solution:
### 常规 BFS

题目给定了从当前值 $x$ 到 $x'$ 的转换规则,同时只有满足 $0 <= x <= 1000$ 的数值才能继续进行转换,因此配合去重,转换次数具有明确上界,使用常规的 `BFS`  即可求解。

需要注意的是,为了避免 TLE,我们应当将「满足 $0 <= x <= 1000$ 才能进行下一步转换」这一规则应用到「限制入队」,而不是简单的应用在「限制拓展」。

即如果由 $x$ 拓展出来的 $x'$,既不与 $goal$ 相等,也不满足 $0 <= x' <= 1000$ 条件,那么 $x'$ 并无入队必要。

代码:
```Java
class Solution {
    public int minimumOperations(int[] nums, int s, int t) {
        Deque<Integer> d = new ArrayDeque<>();
        Map<Integer, Integer> map = new HashMap<>();
        d.addLast(s);
        map.put(s, 0);
        while (!d.isEmpty()) {
            int cur = d.pollFirst();
            int step = map.get(cur);
            for (int i : nums) {
                int[] result = new int[]{cur + i, cur - i, cur ^ i};
                for (int next : result) {
                    if (next == t) return step + 1;
                    if (next < 0 || next > 1000) continue;
                    if (map.containsKey(next)) continue;
                    map.put(next, step + 1);
                    d.addLast(next);
                }
            }
        }
        return -1;
    }
}
```
* 时间复杂度:令 $n$ 为 $nums$ 的长度,常数 $C$ 为队列内元素的值域大小(本题值域范围为 $[0, 1000]$ )。整体复杂度为 $O(C * n)$
* 空间复杂度:$O(C)$

---

### 双向 BFS

这还是一道很好的双向 `BFS` 模板题。

之前我没有找到这样的模板题,不得已使用了 LeetCode 难度标记为「困难」的 [127. 单词接龙](https://mp.weixin.qq.com/s?__biz=MzU4NDE3MTEyMA==&mid=2247486981&idx=1&sn=045ea6c880080fea1ce807794ccff69b&chksm=fd9ca51acaeb2c0c83d13e3b2a5196895d1a1b44f8981cc3efad9d6a2af158267010646cc262&token=1901895674&lang=zh_CN#rd) 来作为双向 `BFS`  的入门题。

> PS. 事实上,那道题也不难,如果你还没做过 [127. 单词接龙](https://mp.weixin.qq.com/s?__biz=MzU4NDE3MTEyMA==&mid=2247486981&idx=1&sn=045ea6c880080fea1ce807794ccff69b&chksm=fd9ca51acaeb2c0c83d13e3b2a5196895d1a1b44f8981cc3efad9d6a2af158267010646cc262&token=1901895674&lang=zh_CN#rd),在学习完本题解后,可以尝试做一下。

回到本题,我们可以同时从两个方向进行搜索:

1. 正向搜索:使用队列 `d1` 实现从 $start$ 到 $goal$ 的通路搜索,为满足「$0 <= x <= 1000$」的条件限制,我们需要进行「出队检查」,只有满足「$0 <= x <= 1000$」的出队元素,才进行下一步的拓展;
2. 反向搜索:使用队列 `d2` 实现从 $goal$ 到 $start$ 的通路搜索,为满足「$0 <= x <= 1000$」的条件限制,我们需要进行「入队检查」,只有下一个拓展元素 $next$ 满足「$0 <= x <= 1000$」才能进行入队。

同时,我们使用两个「哈希表」分别记录两个搜索方向中出现过的结果。

一旦在某条搜索通路中搜到了另一条搜索通路中出现过的结果,说明找到了一条合法的搜索通路,返回该通路长度。

代码:
```Java
class Solution {
    int[] nums;
    public int minimumOperations(int[] _nums, int s, int t) {
        nums = _nums;
        Deque<Long> d1 = new ArrayDeque<>(), d2 = new ArrayDeque<>();
        Map<Long, Integer> m1 = new HashMap<>(), m2 = new HashMap<>();
        d1.addLast(s * 1L);
        d2.addLast(t * 1L);
        m1.put(s * 1L, 0);
        m2.put(t * 1L, 0);
        while (!d1.isEmpty() && !d2.isEmpty()) {
            if (d1.size() < d2.size()) {
                int ans = update(d1, m1, d2, m2, true);
                if (ans != -1) return ans;
            } else {
                int ans = update(d2, m2, d1, m1, false);
                if (ans != -1) return ans;
            }
        }
        return -1;
    }
    int update(Deque<Long> d1, Map<Long, Integer> m1, Deque<Long> d2, Map<Long, Integer> m2, boolean flag) {
        int m = d1.size();
        while (m-- > 0) {
            long cur = d1.pollFirst();
            int step = m1.get(cur);
            for (int i : nums) {
                if (flag) {
                    // 正向搜索:进行出队检查,只有出队元素符合条件,才能使用出队元素往下拓展
                    if (0 <= cur && cur <= 1000) {
                        long[] result = new long[]{cur + i, cur - i, cur ^ i};
                        for (long next : result) {
                            if (m2.containsKey(next)) return step + 1 + m2.get(next);
                            if (!m1.containsKey(next)) {
                                d1.addLast(next);
                                m1.put(next, step + 1);
                            }
                        }
                    }
                } else {
                    // 反向搜索:进行入队检查,只有拓展元素符合条件,才能将拓展元素入队
                    long[] result = new long[]{cur + i, cur - i, cur ^ i};
                    for (long next : result) {
                        if (0 <= next && next <= 1000) {
                            if (m2.containsKey(next)) return step + 1 + m2.get(next);
                            if (!m1.containsKey(next)) {
                                d1.addLast(next);
                                m1.put(next, step + 1);
                            }
                        }
                    }
                }
            }
        }
        return -1;
    }
}
```
* 时间复杂度:令 $n$ 为 $nums$ 的长度,常数 $C$ 为队列内元素的值域大小(本题值域范围为 $[0, 1000]$ )。整体复杂度为 $O(C * n)$
* 空间复杂度:$O(C)$

### 最后

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

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

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

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


2069. 模拟行走机器人 II

Question:
给你一个在 `XY` 平面上的 `width x height` 的网格图,左下角 的格子为 $(0, 0)$,右上角的格子为 $(width - 1, height - 1)$ 。网格图中相邻格子为四个基本方向之一(`"North"`,`"East"`,`"South"` 和 `"West"`)。一个机器人初始在格子 $(0, 0)$ ,方向为 `"East"`。

机器人可以根据指令移动指定的步数。每一步,它可以执行以下操作。

1. 沿着当前方向尝试往前一步 。
2. 如果机器人下一步将到达的格子超出了边界,机器人会逆时针转 90 度,然后再尝试往前一步。

如果机器人完成了指令要求的移动步数,它将停止移动并等待下一个指令。

请你实现 `Robot` 类:
* `Robot(int width, int height)` 初始化一个 `width x height` 的网格图,机器人初始在 $(0, 0)$,方向朝 `"East"`。
* `void move(int num)` 给机器人下达前进 `num` 步的指令。
* `int[] getPos()` 返回机器人当前所处的格子位置,用一个长度为 $2$ 的数组 $[x, y]$ 表示。
* `String getDir()` 返回当前机器人的朝向,为 `"North"`,`"East"`,`"South"` 或者 `"West"`。

示例 1:

```
输入:
["Robot", "move", "move", "getPos", "getDir", "move", "move", "move", "getPos", "getDir"]
[[6, 3], [2], [2], [], [], [2], [1], [4], [], []]

输出:
[null, null, null, [4, 0], "East", null, null, null, [1, 2], "West"]

解释:
Robot robot = new Robot(6, 3); // 初始化网格图,机器人在 (0, 0) ,朝东。
robot.move(2);  // 机器人朝东移动 2 步,到达 (2, 0) ,并朝东。
robot.move(2);  // 机器人朝东移动 2 步,到达 (4, 0) ,并朝东。
robot.getPos(); // 返回 [4, 0]
robot.getDir(); // 返回 "East"
robot.move(2);  // 朝东移动 1 步到达 (5, 0) ,并朝东。
                // 下一步继续往东移动将出界,所以逆时针转变方向朝北。
                // 然后,往北移动 1 步到达 (5, 1) ,并朝北。
robot.move(1);  // 朝北移动 1 步到达 (5, 2) ,并朝 北 (不是朝西)。
robot.move(4);  // 下一步继续往北移动将出界,所以逆时针转变方向朝西。
                // 然后,移动 4 步到 (1, 2) ,并朝西。
robot.getPos(); // 返回 [1, 2]
robot.getDir(); // 返回 "West"
```

提示:
* $2 <= width, height <= 100$
* $1 <= num <= 10^5$
* `move`,`getPos` 和 `getDir` 总共调用次数不超过 $10^4$ 次。

Solution:
### 模拟

根据题目给定的移动规则可知,机器人总是在外圈移动(共上下左右四条),而移动方向分为四类:

当行走步数为 $mod = 2 * (w - 1) + 2 * (h - 1)$ 的整数倍时,会回到起始位置,因此我们可以通过维护一个变量 `loc` 来记录行走的总步数,并且每次将 `loc` 对 `mod` 进行取模来得到有效步数。

在回答 `getPos` 和 `getDir` 询问时,根据当前 `loc` 进行分情况讨论(见注释)。

另外还有一个小细节:根据题意,如果当前处于 $(0, 0)$ 位置,并且没有移动过,方向为 `East`,若移动过,方向则为 `South`,这可以通过一个变量 `moved` 来进行特判处理。

代码:
```Java
class Robot {
    String[] ss = new String[]{"East", "North", "West", "South"};
    int w, h, loc; // loc: 有效(取模后)移动步数
    boolean moved; // 记录是否经过移动,用于特判 (0,0) 的方向
    public Robot(int width, int height) {
        w = width; h = height;
    }
    public void step(int num) {
        moved = true;
        loc += num;
        loc %= 2 * (w - 1) + 2 * (h - 1);
    }
    public int[] getPos() {
        int[] info = move();
        return new int[]{info[0], info[1]};
    }
    public String getDir() {
        int[] info = move();
        int x = info[0], y = info[1], dir = info[2];
        // 特殊处理当前在 (0,0) 的情况,当未移动过方向为 East,移动过方向为 South
        if (x == 0 && y == 0) return moved ? ss[3] : ss[0];
        return ss[dir];
    }
    int[] move() {
        if (loc <= w - 1) {
            // 当移动步数范围在 [0,w-1] 时,所在位置为外圈的下方,方向为 East
            return new int[]{loc, 0, 0};
        } else if (loc <= (w - 1) + (h - 1)) {
            // 当移动步数范围在 [w,(w-1)+(h-1)] 时,所在位置为外圈的右方,方向为 North
            return new int[]{w - 1, loc - (w - 1), 1};
        } else if (loc <= 2 * (w - 1) + (h - 1)) {
            // 当移动步数范围在 [(w-1)+(h-1)+1,2*(w-1)+(h-1)] 时,所在位置为外圈的上方,方向为 West
            return new int[]{(w - 1) - (loc - ((w - 1) + (h - 1))), h - 1, 2};
        } else {
            // 当移动步数范围在 [2*(w-1)+(h-1)+1,2*(w-1)+2*(h-1)] 时,所在位置为外圈的左方,方向为 South
            return new int[]{0, (h - 1) - (loc - (2 * (w - 1) + (h - 1))), 3};
        }
    }
}
```
* 时间复杂度:$O(1)$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


LeetCode/2091-2100/2100. 适合打劫银行的日子（中等）.md

2100. 适合打劫银行的日子

Question:
你和一群强盗准备打劫银行。给你一个下标从 $0$ 开始的整数数组 $security$ ,其中 $security[i]$ 是第 $i$ 天执勤警卫的数量。日子从 $0$ 开始编号。同时给你一个整数 $time$ 。

如果第 $i$ 天满足以下所有条件,我们称它为一个适合打劫银行的日子:

* 第 $i$ 天前和后都分别至少有 $time$ 天。
* 第 $i$ 天前连续 $time$ 天警卫数目都是非递增的。
* 第 $i$ 天后连续 $time$ 天警卫数目都是非递减的。

更正式的,第 $i$ 天是一个合适打劫银行的日子当且仅当:

$security[i - time] >= security[i - time + 1] >= ... >= security[i] <= ... <= security[i + time - 1] <= security[i + time]$

请你返回一个数组,包含 所有 适合打劫银行的日子(下标从 $0$ 开始)。返回的日子可以 **任意** 顺序排列。

示例 1:
```
输入:security = [5,3,3,3,5,6,2], time = 2

输出:[2,3]

解释:
第 2 天,我们有 security[0] >= security[1] >= security[2] <= security[3] <= security[4] 。
第 3 天,我们有 security[1] >= security[2] >= security[3] <= security[4] <= security[5] 。
没有其他日子符合这个条件,所以日子 2 和 3 是适合打劫银行的日子。
```
示例 2:
```
输入:security = [1,1,1,1,1], time = 0

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

解释:
因为 time 等于 0 ,所以每一天都是适合打劫银行的日子,所以返回每一天。
```
示例 3:
```
输入:security = [1,2,3,4,5,6], time = 2

输出:[]

解释:
没有任何一天的前 2 天警卫数目是非递增的。
所以没有适合打劫银行的日子,返回空数组。
```
示例 4:
```
输入:security = [1], time = 5

输出:[]

解释:
没有日子前面和后面有 5 天时间。
所以没有适合打劫银行的日子,返回空数组。
```

提示:
* $1 <= security.length <= 10^5$
* $0 <= security[i], time <= 10^5$

Solution:
### 前缀和

为了方便,我们令 $n$ 为 $security$ 长度。

根据题目对「适合打劫银行的日子」的定义,首先我们可以确定答案落在 $[time, n - time]$ 范围内,另外规定了「适合打劫银行的日子」左右侧需要满足「非递增」和「非递减」的性质。

首先我们可以预处理 `g` 数组,$g[i]$ 代表当前时间 $security[i]$ 与前一时间 $security[i - 1]$ 的大小关系,当 $security[i] > security[i - 1]$ 时有 $g[i] = 1$,当 $security[i] < security[i - 1]$ 时有 $g[i] = -1$,否则 $g[i] = 0$,另外我们特别的有 $g[0] = 0$ 的边界情况。

然后我们对 `g` 应用「前缀和」思想:使用 `a` 数组记录前缀 $1$ 的数量,使用 `b` 数组记录前缀 $-1$ 的数量。

最终,如果 $i$ 为「适合打劫银行的日子」,那么满足 $time <= i <= n - time$,且满足 $(i - time, i]$ 范围前缀 $1$ 数量为 $0$,$(i, i + time]$ 范围前缀 $-1$ 数量为 $0$。

代码:
```Java
class Solution {
    public List<Integer> goodDaysToRobBank(int[] security, int time) {
        int n = security.length;
        int[] g = new int[n];
        for (int i = 1; i < n; i++) {
            if (security[i] == security[i - 1]) continue;
            g[i] = security[i] > security[i - 1] ? 1 : -1;
        }
        int[] a = new int[n + 1], b = new int[n + 1];
        for (int i = 1; i <= n; i++) a[i] = a[i - 1] + (g[i - 1] == 1 ? 1 : 0);
        for (int i = 1; i <= n; i++) b[i] = b[i - 1] + (g[i - 1] == -1 ? 1 : 0);
        List<Integer> ans = new ArrayList<>();
        for (int i = time; i < n - time; i++) {
            int c1 = a[i + 1] - a[i + 1 - time], c2 = b[i + 1 + time] - b[i + 1];
            if (c1 == 0 && c2 == 0) ans.add(i);
        }
        return ans;
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


21. 合并两个有序链表

Question:
将两个升序链表合并为一个新的 升序 链表并返回。

新链表是通过拼接给定的两个链表的所有节点组成的。

示例 1:

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

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

输出:[]
```
示例 3:
```
输入:l1 = [], l2 = [0]

输出:[0]
```

提示:
* 两个链表的节点数目范围是 $[0, 50]$
* $-100 <= Node.val <= 100$
* `l1` 和 `l2` 均按 非递减顺序 排列

Solution:
### 多路归并(哨兵技巧)

哨兵技巧我们之前在「2. 两数相加」讲过啦,让三叶来帮你回忆一下:

**做有关链表的题目,有个常用技巧:添加一个虚拟头结点(哨兵),帮助简化边界情况的判断。**

由于两条链表本身就是有序的,只需要在遍历过程中进行比较即可:

代码:
```Java
class Solution {
    public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
        if (l1 == null) return l2;
        if (l2 == null) return l1;

        ListNode dummy = new ListNode(0);
        ListNode cur = dummy;
        while (l1 != null && l2 != null) {
            if (l1.val < l2.val) {
                cur.next = l1;
                cur = cur.next;
                l1 = l1.next;
            } else {
                cur.next = l2;
                cur = cur.next;
                l2 = l2.next;
            }
        }

        while (l1 != null) {
            cur.next = l1;
            cur = cur.next;
            l1 = l1.next;
        }
        while (l2 != null) {
            cur.next = l2;
            cur = cur.next;
            l2 = l2.next;
        }

        return dummy.next;
    }
}
```
* 时间复杂度:对两条链表扫描一遍。复杂度为 $O(n)$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


Question:
数字 $n$ 代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且 有效的 括号组合。

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

输出:["((()))","(()())","(())()","()(())","()()()"]
```
示例 2:
```
输入:n = 1

输出:["()"]
```

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

Solution:
### DFS

既然题目是求所有的方案,那只能爆搜了,爆搜可以使用 `DFS` 来做。

从数据范围 `1 <= n <= 8` 来说,`DFS` 应该是稳稳的 AC。

这题的关键是我们要从题目中发掘一些性质:

1. 括号数为 `n`,那么一个合法的括号组合,应该包含 `n` 个左括号和 `n` 个右括号,组合总长度为 `2n`

2. 一对合法的括号,应该是先出现左括号,再出现右括号。那么意味着**任意一个右括号的左边,至少有一个左括号**

其中性质 2 是比较难想到的,我们可以用反证法来证明性质 2 总是成立:

假设某个右括号不满足「其左边至少有一个左括号」,即其左边没有左括号,那么这个右括号就找不到一个与之对应的左括号进行匹配。

这样的组合必然不是有效的括号组合。

使用我们「20. 有效的括号(简单)」的思路(栈)去验证的话,必然验证不通过。

掌握了这两个性质之后,我们可以设定一个初始值为 0 的得分值,令往组合添加一个 `(` 得分值 + 1,往组合添加一个 `)` 得分值 -1。

这样就有:

1. 一个合法的括号组合,最终得分必然为 0 (左括号和右括号的数量相等,对应了性质 1)

2. 整个 `DFS` 过程中,得分值范围在 `[0, n]`(得分不可能超过 `n` 意味着不可能添加数量超过 `n` 的左括号,对应了性质 1;得分不可能为负数,意味着每一个右括号必然有一个左括号进行匹配,对应性质 2)

代码:
```Java
class Solution {
    public List<String> generateParenthesis(int n) {
        List<String> ans = new ArrayList<>();
        dfs(0, n * 2, 0, n, "", ans);
        return ans;
    }

    /**
    * i: 当前遍历到位置
    * n: 字符总长度
    * score: 当前得分,令 '(' 为 1, ')' 为 -1
    * max: 最大得分值
    * path: 当前的拼接结果
    * ans: 最终结果集
    */
    void dfs(int i, int n, int score, int max, String path, List<String> ans) {
        if (i == n) {
            if (score == 0) ans.add(path);
        } else {
            if (score + 1 <= max) dfs(i + 1, n, score + 1, max, path + "(", ans);
            if (score - 1 >= 0) dfs(i + 1, n, score - 1, max, path + ")", ans);
        }
    }
}
```
* 时间复杂度:放置的左括号数量为 `n`,右括号的个数总是小于等于左括号的个数,典型的卡特兰数问题。复杂度为 $O(C_{2n}^n)$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


23. 合并K个升序链表

Question:
给你一个链表数组,每个链表都已经按升序排列。

请你将所有链表合并到一个升序链表中,返回合并后的链表。

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

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

解释:链表数组如下:
[
  1->4->5,
  1->3->4,
  2->6
]
将它们合并到一个有序链表中得到。
1->1->2->3->4->4->5->6
```
示例 2:
```
输入:lists = []

输出:[]
```
示例 3:
```
输入:lists = [[]]

输出:[]
```

提示:
* $k == lists.length$
* $0 <= k <= 10^4$
* $0 <= lists[i].length <= 500$
* $-10^4 <= lists[i][j] <= 10^4$
* $lists[i]$ 按 升序 排列
* $lists[i].length$ 的总和不超过 $10^4$

Solution:
### 多路归并 + 优先队列

**做有关链表的题目,有个常用技巧:添加一个虚拟头结点(哨兵),帮助简化边界情况的判断。**

由于所有链表本身满足「升序」,一个直观的做法是,我们比较每条链表的头结点,选取值最小的节点,添加到结果中,然后更新该链表的的头结点为该节点的 next 指针。循环比较,直到所有的节点都被加入结果中。

对应到代码的话,相当于我们需要准备一个「集合」,将所有链表的头结点放入「集合」,然后每次都从「集合」中挑出最小值,并将最小值的下一个节点添加进「集合」(如果有的话),循环这个过程,直到「集合」为空(说明所有节点都处理完,进过集合又从集合中出来)。

优先队列(堆)则是满足这样要求的数据结构,而整个过程其实就是「多路归并」过程。

代码:
```Java
class Solution {
    public ListNode mergeKLists(ListNode[] lists) {
        ListNode dummy = new ListNode(-1), tail = dummy;
        PriorityQueue<ListNode> q = new PriorityQueue<>((a,b)->a.val-b.val);
        for (ListNode node : lists) {
            if (node != null) q.add(node);
        }
        while (!q.isEmpty()) {
            ListNode poll = q.poll();
            tail.next = poll;
            tail = tail.next;
            if (poll.next != null) q.add(poll.next);
        }
        return dummy.next;
    }
}
```
* 时间复杂度:假设共有 $k$ 条建表,共 $n$ 个节点,复杂度为 $O(n\log{k})$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


24. 两两交换链表中的节点

Question:
给定一个链表,两两交换其中相邻的节点,并返回交换后的链表。

你不能只是单纯的改变节点内部的值,而是需要实际的进行节点交换。

示例 1:

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

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

输出:[]
```
示例 3:
```
输入:head = [1]

输出:[1]
```

提示:
* 链表中节点的数目在范围 $[0, 100]$ 内
* $0 <= Node.val <= 100$

进阶:你能在不修改链表节点值的情况下解决这个问题吗?(也就是说,仅修改节点本身。)

Solution:
### 递归解法(哨兵技巧)

哨兵技巧我们之前在前面的多道链表题讲过,让三叶来帮你回忆一下:

**做有关链表的题目,有个常用技巧:添加一个虚拟头结点(哨兵),帮助简化边界情况的判断。**

链表和树的题目天然适合使用递归来做。

我们可以设计一个递归函数,接受一个 `ListNode` 节点 root 作为参数,函数的作用是将 root 后面的两个节点进行交换,交换完成后再将下一个节点传入 ...

交换的前提条件:节点 root 后面至少有两个节点。同时别忘了应用我们的「哨兵技巧」。

代码:
```Java
class Solution {
    public ListNode swapPairs(ListNode head) {
        ListNode dummy = new ListNode(-1);
        dummy.next = head;
        recursive(dummy);
        return dummy.next;
    }
    void recursive(ListNode root) {
        if (root.next != null && root.next.next != null) {
            ListNode a = root.next, b = a.next;
            root.next = b;
            a.next = b.next;
            b.next = a;
            recursive(a);
        }
    }
}
```
* 时间复杂度:会将每个节点处理一遍。复杂度为 $O(n)$
* 空间复杂度:$O(1)$

---

### 迭代解法(哨兵技巧)

所有的递归都能转化为迭代。

代码:
```Java
class Solution {
    public ListNode swapPairs(ListNode head) {
        ListNode dummy = new ListNode(-1);
        dummy.next = head;
        for (ListNode cur = dummy; cur.next != null && cur.next.next != null;) {
            ListNode a = cur.next, b = a.next;
            cur.next = b;
            a.next = b.next;
            b.next = a;
            cur = a;
        }
        return dummy.next;
    }
}
```
* 时间复杂度:会将每个节点处理一遍。复杂度为 $O(n)$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


25. K 个一组翻转链表

Question:
给你一个链表,每 `k` 个节点一组进行翻转,请你返回翻转后的链表。

`k` 是一个正整数,它的值小于或等于链表的长度。

如果节点总数不是 `k` 的整数倍,那么请将最后剩余的节点保持原有顺序。

进阶:

* 你可以设计一个只使用常数额外空间的算法来解决此问题吗?
* 你不能只是单纯的改变节点内部的值,而是需要实际进行节点交换。

示例 1:

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

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

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

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

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

输出:[1]
```
提示:
* 列表中节点的数量在范围 `sz` 内
* $1 <= sz <= 5000$
* $0 <= Node.val <= 1000$
* $1 <= k <= sz$

Solution:
### 迭代(哨兵技巧)

哨兵技巧我们在前面的多道链表题讲过,让三叶来帮你回忆一下:

**做有关链表的题目,有个常用技巧:添加一个虚拟头结点(哨兵),帮助简化边界情况的判断。**

链表和树的题目天然适合使用递归来做。

但这次我们先将简单的「递归版本」放一放,先搞清楚迭代版本该如何实现。

我们可以设计一个翻转函数 `reverse` :

**传入节点 `root` 作为参数,函数的作用是将以 `root` 为起点的 $k$ 个节点进行翻转。**

**当以 `root` 为起点的长度为 $k$ 的一段翻转完成后,再将下一个起始节点传入,直到整条链表都被处理完成。**

**当然,在 `reverse` 函数真正执行翻转前,需要先确保节点 `root` 后面至少有 $k$ 个节点。**

我们可以结合图解再来体会一下这个过程:

假设当前样例为 `1->2->3->4->5->6->7` 和 `k = 3` :

然后我们调用 `reverse(cur, k)`,在 `reverse()` 方法内部,几个指针的指向如图所示,会通过先判断 `cur` 是否为空,从而确定是否有足够的节点进行翻转:

然后先通过 `while` 循环,将中间的数量为 `k - 1` 的 next 指针进行翻转:
.png)

最后再处理一下局部的头结点和尾结点,这样一次 `reverse(cur, k)` 执行就结束了:
.png)

回到主方法,将 `cur` 往前移动 `k` 步,再调用 `reverse(cur, k)` 实现 `k` 个一组翻转:
.png)

Java 代码:
```Java
class Solution {
    public ListNode reverseKGroup(ListNode head, int k) {
        ListNode dummy = new ListNode(-1);
        dummy.next = head;
        ListNode cur = dummy;
        while (cur != null) {
            reverse(cur, k);
            int u = k;
            while (u-- > 0 && cur != null) cur = cur.next;
        }
        return dummy.next;
    }
    // reverse 的作用是将 root 后面的 k 个节点进行翻转
    void reverse(ListNode root, int k) {
        // 检查 root 后面是否有 k 个节点
        int u = k;
        ListNode cur = root;
        while (u-- > 0 && cur != null) cur = cur.next;
        if (cur == null) return;
        // 进行翻转
        ListNode tail = cur.next;
        ListNode a = root.next, b = a.next;
        // 当需要翻转 k 个节点时,中间有 k - 1 个 next 指针需要翻转
        while (k-- > 1) {
            ListNode c = b.next;
            b.next = a;
            a = b;
            b = c;
        }
        root.next.next = tail;
        root.next = a;
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    ListNode* reverseKGroup(ListNode* head, int k) {
        ListNode* dummy = new ListNode(-1);
        dummy->next = head;
        ListNode* cur = dummy;
        while (cur != NULL) {
            reverse(cur, k);
            int u = k;
            while (u-- > 0 && cur != NULL) cur = cur->next;
        }
        return dummy->next;
    }
    // reverse 的作用是将 root 后面的 k 个节点进行翻转
    void reverse(ListNode* root, int k) {
        // 检查 root 后面是否有 k 个节点
        int u = k;
        ListNode* cur = root;
        while (u-- > 0 && cur != NULL) cur = cur->next;
        if (cur == NULL) return;
        // 进行翻转
        ListNode* tail = cur->next;
        ListNode* a = root->next, *b = a->next;
        // 当需要翻转 k 个节点时,中间有 k - 1 个 next 指针需要翻转
        while (k-- > 1) {
            ListNode* c = b->next;
            b->next = a;
            a = b;
            b = c;
        }
        root->next->next = tail;
        root->next = a;
    }
};
```
Python 代码:
```Python
class Solution:
    def reverseKGroup(self, head: Optional[ListNode], k: int) -> Optional[ListNode]:
        # reverse 的作用是将 root 后面的 k 个节点进行翻转
        def reverse(root, k):
            # 检查 root 后面是否有 k 个节点
            u, cur = k, root
            while u > 0 and cur:
                cur = cur.next
                u -= 1
            if not cur: return
            # 进行翻转
            tail = cur.next
            a, b = root.next, root.next.next
            # 当需要翻转 k 个节点时,中间有 k - 1 个 next 指针需要翻转
            while k > 1:
                c, b.next = b.next, a
                a, b = b, c
                k -= 1
            root.next.next = tail
            root.next = a

        dummy = ListNode(-1)
        dummy.next = head
        cur = dummy
        while cur:
            reverse(cur, k)
            u = k
            while u > 0 and cur:
                cur = cur.next
                u -= 1
        return dummy.next
```
TypeScript 代码:
```TypeScript
function reverseKGroup(head: ListNode | null, k: number): ListNode | null {
    // reverse 的作用是将 root 后面的 k 个节点进行翻转
    const reverse = function(root: ListNode | null, k: number): void {
        // 检查 root 后面是否有 k 个节点
        let u = k, cur = root;
        while (u-- > 0 && cur != null) cur = cur.next;
        if (cur == null) return;
        // 进行翻转
        let tail = cur.next, a = root.next, b = a.next;
        // 当需要翻转 k 个节点时,中间有 k - 1 个 next 指针需要翻转
        while (k-- > 1) {
            let c = b.next;
            b.next = a;
            a = b;
            b = c;
        }
        root.next.next = tail;
        root.next = a;
    };
    let dummy = new ListNode(-1);
    dummy.next = head;
    let cur = dummy;
    while (cur != null) {
        reverse(cur, k);
        let u = k;
        while (u-- > 0 && cur != null) cur = cur.next;
    }
    return dummy.next;
};
```
* 时间复杂度:会将每个节点处理一遍。复杂度为 $O(n)$
* 空间复杂度:$O(1)$

---

### 递归

搞懂了较难的「迭代哨兵」版本之后,常规的「递归无哨兵」版本写起来应该更加容易了。

需要注意的是,当我们不使用「哨兵」时,检查是否足够 $k$ 位,只需要检查是否有 $k - 1$ 个 $next$ 指针即可。

代码:
```Java
class Solution {
    public ListNode reverseKGroup(ListNode head, int k) {
        int u = k;
        ListNode p = head;
        while (p != null && u-- > 1) p = p.next;
        if (p == null) return head;
        ListNode tail = head;
        ListNode prev = head, cur = prev.next;
        u = k;
        while (u-- > 1) {
            ListNode tmp = cur.next;
            cur.next = prev;
            prev = cur;
            cur = tmp;
        }
        tail.next = reverseKGroup(cur, k);
        return prev;
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    ListNode* reverseKGroup(ListNode* head, int k) {
        int u = k;
        ListNode* p = head;
        while (p != NULL && u-- > 1) p = p->next;
        if (p == NULL) return head;
        ListNode* tail = head;
        ListNode* prev = head, *cur = prev->next;
        u = k;
        while (u-- > 1) {
            ListNode* tmp = cur->next;
            cur->next = prev;
            prev = cur;
            cur = tmp;
        }
        tail->next = reverseKGroup(cur, k);
        return prev;
    }
};
```
Python 代码:
```Python
class Solution:
    def reverseKGroup(self, head: Optional[ListNode], k: int) -> Optional[ListNode]:
        u = k
        p = head
        while p and u > 1:
            p = p.next
            u -= 1
        if not p: return head

        tail = prev = head
        cur = prev.next
        u = k
        while u > 1:
            tmp = cur.next
            cur.next = prev
            prev, cur = cur, tmp
            u -= 1
        tail.next = self.reverseKGroup(cur, k)
        return prev
```
TypeScript 代码:
```TypeScript
function reverseKGroup(head: ListNode | null, k: number): ListNode | null {
    let u = k;
    let p = head;
    while (p != null && u-- > 1) p = p.next;
    if (p == null) return head;
    let tail = head, prev = head, cur = prev.next;
    u = k;
    while (u-- > 1) {
        let tmp = cur.next;
        cur.next = prev;
        prev = cur;
        cur = tmp;
    }
    tail.next = reverseKGroup(cur, k);
    return prev;
};
```
* 时间复杂度:会将每个节点处理一遍。复杂度为 $O(n)$
* 空间复杂度:只有忽略递归带来的空间开销才是 $O(1)$

### 最后

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

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

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

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


26. 删除有序数组中的重复项

Question:
给你一个有序数组 `nums` ,请你 原地 删除重复出现的元素,使每个元素 只出现一次 ,返回删除后数组的新长度。

不要使用额外的数组空间,你必须在 原地 修改输入数组 并在使用 $O(1)$ 额外空间的条件下完成。

说明:

为什么返回数值是整数,但输出的答案是数组呢?

请注意,输入数组是以「引用」方式传递的,这意味着在函数里修改输入数组对于调用者是可见的。

你可以想象内部操作如下:
```
// nums 是以“引用”方式传递的。也就是说,不对实参做任何拷贝
int len = removeDuplicates(nums);

// 在函数里修改输入数组对于调用者是可见的。
// 根据你的函数返回的长度, 它会打印出数组中 该长度范围内 的所有元素。
for (int i = 0; i < len; i++) {
    print(nums[i]);
}
```

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

输出:2, nums = [1,2]

解释:函数应该返回新的长度 2 ,并且原数组 nums 的前两个元素被修改为 1, 2 。不需要考虑数组中超出新长度后面的元素。
```
示例 2:
```
输入:nums = [0,0,1,1,1,2,2,3,3,4]

输出:5, nums = [0,1,2,3,4]

解释:函数应该返回新的长度 5 , 并且原数组 nums 的前五个元素被修改为 0, 1, 2, 3, 4 。不需要考虑数组中超出新长度后面的元素。
```

提示:
* $0 <= nums.length <= 3 \times 10^4$
* $-10^4 <= nums[i] <= 10^4$
* `nums` 已按升序排列

Solution:
### 双指针

一个指针 `i` 进行数组遍历,另外一个指针 `j` 指向有效数组的最后一个位置。

只有当 `i` 所指向的值和 `j` 不一致(不重复),才将 `i` 的值添加到 `j` 的下一位置。

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

---

### 通用解法

为了让解法更具有一般性,我们将原问题的「最多保留 1 位」修改为「最多保留 k 位」。

对于此类问题,我们应该进行如下考虑:

* 由于是保留 `k` 个相同数字,**对于前 `k` 个数字,我们可以直接保留。**
* 对于后面的任意数字,能够保留的前提是:**与当前写入的位置前面的第 `k` 个元素进行比较,不相同则保留。**

举个🌰,我们令 `k=1`,假设有样例:`[3,3,3,3,4,4,4,5,5,5]`

0. 设定变量 `idx`,指向待插入位置。`idx` 初始值为 `0`,目标数组为 `[]`

1. 首先我们先让第 `1` 位直接保留(性质 1)。`idx` 变为 `1`,目标数组为 `[3]`

2. 继续往后遍历,能够保留的前提是与 `idx` 的前面 `1` 位元素不同(性质 2),因此我们会跳过剩余的 `3`,将第一个 `4` 追加进去。`idx` 变为 `2`,目标数组为 `[3,4]`

3. 继续这个过程,跳过剩余的 `4`,将第一个 `5` 追加进去。`idx` 变为 `3`,目标数组为 `[3,4,5]`

4. 当整个数组被扫描完,最终我们得到了目标数组 `[3,4,5]` 和 答案 `idx` 为 `3`。

代码:
```Java
class Solution {
    public int removeDuplicates(int[] nums) {
        return process(nums, 1);
    }
    int process(int[] nums, int k) {
        int idx = 0;
        for (int x : nums) {
            if (idx < k || nums[idx - k] != x) nums[idx++] = x;
        }
        return idx;
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(1)$

基于上述解法我们还能做一点小剪枝:**利用目标数组的最后一个元素必然与原数组的最后一个元素相同进行剪枝,从而确保当数组有超过 `k` 个最大值时,数组不会被完整扫描。**

**但需要注意这种「剪枝」同时会让我们单次循环的常数变大,所以仅作为简单拓展。**

代码:
```Java
class Solution {
    public int removeDuplicates(int[] nums) {
        int n = nums.length;
        if (n <= 1) return n;
        return process(nums, 1, nums[n - 1]);
    }
    int process(int[] nums, int k, int max) {
        int idx = 0;
        for (int x : nums) {
            if (idx < k || nums[idx - k] != x) nums[idx++] = x;
            if (idx - k >= 0 && nums[idx - k] == max) break;
        }
        return idx;
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


Question:
给你一个数组 `nums` 和一个值 `val`,你需要「原地」移除所有数值等于 `val` 的元素,并返回移除后数组的新长度。

不要使用额外的数组空间,你必须仅使用 $O(1)$ 额外空间并「原地」修改输入数组。

元素的顺序可以改变。

你不需要考虑数组中超出新长度后面的元素。

**说明**:

为什么返回数值是整数,但输出的答案是数组呢?

请注意,输入数组是以「引用」方式传递的,这意味着在函数里修改输入数组对于调用者是可见的。

你可以想象内部操作如下:

```
// nums 是以“引用”方式传递的。也就是说,不对实参作任何拷贝
int len = removeElement(nums, val);

// 在函数里修改输入数组对于调用者是可见的。
// 根据你的函数返回的长度, 它会打印出数组中 该长度范围内 的所有元素。
for (int i = 0; i < len; i++) {
    print(nums[i]);
}
```

示例 1:

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

输出:2, nums = [2,2]

解释:函数应该返回新的长度 2, 并且 nums 中的前两个元素均为 2。你不需要考虑数组中超出新长度后面的元素。例如,函数返回的新长度为 2 ,而 nums = [2,2,3,3] 或 nums = [2,2,0,0],也会被视作正确答案。
```
示例 2:
```
输入:nums = [0,1,2,2,3,0,4,2], val = 2

输出:5, nums = [0,1,4,0,3]

解释:函数应该返回新的长度 5, 并且 nums 中的前五个元素为 0, 1, 3, 0, 4。注意这五个元素可为任意顺序。你不需要考虑数组中超出新长度后面的元素。
```

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

Solution:
### 双指针

本解法的思路与 [【题解】26. 删除排序数组中的重复项](https://leetcode-cn.com/problems/remove-duplicates-from-sorted-array/solution/shua-chuan-lc-jian-ji-shuang-zhi-zhen-ji-2eg8/) 中的「双指针解法」类似。

根据题意,我们可以将数组分成「前后」两段:

* 前半段是有效部分,存储的是不等于 `val` 的元素。
* 后半段是无效部分,存储的是等于 `val` 的元素。

最终答案返回有效部分的结尾下标。

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

---

### 通用解法

本解法的思路与 [【题解】26. 删除排序数组中的重复项](https://leetcode-cn.com/problems/remove-duplicates-from-sorted-array/solution/shua-chuan-lc-jian-ji-shuang-zhi-zhen-ji-2eg8/) 中的「通用解法」类似。

先设定变量 `idx`,指向待插入位置。`idx` 初始值为 0

**然后从题目的「要求/保留逻辑」出发,来决定当遍历到任意元素 `x` 时,应该做何种决策:**
* 如果当前元素 `x` 与移除元素 `val` 相同,那么跳过该元素。
* 如果当前元素 `x` 与移除元素 `val` 不同,那么我们将其放到下标 `idx` 的位置,并让 `idx` 自增右移。

最终得到的 `idx` 即是答案。

代码:
```Java
class Solution {
    public int removeElement(int[] nums, int val) {
        int idx = 0;
        for (int x : nums) {
            if (x != val) nums[idx++] = x;
        }
        return idx;
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(1)$

---

### 总结

**对于诸如「相同元素最多保留 `k` 位元素」或者「移除特定元素」的问题,更好的做法是从题目本身性质出发,利用题目给定的要求提炼出具体的「保留逻辑」,将「保留逻辑」应用到我们的遍历到的每一个位置。**

### 最后

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

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

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

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


Question:
实现 `strStr()` 函数。

给你两个字符串 `haystack` 和 `needle` ,请你在 `haystack` 字符串中找出 `needle` 字符串出现的第一个位置(下标从 $0$ 开始)。

如果不存在,则返回  $-1$ 。

说明:
* 当 `needle` 是空字符串时,我们应当返回什么值呢?这是一个在面试中很好的问题。

* 对于本题而言,当 `needle` 是空字符串时我们应当返回 $0$ 。这与 C 语言的 `strstr()` 以及 Java 的 `indexOf()` 定义相符。

示例 1:
```
输入:haystack = "hello", needle = "ll"

输出:2
```
示例 2:
```
输入:haystack = "aaaaa", needle = "bba"

输出:-1
```
示例 3:
```
输入:haystack = "", needle = ""

输出:0
```

提示:
* $0 <= haystack.length, needle.length <= 5 \times 10^4$
* `haystack` 和 `needle` 仅由小写英文字符组成

Solution:
### 朴素解法

直观的解法的是:枚举原串 `ss` 中的每个字符作为「发起点」,每次从原串的「发起点」和匹配串的「首位」开始尝试匹配:

* 匹配成功:返回本次匹配的原串「发起点」。
* 匹配失败:枚举原串的下一个「发起点」,重新尝试匹配。

代码:
```Java
class Solution {
    public int strStr(String ss, String pp) {
        int n = ss.length(), m = pp.length();
        char[] s = ss.toCharArray(), p = pp.toCharArray();
        // 枚举原串的「发起点」
        for (int i = 0; i <= n - m; i++) {
            // 从原串的「发起点」和匹配串的「首位」开始,尝试匹配
            int a = i, b = 0;
            while (b < m && s[a] == p[b]) {
                a++;
                b++;
            }
            // 如果能够完全匹配,返回原串的「发起点」下标
            if (b == m) return i;
        }
        return -1;
    }
}
```
* 时间复杂度:`n` 为原串的长度,`m` 为匹配串的长度。其中枚举的复杂度为 $O(n - m)$,构造和比较字符串的复杂度为 $O(m)$。整体复杂度为 $O((n - m) \times m)$。

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

---

### KMP 解法

KMP 算法是一个快速查找匹配串的算法,它的作用其实就是本题问题:**如何快速在「原字符串」中找到「匹配字符串」。**

上述的朴素解法,不考虑剪枝的话复杂度是 $O(m \times n)$ 的,而 KMP 算法的复杂度为 $O(m + n)$。

**KMP 之所以能够在 $O(m + n)$ 复杂度内完成查找,是因为其能在「非完全匹配」的过程中提取到有效信息进行复用,以减少「重复匹配」的消耗。**

你可能不太理解,没关系,我们可以通过举 🌰 来理解 KMP。

#### 1. 匹配过程

在模拟 KMP 匹配过程之前,我们先建立两个概念:

* 前缀:对于字符串 `abcxxxxefg`,我们称 `abc` 属于 `abcxxxxefg` 的某个前缀。
* 后缀:对于字符串 `abcxxxxefg`,我们称 `efg` 属于 `abcxxxxefg` 的某个后缀。

然后我们假设原串为 `abeababeabf`,匹配串为 `abeabf`:

我们可以先看看如果不使用 KMP,会如何进行匹配(不使用 `substring` 函数的情况下)。

首先在「原串」和「匹配串」分别各自有一个指针指向当前匹配的位置。

首次匹配的「发起点」是第一个字符 `a`。显然,后面的 `abeab` 都是匹配的,两个指针会同时往右移动(黑标)。

在都能匹配上 `abeab` 的部分,「朴素匹配」和「KMP」并无不同。

直到出现第一个不同的位置(红标):

**接下来,正是「朴素匹配」和「KMP」出现不同的地方:**

* **先看下「朴素匹配」逻辑:**

**1. 将原串的指针移动至本次「发起点」的下一个位置(`b` 字符处);匹配串的指针移动至起始位置。**

**2. 尝试匹配,发现对不上,原串的指针会一直往后移动,直到能够与匹配串对上位置。**

如图:

也就是说,对于「朴素匹配」而言,一旦匹配失败,将会将原串指针调整至下一个「发起点」,匹配串的指针调整至起始位置,然后重新尝试匹配。

这也就不难理解为什么「朴素匹配」的复杂度是 $O(m \times n)$ 了。

* **然后我们再看看「KMP 匹配」过程:**

**首先匹配串会检查之前已经匹配成功的部分中里是否存在相同的「前缀」和「后缀」。如果存在,则跳转到「前缀」的下一个位置继续往下匹配:**

**跳转到下一匹配位置后,尝试匹配,发现两个指针的字符对不上,并且此时匹配串指针前面不存在相同的「前缀」和「后缀」,这时候只能回到匹配串的起始位置重新开始:**

**到这里,你应该清楚 KMP 为什么相比于朴素解法更快:**

* **因为 KMP 利用已匹配部分中相同的「前缀」和「后缀」来加速下一次的匹配。**

* **因为 KMP 的原串指针不会进行回溯(没有朴素匹配中回到下一个「发起点」的过程)。**

第一点很直观,也很好理解。

我们可以把重点放在第二点上,原串不回溯至「发起点」意味着什么?

**其实是意味着:随着匹配过程的进行,原串指针的不断右移,我们本质上是在不断地在否决一些「不可能」的方案。**

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

#### 2. 分析实现

到这里,就结束了吗?要开始动手实现上述匹配过程了吗?

我们可以先分析一下复杂度。如果严格按照上述解法的话,最坏情况下我们需要扫描整个原串,复杂度为 $O(n)$。同时在每一次匹配失败时,去检查已匹配部分的相同「前缀」和「后缀」,跳转到相应的位置,如果不匹配则再检查前面部分是否有相同「前缀」和「后缀」,再跳转到相应的位置 ... 这部分的复杂度是 $O(m^2)$ ,因此整体的复杂度是 $O(n \times m^2)$,而我们的朴素解法是 $O(m \times n)$ 的。

说明还有一些性质我们没有利用到。

显然,扫描完整原串操作这一操作是不可避免的,我们可以优化的只能是「**检查已匹配部分的相同前缀和后缀**」这一过程。

再进一步,我们检查「前缀」和「后缀」的目的其实是「为了确定匹配串中的下一段开始匹配的位置」。

**同时我们发现,对于匹配串的任意一个位置而言,由该位置发起的下一个匹配点位置其实与原串无关。**

举个 🌰,对于匹配串 `abcabd` 的字符 `d` 而言,由它发起的下一个匹配点跳转必然是字符 `c` 的位置。因为字符 `d` 位置的相同「前缀」和「后缀」字符 `ab` 的下一位置就是字符 `c`。

**可见从匹配串某个位置跳转下一个匹配位置这一过程是与原串无关的,我们将这一过程称为找 `next` 点。**

**显然我们可以预处理出 `next` 数组,数组中每个位置的值就是该下标应该跳转的目标位置( `next` 点)。**

当我们进行了这一步优化之后,复杂度是多少呢?

预处理 `next` 数组的复杂度未知,匹配过程最多扫描完整个原串,复杂度为 $O(n)$。

因此如果我们希望整个 KMP 过程是 $O(m + n)$ 的话,那么我们需要在 $O(m)$ 的复杂度内预处理出 `next`数组。

**所以我们的重点在于如何在 $O(m)$ 复杂度内处理处 `next` 数组。**

#### 3. `next` 数组的构建

接下来,我们看看 `next` 数组是如何在 $O(m)$ 的复杂度内被预处理出来的。

假设有匹配串 `aaabbab`,我们来看看对应的 `next` 是如何被构建出来的。

这就是整个 `next` 数组的构建过程,时空复杂度均为 $O(m)$。

至此整个 KMP 匹配过程复杂度是 $O(m + n)$ 的。

#### 4. 代码实现

在实际编码时,通常我会往原串和匹配串头部追加一个空格(哨兵)。

目的是让 `j` 下标从 `0` 开始,省去 `j` 从 `-1` 开始的麻烦。

整个过程与上述分析完全一致,一些相关的注释我已经写到代码里。

代码:
```Java
class Solution {
    // KMP 算法
    // ss: 原串(string)  pp: 匹配串(pattern)
    public int strStr(String ss, String pp) {
        if (pp.isEmpty()) return 0;

        // 分别读取原串和匹配串的长度
        int n = ss.length(), m = pp.length();
        // 原串和匹配串前面都加空格,使其下标从 1 开始
        ss = " " + ss;
        pp = " " + pp;

        char[] s = ss.toCharArray();
        char[] p = pp.toCharArray();

        // 构建 next 数组,数组长度为匹配串的长度(next 数组是和匹配串相关的)
        int[] next = new int[m + 1];
        // 构造过程 i = 2,j = 0 开始,i 小于等于匹配串长度 【构造 i 从 2 开始】
        for (int i = 2, j = 0; i <= m; i++) {
            // 匹配不成功的话,j = next(j)
            while (j > 0 && p[i] != p[j + 1]) j = next[j];
            // 匹配成功的话,先让 j++
            if (p[i] == p[j + 1]) j++;
            // 更新 next[i],结束本次循环,i++
            next[i] = j;
        }

        // 匹配过程,i = 1,j = 0 开始,i 小于等于原串长度 【匹配 i 从 1 开始】
        for (int i = 1, j = 0; i <= n; i++) {
            // 匹配不成功 j = next(j)
            while (j > 0 && s[i] != p[j + 1]) j = next[j];
            // 匹配成功的话,先让 j++,结束本次循环后 i++
            if (s[i] == p[j + 1]) j++;
            // 整一段匹配成功,直接返回下标
            if (j == m) return i - m;
        }

        return -1;
    }
}
```
* 时间复杂度:`n` 为原串的长度,`m` 为匹配串的长度。复杂度为 $O(m + n)$。
* 空间复杂度:构建了 `next` 数组。复杂度为 $O(m)$。

---

### 总结

**KMP 算法的应用范围要比 Manacher 算法广,Manacher 算法只能应用于「回文串」问题,相对比较局限,而「子串匹配」问题还是十分常见的。**

**背过这样的算法的意义在于:相当于大脑里有了一个时间复杂度为 $O(n)$ 的 api 可以使用,这个 api 传入一个原串和匹配串,返回匹配串在原串的位置。**

因此,三叶十分建议大家在「理解 KMP」的基础上,对模板进行背过 ~

### 最后

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

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

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

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


Question:
给定两个整数,被除数 `dividend` 和除数 `divisor`。

将两数相除,要求不使用乘法、除法和 `mod` 运算符。

返回被除数 `dividend` 除以除数 `divisor` 得到的商。

整数除法的结果应当截去(`truncate`)其小数部分,例如:`truncate(8.345) = 8` 以及 `truncate(-2.7335) = -2`

示例 1:
```
输入: dividend = 10, divisor = 3

输出: 3

解释: 10/3 = truncate(3.33333..) = truncate(3) = 3
```
示例 2:
```
输入: dividend = 7, divisor = -3

输出: -2

解释: 7/-3 = truncate(-2.33333..) = -2
```

提示:
* 被除数和除数均为 $32$ 位有符号整数。
* 除数不为 $0$。
* 假设我们的环境只能存储 32 位有符号整数,其数值范围是 [−$2^{31}$,  $2^{31}$ − 1]。本题中,如果除法结果溢出,则返回 $2^{31}$ − 1。

Solution:
### 基本分析

主要的歧义在于第三点限制「假设我们的环境只能存储 $32$ 位有符号整数,其数值范围是 $[−2^{31}, 2^{31} − 1]$。本题中,如果除法结果溢出,则返回 $2^{31} − 1$」的理解。

该限制有两种理解方式:

1. 不限制算法使用 `long`,只是解释为什么在溢出时,返回 $2^{31} − 1$;
2. 限制算法使用 `long`。

原始题解在 [这里](https://leetcode-cn.com/problems/divide-two-integers/solution/shua-chuan-lc-er-fen-bei-zeng-cheng-fa-j-m73b/) 。

---

### 理解一(不限制 `long`)

当不限制使用 `long` 时,基本思路为:

* 首先,`dividend` 和 `divisor` 均有「正数」和「负数」两种可能,当且仅当其中一者为负数时,结果为负,为了方便,我们可以先记录最终结果的正负号,然后将 `dividend` 和 `divisor` 都当成正数来处理;
* 现在两者都满足 $x >= 0$,然后利用 `dividend` 和 `divisor` 均为 `int`,可以确定答案的绝对值落在 $[0, dividend]$ 范围内(当且仅当 `divisor` 是范围在 $[0, 1]$ 的浮点数时,答案会大于 `dividend`);
* 假设答案为 $x$,那么在以 $x$ 为分割点的整数数轴上,具有「二段性」,因此我们可以二分找到该分割点:
    * 大于 $x$ 的数 $y$ 满足 $y * b > a$;
    * 小于等于 $x$ 的数 $y$ 满足 $y * b <= a$;
* 根据「二段性」分析,我们发现二分的 `check` 实现需要用到乘法,因此我们需要实现一个「不用乘法符号」的乘法实现(这可以使用倍增思想来实现 `mul` 操作)。

代码:
```Java
class Solution {
    int INF = Integer.MAX_VALUE;
    public int divide(int _a, int _b) {
        long a = _a, b = _b;
        boolean flag = false;
        if ((a < 0 && b > 0) || (a > 0 && b < 0)) flag = true;
        if (a < 0) a = -a;
        if (b < 0) b = -b;
        long l = 0, r = a;
        while (l < r) {
            long mid = l + r + 1 >> 1;
            if (mul(mid, b) <= a) l = mid;
            else r = mid - 1;
        }
        r = flag ? -r : r;
        if (r > INF || r < -INF - 1) return INF;
        return (int)r;
    }
    long mul(long a, long k) {
        long ans = 0;
        while (k > 0) {
            if ((k & 1) == 1) ans += a;
            k >>= 1;
            a += a;
        }
        return ans;
    }
}
```
* 时间复杂度:在 $[0, a]$ 范围内二分操作,复杂度为$O(\log{a})$;倍增乘法的与操作数的二进制长度相关,复杂度为 $O(\log{b})$。整体复杂度为 $O(\log{a} \times \log{b})$
* 空间复杂度:$O(1)$

---

### 理解二(限制 `long`)

对于全程不使用 `long` 的做法,我们需要将所有数映射到负数进行处理(以 $0$ 为分割点,负数所能表示的范围更大)。

基本思路为:

* 起始先对边界情况进行特判;
* 记录最终结果的符号,并将两数都映射为负数;
* 可以预处理出倍增数组,或采取逐步增大 `dividend` 来逼近 `divisor` 的方式。

> 由于操作数都是负数,因此自倍增过程中,如果操作数小于 `INT_MIN` 的一半(`-1073741824`),则代表发生溢出。

代码:
```Java
class Solution {
    int MIN = Integer.MIN_VALUE, MAX = Integer.MAX_VALUE;
    int LIMIT = -1073741824; // MIN 的一半
    public int divide(int a, int b) {
        if (a == MIN && b == -1) return MAX;
        boolean flag = false;
        if ((a > 0 && b < 0) || (a < 0 && b > 0)) flag = true;
        if (a > 0) a = -a;
        if (b > 0) b = -b;
        int ans = 0;
        while (a <= b){
            int c = b, d = -1;
            while (c >= LIMIT && d >= LIMIT && c >= a - c){
                c += c; d += d;
            }
            a -= c;
            ans += d;
        }
        return flag ? ans : -ans;
    }
}
```
* 时间复杂度:$O(\log{a} \times \log{b})$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


30. 串联所有单词的子串

Question:
给定一个字符串 `s` 和一些长度相同的单词 `words`。

找出 `s` 中恰好可以由 `words` 中所有单词串联形成的子串的起始位置。

注意子串要与 `words` 中的单词完全匹配,中间不能有其他字符,但不需要考虑 `words` 中单词串联的顺序。

示例 1:
```
输入:
s = "barfoothefoobarman",
words = ["foo","bar"]

输出:[0,9]

解释:
从索引 0 和 9 开始的子串分别是 "barfoo" 和 "foobar" 。
输出的顺序不重要, [9,0] 也是有效答案。
```
示例 2:
```
输入:
s = "wordgoodgoodgoodbestword",
words = ["word","good","best","word"]

输出:[]
```

提示:
* $1 <= s.length <= 10^4$
* `s` 由小写英文字母组成
* $1 <= words.length <= 5000$
* $1 <= words[i].length <= 30$
* `words[i]` 由小写英文字母组成

Solution:
### 朴素哈希表

令 `n` 为字符串 `s` 的长度,`m` 为数组 `words` 的长度(单词的个数),`w` 为单个单词的长度。

由于 `words` 里面每个单词长度固定,而我们要找的字符串只能恰好包含所有的单词,因此我们要找的目标子串的长度为 $m \times w$。

那么一个直观的思路是:

1. 使用哈希表 `map` 记录 `words` 中每个单词的出现次数
2. 枚举 `s` 中的每个字符作为起点,往后取得长度为 $m \times w$ 的子串 `sub`
3. 使用哈希表 `cur ` 统计 `sub` 每个单词的出现次数(每隔 `w` 长度作为一个单词)
4. 比较 `cur` 和 `map` 是否相同

注意:在步骤 $3$ 中,如果发现 `sub` 中包含了 `words` 没有出现的单词,可以直接剪枝。

剪枝处使用了带标签的 `continue` 语句直接回到外层循环进行。

代码:
```Java
class Solution {
    public List<Integer> findSubstring(String s, String[] words) {
        int n = s.length(), m = words.length, w = words[0].length();
        Map<String, Integer> map = new HashMap<>();
        for (String word : words) map.put(word, map.getOrDefault(word, 0) + 1);
        List<Integer> ans = new ArrayList<>();
        out:for (int i = 0; i + m * w <= n; i++) {
            Map<String, Integer> cur = new HashMap<>();
            String sub = s.substring(i, i + m * w);
            for (int j = 0; j < sub.length(); j += w) {
                String item = sub.substring(j, j + w);
                if (!map.containsKey(item)) continue out;
                cur.put(item, cur.getOrDefault(item, 0) + 1);
            }
            if (cur.equals(map)) ans.add(i);
        }
        return ans;
    }
}
```
* 时间复杂度:将 `words` 中的单词存入哈希表,复杂度为 $O(m)$(由于字符串长度固定且不超过 $30$,假定所有哈希操作均为 $O(1)$ 的);然后第一层循环枚举 `s` 中的每个字符作为起点,复杂度为 $O(n)$;在循环中将 `sub` 划分为 `m` 个单词进行统计,枚举了 `m - 1` 个下标,复杂度为 $O(m)$;每个字符串的长度为 `w`。整体复杂度为 $O(n \times m \times w)$
* 空间复杂度:$O(m \times w)$

---

### 滑动窗口 + 哈希表

事实上,我们可以**优化这个枚举起点的过程**。

我们可以将起点根据 **当前下标与单词长度的取余结果** 进行分类,这样我们就不用频繁的建立新的哈希表和进行单词统计。

代码:
```Java
class Solution {
    public List<Integer> findSubstring(String s, String[] words) {
        int n = s.length(), m = words.length, w = words[0].length();
        // 统计 words 中「每个目标单词」的出现次数
        Map<String, Integer> map = new HashMap<>();
        for (String word : words) map.put(word, map.getOrDefault(word, 0) + 1);
        List<Integer> ans = new ArrayList<>();
        for (int i = 0; i < w; i++) {
            // 构建一个当前子串对应 map,统计当前子串中「每个目标单词」的出现次数
            Map<String, Integer> curMap = new HashMap<>();
            // 滑动窗口的大小固定是 m * w,每次将下一个单词添加进 curMap,上一个单词移出 curMap
            for (int j = i; j + w <= n; j += w) {
                String cur = s.substring(j, j + w);
                curMap.put(cur, curMap.getOrDefault(cur, 0) + 1);
                if (j >= i + (m * w)) {
                    int idx = j - m * w;
                    String prev = s.substring(idx, idx + w);
                    if (curMap.get(prev) == 1) curMap.remove(prev);
                    else curMap.put(prev, curMap.get(prev) - 1);
                    if (!curMap.getOrDefault(prev, 0).equals(map.getOrDefault(prev, 0))) continue;
                }
                if (!curMap.getOrDefault(cur, 0).equals(map.getOrDefault(cur, 0))) continue;
                // 上面两个 continue 可以减少 map 之间的 equals 操作
                if (curMap.equals(map)) ans.add(j - (m - 1) * w);
            }
        }
        return ans;
    }
}
```
* 时间复杂度:将 `words` 中的单词存入哈希表,复杂度为 $O(m)$(由于字符串长度固定且不超过 $30$,假定所有哈希操作均为 $O(1)$ 的);然后枚举了取余的结果,复杂度为 $O(w)$;每次循环最多处理 `n` 长度的字符串,复杂度为 $O(n)$。整体复杂度为 $O(m + w \times n)$
* 空间复杂度:$O(m \times w)$

### 最后

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

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

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

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


Question:
总计有 `n` 个环,环的颜色可以是红、绿、蓝中的一种。

这些环分别穿在 $10$ 根编号为 $0$ 到 $9$ 的杆上。

给你一个长度为 `2n` 的字符串 `rings`,表示这 `n` 个环在杆上的分布。

`rings` 中每两个字符形成一个 颜色位置对 ,用于描述每个环:

* 第 `i` 对中的 第一个 字符表示第 `i` 个环的 颜色(`'R'`、`'G'`、`'B'`)。
* 第 `i` 对中的 第二个 字符表示第 `i` 个环的 位置,也就是位于哪根杆上(`'0'` 到 `'9'`)。

例如,`"R3G2B1"` 表示:共有 $n = 3$ 个环,红色的环在编号为 $3$ 的杆上,绿色的环在编号为 $2$ 的杆上,蓝色的环在编号为 $1$ 的杆上。

找出所有集齐 全部三种颜色 环的杆,并返回这种杆的数量。

示例 1:

```
输入:rings = "B0B6G0R6R0R6G9"

输出:1

解释:
- 编号 0 的杆上有 3 个环,集齐全部颜色:红、绿、蓝。
- 编号 6 的杆上有 3 个环,但只有红、蓝两种颜色。
- 编号 9 的杆上只有 1 个绿色环。
因此,集齐全部三种颜色环的杆的数目为 1 。
```
示例 2:

```
输入:rings = "B0R0G0R9R0B0G0"

输出:1

解释:
- 编号 0 的杆上有 6 个环,集齐全部颜色:红、绿、蓝。
- 编号 9 的杆上只有 1 个红色环。
因此,集齐全部三种颜色环的杆的数目为 1 。
```
示例 3:
```
输入:rings = "G4"

输出:0

解释:
只给了一个环,因此,不存在集齐全部三种颜色环的杆。
```

提示:
* $rings.length = 2 \times n$
* $1 <= n <= 100$
* 如 `i` 是 偶数 ,则 `rings[i]` 的值可以取 `'R'`、`'G'` 或 `'B'`(下标从 `0` 开始计数)
* 如 `i` 是 奇数 ,则 `rings[i]` 的值可以取 `'0'` 到 `'9'` 中的一个数字(下标从 `0` 开始计数)

Solution:
### 位运算 - 统计环

环的数量不定,但杆的数量就 $10$ 根。

我们可以从「环」的角度出发,进行统计。

用一个 `int` 来代表环的统计情况,根据题意,共有 `RGB`  三种颜色的环,共需要 $3$ 个 `int` 数(为了方便,代码直接开了大小为 $128$ 的数组)。

对于一个代表环的数值 $x$ 而言,从低位往高位数,若第 $k$ 位为 $1$,代表编号为 $k$ 的杆包含该颜色的环。

用示例 $1$ 来举个 🌰,`rings = "B0B6G0R6R0R6G9"`

* 红色:在 `0` 和 `6` 中出现过,对应数值 $x = (0001000001)_2$
* 蓝色:在 `0` 和 `6` 中出现过,对应数值 $x = (0001000001)_2$
* 绿色:在 `9` 中出现过,对应数值 $x = (100000000)_2$

在代表三种颜色的数值中,相同位均为 $1$,假设为第 $k$ 位,则代表三种颜色均在第 $k$ 杆中出现过。

最后,统计 $10$ 根杆中有多少满足要求即可。

Java 代码:

```Java
class Solution {
    public int countPoints(String s) {
        int n = s.length(), ans = 0;
        int[] map = new int[128];
        for (int i = 0; i < n; i += 2) map[s.charAt(i) - 'B'] |= 1 << (s.charAt(i + 1) - '0');
        for (int i = 0; i < 10; i++) {
            int tot = 0;
            for (char c : new char[]{'R', 'G', 'B'}) tot += (map[c - 'B'] >> i) & 1;
            if (tot == 3) ans++;
        }
        return ans;
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    int countPoints(string s) {
        int n = s.size(), ans = 0;
        vector<int> map(128, 0);
        for (int i = 0; i < n; i += 2) map[s[i] - 'B'] |= 1 << (s[i + 1] - '0');
        for (int i = 0; i < 10; i++) {
            int tot = 0;
            for (char c : {'R', 'G', 'B'}) tot += (map[c - 'B'] >> i) & 1;
            if (tot == 3) ans++;
        }
        return ans;
    }
};
```
Python 代码:
```Python
class Solution:
    def countPoints(self, s: str) -> int:
        n, ans = len(s), 0
        map = [0] * 128
        for i in range(0, n, 2):
            map[ord(s[i]) - ord('B')] |= 1 << (int(s[i + 1]) - int('0'))
        for i in range(10):
            tot = 0
            for c in ['R', 'G', 'B']:
                tot += (map[ord(c) - ord('B')] >> i) & 1
            ans += 1 if tot == 3 else 0
        return ans
```
TypeScript 代码:
```TypeScript
function countPoints(s: string): number {
    let n = s.length, ans = 0;
    const map = new Array(128).fill(0);
    for (let i = 0; i < n; i += 2) {
        map[s.charCodeAt(i) - 'B'.charCodeAt(0)] |= 1 << (s.charCodeAt(i + 1) - '0'.charCodeAt(0));
    }
    for (let i = 0; i < 10; i++) {
        let tot = 0;
        for (const c of ['R', 'G', 'B']) tot += ((map[c.charCodeAt(0) - 'B'.charCodeAt(0)]) >> i) & 1;
        if (tot == 3) ans++;
    }
    return ans;
};
```
* 时间复杂度:$O(n + C \times K)$,其中 $n$ 为字符串长度,$C = 10$ 为杆的数量,$K = 3$ 为环类型
* 空间复杂度:$O(K)$

---

### 位运算 - 统计杆

虽然环的数量不定,但我们只关心其在某根杆上是否出现过,而不关心其出现次数。

因此,我们也可以从「杆」的角度出发,进行统计。

创建一个大小为 $10$ 的整型数组 `cnt`,其中 $cnt[k] = x$ 代表第 $k$ 根杆的统计情况为 $x$。

从低位到高位,我们对三种颜色 `RGB` 的出现与否进行统计,使用 `0` 和 `1` 分别代表「没出现」和「出现」两种情况。

用示例 $1$ 来举个 🌰,`rings = "B0B6G0R6R0R6G9"`

* 编号为 $0$ 的杆:三种颜色均出现过,其数值为 $cnt[0] = (...111)_2$,从低位到高位,分别代表 `RGB`
* 编号为 $6$ 的杆:`R`  和 `B` 出现过,其数值为 $cnt[6] = (...101)_2$
* 编号为 $9$ 的杆:`G` 出现过,其数值为 $cnt[9] = (...010)_2$
* 其他编号的杆:没有任何颜色出现过,其数值为 $cnt[i] = 0$

Java 代码:

```Java
class Solution {
    public int countPoints(String s) {
        int n = s.length(), ans = 0;
        int[] cnt = new int[10];
        for (int i = 0; i < n; i += 2) {
            char c = s.charAt(i);
            int idx = -1, t = s.charAt(i + 1) - '0';
            if (c == 'R') idx = 0;
            else if (c == 'G') idx = 1;
            else idx = 2;
            cnt[t] |= 1 << idx;
        }
        for (int i = 0; i < 10; i++) {
            if (cnt[i] == (1 << 3) - 1) ans++;
        }
        return ans;
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    int countPoints(string s) {
        int n = s.size(), ans = 0;
        vector<int> cnt(10, 0);
        for (int i = 0; i < n; i += 2) {
            int idx = -1, t = s[i + 1] - '0';
            if (s[i] == 'R') idx = 0;
            else if (s[i] == 'G') idx = 1;
            else idx = 2;
            cnt[t] |= 1 << idx;
        }
        for (int i = 0; i < 10; i++) {
            if (cnt[i] == (1 << 3) - 1) ans++;
        }
        return ans;
    }
};
```
Python 代码:
```Python
class Solution:
    def countPoints(self, s: str) -> int:
        n, ans = len(s), 0
        cnt = [0] * 10
        for i in range(0, n, 2):
            idx, t = -1, int(s[i + 1])
            if s[i] == 'R':
                idx = 0
            elif s[i] == 'G':
                idx = 1
            else:
                idx = 2
            cnt[t] |= 1 << idx
        for i in range(10):
            ans += 1 if cnt[i] == (1 << 3) - 1 else 0
        return ans
```
TypeScript 代码:
```TypeScript
function countPoints(s: string): number {
    let n = s.length, ans = 0;
    const cnt = new Array(10).fill(0);
    for (let i = 0; i < n; i += 2) {
        let idx = -1, t = parseInt(s[i + 1]);
        if (s[i] == 'R') idx = 0;
        else if (s[i] == 'G') idx = 1;
        else idx = 2;
        cnt[t] |= 1 << idx;
    }
    for (let i = 0; i < 10; i++) {
        if (cnt[i] == (1 << 3) - 1) ans++;
    }
    return ans;
};
```
* 时间复杂度:$O(n \times K + C)$,其中 $n$ 为字符串长度,$C = 10$ 为杆的数量,$K = 3$ 为环类型。
注:这里为什么不是 $O(n + C)$,在首个循环中,环的类型决定了分支数量,因此首个循环复杂度为 $O(n \times K)$
* 空间复杂度:$O(C)$

### 最后

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

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

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

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


2104. 子数组范围和

Question:
给你一个整数数组 `nums` 。`nums` 中,子数组的 **范围** 是子数组中最大元素和最小元素的差值。

返回 `nums` 中 **所有** 子数组范围的 **和** 。

子数组是数组中一个连续 **非空** 的元素序列。

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

输出:4

解释:nums 的 6 个子数组如下所示:
[1],范围 = 最大 - 最小 = 1 - 1 = 0
[2],范围 = 2 - 2 = 0
[3],范围 = 3 - 3 = 0
[1,2],范围 = 2 - 1 = 1
[2,3],范围 = 3 - 2 = 1
[1,2,3],范围 = 3 - 1 = 2
所有范围的和是 0 + 0 + 0 + 1 + 1 + 2 = 4
```
示例 2:
```
输入:nums = [1,3,3]

输出:4

解释:nums 的 6 个子数组如下所示:
[1],范围 = 最大 - 最小 = 1 - 1 = 0
[3],范围 = 3 - 3 = 0
[3],范围 = 3 - 3 = 0
[1,3],范围 = 3 - 1 = 2
[3,3],范围 = 3 - 3 = 0
[1,3,3],范围 = 3 - 1 = 2
所有范围的和是 0 + 0 + 0 + 2 + 0 + 2 = 4
```
示例 3:
```
输入:nums = [4,-2,-3,4,1]

输出:59

解释:nums 中所有子数组范围的和是 59
```

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

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

Solution:
### 区间 DP(预处理)

数据范围为 $10^3$,最为朴素的三层循环为:枚举区间(左右端点)+ 扫描区间统计最值,并累加到答案中。该做法复杂度为 $O(n^3)$,会 `TLE`。

考虑在此基础上优化,枚举所有区间的操作不好避免,考虑通过「预处理」手段来优化「扫描区间统计最值」操作,通常会将其优化为 $O(1)$ 查表。

定义 $f[l][r][k]$ 为区间 $[l, r]$ 范围内的最值情况,其中 $k$ 非 $0$ 即 $1$:$f[l][r][0]$ 代表区间 $[l, r]$ 内的最小值,$f[l][r][1]$ 代表区间 $[l, r]$ 内的最大值。

不失一般性考虑 $f[l][r][0]$ 和 $f[l][r][1]$ 该如何计算:$[l, r]$ 区间的最值可由 $[l, r - 1]$ 与 $nums[r]$ 更新而来:

$$
f[l][r][0] = \min(f[l][r - 1][0], nums[r])
$$

$$
f[l][r][1] = \max(f[l][r - 1][1], nums[r])
$$

最后再枚举所有区间统计答案即可。

代码:
```Java
class Solution {
    public long subArrayRanges(int[] nums) {
        int n = nums.length;
        int[][][] f = new int[n][n][2];
        for (int i = 0; i < n; i++) f[i][i][0] = f[i][i][1] = nums[i];
        for (int len = 2; len <= n; len++) {
            for (int l = 0; l + len - 1 < n; l++) {
                int r = l + len - 1;
                f[l][r][0] = Math.min(nums[r], f[l][r - 1][0]);
                f[l][r][1] = Math.max(nums[r], f[l][r - 1][1]);
            }
        }
        long ans = 0;
        for (int i = 0; i < n; i++) {
            for (int j = i + 1; j < n; j++) {
                ans += f[i][j][1] - f[i][j][0];
            }
        }
        return ans;
    }
}
```
* 时间复杂度:区间 DP 复杂度为 $O(n^2)$;统计范围和的复杂度为 $O(n^2)$。整体复杂度为 $O(n^2)$
* 空间复杂度:$O(n^2)$

---

### 枚举

更进一步,我们发现在转移计算 $[l, r]$ 的最值情况时,仅依赖于 $[l, r - 1]$(小区间),因此我们可以使用两变量代替动规数组,边遍历边维护并统计答案。

代码:
```Java
class Solution {
    public long subArrayRanges(int[] nums) {
        int n = nums.length;
        long ans = 0;
        for (int i = 0; i < n; i++) {
            int min = nums[i], max = nums[i];
            for (int j = i + 1; j < n; j++) {
                min = Math.min(min, nums[j]);
                max = Math.max(max, nums[j]);
                ans += max - min;
            }
        }
        return ans;
    }
}
```
* 时间复杂度:$O(n^2)$
* 空间复杂度:$O(1)$

---

### 单调栈

假设有 $m$ 个区间,最终的表达式为 $m$ 个等式 $\max - \min$ 之和。

若某个 $nums[i]$,如果在这 $k_1$ 个区间中充当最大值,则在最终等式中以 $\max$ 的形式出现 $k_1$ 次,如果在 $k_2$ 个区间中充当最小值,则在最终等式中以 $\min$ 形式出现 $k_2$ 次。

因此我们可以统计每个 $nums[i]$ 成为区间最大值的次数 $k_1$ 和成为区间最小值的次数 $k_2$,$(k_1 - k_2) * nums[i]$ 为 $nums[i]$ 对于最终答案的贡献。

考虑如何统计每个 $nums[i]$ 成为区间最值的次数:

* $nums[i]$ 作为区间最大值的次数:找到 $nums[i]$ 左右最近一个不满足「小于等于 $nums[i]$」的位置,记其为 $p$ 和 $q$。此时区间左端点共有 $i - p$ 个选择,区间右端点共有 $q - i$ 个选择,根据乘法原理,区间个数为 $(i - p) * (q - i)$ 个;
* $nums[i]$ 作为区间最小值的次数:同理,找到 $nums[i]$ 左右最近一个不满足「大于等于 $nums[i]$」的位置,记其为 $p$ 和 $q$,区间个数为 $(i - p) * (q - i)$ 个。

即问题切换为:**使用「单调栈」找到某个 $nums[i]$ 的左边/右边的最近一个符合某种性质的位置,从而知道 $nums[i]$ 作为区间最值时,左右端点的可选择个数,再结合乘法原理知道 $nums[i]$ 能够作为区间最值的区间个数,从而知道 $nums[i]$ 对答案的贡献。**

值得注意的是,由于 $nums[i]$ 存在相同元素,因此上述两边均取等号的做法会导致某些区间被重复计算,因此我们可以令最近右端点的部分不取等号,确保区间统计不重不漏。

代码:
```Java
class Solution {
    int n;
    public long subArrayRanges(int[] nums) {
        n = nums.length;
        // min[i] 为 nums[i] 作为区间最小值的次数;max[i] 为 nums[i] 作为区间最大值的次数
        long[] min = getCnt(nums, true), max = getCnt(nums, false);
        long ans = 0;
        for (int i = 0; i < n; i++) ans += (max[i] - min[i]) * nums[i];
        return ans;
    }
    long[] getCnt(int[] nums, boolean isMin) {
        int[] a = new int[n], b = new int[n];
        Deque<Integer> d = new ArrayDeque<>();
        for (int i = 0; i < n; i++) {
            while (!d.isEmpty() && (isMin ? nums[d.peekLast()] >= nums[i] : nums[d.peekLast()] <= nums[i])) d.pollLast();
            a[i] = d.isEmpty() ? -1 : d.peekLast();
            d.addLast(i);
        }
        d.clear();
        for (int i = n - 1; i >= 0; i--) {
            while (!d.isEmpty() && (isMin ? nums[d.peekLast()] > nums[i] : nums[d.peekLast()] < nums[i])) d.pollLast();
            b[i] = d.isEmpty() ? n : d.peekLast();
            d.addLast(i);
        }
        long[] ans = new long[n];
        for (int i = 0; i < n; i++) ans[i] = (i - a[i]) * 1L * (b[i] - i);
        return ans;
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


LeetCode/211-220/211. 添加与搜索单词 - 数据结构设计（中等）.md

211. 添加与搜索单词 - 数据结构设计

Question:
请你设计一个数据结构,支持 添加新单词 和 查找字符串是否与任何先前添加的字符串匹配 。

实现词典类 `WordDictionary` :
* `WordDictionary()` 初始化词典对象
* `void addWord(word)` 将 `word` 添加到数据结构中,之后可以对它进行匹配
* `bool search(word)` 如果数据结构中存在字符串与 `word` 匹配,则返回 `true` ;否则,返回  `false`
* `word` 中可能包含一些 `'.'` ,每个 `.` 都可以表示任何一个字母。

示例:
```
输入:
["WordDictionary","addWord","addWord","addWord","search","search","search","search"]
[[],["bad"],["dad"],["mad"],["pad"],["bad"],[".ad"],["b.."]]

输出:
[null,null,null,null,false,true,true,true]

解释:
WordDictionary wordDictionary = new WordDictionary();
wordDictionary.addWord("bad");
wordDictionary.addWord("dad");
wordDictionary.addWord("mad");
wordDictionary.search("pad"); // return False
wordDictionary.search("bad"); // return True
wordDictionary.search(".ad"); // return True
wordDictionary.search("b.."); // return True
```

提示:
* $1 <= word.length <= 500$
* `addWord` 中的 `word` 由小写英文字母组成
* `search` 中的 `word` 由 `'.'` 或小写英文字母组成
* 最多调用 $50000$ 次 `addWord` 和 `search`

Solution:
### 基本分析

一道 $Trie$ 的轻度变形模板题,还不熟悉 $Trie$ 的同学可以看 [这里](https://mp.weixin.qq.com/s?__biz=MzU4NDE3MTEyMA==&mid=2247488490&idx=1&sn=db2998cb0e5f08684ee1b6009b974089&chksm=fd9cb8f5caeb31e3f7f67dba981d8d01a24e26c93ead5491edb521c988adc0798d8acb6f9e9d&token=1006889101&lang=zh_CN#rd),里面详细介绍了实现 $Trie$ 的两种方式、注意事项以及 $Trie$ 应用面等等,是解决本题的前置芝士 🧀。

简单回顾一下:

$Trie$ 树(又叫「前缀树」或「字典树」)是一种用于快速查询「某个字符串/字符前缀」是否存在的数据结构。

其核心是使用「边」来代表有无字符,使用「点」来记录是否为「单词结尾」以及「其后续字符串的字符是什么」。

回到本题,**首先 `addWord` 操作不会带 `.` 符号,因此我们采用原有的 $Trie$ 插入方式即可;而在 `search` 操作中会有 `.` 符号,我们需要枚举某个 `.` 所代指的字母是什么,这需要结合 `DFS` 来做。**

---

### 二维数组

使用数组实现,需要预先估算数组大小。

通常估算值会很大,直接使用估算值会 MLE。利用 $Trie$  会有很多位置被共用,以及合格的测试用例,应该至少做到「查询」调用次数和「插入」调用次数相当,我们可以使用比估算值小的数(往下调整一个数量级),更详细的估算逻辑在 [前置芝士](https://mp.weixin.qq.com/s?__biz=MzU4NDE3MTEyMA==&mid=2247488490&idx=1&sn=db2998cb0e5f08684ee1b6009b974089&chksm=fd9cb8f5caeb31e3f7f67dba981d8d01a24e26c93ead5491edb521c988adc0798d8acb6f9e9d&token=1006889101&lang=zh_CN#rd) 讲过,不再赘述。

使用数组实现,还有一个可优化的地方是使用 `static` 修饰所有用到的数组,然后在初始化 `Solution` 的时候做清理工作,这样可以有效避免跑每个样例都创建大数组。

实际测试使用 `static` 与否执行时间会相差超过一半。

代码($P1$ 带 `static` 优化):
```Java
class WordDictionary {
    static int N = 250000;
    static int[][] tr = new int[N][26];
    static boolean[] isWord = new boolean[N];
    static int idx;
    public WordDictionary() {
        for (int i = 0; i < idx; i++) {
            Arrays.fill(tr[i], 0);
        }
        Arrays.fill(isWord, false);
        idx = 0;
    }
    public void addWord(String s) {
        int p = 0;
        for (int i = 0; i < s.length(); i++) {
            int u = s.charAt(i) - 'a';
            if (tr[p][u] == 0) tr[p][u] = ++idx;
            p = tr[p][u];
        }
        isWord[p] = true;
    }
    public boolean search(String s) {
        return dfs(s, 0, 0);
    }
    boolean dfs(String s, int trIdx, int sIdx) {
        int n = s.length();
        if (n == sIdx) return isWord[trIdx];
        char c = s.charAt(sIdx);
        if (c == '.') {
            for (int j = 0; j < 26; j++) {
                if (tr[trIdx][j] != 0 && dfs(s, tr[trIdx][j], sIdx + 1)) return true;
            }
            return false;
        } else {
            int u = c - 'a';
            if (tr[trIdx][u] == 0) return false;
            return dfs(s, tr[trIdx][u], sIdx + 1);
        }
    }
}
```

```Java
class WordDictionary {
    int N = 250000;
    int[][] tr = new int[N][26];
    boolean[] isWord = new boolean[N];
    int idx;
    public void addWord(String s) {
        int p = 0;
        for (int i = 0; i < s.length(); i++) {
            int u = s.charAt(i) - 'a';
            if (tr[p][u] == 0) tr[p][u] = ++idx;
            p = tr[p][u];
        }
        isWord[p] = true;
    }
    public boolean search(String s) {
        return dfs(s, 0, 0);
    }
    boolean dfs(String s, int trIdx, int sIdx) {
        int n = s.length();
        if (sIdx == n) return isWord[trIdx];
        char c = s.charAt(sIdx);
        if (c == '.') {
            for (int j = 0; j < 26; j++) {
                if (tr[trIdx][j] != 0 && dfs(s, tr[trIdx][j], sIdx + 1)) return true;
            }
            return false;
        } else {
            int u = c - 'a';
            if (tr[trIdx][u] == 0) return false;
            return dfs(s, tr[trIdx][u], sIdx + 1);
        }
    }
}
```
* 时间复杂度:$L$ 为字符串的最大长度,`addWord` 操作的复杂度为 $O(L)$;`search` 操作的复杂度为 $O(C * L)$,其中 $C$ 为字符集大小,固定为 $26$
* 空间复杂度:静态数组大小固定,复杂度为 $O(1e7)$

---

### TrieNode

同理,我们也能够使用 $TrieNode$ 的方式实现。

好处是不需要进行数组大小估算,但是不可避免需要在一个样例中执行多次 $new$ 动作。

代码:
```Java
class WordDictionary {
    class Node {
        Node[] tns = new Node[26];
        boolean isWord;
    }
    Node root = new Node();
    public void addWord(String s) {
        Node p = root;
        for (int i = 0; i < s.length(); i++) {
            int u = s.charAt(i) - 'a';
            if (p.tns[u] == null) p.tns[u] = new Node();
            p = p.tns[u];
        }
        p.isWord = true;
    }
    public boolean search(String s) {
        return dfs(s, root, 0);
    }
    boolean dfs(String s, Node p, int sIdx) {
        int n = s.length();
        if (n == sIdx) return p.isWord;
        char c = s.charAt(sIdx);
        if (c == '.') {
            for (int j = 0; j < 26; j++) {
                if (p.tns[j] != null && dfs(s, p.tns[j], sIdx + 1)) return true;
            }
            return false;
        } else {
            int u = c - 'a';
            if (p.tns[u] == null) return false;
            return dfs(s, p.tns[u], sIdx + 1);
        }
    }
}
```
* 时间复杂度:`addWord` 操作的复杂度为 $O(L)$;`search` 操作的复杂度为 $O(C * L)$,其中 $C$ 为字符集大小,固定为 $26$
* 空间复杂度:令 `n` 为插入字符串数量,$L$ 为字符串的最大长度,复杂度为 $O(n * L)$

### 最后

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

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

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

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


Question:
给定一个 $m x n$ 二维字符网格 $board$ 和一个单词(字符串)列表 $words$,找出所有同时在二维网格和字典中出现的单词。

单词必须按照字母顺序,通过 相邻的单元格 内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母在一个单词中不允许被重复使用。

示例 1:

```
输入:board = [["o","a","a","n"],["e","t","a","e"],["i","h","k","r"],["i","f","l","v"]], words = ["oath","pea","eat","rain"]

输出:["eat","oath"]
```
示例 2:

```
输入:board = [["a","b"],["c","d"]], words = ["abcb"]

输出:[]
```

提示:
* m == board.length
* n == board[i].length
* 1 <= m, n <= 12
* board[i][j] 是一个小写英文字母
* 1 <= words.length <= 3 * $10^4$
* 1 <= words[i].length <= 10
* words[i] 由小写英文字母组成
* words 中的所有字符串互不相同

Solution:
### 回溯算法

**数据范围只有 $12$,且 `words` 中出现的单词长度不会超过 $10$,可以考虑使用「回溯算法」。**

起始先将所有 `words` 出现的单词放到 `Set` 结构中,然后以 `board` 中的每个点作为起点进行爆搜(由于题目规定在一个单词中每个格子只能被使用一次,因此还需要一个 `vis` 数组来记录访问过的位置):

1. 如果当前爆搜到的字符串长度超过 $10$,直接剪枝;
2. 如果当前搜索到的字符串在 `Set` 中,则添加到答案(同时了防止下一次再搜索到该字符串,需要将该字符串从 `Set` 中移除)。

代码:
```Java
class Solution {
    Set<String> set = new HashSet<>();
    List<String> ans = new ArrayList<>();
    char[][] board;
    int[][] dirs = new int[][]{{1,0},{-1,0},{0,1},{0,-1}};
    int n, m;
    boolean[][] vis = new boolean[15][15];
    public List<String> findWords(char[][] _board, String[] words) {
        board = _board;
        m = board.length; n = board[0].length;
        for (String w : words) set.add(w);
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                vis[i][j] = true;
                sb.append(board[i][j]);
                dfs(i, j, sb);
                vis[i][j] = false;
                sb.deleteCharAt(sb.length() - 1);
            }
        }
        return ans;
    }
    void dfs(int i, int j, StringBuilder sb) {
        if (sb.length() > 10) return ;
        if (set.contains(sb.toString())) {
            ans.add(sb.toString());
            set.remove(sb.toString());
        }
        for (int[] d : dirs) {
            int dx = i + d[0], dy = j + d[1];
            if (dx < 0 || dx >= m || dy < 0 || dy >= n) continue;
            if (vis[dx][dy]) continue;
            vis[dx][dy] = true;
            sb.append(board[dx][dy]);
            dfs(dx, dy, sb);
            vis[dx][dy] = false;
            sb.deleteCharAt(sb.length() - 1);
        }
    }
}
```
* 时间复杂度:共有 $m * n$ 个起点,每次能往 $4$ 个方向搜索(不考虑重复搜索问题),且搜索的长度不会超过 $10$。整体复杂度为 $O(m * n * 4^{10})$
* 空间复杂度:$O(\sum_{i=0}^{words.length - 1} words[i].length)$

---

### Trie

**在「解法一」中,对于任意一个当前位置 $(i, j)$,我们都不可避免的搜索了四联通的全部方向,这导致了那些无效搜索路径最终只有长度达到 $10$ 才会被剪枝。**

**要进一步优化我们的搜索过程,需要考虑如何在每一步的搜索中进行剪枝。**

我们可以使用 $Trie$ 结构进行建树,对于任意一个当前位置 $(i, j)$ 而言,只有在 $Trie$ 中存在往从字符 $a$ 到 $b$ 的边时,我们才在棋盘上搜索从 $a$ 到 $b$ 的相邻路径。

不了解 $Trie$ 的同学,可以看看这篇题解 [(题解) 208. 实现 Trie (前缀树)](https://leetcode-cn.com/problems/implement-trie-prefix-tree/solution/gong-shui-san-xie-yi-ti-shuang-jie-er-we-esm9/),里面写了两种实现 $Trie$ 的方式。

对于本题,我们可以使用「TrieNode」的方式进行建 $Trie$。

因为 `words` 里最多有 $10^4$ 个单词,每个单词长度最多为 $10$,如果开成静态数组的话,不考虑共用行的问题,我们需要开一个大小为 $10^5 * 26$ 的大数组,可能会有 TLE 或 MLE 的风险。

与此同时,我们需要将平时建 $TrieNode$ 中的 `isEnd` 标记属性直接换成记录当前字符 `s`,这样我们在 `DFS` 的过程中则无须额外记录当前搜索字符串。

代码:
```Java
class Solution {
    class TrieNode {
        String s;
        TrieNode[] tns = new TrieNode[26];
    }
    void insert(String s) {
        TrieNode p = root;
        for (int i = 0; i < s.length(); i++) {
            int u = s.charAt(i) - 'a';
            if (p.tns[u] == null) p.tns[u] = new TrieNode();
            p = p.tns[u];
        }
        p.s = s;
    }
    Set<String> set = new HashSet<>();
    char[][] board;
    int n, m;
    TrieNode root = new TrieNode();
    int[][] dirs = new int[][]{{1,0},{-1,0},{0,1},{0,-1}};
    boolean[][] vis = new boolean[15][15];
    public List<String> findWords(char[][] _board, String[] words) {
        board = _board;
        m = board.length; n = board[0].length;
        for (String w : words) insert(w);
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                int u = board[i][j] - 'a';
                if (root.tns[u] != null) {
                    vis[i][j] = true;
                    dfs(i, j, root.tns[u]);
                    vis[i][j] = false;
                }
            }
        }
        List<String> ans = new ArrayList<>();
        for (String s : set) ans.add(s);
        return ans;
    }
    void dfs(int i, int j, TrieNode node) {
        if (node.s != null) set.add(node.s);
        for (int[] d : dirs) {
            int dx = i + d[0], dy = j + d[1];
            if (dx < 0 || dx >= m || dy < 0 || dy >= n) continue;
            if (vis[dx][dy]) continue;
            int u = board[dx][dy] - 'a';
            if (node.tns[u] != null) {
                vis[dx][dy] = true;
                dfs(dx, dy, node.tns[u]);
                vis[dx][dy] = false;
            }
        }
    }
}
```
* 时间复杂度:共有 $m * n$ 个起点,每次能往 $4$ 个方向搜索(不考虑重复搜索问题),且搜索的长度不会超过 $10$。整体复杂度为 $O(m * n * 4^{10})$
* 空间复杂度:$O(\sum_{i=0}^{words.length - 1} words[i].length * C)$,$C$ 为字符集大小,固定为 $26$

### 最后

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

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

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

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


Question:
你是一个专业的小偷,计划偷窃沿街的房屋,每间房内都藏有一定的现金。

这个地方所有的房屋都 围成一圈 ,这意味着第一个房屋和最后一个房屋是紧挨着的。

同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警 。

给定一个代表每个房屋存放金额的非负整数数组,计算你 在不触动警报装置的情况下 ,今晚能够偷窃到的最高金额。

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

输出:3

解释:你不能先偷窃 1 号房屋(金额 = 2),然后偷窃 3 号房屋(金额 = 2), 因为他们是相邻的。
```
示例 2:
```
输入:nums = [1,2,3,1]

输出:4

解释:你可以先偷窃 1 号房屋(金额 = 1),然后偷窃 3 号房屋(金额 = 3)。
     偷窃到的最高金额 = 1 + 3 = 4 。
```
示例 3:
```
输入:nums = [0]

输出:0
```

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

Solution:
### 动态规划

在 [198. 打家劫舍](https://leetcode-cn.com/problems/house-robber/) 中,并没有「第一间」和「最后一间」不能同时选择的限制,因此我们从头到尾做一遍 DP 即可。

在 [198. 打家劫舍](https://leetcode-cn.com/problems/house-robber/) 中,我们可以将状态定义为两维:

**$f[i][j]$ 代表考虑前 $i$ 个房间,当前 $i$ 房间的现在状态为 $j$ 的最大价值。**

* $f[i][0]$ 代表考虑前 $i$ 个房间,并且「不选」第 $i$ 个房间的最大价值。由于已经明确了第 $i$ 个房间不选,因此 $f[i][0]$ 可以直接由 $max(f[i - 1][0], f[i - 1][1])$ 转移而来。

* $f[i][1]$ 代表考虑前 $i$ 个房间,并且「选」第 $i$ 个房间的最大价值。由于已经明确了第 $i$ 个房间被选,因此 $f[i][1]$ 直接由 $f[i - 1][0] + nums[i]$ 转移过来。

到这里,你已经解决了 [198. 打家劫舍](https://leetcode-cn.com/problems/house-robber/) 了。

对于本题,由于只是增加了「第一间」和「最后一间」不能同时选择的限制。

**通常,对于一些明显不是「增加维度」的新限制条件,我们应当考虑直接将其拎出讨论,而不是多增加一维进行状态记录。**

我们可以把「第一间」&「最后一间」单独拎出来讨论:

* 明确「不选」第一间:
    1. 初始化 $f[0][0]$ 和 $f[0][1]$,均为 $0$。
    2. 先从「第二间」开始递推到「倒数第二间」的最大价值。
    3. 再处理「最后一间」的情况:由于明确了「不选第一间」,则最后的最大价值为 $max(f[n - 2][1], f[n - 2][0] + nums[n - 1])$。

* 允许「选」第一间:
    1. 初始化 $f[0][0]$ 和 $f[0][1]$,分别为 $0$ 和 $nums[0]$。
    2. 先从「第二间」开始递推到「倒数第二间」的最大价值。
    3. 再处理「最后一间」的情况:由于明确了「选第一间」,则最后的最大价值为 $max(f[n - 2][0], f[n - 2][1])$。

走完两遍 DP 后,再从两种情况的最大价值中再取一个 $max$ 即是答案。

代码:
```java []
class Solution {
    public int rob(int[] nums) {
        int n = nums.length;
        if (n == 0) return 0;
        if (n == 1) return nums[0];

        // 第一间「必然不选」的情况
        int[][] f = new int[n][2];
        for (int i = 1; i < n - 1; i++) {
            f[i][0] = Math.max(f[i - 1][0], f[i - 1][1]);
            f[i][1] = f[i - 1][0] + nums[i];
        }
        int a = Math.max(f[n - 2][1], f[n - 2][0] + nums[n - 1]);

        // 第一间「允许选」的情况
        f[0][0] = 0; f[0][1] = nums[0];
        for (int i = 1; i < n - 1; i++) {
            f[i][0] = Math.max(f[i - 1][0], f[i - 1][1]);
            f[i][1] = f[i - 1][0] + nums[i];
        }
        int b = Math.max(f[n - 2][0], f[n - 2][1]);

        return Math.max(a, b);
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n)$

---

### 空间优化

不难发现,我们状态转移最多依赖到前面的 1 行,因此可以通过很机械的「滚动数组」方式将空间修改到 $O(1)$。

代码:
```java []
class Solution {
    public int rob(int[] nums) {
        int n = nums.length;
        if (n == 0) return 0;
        if (n == 1) return nums[0];

        // 第一间「必然不选」的情况
        int[][] f = new int[2][2];
        for (int i = 1; i < n - 1; i++) {
            f[i%2][0] = Math.max(f[(i - 1)%2][0], f[(i - 1)%2][1]);
            f[i%2][1] = f[(i - 1)%2][0] + nums[i];
        }
        int a = Math.max(f[(n - 2)%2][1], f[(n - 2)%2][0] + nums[n - 1]);

        // 第一间「允许选」的情况
        f[0][0] = 0; f[0][1] = nums[0];
        for (int i = 1; i < n - 1; i++) {
            f[i%2][0] = Math.max(f[(i - 1)%2][0], f[(i - 1)%2][1]);
            f[i%2][1] = f[(i - 1)%2][0] + nums[i];
        }
        int b = Math.max(f[(n - 2)%2][0], f[(n - 2)%2][1]);

        return Math.max(a, b);
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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