Question:
在无限长的数轴(即 `x` 轴)上,我们根据给定的顺序放置对应的正方形方块。

第 i 个掉落的方块(`positions[i] = (left, side_length)`)是正方形,其中 `left` 表示该方块最左边的点位置(`positions[i][0]`),`side_length` 表示该方块的边长(`positions[i][1]`)。

每个方块的底部边缘平行于数轴(即 `x` 轴),并且从一个比目前所有的落地方块更高的高度掉落而下。在上一个方块结束掉落,并保持静止后,才开始掉落新方块。

方块的底边具有非常大的粘性,并将保持固定在它们所接触的任何长度表面上(无论是数轴还是其他方块)。邻接掉落的边不会过早地粘合在一起,因为只有底边才具有粘性。

返回一个堆叠高度列表 `ans`。每一个堆叠高度 `ans[i]` 表示在通过 `positions[0], positions[1], ..., positions[i]` 表示的方块掉落结束后,目前所有已经落稳的方块堆叠的最高高度。

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

输出: [2, 5, 5]

解释:

第一个方块 positions[0] = [1, 2] 掉落:
_aa
_aa
-------
方块最大高度为 2 。

第二个方块 positions[1] = [2, 3] 掉落:
__aaa
__aaa
__aaa
_aa__
_aa__
--------------
方块最大高度为5。
大的方块保持在较小的方块的顶部,不论它的重心在哪里,因为方块的底部边缘有非常大的粘性。

第三个方块 positions[1] = [6, 1] 掉落:
__aaa
__aaa
__aaa
_aa
_aa___a
--------------
方块最大高度为5。

因此,我们返回结果[2, 5, 5]。
```

示例 2:
```
输入: [[100, 100], [200, 100]]

输出: [100, 100]

解释: 相邻的方块不会过早地卡住,只有它们的底部边缘才能粘在表面上。
```

注意:
* $1 <= positions.length <= 1000$
* $1 <= positions[i][0] <= 10^8$
* $1 <= positions[i][1] <= 10^6$

Solution:
### 基本分析

为了方便,我们使用 `ps` 来代指 `positions`。

每次从插入操作都附带一次询问,因此询问次数为 $1e3$,左端点的最大值为 $10e8$,边长最大值为 $1e6$,由此可知值域范围大于 $1e8$,但不超过 $1e9$。

对于值域范围大,但查询次数有限的区间和问题,不久前曾经总结过 : [求解常见「值域爆炸,查询有限」区间问题的几种方式](https://mp.weixin.qq.com/s?__biz=MzU4NDE3MTEyMA==&mid=2247491187&idx=2&sn=bb2d8b7e89c535914da8107387e951a2),可作为前置 🧀 进行了解。

而我的个人习惯,一般要么使用「离散化 + 线段树」,要么使用「线段树(动态开点)」进行求解。

本题为「非强制在线」问题,因此可以先对 `ps` 数组进行离散化,将值域映射到较小的空间,然后套用固定占用 $4 \times n$ 空间的线段树求解。

但更为灵活(能够同时应对强制在线问题)的求解方式是「线段树(动态开点)」。

同时实现动态开点的方式有两种:

1. 根据操作次数对使用到的最大点数进行预估,并采用数组方式进行实现线段树(解法一);
2. 使用动态指针(解法二);

方式一在不久之前的每日一题 [933. 最近的请求次数](https://sharingsource.github.io/2022/05/06/933.%20%E6%9C%80%E8%BF%91%E7%9A%84%E8%AF%B7%E6%B1%82%E6%AC%A1%E6%95%B0%EF%BC%88%E7%AE%80%E5%8D%95%EF%BC%89/) 讲过,因此今天把方式二也写一下。

具体的,我们将顺序放置方块的操作(假设当前方块的左端点为 $a$,边长为 $len$,则有右端点为 $b = a + len$),分成如下两步进行:

* 查询当前范围 $[a, b]$ 的最大高度为多少,假设为 $cur$;
* 更新当前范围 $[a, b]$ 的最新高度为 $cur + len$。

因此这本质上是一个「区间修改 + 区间查询」的问题,我们需要实现带「懒标记」的线段树,从而确保在进行「区间修改」时复杂度仍为 $O(\log{n})$。

> 另外有一个需要注意的细节是:不同方块之间的边缘可以重合,但不会导致方块叠加,因此我们当我们对一个区间 $[a, b]$ 进行操作(查询或插入)时,可以将其调整为 $[a, b - 1]$,从而解决边缘叠加操作高度错误的问题。

---

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

估点的基本方式在前置 🧀 [求解常见「值域爆炸,查询有限」区间问题的几种方式](https://mp.weixin.qq.com/s?__biz=MzU4NDE3MTEyMA==&mid=2247491187&idx=2&sn=bb2d8b7e89c535914da8107387e951a2) 详细讲过。

简单来说,可以直接估算为 $6 \times m \times \log{n}$ 即可,其中 $m$ 为询问次数(对应本题就是 `ps` 的长度),而 $n$ 为值域大小(对应本题可直接取成 $1e9$);而另外一个比较实用(避免估算)的估点方式可以「尽可能的多开点数」,利用题目给定的空间上界和我们创建的自定义类(结构体)的大小,尽可能的多开(不考虑字节对齐,或者结构体过大的情况,`Java` 的 $128M$ 可以开到 $5 \times 10^6$ 以上)。

代码:
```Java
class Solution {
    class Node {
        // ls 和 rs 分别代表当前区间的左右子节点所在 tr 数组中的下标
        // val 代表当前区间的最大高度,add 为懒标记
        int ls, rs, val, add;
    }
    int N = (int)1e9, cnt = 0;
    Node[] tr = new Node[1000010];
    void update(int u, int lc, int rc, int l, int r, int v) {
        if (l <= lc && rc <= r) {
            tr[u].val = v;
            tr[u].add = v;
            return ;
        }
        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].val;
        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 pushdown(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();
        }
        if (tr[u].add == 0) return ;
        int add = tr[u].add;
        tr[tr[u].ls].add = add; tr[tr[u].rs].add = add;
        tr[tr[u].ls].val = add; tr[tr[u].rs].val = add;
        tr[u].add = 0;
    }
    void pushup(int u) {
        tr[u].val = Math.max(tr[tr[u].ls].val, tr[tr[u].rs].val);
    }
    public List<Integer> fallingSquares(int[][] ps) {
        List<Integer> ans = new ArrayList<>();
        tr[1] = new Node();
        for (int[] info : ps) {
            int x = info[0], h = info[1], cur = query(1, 1, N, x, x + h - 1);
            update(1, 1, N, x, x + h - 1, cur + h);
            ans.add(tr[1].val);
        }
        return ans;
    }
}
```
* 时间复杂度:令 $m$ 为查询次数,$n$ 为值域大小,复杂度为 $O(m\log{n})$
* 空间复杂度:$O(m\log{n})$

---

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

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

代码:
```Java
class Solution {
    int N = (int)1e9;
    class Node {
        // ls 和 rs 分别代表当前区间的左右子节点
        Node ls, rs;
        // val 代表当前区间的最大高度,add 为懒标记
        int val, add;
    }
    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.val = 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.val;
        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();
        if (node.add == 0) return ;
        node.ls.add = node.add; node.rs.add = node.add;
        node.ls.val = node.add; node.rs.val = node.add;
        node.add = 0;
    }
    void pushup(Node node) {
        node.val = Math.max(node.ls.val, node.rs.val);
    }
    public List<Integer> fallingSquares(int[][] ps) {
        List<Integer> ans = new ArrayList<>();
        for (int[] info : ps) {
            int x = info[0], h = info[1], cur = query(root, 0, N, x, x + h - 1);
            update(root, 0, N, x, x + h - 1, cur + h);
            ans.add(root.val);
        }
        return ans;
    }
}
```
* 时间复杂度:令 $m$ 为查询次数,$n$ 为值域大小,复杂度为 $O(m\log{n})$
* 空间复杂度:$O(m\log{n})$

### 最后

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

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

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

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


700. 二叉搜索树中的搜索

Question:
给定二叉搜索树(BST)的根节点和一个值。 你需要在BST中找到节点值等于给定值的节点。 返回以该节点为根的子树。 如果节点不存在,则返回 NULL。

例如,
```
给定二叉搜索树:

        4
       / \
      2   7
     / \
    1   3

和值: 2
```
你应该返回如下子树:
```
      2
     / \
    1   3
```
在上述示例中,如果要找的值是 5,但因为没有节点值为 5,我们应该返回 NULL。

Solution:
### 递归

根据题意,进行「递归」搜索即可。

代码:
```Java
class Solution {
    public TreeNode searchBST(TreeNode root, int val) {
        if (root == null || root.val == val) return root;
        return root.val < val ? searchBST(root.right, val) : searchBST(root.left, val);
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:忽略递归带来的额外空间开销,复杂度为 $O(n)$

---

### 迭代

同理,可以使用「迭代」进行搜索。

代码:
```Java
class Solution {
    public TreeNode searchBST(TreeNode root, int val) {
        while (root != null && root.val != val) {
            root = root.val < val ? root.right : root.left;
        }
        return root;
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


LeetCode/701-710/703. 数据流中的第 K 大元素（简单）.md

703. 数据流中的第 K 大元素

Question:
设计一个找到数据流中第 `k` 大元素的类(class)。注意是排序后的第 `k` 大元素,不是第 `k` 个不同的元素。

请实现 `KthLargest` 类:
* `KthLargest(int k, int[] nums)` 使用整数 `k` 和整数流 `nums` 初始化对象。
* `int add(int val)` 将 `val` 插入数据流 `nums` 后,返回当前数据流中第 `k` 大的元素。

示例:
```
输入:
["KthLargest", "add", "add", "add", "add", "add"]
[[3, [4, 5, 8, 2]], [3], [5], [10], [9], [4]]
输出:
[null, 4, 5, 5, 8, 8]

解释:
KthLargest kthLargest = new KthLargest(3, [4, 5, 8, 2]);
kthLargest.add(3);   // return 4
kthLargest.add(5);   // return 5
kthLargest.add(10);  // return 5
kthLargest.add(9);   // return 8
kthLargest.add(4);   // return 8
```

提示:
* $1 <= k <= 10^4$
* $0 <= nums.length <= 10^4$
* $-10^4 <= nums[i] <= 10^4$
* $-10^4 <= val <= 10^4$
* 最多调用 `add` 方法 $10^4$ 次
* 题目数据保证,在查找第 `k` 大元素时,数组中至少有 `k` 个元素

Solution:
### 冒泡排序 (TLE)

每次调用 `add` 时先将数装入数组,然后遍历 `k` 次,通过找 `k` 次最大值来找到 Top K。

代码:
```Java
class KthLargest {
    int k;
    List<Integer> list = new ArrayList<>(10009);
    public KthLargest(int _k, int[] _nums) {
        k = _k;
        for (int i : _nums) list.add(i);
    }
    public int add(int val) {
        list.add(val);
        int cur = 0;
        for (int i = 0; i < k; i++) {
            int idx = findMax(cur, list.size() - 1);
            swap(cur++, idx);
        }
        return list.get(cur - 1);
    }
    int findMax(int start, int end) {
        int ans = 0, max = Integer.MIN_VALUE;
        for (int i = start; i <= end; i++) {
            int t = list.get(i);
            if (t > max) {
                max = t;
                ans = i;
            }
        }
        return ans;
    }
    void swap(int a, int b) {
        int c = list.get(a);
        list.set(a, list.get(b));
        list.set(b, c);
    }
}
```
* 时间复杂度:$O(nk)$
* 空间复杂度:$O(n)$

---

### 快速排序

上述的解法时间复杂度是 $O(nk)$ 的,当 `k` 很大的时候会超时。

我们可以使用快排来代替冒泡,将复杂度变为 $O(n\log{n})$。

代码:
```Java
class KthLargest {
    int k;
    List<Integer> list = new ArrayList<>(10009);
    public KthLargest(int _k, int[] _nums) {
        k = _k;
        for (int i : _nums) list.add(i);
    }

    public int add(int val) {
        list.add(val);
        Collections.sort(list);
        return list.get(list.size() - k);
    }
}
```
* 时间复杂度:$O(n\log{n})$
* 空间复杂度:$O(n)$

---

### 优先队列

使用优先队列构建一个容量为 `k` 的小根堆。

将 `nums` 中的前 `k` 项放入优先队列(此时堆顶元素为前 `k` 项的最大值)。

随后逐项加入优先队列:

* 堆内元素个数达到 `k` 个:

  * 加入项小于等于堆顶元素:加入项排在第 `k` 大元素的后面。直接忽略
  * 加入项大于堆顶元素:将堆顶元素弹出,加入项加入优先队列,调整堆

* 堆内元素个数不足 `k` 个,将加入项加入优先队列

将堆顶元素进行返回(数据保证返回答案时,堆内必然有 `k` 个元素):

代码:
```Java
class KthLargest {
    int k;
    PriorityQueue<Integer> queue;
    public KthLargest(int _k, int[] _nums) {
        k = _k;
        queue = new PriorityQueue<>(k, (a,b)->Integer.compare(a,b));
        int n = _nums.length;
        for (int i = 0; i < k && i < n; i++) queue.add(_nums[i]);
        for (int i = k; i < n; i++) add(_nums[i]);
    }
    public int add(int val) {
        int t = !queue.isEmpty() ? queue.peek() : Integer.MIN_VALUE;
        if (val > t || queue.size() < k) {
            if (!queue.isEmpty() && queue.size() >= k) queue.poll();
            queue.add(val);
        }
        return queue.peek();
    }
}
```
* 时间复杂度:最坏情况下,`n` 个元素都需要入堆。复杂度为 $O(n\log{k})$
* 空间复杂度:$O(k)$

### 最后

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

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

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

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


Question:
给定一个 n 个元素有序的(升序)整型数组 nums 和一个目标值 target  ,写一个函数搜索 nums 中的 target,如果目标值存在返回下标,否则返回 -1。

示例 1:
```
输入: nums = [-1,0,3,5,9,12], target = 9

输出: 4

解释: 9 出现在 nums 中并且下标为 4
```
示例 2:
```
输入: nums = [-1,0,3,5,9,12], target = 2

输出: -1

解释: 2 不存在 nums 中因此返回 -1
```

提示:
* 你可以假设 nums 中的所有元素是不重复的。
* n 将在 [1, 10000]之间。
* nums 的每个元素都将在 [-9999, 9999]之间。

Solution:
### 二分

一道二分裸题,写二分重点在于学会从「二段性」出发进行分析,想清楚二分的是哪个端点/边界。

而不是局限于用什么模板,大于还是小于,硬记这些对理解二分不会有任何帮助。

事实上,当搞清楚二分的是什么内容之后,何种形式写法都能写出。

代码:

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

### 最后

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

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

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

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


Question:
不使用任何内建的哈希表库设计一个哈希集合(HashSet)。

实现 MyHashSet 类:

void add(key) 向哈希集合中插入值 key 。
bool contains(key) 返回哈希集合中是否存在这个值 key 。
void remove(key) 将给定值 key 从哈希集合中删除。如果哈希集合中没有这个值,什么也不做。

示例:
```
输入:
["MyHashSet", "add", "add", "contains", "contains", "add", "contains", "remove", "contains"]
[[], [1], [2], [1], [3], [2], [2], [2], [2]]
输出:
[null, null, null, true, false, null, true, null, false]

解释:
MyHashSet myHashSet = new MyHashSet();
myHashSet.add(1);      // set = [1]
myHashSet.add(2);      // set = [1, 2]
myHashSet.contains(1); // 返回 True
myHashSet.contains(3); // 返回 False ,(未找到)
myHashSet.add(2);      // set = [1, 2]
myHashSet.contains(2); // 返回 True
myHashSet.remove(2);   // set = [1]
myHashSet.contains(2); // 返回 False ,(已移除)
```

提示:
* 0 <= key <= $10^6$
* 最多调用 $10^4$ 次 add、remove 和 contains 。

Solution:
### 简单数组

由于题目给出了 `0 <= key <= 10^6` 数据范围,同时限定了 key 只能是 int。

我们可以直接使用一个 boolean 数组记录某个 key 是否存在,key 直接对应 boolean 的下标。

代码:
```Java
class MyHashSet {
    boolean[] nodes = new boolean[1000009];

    public void add(int key) {
        nodes[key] = true;
    }

    public void remove(int key) {
        nodes[key] = false;
    }

    public boolean contains(int key) {
        return nodes[key];
    }
}
```
* 时间复杂度:$O(1)$
* 空间复杂度:$O(1)$

***

### 链表

我们利用「链表」来构建 Set,这也是工程上最简单的一种实现方式。

代码:
```Java
class MyHashSet {
    // 由于使用的是「链表」,这个值可以取得很小
    Node[] nodes = new Node[10009];

    public void add(int key) {
        // 根据 key 获取哈希桶的位置
        int idx = getIndex(key);
        // 判断链表中是否已经存在
        Node loc = nodes[idx], tmp = loc;
        if (loc != null) {
            Node prev = null;
            while (tmp != null) {
                if (tmp.key == key) {
                    return;
                }
                prev = tmp;
                tmp = tmp.next;
            }
            tmp = prev;
        }
        Node node = new Node(key);

        // 头插法
        // node.next = loc;
        // nodes[idx] = node;

        // 尾插法
        if (tmp != null) {
            tmp.next = node;
        } else {
            nodes[idx] = node;
        }
    }

    public void remove(int key) {
        int idx = getIndex(key);
        Node loc = nodes[idx];
        if (loc != null) {
            Node prev = null;
            while (loc != null) {
                if (loc.key == key) {
                    if (prev != null) {
                        prev.next = loc.next;
                    } else {
                        nodes[idx] = loc.next;
                    }
                    return;
                }
                prev = loc;
                loc = loc.next;
            }
        }
    }

    public boolean contains(int key) {
        int idx = getIndex(key);
        Node loc = nodes[idx];
        if (loc != null) {
            while (loc != null) {
                if (loc.key == key) {
                    return true;
                }
                loc = loc.next;
            }
        }
        return false;
    }

    static class Node {
        private int key;
        private Node next;
        private Node(int key) {
            this.key = key;
        }
    }

    int getIndex(int key) {
        // 因为 nodes 的长度只有 10009,对应的十进制的 10011100011001(总长度为 32 位,其余高位都是 0)
        // 为了让 key 对应的 hash 高位也参与运算,这里对 hashCode 进行右移异或
        // 使得 hashCode 的高位随机性和低位随机性都能体现在低 16 位中
        int hash = Integer.hashCode(key);
        hash ^= (hash >>> 16);
        return hash % nodes.length;
    }
}
```
* 时间复杂度:由于没有扩容的逻辑,最坏情况下复杂度为 $O(n)$,一般情况下复杂度为 $O(1)$
* 空间复杂度:$O(1)$

***

### 分桶数组

事实上我们还可以实现一个类似「bitmap」数据结构。

使用 int 中的每一位代表一个位置。

由于数据范围为 `0 <= key <= 10^6`,我们最多需要的 int 数量不会超过 40000。

因此我们可以建立一个 buckets 数组,数组装载的 int 类型数值。

* 先对 key 进行 `key / 32`,确定当前 key 所在桶的位置(大概位置)
* 再对 key 进行 `key % 32`,确定当前 key 所在桶中的哪一位(精确位置)

根据位运算对「精确位置」进行修改。

代码:
```Java
class MyHashSet {
    int[] bs = new int[40000];
    public void add(int key) {
        int bucketIdx = key / 32;
        int bitIdx = key % 32;
        setVal(bucketIdx, bitIdx, true);
    }

    public void remove(int key) {
        int bucketIdx = key / 32;
        int bitIdx = key % 32;
        setVal(bucketIdx, bitIdx, false);
    }

    public boolean contains(int key) {
        int bucketIdx = key / 32;
        int bitIdx = key % 32;
        return getVal(bucketIdx, bitIdx);
    }

    void setVal(int bucket, int loc, boolean val) {
        if (val) {
            int u = bs[bucket] | (1 << loc);
            bs[bucket] = u;
        } else {
            int u = bs[bucket] & ~(1 << loc);
            bs[bucket] = u;
        }
    }

    boolean getVal(int bucket, int loc) {
        int u = (bs[bucket] >> loc) & 1;
        return u == 1;
    }
}
```
* 时间复杂度:$O(1)$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


Question:
不使用任何内建的哈希表库设计一个哈希映射(HashMap)。

实现 MyHashMap 类:

MyHashMap() 用空映射初始化对象
void put(int key, int value) 向 HashMap 插入一个键值对 (key, value) 。如果 key 已经存在于映射中,则更新其对应的值 value 。
int get(int key) 返回特定的 key 所映射的 value ;如果映射中不包含 key 的映射,返回 -1 。
void remove(key) 如果映射中存在 key 的映射,则移除 key 和它所对应的 value 。

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

解释:
MyHashMap myHashMap = new MyHashMap();
myHashMap.put(1, 1); // myHashMap 现在为 [[1,1]]
myHashMap.put(2, 2); // myHashMap 现在为 [[1,1], [2,2]]
myHashMap.get(1);    // 返回 1 ,myHashMap 现在为 [[1,1], [2,2]]
myHashMap.get(3);    // 返回 -1(未找到),myHashMap 现在为 [[1,1], [2,2]]
myHashMap.put(2, 1); // myHashMap 现在为 [[1,1], [2,1]](更新已有的值)
myHashMap.get(2);    // 返回 1 ,myHashMap 现在为 [[1,1], [2,1]]
myHashMap.remove(2); // 删除键为 2 的数据,myHashMap 现在为 [[1,1]]
myHashMap.get(2);    // 返回 -1(未找到),myHashMap 现在为 [[1,1]]
```

提示:
* 0 <= key, value <= $10^6$
* 最多调用 $10^4$ 次 put、get 和 remove 方法

Solution:
### 简单数组解法

与昨天的 [705. 设计哈希集合](https://leetcode-cn.com/problems/design-hashset/solution/yi-ti-san-jie-jian-dan-shu-zu-lian-biao-nj3dg/) 不同。

我们不仅仅需要记录一个元素存在与否,还需要记录该元素对应的值是什么。

由于题目限定了数据范围 $0 <= key, value <= 10^6$,和 kv 的数据类型。

我们可以使用 int 类型的数组实现哈希表功能。

```java
class MyHashMap {
    int INF = Integer.MAX_VALUE;
    int N = 1000009;
    int[] map = new int[N];
    public MyHashMap() {
        Arrays.fill(map, INF);
    }

    public void put(int key, int value) {
        map[key] = value;
    }

    public int get(int key) {
        return map[key] == INF ? -1 : map[key];
    }

    public void remove(int key) {
        map[key] = INF;
    }
}
```
* 时间复杂度:$O(1)$
* 空间复杂度:$O(1)$

***

### 链表解法

与 [705. 设计哈希集合](https://leetcode-cn.com/problems/design-hashset/solution/yi-ti-san-jie-jian-dan-shu-zu-lian-biao-nj3dg/) 同理,我们可以利用「链表」来构建 Map,这也是工程上最简单的一种实现方式。

```java
class MyHashMap {
    static class Node {
        int key, value;
        Node next;
        Node(int _key, int _value) {
            key = _key;
            value = _value;
        }
    }

    // 由于使用的是「链表」,这个值可以取得很小
    Node[] nodes = new Node[10009];

    public void put(int key, int value) {
        // 根据 key 获取哈希桶的位置
        int idx = getIndex(key);
        // 判断链表中是否已经存在
        Node loc = nodes[idx], tmp = loc;
        if (loc != null) {
            Node prev = null;
            while (tmp != null) {
                if (tmp.key == key) {
                    tmp.value = value;
                    return;
                }
                prev = tmp;
                tmp = tmp.next;
            }
            tmp = prev;
        }
        Node node = new Node(key, value);

        // 头插法
        // node.next = loc;
        // nodes[idx] = node;

        // 尾插法
        if (tmp != null) {
            tmp.next = node;
        } else {
            nodes[idx] = node;
        }
    }

    public void remove(int key) {
        int idx = getIndex(key);
        Node loc = nodes[idx];
        if (loc != null) {
            Node prev = null;
            while (loc != null) {
                if (loc.key == key) {
                    if (prev != null) {
                        prev.next = loc.next;
                    } else {
                        nodes[idx] = loc.next;
                    }
                    return;
                }
                prev = loc;
                loc = loc.next;
            }
        }
    }

    public int get(int key) {
        int idx = getIndex(key);
        Node loc = nodes[idx];
        if (loc != null) {
            while (loc != null) {
                if (loc.key == key) {
                    return loc.value;
                }
                loc = loc.next;
            }
        }
        return -1;
    }

    int getIndex(int key) {
        // 因为 nodes 的长度只有 10009,对应的十进制的 10011100011001(总长度为 32 位,其余高位都是 0)
        // 为了让 key 对应的 hash 高位也参与运算,这里对 hashCode 进行右移异或
        // 使得 hashCode 的高位随机性和低位随机性都能体现在低 16 位中
        int hash = Integer.hashCode(key);
        hash ^= (hash >>> 16);
        return hash % nodes.length;
    }
}
```
* 时间复杂度:由于没有扩容的逻辑,最坏情况下复杂度为 $O(n)$,一般情况下复杂度为 $O(1)$
* 空间复杂度:$O(1)$

***

### 开放寻址解法

除了使用「链表」来解决哈希冲突以外,还能使用「开放寻址法」来解决。

```java
class MyHashMap {
    static class Node {
        int key, value;
        Node next;
        boolean isDeleted;
        Node(int _key, int _value) {
            key = _key;
            value = _value;
        }
    }

    // 冲突时的偏移量
    int OFFSET = 1;
    Node[] nodes = new Node[10009];

    public void put(int key, int value) {
        int idx = getIndex(key);
        Node node = nodes[idx];
        if (node != null) {
            node.value = value;
            node.isDeleted = false;
        } else {
            node = new Node(key, value);
            nodes[idx] = node;
        }
    }

    public void remove(int key) {
        Node node = nodes[getIndex(key)];
        if (node != null) node.isDeleted = true;
    }

    public int get(int key) {
        Node node = nodes[getIndex(key)];
        if (node == null) return -1;
        return node.isDeleted ? -1 : node.value;
    }

    // 当 map 中没有 key 的时候,getIndex 总是返回一个空位置
    // 当 map 中包含 key 的时候,getIndex 总是返回 key 所在的位置
    int getIndex(int key) {
        int hash = Integer.hashCode(key);
        hash ^= (hash >>> 16);
        int n = nodes.length;
        int idx = hash % n;
        while (nodes[idx] != null && nodes[idx].key != key) {
            hash += OFFSET;
            idx = hash % n;
        }
        return idx;
    }
}
```
* 时间复杂度:一般情况下复杂度为 $O(1)$,极端情况下为 $O(n)$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


Question:
设计链表的实现。您可以选择使用单链表或双链表。

单链表中的节点应该具有两个属性:`val` 和 `next`。`val` 是当前节点的值,`next` 是指向下一个节点的指针/引用。

如果要使用双向链表,则还需要一个属性 `prev` 以指示链表中的上一个节点。假设链表中的所有节点都是 `0-index` 的。

在链表类中实现这些功能:

* `get(index)`:获取链表中第 `index` 个节点的值。如果索引无效,则返回`-1`。
* `addAtHead(val)`:在链表的第一个元素之前添加一个值为 `val` 的节点。插入后,新节点将成为链表的第一个节点。
* `addAtTail(val)`:将值为 `val` 的节点追加到链表的最后一个元素。
* `addAtIndex(index,val)`:在链表中的第 `index` 个节点之前添加值为 `val`  的节点。如果 `index` 等于链表的长度,则该节点将附加到链表的末尾。如果 `index` 大于链表长度,则不会插入节点。如果 `index` 小于 `0`,则在头部插入节点。
* `deleteAtIndex(index)`:如果索引 `index` 有效,则删除链表中的第 `index` 个节点。

示例:
```
MyLinkedList linkedList = new MyLinkedList();
linkedList.addAtHead(1);
linkedList.addAtTail(3);
linkedList.addAtIndex(1,2);   //链表变为1-> 2-> 3
linkedList.get(1);            //返回2
linkedList.deleteAtIndex(1);  //现在链表是1-> 3
linkedList.get(1);            //返回3
```

提示:
* 所有 `val` 值都在 $[1, 1000]$ 之内。
* 操作次数将在  $[1, 1000]$ 之内。
* 请不要使用内置的 `LinkedList` 库。

Solution:
### 双向链表

一道手写链表裸题(啊嘿,$9$ 月活动的同学表示很棒 🤣

由于需要涉及部分 `index` 相关操作,因此我们可以实现一个「双向链表」,同时使用变量 `sz` 记录下当前链表的总长度,这样在涉及 `index` 操作时,可从较近的一边出发遍历,剩下的则是常规的链表节点操作。

> 一些细节:所有的链表题目我们都可以引入前后哨兵来简化边界处理;同时能写双链表自然能写单链表,因此如果你没有顺利写出双链表的话,在看了题解写完双链表后,再手写一遍单链表作为练习。

Java 代码:
```Java
class MyLinkedList {
    class Node {
        Node prev, next;
        int val;
        Node (int _val) {
            val = _val;
        }
    }
    Node he = new Node(-1), ta = new Node(-1);
    int sz = 0;
    public MyLinkedList() {
        he.next = ta; ta.prev = he;
    }
    public int get(int index) {
        Node node = getNode(index);
        return node == null ? -1 : node.val;
    }
    public void addAtHead(int val) {
        Node node = new Node(val);
        node.next = he.next; node.prev = he;
        he.next.prev = node; he.next = node;
        sz++;
    }
    public void addAtTail(int val) {
        Node node = new Node(val);
        node.prev = ta.prev; node.next = ta;
        ta.prev.next = node; ta.prev = node;
        sz++;
    }
    public void addAtIndex(int index, int val) {
        if (index > sz) return ;
        if (index <= 0) {
            addAtHead(val);
        } else if (index == sz) {
            addAtTail(val);
        } else {
            Node node = new Node(val), cur = getNode(index);
            node.next = cur; node.prev = cur.prev;
            cur.prev.next = node; cur.prev = node;
            sz++;
        }
    }
    public void deleteAtIndex(int index) {
        Node cur = getNode(index);
        if (cur == null) return ;
        cur.next.prev = cur.prev;
        cur.prev.next = cur.next;
        sz--;
    }
    Node getNode(int index) {
        boolean isLeft = index < sz / 2;
        if (!isLeft) index = sz - index - 1;
        for (Node cur = isLeft ? he.next : ta.prev; cur != ta && cur != he; cur = isLeft ? cur.next : cur.prev) {
            if (index-- == 0) return cur;
        }
        return null;
    }
}
```
TypeScript 代码:
```TypeScript
class TNode {
    prev: TNode
    next: TNode
    val: number
    constructor(_val: number) {
        this.val = _val
    }
}
class MyLinkedList {
    he = new TNode(-1)
    ta = new TNode(-1)
    sz = 0
    constructor() {
        this.he.next = this.ta; this.ta.prev = this.he
    }
    get(index: number): number {
        const node = this.getNode(index)
        return node == null ? -1 : node.val
    }
    addAtHead(val: number): void {
        const node = new TNode(val)
        node.next = this.he.next; node.prev = this.he
        this.he.next.prev = node; this.he.next = node
        this.sz++
    }
    addAtTail(val: number): void {
        const node = new TNode(val)
        node.prev = this.ta.prev; node.next = this.ta
        this.ta.prev.next = node; this.ta.prev = node
        this.sz++
    }
    addAtIndex(index: number, val: number): void {
        if (index > this.sz) return
        if (index <= 0) {
            this.addAtHead(val)
        } else if (index == this.sz) {
            this.addAtTail(val)
        } else {
            const node = new TNode(val), cur = this.getNode(index)
            node.next = cur; node.prev = cur.prev
            cur.prev.next = node; cur.prev = node
            this.sz++
        }
    }
    deleteAtIndex(index: number): void {
        const cur = this.getNode(index)
        if (cur == null) return
        cur.prev.next = cur.next
        cur.next.prev = cur.prev
        this.sz--
    }
    getNode(index: number): TNode | null {
        const isLeft = index < this.sz / 2
        if (!isLeft) index = this.sz - index - 1
        for (let cur = isLeft ? this.he.next : this.ta.prev; cur != this.ta && cur != this.he; cur = isLeft ? cur.next : cur.prev) {
            if (index-- == 0) return cur
        }
        return null
    }
}
```
Python 代码:
```Python
class Node:
    def __init__(self, _val):
        self.val = _val
        self.prev = None
        self.next = None

class MyLinkedList:
    def __init__(self):
        self.he = Node(-1)
        self.ta = Node(-1)
        self.he.next = self.ta
        self.ta.prev = self.he
        self.sz = 0

    def get(self, index: int) -> int:
        node = self.getNode(index)
        return node.val if node else -1

    def addAtHead(self, val: int) -> None:
        node = Node(val)
        node.next = self.he.next
        node.prev = self.he
        self.he.next.prev = node
        self.he.next = node
        self.sz += 1

    def addAtTail(self, val: int) -> None:
        node = Node(val)
        node.prev = self.ta.prev
        node.next = self.ta
        self.ta.prev.next = node
        self.ta.prev = node
        self.sz += 1

    def addAtIndex(self, index: int, val: int) -> None:
        if index > self.sz:
            return
        if index <= 0:
            self.addAtHead(val)
        elif index == self.sz:
            self.addAtTail(val)
        else:
            node, cur = Node(val), self.getNode(index)
            node.next = cur
            node.prev = cur.prev
            cur.prev.next = node
            cur.prev = node
            self.sz += 1

    def deleteAtIndex(self, index: int) -> None:
        node = self.getNode(index)
        if node:
            node.prev.next = node.next
            node.next.prev = node.prev
            self.sz -= 1

    def getNode(self, index: int) -> Node | None:
        isLeft = index < self.sz / 2
        if not isLeft:
            index = self.sz - index - 1
        cur = self.he.next if isLeft else self.ta.prev
        while cur != self.he and cur != self.ta:
            if index == 0:
                return cur
            index -= 1
            cur = cur.next if isLeft else cur.prev
        return None
```
* 时间复杂度:涉及 `index` 的相关操作复杂度为 $O(index)$;其余操作均为 $O(1)$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


709. 转换成小写字母

Question:
给你一个字符串 `s` ,将该字符串中的大写字母转换成相同的小写字母,返回新的字符串。

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

输出:"hello"
```
示例 2:
```
输入:s = "here"

输出:"here"
```
示例 3:
```
输入:s = "LOVELY"

输出:"lovely"
```

提示:
* `1 <= s.length <= 100`
* `s` 由 `ASCII` 字符集中的可打印字符组成

Solution:
### 模拟

根据题意进行模拟。

代码:
```Java
class Solution {
    public String toLowerCase(String s) {
        return s.toLowerCase();
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


710. 黑名单中的随机数

Question:
给定一个整数 `n` 和一个 无重复 黑名单整数数组 `blacklist`。

设计一种算法,从 $[0, n - 1]$ 范围内的任意整数中选取一个 未加入 黑名单 `blacklist` 的整数。任何在上述范围内且不在黑名单 `blacklist` 中的整数都应该有 同等的可能性 被返回。

优化你的算法,使它最小化调用语言 内置 随机函数的次数。

实现 `Solution` 类:
* `Solution(int n, int[] blacklist)` 初始化整数 `n` 和被加入黑名单 `blacklist` 的整数
* `int pick()` 返回一个范围为 $[0, n - 1]$ 且不在黑名单 `blacklist` 中的随机整数

示例 1:
```
输入
["Solution", "pick", "pick", "pick", "pick", "pick", "pick", "pick"]
[[7, [2, 3, 5]], [], [], [], [], [], [], []]

输出
[null, 0, 4, 1, 6, 1, 0, 4]

解释
Solution solution = new Solution(7, [2, 3, 5]);
solution.pick(); // 返回0,任何[0,1,4,6]的整数都可以。注意,对于每一个pick的调用,
                 // 0、1、4和6的返回概率必须相等(即概率为1/4)。
solution.pick(); // 返回 4
solution.pick(); // 返回 1
solution.pick(); // 返回 6
solution.pick(); // 返回 1
solution.pick(); // 返回 0
solution.pick(); // 返回 4
```

提示:
* $1 <= n <= 10^9$
* $0 <= blacklist.length <= \min(1065, n - 1)$
* $0 <= blacklist[i] < n$
* `blacklist` 中所有值都 不同
*  `pick` 最多被调用 $2 \times 10^4$ 次

Solution:
### 前缀和 + 二分

为了方便,我们记 `blacklist` 为 `bs`,将其长度记为 `m`。

问题本质是让我们从 $[0, n - 1]$ 范围内随机一个数,这数不能在 `bs` 里面。

由于 $n$ 的范围是 $1e9$,我们不能在对范围在 $[0, n - 1]$ 且不在 `bs` 中的点集进行离散化,因为离散化后的点集大小仍很大。

同时 $m$ 的范围是 $1e5$,我们也不能使用普通的拒绝采样做法,这样单次 `pick` 被拒绝的次数可能很大。

一个简单且绝对正确的做法是:**我们不对「点」做离散化,而利用 `bs` 数据范围为 $1e5$,来对「线段」做离散化**。

具体的,我们先对 `bs` 进行排序,然后从前往后处理所有的 $bs[i]$,将相邻 $bs[i]$ 之间的能被选择的「线段」以二元组 $(a, b)$ 的形式进行记录(即一般情况下的 $a = bs[i - 1] + 1$,$b = bs[i] - 1$),存入数组 `list` 中(注意特殊处理一下两端的线段)。

当处理完所有的 $bs[i]$ 后,我们得到了所有可被选择线段,同时对于每个线段可直接算得其所包含的整数点数。

我们可以对 `list` 数组做一遍「线段所包含点数」的「前缀和」操作,得到 `sum` 数组,同时得到所有线段所包含的总点数(前缀和数组的最后一位)。

对于 `pick` 操作而言,我们先在 $[1, tot]$ 范围进行随机(其中 $tot$ 代表总点数),假设取得的随机值为 $val$,然后在前缀和数组中进行二分,找到第一个满足「值大于等于 $val$」的位置(含义为找到这个点所在的线段),然后再利用该线段的左右端点的值,取出对应的点。

代码:
```Java
class Solution {
    List<int[]> list = new ArrayList<>();
    int[] sum = new int[100010];
    int sz;
    Random random = new Random();
    public Solution(int n, int[] bs) {
        Arrays.sort(bs);
        int m = bs.length;
        if (m == 0) {
            list.add(new int[]{0, n - 1});
        } else {
            if (bs[0] != 0) list.add(new int[]{0, bs[0] - 1});
            for (int i = 1; i < m; i++) {
                if (bs[i - 1] == bs[i] - 1) continue;
                list.add(new int[]{bs[i - 1] + 1, bs[i] - 1});
            }
            if (bs[m - 1] != n - 1) list.add(new int[]{bs[m - 1] + 1, n - 1});
        }
        sz = list.size();
        for (int i = 1; i <= sz; i++) {
            int[] info = list.get(i - 1);
            sum[i] = sum[i - 1] + info[1] - info[0] + 1;
        }
    }
    public int pick() {
        int val = random.nextInt(sum[sz]) + 1;
        int l = 1, r = sz;
        while (l < r) {
            int mid = l + r >> 1;
            if (sum[mid] >= val) r = mid;
            else l = mid + 1;
        }
        int[] info = list.get(r - 1);
        int a = info[0], b = info[1], end = sum[r];
        return b - (end - val);
    }
}
```
* 时间复杂度:在初始化操作中:对 `bs` 进行排序复杂度为 $O(m\log{m})$;统计所有线段复杂度为 $O(m)$;对所有线段求前缀和复杂度为 $O(m)$。在 `pick` 操作中:随机后会对所有线段做二分,复杂度为 $O(\log{m})$
* 空间复杂度:$O(m)$

---

### 哈希表

总共有 $n$ 个数,其中 $m$ 个数不可被选,即真实可选个数为 $n - m$ 个。

为了能够在 $[0, n - m)$ 连续段内进行随机,我们可以将 $[0, n - m)$ 内不可被选的数映射到 $[n - m, n - 1]$ 内可选的数上。

具体的,我们可以使用两个 `Set` 结构 `s1` 和 `s2` 分别记录在 $[0, n - m)$ 和 $[n - m, n - 1]$ 范围内的黑名单数字。

在 `pick` 操作时,我们在 $[0, n - m)$ 范围内进行随机,根据随机值 $val$ 是否为黑名单内数字(是否在 `s1` 中)进行分情况讨论:

* 随机值 `val` 不在 `s1` 中,说明 `val` 是真实可选的数值,直接返回;
* 随机值 `val` 在 `s1` 中,说明 `val` 是黑名单内的数值,我们先查询是否已存在 `val` 的映射记录,若已存在直接返回其映射值;否则需要在 $[n - m, n - 1]$ 内找到一个可替代它的数值,我们可以使用一个变量 `idx` 在 $[n- m, n - 1]$ 范围内从前往后扫描,找到第一个未被使用的,同时不在 `s2` 中(不在黑名单内)的数字,假设找到的值为 `x`,将 `x` 进行返回(同时将 `val` 与 `x` 的映射关系,用哈希表进行记录)。

代码:
```Java
class Solution {
    int n, m, idx;
    Random random = new Random();
    Set<Integer> s1 = new HashSet<>(), s2 = new HashSet<>();
    Map<Integer, Integer> map = new HashMap<>();
    public Solution(int _n, int[] bs) {
        n = _n; m = bs.length;
        int max = n - m;
        for (int x : bs) {
            if (x < max) s1.add(x);
            else s2.add(x);
        }
        idx = n - m;
    }
    public int pick() {
        int val = random.nextInt(n - m);
        if (!s1.contains(val)) return val;
        if (!map.containsKey(val)) {
            while (s2.contains(idx)) idx++;
            map.put(val, idx++);
        }
        return map.get(val);
    }
}
```
* 时间复杂度:初始化操作复杂度为 $O(m)$,`pick` 操作复杂度为 $O(1)$
* 空间复杂度:$O(m)$

### 最后

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

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

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

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


Question:
给你一个字符串 `path`,表示指向某一文件或目录的 `Unix` 风格 绝对路径 (以 `'/'` 开头),请你将其转化为更加简洁的规范路径。

在 `Unix` 风格的文件系统中,一个点(`.`)表示当前目录本身;此外,两个点 (`..`) 表示将目录切换到上一级(指向父目录);两者都可以是复杂相对路径的组成部分。任意多个连续的斜杠(即,`'//'`)都被视为单个斜杠 `'/'` 。 对于此问题,任何其他格式的点(例如,`'...'`)均被视为文件`/`目录名称。

请注意,返回的 规范路径 必须遵循下述格式:

* 始终以斜杠 `'/'` 开头。
* 两个目录名之间必须只有一个斜杠 `'/'` 。
* 最后一个目录名(如果存在)不能 以 `'/'` 结尾。
* 此外,路径仅包含从根目录到目标文件或目录的路径上的目录(即,不含 '`.'` 或 `'..'`)。

返回简化后得到的 **规范路径**。

示例 1:
```
输入:path = "/home/"

输出:"/home"

解释:注意,最后一个目录名后面没有斜杠。
```
示例 2:
```
输入:path = "/../"

输出:"/"

解释:从根目录向上一级是不可行的,因为根目录是你可以到达的最高级。
```
示例 3:
```
输入:path = "/home//foo/"

输出:"/home/foo"

解释:在规范路径中,多个连续斜杠需要用一个斜杠替换。
```
示例 4:
```
输入:path = "/a/./b/../../c/"

输出:"/c"
```

提示:
* $1 <= path.length <= 3000$
* `path` 由英文字母,数字,`'.'`,`'/'` 或 `'_'` 组成。
* `path` 是一个有效的 `Unix` 风格绝对路径。

Solution:
### 模拟

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

具体的,从前往后处理 `path`,每次以 `item` 为单位进行处理(有效的文件名),根据 `item` 为何值进行分情况讨论:

* `item` 为有效值 :存入栈中;
* `item` 为 `..` :弹出栈顶元素(若存在);
* `item` 为 `.` :不作处理。

代码:
```Java
class Solution {
    public String simplifyPath(String path) {
        Deque<String> d = new ArrayDeque<>();
        int n = path.length();
        for (int i = 1; i < n; ) {
            if (path.charAt(i) == '/' && ++i >= 0) continue;
            int j = i + 1;
            while (j < n && path.charAt(j) != '/') j++;
            String item = path.substring(i, j);
            if (item.equals("..")) {
                if (!d.isEmpty()) d.pollLast();
            } else if (!item.equals(".")) {
                d.addLast(item);
            }
            i = j;
        }
        StringBuilder sb = new StringBuilder();
        while (!d.isEmpty()) sb.append("/" + d.pollFirst());
        return sb.length() == 0 ? "/" : sb.toString();
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


Question:
给定一个 $m \times n$ 的矩阵,如果一个元素为 $0$ ,则将其所在行和列的所有元素都设为 $0$ 。

请使用「原地」算法。

进阶:
* 一个直观的解决方案是使用  $O(m \times n)$ 的额外空间,但这并不是一个好的解决方案。
* 一个简单的改进方案是使用 $O(m + n)$ 的额外空间,但这仍然不是最好的解决方案。
* 你能想出一个仅使用常量空间的解决方案吗?

示例 1:

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

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

```
输入:matrix = [[0,1,2,0],[3,4,5,2],[1,3,1,5]]

输出:[[0,0,0,0],[0,4,5,0],[0,3,1,0]]
```

提示:
* $m == matrix.length$
* $n == matrix[0].length$
* $1 <= m, n <= 200$
* $-2^{31} <= matrix[i][j] <= 2^{31} - 1$

Solution:
### 前言

由于 $O(m \times n)$ 和 $O(m+n)$ 空间的解法都十分简单,无非是「同等大小的矩阵」或「与行列数量相等的标识」来记录置零信息。

这里着重讲解利用原矩阵的 $O(1)$ 空间解法。

---

### O(1) 空间

**1. 使用两个变量(r0 & c0),记录「首行 & 首列」是否该被置零**

**2.「非首行首列」的位置**
* 将置零信息存储到原矩阵
* 根据置零信息,置零「非首行首列」的位置

**3. 使用 r0 & c0 ,置零「首行 & 首列」**

代码:
```Java
class Solution {
    public void setZeroes(int[][] mat) {
        int m = mat.length, n = mat[0].length;
        // 1. 扫描「首行」和「首列」记录「首行」和「首列」是否该被置零
        boolean r0 = false, c0 = false;
        for (int i = 0; i < m; i++) {
            if (mat[i][0] == 0) {
                r0 = true;
                break;
            }
        }
        for (int j = 0; j < n; j++) {
            if (mat[0][j] == 0) {
                c0 = true;
                break;
            }
        }
        // 2.1 扫描「非首行首列」的位置,如果发现零,将需要置零的信息存储到该行的「最左方」和「最上方」的格子内
        for (int i = 1; i < m; i++) {
            for (int j = 1; j < n; j++) {
                if (mat[i][j] == 0) mat[i][0] = mat[0][j] = 0;
            }
        }
        // 2.2 根据刚刚记录在「最左方」和「最上方」格子内的置零信息,进行「非首行首列」置零
        for (int j = 1; j < n; j++) {
            if (mat[0][j] == 0) {
                for (int i = 1; i < m; i++) mat[i][j] = 0;
            }
        }
        for (int i = 1; i < m; i++) {
            if (mat[i][0] == 0) Arrays.fill(mat[i], 0);
        }
        // 3. 根据最开始记录的「首行」和「首列」信息,进行「首行首列」置零
        if (r0) for (int i = 0; i < m; i++) mat[i][0] = 0;
        if (c0) Arrays.fill(mat[0], 0);
    }
}
```
* 时间复杂度:$O(n \times m)$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


Question:
编写一个高效的算法来判断 m x n 矩阵中,是否存在一个目标值。该矩阵具有如下特性:

每行中的整数从左到右按升序排列。
每行的第一个整数大于前一行的最后一个整数。

示例 1:

```
输入:matrix = [[1,3,5,7],[10,11,16,20],[23,30,34,60]], target = 3

输出:true
```
示例 2:

```
输入:matrix = [[1,3,5,7],[10,11,16,20],[23,30,34,60]], target = 13

输出:false
```

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

Solution:
### 二分解法(一)

由于二维矩阵固定列的「从上到下」或者固定行的「从左到右」都是升序的。

因此我们可以使用两次二分来定位到目标位置:

1. 第一次二分:从第 0 列中的「所有行」开始找,找到合适的行 `row`

2. 第二次二分:从 `row` 中「所有列」开始找,找到合适的列 `col`

代码:
```Java
class Solution {
    public boolean searchMatrix(int[][] mat, int t) {
        int m = mat.length, n = mat[0].length;

        // 第一次二分:定位到所在行(从上往下,找到最后一个满足 mat[x]][0] <= t 的行号)
        int l = 0, r = m - 1;
        while (l < r) {
            int mid = l + r + 1 >> 1;
            if (mat[mid][0] <= t) {
                l = mid;
            } else {
                r = mid - 1;
            }
        }

        int row = r;
        if (mat[row][0] == t) return true;
        if (mat[row][0] > t) return false;

        // 第二次二分:从所在行中定位到列(从左到右,找到最后一个满足 mat[row][x] <= t 的列号)
        l = 0; r = n - 1;
        while (l < r) {
            int mid = l + r + 1 >> 1;
            if (mat[row][mid] <= t) {
                l = mid;
            } else {
                r = mid - 1;
            }
        }
        int col = r;

        return mat[row][col] == t;
    }
}
```
* 时间复杂度:$O(\log{m} + \log{n})$
* 空间复杂度:$O(1)$

***

### 二分解法(二)

当然,因为将二维矩阵的行尾和行首连接,也具有单调性。

我们可以将「二维矩阵」当做「一维矩阵」来做。

代码:
```Java
class Solution {
    public boolean searchMatrix(int[][] mat, int t) {
        int m = mat.length, n = mat[0].length;
        int l = 0, r = m * n - 1;
        while (l < r) {
            int mid = l + r + 1 >> 1;
            if (mat[mid / n][mid % n] <= t) {
                l = mid;
            } else {
                r = mid - 1;
            }
        }
        return mat[r / n][r % n] == t;
    }
}
```
* 时间复杂度:$O(\log{(m * n)})$
* 空间复杂度:$O(1)$

***

### 抽象 BST 解法

我们可以将二维矩阵抽象成「以右上角为根的 BST」:

那么我们可以从根(右上角)开始搜索,如果当前的节点不等于目标值,可以按照树的搜索顺序进行:

1. 当前节点「大于」目标值,搜索当前节点的「左子树」,也就是当前矩阵位置的「左方格子」,即 y--
2. 当前节点「小于」目标值,搜索当前节点的「右子树」,也就是当前矩阵位置的「下方格子」,即 x++

代码:
```Java
class Solution {
    int m, n;
    public boolean searchMatrix(int[][] mat, int t) {
        m = mat.length; n = mat[0].length;
        int x = 0, y = n - 1;
        while (check(x, y) && mat[x][y] != t) {
            if (mat[x][y] > t) {
                y--;
            } else {
                x++;
            }
        }
        return check(x, y) && mat[x][y] == t;
    }
    boolean check(int x, int y) {
        return x >= 0 && x < m && y >= 0 && y < n;
    }
}
```
* 时间复杂度:$O(m+n)$
* 空间复杂度:$O(1)$

***

### 拓展

如果你掌握了上述解法的话,你还可以试试这题:

[240. 搜索二维矩阵 II](https://leetcode-cn.com/problems/search-a-2d-matrix-ii/)

### 最后

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

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

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

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


Question:
给定一个包含红色、白色和蓝色、共 `n` 个元素的数组 `nums`,原地对它们进行排序,使得相同颜色的元素相邻,并按照红色、白色、蓝色顺序排列。

我们使用整数 `0`、 `1` 和 `2` 分别表示红色、白色和蓝色。

必须在不使用库的 `sort` 函数的情况下解决这个问题。

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

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

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

提示:
* $n = nums.length$
* $1 <= n <= 300$
* `nums[i]` 为 `0`、`1` 或 `2`

进阶:
* 你可以不使用代码库中的排序函数来解决这道题吗?
* 你能想出一个仅使用常数空间的一趟扫描算法吗?

Solution:
### 双指针

该题为经典的荷兰国旗问题,由于题目本质是要我们将数分成三段。

因此除了使用一个变量 `idx` 代指处理到哪一个 $nums[i]$ 以外,至少还需要两个变量来代指三段的边界:

* 变量 `l` 为下一个填入 `0` 的位置(因此范围 $[0, l - 1]$ 均为 `0`,初始化 $l = 0$,代表空集)
* 变量 `r` 为下一个填入 `2` 的位置(因此范围 $[r + 1, n - 1]$ 均为 `2`,初始化 $r = n - 1$,代表空集)

由于 $[0, idx - 1]$ 均为处理过的数值(即 `0` 和 `2` 必然都被分到了两端),同时 $l - 1$ 又是 `0` 的右边界,因此 $[l, idx - 1]$ 为 `1` 的区间,而 $[idx, r]$ 为未处理的数值。

上述几个变量的定义是该题唯一需要理清的地方。

不失一般性,根据当前处理到的 $nums[idx]$ 进行分情况讨论:

* $nums[idx] = 0$:此时将其与位置 `l` 进行互换(记住 `l` 为下一个待填入 `0` 的位置,同时 $[l, idx - 1]$ 为 `1` 的区间),本质是将 $nums[idx]$ 的 `0` 和 $nums[l]$ 的 `1` 进行互换,因此互换后将 `l` 和 `idx` 进行右移;
* $nums[idx] = 1$:由于 $[l, idx - 1]$ 本身就是 `1` 的区间,直接将 `idx` 进行右移即可;
* $nums[idx] = 2$:此时将其与位置 `r` 进行互换(`r` 为下一个待填入 `2` 的位置,但 $[idx, r]$ 为未处理区间),也就是我们互换后,只能明确换到位置 $nums[r]$ 的位置为 `2`,可以对 `r` 进行左移,但不确定新 $nums[idx]$ 为何值,因此保持 `idx` 不变再入循环判断。

最后当 $idx > r$(含义为未处理区间为空集),整个分三段过程结束。

Java 代码:
```Java
class Solution {
    public void sortColors(int[] nums) {
        int n = nums.length;
        int l = 0, r = n - 1, idx = 0;
        while (idx <= r) {
            if (nums[idx] == 0) swap(nums, l++, idx++);
            else if (nums[idx] == 1) idx++;
            else swap(nums, idx, r--);
        }
    }
    void swap(int[] nums, int i, int j) {
        int c = nums[i];
        nums[i] = nums[j];
        nums[j] = c;
    }
}
```
TypeScript 代码:
```TypeScript
function sortColors(nums: number[]): void {
    const n = nums.length
    let l = 0, r = n - 1, idx = 0
    while (idx <= r) {
        if (nums[idx] == 0) swap(nums, l++, idx++)
        else if (nums[idx] == 1) idx++
        else swap(nums, idx, r--)
    }
}
function swap(nums: number[], i: number, j: number): void {
    const c = nums[i]
    nums[i] = nums[j]
    nums[j] = c
}
```
Python 代码:
```Python
class Solution:
    def sortColors(self, nums: List[int]) -> None:
        n = len(nums)
        l, r, idx = 0, n - 1, 0
        while idx <= r:
            if nums[idx] == 0:
                nums[l], nums[idx] = nums[idx], nums[l]
                l += 1
                idx += 1
            elif nums[idx] == 1:
                idx += 1
            else:
                nums[r], nums[idx] = nums[idx], nums[r]
                r -= 1
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


Question:
给你一个字符串 `s`、一个字符串 `t`。返回 `s` 中涵盖 `t` 所有字符的最小子串。

如果 `s` 中不存在涵盖 `t` 所有字符的子串,则返回空字符串 `""`。

注意:
* 对于 `t` 中重复字符,我们寻找的子字符串中该字符数量必须不少于 `t` 中该字符数量。
* 如果 `s` 中存在这样的子串,我们保证它是唯一的答案。

示例 1:
```
输入:s = "ADOBECODEBANC", t = "ABC"

输出:"BANC"

解释:最小覆盖子串 "BANC" 包含来自字符串 t 的 'A'、'B' 和 'C'。
```
示例 2:
```
输入:s = "a", t = "a"

输出:"a"

解释:整个字符串 s 是最小覆盖子串。
```
示例 3:
```
输入: s = "a", t = "aa"

输出: ""

解释: t 中两个字符 'a' 均应包含在 s 的子串中,
因此没有符合条件的子字符串,返回空字符串。
```

提示:
* `m == s.length`
* `n == t.length`
* $1 <= m, n <= 10^5$
* `s` 和 `t` 由英文字母组成

进阶:你能设计一个在 $O(m+n)$ 时间内解决此问题的算法吗?

Solution:
### 滑动窗口

定义两个长度为 $60$(足够存下所有字母种类)的数组 `c1` 和 `c2`,用于存储字符频率。其中 `c1` 用于记录字符串 `t` 中字符的频率,`c2` 用于记录当前滑动窗口内字符的频率。

设定好字母与频率数组下标的映射关系:小写字母 `a-z` 对应下标 `0-25`,大写字母 `A-Z` 对应下标 `26-51`。

使用变量 `tot` 来记录还需要匹配的字符种类数,当 `tot = 0` 代表当前滑动窗口对应的子串能够实现对 `t` 的覆盖,即任意字符满足 $c2[i] \geq c1[i]$。

使用双指针 `j` 和 `i` 表示滑动窗口的左右边界。从前往后遍历字符串 `s`,在每个位置上更新字符频率数组 `c2`。若 `c2` 中字符的频率达到了 `c1` 中的字符频率,则将 `tot` 减 1,表示一个字符已经匹配完成。

每当右边界往后移动一步之后,滑动窗口会增加一个字符。此时我们检查左边界能否右移,同时不会使得 `tot` 变大。即每次右边界右移后,我们检查左边界 $c2[j] > c1[j]$ 是否满足:

* 若满足:说明当前左边界指向字符并非必须,当前子串 $s[j...i]$ 必然不是最短子串。我们让左边界 `j` 进行右移,并重复进行左边界 $c2[j] > c1[j]$ 的检查,直到窗口不能再收缩
* 若不满足:说明当前窗口没有任何一个后缀字符串能够实现对 `t` 的覆盖,我们并不能对窗口实现收缩

每次对窗口移动完成后,我们检查当前 `tot` 是否为 $0$(对字符串 `t` 的覆盖是否完成),若为 $0$ 则尝试用当前窗口对应的字符串 $s[j...i]$ 更新 `ans`。

Java 代码:
```Java
class Solution {
    public String minWindow(String s, String t) {
        int n = s.length(), tot = 0;
        int[] c1 = new int[60], c2 = new int[60];
        for (char x : t.toCharArray()) {
            if (++c1[getIdx(x)] == 1) tot++;
        }
        String ans = "";
        for (int i = 0, j = 0; i < n; i++) {
            int idx1 = getIdx(s.charAt(i));
            if (++c2[idx1] == c1[idx1]) tot--;
            while (j < i) {
                int idx2 = getIdx(s.charAt(j));
                if (c2[idx2] > c1[idx2] && --c2[idx2] >= 0) j++;
                else break;
            }
            if (tot == 0 && (ans.length() == 0 || ans.length() > i - j + 1)) ans = s.substring(j, i + 1);
        }
        return ans;
    }
    int getIdx(char x) {
        return x >= 'A' && x <= 'Z' ? x - 'A' + 26 : x - 'a';
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    string minWindow(string s, string t) {
        int n = s.length(), tot = 0;
        vector<int> c1(60), c2(60);
        for (char x : t) {
            if (++c1[getIdx(x)] == 1) tot++;
        }
        string ans = "";
        for (int i = 0, j = 0; i < n; i++) {
            int idx1 = getIdx(s[i]);
            if (++c2[idx1] == c1[idx1]) tot--;
            while (j < i) {
                int idx2 = getIdx(s[j]);
                if (c2[idx2] > c1[idx2] && --c2[idx2] >= 0) j++;
                else break;
            }
            if (tot == 0 && (ans.empty() || ans.length() > i - j + 1)) ans = s.substr(j, i - j + 1);
        }
        return ans;
    }
    int getIdx(char x) {
        return x >= 'A' && x <= 'Z' ? x - 'A' + 26 : x - 'a';
    }
};
```
Python 代码:
```Python
class Solution:
    def minWindow(self, s: str, t: str) -> str:
        def getIdx(x):
            return ord(x) - ord('A') + 26 if 'A' <= x <= 'Z' else ord(x) - ord('a')

        n, tot = len(s), 0
        c1, c2 = [0] * 60, [0] * 60
        for x in t:
            idx = getIdx(x)
            c1[idx] += 1
            if c1[idx] == 1:
                tot += 1
        ans = ""
        j = 0
        for i in range(n):
            idx1 = getIdx(s[i])
            c2[idx1] += 1
            if c2[idx1] == c1[idx1]:
                tot -= 1
            while j < i:
                idx2 = getIdx(s[j])
                if c2[idx2] > c1[idx2]:
                    j += 1
                    c2[idx2] -= 1
                else:
                    break
            if tot == 0 and (not ans or len(ans) > i - j + 1):
                ans = s[j:i + 1]
        return ans
```
TypeScript 代码:
```TypeScript
function minWindow(s: string, t: string): string {
    let n = s.length, tot = 0;
    const c1 = Array(60).fill(0), c2 = Array(60).fill(0);
    for (const x of t) {
        if (++c1[getIdx(x)] === 1) tot++;
    }
    let ans = "";
    for (let i = 0, j = 0; i < n; i++) {
        const idx1 = getIdx(s[i]);
        if (++c2[idx1] == c1[idx1]) tot--;
        while (j < i) {
            const idx2 = getIdx(s[j]);
            if (c2[idx2] > c1[idx2] && --c2[idx2] >= 0) j++;
            else break;
        }
        if (tot == 0 && (!ans || ans.length > i - j + 1)) ans = s.substring(j, i + 1);
    }
    return ans;
}
function getIdx(x: string): number {
    return x >= 'A' && x <= 'Z' ? x.charCodeAt(0) - 'A'.charCodeAt(0) + 26 : x.charCodeAt(0) - 'a'.charCodeAt(0);
}
```
* 时间复杂度:$O(n + m)$
* 空间复杂度:$O(C)$,其中 $C = 26 \times 2$,为字符集大小

### 最后

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

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

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

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


LeetCode/71-80/80. 删除有序数组中的重复项 II（中等）.md

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

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

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

说明:

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

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

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

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

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

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

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

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

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

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

提示:

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

Solution:
### 通用解法

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

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

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

举个🌰,我们令 `k=2`,假设有如下样例 $[1,1,1,1,1,1,2,2,2,2,2,2,3]$

1. 首先我们先让前 2 位直接保留,得到 1,1
2. 对后面的每一位进行继续遍历,能够保留的前提是与当前位置的前面 `k` 个元素不同(答案中的第一个 1),因此我们会跳过剩余的 1,将第一个 2 追加,得到 1,1,2
3. 继续这个过程,这时候是和答案中的第 2 个 1 进行对比,因此可以得到 1,1,2,2
4. 这时候和答案中的第 1 个 2 比较,只有与其不同的元素能追加到答案,因此剩余的 2 被跳过,3 被追加到答案:1,1,2,2,3

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

---

### 其他

**这是一种针对「数据有序,相同元素保留 `k` 位」问题更加本质的解法,该解法是从性质出发提炼的,利用了「数组有序 & 保留逻辑」两大主要性质。**

当你掌握这种通解之后,要解决 [26. 删除有序数组中的重复项](https://leetcode-cn.com/problems/remove-duplicates-from-sorted-array/) ,只需要改上述代码一个数字即可(将相同数字保留 2 个修改为保留 1 个)。

这种通解最早我也在 [「双指针」&「通用」解法](https://leetcode-cn.com/problems/remove-duplicates-from-sorted-array/solution/shua-chuan-lc-jian-ji-shuang-zhi-zhen-ji-2eg8/) 讲过。

### 最后

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

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

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

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


LeetCode/711-720/713. 乘积小于 K 的子数组（中等）.md

713. 乘积小于 K 的子数组

Question:
给你一个整数数组 `nums` 和一个整数 $k$ ,请你返回子数组内所有元素的乘积严格小于 $k$ 的连续子数组的数目。

示例 1:
```
输入:nums = [10,5,2,6], k = 100

输出:8

解释:8 个乘积小于 100 的子数组分别为:[10]、[5]、[2],、[6]、[10,5]、[5,2]、[2,6]、[5,2,6]。
需要注意的是 [10,5,2] 并不是乘积小于 100 的子数组。
```
示例 2:
```
输入:nums = [1,2,3], k = 0

输出:0
```

提示:
* $1 <= nums.length <= 3 \times 10^4$
* $1 <= nums[i] <= 1000$
* $0 <= k <= 10^6$

Solution:
### 滑动窗口

利用 $1 <= nums[i] <= 1000$,我们可以从前往后处理所有的 $nums[i]$,使用一个变量 $cur$ 记录当前窗口的乘积,使用两个变量 $j$ 和 $i$ 分别代表当前窗口的左右端点。

当 $cur >= k$ 时,我们考虑将左端点 $j$ 右移,同时消除原来左端点元素 $nums[j]$ 对 $cur$ 的贡献,直到 $cur >= k$ 不再满足,这样我们就可以得到每个右端点 $nums[i]$ 的最远左端点 $nums[j]$,从而得知以 $nums[i]$ 为结尾的合法子数组个数为 $i - j + 1$。

Java 代码:
```Java
class Solution {
    public int numSubarrayProductLessThanK(int[] nums, int k) {
        int n = nums.length, ans = 0;
        if (k <= 1) return 0;
        for (int i = 0, j = 0, cur = 1; i < n; i++) {
            cur *= nums[i];
            while (cur >= k) cur /= nums[j++];
            ans += i - j + 1;
        }
        return ans;
    }
}
```
TypeScript 代码:
```TypeScript
function numSubarrayProductLessThanK(nums: number[], k: number): number {
    let n = nums.length, ans = 0
    if (k <= 1) return 0
    for (let i = 0, j = 0, cur = 1; i < n; i++) {
        cur *= nums[i]
        while (cur >= k) cur /= nums[j++]
        ans += i - j + 1
    }
    return ans
};
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


Question:
`Range` 模块是跟踪数字范围的模块。设计一个数据结构来跟踪表示为 半开区间 的范围并查询它们。

半开区间 $[left, right)$ 表示所有 $left <= x < right$ 的实数 `x` 。

实现 `RangeModule` 类:

* `RangeModule()` 初始化数据结构的对象。
* `void addRange(int left, int right)` 添加 半开区间 `[left, right)`,跟踪该区间中的每个实数。添加与当前跟踪的数字部分重叠的区间时,应当添加在区间 `[left, right)` 中尚未跟踪的任何数字到该区间中。
* `boolean queryRange(int left, int right)` 只有在当前正在跟踪区间 `[left, right)` 中的每一个实数时,才返回 `true` ,否则返回 `false` 。
* `void removeRange(int left, int right)` 停止跟踪 半开区间 `[left, right)` 中当前正在跟踪的每个实数。

示例 1:
```
输入
["RangeModule", "addRange", "removeRange", "queryRange", "queryRange", "queryRange"]
[[], [10, 20], [14, 16], [10, 14], [13, 15], [16, 17]]

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

解释
RangeModule rangeModule = new RangeModule();
rangeModule.addRange(10, 20);
rangeModule.removeRange(14, 16);
rangeModule.queryRange(10, 14); 返回 true (区间 [10, 14) 中的每个数都正在被跟踪)
rangeModule.queryRange(13, 15); 返回 false(未跟踪区间 [13, 15) 中像 14, 14.03, 14.17 这样的数字)
rangeModule.queryRange(16, 17); 返回 true (尽管执行了删除操作,区间 [16, 17) 中的数字 16 仍然会被跟踪)
```

提示:
* $1 <= left < right <= 10^9$
* 在单个测试用例中,对 `addRange`、 `queryRange` 和 `removeRange` 的调用总数不超过 $10^4$ 次

Solution:
### 基本分析

线段树(动态开点)写过超多次了。

令 $m$ 为 `addRange`、`queryRange` 和 `removeRange` 的调用总数,$n = 1e9$ 为值域大小。

由于值域过大,我们无法直接使用空间大小固定为 $4 \times n$ 的常规线段树,而要采用「动态开点」的方式,其中动态开点的方式有两种 :「需要进行估点的数组实现」和「无须估点的动态指针」。

设计 `Node` 节点维护什么信息:

* `ls` 和 `rs` 分别指向左右区间子节点(当采用「估点数组」方式时,记录的是左右区间子节点在线段树数组中的下标;在「动态指针」方式时,记录的是左右区间子节点对象);
* `sum` 为记录当前区间有多少个整数被追踪;
* `add` 为懒标记,当 `add = -1` 代表 `removeRange` 懒标记,当 `add = 1` 则代表 `addRange` 懒标记。

---

### 线段树(动态开点)- 数组估点

对于常规的线段树实现来说,都是一开始就调用 `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 = 1e4$ 分别代表值域大小和查询次数。

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

代码:
```Java
class RangeModule {
    class Node {
        int ls, rs, sum, add;
    }
    int N = (int)1e9 + 10, M = 500010, cnt = 1;
    Node[] tr = new Node[M];
    void update(int u, int lc, int rc, int l, int r, int v) {
        int len = rc - lc + 1;
        if (l <= lc && rc <= r) {
            tr[u].sum = v == 1 ? len : 0;
            tr[u].add = v;
            return ;
        }
        pushdown(u, len);
        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].sum;
        pushdown(u, rc - lc + 1);
        int mid = lc + rc >> 1, ans = 0;
        if (l <= mid) ans = query(tr[u].ls, lc, mid, l, r);
        if (r > mid) ans += query(tr[u].rs, mid + 1, rc, l, r);
        return ans;
    }
    void pushdown(int u, int len) {
        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();
        }
        if (tr[u].add == 0) return;
        if (tr[u].add == -1) {
            tr[tr[u].ls].sum = tr[tr[u].rs].sum = 0;
        } else {
            tr[tr[u].ls].sum = len - len / 2;
            tr[tr[u].rs].sum = len / 2;
        }
        tr[tr[u].ls].add = tr[tr[u].rs].add = tr[u].add;
        tr[u].add = 0;
    }
    void pushup(int u) {
        tr[u].sum = tr[tr[u].ls].sum + tr[tr[u].rs].sum;
    }
    public void addRange(int left, int right) {
        update(1, 1, N - 1, left, right - 1, 1);
    }
    public boolean queryRange(int left, int right) {
        return query(1, 1, N - 1, left, right - 1) == right - left;
    }
    public void removeRange(int left, int right) {
        update(1, 1, N - 1, left, right - 1, -1);
    }
}
```
* 时间复杂度:`addRange`、`queryRange` 和 `removeRange` 操作复杂度均为 $O(\log{n})$
* 空间复杂度:$O(m\log{n})$

---

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

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

代码:
```Java
class RangeModule {
    class Node {
        Node ls, rs;
        int sum, add;
    }
    int N = (int)1e9 + 10;
    Node root = new Node();
    void update(Node node, int lc, int rc, int l, int r, int v) {
        int len = rc - lc + 1;
        if (l <= lc && rc <= r) {
            node.sum = v == 1 ? len : 0;
            node.add = v;
            return ;
        }
        pushdown(node, len);
        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.sum;
        pushdown(node, rc - lc + 1);
        int mid = lc + rc >> 1, ans = 0;
        if (l <= mid) ans = query(node.ls, lc, mid, l, r);
        if (r > mid) ans += query(node.rs, mid + 1, rc, l, r);
        return ans;
    }
    void pushdown(Node node, int len) {
        if (node.ls == null) node.ls = new Node();
        if (node.rs == null) node.rs = new Node();
        if (node.add == 0) return ;
        int add = node.add;
        if (add == -1) {
            node.ls.sum = node.rs.sum = 0;
        } else {
            node.ls.sum = len - len / 2;
            node.rs.sum = len / 2;
        }
        node.ls.add = node.rs.add = add;
        node.add = 0;
    }
    void pushup(Node node) {
        node.sum = node.ls.sum + node.rs.sum;
    }
    public void addRange(int left, int right) {
        update(root, 1, N - 1, left, right - 1, 1);
    }
    public boolean queryRange(int left, int right) {
        return query(root, 1, N - 1, left, right - 1) == right - left;
    }
    public void removeRange(int left, int right) {
        update(root, 1, N - 1, left, right - 1, -1);
    }
}
```
* 时间复杂度:`addRange`、`queryRange` 和 `removeRange` 操作复杂度均为 $O(\log{n})$
* 空间复杂度:$O(m\log{n})$

### 最后

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

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

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

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


717. 1比特与2比特字符

Question:
有两种特殊字符:
* 第一种字符可以用一个比特 $0$ 来表示
* 第二种字符可以用两个比特($10$ 或 $11$)来表示、
给定一个以 $0$ 结尾的二进制数组 `bits` ,如果最后一个字符必须是一位字符,则返回 `true` 。

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

输出: true

解释: 唯一的编码方式是一个两比特字符和一个一比特字符。
所以最后一个字符是一比特字符。
```
示例 2:
```
输入: bits = [1, 1, 1, 0]

输出: false

解释: 唯一的编码方式是两比特字符和两比特字符。
所以最后一个字符不是一比特字符。
```

提示:
* $1 <= bits.length <= 1000$
* $bits[i] == 0$ or $1$

Solution:
### 模拟

根据题意进行模拟即可,使用 $n$ 代指 `bits` 的长度,$idx$ 为当前「比特字」的开头,从前往后扫描每个「比特字」,如果最后一个「比特字」的开头为 $n - 1$ 返回 `True`,否则返回 `False`。

代码:
```Java
class Solution {
    public boolean isOneBitCharacter(int[] bits) {
        int n = bits.length, idx = 0;
        while (idx < n - 1) {
            if (bits[idx] == 0) idx++;
            else idx += 2;
        }
        return idx == n - 1;
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


LeetCode/711-720/719. 找出第 K 小的数对距离（困难）.md

719. 找出第 K 小的数对距离

Question:
数对 $(a,b)$ 由整数 `a` 和 `b` 组成,其数对距离定义为 `a` 和 `b` 的绝对差值。

给你一个整数数组 `nums` 和一个整数 $k$ ,数对由 $nums[i]$ 和 $nums[j]$ 组成且满足 $0 <= i < j < nums.length$ 。

返回 所有数对距离中 第 $k$ 小的数对距离。

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

输出:0

解释:数对和对应的距离如下:
(1,3) -> 2
(1,1) -> 0
(3,1) -> 2
距离第 1 小的数对是 (1,1) ,距离为 0 。
```
示例 2:
```
输入:nums = [1,1,1], k = 2

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

输出:5
```

提示:
* $n == nums.length$
* $2 <= n <= 10^4$
* $0 <= nums[i] <= 10^6$
* $1 <= k <= n \times (n - 1) / 2$

Solution:
### 二分 + 双指针

根据题意,由于对距离定义使用的是绝对值,因此从原数组中找数对 $(i, j)$,等价于在排序数组中找数对 $(i, j)$。

同时由于 $k$ 的范围为 $n^2$,因此我们不能使用复杂度为 $O(k\log{n})$ 的「多路归并」做法来做。

利用数据范围 $0 <= nums[i] <= 10^6$,我们知道距离值域范围为 $[0, 10^6]$,假设所能形成的距离序列为 $A = a_1, a_2, ... ,a_m$,此时在以第 $k$ 小的距离值为分割点的数轴上,具有「二段性」,记这第 $k$ 小的距离值为 $a_k$:

* 处于 $a_k$ 右侧的所有位置 $a_i$(包含 $a_k$)**必然满足**「序列 $A$ 中值小于等于 $a_i$ 的数不少于 $k$ 个」;
* 处于 $a_k$ 左侧的所有位置 $a_i$(不包含 $a_k$)**不一定满足**「序列 $A$ 中值小于等于 $a_i$ 的数不少于 $k$ 个」(当且仅当 $a_k$ 在序列 $A$ 中不重复,或 $a_k$ 恰好是连续段距离值中的左端点时,必然不满足)。

因此这本质上是一个满足 `1?` 特性(而不是 `10` 特性)的问题,我们可以使用「二分」来找到 $a_k$ 值。

假设当前我们二分到的值是 $x$,利用我们排序好的 `nums`,我们并不需要真正的构建出序列 $A$,即可统计值小于等于 $x$ 的数量:枚举左端点 $i$,每次找第一个不满足条件的右端点 $j$(由于 $j$ 是第一个不满足条件的值,因此合法右端点范围为 $[i + 1, j - 1]$,共 $j - i - 1$ 个),利用 `nums` 有序,并且所有 $nums[i]$ 均为正数,可知 $j$ 会随着 $i$ 增大而逐步增大,即这部分利用「双指针」可实现 $O(n)$ 复杂度。

代码:
```Java
class Solution {
    public int smallestDistancePair(int[] nums, int k) {
        Arrays.sort(nums);
        int l = 0, r = (int)1e6;
        while (l < r) {
            int mid = l + r >> 1;
            if (check(nums, mid) >= k) r = mid;
            else l = mid + 1;
        }
        return r;
    }
    int check(int[] nums, int x) {
        int n = nums.length, ans = 0;
        for (int i = 0, j = 1; i < n; i++) {
            while (j < n && nums[j] - nums[i] <= x) j++;
            ans += j - i - 1;
        }
        return ans;
    }
}
```
* 时间复杂度:排序的复杂度为 $O(n\log{n})$,二分答案复杂度为 $O(n\log{m})$,其中 $m = 1e6$ 为距离值域。整体复杂度为 $O(n\log{m})$
* 空间复杂度:$O(\log{n})$

### 最后

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

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

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

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


720. 词典中最长的单词

Question:
给出一个字符串数组 `words` 组成的一本英语词典。返回 `words` 中最长的一个单词,该单词是由 `words` 词典中其他单词逐步添加一个字母组成。

若其中有多个可行的答案,则返回答案中字典序最小的单词。若无答案,则返回空字符串。

示例 1:
```
输入:words = ["w","wo","wor","worl", "world"]

输出:"world"

解释: 单词"world"可由"w", "wo", "wor", 和 "worl"逐步添加一个字母组成。
```
示例 2:
```
输入:words = ["a", "banana", "app", "appl", "ap", "apply", "apple"]

输出:"apple"

解释:"apply" 和 "apple" 都能由词典中的单词组成。但是 "apple" 的字典序小于 "apply"
```

提示:
* $1 <= words.length <= 1000$
* $1 <= words[i].length <= 30$
* 所有输入的字符串 $words[i]$ 都只包含小写字母。

Solution:
### 模拟

数据范围很小,我们可以直接模拟来做。

先将所有的 $words[i]$ 存入 `Set` 集合,方便后续可以近似 $O(1)$ 查询某个子串是否存在 $words$ 中。

遍历 $words$ 数组(题目没有说 $words$ 不重复,因此最好遍历刚刚预处理的 `Set`  集合),判断每个 $words[i]$ 是否为「合法单词」,同时利用当前的最长单词来做剪枝。

不算剪枝效果,该做法计算量不超过 $10^6$,可以过。

代码:
```Java
class Solution {
    public String longestWord(String[] words) {
        String ans = "";
        Set<String> set = new HashSet<>();
        for (String s : words) set.add(s);
        for (String s : set) {
            int n = s.length(), m = ans.length();
            if (n < m) continue;
            if (n == m && s.compareTo(ans) > 0) continue;
            boolean ok = true;
            for (int i = 1; i <= n && ok; i++) {
                String sub = s.substring(0, i);
                if (!set.contains(sub)) ok = false;
            }
            if (ok) ans = s;
        }
        return ans;
    }
}
```
* 时间复杂度:预处理 `Set` 集合复杂度近似 $O(n)$;判断某个 $words[i]$ 是否合法需要判断所有子串是否均在 $words$ 中,复杂度为 $O(m^2)$,其中 $m$ 为字符串长度,处理 $words[i]$ 的过程还使用到 `compareTo` 操作,其复杂度为 $O(\min(N, M))$,其中 $N$ 和 $M$ 为参与比较的两字符串长度,该操作相比于生成子串可忽略,而对于一个长度为 $m$ 的字符串而言,生成其所有的子串的计算量为首项为 $1$,末项为 $m$,公差为 $1$ 的等差数列求和结果。整体复杂度为 $O(\sum_{i = 0}^{n - 1}words[i].length^2)$
* 空间复杂度:$O(\sum_{i = 0}^{n - 1}words[i].length)$

---

### 字典树

上述解法中「枚举某个 $words[i]$ 的所有子串,并判断子串是否在 $words$ 数组中出现」的操作可使用「字典树」来实现。

**不了解「Trie / 字典树」的同学可以看前置 🧀:[字典树入门](https://mp.weixin.qq.com/s?__biz=MzU4NDE3MTEyMA==&mid=2247488490&idx=1&sn=db2998cb0e5f08684ee1b6009b974089)。里面通过图例展示了字典树基本形态,以及提供了「数组实现」和「TrieNode 实现」两种方式,还有「数组大小估算方式」和「Trie 应用面」介绍。**

回到本题,起始先将所有的 $words[i]$ 存入字典树,并记录每个字符的结尾编号。

对于某个 $words[i]$ 而言,其能成为「合法单词」的充要条件为:$words[i]$ 的每个前缀编号都有「以结尾编号」所被记录。

> 一些细节:为了防止每个样例都 `new` 大数组,我们使用 `static` 进行优化,并在跑样例前进行相应的清理工作。

代码:
```Java
class Solution {
    static int N = 30010, M = 26;
    static int[][] tr = new int[N][M];
    static boolean[] isEnd = new boolean[N];
    static int idx = 0;
    void add(String s) {
        int p = 0, n = s.length();
        for (int i = 0; i < n; i++) {
            int u = s.charAt(i) - 'a';
            if (tr[p][u] == 0) tr[p][u] = ++idx;
            p = tr[p][u];
        }
        isEnd[p] = true;
    }
    boolean query(String s) {
        int p = 0, n = s.length();
        for (int i = 0; i < n; i++) {
            int u = s.charAt(i) - 'a';
            p = tr[p][u];
            if (!isEnd[p]) return false;
        }
        return true;
    }
    public String longestWord(String[] words) {
        Arrays.fill(isEnd, false);
        for (int i = 0; i <= idx; i++) Arrays.fill(tr[i], 0);
        idx = 0;

        String ans = "";
        for (String s : words) add(s);
        for (String s : words) {
            int n = s.length(), m = ans.length();
            if (n < m) continue;
            if (n == m && s.compareTo(ans) > 0) continue;
            if (query(s)) ans = s;
        }
        return ans;
    }
}
```
* 时间复杂度:将所有 $words[i]$ 存入字典树的复杂度为 $O(\sum_{i = 0}^{n - 1}words[i].length)$;查询每个 $words[i]$ 是否合法的复杂度为 $O(m)$,其中 $m$ 为当前 $words[i]$ 长度。整体复杂度为 $O(\sum_{i = 0}^{n - 1}words[i].length)$
* 空间复杂度:$O(C)$

### 最后

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

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

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

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


724. 寻找数组的中心下标

Question:
给你一个整数数组 nums,请编写一个能够返回数组 “中心下标” 的方法。

数组 中心下标 是数组的一个下标,其左侧所有元素相加的和等于右侧所有元素相加的和。

如果数组不存在中心下标,返回 -1 。

如果数组有多个中心下标,应该返回最靠近左边的那一个。

注意:中心下标可能出现在数组的两端。

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

输出:3

解释:
中心下标是 3 。
左侧数之和 (1 + 7 + 3 = 11),
右侧数之和 (5 + 6 = 11) ,二者相等。
```
示例 2:
```
输入:nums = [1, 2, 3]

输出:-1

解释:
数组中不存在满足此条件的中心下标。
```
示例 3:
```
输入:nums = [2, 1, -1]

输出:0

解释:
中心下标是 0 。
下标 0 左侧不存在元素,视作和为 0 ;
右侧数之和为 1 + (-1) = 0 ,二者相等。
```

提示:
* nums 的长度范围为 [0, 10000]。
* 任何一个 nums[i] 将会是一个范围在 [-1000, 1000]的整数。

Solution:
### 基本分析

这是一道前缀和的裸题。

只需要用两个数组,前后处理两遍前缀和,再对两个前缀和数组的相同下标进行判别即可。

为了简化数组越界的判断,我们通常会给前缀和数组多预留一位作为哨兵。

这里由于要求前后前缀和。所以我们直接多开两位。

代码:
```Java
class Solution {
    public int pivotIndex(int[] nums) {
        int n = nums.length;
        int[] s1 = new int[n + 2], s2 = new int[n + 2];
        for (int i = 1; i <= n; i++) s1[i] = s1[i - 1] + nums[i - 1];
        for (int i = n; i >= 1; i--) s2[i] = s2[i + 1] + nums[i - 1];
        for (int i = 1; i <= n; i++) {
            if (s1[i] == s2[i]) return i - 1;
        }
        return -1;
    }
}
```
* 时间复杂度:对数组进行线性扫描。复杂度为 $O(n)$
* 空间复杂度:使用了前缀和数组。复杂度为$O(n)$

---

### 空间优化(常数级别的优化)

当然,我们也可以只处理一遍前缀和。

然后在判定一个下标是否为”中心索引“的时候,利用前缀和计算左侧值和右侧值。

但这只是常数级别的优化,并不影响其时空复杂度。

代码:
```Java
class Solution {
    public int pivotIndex(int[] nums) {
        int n = nums.length;
        int[] sum = new int[n + 1];
        for (int i = 1; i <= n; i++) sum[i] = sum[i - 1] + nums[i - 1];
        for (int i = 1; i <= n; i++) {
            int left = sum[i - 1], right = sum[n] - sum[i];
            if (left == right) return i - 1;
        }
        return -1;
    }
}
```
* 时间复杂度:对数组进行线性扫描。复杂度为 $O(n)$
* 空间复杂度:使用了前缀和数组。复杂度为$O(n)$

---

### 空间优化(优化至常数级别)

甚至可以不使用额外空间。

先求一遍总和 `total`,再使用 `sum` 记录当前遍历位置的左侧总和。

对于中心索引必然有:`sum = total - sum - nums[i]` (左边值 = 右边值)

代码:
```Java
class Solution {
    public int pivotIndex(int[] nums) {
        int n = nums.length;
        int total = 0, sum = 0;
        // 我们的 nums 处理不涉及并行操作,使用循环要比 Arrays.stream 快
        // total = Arrays.stream(nums).sum();
        for (int i = 0; i < n; i++) total += nums[i];
        for (int i = 0; i < n; i++) {
            if (sum == total - sum - nums[i]) return i;
            sum += nums[i];
        }
        return -1;
    }
}
```
* 时间复杂度:对数组进行线性扫描。复杂度为 $O(n)$
* 空间复杂度:$O(1)$

---

### 总结

这是我使用到的前缀和模板(高频):

```Java
class Solution {
    public void func(int[] nums) {
        int n = nums.length;
        int[] sum = new int[n + 1];
        for (int i = 1; i <= n; i++) sum[i] = sum[i - 1] + nums[i - 1];
    }
}
```

### 最后

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

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

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

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


Question:
给你一个头结点为 `head` 的单链表和一个整数 `k` ,请你设计一个算法将链表分隔为 `k` 个连续的部分。

每部分的长度应该尽可能的相等:任意两部分的长度差距不能超过 $1$ 。这可能会导致有些部分为 null 。

这 `k` 个部分应该按照在链表中出现的顺序排列,并且排在前面的部分的长度应该大于或等于排在后面的长度。

返回一个由上述 k 部分组成的数组。

示例 1:

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

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

解释:
第一个元素 output[0] 为 output[0].val = 1 ,output[0].next = null 。
最后一个元素 output[4] 为 null ,但它作为 ListNode 的字符串表示是 [] 。
```
示例 2:

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

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

解释:
输入被分成了几个连续的部分,并且每部分的长度相差不超过 1 。前面部分的长度大于等于后面部分的长度。
```

提示:
* 链表中节点的数目在范围 [0, 1000]
* 0 <= Node.val <= 1000
* 1 <= k <= 50

Solution:
### 模拟

根据题意,我们应当近可能将链表平均分为 $k$ 份。

我们可以采取与 [(题解) 68. 文本左右对齐](https://leetcode-cn.com/problems/text-justification/solution/gong-shui-san-xie-zi-fu-chuan-mo-ni-by-a-s3v7/) 类似的思路(在 $68$ 中,填充空格的操作与本题一致:尽可能平均,无法均分时,应当使前面比后面多)。

回到本题,我们可以先对链表进行一次扫描,得到总长度 $cnt$,再结合需要将将链表划分为 $k$ 份,可知每一份的 **最小** 分配单位 $per = \left \lfloor \frac{cnt}{k} \right \rfloor$(当 $cnt < k$ 时,$per$ 为 $0$)。

然后从前往后切割出 $k$ 份链表,由于是在原链表的基础上进行,因此这里的切分只需要在合适的位置将节点的 $next$ 指针置空即可。

**当我们需要构造出 $ans[i]$ 的链表长度时,首先可以先分配 $per$ 的长度,如果 `已处理的链表长度 + 剩余待分配份数 * per < cnt`,说明后面「待分配的份数」如果按照每份链表分配 $per$ 长度的话,会有节点剩余,基于「不能均分时,前面的应当比后面长」原则,此时只需为当前 $ans[i]$ 多分一个单位长度即可。**

代码:
```Java
class Solution {
    public ListNode[] splitListToParts(ListNode head, int k) {
        // 扫描链表,得到总长度 cnt
        int cnt = 0;
        ListNode tmp = head;
        while (tmp != null && ++cnt > 0) tmp = tmp.next;
        // 理论最小分割长度
        int per = cnt / k;
        // 将链表分割为 k 份(sum 代表已经被处理的链表长度为多少)
        ListNode[] ans = new ListNode[k];
        for (int i = 0, sum = 1; i < k; i++, sum++) {
            ans[i] = head;
            tmp = ans[i];
            // 每次首先分配 per 的长度
            int u = per;
            while (u-- > 1 && ++sum > 0) tmp = tmp.next;
            // 当「已处理的链表长度 + 剩余待分配份数 * per < cnt」,再分配一个单位长度
            int remain = k - i - 1;
            if (per != 0 && sum + per * remain < cnt && ++sum > 0) tmp = tmp.next;
            head = tmp != null ? tmp.next : null;
            if (tmp != null) tmp.next = null;
        }
        return ans;
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


Question:
给定一个化学式 `formula`(作为字符串),返回每种原子的数量。

原子总是以一个大写字母开始,接着跟随0个或任意个小写字母,表示原子的名字。

如果数量大于 1,原子后会跟着数字表示原子的数量。如果数量等于 1 则不会跟数字。例如,`H2O` 和 `H2O2` 是可行的,但 `H1O2` 这个表达是不可行的。

两个化学式连在一起是新的化学式。例如 `H2O2He3Mg4` 也是化学式。

一个括号中的化学式和数字(可选择性添加)也是化学式。例如 `(H2O2)` 和 `(H2O2)3` 是化学式。

给定一个化学式,输出所有原子的数量。格式为:第一个(按字典序)原子的名子,跟着它的数量(如果数量大于 1),然后是第二个原子的名字(按字典序),跟着它的数量(如果数量大于 1),以此类推。

示例 1:
```
输入: formula = "H2O"

输出: "H2O"

解释: 原子的数量是 {'H': 2, 'O': 1}。
```
示例 2:
```
输入: formula = "Mg(OH)2"

输出: "H2MgO2"

解释: 原子的数量是 {'H': 2, 'Mg': 1, 'O': 2}。
```
示例 3:
```
输入: formula = "K4(ON(SO3)2)2"

输出: "K4N2O14S4"

解释: 原子的数量是 {'K': 4, 'N': 2, 'O': 14, 'S': 4}。
```

注意:
* 所有原子的第一个字母为大写,剩余字母都是小写。
* `formula` 的长度在[1, 1000]之间。
* `formula` 只包含字母、数字和圆括号,并且题目中给定的是合法的化学式。

Solution:
### 数据结构 + 模拟

一道综合模拟题。

相比于[(题解)227. 基本计算器 II](https://leetcode-cn.com/problems/basic-calculator-ii/solution/shi-yong-shuang-zhan-jie-jue-jiu-ji-biao-c65k/) 的表达式计算问题,本题设计模拟流程的难度要低很多,之所谓定位困难估计是使用到的数据结构较多一些。

为了方便,我们约定以下命名:
* 称一段完整的连续字母为「原子」
* 称一段完整的连续数字为「数值」
* 称 `(` 和`)` 为「符号」

基本实现思路如下:

1. 在处理入参 `s` 的过程中,始终维护着一个哈希表 `map`,`map` 中 **实时维护** 着某个「原子」对应的实际「数值」(即存储格式为 `{H:2,S:1}`);

    > 由于相同原子可以出在 `s` 的不同位置中,为了某个「数值」对「原子」的累乘效果被重复应用,我们这里应用一个”小技巧“:为每个「原子」增加一个”编号后缀“。即实际存储时为 `{H_1:2, S_2:1, H_3:1}`。

2. 根据当前处理到的字符分情况讨论:
    * 符号:直接入栈;
    * 原子:继续往后取,直到取得完整的原子名称,将完整原子名称入栈,同时在 `map` 中计数加 $1$;
    * 数值:继续往后取,直到取得完整的数值并解析,然后根据栈顶元素是否为 `)` 符号,决定该数值应用给哪些原子:
        * 如果栈顶元素不为 `)`,说明该数值只能应用给栈顶的原子
        * 如果栈顶元素是 ),说明当前数值可以应用给「连续一段」的原子中

3. 对 `map` 的原子做 “合并” 操作:`{H_1:2, S_2:1, H_3:1}` => `{H:3, S:1}` ;

4. 使用「优先队列(堆)」实现字典序排序(也可以直接使用 `List`,然后通过 `Collections.sort` 进行排序),并构造答案。

代码:
```Java
class Solution {
    class Node {
        String s; int v;
        Node (String _s, int _v) {
            s = _s; v = _v;
        }
    }
    public String countOfAtoms(String s) {
        int n = s.length();
        char[] cs = s.toCharArray();
        Map<String, Integer> map = new HashMap<>();
        Deque<String> d = new ArrayDeque<>();
        int idx = 0;
        for (int i = 0; i < n; ) {
            char c = cs[i];
            if (c == '(' || c == ')') {
                d.addLast(String.valueOf(c));
                i++;
            } else {
                if (Character.isDigit(c)) {
                    // 获取完整的数字,并解析出对应的数值
                    int j = i;
                    while (j < n && Character.isDigit(cs[j])) j++;
                    String numStr = s.substring(i, j);
                    i = j;
                    int cnt = Integer.parseInt(String.valueOf(numStr));

                    // 如果栈顶元素是 ),说明当前数值可以应用给「连续一段」的原子中
                    if (!d.isEmpty() && d.peekLast().equals(")")) {
                        List<String> tmp = new ArrayList<>();

                        d.pollLast(); // pop )
                        while (!d.isEmpty() && !d.peekLast().equals("(")) {
                            String cur = d.pollLast();
                            map.put(cur, map.getOrDefault(cur, 1) * cnt);
                            tmp.add(cur);
                        }
                        d.pollLast(); // pop (

                        for (int k = tmp.size() - 1; k >= 0; k--) {
                            d.addLast(tmp.get(k));
                        }

                    // 如果栈顶元素不是 ),说明当前数值只能应用给栈顶的原子
                    } else {
                        String cur = d.pollLast();
                        map.put(cur, map.getOrDefault(cur, 1) * cnt);
                        d.addLast(cur);
                    }
                } else {
                    // 获取完整的原子名
                    int j = i + 1;
                    while (j < n && Character.isLowerCase(cs[j])) j++;
                    String cur = s.substring(i, j) + "_" + String.valueOf(idx++);
                    map.put(cur, map.getOrDefault(cur, 0) + 1);
                    i = j;
                    d.addLast(cur);
                }
            }
        }

        // 将不同编号的相同原子进行合并
        Map<String, Node> mm = new HashMap<>();
        for (String key : map.keySet()) {
            String atom = key.split("_")[0];
            int cnt = map.get(key);
            Node node = null;
            if (mm.containsKey(atom)) {
                node = mm.get(atom);
            } else {
                node = new Node(atom, 0);
            }
            node.v += cnt;
            mm.put(atom, node);
        }

        // 使用优先队列(堆)对 Node 进行字典序排序,并构造答案
        PriorityQueue<Node> q = new PriorityQueue<Node>((a,b)->a.s.compareTo(b.s));
        for (String atom : mm.keySet()) q.add(mm.get(atom));

        StringBuilder sb = new StringBuilder();
        while (!q.isEmpty()) {
            Node poll = q.poll();
            sb.append(poll.s);
            if (poll.v > 1) sb.append(poll.v);
        }

        return sb.toString();
    }
}
```
* 时间复杂度:最坏情况下,每次处理数值都需要从栈中取出元素进行应用,处理 `s` 的复杂度为 $O(n^2)$;最坏情况下,每个原子独立分布,合并的复杂度为 $O(n)$;将合并后的内容存入优先队列并取出构造答案的复杂度为 $O(n\log{n})$;整体复杂度为 $O(n^2)$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


Question:
自除数是指可以被它包含的每一位数整除的数。

* 例如,$128$ 是一个 自除数 ,因为 `128 % 1 == 0`,`128 % 2 == 0`,`128 % 8 == 0`。

自除数不允许包含 $0$ 。

给定两个整数 `left` 和 `right` ,返回一个列表,列表的元素是范围 $[left, right]$ 内所有的 自除数 。

示例 1:
```
输入:left = 1, right = 22

输出:[1, 2, 3, 4, 5, 6, 7, 8, 9, 11, 12, 15, 22]
```
示例 2:
```
输入:left = 47, right = 85

输出:[48,55,66,77]
```

提示:
* $1 <= left <= right <= 10^4$

Solution:
### 模拟

根据题意进行模拟即可。

代码:
```Java
class Solution {
    public List<Integer> selfDividingNumbers(int left, int right) {
        List<Integer> ans = new ArrayList<>();
        out:for (int i = left; i <= right; i++) {
            int cur = i;
            while (cur != 0) {
                int t = cur % 10;
                if (t == 0 || i % t != 0) continue out;
                cur /= 10;
            }
            ans.add(i);
        }
        return ans;
    }
}
```

-

```Python
class Solution:
    def selfDividingNumbers(self, left: int, right: int) -> List[int]:
        ans = []
        for i in range(left, right + 1):
            cur, ok = i, True
            while cur and ok:
                ok = not ((t := cur % 10) == 0 or i % t != 0)
                cur //= 10
            if ok:
                ans.append(i)
        return ans
```
* 时间复杂度:令 $n = right - left + 1$,复杂度为 $O(n * \log{right})$
* 空间复杂度:$O(1)$

---

### 打表 + 二分

利用数据范围只有 $1e4$,我们可以打表预处理出所有的自除数,通过二分找到 $[left, right]$ 范围内的最小自除数,再从前往后找到所有合法的自除数。

代码:
```Java
class Solution {
    static List<Integer> list = new ArrayList<>();
    static {
        out:for (int i = 1; i <= 10000; i++) {
            int cur = i;
            while (cur != 0) {
                int u = cur % 10;
                if (u == 0 || i % u != 0) continue out;
                cur /= 10;
            }
            list.add(i);
        }
    }
    public List<Integer> selfDividingNumbers(int left, int right) {
        List<Integer> ans = new ArrayList<>();
        int l = 0, r = list.size() - 1;
        while (l < r) {
            int mid = l + r >> 1;
            if (list.get(mid) >= left) r = mid;
            else l = mid + 1;
        }
        while (r < list.size() && list.get(r) <= right) ans.add(list.get(r++));
        return ans;
    }
}
```
* 时间复杂度:令 $m$ 为范围在 $[left, right]$ 之间的自除数的数量,$n = right - left + 1$。复杂度为$O(\log{m} + n)$,对于本题,$m$ 上界为 $339$
* 空间复杂度:$O(m)$

---

### 打表 + 哈希表

在打表预处理了所有范围内的自除数的基础上,我们可以干脆将索引也预处理出来,从而避免二分操作。

其中 $hash[x]$ 的含义为值不超过 $x$ 的最大自除数在 `list` 中的下标。

代码:
```Java
class Solution {
    static List<Integer> list = new ArrayList<>();
    static int[] hash = new int[10010];
    static {
        for (int i = 1; i <= 10000; i++) {
            int cur = i;
            boolean ok = true;
            while (cur != 0 && ok) {
                int u = cur % 10;
                if (u == 0 || i % u != 0) ok = false;
                cur /= 10;
            }
            if (ok) list.add(i);
            hash[i] = list.size() - 1;
        }
    }
    public List<Integer> selfDividingNumbers(int left, int right) {
        List<Integer> ans = new ArrayList<>();
        int idx = list.get(hash[left]) == left ? hash[left] : hash[left] + 1;
        while (idx < list.size() && list.get(idx) <= right) ans.add(list.get(idx++));
        return ans;
    }
}
```
* 时间复杂度:$n = right - left + 1$。复杂度为$O(n)$
* 空间复杂度:$O(C)$

### 最后

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

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

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

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


729. 我的日程安排表 I

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

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

日程可以用一对整数 `start` 和 `end` 表示,这里的时间是半开区间,即 $[start, end)$, 实数 $x$ 的范围为,  $start <= x < end$ 。

实现 MyCalendar 类:

* `MyCalendar()` 初始化日历对象。
* `boolean book(int start, int end)` 如果可以将日程安排成功添加到日历中而不会导致重复预订,返回 `true`。否则,返回 `false` 并且不要将该日程安排添加到日历中。

示例:
```
输入:
["MyCalendar", "book", "book", "book"]
[[], [10, 20], [15, 25], [20, 30]]

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

解释:
MyCalendar myCalendar = new MyCalendar();
myCalendar.book(10, 20); // return True
myCalendar.book(15, 25); // return False ,这个日程安排不能添加到日历中,因为时间 15 已经被另一个日程安排预订了。
myCalendar.book(20, 30); // return True ,这个日程安排可以添加到日历中,因为第一个日程安排预订的每个时间都小于 20 ,且不包含时间 20 。
```

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

Solution:
### 模拟

利用 `book` 操作最多调用 $1000$ 次,我们可以使用一个数组存储所有已被预定的日期 $[start, end - 1]$,对于每次 `book` 操作,检查当前传入的 $[start, end)$ 是否会与已有的日期冲突,冲突返回 `False`,否则将 $[start, end- 1]$ 插入数组并返回 `True`。

代码:
```Java
class MyCalendar {
    List<int[]> list = new ArrayList<>();
    public boolean book(int start, int end) {
        end--;
        for (int[] info : list) {
            int l = info[0], r = info[1];
            if (start > r || end < l) continue;
            return false;
        }
        list.add(new int[]{start, end});
        return true;
    }
}
```
* 时间复杂度:令 $n$ 为 `book` 的最大调用次数,复杂度为 $O(n^2)$
* 空间复杂度:$O(n)$

---

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

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

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

对于常规的线段树实现来说,都是一开始就调用 `build` 操作创建空树,而线段树一般以「满二叉树」的形式用数组存储,因此需要 $4 * 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 * n)$,而开点数的预估需不能仅仅根据 $\log{n}$ 来进行,还要对常熟进行分析,才能得到准确的点数上界。

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

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

代码:
```Java
class MyCalendar {
    class Node {
        // ls 和 rs 分别代表当前节点的左右子节点在 tr 的下标
        // val 代表当前节点有多少数
        // add 为懒标记
        int ls, rs, add, val;
    }
    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].val += (rc - lc + 1) * v;
            tr[u].add += v;
            return ;
        }
        lazyCreate(u);
        pushdown(u, rc - lc + 1);
        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].val;
        lazyCreate(u);
        pushdown(u, rc - lc + 1);
        int mid = lc + rc >> 1, ans = 0;
        if (l <= mid) ans = query(tr[u].ls, lc, mid, l, r);
        if (r > mid) 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, int len) {
        tr[tr[u].ls].add += tr[u].add; tr[tr[u].rs].add += tr[u].add;
        tr[tr[u].ls].val += (len - len / 2) * tr[u].add; tr[tr[u].rs].val += len / 2 * tr[u].add;
        tr[u].add = 0;
    }
    void pushup(int u) {
        tr[u].val = tr[tr[u].ls].val + tr[tr[u].rs].val;
    }
    public boolean book(int start, int end) {
        if (query(1, 1, N + 1, start + 1, end) > 0) return false;
        update(1, 1, N + 1, start + 1, end, 1);
        return true;
    }
}
```
* 时间复杂度:令 $n$ 为值域大小,本题固定为 $1e9$,线段树的查询和增加复杂度均为 $O(\log{n})$
* 空间复杂度:令询问数量为 $m$,复杂度为 $O(m\log{n})$

---

### 分块 + 位运算(分桶)

另一个留有遗憾的算法是「分块」,朴素的分块做法是使用一个布尔数组 `region` 代表某个块是否有被占用,使用哈希表记录具体某个位置是否被占用,但可惜被奇怪的测评机制卡了。

`TLE` 代码:
```Java
class MyCalendar {
    static Boolean T = Boolean.TRUE, F = Boolean.FALSE;
    static int n = (int)1e9, len = (int) Math.sqrt(n) + 30;
    static boolean[] region = new boolean[len];
    static Map<Integer, Boolean> map = new HashMap<>();
    int getIdx(int x) {
        return x / len;
    }
    void add(int l, int r) {
        if (getIdx(l) == getIdx(r)) {
            for (int k = l; k <= r; k++) map.put(k, T);
        } else {
            int j = l, i = r;
            while (getIdx(j) == getIdx(l)) map.put(j++, T);
            while (getIdx(i) == getIdx(r)) map.put(i--, T);
            for (int k = getIdx(j); k <= getIdx(i); k++) region[k] = true;
        }
    }
    boolean query(int l, int r) {
        if (getIdx(l) == getIdx(r)) {
            boolean cur = region[getIdx(l)];
            for (int k = l; k <= r; k++) {
                if (map.getOrDefault(k, F) || cur) return false;
            }
            return true;
        } else {
            int j = l, i = r;
            while (getIdx(j) == getIdx(l)) {
                if (map.getOrDefault(j, F) || region[getIdx(j)]) return false;
                j++;
            }
            while (getIdx(i) == getIdx(r)) {
                if (map.getOrDefault(i, F) || region[getIdx(i)]) return false;
                i--;
            }
            for (int k = getIdx(j); k <= getIdx(i); k++) {
                if (region[k]) return false;
            }
            return true;
        }
    }
    public MyCalendar() {
        Arrays.fill(region, false);
        map.clear();
    }
    public boolean book(int start, int end) {
        if (query(start, end - 1)) {
            add(start, end - 1);
            return true;
        }
        return false;
    }
}
```

但我们知道分块算法的复杂度并不糟糕,而哈希表可能是被卡常数的关键,因此我们可以使用 `int` 数组来充当哈希表,由于只需要记录「是否被占用」,因此我们可以使用 `int` 的每一位充当格子,通过这种「分桶 + 位运算」,有效降低常数。

`AC` 代码(`2022-07-05` 可过):
```Java
class MyCalendar {
    static int n = (int)1e9, len = (int) Math.sqrt(n) + 50, cnt = 32;
    static boolean[] region = new boolean[len];
    static int[] map = new int[n / cnt];
    int getIdx(int x) {
        return x / len;
    }
    boolean get(int x) {
        return ((map[x / cnt] >> (x % cnt)) & 1) == 1;
    }
    void set(int x) {
        map[x / cnt] |= (1 << (x % cnt));
    }
    void add(int l, int r) {
        if (getIdx(l) == getIdx(r)) {
            for (int k = l; k <= r; k++) set(k);
        } else {
            int j = l, i = r;
            while (getIdx(j) == getIdx(l)) set(j++);
            while (getIdx(i) == getIdx(r)) set(i--);
            for (int k = getIdx(j); k <= getIdx(i); k++) region[k] = true;
        }
    }
    boolean query(int l, int r) {
        if (getIdx(l) == getIdx(r)) {
            boolean cur = region[getIdx(l)];
            for (int k = l; k <= r; k++) {
                if (get(k) || cur) return false;
            }
            return true;
        } else {
            int j = l, i = r;
            while (getIdx(j) == getIdx(l)) {
                if (get(j) || region[getIdx(j)]) return false;
                j++;
            }
            while (getIdx(i) == getIdx(r)) {
                if (get(i) || region[getIdx(i)]) return false;
                i--;
            }
            for (int k = getIdx(j); k <= getIdx(i); k++) {
                if (region[k]) return false;
            }
            return true;
        }
    }
    public MyCalendar() {
        Arrays.fill(region, false);
        Arrays.fill(map, 0);
    }
    public boolean book(int start, int end) {
        if (query(start, end - 1)) {
            add(start, end - 1);
            return true;
        }
        return false;
    }
}
```
* 时间复杂度:$O(n\sqrt{m})$
* 空间复杂度:$O(\sqrt{m} + \frac{M}{k})$,其中 $M = 1e9$ 为值域大小,$k = 32$ 为单个数字的能够存储的格子数

### 最后

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

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

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

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


730. 统计不同回文子序列

Question:
给定一个字符串 `s`,返回 `s` 中不同的非空「回文子序列」个数 。

通过从 `s` 中删除 $0$ 个或多个字符来获得子序列。

如果一个字符序列与它反转后的字符序列一致,那么它是「回文字符序列」。

如果有某个 $i$ , 满足 $a_i$ != $b_i$ ,则两个序列 `a1, a2, ...` 和 `b1, b2, ...` 不同。

注意:
* 结果可能很大,你需要对 $10^9 + 7$ 取模 。

示例 1:
```
输入:s = 'bccb'

输出:6

解释:6 个不同的非空回文子字符序列分别为:'b', 'c', 'bb', 'cc', 'bcb', 'bccb'。
注意:'bcb' 虽然出现两次但仅计数一次。
```
示例 2:
```
输入:s = 'abcdabcdabcdabcdabcdabcdabcdabcddcbadcbadcbadcbadcbadcbadcbadcba'

输出:104860361

解释:共有 3104860382 个不同的非空回文子序列,104860361 对 109 + 7 取模后的值。
```

提示:
* $1 <= s.length <= 1000$
* `s[i]` 仅包含 `'a'`, `'b'`, `'c'` 或 `'d'`

Solution:
### 区间 DP

往长度较少的回文串两端添加字符,可能组成新的长度大的回文串,容易想到「区间 DP」,同时 `s` 仅由 $4$ 类小写字母组成,也是一个切入点。

根据区间 DP 的一般思路,定义 $f[i][j]$ 为考虑字符串 `s` 中的 $[i,j]$ 范围内回文子序列的个数,最终答案为 $f[0][n - 1]$。

**不失一般性考虑 $f[i][j]$ 该如何转移,通过枚举 `abcd` 作为回文方案「边缘字符」来进行统计,即分别统计各类字符作为「边缘字符」时对 $f[i][j]$ 的贡献,此类统计方式天生不存在重复性问题。**

假设当前枚举到的字符为 $k$ :

* 若 $s[i...j]$ 中没有字符 $k$,则字符 $k$ 对 $f[i][j]$ 贡献为 $0$,跳过;
* 若 $s[i...j]$ 中存在字符 $k$,根据字符 $k$ 在范围 $s[i...j]$ 中「最小下标」和「最大下标」进行分情况讨论,假设字符 $k$ 在 $s[i...j]$ 中「最靠左」的位置为 $l$,「最靠右」的位置为 $r$:
  * 当 $l = r$ 时,此时字符 $k$ 对 $f[i][j]$ 的贡献为 $1$,即 `k` 本身;
  * 当 $l = r - 1$ 时,说明字符 $k$ 中间不存在任何字符,此时字符 $k$ 对 $f[i][j]$ 的贡献为 $2$,包括 `k` 和 `kk` 两种回文方案;
  * 其余情况,可根据已算得的「小区间回文方案」进行延伸(两段分别补充位于 $l$ 和 $r$ 的字符 $k$),得到新的大区间方案,此部分对 $f[i][j]$ 的贡献是 $f[l + 1][r - 1]$,另外还有 `k` 和 `kk` 两种回文方案,因此总的对答案的贡献为 $f[l + 1][r - 1] + 2$。

统计 $s[i...j]$ 中各类字符「最靠左」和「最靠右」的位置,可通过调整枚举方向来实现:从大到小枚举 $i$,同时维护 `L[s[i]-'a'] = i`,即可得到「最靠左」的位置;在确定左端点 $i$ 之后,从小到大枚举右端点 $j$,同时维护 `R[s[j]-'a'] = j`,即可得到「最靠右」的位置。

代码:
```Java
class Solution {
    int MOD = (int)1e9+7;
    public int countPalindromicSubsequences(String s) {
        char[] cs = s.toCharArray();
        int n = cs.length;
        int[][] f = new int[n][n];
        int[] L = new int[4], R = new int[4];
        Arrays.fill(L, -1);
        for (int i = n - 1; i >= 0; i--) {
            L[cs[i] - 'a'] = i;
            Arrays.fill(R, -1);
            for (int j = i; j < n; j++) {
                R[cs[j] - 'a'] = j;
                for (int k = 0; k < 4; k++) {
                    if (L[k] == -1 || R[k] == -1) continue;
                    int l = L[k], r = R[k];
                    if (l == r) f[i][j] = (f[i][j] + 1) % MOD;
                    else if (l == r - 1) f[i][j] = (f[i][j] + 2) % MOD;
                    else f[i][j] = (f[i][j] + f[l + 1][r - 1] + 2) % MOD;
                }
            }
        }
        return f[0][n - 1];
    }
}
```
* 时间复杂度:$O(C \times n^2)$,其中 $C = 4$ 为字符集大小
* 空间复杂度:$O(n^2)$

### 最后

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

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

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

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