731. 我的日程安排表 II

Question:
实现一个 `MyCalendar` 类来存放你的日程安排。如果要添加的时间内不会导致三重预订时,则可以存储这个新的日程安排。

`MyCalendar` 有一个 `book(int start, int end)` 方法。它意味着在 `start` 到 `end` 时间内增加一个日程安排,注意,这里的时间是半开区间,即 $[start, end)$, 实数 $x$ 的范围为,$ start <= x < end$。

当三个日程安排有一些时间上的交叉时(例如三个日程安排都在同一时间内),就会产生三重预订。

每次调用 `MyCalendar.book` 方法时,如果可以将日程安排成功添加到日历中而不会导致三重预订,返回 `true`。否则,返回 `false` 并且不要将该日程安排添加到日历中。

请按照以下步骤调用 `MyCalendar` 类: `MyCalendar cal = new MyCalendar(); MyCalendar.book(start, end)`

示例:
```
MyCalendar();
MyCalendar.book(10, 20); // returns true
MyCalendar.book(50, 60); // returns true
MyCalendar.book(10, 40); // returns true
MyCalendar.book(5, 15); // returns false
MyCalendar.book(5, 10); // returns true
MyCalendar.book(25, 55); // returns true

解释:
前两个日程安排可以添加至日历中。 第三个日程安排会导致双重预订,但可以添加至日历中。
第四个日程安排活动(5,15)不能添加至日历中,因为它会导致三重预订。
第五个日程安排(5,10)可以添加至日历中,因为它未使用已经双重预订的时间10。
第六个日程安排(25,55)可以添加至日历中,因为时间 [25,40] 将和第三个日程安排双重预订;
时间 [40,50] 将单独预订,时间 [50,55)将和第二个日程安排双重预订。
```

提示:
* 每个测试用例,调用 `MyCalendar.book` 函数最多不超过 $1000$ 次。
* 调用函数 `MyCalendar.book(start, end)` 时, `start` 和 `end` 的取值范围为 $[0, 10^9]$。

Solution:
### 线段树(动态开点)- 估点

和 [729. 我的日程安排表 I](https://leetcode-cn.com/problems/my-calendar-i/solution/by-ac_oier-1znx/) 几乎完全一致,只需要将对「线段树」所维护的节点信息进行调整即可。

线段树维护的节点信息包括:

* `ls/rs`: 分别代表当前节点的左右子节点在线段树数组 `tr` 中的下标;
* `add`: 懒标记;
* `max`: 为当前区间的最大值。

对于常规的线段树实现来说,都是一开始就调用 `build` 操作创建空树,而线段树一般以「满二叉树」的形式用数组存储,因此需要 $4 \times n$ 的空间,并且这些空间在起始 `build` 空树的时候已经锁死。

如果一道题仅仅是「值域很大」的离线题(提前知晓所有的询问),我们还能通过「离散化」来进行处理,将值域映射到一个小空间去,从而解决 `MLE`  问题。

但对于本题而言,由于「强制在线」的原因,我们无法进行「离散化」,同时值域大小达到 $1e9$ 级别,因此如果我们想要使用「线段树」进行求解,只能采取「动态开点」的方式进行。

动态开点的优势在于,不需要事前构造空树,而是在插入操作 `add` 和查询操作 `query` 时根据访问需要进行「开点」操作。由于我们不保证查询和插入都是连续的,因此对于父节点 $u$ 而言,我们不能通过 `u << 1` 和 `u << 1  | 1` 的固定方式进行访问,而要将节点 $tr[u]$ 的左右节点所在 `tr` 数组的下标进行存储,分别记为 `ls` 和 `rs` 属性。对于 $tr[u].ls = 0$ 和 $tr[u].rs = 0$ 则是代表子节点尚未被创建,当需要访问到它们,而又尚未创建的时候,则将其进行创建。

由于存在「懒标记」,线段树的插入和查询都是 $\log{n}$ 的,因此我们在单次操作的时候,最多会创建数量级为 $\log{n}$ 的点,因此空间复杂度为 $O(m\log{n})$,而不是 $O(4 \times n)$,而开点数的预估需不能仅仅根据 $\log{n}$ 来进行,还要对常熟进行分析,才能得到准确的点数上界。

动态开点相比于原始的线段树实现,本质仍是使用「满二叉树」的形式进行存储,只不过是按需创建区间,如果我们是按照连续段进行查询或插入,最坏情况下仍然会占到 $4 \times n$ 的空间,因此盲猜 $\log{n}$ 的常数在 $4$ 左右,保守一点可以直接估算到 $6$,因此我们可以估算点数为 $6 \times m \times \log{n}$,其中 $n = 1e9$ 和 $m = 1e3$ 分别代表值域大小和查询次数。

当然一个比较实用的估点方式可以「尽可能的多开点数」,利用题目给定的空间上界和我们创建的自定义类(结构体)的大小,尽可能的多开( `Java` 的 $128M$ 可以开到 $5 \times 10^6$ 以上)。

Java 代码:
```Java
class MyCalendarTwo {
    class Node {
        int ls, rs, add, max;
    }
    int N = (int)1e9, M = 120010, cnt = 1;
    Node[] tr = new Node[M];
    void update(int u, int lc, int rc, int l, int r, int v) {
        if (l <= lc && rc <= r) {
            tr[u].add += v;
            tr[u].max += v;
            return ;
        }
        lazyCreate(u);
        pushdown(u);
        int mid = lc + rc >> 1;
        if (l <= mid) update(tr[u].ls, lc, mid, l, r, v);
        if (r > mid) update(tr[u].rs, mid + 1, rc, l, r, v);
        pushup(u);
    }
    int query(int u, int lc, int rc, int l, int r) {
        if (l <= lc && rc <= r) return tr[u].max;
        lazyCreate(u);
        pushdown(u);
        int mid = lc + rc >> 1, ans = 0;
        if (l <= mid) ans = Math.max(ans, query(tr[u].ls, lc, mid, l, r));
        if (r > mid) ans = Math.max(ans, query(tr[u].rs, mid + 1, rc, l, r));
        return ans;
    }
    void lazyCreate(int u) {
        if (tr[u] == null) tr[u] = new Node();
        if (tr[u].ls == 0) {
            tr[u].ls = ++cnt;
            tr[tr[u].ls] = new Node();
        }
        if (tr[u].rs == 0) {
            tr[u].rs = ++cnt;
            tr[tr[u].rs] = new Node();
        }
    }
    void pushup(int u) {
        tr[u].max = Math.max(tr[tr[u].ls].max, tr[tr[u].rs].max);
    }
    void pushdown(int u) {
        tr[tr[u].ls].add += tr[u].add; tr[tr[u].rs].add += tr[u].add;
        tr[tr[u].ls].max += tr[u].add; tr[tr[u].rs].max += tr[u].add;
        tr[u].add = 0;
    }
    public boolean book(int start, int end) {
        if (query(1, 1, N + 1, start + 1, end) >= 2) return false;
        update(1, 1, N + 1, start + 1, end, 1);
        return true;
    }
}
```
TypeScript 代码:
```TypeScript
class TNode {
    ls: number = 0; rs: number = 0
    max:number = 0; add: number = 0;
}
class MyCalendarTwo {
    N = 1e9; M = 120010; cnt = 1
    tr: TNode[] = new Array<TNode>(this.M)
    query(u: number, lc: number, rc: number, l: number, r: number): number {
        if (l <= lc && rc <= r) return this.tr[u].max;
        this.pushdown(u)
        let mid = lc + rc >> 1, ans = 0
        if (l <= mid) ans = Math.max(ans, this.query(this.tr[u].ls, lc, mid, l, r))
        if (r > mid) ans = Math.max(ans, this.query(this.tr[u].rs, mid + 1, rc, l, r))
        return ans
    }
    update(u: number, lc: number, rc: number, l: number, r: number, v: number): number {
        if (l <= lc && rc <= r) {
            this.tr[u].add += v
            this.tr[u].max += v
            return
        }
        this.pushdown(u)
        let mid = lc + rc >> 1
        if (l <= mid) this.update(this.tr[u].ls, lc, mid, l, r, v)
        if (r > mid) this.update(this.tr[u].rs, mid + 1, rc, l, r, v)
        this.pushdup(u)
    }
    pushdown(u: number): void {
        if (this.tr[u] == null) this.tr[u] = new TNode()
        if (this.tr[u].ls == 0) {
            this.tr[u].ls = ++this.cnt
            this.tr[this.tr[u].ls] = new TNode()
        }
        if (this.tr[u].rs == 0) {
            this.tr[u].rs = ++this.cnt
            this.tr[this.tr[u].rs] = new TNode()
        }
        const add = this.tr[u].add
        this.tr[this.tr[u].ls].add += add; this.tr[this.tr[u].rs].add += add
        this.tr[this.tr[u].ls].max += add; this.tr[this.tr[u].rs].max += add
        this.tr[u].add = 0
    }
    pushdup(u: number): void {
        this.tr[u].max = Math.max(this.tr[this.tr[u].ls].max, this.tr[this.tr[u].rs].max)
    }
    book(start: number, end: number): boolean {
        if (this.query(1, 1, this.N + 1, start + 1, end) >= 2) return false
        this.update(1, 1, this.N + 1, start + 1, end, 1)
        return true
    }
}
```
* 时间复杂度:令 $n$ 为值域大小,本题固定为 $1e9$,线段树的查询和增加复杂度均为 $O(\log{n})$
* 空间复杂度:令询问数量为 $m$,复杂度为 $O(m\log{n})$

---

### 线段树(动态开点)- 动态指针

利用「动态指针」实现的「动态开点」可以有效避免数组估点问题,更重要的是可以有效避免 `new` 大数组的初始化开销,对于 LC 这种还跟你算所有样例总时长的 OJ 来说,在不考虑 `static` 优化/全局数组优化 的情况下,动态指针的方式要比估点的方式来得好。

Java 代码:
```Java
class MyCalendarTwo {
    class Node {
        Node ls, rs;
        int max, add;
    }
    int N = (int)1e9;
    Node root = new Node();
    void update(Node node, int lc, int rc, int l, int r, int v) {
        if (l <= lc && rc <= r) {
            node.add += v;
            node.max += v;
            return ;
        }
        pushdown(node);
        int mid = lc + rc >> 1;
        if (l <= mid) update(node.ls, lc, mid, l, r, v);
        if (r > mid) update(node.rs, mid + 1, rc, l, r, v);
        pushup(node);
    }
    int query(Node node, int lc, int rc, int l, int r) {
        if (l <= lc && rc <= r) return node.max;
        pushdown(node);
        int mid = lc + rc >> 1, ans = 0;
        if (l <= mid) ans = query(node.ls, lc, mid, l, r);
        if (r > mid) ans = Math.max(ans, query(node.rs, mid + 1, rc, l, r));
        return ans;
    }
    void pushdown(Node node) {
        if (node.ls == null) node.ls = new Node();
        if (node.rs == null) node.rs = new Node();
        int add = node.add;
        node.ls.max += add; node.rs.max += add;
        node.ls.add += add; node.rs.add += add;
        node.add = 0;
    }
    void pushup(Node node) {
        node.max = Math.max(node.ls.max, node.rs.max);
    }
    public boolean book(int start, int end) {
        if (query(root, 0, N, start, end - 1) >= 2) return false;
        update(root, 0, N, start, end - 1, 1);
        return true;
    }
}
```
TypeScript 代码:
```TypeScript
class TNode {
    ls: TNode = null; rs: TNode = null
    max: number = 0; add: number = 0
}
class MyCalendarTwo {
    root: TNode = new TNode()
    update(node: TNode, lc: number, rc: number, l: number, r: number, v: number): void {
        if (l <= lc && rc <= r) {
            node.add += v
            node.max += v
            return
        }
        this.pushdown(node)
        let mid = lc + rc >> 1
        if (l <= mid) this.update(node.ls, lc, mid, l, r, v)
        if (r > mid) this.update(node.rs, mid + 1, rc, l, r, v)
        this.pushup(node)
    }
    query(node: TNode, lc: number, rc: number, l: number, r: number): number {
        if (l <= lc && rc <= r) return node.max
        let mid = lc + rc >> 1, ans = 0
        this.pushdown(node)
        if (l <= mid) ans = this.query(node.ls, lc, mid, l, r)
        if (r > mid) ans = Math.max(ans, this.query(node.rs, mid + 1, rc, l, r))
        return ans
    }
    pushdown(node: TNode): void {
        if (node.ls == null) node.ls = new TNode()
        if (node.rs == null) node.rs = new TNode()
        const add = node.add
        node.ls.add += add; node.rs.add += add
        node.ls.max += add; node.rs.max += add
        node.add = 0
    }
    pushup(node: TNode): void {
        node.max = Math.max(node.ls.max, node.rs.max)
    }
    book(start: number, end: number): boolean {
        if (this.query(this.root, 0, 1e9, start, end - 1) >= 2) return false
        this.update(this.root, 0, 1e9, start, end - 1, 1)
        return true
    }
}
```
* 时间复杂度:令 $n$ 为值域大小,本题固定为 $1e9$,线段树的查询和增加复杂度均为 $O(\log{n})$
* 空间复杂度:令询问数量为 $m$,复杂度为 $O(m\log{n})$

### 最后

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

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

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

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


732. 我的日程安排表 III

Question:
当 $k$ 个日程安排有一些时间上的交叉时(例如 $k$ 个日程安排都在同一时间内),就会产生 $k$ 次预订。

给你一些日程安排 $[start, end)$ ,请你在每个日程安排添加后,返回一个整数 $k$ ,表示所有先前日程安排会产生的最大 $k$ 次预订。

实现一个 `MyCalendarThree` 类来存放你的日程安排,你可以一直添加新的日程安排。

* `MyCalendarThree()` 初始化对象。
* `int book(int start, int end)` 返回一个整数 $k$ ,表示日历中存在的 $k$ 次预订的最大值。

示例:
```
输入:
["MyCalendarThree", "book", "book", "book", "book", "book", "book"]
[[], [10, 20], [50, 60], [10, 40], [5, 15], [5, 10], [25, 55]]

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

解释:
MyCalendarThree myCalendarThree = new MyCalendarThree();
myCalendarThree.book(10, 20); // 返回 1 ,第一个日程安排可以预订并且不存在相交,所以最大 k 次预订是 1 次预订。
myCalendarThree.book(50, 60); // 返回 1 ,第二个日程安排可以预订并且不存在相交,所以最大 k 次预订是 1 次预订。
myCalendarThree.book(10, 40); // 返回 2 ,第三个日程安排 [10, 40) 与第一个日程安排相交,所以最大 k 次预订是 2 次预订。
myCalendarThree.book(5, 15); // 返回 3 ,剩下的日程安排的最大 k 次预订是 3 次预订。
myCalendarThree.book(5, 10); // 返回 3
myCalendarThree.book(25, 55); // 返回 3
```

提示:
* $0 <= start < end <= 10^9$
* 每个测试用例,调用 `book` 函数最多不超过 $400$ 次

Solution:
### 线段树(动态开点)

和 [731. 我的日程安排表 II](https://mp.weixin.qq.com/s?__biz=MzU4NDE3MTEyMA==&mid=2247491636&idx=1&sn=48bfdd56cdcc33ededd6f154ffd41f0a) 几乎完全一致,只需要将对「线段树」所维护的节点信息进行调整即可。

线段树维护的节点信息包括:

* `ls/rs`: 分别代表当前节点的左右子节点在线段树数组 `tr` 中的下标;
* `add`: 懒标记;
* `max`: 为当前区间的最大值。

对于常规的线段树实现来说,都是一开始就调用 `build` 操作创建空树,而线段树一般以「满二叉树」的形式用数组存储,因此需要 $4 \times n$ 的空间,并且这些空间在起始 `build` 空树的时候已经锁死。

如果一道题仅仅是「值域很大」的离线题(提前知晓所有的询问),我们还能通过「离散化」来进行处理,将值域映射到一个小空间去,从而解决 `MLE`  问题。

但对于本题而言,由于「强制在线」的原因,我们无法进行「离散化」,同时值域大小达到 $1e9$ 级别,因此如果我们想要使用「线段树」进行求解,只能采取「动态开点」的方式进行。

动态开点的优势在于,不需要事前构造空树,而是在插入操作 `add` 和查询操作 `query` 时根据访问需要进行「开点」操作。由于我们不保证查询和插入都是连续的,因此对于父节点 $u$ 而言,我们不能通过 `u << 1` 和 `u << 1  | 1` 的固定方式进行访问,而要将节点 $tr[u]$ 的左右节点所在 `tr` 数组的下标进行存储,分别记为 `ls` 和 `rs` 属性。对于 $tr[u].ls = 0$ 和 $tr[u].rs = 0$ 则是代表子节点尚未被创建,当需要访问到它们,而又尚未创建的时候,则将其进行创建。

由于存在「懒标记」,线段树的插入和查询都是 $\log{n}$ 的,因此我们在单次操作的时候,最多会创建数量级为 $\log{n}$ 的点,因此空间复杂度为 $O(m\log{n})$,而不是 $O(4 \times n)$,而开点数的预估需不能仅仅根据 $\log{n}$ 来进行,还要对常熟进行分析,才能得到准确的点数上界。

动态开点相比于原始的线段树实现,本质仍是使用「满二叉树」的形式进行存储,只不过是按需创建区间,如果我们是按照连续段进行查询或插入,最坏情况下仍然会占到 $4 \times n$ 的空间,因此盲猜 $\log{n}$ 的常数在 $4$ 左右,保守一点可以直接估算到 $6$,因此我们可以估算点数为 $6 \times m \times \log{n}$,其中 $n = 1e9$ 和 $m = 1e3$ 分别代表值域大小和查询次数。

当然一个比较实用的估点方式可以「尽可能的多开点数」,利用题目给定的空间上界和我们创建的自定义类(结构体)的大小,尽可能的多开( `Java` 的 `128M` 可以开到 $5 \times 10^6$ 以上)。

代码:
```Java
class MyCalendarThree {
    class Node {
        int ls, rs, add, max;
    }
    int N = (int)1e9, M = 4 * 400 * 20, cnt = 1;
    Node[] tr = new Node[M];
    void update(int u, int lc, int rc, int l, int r, int v) {
        if (l <= lc && rc <= r) {
            tr[u].add += v;
            tr[u].max += v;
            return ;
        }
        lazyCreate(u);
        pushdown(u);
        int mid = lc + rc >> 1;
        if (l <= mid) update(tr[u].ls, lc, mid, l, r, v);
        if (r > mid) update(tr[u].rs, mid + 1, rc, l, r, v);
        pushup(u);
    }
    int query(int u, int lc, int rc, int l, int r) {
        if (l <= lc && rc <= r) return tr[u].max;
        lazyCreate(u);
        pushdown(u);
        int mid = lc + rc >> 1, ans = 0;
        if (l <= mid) ans = query(tr[u].ls, lc, mid, l, r);
        if (r > mid) ans = Math.max(ans, query(tr[u].rs, mid + 1, rc, l, r));
        return ans;
    }
    void lazyCreate(int u) {
        if (tr[u] == null) tr[u] = new Node();
        if (tr[u].ls == 0) {
            tr[u].ls = ++cnt;
            tr[tr[u].ls] = new Node();
        }
        if (tr[u].rs == 0) {
            tr[u].rs = ++cnt;
            tr[tr[u].rs] = new Node();
        }
    }
    void pushdown(int u) {
        tr[tr[u].ls].add += tr[u].add; tr[tr[u].rs].add += tr[u].add;
        tr[tr[u].ls].max += tr[u].add; tr[tr[u].rs].max += tr[u].add;
        tr[u].add = 0;
    }
    void pushup(int u) {
        tr[u].max = Math.max(tr[tr[u].ls].max, tr[tr[u].rs].max);
    }
    public int book(int start, int end) {
        update(1, 1, N + 1, start + 1, end, 1);
        return query(1, 1, N + 1, 1, N + 1);
    }
}
```
* 时间复杂度:令 $n$ 为值域大小,本题固定为 $1e9$,线段树的查询和增加复杂度均为 $O(\log{n})$
* 空间复杂度:令询问数量为 $m$,复杂度为 $O(m\log{n})$

---

### 带懒标记的分块(TLE)

实际上,还存在另外一种十分值得学习的算法:分块。

但该做法被奇怪的测评机制卡掉,是给每个样例都定了执行用时吗 🤣 ?

旧题解没有这种做法,今天补充的,我们可以大概讲讲「分块」算法是如何解决涉及「区间修改」,也就是带懒标记的问题。

对于本题,我们设定两个块数组 `max` 和 `add`,其中 `max[idx]` 含义为块编号为 `idx` 的连续区间的最大重复值,而 `add[idx]` 代表块编号的懒标记,同时使用「哈希表」记录下某些具体位置的覆盖次数。

然后我们考虑如何指定块大小,设定一个合理的块大小是减少运算量的关键。

通常情况下,我们会设定块大小为 $\sqrt{n}$,从而确保块内最多不超过 $\sqrt{n}$ 个元素,同时块的总个数也不超过 $\sqrt{n}$,即无论我们是块内暴力,还是操作多个块,复杂度均为 $O(\sqrt{n})$。因此对于本题而言,设定块大小为 $\sqrt{1e9}$(经试验,调成 $1200010$ 能够通过较多样例),较为合适。

对于涉及「区间修改」的分块算法,我们需要在每次修改和查询前,先将懒标记下传到区间的每个元素中。

然后考虑如何处理「块内」和「块间」操作:

* 块内操作:
    * 块内更新:直接操作 `map` 进行累加,同时使用更新后的 `map` 来维护相应的 `max[idx]`;
    * 块内查询:直接查询 `map` 即可;
* 块间操作:
    * 块间更新:直接更新懒标记 `add[idx]` 即可,同时由于我们是对整个区间进行累加操作,因此最大值必然也会同步被累加,因此同步更新 `max[idx]`;
    * 块间查询:直接查询 `max[idx]` 即可。

代码:
```Java
class MyCalendarThree {
    static int N = (int)1e9 + 10, M = 1200010, SZ = N / M + 10;
    static int[] max = new int[M], add = new int[M];
    static Map<Integer, Integer> map = new HashMap<>();
    int minv = -1, maxv = -1;
    int getIdx(int x) {
        return x / SZ;
    }
    void add(int l, int r) {
        pushdown(l); pushdown(r);
        if (getIdx(l) == getIdx(r)) {
            for (int k = l; k <= r; k++) {
                map.put(k, map.getOrDefault(k, 0) + 1);
                max[getIdx(k)] = Math.max(max[getIdx(k)], map.get(k));
            }
        } else {
            int i = l, j = r;
            while (getIdx(i) == getIdx(l)) {
                map.put(i, map.getOrDefault(i, 0) + 1);
                max[getIdx(i)] = Math.max(max[getIdx(i)], map.get(i));
                i++;
            }
            while (getIdx(j) == getIdx(r)) {
                map.put(j, map.getOrDefault(j, 0) + 1);
                max[getIdx(j)] = Math.max(max[getIdx(j)], map.get(j));
                j--;
            }
            for (int k = getIdx(i); k <= getIdx(j); k++) {
                max[k]++; add[k]++;
            }
        }
    }
    int query(int l, int r) {
        pushdown(l); pushdown(r);
        int ans = 0;
        if (getIdx(l) == getIdx(r)) {
            for (int k = l; k <= r; k++) ans = Math.max(ans, map.getOrDefault(k, 0));
        } else {
            int i = l, j = r;
            while (getIdx(i) == getIdx(l)) ans = Math.max(ans, map.getOrDefault(i++, 0));
            while (getIdx(j) == getIdx(r)) ans = Math.max(ans, map.getOrDefault(j--, 0));
            for (int k = getIdx(i); k <= getIdx(j); k++) ans = Math.max(ans, max[k]);
        }
        return ans;
    }
    void pushdown(int x) {
        int idx = getIdx(x);
        if (add[idx] == 0) return ;
        int i = x, j = x + 1;
        while (getIdx(i) == idx) map.put(i, map.getOrDefault(i--, 0) + add[idx]);
        while (getIdx(j) == idx) map.put(j, map.getOrDefault(j++, 0) + add[idx]);
        add[idx] = 0;
    }
    public MyCalendarThree() {
        Arrays.fill(max, 0);
        Arrays.fill(add, 0);
        map.clear();
    }
    public int book(int start, int end) {
        add(start + 1, end);
        minv = minv == -1 ? start : Math.min(minv, start);
        maxv = maxv == -1 ? end + 1 : Math.max(maxv, end + 1);
        return query(minv, maxv);
    }
}
```
* 时间复杂度:令 $M$ 为块大小,复杂度为 $O(\sqrt{M})$
* 空间复杂度:$O(C \times \sqrt{M})$,其中 $C = 1e3$ 为最大调用次数

### 最后

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

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

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

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


Question:
给定一个整数数组 `asteroids`,表示在同一行的行星。

对于数组中的每一个元素,其绝对值表示行星的大小,正负表示行星的移动方向(正表示向右移动,负表示向左移动)。

每一颗行星以相同的速度移动,找出碰撞后剩下的所有行星。

碰撞规则:

1. 两个行星相互碰撞,较小的行星会爆炸。
2. 如果两颗行星大小相同,则两颗行星都会爆炸。
3. 两颗移动方向相同的行星,永远不会发生碰撞。

示例 1:
```
输入:asteroids = [5,10,-5]

输出:[5,10]

解释:10 和 -5 碰撞后只剩下 10 。 5 和 10 永远不会发生碰撞。
```
示例 2:
```
输入:asteroids = [8,-8]

输出:[]

解释:8 和 -8 碰撞后,两者都发生爆炸。
```
示例 3:
```
输入:asteroids = [10,2,-5]

输出:[10]

解释:2 和 -5 发生碰撞后剩下 -5 。10 和 -5 发生碰撞后剩下 10 。
```

提示:
* $2 <= asteroids.length <= 10^4$
* $-1000 <= asteroids[i] <= 1000$
* $asteroids[i] != 0$

Solution:
### 模拟 + 栈

由于碰撞抵消总是从相邻行星之间发生,我们可以使用「栈」来模拟该过程。

从前往后处理所有的 $ats[i]$,使用栈存储当前未被抵消的行星,当栈顶元素方向往右,当前 $ats[i]$ 方向往左时,会发生抵消操作,抵消过程根据规则进行即可。

Java 代码:
```Java
class Solution {
    public int[] asteroidCollision(int[] asteroids) {
        Deque<Integer> d = new ArrayDeque<>();
        for (int t : asteroids) {
            boolean ok = true;
            while (ok && !d.isEmpty() && d.peekLast() > 0 && t < 0) {
                int a = d.peekLast(), b = -t;
                if (a <= b) d.pollLast();
                if (a >= b) ok = false;
            }
            if (ok) d.addLast(t);
        }
        int sz = d.size();
        int[] ans = new int[sz];
        while (!d.isEmpty()) ans[--sz] = d.pollLast();
        return ans;
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    vector<int> asteroidCollision(vector<int>& asteroids) {
        deque<int> d;
        for (int t : asteroids) {
            bool ok = true;
            while (ok && !d.empty() && d.back() > 0 && t < 0) {
                int a = d.back(), b = -t;
                if (a <= b) d.pop_back();
                if (a >= b) ok = false;
            }
            if (ok) d.push_back(t);
        }
        vector<int> ans(d.begin(), d.end());
        return ans;
    }
};
```
Python 3 代码:
```Python3
class Solution:
    def asteroidCollision(self, asteroids: List[int]) -> List[int]:
        stk = []
        for t in asteroids:
            ok = True
            while ok and stk and stk[-1] > 0 and t < 0:
                a, b = stk[-1], -t
                if a <= b:
                    stk.pop(-1)
                if a >= b:
                    ok = False
            if ok:
                stk.append(t)
        return stk
```
TypeScript 代码:
```TypeScript
function asteroidCollision(asteroids: number[]): number[] {
    const stk = new Array<number>()
    for (const t of asteroids) {
        let ok = true
        while (ok && stk.length > 0 && stk[stk.length - 1] > 0 && t < 0) {
            const a = stk[stk.length - 1], b = -t
            if (a <= b) stk.pop()
            if (a >= b) ok = false
        }
        if (ok) stk.push(t)
    }
    return stk
};
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


Question:
给你一个类似 `Lisp` 语句的字符串表达式 `expression`,求出其计算结果。

表达式语法如下所示:

* 表达式可以为整数,`let` 表达式,`add` 表达式,`mult` 表达式,或赋值的变量。表达式的结果总是一个整数。
* 整数可以是正整数、负整数、$0$
* `let` 表达式采用 `"(let v1 e1 v2 e2 ... vn en expr)"` 的形式,其中 `let` 总是以字符串 `"let"`来表示,接下来会跟随一对或多对交替的变量和表达式,也就是说,第一个变量 `v1`被分配为表达式 `e1` 的值,第二个变量 `v2` 被分配为表达式 `e2` 的值,依次类推;最终 `let` 表达式的值为 `expr` 表达式的值。
* `add` 表达式表示为 `"(add e1 e2)"` ,其中 `add` 总是以字符串 `"add"` 来表示,该表达式总是包含两个表达式 `e1`、`e2` ,最终结果是 `e1` 表达式的值与 `e2` 表达式的值之 和 。
* `mult` 表达式表示为 `"(mult e1 e2)"` ,其中 `mult` 总是以字符串 `"mult"` 表示,该表达式总是包含两个表达式 `e1`、e2,最终结果是 `e1` 表达式的值与 `e2` 表达式的值之 积 。
* 在该题目中,变量名以小写字符开始,之后跟随 $0$ 个或多个小写字符或数字。为了方便,`"add"` ,`"let"` ,`"mult"` 会被定义为 `"关键字" ,不会用作变量名。
* 最后,要说一下作用域的概念。计算变量名所对应的表达式时,在计算上下文中,首先检查最内层作用域(按括号计),然后按顺序依次检查外部作用域。测试用例中每一个表达式都是合法的。有关作用域的更多详细信息,请参阅示例。

示例 1:

```
输入:expression = "(let x 2 (mult x (let x 3 y 4 (add x y))))"

输出:14

解释:
计算表达式 (add x y), 在检查变量 x 值时,
在变量的上下文中由最内层作用域依次向外检查。
首先找到 x = 3, 所以此处的 x 值是 3 。
```
示例 2:
```
输入:expression = "(let x 3 x 2 x)"

输出:2

解释:let 语句中的赋值运算按顺序处理即可。
```
示例 3:
```
输入:expression = "(let x 1 y 2 x (add x y) (add x y))"

输出:5

解释:
第一个 (add x y) 计算结果是 3,并且将此值赋给了 x 。
第二个 (add x y) 计算结果是 3 + 2 = 5 。
```

提示:
* $1 <= expression.length <= 2000$
* `exprssion` 中不含前导和尾随空格
* `expressoin` 中的不同部分(`token`)之间用单个空格进行分隔
* 答案和所有中间计算结果都符合 `32-bit` 整数范围

Solution:
### DFS 模拟

今天身体不舒服,不写太详细,题目不难,大家结合代码看吧。

设计函数 `int dfs(int l, int r, Map<String, Integer> map)`,代表计算 $s[l...r]$ 的结果,答案为 `dfs(0,n-1,map)`,其中 $n$ 为字符串的长度。

根据传入的 $[l, r]$ 是否为表达式分情况讨论:

* 若 $s[l] = ($,说明是表达式,此时从 $l$ 开始往后取,取到空格为止(假设位置为 `idx`),从而得到操作 `op`(其中 `op` 为 `let`、`add` 或 `mult` 三者之一),此时 `op` 对应参数为 $[idx + 1, r - 1]$,也就是需要跳过位置 $r$(即跳过 `op` 对应的 `)` 符号);

    再根据 `op` 为何种操作进一步处理,我们设计一个「传入左端点找右端点」的函数 `int getRight(int left, int end)`,含义为从 `left` 出发,一直往右找(不超过 `end`),直到取得合法的「子表达式」或「对应的值」。

    ```Java
    // 对于 getRight 函数作用,给大家举个 🌰 理解吧,其实就是从 left 出发,找到合法的「子表达式」或「值」为止

    // (let x 2 (mult x (let x 3 y 4 (add x y))))
    //          a                               b
    // 传入 a 返回 b,代表 [a, b) 表达式为 (mult x (let x 3 y 4 (add x y)))

    // (let x 2 (mult x (let x 3 y 4 (add x y))))
    //      ab
    // 传入 a 返回 b,代表 [a, b) 表达式为 x
    ```

* 否则 $s[l...r]$ 不是表达式,通过判断 $s[l...r]$ 是否有被哈希表记录来得知结果:若在哈希表中有记录,结果为哈希表中的映射值,否则结果为本身所代表的数值。

需要注意,根据「作用域」的定义,我们不能使用全局哈希表,而要在每一层递归时,传入 `clone` 出来的 `map`。

代码:
```Java
class Solution {
    char[] cs;
    String s;
    public int evaluate(String _s) {
        s = _s;
        cs = s.toCharArray();
        return dfs(0, cs.length - 1, new HashMap<>());
    }
    int dfs(int l, int r, Map<String, Integer> map) {
        if (cs[l] == '(') {
            int idx = l;
            while (cs[idx] != ' ') idx++;
            String op = s.substring(l + 1, idx);
            r--;
            if (op.equals("let")) {
                for (int i = idx + 1; i <= r; ) {
                    int j = getRight(i, r);
                    String key = s.substring(i, j);
                    if (j >= r) return dfs(i, j - 1, new HashMap<>(map));
                    j++; i = j;
                    j = getRight(i, r);
                    int value = dfs(i, j - 1, new HashMap<>(map));
                    map.put(key, value);
                    i = j + 1;
                }
                return -1; // never
            } else {
                int j = getRight(idx + 1, r);
                int a = dfs(idx + 1, j - 1, new HashMap<>(map)), b = dfs(j + 1, r, new HashMap<>(map));
                return op.equals("add") ? a + b : a * b;
            }
        } else {
            String cur = s.substring(l, r + 1);
            if (map.containsKey(cur)) return map.get(cur);
            return Integer.parseInt(cur);
        }
    }
    int getRight(int left, int end) {
        int right = left, score = 0;
        while (right <= end) {
            if (cs[right] == '(') {
                score++; right++;
            } else if (cs[right] == ')') {
                score--; right++;
            } else if (cs[right] == ' ') {
                if (score == 0) break;
                right++;
            } else {
                right++;
            }
        }
        return right;
    }
}
```
* 时间复杂度:除对表达式进行递归划分以外,还存在对 `map` 的每层拷贝操作,复杂度为 $O(n^2)$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


Question:
给定一个整数数组 `temperatures`,表示每天的温度,返回一个数组 `answer`,其中 `answer[i]` 是指对于第 `i` 天,下一个更高温度出现在几天后。如果气温在这之后都不会升高,请在该位置用 `0` 来代替。

示例 1:
```
输入: temperatures = [73,74,75,71,69,72,76,73]

输出: [1,1,4,2,1,1,0,0]
```
示例 2:
```
输入: temperatures = [30,40,50,60]

输出: [1,1,1,0]
```
示例 3:
```
输入: temperatures = [30,60,90]

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

提示:
* $1 <= temperatures.length <= 10^5$
* $30 <= temperatures[i] <= 100$

Solution:
### 单调栈

抽象题意为 : 求解给定序列中每个位置(右边)最近一个比其大的位置,可使用「单调栈」来进行求解。

具体的,我们可以从前往后处理所有的 $temperatures[i]$,使用某类容器装载我们所有的「待更新」的位置(下标),假设当前处理到的是 $temperatures[i]$:

* 若其比容器内的任意位置(下标)对应温度要低,其必然不能更新任何位置(下标),将其也加入容器尾部(此时我们发现,若有一个新的位置(下标)加入容器,其必然是当前所有待更新位置(下标)中的温度最低的,即容器内的温度单调递减);

* 若其价格高于容器内的任一位置(下标)对应温度,其能够更新容器位置(下标)的答案,并且由于我们容器满足单调递减特性,我们必然能够从尾部开始取出待更新位置来进行更新答案,直到处理完成或遇到第一个无法更新位置。

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

Java 代码:
```Java
class Solution {
    public int[] dailyTemperatures(int[] temperatures) {
        int n = temperatures.length;
        int[] ans = new int[n];
        Deque<Integer> d = new ArrayDeque<>();
        for (int i = 0; i < n; i++) {
            while (!d.isEmpty() && temperatures[d.peekLast()] < temperatures[i]) {
                int idx = d.pollLast();
                ans[idx] = i - idx;
            }
            d.addLast(i);
        }
        return ans;
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    vector<int> dailyTemperatures(vector<int>& temperatures) {
        int n = temperatures.size();
        vector<int> ans(n);
        deque<int> d;
        for (int i = 0; i < n; i++) {
            while (!d.empty() && temperatures[d.back()] < temperatures[i]) {
                int idx = d.back();
                ans[idx] = i - idx;
                d.pop_back();
            }
            d.push_back(i);
        }
        return ans;
    }
};
```
Python3 代码:
```Python
class Solution:
    def dailyTemperatures(self, temperatures: List[int]) -> List[int]:
        n, he, ta = len(temperatures), 0, 0
        ans, stk = [0] * n, [-1] * n
        for i in range(n):
            while he < ta and temperatures[stk[ta - 1]] < temperatures[i]:
                ta -= 1
                idx = stk[ta]
                ans[idx] = i - idx
            stk[ta] = i
            ta += 1
        return ans
```
TypeScript 代码:
```TypeScript
function dailyTemperatures(temperatures: number[]): number[] {
    const n = temperatures.length
    const ans = new Array<number>(n).fill(0)
    const stk = new Array<number>(n).fill(-1)
    let he = 0, ta = 0
    for (let i = 0; i < n; i++) {
        while (he < ta && temperatures[stk[ta - 1]] < temperatures[i]) {
            const idx = stk[--ta]
            ans[idx] = i - idx
        }
        stk[ta++] = i
    }
    return ans
};
```

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

### 最后

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

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

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

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


740. 删除并获得点数

Question:
给你一个整数数组 nums ,你可以对它进行一些操作。

每次操作中,选择任意一个 nums[i] ,删除它并获得 nums[i] 的点数。之后,你必须删除 所有 等于 nums[i] - 1 和 nums[i] + 1 的元素。

开始你拥有 0 个点数。返回你能通过这些操作获得的最大点数。

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

输出:6

解释:
删除 4 获得 4 个点数,因此 3 也被删除。
之后,删除 2 获得 2 个点数。总共获得 6 个点数。
```
示例 2:
```
输入:nums = [2,2,3,3,3,4]

输出:9

解释:
删除 3 获得 3 个点数,接着要删除两个 2 和 4 。
之后,再次删除 3 获得 3 个点数,再次删除 3 获得 3 个点数。
总共获得 9 个点数。
```

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

Solution:
### 动态规划

根据题意,当我们选择 $nums[i]$ 的时候,比 $nums[i]$ 大/小 一个单位的数都不能被选择。

如果我们将数组排好序,从前往后处理,其实只需要考虑“当前数”与“前一个数”的「大小 & 选择」关系即可,这样处理完,显然每个数的「前一位/后一位」都会被考虑到。

这样我们将问题转化为一个「序列 DP」问题(选择某个数,需要考虑前一个数的「大小/选择」状态)。

**定义 $f[i][0]$ 代表数值为 $i$ 的数字「不选择」的最大价值;$f[i][1]$ 代表数值为 $i$ 的数字「选择」的最大价值。**

为了方便,我们可以先对 $nums$ 中出现的所有数值进行计数,而且由于数据范围只有 $10^4$,我们可以直接使用数组 $cnts[]$ 进行计数:$cnts[x] = i$ 代表数值 $x$ 出现了 $i$ 次。

**然后分别考虑一般性的 $f[i][0]$ 和 $f[i][1]$ 该如何计算:**

* **$f[i][0]$:当数值 $i$ 不被选择,那么前一个数「可选/可不选」,在两者中取 $max$ 即可。转移方程为 $f[i][0] = \max(f[i - 1][0], f[i - 1][1])$**
* **$f[i][1]$:当数值 $i$ 被选,那么前一个数只能「不选」,同时为了总和最大数值 $i$ 要选就全部选完。转移方程为 $f[i][1] = f[i - 1][0] + i * cnts[i]$**

代码:
```Java
class Solution {
    int[] cnts = new int[10009];
    public int deleteAndEarn(int[] nums) {
        int n = nums.length;
        int max = 0;
        for (int x : nums) {
            cnts[x]++;
            max = Math.max(max, x);
        }
        // f[i][0] 代表「不选」数值 i;f[i][1] 代表「选择」数值 i
        int[][] f = new int[max + 1][2];
        for (int i = 1; i <= max; i++) {
            f[i][1] = f[i - 1][0] + i * cnts[i];
            f[i][0] = Math.max(f[i - 1][1], f[i - 1][0]);
        }
        return Math.max(f[max][0], f[max][1]);
    }
}
```
* 时间复杂度:遍历 $nums$ 进行计数和取最大值 $max$,复杂度为 $O(n)$;共有 $max * 2$ 个状态需要被转移,每个状态转移的复杂度为 $O(1)$。整体复杂度为 $O(n + max)$。
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


Question:
一个$N \times N$ 的网格( `grid`) 代表了一块樱桃地,每个格子由以下三种数字的一种来表示:

* $0$ 表示这个格子是空的,所以你可以穿过它。
* $1$ 表示这个格子里装着一个樱桃,你可以摘到樱桃然后穿过它。
* $-1$ 表示这个格子里有荆棘,挡着你的路。

你的任务是在遵守下列规则的情况下,尽可能的摘到最多樱桃:

* 从位置 $(0, 0)$ 出发,最后到达 $(N-1, N-1)$ ,只能向下或向右走,并且只能穿越有效的格子(即只可以穿过值为 $0$ 或者 $1$ 的格子);
* 当到达 $(N-1, N-1)$ 后,你要继续走,直到返回到 $(0, 0)$ ,只能向上或向左走,并且只能穿越有效的格子;
* 当你经过一个格子且这个格子包含一个樱桃时,你将摘到樱桃并且这个格子会变成空的(值变为0);
* 如果在 $(0, 0)$ 和 $(N-1, N-1)$ 之间不存在一条可经过的路径,则没有任何一个樱桃能被摘到。

示例 1:
```
输入: grid =
[[0, 1, -1],
 [1, 0, -1],
 [1, 1,  1]]

输出: 5

解释:
玩家从(0,0)点出发,经过了向下走,向下走,向右走,向右走,到达了点(2, 2)。
在这趟单程中,总共摘到了4颗樱桃,矩阵变成了[[0,1,-1],[0,0,-1],[0,0,0]]。
接着,这名玩家向左走,向上走,向上走,向左走,返回了起始点,又摘到了1颗樱桃。
在旅程中,总共摘到了5颗樱桃,这是可以摘到的最大值了。
```

说明:
* `grid` 是一个 $N \times N$ 的二维数组,N的取值范围是 $1 <= N <= 50$
* 每一个 `grid[i][j]` 都是集合 `{-1, 0, 1}` 其中的一个数
* 可以保证起点 `grid[0][0]` 和终点 `grid[N-1][N-1]` 的值都不会是 $-1$

Solution:
### 线性 DP

为了方便,我们令 `grid` 为 `g`,同时调整矩阵横纵坐标从 $1$ 开始。

原问题为先从左上角按照「只能往下 + 只能往右」的规则走到右下角,然后再按照「只能往上 + 只能往左」的规则走回左上角,途径的值为 $1$ 的格子得一分(只能得分一次,得分后置零),同时不能经过值为 $-1$ 的格子。

其中第二趟的规则等价于按照第一趟的规则从左上角到右下角再走一遍,再结合每个位置的只能得分一次,可以将原问题等价于:两个点从左上角开始同时走,最终都走到右下角的最大得分。

定义 $f[k][i1][i2]$ 为当前走了 $k$ 步(横纵坐标之和),且第一个点当前在第 $i1$ 行,第二点在第 $i2$ 行时的最大得分,最终答案为 $f[2n][n][n]$,同时有 $f[2][1][1] = g[0][0]$ 的起始状态。

由于两个点是同时走(都走了 $k$ 步),结合「只能往下 + 只能往右」的规则,可直接算得第一个点所在的列为 $j1 = k - i1$,第二点所在的列为 $j2 = k - i2$。

不失一般性考虑 $f[k][i1][i2]$ 该如何转移,两个点均有可能走行或走列,即有 $4$ 种前驱状态:$f[k - 1][i1 - 1][i2]$、$f[k - 1][i1 - 1][i2 - 1]$、$f[k - 1][i1][i2 - 1]$ 和 $f[k - 1][i1][i2]$,在四者中取最大值,同时当前位置 $(i1, j1)$ 和 $(i2, j2)$ 的得分需要被累加,假设两者得分别为 $A$ 和 $B$,若两个位置不重叠的话,可以同时累加,否则只能累加一次。

一些细节:为了防止从值为 $-1$ 的格子进行转移影响正确性,我们需要先将所有 $f[k][i1][i2]$ 初始化为负无穷。

代码:
```Java
class Solution {
    static int N = 55, INF = Integer.MIN_VALUE;
    static int[][][] f = new int[2 * N][N][N];
    public int cherryPickup(int[][] g) {
        int n = g.length;
        for (int k = 0; k <= 2 * n; k++) {
            for (int i1 = 0; i1 <= n; i1++) {
                for (int i2 = 0; i2 <= n; i2++) {
                    f[k][i1][i2] = INF;
                }
            }
        }
        f[2][1][1] = g[0][0];
        for (int k = 3; k <= 2 * n; k++) {
            for (int i1 = 1; i1 <= n; i1++) {
                for (int i2 = 1; i2 <= n; i2++) {
                    int j1 = k - i1, j2 = k - i2;
                    if (j1 <= 0 || j1 > n || j2 <= 0 || j2 > n) continue;
                    int A = g[i1 - 1][j1 - 1], B = g[i2 - 1][j2 - 1];
                    if (A == -1 || B == -1) continue;
                    int a = f[k - 1][i1 - 1][i2], b = f[k - 1][i1 - 1][i2 - 1], c = f[k - 1][i1][i2 - 1], d = f[k - 1][i1][i2];
                    int t = Math.max(Math.max(a, b), Math.max(c, d)) + A;
                    if (i1 != i2) t += B;
                    f[k][i1][i2] = t;
                }
            }
        }
        return f[2 * n][n][n] <= 0 ? 0 : f[2 * n][n][n];
    }
}
```
* 时间复杂度:状态数量级为 $n^3$,每个状态转移复杂度为 $O(1)$。整体复杂度为 $O(n^3)$
* 空间复杂度:$O(n^3)$

### 最后

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

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

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

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


Question:
有 $n$ 个网络节点,标记为 $1$ 到 $n$。

给你一个列表 times,表示信号经过 有向 边的传递时间。 times[i] = (ui, vi, wi),其中 ui 是源节点,vi 是目标节点, wi 是一个信号从源节点传递到目标节点的时间。

现在,从某个节点 K 发出一个信号。需要多久才能使所有节点都收到信号?如果不能使所有节点收到信号,返回 -1 。

示例 1:

```
输入:times = [[2,1,1],[2,3,1],[3,4,1]], n = 4, k = 2

输出:2
```
示例 2:
```
输入:times = [[1,2,1]], n = 2, k = 1

输出:1
```
示例 3:
```
输入:times = [[1,2,1]], n = 2, k = 2

输出:-1
```

提示:
* $1 <= k <= n <= 100$
* $1 <= times.length <= 6000$
* $times[i].length == 3$
* $1 <= u_i, v_i <= n$
* $u_i != v_i$
* $0 <= w_i <= 100$
* 所有 $(u_i, v_i)$ 对都 互不相同(即,不含重复边)

Solution:
### 基本分析

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

根据题意,首先 $n$ 的数据范围只有 $100$,$m$ 的数据范围为 $6000$,使用「邻接表」或「邻接矩阵」来存图都可以。

同时求的是「**从 $k$ 点出发,所有点都被访问到的最短时间**」,将问题转换一下其实就是求「**从 $k$ 点出发,到其他点 $x$ 的最短距离的最大值**」。

---

### 存图方式

在开始讲解最短路之前,我们先来学习三种「存图」方式。

#### 邻接矩阵

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

适用于边数较多的**稠密图**使用,当边数量接近点的数量的平方,即 $m \approx n^2$ 时,可定义为**稠密图**。

```Java
// 邻接矩阵数组:w[a][b] = c 代表从 a 到 b 有权重为 c 的边
int[][] w = new int[N][N];

// 加边操作
void add(int a, int b, int c) {
    w[a][b] = c;
}
```
#### 邻接表

这也是一种在图论中十分常见的存图方式,与数组存储单链表的实现一致(头插法)。

这种存图方式又叫**链式前向星存图**。

适用于边数较少的**稀疏图**使用,当边数量接近点的数量,即 $m \approx n$ 时,可定义为**稀疏图**。

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

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

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

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

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

#### 类

这是一种最简单,但是相比上述两种存图方式,使用得较少的存图方式。

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

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

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

通常我们会使用 List 存起所有的边对象,并在需要遍历所有边的时候,进行遍历:

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

...

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

---

### Floyd(邻接矩阵)

根据「基本分析」,我们可以使用复杂度为 $O(n^3)$ 的「多源汇最短路」算法 Floyd 算法进行求解,同时使用「邻接矩阵」来进行存图。

此时计算量约为 $10^6$,可以过。

跑一遍 Floyd,可以得到「**从任意起点出发,到达任意起点的最短距离**」。然后从所有 $w[k][x]$ 中取 $max$ 即是「**从 $k$ 点出发,到其他点 $x$ 的最短距离的最大值**」。

代码:
```Java
class Solution {
    int N = 110, M = 6010;
    // 邻接矩阵数组:w[a][b] = c 代表从 a 到 b 有权重为 c 的边
    int[][] w = new int[N][N];
    int INF = 0x3f3f3f3f;
    int n, k;
    public int networkDelayTime(int[][] ts, int _n, int _k) {
        n = _n; k = _k;
        // 初始化邻接矩阵
        for (int i = 1; i <= n; i++) {
            for (int j = 1; j <= n; j++) {
                w[i][j] = w[j][i] = i == j ? 0 : INF;
            }
        }
        // 存图
        for (int[] t : ts) {
            int u = t[0], v = t[1], c = t[2];
            w[u][v] = c;
        }
        // 最短路
        floyd();
        // 遍历答案
        int ans = 0;
        for (int i = 1; i <= n; i++) {
            ans = Math.max(ans, w[k][i]);
        }
        return ans >= INF / 2 ? -1 : ans;
    }
    void floyd() {
        // floyd 基本流程为三层循环:
        // 枚举中转点 - 枚举起点 - 枚举终点 - 松弛操作
        for (int p = 1; p <= n; p++) {
            for (int i = 1; i <= n; i++) {
                for (int j = 1; j <= n; j++) {
                    w[i][j] = Math.min(w[i][j], w[i][p] + w[p][j]);
                }
            }
        }
    }
}
```
* 时间复杂度:$O(n^3)$
* 空间复杂度:$O(n^2)$

---

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

同理,我们可以使用复杂度为 $O(n^2)$ 的「单源最短路」算法朴素 Dijkstra 算法进行求解,同时使用「邻接矩阵」来进行存图。

根据题意,$k$ 点作为源点,跑一遍 Dijkstra 我们可以得到从源点 $k$ 到其他点 $x$ 的最短距离,再从所有最短路中取 $max$ 即是「**从 $k$ 点出发,到其他点 $x$ 的最短距离的最大值**」。

朴素 Dijkstra 复杂度为 $O(n^2)$,可以过。

代码:
```Java
class Solution {
    int N = 110, M = 6010;
    // 邻接矩阵数组:w[a][b] = c 代表从 a 到 b 有权重为 c 的边
    int[][] w = new int[N][N];
    // dist[x] = y 代表从「源点/起点」到 x 的最短距离为 y
    int[] dist = new int[N];
    // 记录哪些点已经被更新过
    boolean[] vis = new boolean[N];
    int INF = 0x3f3f3f3f;
    int n, k;
    public int networkDelayTime(int[][] ts, int _n, int _k) {
        n = _n; k = _k;
        // 初始化邻接矩阵
        for (int i = 1; i <= n; i++) {
            for (int j = 1; j <= n; j++) {
                w[i][j] = w[j][i] = i == j ? 0 : INF;
            }
        }
        // 存图
        for (int[] t : ts) {
            int u = t[0], v = t[1], c = t[2];
            w[u][v] = c;
        }
        // 最短路
        dijkstra();
        // 遍历答案
        int ans = 0;
        for (int i = 1; i <= n; i++) {
            ans = Math.max(ans, dist[i]);
        }
        return ans > INF / 2 ? -1 : ans;
    }
    void dijkstra() {
        // 起始先将所有的点标记为「未更新」和「距离为正无穷」
        Arrays.fill(vis, false);
        Arrays.fill(dist, INF);
        // 只有起点最短距离为 0
        dist[k] = 0;
        // 迭代 n 次
        for (int p = 1; p <= n; p++) {
            // 每次找到「最短距离最小」且「未被更新」的点 t
            int t = -1;
            for (int i = 1; i <= n; i++) {
                if (!vis[i] && (t == -1 || dist[i] < dist[t])) t = i;
            }
            // 标记点 t 为已更新
            vis[t] = true;
            // 用点 t 的「最小距离」更新其他点
            for (int i = 1; i <= n; i++) {
                dist[i] = Math.min(dist[i], dist[t] + w[t][i]);
            }
        }
    }
}
```
* 时间复杂度:$O(n^2)$
* 空间复杂度:$O(n^2)$

---

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

由于边数据范围不算大,我们还可以使用复杂度为 $O(m\log{n})$ 的堆优化 Dijkstra 算法进行求解。

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

跑一遍堆优化 Dijkstra 算法求最短路,再从所有最短路中取 $max$ 即是「**从 $k$ 点出发,到其他点 $x$ 的最短距离的最大值**」。

此时算法复杂度为 $O(m\log{n})$,可以过。

代码:
```Java
class Solution {
    int N = 110, M = 6010;
    // 邻接表
    int[] he = new int[N], e = new int[M], ne = new int[M], w = new int[M];
    // dist[x] = y 代表从「源点/起点」到 x 的最短距离为 y
    int[] dist = new int[N];
    // 记录哪些点已经被更新过
    boolean[] vis = new boolean[N];
    int n, k, idx;
    int INF = 0x3f3f3f3f;
    void add(int a, int b, int c) {
        e[idx] = b;
        ne[idx] = he[a];
        he[a] = idx;
        w[idx] = c;
        idx++;
    }
    public int networkDelayTime(int[][] ts, int _n, int _k) {
        n = _n; k = _k;
        // 初始化链表头
        Arrays.fill(he, -1);
        // 存图
        for (int[] t : ts) {
            int u = t[0], v = t[1], c = t[2];
            add(u, v, c);
        }
        // 最短路
        dijkstra();
        // 遍历答案
        int ans = 0;
        for (int i = 1; i <= n; i++) {
            ans = Math.max(ans, dist[i]);
        }
        return ans > INF / 2 ? -1 : ans;
    }
    void dijkstra() {
        // 起始先将所有的点标记为「未更新」和「距离为正无穷」
        Arrays.fill(vis, false);
        Arrays.fill(dist, INF);
        // 只有起点最短距离为 0
        dist[k] = 0;
        // 使用「优先队列」存储所有可用于更新的点
        // 以 (点编号, 到起点的距离) 进行存储,优先弹出「最短距离」较小的点
        PriorityQueue<int[]> q = new PriorityQueue<>((a,b)->a[1]-b[1]);
        q.add(new int[]{k, 0});
        while (!q.isEmpty()) {
            // 每次从「优先队列」中弹出
            int[] poll = q.poll();
            int id = poll[0], step = poll[1];
            // 如果弹出的点被标记「已更新」,则跳过
            if (vis[id]) continue;
            // 标记该点「已更新」,并使用该点更新其他点的「最短距离」
            vis[id] = true;
            for (int i = he[id]; i != -1; i = ne[i]) {
                int j = e[i];
                if (dist[j] > dist[id] + w[i]) {
                    dist[j] = dist[id] + w[i];
                    q.add(new int[]{j, dist[j]});
                }
            }
        }
    }
}
```
* 时间复杂度:$O(m\log{n} + n)$
* 空间复杂度:$O(m)$

---

### Bellman Ford(类 & 邻接表)

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

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

由于本题边的数量级大于点的数量级,因此也能够继续使用「邻接表」的方式进行边的遍历,遍历所有边的复杂度的下界为 $O(n)$,上界可以确保不超过 $O(m)$(代码见 $P2$)。

代码:
```Java
class Solution {
    class Edge {
        int a, b, c;
        Edge(int _a, int _b, int _c) {
            a = _a; b = _b; c = _c;
        }
    }
    int N = 110, M = 6010;
    // dist[x] = y 代表从「源点/起点」到 x 的最短距离为 y
    int[] dist = new int[N];
    int INF = 0x3f3f3f3f;
    int n, m, k;
    // 使用类进行存边
    List<Edge> es = new ArrayList<>();
    public int networkDelayTime(int[][] ts, int _n, int _k) {
        n = _n; k = _k;
        m = ts.length;
        // 存图
        for (int[] t : ts) {
            int u = t[0], v = t[1], c = t[2];
            es.add(new Edge(u, v, c));
        }
        // 最短路
        bf();
        // 遍历答案
        int ans = 0;
        for (int i = 1; i <= n; i++) {
            ans = Math.max(ans, dist[i]);
        }
        return ans > INF / 2 ? -1 : ans;
    }
    void bf() {
        // 起始先将所有的点标记为「距离为正无穷」
        Arrays.fill(dist, INF);
        // 只有起点最短距离为 0
        dist[k] = 0;
        // 迭代 n 次
        for (int p = 1; p <= n; p++) {
            int[] prev = dist.clone();
            // 每次都使用上一次迭代的结果,执行松弛操作
            for (Edge e : es) {
                int a = e.a, b = e.b, c = e.c;
                dist[b] = Math.min(dist[b], prev[a] + c);
            }
        }
    }
}
```
```Java
class Solution {
    int N = 110, M = 6010;
    // 邻接表
    int[] he = new int[N], e = new int[M], ne = new int[M], w = new int[M];
    // dist[x] = y 代表从「源点/起点」到 x 的最短距离为 y
    int[] dist = new int[N];
    int INF = 0x3f3f3f3f;
    int n, m, k, idx;
    void add(int a, int b, int c) {
        e[idx] = b;
        ne[idx] = he[a];
        he[a] = idx;
        w[idx] = c;
        idx++;
    }
    public int networkDelayTime(int[][] ts, int _n, int _k) {
        n = _n; k = _k;
        m = ts.length;
        // 初始化链表头
        Arrays.fill(he, -1);
        // 存图
        for (int[] t : ts) {
            int u = t[0], v = t[1], c = t[2];
            add(u, v, c);
        }
        // 最短路
        bf();
        // 遍历答案
        int ans = 0;
        for (int i = 1; i <= n; i++) {
            ans = Math.max(ans, dist[i]);
        }
        return ans > INF / 2 ? -1 : ans;
    }
    void bf() {
        // 起始先将所有的点标记为「距离为正无穷」
        Arrays.fill(dist, INF);
        // 只有起点最短距离为 0
        dist[k] = 0;
        // 迭代 n 次
        for (int p = 1; p <= n; p++) {
            int[] prev = dist.clone();
            // 每次都使用上一次迭代的结果,执行松弛操作
            for (int a = 1; a <= n; a++) {
                for (int i = he[a]; i != -1; i = ne[i]) {
                    int b = e[i];
                    dist[b] = Math.min(dist[b], prev[a] + w[i]);
                }
            }
        }
    }
}
```
* 时间复杂度:$O(n \times m)$
* 空间复杂度:$O(m)$

---

### SPFA(邻接表)

SPFA 是对 Bellman Ford 的优化实现,可以使用队列进行优化,也可以使用栈进行优化。

通常情况下复杂度为 $O(k*m)$,$k$ 一般为 $4$ 到 $5$,最坏情况下仍为 $O(n * m)$,当数据为网格图时,复杂度会从 $O(k*m)$ 退化为 $O(n*m)$。

代码:
```Java
class Solution {
    int N = 110, M = 6010;
    // 邻接表
    int[] he = new int[N], e = new int[M], ne = new int[M], w = new int[M];
    // dist[x] = y 代表从「源点/起点」到 x 的最短距离为 y
    int[] dist = new int[N];
    // 记录哪一个点「已在队列」中
    boolean[] vis = new boolean[N];
    int INF = 0x3f3f3f3f;
    int n, k, idx;
    void add(int a, int b, int c) {
        e[idx] = b;
        ne[idx] = he[a];
        he[a] = idx;
        w[idx] = c;
        idx++;
    }
    public int networkDelayTime(int[][] ts, int _n, int _k) {
        n = _n; k = _k;
        // 初始化链表头
        Arrays.fill(he, -1);
        // 存图
        for (int[] t : ts) {
            int u = t[0], v = t[1], c = t[2];
            add(u, v, c);
        }
        // 最短路
        spfa();
        // 遍历答案
        int ans = 0;
        for (int i = 1; i <= n; i++) {
            ans = Math.max(ans, dist[i]);
        }
        return ans > INF / 2 ? -1 : ans;
    }
    void spfa() {
        // 起始先将所有的点标记为「未入队」和「距离为正无穷」
        Arrays.fill(vis, false);
        Arrays.fill(dist, INF);
        // 只有起点最短距离为 0
        dist[k] = 0;
        // 使用「双端队列」存储,存储的是点编号
        Deque<Integer> d = new ArrayDeque<>();
        // 将「源点/起点」进行入队,并标记「已入队」
        d.addLast(k);
        vis[k] = true;
        while (!d.isEmpty()) {
            // 每次从「双端队列」中取出,并标记「未入队」
            int poll = d.pollFirst();
            vis[poll] = false;
            // 尝试使用该点,更新其他点的最短距离
            // 如果更新的点,本身「未入队」则加入队列中,并标记「已入队」
            for (int i = he[poll]; i != -1; i = ne[i]) {
                int j = e[i];
                if (dist[j] > dist[poll] + w[i]) {
                    dist[j] = dist[poll] + w[i];
                    if (vis[j]) continue;
                    d.addLast(j);
                    vis[j] = true;
                }
            }
        }
    }
}
```
* 时间复杂度:$O(n \times m)$
* 空间复杂度:$O(m)$

### 最后

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

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

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

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


LeetCode/741-750/744. 寻找比目标字母大的最小字母（简单）.md

744. 寻找比目标字母大的最小字母

Question:
给你一个排序后的字符列表 `letters`,列表中只包含小写英文字母。另给出一个目标字母 `target`,请你寻找在这一有序列表里比目标字母大的最小字母。

在比较时,字母是依序循环出现的。举个例子:

如果目标字母 `target = 'z'` 并且字符列表为 `letters = ['a', 'b']`,则答案返回 `'a'`

示例 1:
```
输入: letters = ["c", "f", "j"],target = "a"

输出: "c"
```
示例 2:
```
输入: letters = ["c","f","j"], target = "c"

输出: "f"
```
示例 3:
```
输入: letters = ["c","f","j"], target = "d"

输出: "f"
```

提示:
* $2 <= letters.length <= 10^4$
* $letters[i]$ 是一个小写字母
* $letters$ 按非递减顺序排序
* $letters$ 最少包含两个不同的字母
* $target$ 是一个小写字母

Solution:
### 二分

给定的数组「有序」,找到比 $target$ 大的最小字母,容易想到二分。

唯一需要注意的是,二分结束后需要再次 `check`,如果不满足,则取数组首位元素。

代码:
```Java
class Solution {
    public char nextGreatestLetter(char[] letters, char target) {
        int n = letters.length;
        int l = 0, r = n - 1;
        while (l < r) {
            int mid = l + r >> 1;
            if (letters[mid] > target) r = mid;
            else l = mid + 1;
        }
        return letters[r] > target ? letters[r] : letters[0];
    }
}
```
* 时间复杂度:$O(\log{n})$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


745. 前缀和后缀搜索

Question:
设计一个包含一些单词的特殊词典,并能够通过前缀和后缀来检索单词。

实现 `WordFilter` 类:

* `WordFilter(string[] words)` 使用词典中的单词 `words` 初始化对象。
* `f(string pref, string suff)` 返回词典中具有前缀 `prefix` 和后缀 `suff` 的单词的下标。如果存在不止一个满足要求的下标,返回其中 最大的下标 。如果不存在这样的单词,返回 $-1$ 。

示例:
```
输入
["WordFilter", "f"]
[[["apple"]], ["a", "e"]]

输出
[null, 0]

解释
WordFilter wordFilter = new WordFilter(["apple"]);
wordFilter.f("a", "e"); // 返回 0 ,因为下标为 0 的单词:前缀 prefix = "a" 且 后缀 suff = "e" 。
```
提示:
* $1 <= words.length <= 10^4$
* $1 <= words[i].length <= 7$
* $1 <= pref.length, suff.length <= 7$
* `words[i]`、`pref` 和 `suff` 仅由小写英文字母组成
* 最多对函数 `f` 执行 $10^4$ 次调用

Solution:
### 基本分析

为了方便,我们令 `words` 为 `ss`,令 `pref` 和 `suff` 分别为 `a` 和 `b`。

搜索某个前缀(后缀可看做是反方向的前缀)容易想到字典树,但单词长度数据范围只有 $7$,十分具有迷惑性,使用暴力做法最坏情况下会扫描所有的 $ss[i]$,不考虑任何的剪枝操作的话,计算量也才为 $2 \times \ 7 \times 1e4 = 1.4 \times 10^5$,按道理是完全可以过的。

但不要忘记 LC 是一个具有「设定每个样例时长,同时又有总时长」这样奇怪机制的 OJ。

---

### 暴力(TLE or 双百)

于是有了 `Java` 总时间超时,`TypeScripe` 双百的结果(应该是 `TypeScript` 提交不多,同时设限宽松的原因):

Java 代码:
```Java
class WordFilter {
    String[] ss;
    public WordFilter(String[] words) {
        ss = words;
    }
    public int f(String a, String b) {
        int n = a.length(), m = b.length();
        for (int k = ss.length - 1; k >= 0; k--) {
            String cur = ss[k];
            int len = cur.length();
            if (len < n || len < m) continue;
            boolean ok = true;
            for (int i = 0; i < n && ok; i++) {
                if (cur.charAt(i) != a.charAt(i)) ok = false;
            }
            for (int i = 0; i < m && ok; i++) {
                if (cur.charAt(len - 1 - i) != b.charAt(m - 1 - i)) ok = false;
            }
            if (ok) return k;
        }
        return -1;
    }
}
```
TypeScript 代码:
```TypeScript
class WordFilter {
    ss: string[]
    constructor(words: string[]) {
        this.ss = words
    }
    f(a: string, b: string): number {
        const n = a.length, m = b.length
        for (let k = this.ss.length - 1; k >= 0; k--) {
            const cur = this.ss[k]
            const len = cur.length
            if (len < n || len < m) continue
            let ok = true
            for (let i = 0; i < n && ok; i++) {
                if (cur[i] != a[i]) ok = false
            }
            for (let i = m - 1; i >= 0; i--) {
                if (cur[len - 1 - i] != b[m - 1 - i]) ok = false
            }
            if (ok) return k
        }
        return -1
    }
}
```
* 时间复杂度:初始化操作复杂度为 $O(1)$,检索操作复杂度为 $O(\sum_{i = 0}^{n - 1} ss[i].length)$
* 空间复杂度:$O(\sum_{i = 0}^{n - 1} ss[i].length)$

---

### Trie

使用字典树优化检索过程也是容易的,分别使用两棵 `Trie` 树来记录 $ss[i]$ 的前后缀,即正着存到 `tr1` 中,反着存到 `Tr2` 中。

> 还不了解 `Trie` 的同学可以先看前置 🧀:[【设计数据结构】实现 Trie (前缀树)](https://mp.weixin.qq.com/s?__biz=MzU4NDE3MTEyMA==&mid=2247488490&idx=1&sn=db2998cb0e5f08684ee1b6009b974089)
前置 🧀 通过图解形式讲解了 `Trie` 的结构与原理,以及提供了两种实现 `Trie` 的方式

同时对于字典树的每个节点,我们使用数组 `idxs` 记录经过该节点的字符串 $ss[i]$ 所在 `ss` 中的下标 $i$,若某个字典树节点的索引数组 `tr.idxs` 为 $[a_1, a_2, ..., a_k]$ 则代表「从根节点到 `tr` 节点所对应的字符串」为 $ss[a_1], ss[a_2], ..., ss[a_k]$ 的前缀。

这样我们可以即可在扫描前后缀 `a` 和 `b` 时,得到对应的候选下标列表 `l1` 和 `l2`,由于我们将 $ss[i]$ 添加到两棵 `tr` 中是按照下标「从小到大」进行,因此我们使用「双指针」算法分别从 `l1` 和 `l2` 结尾往后找到第一个共同元素即是答案(满足条件的最大下标)。

> 使用 `Trie` 优化后,`Java` 从 `TLE` 到 `AC`,`TypeScript` 耗时为原本的 $\frac{1}{7}$ :

Java 代码:
```Java
class WordFilter {
    class TrieNode {
        TrieNode[] tns = new TrieNode[26];
        List<Integer> idxs = new ArrayList<>();
    }
    void add(TrieNode p, String s, int idx, boolean isTurn) {
        int n = s.length();
        p.idxs.add(idx);
        for (int i = isTurn ? n - 1 : 0; i >= 0 && i < n; i += isTurn ? -1 : 1) {
            int u = s.charAt(i) - 'a';
            if (p.tns[u] == null) p.tns[u] = new TrieNode();
            p = p.tns[u];
            p.idxs.add(idx);
        }
    }
    int query(String a, String b) {
        int n = a.length(), m = b.length();
        TrieNode p = tr1;
        for (int i = 0; i < n; i++) {
            int u = a.charAt(i) - 'a';
            if (p.tns[u] == null) return -1;
            p = p.tns[u];
        }
        List<Integer> l1 = p.idxs;
        p = tr2;
        for (int i = m - 1; i >= 0; i--) {
            int u = b.charAt(i) - 'a';
            if (p.tns[u] == null) return -1;
            p = p.tns[u];
        }
        List<Integer> l2 = p.idxs;
        n = l1.size(); m = l2.size();
        for (int i = n - 1, j = m - 1; i >= 0 && j >= 0; ) {
            if (l1.get(i) > l2.get(j)) i--;
            else if (l1.get(i) < l2.get(j)) j--;
            else return l1.get(i);
        }
        return -1;
    }
    TrieNode tr1 = new TrieNode(), tr2 = new TrieNode();
    public WordFilter(String[] ss) {
        int n = ss.length;
        for (int i = 0; i < n; i++) {
            add(tr1, ss[i], i, false);
            add(tr2, ss[i], i, true);
        }
    }
    public int f(String a, String b) {
        return query(a, b);
    }
}
```
TypeScript 代码:
```TypeScript
class TrieNode {
    tns: TrieNode[] = new Array<TrieNode>()
    idxs: number[] = new Array<number>()
}
class WordFilter {
    add(p: TrieNode, s: string, idx: number, isTurn: boolean): void {
        const n = s.length
        p.idxs.push(idx)
        for (let i = isTurn ? n - 1 : 0; i >= 0 && i < n; i += isTurn ? -1 : 1) {
            const u = s.charCodeAt(i) - 'a'.charCodeAt(0)
            if (p.tns[u] == null) p.tns[u] = new TrieNode()
            p = p.tns[u]
            p.idxs.push(idx)
        }
    }
    query(a: string, b: string): number {
        let n = a.length, m = b.length
        let p = this.tr1
        for (let i = 0; i < n; i++) {
            const u = a.charCodeAt(i) - 'a'.charCodeAt(0)
            if (p.tns[u] == null) return -1
            p = p.tns[u]
        }
        const l1 = p.idxs
        p = this.tr2
        for (let i = m - 1; i >= 0; i--) {
            const u = b.charCodeAt(i) - 'a'.charCodeAt(0)
            if (p.tns[u] == null) return -1
            p = p.tns[u]
        }
        const l2 = p.idxs
        n = l1.length; m = l2.length
        for (let i = n - 1, j = m - 1; i >= 0 && j >= 0; ) {
            if (l1[i] < l2[j]) j--
            else if (l1[i] > l2[j]) i--
            else return l1[i]
        }
        return -1
    }
    tr1: TrieNode = new TrieNode()
    tr2: TrieNode = new TrieNode()
    constructor(ss: string[]) {
        for (let i = 0; i < ss.length; i++) {
            this.add(this.tr1, ss[i], i, false)
            this.add(this.tr2, ss[i], i, true)
        }
    }
    f(a: string, b: string): number {
        return this.query(a, b)
    }
}
```
C++ 代码:
```C++
class WordFilter {
public:
    struct TrieNode {
        TrieNode* tns[26] {nullptr};
        vector<int> idxs;
    };

    void add(TrieNode* p, const string& s, int idx, bool isTurn) {
        int n = s.size();
        p->idxs.push_back(idx);
        for(int i = isTurn ? n - 1 : 0; i >= 0 && i < n; i += isTurn ? -1 : 1) {
            int u = s[i] - 'a';
            if(p->tns[u] == nullptr) p->tns[u] = new TrieNode();
            p = p->tns[u];
            p->idxs.push_back(idx);
        }
    }

    int query(const string& a, const string& b) {
        int n = a.size(), m = b.size();
        auto p = tr1;
        for(int i = 0; i < n; i++) {
            int u = a[i] - 'a';
            if(p->tns[u] == nullptr) return -1;
            p = p->tns[u];
        }
        vector<int>& l1 = p->idxs;
        p = tr2;
        for(int i = m - 1; i >= 0; i--) {
            int u = b[i] - 'a';
            if(p->tns[u] == nullptr) return -1;
            p = p->tns[u];
        }
        vector<int>& l2 = p->idxs;
        n = l1.size(), m = l2.size();
        for(int i = n - 1, j = m - 1; i >= 0 && j >= 0; ) {
            if(l1[i] > l2[j]) i--;
            else if(l1[i] < l2[j]) j--;
            else return l1[i];
        }
        return -1;
    }

    TrieNode* tr1 = new TrieNode, *tr2 = new TrieNode;
    WordFilter(vector<string>& ss) {
        int n = ss.size();
        for(int i = 0; i < n; i++) {
            add(tr1, ss[i], i, false);
            add(tr2, ss[i], i, true);
        }
    }

    int f(string a, string b) {
        return query(a, b);
    }
};
```
* 时间复杂度:初始化操作复杂度为 $O(\sum_{i = 0}^{n - 1} ss[i].length)$,检索过程复杂度为 $O(a + b + n)$,其中 $a = b = 7$ 为前后缀的最大长度,$n = 1e4$ 为初始化数组长度,代表最多有 $n$ 个候选下标(注意:这里的 $n$ 只是粗略分析,实际上如果候选集长度越大的话,说明两个候选集是重合度是越高的,从后往前找的过程是越快结束的,可以通过方程算出一个双指针的理论最大比较次数 $k$,如果要将 $n$ 卡满成 $1e4$ 的话,需要将两个候选集设计成交替下标,此时 `f` 如果仍是 $1e4$ 次调用的话,必然会面临大量的重复查询,可通过引入 `map` 记录查询次数来进行优化)
* 空间复杂度:$O(\sum_{i = 0}^{n - 1} ss[i].length)$

### 最后

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

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

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

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


LeetCode/741-750/747. 至少是其他数字两倍的最大数（简单）.md

747. 至少是其他数字两倍的最大数

Question:
给你一个整数数组 $nums$ ,其中总是存在 **唯一的** 一个最大整数 。

请你找出数组中的最大元素并检查它是否 至少是数组中每个其他数字的两倍 。

如果是,则返回 **最大元素的下标** ,否则返回 $-1$ 。

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

输出:1

解释:6 是最大的整数,对于数组中的其他整数,6 大于数组中其他元素的两倍。6 的下标是 1 ,所以返回 1 。
```

示例 2:
```
输入:nums = [1,2,3,4]
输出:-1
解释:4 没有超过 3 的两倍大,所以返回 -1 。
```
示例 3:
```
输入:nums = [1]

输出:0

解释:因为不存在其他数字,所以认为现有数字 1 至少是其他数字的两倍。
```

提示:
* $1 <= nums.length <= 50$
* $0 <= nums[i] <= 100$
* $nums$ 中的最大元素是唯一的

Solution:
### 模拟

根据题意进行模拟即可,遍历过程中维护最大值和次大值的下标,最后判断最大值是否至少为次大值两倍。

然后?今天属于圆梦了?(这真的只是他们的日常可爱 🤣

**代码(感谢 [@5cm/s 🌸](/u/megurine/)、[@Benhao](/u/himymben/) 和 [@Qian](/u/qian2/) 几位总提供的其他语言版本 🤣 🤣 ):**
```Java
class Solution {
    public int dominantIndex(int[] nums) {
        int n = nums.length;
        if (n == 1) return 0;
        int a = -1, b = 0;
        for (int i = 1; i < n; i++) {
            if (nums[i] > nums[b]) {
                a = b; b = i;
            } else if (a == -1 || nums[i] > nums[a]) {
                a = i;
            }
        }
        return nums[b] >= nums[a] * 2 ? b : -1;
    }
}
```
-
```Python
class Solution(object):
    def dominantIndex(self, nums):
        n = len(nums)
        if n == 1:
            return 0
        a, b = -1, 0
        for i in range(1, n):
            if nums[i] > nums[b]:
                a, b = b, i
            elif a == -1 or nums[i] > nums[a]:
                a = i
        return b if nums[b] >= nums[a] * 2 else -1
```
-
```Python
class Solution:
    def dominantIndex(self, nums: List[int]) -> int:
        n = len(nums)
        if n == 1:
            return 0
        a, b = -1, 0
        for i in range(1, n):
            if nums[i] > nums[b]:
                a, b = b, i
            elif a == -1 or nums[i] > nums[a]:
                a = i
        return b if nums[b] >= nums[a] * 2 else -1
```
-
```Go
func dominantIndex(nums []int) int {
    n := len(nums)
    if n == 1{
        return 0
    }
    a, b := -1, 0
    for i := 1; i < n; i++ {
        if nums[i] > nums[b] {
            a, b = b, i
        } else if a == -1 || nums[i] > nums[a] {
            a = i
        }
    }
    if nums[b] >= nums[a] * 2{
        return b
    }
    return -1
}
```
-
```Rust
impl Solution {
    pub fn dominant_index(nums: Vec<i32>) -> i32 {
        let mut mx = 0;
        let mut ans = -1 as i32;
        for i in 0..nums.len() {
            if nums[i] >= mx * 2 {
                ans = i as i32;
            }
            else if nums[i] * 2 > mx {
                ans = -1 as i32;
            }
            if nums[i] > mx {
                mx = nums[i];
            }
        }
        ans
    }
}
```
-
```C
int dominantIndex(int* nums, int numsSize){
    if(numsSize == 1) return 0;
    int a = -1, b = 0;
    for(int i = 1; i < numsSize; i++){
        if (nums[i] > nums[b]) {
            a = b; b = i;
        } else if (a == -1 || nums[i] > nums[a]) {
            a = i;
        }
    }
    return nums[b] >= nums[a] * 2 ? b : -1;
}
```
-
```C#
public class Solution {
    public int DominantIndex(int[] nums) {
        int n = nums.Length;
        if (n == 1) return 0;
        int a = -1, b = 0;
        for (int i = 1; i < n; i++) {
            if (nums[i] > nums[b]) {
                a = b; b = i;
            } else if (a == -1 || nums[i] > nums[a]) {
                a = i;
            }
        }
        return nums[b] >= nums[a] * 2 ? b : -1;
    }
}
```
-
```C++
class Solution {
public:
    int dominantIndex(vector<int>& nums) {
        int mx = 0, ans = -1;
        for (int i = 0; i < nums.size(); ++i) {
            if (nums[i] >= mx * 2) ans = i;
            else if (nums[i] * 2 > mx) ans = -1;
            mx = max(nums[i], mx);
        }
        return ans;
    }
};
```
-
```JS
var dominantIndex = function(nums) {
    const n = nums.length
    if(n == 1) return 0
    let a = -1, b = 0
    for (let i = 1; i < n; i++){
        if (nums[i] > nums[b]) {
            a = b; b = i;
        } else if (a == -1 || nums[i] > nums[a]) {
            a = i;
        }
    }
    return nums[b] >= nums[a] * 2 ? b : -1;
};
```
-
```TS
function dominantIndex(nums: number[]): number {
    const n = nums.length
    if(n == 1) return 0
    let a = -1, b = 0
    for (let i = 1; i < n; i++){
        if (nums[i] > nums[b]) {
            a = b; b = i;
        } else if (a == -1 || nums[i] > nums[a]) {
            a = i;
        }
    }
    return nums[b] >= nums[a] * 2 ? b : -1;
};
```
-
```PHP
class Solution {
    function dominantIndex($nums) {
        $n = sizeof($nums);
        if ($n == 1) return 0;
        $a = -1; $b = 0;
        for ($i = 1; $i < $n; $i++) {
            if ($nums[$i] > $nums[$b]) {
                $a = $b;
                $b = $i;
            } else if ($a == -1 || $nums[$i] > $nums[$a]) {
                $a = $i;
            }
        }
        if($nums[$b] >= $nums[$a] * 2){
            return $b;
        }
        return -1;
    }
}
```
-
```Swift
class Solution {
    func dominantIndex(_ nums: [Int]) -> Int {
        var n = nums.count
        if n == 1 {
            return 0
        }
        var (a, b) = (-1, 0)
        for i in 1..<nums.count {
            if nums[i] > nums[b] {
                (a, b) = (b, i)
            }
            else if a == -1 || nums[i] > nums[a] {
                a = i
            }
        }
        return nums[b] >= nums[a] * 2 ? b : -1
    }
}
```
-
```Kotlin
class Solution {
    fun dominantIndex(nums: IntArray): Int {
        var mx = 0
        var ans = -1
        for (i in nums.indices) {
            when {
                nums[i] >= mx * 2 -> ans = i
                nums[i] * 2 > mx -> ans = -1
            }
            if (nums[i] > mx) mx = nums[i]
        }
        return ans
    }
}
```
-
```Scala
object Solution {
    def dominantIndex(nums: Array[Int]): Int = {
        var mx = 0
        var ans = -1
        for (i <- 0 to (nums.length - 1)) {
            if (nums(i) >= mx * 2) ans = i
            else if (nums(i) * 2 > mx) ans = -1
            if (nums(i) > mx) mx = nums(i)
        }
        return ans
    }
}
```
-
```Ruby
def dominant_index(nums)
    n = nums.length
    return 0 if n == 1
    a = -1
    b = 0
    nums.each_with_index do |num, i|
        next if i == 0
        if nums[i] > nums[b]
            a = b
            b = i
        elsif a.eql?(-1) or nums[i] > nums[a]
            a = i
        end
    end
    return nums[b] >= nums[a] * 2 ? b : -1
end
```
-
```lisp
(define/contract (dominant-index nums)
  (-> (listof exact-integer?) exact-integer?)
  (let loop ([nums nums] [i 0] [mx 0] [ans -1])
    (cond
      [(empty? nums) ans]
      [else
        (define x (car nums))
        (define mxx (max x mx))
        (cond
          [(>= x (* mx 2)) (loop (cdr nums) (+ i 1) mxx i)]
          [(> (* x 2) mx) (loop (cdr nums) (+ i 1) mxx -1)]
          [else (loop (cdr nums) (+ i 1) mxx ans)]
        )
      ]
    )
  )
)
```
-

```Elixir
defmodule Solution do
  @spec dominant_index(nums :: [integer]) :: integer
  def dominant_index(nums) do
    solve(nums, 0, -1, 0)
  end
  def solve([], mx, ans, cur) do
    ans
  end
  def solve([x | rest], mx, ans, cur) when x >= mx * 2 do
    solve(rest, max(x, mx), cur, cur + 1)
  end
  def solve([x | rest], mx, ans, cur) when x * 2 > mx do
    solve(rest, max(x, mx), -1, cur + 1)
  end
  def solve([x | rest], mx, ans, cur) do
    solve(rest, max(x, mx), ans, cur + 1)
  end
end
```
-
```Erlang
-spec dominant_index(Nums :: [integer()]) -> integer().
dominant_index(Nums) ->
  solve(Nums).

solve(List) ->
  solve(List, 0, -1, 0).
solve([], Max, Ans, Cur) ->
  Ans;
solve([Head|Rest], Max, Ans, Cur) when Head >= Max * 2 ->
  solve(Rest, max(Max, Head), Cur, Cur + 1);
solve([Head|Rest], Max, Ans, Cur) when Head * 2 > Max ->
  solve(Rest, max(Max, Head), -1, Cur + 1);
solve([Head|Rest], Max, Ans, Cur) ->
  solve(Rest, max(Max, Head), Ans, Cur + 1).

max(A, B) when A > B ->
  A;
max(A, B) ->
  B.
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(1)$

---

### 全鱼宴(是全语言 这错别字还挺可爱 🤣

经过三位总 [@5cm/s 🌸](/u/megurine/)、[@Benhao](/u/himymben/) 和 [@Qian](/u/qian2/) 的不懈努力,通过举一反三、连蒙带猜,把所有语言弄出了 🤣

这个过程中也有幸见到,如果一个语言只有一份代码,居然界面是这样的(没有时间和内存的分布图:

我和我的小伙伴纷纷表示这样的 Flag 再也不敢了 🤣

### 最后

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

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

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

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


Question:
给你一个字符串 $licensePlate$ 和一个字符串数组 $words$ ,请你找出并返回 $words$ 中的 最短补全词 。

补全词 是一个包含 $licensePlate$ 中所有的字母的单词。在所有补全词中,最短的那个就是 最短补全词 。

在匹配 $licensePlate$ 中的字母时:
* 忽略 $licensePlate$ 中的 数字和空格 。
* 不区分大小写。
* 如果某个字母在 $licensePlate$ 中出现不止一次,那么该字母在补全词中的出现次数应当一致或者更多。

例如:`licensePlate = "aBc 12c"`,那么它的补全词应当包含字母 `'a'`、`'b'` (忽略大写)和两个 `'c'` 。可能的 补全词 有 `"abccdef"`、`"caaacab"` 以及 `"cbca"`。

请你找出并返回 $words$ 中的 最短补全词 。题目数据保证一定存在一个最短补全词。当有多个单词都符合最短补全词的匹配条件时取 $words$ 中 最靠前的 那个。

示例 1:
```
输入:licensePlate = "1s3 PSt", words = ["step", "steps", "stripe", "stepple"]

输出:"steps"

解释:最短补全词应该包括 "s"、"p"、"s"(忽略大小写) 以及 "t"。
"step" 包含 "t"、"p",但只包含一个 "s",所以它不符合条件。
"steps" 包含 "t"、"p" 和两个 "s"。
"stripe" 缺一个 "s"。
"stepple" 缺一个 "s"。
因此,"steps" 是唯一一个包含所有字母的单词,也是本例的答案。
```
示例 2:
```
输入:licensePlate = "1s3 456", words = ["looks", "pest", "stew", "show"]

输出:"pest"

解释:licensePlate 只包含字母 "s" 。所有的单词都包含字母 "s" ,其中 "pest"、"stew"、和 "show" 三者最短。答案是 "pest" ,因为它是三个单词中在 words 里最靠前的那个。
```
示例 3:
```
输入:licensePlate = "Ah71752", words = ["suggest","letter","of","husband","easy","education","drug","prevent","writer","old"]

输出:"husband"
```
示例 4:
```
输入:licensePlate = "OgEu755", words = ["enough","these","play","wide","wonder","box","arrive","money","tax","thus"]

输出:"enough"
```
示例 5:
```
输入:licensePlate = "iMSlpe4", words = ["claim","consumer","student","camera","public","never","wonder","simple","thought","use"]

输出:"simple"
```

提示:
* $1 <= licensePlate.length <= 7$
* $licensePlate$ 由数字、大小写字母或空格 ' ' 组成
* $1 <= words.length <= 1000$
* $1 <= words[i].length <= 15$
* $words[i]$ 由小写英文字母组成

Solution:
### 模拟

根据题意,先对 $licensePlate$ 进行词频统计(忽略空格和数字),然后遍历 $words$ 进行词频统计,从所有符合要求的 $words[i]$ 找到最短的补全词。

代码:
```Java
class Solution {
    public String shortestCompletingWord(String licensePlate, String[] words) {
        int[] cnt = getCnt(licensePlate);
        String ans = null;
        for (String s : words) {
            int[] cur = getCnt(s);
            boolean ok = true;
            for (int i = 0; i < 26 && ok; i++) {
                if (cnt[i] > cur[i]) ok = false;
            }
            if (ok && (ans == null || ans.length() > s.length())) ans = s;
        }
        return ans;
    }
    int[] getCnt(String s) {
        int[] cnt = new int[26];
        for (char c : s.toCharArray()) {
            if (Character.isLetter(c)) cnt[Character.toLowerCase(c) - 'a']++;
        }
        return cnt;
    }
}
```
* 时间复杂度:令 $n$ 为字符串 $licensePlate$ 的长度,$m$ 为数组 $words$ 的长度,$L$ 为所有 $words[i]$ 的长度总和,字符集大小为 $C = 26$。整体复杂度为 $O(n + \sum_{i = 0}^{m - 1}{(C + words[i].length)})$
* 空间复杂度:偷懒使用 `toCharArray` 的话,复杂度和时间复杂度一样;仅使用 `charAt` 的话复杂度为 $O(C)$

### 最后

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

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

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

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


Question:
病毒扩散得很快,现在你的任务是尽可能地通过安装防火墙来隔离病毒。

假设世界由 $m \times n$ 的二维矩阵 `isInfected` 组成,`isInfected[i][j] == 0` 表示该区域未感染病毒,而  `isInfected[i][j] == 1` 表示该区域已感染病毒。可以在任意 $2$ 个相邻单元之间的共享边界上安装一个防火墙(并且只有一个防火墙)。

每天晚上,病毒会从被感染区域向相邻未感染区域扩散,除非被防火墙隔离。现由于资源有限,每天你只能安装一系列防火墙来隔离其中一个被病毒感染的区域(一个区域或连续的一片区域),且该感染区域对未感染区域的威胁最大且 保证唯一 。

你需要努力使得最后有部分区域不被病毒感染,如果可以成功,那么返回需要使用的防火墙个数; 如果无法实现,则返回在世界被病毒全部感染时已安装的防火墙个数。

示例 1:

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

输出: 10

解释:一共有两块被病毒感染的区域。
在第一天,添加 5 墙隔离病毒区域的左侧。病毒传播后的状态是:

第二天,在右侧添加 5 个墙来隔离病毒区域。此时病毒已经被完全控制住了。
```

示例 2:

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

输出: 4

解释: 虽然只保存了一个小区域,但却有四面墙。
注意,防火墙只建立在两个不同区域的共享边界上。
```
示例 3:
```
输入: isInfected = [[1,1,1,0,0,0,0,0,0],[1,0,1,0,1,1,1,1,1],[1,1,1,0,0,0,0,0,0]]

输出: 13

解释: 在隔离右边感染区域后,隔离左边病毒区域只需要 2 个防火墙。
```

提示:
* $m == isInfected.length$
* $n == isInfected[i].length$
* $1 <= m, n <= 50$
* `isInfected[i][j]` is either `0` or `1`
* 在整个描述的过程中,总有一个相邻的病毒区域,它将在下一轮 严格地感染更多未受污染的方块

Solution:
### 搜索模拟

根据题意,我们可以按天进行模拟,设计函数 `getCnt` 用于返回当天会被安装的防火墙数量,在 `getCnt` 内部我们会进行如下操作:

* 找出当天「对未感染区域的威胁最大」的区域,并将该区域进行隔离(将 $1$ 设置为 $-1$);
* 对其他区域,进行步长为 $1$ 的感染操作。

考虑如何实现 `getCnt`:我们需要以「连通块」为单位进行处理,因此每次的 `getCnt` 操作,我们先重建一个与矩阵等大的判重数组 `vis`,对于每个 $g[i][j] = 1$ 且未被 $vis[i][j]$ 标记为 `True` 的位置进行搜索,搜索过程使用 `BFS` 实现。

**在 `BFS` 过程中,我们除了统计该连通块所需要的防火墙数量 $b$ 以外,还需要额外记录当前连通块中 $1$ 的点集 `s1`(简称为原集,含义为连通块的格子集合),以及当前连通块相邻的 $0$ 的点集 `s2`(简称为扩充集,含义为将要被感染的格子集合)。**

根据题意,在单次的 `getCnt` 中,我们需要在所有连通块中取出其 `s2` 大小最大(对未感染区域的威胁最大)的连通块进行隔离操作,而其余连通块则进行扩充操作。

因此我们可以使用两个变量 `max` 和 `ans` 分别记录所有 `s2` 中的最大值,以及取得最大 `s2` 所对应连通块所需要的防火墙数量,同时需要使用两个数组 `l1` 和 `l2` 分别记录每个连通块对应的「原集」和「扩充集」,方便我们后续进行「隔离」和「感染」。

Java 代码:
```Java
class Solution {
    int[][] g;
    int n, m, ans;
    int[][] dirs = new int[][]{{1,0},{-1,0},{0,1},{0,-1}};
    boolean[][] vis;
    int search(int _x, int _y, Set<Integer> s1, Set<Integer> s2) {
        int ans = 0;
        Deque<int[]> d = new ArrayDeque<>();
        vis[_x][_y] = true;
        d.addLast(new int[]{_x, _y});
        s1.add(_x * m + _y);
        while (!d.isEmpty()) {
            int[] info = d.pollFirst();
            int x = info[0], y = info[1];
            for (int[] di : dirs) {
                int nx = x + di[0], ny = y + di[1], loc = nx * m + ny;
                if (nx < 0 || nx >= n || ny < 0 || ny >= m || vis[nx][ny]) continue;
                if (g[nx][ny] == 1) {
                    s1.add(loc);
                    vis[nx][ny] = true;
                    d.addLast(new int[]{nx, ny});
                } else if (g[nx][ny] == 0) {
                    s2.add(loc);
                    ans++;
                }
            }
        }
        return ans;
    }
    int getCnt() {
        vis = new boolean[n][m];
        int max = 0, ans = 0;
        // l1: 每个连通块的点集 s2: 每个连通块的候选感染点集
        List<Set<Integer>> l1 = new ArrayList<>(), l2 = new ArrayList<>();
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < m; j++) {
                if (g[i][j] == 1 && !vis[i][j]) {
                    // s1: 当前连通块的点集 s2: 当前连通块的候选感染点集
                    Set<Integer> s1 = new HashSet<>(), s2 = new HashSet<>();
                    int b = search(i, j, s1, s2), a = s2.size();
                    if (a > max) {
                        max = a; ans = b;
                    }
                    l1.add(s1); l2.add(s2);
                }
            }
        }
        for (int i = 0; i < l2.size(); i++) {
            for (int loc : l2.get(i).size() == max ? l1.get(i) : l2.get(i)) {
                int x = loc / m, y = loc % m;
                g[x][y] = l2.get(i).size() == max ? -1 : 1;
            }
        }
        return ans;
    }
    public int containVirus(int[][] _g) {
        g = _g;
        n = g.length; m = g[0].length;
        while (true) {
            int cnt = getCnt();
            if (cnt == 0) break;
            ans += cnt;
        }
        return ans;
    }
}
```
TypeScript 代码:
```TypeScript
let g: number[][] = null
let n: number = 0, m: number = 0
let vis: boolean[][] = null
const dirs: number[][] = [[1,0],[-1,0],[0,1],[0,-1]]
function search(_x: number, _y: number, s1: Set<number>, s2: Set<number>): number {
    let he = 0, ta = 0, ans = 0
    let d: Array<number> = new Array<number>()
    s1.add(_x * m + _y)
    vis[_x][_y] = true
    d[ta++] = _x * m + _y
    while (he < ta) {
        const poll = d[he++]
        const x = Math.floor(poll / m), y = poll % m
        for (const di of dirs) {
            const nx = x + di[0], ny = y + di[1], loc = nx * m + ny
            if (nx < 0 || nx >= n || ny < 0 || ny >= m || vis[nx][ny]) continue
            if (g[nx][ny] == 1) {
                s1.add(loc)
                vis[nx][ny] = true
                d[ta++] = loc
            } else if (g[nx][ny] == 0) {
                s2.add(loc)
                ans++
            }
        }
    }
    return ans
}
function getCnt(): number {
    vis = new Array<Array<boolean>>(n)
    for (let i = 0; i < n; i++) vis[i] = new Array<boolean>(m).fill(false)
    let max = 0, ans = 0
    let l1: Array<Set<number>> = new Array<Set<number>>(), l2: Array<Set<number>> = new Array<Set<number>>()
    for (let i = 0; i < n; i++) {
        for (let j = 0; j < m; j++) {
            if (g[i][j] == 1 && !vis[i][j]) {
                let s1 = new Set<number>(), s2 = new Set<number>()
                const b = search(i, j, s1, s2), a = s2.size
                if (a > max) {
                    max = a; ans = b
                }
                l1.push(s1); l2.push(s2)
            }
        }
    }
    for (let i = 0; i < l2.length; i++) {
        for (let loc of l2[i].size == max ? l1[i] : l2[i]) {
            const x = Math.floor(loc / m), y = loc % m
            g[x][y] = l2[i].size == max ? -1 : 1
        }
    }
    return ans
}
function containVirus(_g: number[][]): number {
    g = _g
    n = g.length; m = g[0].length
    let ans: number = 0
    while (true) {
        const cnt = getCnt()
        if (cnt == 0) break
        ans += cnt
    }
    return ans
};
```
* 时间复杂度:最多有 $n + m$ 天需要模拟,每天模拟复杂度 $O(n \times m)$,整体复杂度为 $O((n + m) \times nm)$
* 空间复杂度:$O(nm)$

### 最后

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

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

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

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


Question:
你有一个带有四个圆形拨轮的转盘锁。每个拨轮都有10个数字: '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' 。每个拨轮可以自由旋转:例如把 '9' 变为 '0','0' 变为 '9' 。每次旋转都只能旋转一个拨轮的一位数字。

锁的初始数字为 '0000' ,一个代表四个拨轮的数字的字符串。

列表 deadends 包含了一组死亡数字,一旦拨轮的数字和列表里的任何一个元素相同,这个锁将会被永久锁定,无法再被旋转。

字符串 target 代表可以解锁的数字,你需要给出解锁需要的最小旋转次数,如果无论如何不能解锁,返回 -1 。

示例 1:
```
输入:deadends = ["0201","0101","0102","1212","2002"], target = "0202"
输出:6
解释:
可能的移动序列为 "0000" -> "1000" -> "1100" -> "1200" -> "1201" -> "1202" -> "0202"。
注意 "0000" -> "0001" -> "0002" -> "0102" -> "0202" 这样的序列是不能解锁的,
因为当拨动到 "0102" 时这个锁就会被锁定。
```
示例 2:
```
输入: deadends = ["8888"], target = "0009"
输出:1
解释:
把最后一位反向旋转一次即可 "0000" -> "0009"。
```
示例 3:
```
输入: deadends = ["8887","8889","8878","8898","8788","8988","7888","9888"], target = "8888"
输出:-1
解释:
无法旋转到目标数字且不被锁定。
```
示例 4:
```
输入: deadends = ["0000"], target = "8888"
输出:-1
```

提示:
* 1 <= deadends.length <= 500
* deadends[i].length == 4
* target.length == 4
* target 不在 deadends 之中
* target 和 deadends[i] 仅由若干位数字组成

Solution:
### 基本分析

首先,我建议你先做 [127. 单词接龙](https://mp.weixin.qq.com/s?__biz=MzU4NDE3MTEyMA==&mid=2247486981&idx=1&sn=045ea6c880080fea1ce807794ccff69b&chksm=fd9ca51acaeb2c0c83d13e3b2a5196895d1a1b44f8981cc3efad9d6a2af158267010646cc262&token=394346611&lang=zh_CN#rd),然后再回过头将本题作为「练习题」。

[127. 单词接龙](https://mp.weixin.qq.com/s?__biz=MzU4NDE3MTEyMA==&mid=2247486981&idx=1&sn=045ea6c880080fea1ce807794ccff69b&chksm=fd9ca51acaeb2c0c83d13e3b2a5196895d1a1b44f8981cc3efad9d6a2af158267010646cc262&token=394346611&lang=zh_CN#rd) 定位困难,而本题定位中等。主要体现在数据范围上,思维难度上 [127. 单词接龙](https://mp.weixin.qq.com/s?__biz=MzU4NDE3MTEyMA==&mid=2247486981&idx=1&sn=045ea6c880080fea1ce807794ccff69b&chksm=fd9ca51acaeb2c0c83d13e3b2a5196895d1a1b44f8981cc3efad9d6a2af158267010646cc262&token=394346611&lang=zh_CN#rd) 并不比本题难,大胆做。

回到本题,根据题意,可以确定这是一个「最短路/最小步数」问题。

此类问题,通常我们会使用「BFS」求解,但朴素的 BFS 通常会带来搜索空间爆炸问题。

因此我们可以使用与  [127. 单词接龙](https://mp.weixin.qq.com/s?__biz=MzU4NDE3MTEyMA==&mid=2247486981&idx=1&sn=045ea6c880080fea1ce807794ccff69b&chksm=fd9ca51acaeb2c0c83d13e3b2a5196895d1a1b44f8981cc3efad9d6a2af158267010646cc262&token=394346611&lang=zh_CN#rd) 类似的思路进行求解。

---

### 双向 BFS

我们知道,递归树的展开形式是一棵多阶树。

使用朴素 BFS 进行求解时,队列中最多会存在“两层”的搜索节点。

因此搜索空间的上界取决于 **目标节点所在的搜索层次的深度所对应的宽度**。

下图展示了朴素 BFS 可能面临的搜索空间爆炸问题:

**在朴素的 BFS 实现中,空间的瓶颈主要取决于搜索空间中的最大宽度。**

那么有没有办法让我们不使用这么宽的搜索空间,同时又能保证搜索到目标结果呢?

「双向 BFS」 可以很好的解决这个问题:

**同时从两个方向开始搜索,一旦搜索到相同的值,意味着找到了一条联通起点和终点的最短路径。**

对于「有解」、「有一定数据范围」同时「层级节点数量以倍数或者指数级别增长」的情况,「双向 BFS」的搜索空间通常只有「朴素 BFS」的空间消耗的几百分之一,甚至几千分之一。

「双向 BFS」的基本实现思路如下:

1. 创建「两个队列」分别用于两个方向的搜索;
2. 创建「两个哈希表」用于「解决相同节点重复搜索」和「记录转换次数」;
3. 为了尽可能让两个搜索方向“平均”,每次从队列中取值进行扩展时,先判断哪个队列容量较少;
4. 如果在搜索过程中「搜索到对方搜索过的节点」,说明找到了最短路径。

「双向 BFS」基本思路对应的伪代码大致如下:

```Java
d1、d2 为两个方向的队列
m1、m2 为两个方向的哈希表,记录每个节点距离起点的

// 只有两个队列都不空,才有必要继续往下搜索
// 如果其中一个队列空了,说明从某个方向搜到底都搜不到该方向的目标节点
while(!d1.isEmpty() && !d2.isEmpty()) {
    if (d1.size() < d2.size()) {
        update(d1, m1, m2);
    } else {
        update(d2, m2, m1);
    }
}

// update 为将当前队列 d 中包含的元素取出,进行「一次完整扩展」的逻辑(按层拓展)
void update(Deque d, Map cur, Map other) {}
```

回到本题,我们看看如何使用「双向 BFS」进行求解。

估计不少同学是第一次接触「双向 BFS」,因此这次我写了大量注释。

建议大家带着对「双向 BFS」的基本理解去阅读。

代码:
```Java
class Solution {
    String t, s;
    Set<String> set = new HashSet<>();
    public int openLock(String[] _ds, String _t) {
        s = "0000";
        t = _t;
        if (s.equals(t)) return 0;
        for (String d : _ds) set.add(d);
        if (set.contains(s)) return -1;
        int ans = bfs();
        return ans;
    }
    int bfs() {
        // d1 代表从起点 s 开始搜索(正向)
        // d2 代表从结尾 t 开始搜索(反向)
        Deque<String> d1 = new ArrayDeque<>(), d2 = new ArrayDeque<>();
        /*
         * m1 和 m2 分别记录两个方向出现的状态是经过多少次转换而来
         * e.g.
         * m1 = {"1000":1} 代表 "1000" 由 s="0000" 旋转 1 次而来
         * m2 = {"9999":3} 代表 "9999" 由 t="9996" 旋转 3 次而来
         */
        Map<String, Integer> m1 = new HashMap<>(), m2 = new HashMap<>();
        d1.addLast(s);
        m1.put(s, 0);
        d2.addLast(t);
        m2.put(t, 0);

        /*
         * 只有两个队列都不空,才有必要继续往下搜索
         * 如果其中一个队列空了,说明从某个方向搜到底都搜不到该方向的目标节点
         * e.g.
         * 例如,如果 d1 为空了,说明从 s 搜索到底都搜索不到 t,反向搜索也没必要进行了
         */
        while (!d1.isEmpty() && !d2.isEmpty()) {
            int t = -1;
            if (d1.size() <= d2.size()) {
                t = update(d1, m1, m2);
            } else {
                t = update(d2, m2, m1);
            }
            if (t != -1) return t;
        }
        return -1;
    }
    int update(Deque<String> deque, Map<String, Integer> cur, Map<String, Integer> other) {
        int m = deque.size();
        while (m-- > 0) {
            String poll = deque.pollFirst();
            char[] pcs = poll.toCharArray();
            int step = cur.get(poll);
            // 枚举替换哪个字符
            for (int i = 0; i < 4; i++) {
                // 能「正向转」也能「反向转」,这里直接枚举偏移量 [-1,1] 然后跳过 0
                for (int j = -1; j <= 1; j++) {
                    if (j == 0) continue;

                    // 求得替换字符串 str
                    int origin = pcs[i] - '0';
                    int next = (origin + j) % 10;
                    if (next == -1) next = 9;

                    char[] clone = pcs.clone();
                    clone[i] = (char)(next + '0');
                    String str = String.valueOf(clone);

                    if (set.contains(str)) continue;
                    if (cur.containsKey(str)) continue;

                    // 如果在「另一方向」找到过,说明找到了最短路,否则加入队列
                    if (other.containsKey(str)) {
                        return step + 1 + other.get(str);
                    } else {
                        deque.addLast(str);
                        cur.put(str, step + 1);
                    }
                }
            }
        }
        return -1;
    }
}
```

---

### AStar 算法

可以直接根据本题规则来设计 A* 的「启发式函数」。

比如对于两个状态 `a` 和 `b` 可直接计算出「理论最小转换次数」:**不同字符的转换成本之和** 。

**需要注意的是:由于我们衡量某个字符 `str` 的估值是以目标字符串 `target` 为基准,因此我们只能确保 `target` 出队时为「距离最短」,而不能确保中间节点出队时「距离最短」,因此我们不能单纯根据某个节点是否「曾经入队」而决定是否入队,还要结合当前节点的「最小距离」是否被更新而决定是否入队。**

这一点十分关键,在代码层面上体现在 `map.get(str).step > poll.step + 1` 的判断上。

注意:本题用 A* 过了,但通常我们需要先「确保有解」,A* 的启发搜索才会发挥真正价值。而本题,除非 `t` 本身在 `deadends` 中,其余情况我们无法很好提前判断「是否有解」。对于无解的情况 A* 效果不如「双向 BFS」。

代码:
```Java
class Solution {
    class Node {
        String str;
        int val, step;
       /**
        *  str : 对应字符串
        *  val : 估值(与目标字符串 target 的最小转换成本)
        *  step: 对应字符串是经过多少步转换而来
        */
        Node(String _str, int _val, int _step) {
            str = _str;
            val = _val;
            step = _step;
        }
    }
    int f(String str) {
        int ans = 0;
        for (int i = 0; i < 4; i++) {
            int cur = str.charAt(i) - '0', target = t.charAt(i) - '0';
            int a = Math.min(cur, target), b = Math.max(cur, target);
            // 在「正向转」和「反向转」之间取 min
            int min = Math.min(b - a, a + 10 - b);
            ans += min;
        }
        return ans;
    }
    String s, t;
    Set<String> set = new HashSet<>();
    public int openLock(String[] ds, String _t) {
        s = "0000";
        t = _t;
        if (s.equals(t)) return 0;
        for (String d : ds) set.add(d);
        if (set.contains(s)) return -1;

        PriorityQueue<Node> q = new PriorityQueue<>((a,b)->a.val-b.val);
        Map<String, Node> map = new HashMap<>();
        Node root = new Node(s, f(s), 0);
        q.add(root);
        map.put(s, root);
        while (!q.isEmpty()) {
            Node poll = q.poll();
            char[] pcs = poll.str.toCharArray();
            int step = poll.step;
            if (poll.str.equals(t)) return step;
            for (int i = 0; i < 4; i++) {
                for (int j = -1; j <= 1; j++) {
                    if (j == 0) continue;
                    int cur = pcs[i] - '0';
                    int next = (cur + j) % 10;
                    if (next == -1) next = 9;

                    char[] clone = pcs.clone();
                    clone[i] = (char)(next + '0');
                    String str = String.valueOf(clone);

                    if (set.contains(str)) continue;
                    // 如果 str 还没搜索过,或者 str 的「最短距离」被更新,则入队
                    if (!map.containsKey(str) || map.get(str).step > step + 1) {
                        Node node = new Node(str, step + 1 + f(str), step + 1);
                        map.put(str, node);
                        q.add(node);
                    }
                }
            }
        }
        return -1;
    }
}
```

---

### IDA* 算法

同样我们可以使用基于 `DFS` 的启发式 IDA* 算法:

* 仍然使用 `f()` 作为估值函数
* 利用旋转次数有限:总旋转次数不会超过某个阈值 $\max$。
* 利用「迭代加深」的思路找到最短距离

理想情况下,由于存在正向旋转和反向旋转,每一位转轮从任意数字开始到达任意数字,消耗次数不会超过 $5$ 次,因此理想情况下可以设定 $\max = 5 * 4$ 。

但考虑 `deadends` 的存在,我们需要将 $\max$ 定义得更加保守一些:$\max = 10 * 4$ 。

**但这样的阈值设定,加上 IDA\* 算法每次会重复遍历「距离小于与目标节点距离」的所有节点,会有很大的 TLE 风险。**

**因此我们需要使用动态阈值:不再使用固定的阈值,而是利用 `target` 计算出「最大的转移成本」作为我们的「最深数量级」。**

PS. 上述的阈值分析是科学做法。对于本题可以利用数据弱,直接使用 $\max = 5 * 4$ 也可以通过,并且效果不错。

但必须清楚 $\max = 5 * 4$ 可能是一个错误的阈值,本题起点为 `0000`,考虑将所有正向转换的状态都放入 `deadends` 中,`target` 为 `2222`。这时候我们可以只限定 `0000` 先变为 `9999` 再往回变为 `2222` 的通路不在 `deadends` 中。

这时候使用 $\max = 5 * 4$ 就不对,但本题数据弱,可以通过(想提交错误数据拿积分吗?别试了,我已经提交了 🤣

代码:
```Java
class Solution {
    String s, t;
    String cur;
    Set<String> set = new HashSet<>();
    Map<String, Integer> map = new HashMap<>();
    public int openLock(String[] ds, String _t) {
        s = "0000";
        t = _t;
        if (s.equals(t)) return 0;
        for (String d : ds) set.add(d);
        if (set.contains(s)) return -1;

        int depth = 0, max = getMax();
        cur = s;
        map.put(cur, 0);
        while (depth <= max && !dfs(0, depth)) {
            map.clear();
            cur = s;
            map.put(cur, 0);
            depth++;
        }
        return depth > max ? -1 : depth;
    }
    int getMax() {
        int ans = 0;
        for (int i = 0; i < 4; i++) {
            int origin = s.charAt(i) - '0', next = t.charAt(i) - '0';
            int a = Math.min(origin, next), b = Math.max(origin, next);
            int max = Math.max(b - a, a + 10 - b);
            ans += max;
        }
        return ans;
    }
    int f() {
        int ans = 0;
        for (int i = 0; i < 4; i++) {
            int origin = cur.charAt(i) - '0', next = t.charAt(i) - '0';
            int a = Math.min(origin, next), b = Math.max(origin, next);
            int min = Math.min(b - a, a + 10 - b);
            ans += min;
        }
        return ans;
    }
    boolean dfs(int u, int max) {
        if (u + f() > max) return false;
        if (f() == 0) return true;
        String backup = cur;
        char[] cs = cur.toCharArray();
        for (int i = 0; i < 4; i++) {
            for (int j = -1; j <= 1; j++) {
                if (j == 0) continue;
                int origin = cs[i] - '0';
                int next = (origin + j) % 10;
                if (next == -1) next = 9;
                char[] clone = cs.clone();
                clone[i] = (char)(next + '0');
                String str = String.valueOf(clone);
                if (set.contains(str)) continue;
                if (!map.containsKey(str) || map.get(str) > u + 1) {
                    cur = str;
                    map.put(str, u + 1);
                    if (dfs(u + 1, max)) return true;
                    cur = backup;
                }
            }
        }
        return false;
    }
}
```

### 最后

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

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

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

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


Question:
在一根无限长的数轴上,你站在 `0` 的位置。终点在 `target` 的位置。

你可以做一些数量的移动 numMoves :

* 每次你可以选择向左或向右移动。
* 第 `i` 次移动(从  `i == 1` 开始,到 `i == numMoves`),在选择的方向上走 `i` 步。

给定整数 `target`,返回 到达目标所需的 最小 移动次数(即最小 `numMoves` ) 。

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

输出: 3

解释:
第一次移动,从 0 到 1 。
第二次移动,从 1 到 -1 。
第三次移动,从 -1 到 2 。
```
示例 2:
```
输入: target = 3

输出: 2

解释:
第一次移动,从 0 到 1 。
第二次移动,从 1 到 3 。
```

提示:
* $-10^9 <= target <= 10^9$
* $target != 0$

Solution:
### 数学

#### 提示一:数轴上的任意点都以起点($0$ 点)对称,只需要考虑对称点的任意一边

由于题目没有限制我们「不能到达哪些点」以及「出发的起始方向」,因此以起点为中心的左右两边对称。

即:左边所能到达任意一个点,都能通过调整所达路径的方向来将终点调整到右边。

同时由于起点是一个特殊的位置 $0$ 点,因此相应的「正数点」和「负数点」对称,我们仅需考虑一边(例如正数域)即可。

#### 提示二:先往靠近 `target` 的方向移动,到达或越过 `target` 的时候则停止

只考虑 `target` 为正的情况,我们假定起始先往靠近 `target` 的方向移动(即所有步数均为正值),根据是「到达」还是「越过」`target` 位置分情况讨论:

* 若能直接到达 `target`,此时消耗的必然是最小步数,可直接返回;
* 若越过了 `target`,假设此时消耗的步数为 $k$,所走的距离为 $dist = \frac{k \times (k + 1)}{2} > target$,我们可以考虑是否需要增加额外步数来到达 `target`。

#### 提示三:越过 `target` 时,如何不引入额外步数

若不引入额外步数,意味着我们需要将此前某些移动的方向进行翻转,使得调整后的 $dist = target$。

我们假设需要调整的步数总和为 `tot`,则有 $dist - 2 \times tot = target$,变形可得 $tot = \frac{dist - target}{2}$。

若想满足上述性质,需要确保能找到这样的 `tot`,即 `tot` 合法,

不难推导出当 `dist` 和 `target` 差值为「偶数」时(两者奇偶性相同),我们可以找到这样的 `tot`,从而实现不引入额外步数来到达 `target` 位置。

> 由于我们的 $dist$ 是由数列 $[1,2,3,...,k]$ 累加而来,因此必然能够在该数列 $[1,2,3...k]$ 中通过「不重复选择某些数」来凑成任意一个小于等于 $dist$ 的数。

#### 提示四:越过 `target` 时,如何尽量减少引入额外步数

当 `dist` 和 `target` 差值不为「偶数」时,我们只能通过引入额外步数(继续往右走)来使得,两者差值为偶数。

可以证明,最多引入步数不超过 $4$ 步,可使用得两者奇偶性相同,即不超过 $4$ 步可以覆盖到「奇数」和「偶数」两种情况。

根据 $k$ 与 $4$ 的余数关系分情况讨论:

* 余数为 $0$,即 $k = 4X$,由于 $dist = \frac{k(k+1)}{2} = \frac{4X(4X+1)}{2} = 2X(4X+1)$,其中一数为偶数,$dist$ 为偶数;
* 余数为 $1$,即 $k = 4X + 1$,由于 $dist = \frac{k(k+1)}{2} = \frac{(4X+1)(4X+2)}{2} = (4X+1)(2X+1)$,两个奇数相乘为奇数,$dist$ 为奇数;
* 余数为 $2$,即 $k = 4X + 2$,$dist = \frac{k(k+1)}{2} = \frac{(4X+2)(4X+3)}{2} = (2X+1)(4X+3)$,两个奇数相乘为奇数,$dist$ 为奇数;
* 余数为 $3$,即 $k = 4X + 3$,$dist = \frac{k(k+1)}{2} = \frac{(4X+3)(4X+4)}{2} = (4X+3)(2X+2)$,其中一数为偶数,$dist$ 为偶数。

因此在越过 `target` 后,最多引入不超过 $4$ 步可使得 `dist` 和 `target` 奇偶性相同。

#### 提示五:如何不通过「遍历」或「二分」的方式找到一个合适的 `k` 值,再通过不超过 $4$ 步的调整找到答案

我们期望找到一个合适的 `k` 值,使得 $dist = \frac{k \times (k + 1)}{2} < target$,随后通过增加 `k` 值来找到答案。

利用求和公式 $dist = \frac{k \times (k + 1)}{2}$,我们可以设定 $k = \left \lfloor \sqrt{2 \times target}) \right \rfloor$ 为起始值,随后逐步增大 `k` 值,直到满足「`dist` 和 `target` 奇偶性相同」。

Java 代码:
```Java
class Solution {
    public int reachNumber(int target) {
        if (target < 0) target = -target;
        int k = (int) Math.sqrt(2 * target), dist = k * (k + 1) / 2;
        while (dist < target || (dist - target) % 2 == 1) {
            k++;
            dist = k * (k + 1) / 2;
        }
        return k;
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    int reachNumber(int target) {
        if (target < 0) target = -target;
        int k = static_cast<int>(std::sqrt(2 * target));
        int dist = k * (k + 1) / 2;
        while (dist < target || (dist - target) % 2 == 1) {
            k++;
            dist = k * (k + 1) / 2;
        }
        return k;
    }
};
```
Python 代码:
```Python
class Solution:
    def reachNumber(self, target: int) -> int:
        if target < 0:
            target = -target
        k = int(math.sqrt(2 * target))
        dist = k * (k + 1) / 2
        while dist < target or (dist - target) % 2 == 1:
            k += 1
            dist = k * (k + 1) / 2
        return k
```
TypeScript 代码:
```TypeScript
function reachNumber(target: number): number {
    if (target < 0) target = -target
    let k = Math.floor(Math.sqrt(2 * target)), dist = k * (k + 1) / 2
    while (dist < target || (dist - target) % 2 == 1) {
        k++
        dist = k * (k + 1) / 2
    }
    return k
}
```
* 时间复杂度:$O(1)$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


757. 设置交集大小至少为2

Question:
一个整数区间 $[a, b]$  ($a < b$) 代表着从 `a` 到 `b` 的所有连续整数,包括 `a` 和 `b`。

给你一组整数区间 `intervals`,请找到一个最小的集合 `S`,使得 `S` 里的元素与区间 `intervals` 中的每一个整数区间都至少有 $2$ 个元素相交。

输出这个最小集合 `S` 的大小。

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

输出: 3

解释:
考虑集合 S = {2, 3, 4}. S与intervals中的四个区间都有至少2个相交的元素。
且这是S最小的情况,故我们输出3。
```
示例 2:
```
输入: intervals = [[1, 2], [2, 3], [2, 4], [4, 5]]

输出: 5

解释:
最小的集合S = {1, 2, 3, 4, 5}.
```

注意:
* `intervals` 的长度范围为 $[1, 3000]$。
* `intervals[i]` 长度为 $2$,分别代表左、右边界。
* `intervals[i][j]` 的值是 $[0, 10^8]$ 范围内的整数。

Solution:
### 贪心

不要被样例数据误导了,题目要我们求最小点集的数量,并不规定点集 `S` 是连续段。

为了方便,我们令 `intervals` 为 `ins`。

当只有一个线段时,我们可以在线段内取任意两点作为 `S` 成员,而当只有两个线段时,我们可以两个线段重合情况进行决策:

1. 当两个线段完全不重合时,为满足题意,我们需要从两个线段中各取两个点,此时这四个点都可以任意取;
2. 当两个线段仅有一个点重合,为满足 `S` 最小化的题意,我们可以先取重合点,然后再两个线段中各取一个;
3. 当两个线段有两个及以上的点重合,此时在重合点中任选两个即可。

不难发现,当出现重合的所有情况中,必然可以归纳某个线段的边缘点上。即不存在两个线段存在重合点,仅发生在两线段的中间部分:

因此我们可以从边缘点决策进行入手。

具体的,我们可以按照「右端点从小到大,左端点从大到小」的双关键字排序,然后从前往后处理每个区间,处理过程中不断往 `S` 中添加元素,由于我们已对所有区间排序且从前往后处理,因此我们往 `S` 中增加元素的过程中必然是单调递增,同时在对新的后续区间考虑是否需要往 `S` 中添加元素来满足题意时,也是与 `S` 中的最大/次大值(点集中的边缘元素)做比较,因此我们可以使用两个变量 `a` 和 `b` 分别代指当前集合 `S` 中的次大值和最大值(`a` 和 `b` 初始化为足够小的值 $-1$),而无需将整个 `S` 存下来。

不失一般性的考虑,当我们处理到 $ins[i]$ 时,该如何决策:

1. 若 $ins[i][0] > b$(当前区间的左端点大于当前点集 `S` 的最大值),说明 $ins[i]$ 完全不被 `S` 所覆盖,为满足题意,我们要在 $ins[i]$ 中选两个点,此时直观思路是选择 $ins[i]$ 最靠右的两个点(即 $ins[i][1] - 1$ 和 $ins[i][1]$);
2. 若 $ins[i][0] > a$(即当前区间与点集 `S` 存在一个重合点 `b`,由于次大值 `a` 和 最大值 `b` 不总是相差 $1$,我们不能写成 $ins[i][0] == b$),此时为了满足 $ins[i]$ 至少被 $2$ 个点覆盖,我们需要在 $ins[i]$ 中额外选择一个点,此时直观思路是选择 $ins[i]$ 最靠右的点(即$ins[i][1]$);
3. 其余情况,说明当前区间 $ins[i]$ 与点集 `S` 至少存在两个点 `a` 和 `b`,此时无须额外引入其余点来覆盖 $ins[i]$。

上述情况是对「右端点从小到大」的必要性说明,而「左端点从大到小」目的是为了方便我们处理边界情况而引入的:若在右端点相同的情况下,如果「左端点从小到大」处理的话,会有重复的边缘点被加入 `S`。

> 有同学对重复边缘点加入 `S` 不理解,假设 `S` 当前的次大值和最大值分别为 `j` 和 `k`(其中 $k - j > 1$),如果后面有两个区间分别为 $[k - 1, d]$ 和 $[k + 1, d]$ 时,就会出现问题(其中 `d` 为满足条件的任意右端点)
更具体的:假设当前次大值和最大值分别为 $2$ 和 $4$,后续两个区间分别为 $[3, 8]$ 和 $[5, 8]$,你会发现先处理 $[3, 8]$ 的话,数值 $8$ 会被重复添加

**上述决策存在直观判断,需要证明不存在比该做法取得的点集 `S` 更小的合法解**:

若存在更小的合法集合方案 `A`(最优解),根据我们最前面对两个线段的重合分析知道,由于存在任意选点均能满足覆盖要求的情况,因此最优解 `A` 的具体方案可能并不唯一。

因此首先我们先在不影响 `A` 的集合大小的前提下,**对具体方案 `A` 中的非关键点(即那些被选择,但既不是某个具体区间的边缘点,也不是边缘点的相邻点)进行调整(修改为区间边缘点或边缘点的相邻点)**。

这样我们能够得到一个唯一的最优解具体方案,该方案既能取到最小点集大小,同时与贪心解 `S` 的选点有较大重合度。

此时如果贪心解并不是最优解的话,意味着贪心解中存在某些不必要的点(可去掉,同时不会影响覆盖要求)。

然后我们在回顾下,我们什么情况下会往 `S` 中进行加点,根据上述「不失一般性」的分析:

1. 当 $ins[i][0] > b$ 时,我们会往 `S` 中添加两个点,若这个不必要的点是在这个分支中被添加的话,意味着当前 $ins[i]$ 可以不在此时被覆盖,而在后续其他区间 $ins[j]$ 被覆盖时被同步覆盖(其中 $j > i$),此时必然对应了我们重合分析中的后两种情况,可以将原本在 $ins[j]$ 中被选择的点,调整为 $ins[i]$ 的两个边缘点,结果不会变差(覆盖情况不变,点数不会变多):

即此时原本在最优解 `A` 中不存在,在贪心解 `S` 中存在的「不必要点」会变成「必要点」。

2. 当 $ins[i] > a$ 时,我们会往 `S` 中添加一个点,若这个不必要的点是在这个分支被添加的话,分析方式同理,且情况 $1$ 不会发生,如果 $ins[i]$ 和 $ins[j]$ 只有一个重合点的话,起始 $ins[i][1]$ 不会是不必要点:

**综上,我们可以经过两步的“调整”,将贪心解变为最优解:第一步调整是在最优解的任意具体方案 `A` 中发生,通过将所有非边缘点调整为边缘点,来得到一个唯一的最优解具体方案;然后通过反证法证明,贪心解 `S` 中并不存在所谓的可去掉的「不必要点」,从而证明「贪心解大小必然不会大于最优解的大小」,即 $S > A$ 不成立,$S \leq A$ 恒成立,再结合 `A` 是最优解的前提($A \leq S$),可得 $S = A$。**

Java 代码:
```Java
class Solution {
    public int intersectionSizeTwo(int[][] ins) {
        Arrays.sort(ins, (a, b)->{
            return a[1] != b[1] ? a[1] - b[1] : b[0] - a[0];
        });
        int a = -1, b = -1, ans = 0;
        for (int[] i : ins) {
            if (i[0] > b) {
                a = i[1] - 1; b = i[1];
                ans += 2;
            } else if (i[0] > a) {
                a = b; b = i[1];
                ans++;
            }
        }
        return ans;
    }
}
```
TypeScript 代码:
```TypeScript
function intersectionSizeTwo(ins: number[][]): number {
    ins = ins.sort((a, b)=> {
        return a[1] != b[1] ? a[1] - b[1] : b[0] - a[0]
    });
    let a = -1, b = -1, ans = 0
    for (const i of ins) {
        if (i[0] > b) {
            a = i[1] - 1; b = i[1]
            ans += 2
        } else if (i[0] > a) {
            a = b; b = i[1]
            ans++
        }
    }
    return ans
};
```
* 时间复杂度:$O(n\log{n})$
* 空间复杂度:$O(\log{n})$

### 最后

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

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

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

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


761. 特殊的二进制序列

Question:
特殊的二进制序列是具有以下两个性质的二进制序列:

* `0` 的数量与 `1` 的数量相等。
* 二进制序列的每一个前缀码中 `1` 的数量要大于等于 `0` 的数量。

给定一个特殊的二进制序列 `S`,以字符串形式表示。定义一个操作为首先选择 `S` 的两个连续且非空的特殊的子串,然后将它们交换。(两个子串为连续的当且仅当第一个子串的最后一个字符恰好为第二个子串的第一个字符的前一个字符。)

在任意次数的操作之后,交换后的字符串按照字典序排列的最大的结果是什么?

示例 1:
```
输入: S = "11011000"

输出: "11100100"

解释:
将子串 "10" (在S[1]出现) 和 "1100" (在S[3]出现)进行交换。
这是在进行若干次操作后按字典序排列最大的结果。
```
说明:
* `S` 的长度不超过 $50$。
* `S` 保证为一个满足上述定义的特殊 的二进制序列。

Solution:
### 构造

我们可以定义每个字符的得分:字符 `1` 得分为 $1$ 分,字符 `0` 得分为 $-1$ 分。

根据题目对「特殊字符串」的定义可知,给定字符串 `s` 的总得分为 $0$,且任意前缀串不会出现得分为负数的情况。

考虑将 `s` 进行划分为多个足够小特殊字符串 `item`(足够小的含义为每个 `item` 无法再进行划分),每个 `item` 的总得分为 $0$。根据 `s` 定义,必然可恰好划分为多个 `item`。

每次操作可以将相邻特殊字符串进行交换,于是问题转换为将 `s` 进行重排,求其重排后字典序最大的方案。

首先可以证明一个合法 `item` 必然满足 `1...0` 的形式,可通过「反证法」进行进行证明:定义了 `item` 总得分为 $0$,且长度不为 $0$,因此必然有 `1` 有 `0`。**若第一位字符为 `0`,则必然能够从第一位字符作为起点,找到一个得分为负数的子串,这与 `s` 本身的定义冲突(`s` 中不存在得分为负数的前缀串);若最后一位为 `1`,根据 `item` 总得分为 $0$,可知当前 `item` 去掉最后一位后得分为负,也与 `s` 本身定义冲突(`s` 中不存在得分为负数的前缀串)。**

因此可将构造分两步进行:

1. 对于每个 `item` 进行重排,使其调整为字典序最大
2. 对于 `item` 之间的位置进行重排,使其整体字典序最大

由于题目没有规定重排后的性质,为第一步调整和第二步调整的保持相对独立,我们只能对 `item` 中的 `1...0` 中的非边缘部分进行调整(递归处理子串部分)。

假设所有 `item` 均被处理后,考虑如何进行重排能够使得最终方案字典序最大。

若有两个 `item`,分别为 `a` 和 `b`,我们可以根据拼接结果 `ab` 和 `ba` 的字典序大小来决定将谁放在前面。

**这样根据「排序比较逻辑」需要证明在集合上具有[「全序关系」](https://baike.baidu.com/item/%E5%85%A8%E5%BA%8F%E5%85%B3%E7%B3%BB):**

我们使用符号 $@$ 来代指我们的「排序」逻辑:

* 如果 $a$ 必须排在 $b$ 的前面,我们记作 `a@b`;
* 如果 $a$ 必须排在 $b$ 的后面,我们记作 `b@a`;
* 如果 $a$ 既可以排在 $b$ 的前面,也可以排在 $b$ 的后面,我们记作 `a#b`;

**2.1 完全性**

**具有完全性是指从集合 `items` 中任意取出两个元素 $a$ 和 $b$,必然满足 `a@b`、`b@a` 和 `a#b` 三者之一。**

**这点其实不需要额外证明,因为由 $a$ 和 $b$ 拼接的字符串 $ab$ 和 $ba$ 所在「字典序大小关系中」要么完全相等,要么具有明确的字典序大小关系,导致 $a$ 必须排在前面或者后面。**

**2.2 反对称性**

**具有反对称性是指由 `a@b` 和 `b@a` 能够推导出 `a#b`。**

$a@b$ 说明字符串 $ab$ 的字典序大小数值要比字符串 $ba$ 字典序大小数值大。

$b@a$ 说明字符串 $ab$ 的字典序大小数值要比字符串 $ba$ 字典序大小数值小。

**这样,基于「字典序本身满足全序关系」和「数学上的 $a \geqslant b$ 和 $a \leqslant b$ 可推导出 $a = b$」。**

**得证 `a@b` 和 `b@a` 能够推导出 `a#b`。**

**2.3 传递性**

**具有传递性是指由 `a@b` 和 `b@c` 能够推导出 `a@c`。**

**我们可以利用「两个等长的拼接字符串,字典序大小关系与数值大小关系一致」这一性质来证明,因为字符串 $ac$ 和 $ca$ 必然是等长的。**

接下来,让我们从「自定义排序逻辑」出发,换个思路来证明 `a@c`:

**然后我们只需要证明在不同的 $i$ $j$ 关系之间(共三种情况),`a@c` 恒成立即可:**

1. 当 $i == j$ 的时候:

2. 当 $i > j$ 的时候:

3. 当 $i < j$ 的时候:

**综上,我们证明了无论在何种情况下,只要有 `a@b` 和 `b@c` 的话,那么 `a@c` 恒成立。**

**我们之所以能这样证明「传递性」,本质是利用了自定义排序逻辑中「对于确定任意元素 $a$ 和 $b$ 之间的排序关系只依赖于 $a$ 和 $b$ 的第一个不同元素之间的大小关系」这一性质。**

最终,我们证明了该「排序比较逻辑」必然能排序出字典序最大的方案。

Java 代码:
```Java
class Solution {
    public String makeLargestSpecial(String s) {
        if (s.length() == 0) return s;
        List<String> list = new ArrayList<>();
        char[] cs = s.toCharArray();
        for (int i = 0, j = 0, k = 0; i < cs.length; i++) {
            k += cs[i] == '1' ? 1 : -1;
            if (k == 0) {
                list.add("1" + makeLargestSpecial(s.substring(j + 1, i)) + "0");
                j = i + 1;
            }
        }
        Collections.sort(list, (a, b)->(b + a).compareTo(a + b));
        StringBuilder sb = new StringBuilder();
        for (String str : list) sb.append(str);
        return sb.toString();
    }
}
```
TypeScript 代码:
```TypeScript
function makeLargestSpecial(s: string): string {
    const list = new Array<string>()
    for (let i = 0, j = 0, k = 0; i < s.length; i++) {
        k += s[i] == '1' ? 1 : -1
        if (k == 0) {
            list.push('1' + makeLargestSpecial(s.substring(j + 1, i)) + '0')
            j = i + 1
        }
    }
    list.sort((a, b)=>(b + a).localeCompare(a + b));
    return [...list].join("")
};
```
* 时间复杂度:$O(n^2)$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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