LeetCode/1631-1640/1640. 能否连接形成数组（简单）.md

1640. 能否连接形成数组

Question:
给你一个整数数组 `arr` ,数组中的每个整数 互不相同 。

另有一个由整数数组构成的数组 `pieces`,其中的整数也互不相同。

请你以任意顺序连接 `pieces` 中的数组以形成 `arr` 。

但是,不允许对每个数组 `pieces[i]` 中的整数重新排序。

如果可以连接 `pieces` 中的数组形成 `arr` ,返回 `true` ;否则,返回 `false` 。

示例 1:
```
输入:arr = [15,88], pieces = [[88],[15]]

输出:true

解释:依次连接 [15] 和 [88]
```
示例 2:
```
输入:arr = [49,18,16], pieces = [[16,18,49]]

输出:false

解释:即便数字相符,也不能重新排列 pieces[0]
```
示例 3:
```
输入:arr = [91,4,64,78], pieces = [[78],[4,64],[91]]

输出:true

解释:依次连接 [91]、[4,64] 和 [78]
```

提示:
* $1 <= pieces.length <= arr.length <= 100$
* `sum(pieces[i].length) = arr.length`
* $1 <= pieces[i].length <= arr.length$
* $1 <= arr[i], pieces[i][j] <= 100$
* `arr` 中的整数 互不相同
* `pieces` 中的整数 互不相同(也就是说,如果将 `pieces` 扁平化成一维数组,数组中的所有整数互不相同)

Solution:
### 排序 + 二分

偷懒直接看示例做了,于是漏掉「两数组元素各不相同」且「两数组总元素个数相等」等条件,写了一个「排序 + 二分 + 贪心」的解法。

**但实际上该做法也仅能解决「两数组元素各不相同」的问题,若允许元素重复,并不存在多项式解法。**

回归到元素互不相同的条件,可以退化出「排序 + 二分」解法。

记 `arr` 数组长度为 $n$,`pieces` 数组长度为 $m$。

起始对 `pieces` 进行排序(根据 $pieces[i]$ 的首位元素排升序),从前往后处理每个 `arr[i]` 尝试匹配,先通过二分找到合适的 $pieces[j]$(即满足首位元素与 `arr[i]` 相同的 $pieces[j]$),并进行连续段的匹配,若匹配失败直接返回 `false`。

Java 代码:
```Java
class Solution {
    public boolean canFormArray(int[] arr, int[][] pieces) {
        int n = arr.length, m = pieces.length;
        Arrays.sort(pieces, (a,b)->a[0]-b[0]);
        for (int i = 0; i < n; ) {
            int l = 0, r = m - 1;
            while (l < r) {
                int mid = l + r + 1 >> 1;
                if (pieces[mid][0] <= arr[i]) l = mid;
                else r = mid - 1;
            }
            int len = pieces[r].length, idx = 0;
            while (idx < len && pieces[r][idx] == arr[i + idx]) idx++;
            if (idx == len) i += len;
            else return false;
        }
        return true;
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    bool canFormArray(vector<int>& arr, vector<vector<int>>& pieces) {
        int n = arr.size(), m = pieces.size();
        sort(pieces.begin(), pieces.end());
        for (int i = 0; i < n; ) {
            int l = 0, r = m - 1;
            while (l < r) {
                int mid = l + r + 1 >> 1;
                if (pieces[mid][0] <= arr[i]) l = mid;
                else r = mid - 1;
            }
            int len = pieces[r].size(), idx = 0;
            while (idx < len && pieces[r][idx] == arr[i + idx]) idx++;
            if (idx == len) i += len;
            else return false;
        }
        return true;
    }
};
```
Python 代码:
```Python
class Solution:
    def canFormArray(self, arr: List[int], pieces: List[List[int]]) -> bool:
        n, m = len(arr), len(pieces)
        pieces.sort(key=lambda x: x[0])
        i = 0
        while i < n:
            l, r = 0, m - 1
            while l < r:
                mid = l + r + 1 >> 1
                if pieces[mid][0] <= arr[i]:
                    l = mid
                else:
                    r = mid - 1
            sz, idx = len(pieces[r]), 0
            while idx < sz and pieces[r][idx] == arr[i + idx]:
                idx += 1
            if idx == sz:
                i += sz
            else:
                return False
        return True
```
TypeScript 代码:
```TypeScript
function canFormArray(arr: number[], pieces: number[][]): boolean {
    const n = arr.length, m = pieces.length
    pieces.sort((a,b)=>a[0]-b[0])
    for (let i = 0; i < n; ) {
        let l = 0, r = m - 1
        while (l < r) {
            const mid = l + r + 1 >> 1
            if (pieces[mid][0] <= arr[i]) l = mid
            else r = mid - 1
        }
        let len = pieces[r].length, idx = 0
        while (idx < len && pieces[r][idx] == arr[i + idx]) idx++
        if (idx == len) i += len
        else return false
    }
    return true
};
```
* 时间复杂度:排序复杂度为 $O(m\log{m})$;构造的复杂度为 $O(n\log{m})$。整体复杂度为 $O(m\log{m} + n\log{m})$
* 空间复杂度:$O(\log{m})$

---

### 哈希表

利用元素各不相同,使用哈希表进行预处理即可:$hash[x] = idx$ 含义为 $pieces[idx]$ 的首位元素为 $x$。

Java 代码:
```Java
class Solution {
    public boolean canFormArray(int[] arr, int[][] pieces) {
        int n = arr.length, m = pieces.length;
        int[] hash = new int[110];
        for (int i = 0; i < m; i++) hash[pieces[i][0]] = i;
        for (int i = 0; i < n; ) {
            int[] cur = pieces[hash[arr[i]]];
            int len = cur.length, idx = 0;
            while (idx < len && cur[idx] == arr[i + idx]) idx++;
            if (idx == len) i += len;
            else return false;
        }
        return true;
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    bool canFormArray(vector<int>& arr, vector<vector<int>>& pieces) {
        int n = arr.size(), m = pieces.size();
        vector<int> hash(110);
        for (int i = 0; i < m; i++) hash[pieces[i][0]] = i;
        for (int i = 0; i < n; ) {
            vector<int> cur = pieces[hash[arr[i]]];
            int len = cur.size(), idx = 0;
            while (idx < len && cur[idx] == arr[i + idx]) idx++;
            if (idx == len) i += len;
            else return false;
        }
        return true;
    }
};
```
Python 代码:
```Python
class Solution:
    def canFormArray(self, arr: List[int], pieces: List[List[int]]) -> bool:
        n, m = len(arr), len(pieces)
        hash = [0] * 110
        for i in range(m):
            hash[pieces[i][0]] = i
        i = 0
        while i < n:
            cur = pieces[hash[arr[i]]]
            sz, idx = len(cur), 0
            while idx < sz and cur[idx] == arr[i + idx]:
                idx += 1
            if idx == sz:
                i += sz
            else:
                return False
        return True
```
TypeScript 代码:
```TypeScript
function canFormArray(arr: number[], pieces: number[][]): boolean {
    const n = arr.length, m = pieces.length
    const hash = new Array<number>(110).fill(0)
    for (let i = 0; i < m; i++) hash[pieces[i][0]] = i
    for (let i = 0; i < n; ) {
        let cur = pieces[hash[arr[i]]], sz = cur.length, idx = 0
        while (idx < sz && cur[idx] == arr[i + idx]) idx++
        if (idx == sz) i += sz
        else return false
    }
    return true
};
```
* 时间复杂度:$O(n + m)$
* 空间复杂度:$O(C)$,其中 $C = 110$ 为两数组的值域大小

### 最后

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

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

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

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


LeetCode/1641-1650/1646. 获取生成数组中的最大值（简单）.md

1646. 获取生成数组中的最大值

Question:
给你一个整数 n 。按下述规则生成一个长度为 n + 1 的数组 nums :

* nums[0] = 0
* nums[1] = 1
* 当 2 <= 2 * i <= n 时,nums[2 * i] = nums[i]
* 当 2 <= 2 * i + 1 <= n 时,nums[2 * i + 1] = nums[i] + nums[i + 1]

返回生成数组 nums 中的 最大 值。

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

输出:3

解释:根据规则:
  nums[0] = 0
  nums[1] = 1
  nums[(1 * 2) = 2] = nums[1] = 1
  nums[(1 * 2) + 1 = 3] = nums[1] + nums[2] = 1 + 1 = 2
  nums[(2 * 2) = 4] = nums[2] = 1
  nums[(2 * 2) + 1 = 5] = nums[2] + nums[3] = 1 + 2 = 3
  nums[(3 * 2) = 6] = nums[3] = 2
  nums[(3 * 2) + 1 = 7] = nums[3] + nums[4] = 2 + 1 = 3
因此,nums = [0,1,1,2,1,3,2,3],最大值 3
```
示例 2:
```
输入:n = 2

输出:1

解释:根据规则,nums[0]、nums[1] 和 nums[2] 之中的最大值是 1
```
示例 3:
```
输入:n = 3

输出:2

解释:根据规则,nums[0]、nums[1]、nums[2] 和 nums[3] 之中的最大值是 2
```

提示:
* 0 <= n <= 100

Solution:
### 模拟

按照题意模拟一遍,得到数列 $nums$,再从 $nums$ 中找出最大值即可。

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

---

### 打表

利用数据范围,可以直接使用 `static` 进行打表构造。

代码:
```Java
class Solution {
    static int N = 110;
    static int[] nums = new int[N];
    static {
        nums[0] = 0;
        nums[1] = 1;
        for (int i = 0; i < N; i++) {
            if (2 * i < N) nums[2 * i] = nums[i];
            if (2 * i + 1 < N) nums[2 * i + 1] = nums[i] + nums[i + 1];
        }
        for (int i = 0, max = 0; i < N; i++) {
            nums[i] = max = Math.max(max, nums[i]);
        }
    }
    public int getMaximumGenerated(int n) {
        return nums[n];
    }
}
```
* 时间复杂度:将打表逻辑放到本地进行,复杂度为 $O(1)$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


Question:
你有一个炸弹需要拆除,时间紧迫!

你的情报员会给你一个长度为 `n` 的 循环 数组 `code` 以及一个密钥 `k` 。

为了获得正确的密码,你需要替换掉每一个数字。所有数字会 同时 被替换。

* 如果 $k > 0$,将第 `i` 个数字用 接下来 `k` 个数字之和替换。
* 如果 $k < 0$,将第 `i` 个数字用 之前 `k` 个数字之和替换。
* 如果 $k = 0$,将第 `i` 个数字用 `0` 替换。

由于 `code` 是循环的, `code[n-1]` 下一个元素是 `code[0]` ,且 `code[0]` 前一个元素是 `code[n-1]` 。

给你 循环 数组 `code` 和整数密钥 `k` ,请你返回解密后的结果来拆除炸弹!

示例 1:
```
输入:code = [5,7,1,4], k = 3

输出:[12,10,16,13]

解释:每个数字都被接下来 3 个数字之和替换。解密后的密码为 [7+1+4, 1+4+5, 4+5+7, 5+7+1]。注意到数组是循环连接的。
```
示例 2:
```
输入:code = [1,2,3,4], k = 0

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

解释:当 k 为 0 时,所有数字都被 0 替换。
```
示例 3:
```
输入:code = [2,4,9,3], k = -2

输出:[12,5,6,13]

解释:解密后的密码为 [3+9, 2+3, 4+2, 9+4] 。注意到数组是循环连接的。如果 k 是负数,那么和为 之前 的数字。
```

提示:
* $n = code.length$
* $1 <= n <= 100$
* $1 <= code[i] <= 100$
* $-(n - 1) <= k <= n - 1$

Solution:
### 前缀和

根据题意 `code` 为循环数组,我们可以建立一个长度为 $2 \times n$ 的前缀和数组(为了方便,我们令前缀和数组下标从 $1$ 开始),利用前缀和数组来构建答案。

对于每一位 $ans[i - 1]$ 而言(其中 $i$ 的取值范围为 $[1, n]$),我们根据 `k` 值的正负情况来决定取自前缀和数组中的哪一段:

* 若有 $k < 0$:需要取位置 $i$ 前的 $-k$ 个数,为防止下越界标,先将位置 $i$ 往后进行 $n$ 个偏移(即位置 $i + n$),随后可知对应区间 $[i + n + k, i + n - 1]$,对应区间和为 $sum[i + n - 1] - sum[i + n + k - 1]$

* 若有 $k > 0$:需要取位置 $i$ 后的 $k$ 个数,对应前缀和数组下标 $[i + 1, i + k]$,对应区间和为 $sum[i + k] - sum[i]$

Java 代码:
```Java
class Solution {
    public int[] decrypt(int[] code, int k) {
        int n = code.length;
        int[] ans = new int[n];
        if (k == 0) return ans;
        int[] sum = new int[n * 2 + 10];
        for (int i = 1; i <= 2 * n; i++) sum[i] += sum[i - 1] + code[(i - 1) % n];
        for (int i = 1; i <= n; i++) {
            if (k < 0) ans[i - 1] = sum[i + n - 1] - sum[i + n + k - 1];
            else ans[i - 1] = sum[i + k] - sum[i];
        }
        return ans;
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    vector<int> decrypt(vector<int>& code, int k) {
        int n = code.size();
        vector<int> ans(n);
        if (k == 0) return ans;
        vector<long long> sum((n + 1) * 2, 0);
        for (int i = 1; i <= 2 * n; ++i) sum[i] = sum[i - 1] + code[(i - 1) % n];
        for (int i = 1; i <= n; ++i) {
            if (k < 0) ans[i - 1] = sum[i + n - 1] - sum[i + n + k - 1];
            else ans[i - 1] = sum[i + k] - sum[i];
        }
        return ans;
    }
};
```
Python 代码:
```Python
class Solution:
    def decrypt(self, code: List[int], k: int) -> List[int]:
        n = len(code)
        ans = [0] * n
        if k == 0:
            return ans
        sum = [0] * (2 * n + 10)
        for i in range(1, 2 * n + 1):
            sum[i] = sum[i - 1] + code[(i - 1) % n]
        for i in range(1, n + 1):
            if k < 0:
                ans[i - 1] = sum[i + n - 1] - sum[i + n + k - 1]
            else:
                ans[i - 1] = sum[i + k] - sum[i]
        return ans
```
TypeScript 代码:
```TypeScript
function decrypt(code: number[], k: number): number[] {
    const n = code.length
    const ans = new Array<number>(n).fill(0)
    if (k == 0) return ans
    const sum = new Array<number>(2 * n + 10).fill(0)
    for (let i = 1; i <= 2 * n; i++) sum[i] = sum[i - 1] + code[(i - 1) % n]
    for (let i = 1; i <= n; i++) {
        if (k < 0) ans[i - 1] = sum[i + n - 1] - sum[i + n + k - 1]
        else ans[i - 1] = sum[i + k] - sum[i]
    }
    return ans
};
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


Question:
有 `n` 个 `(id, value)` 对,其中 `id` 是 `1` 到 `n` 之间的一个整数,`value` 是一个字符串。不存在 `id` 相同的两个 `(id, value)` 对。

设计一个流,以任意顺序获取 `n` 个 `(id, value)` 对,并在多次调用时按 `id` 递增的顺序返回一些值。

实现 `OrderedStream` 类:

* `OrderedStream(int n)` 构造一个能接收 `n` 个值的流,并将当前指针 `ptr` 设为 `1` 。
* `String[] insert(int id, String value)` 向流中存储新的 `(id, value)` 对。存储后:
	* 如果流存储有 `id = ptr` 的 `(id, value)` 对,则找出从 `id = ptr` 开始的 最长 `id` 连续递增序列 ,并 按顺序 返回与这些 `id` 关联的值的列表。然后,将 `ptr` 更新为最后那个  `id + 1` 。
	* 否则,返回一个空列表。

示例:

```
输入
["OrderedStream", "insert", "insert", "insert", "insert", "insert"]
[[5], [3, "ccccc"], [1, "aaaaa"], [2, "bbbbb"], [5, "eeeee"], [4, "ddddd"]]

输出
[null, [], ["aaaaa"], ["bbbbb", "ccccc"], [], ["ddddd", "eeeee"]]

解释
OrderedStream os= new OrderedStream(5);
os.insert(3, "ccccc"); // 插入 (3, "ccccc"),返回 []
os.insert(1, "aaaaa"); // 插入 (1, "aaaaa"),返回 ["aaaaa"]
os.insert(2, "bbbbb"); // 插入 (2, "bbbbb"),返回 ["bbbbb", "ccccc"]
os.insert(5, "eeeee"); // 插入 (5, "eeeee"),返回 []
os.insert(4, "ddddd"); // 插入 (4, "ddddd"),返回 ["ddddd", "eeeee"]
```

提示:
* $1 <= n <= 1000$
* $1 <= id <= n$
* $value.length = 5$
* `value` 仅由小写字母组成
* 每次调用 `insert` 都会使用一个唯一的 `id`
* 恰好调用 `n` 次 `insert`

Solution:
### 模拟

根据题意进行模拟即可。

Java 代码:
```Java
class OrderedStream {
    String[] ss = new String[1010];
    int idx, n;
    public OrderedStream(int _n) {
        Arrays.fill(ss, "");
        idx = 1; n = _n;
    }
    public List<String> insert(int key, String value) {
        ss[key] = value;
        List<String> ans = new ArrayList<>();
        while (ss[idx].length() == 5) ans.add(ss[idx++]);
        return ans;
    }
}
```
TypeScript 代码:
```TypeScript
class OrderedStream {
    ss: string[]
    idx: number; n: number;
    constructor(_n: number) {
        this.idx = 1; this.n = _n;
        this.ss = new Array<string>(1010).fill("")
    }
    insert(key: number, value: string): string[] {
        this.ss[key] = value
        const ans = new Array<string>()
        while (this.ss[this.idx].length == 5) ans.push(this.ss[this.idx++])
        return ans
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n)$

---

### 加餐

**加餐一道近期笔试题 : [近期面试原题(简单计算几何运用)](https://mp.weixin.qq.com/s?__biz=MzU4NDE3MTEyMA==&mid=2247492965&idx=1&sn=d0f2b13578e8b61891fbd7f9d1693695)🎉 🎉**

### 最后

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

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

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

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


LeetCode/1651-1660/1657. 确定两个字符串是否接近（中等）.md

1657. 确定两个字符串是否接近

Question:
如果可以使用以下操作从一个字符串得到另一个字符串,则认为两个字符串 接近 :

* 操作 1:交换任意两个 现有 字符。
	* 例如,`abcde -> aecdb`

* 操作 2:将一个 现有 字符的每次出现转换为另一个 现有 字符,并对另一个字符执行相同的操作。
	* 例如,`aacabb -> bbcbaa`(所有 `a` 转化为 `b` ,而所有的 `b` 转换为 `a`)

你可以根据需要对任意一个字符串多次使用这两种操作。

给你两个字符串,`word1` 和 `word2`。如果 `word1` 和 `word2` 接近 ,就返回 `true`;否则,返回 `false`。

示例 1:
```
输入:word1 = "abc", word2 = "bca"

输出:true

解释:2 次操作从 word1 获得 word2 。
执行操作 1:"abc" -> "acb"
执行操作 1:"acb" -> "bca"
```
示例 2:
```
输入:word1 = "a", word2 = "aa"

输出:false

解释:不管执行多少次操作,都无法从 word1 得到 word2 ,反之亦然。
```
示例 3:
```
输入:word1 = "cabbba", word2 = "abbccc"

输出:true

解释:3 次操作从 word1 获得 word2 。
执行操作 1:"cabbba" -> "caabbb"
执行操作 2:"caabbb" -> "baaccc"
执行操作 2:"baaccc" -> "abbccc"
```
示例 4:
```
输入:word1 = "cabbba", word2 = "aabbss"

输出:false

解释:不管执行多少次操作,都无法从 word1 得到 word2 ,反之亦然。
```

提示:
* $1 <= word1.length, word2.length <= 10^5$
* `word1` 和 `word2` 仅包含小写英文字母

Solution:
### 模拟

两类操作都不会凭空产生或删除字符,而仅仅是对字符进行互换。

由于操作 `1` 和 `2` 都不限次数,因此我们只需检查「字符种类是否相同」和「字符频次是否相等」,即可得出两字符串是否接近的结论。

具体的,由于 `s1` 和 `s2` 都仅包含小写字母,因此可以创建两个大小为 `26` 的数组 `c1` 和 `c2` 分别对两字符串进行词频统计。

随后进行合法性检查:

1. 字符种类是否相同:若存在某个字符仅在 `s1` 或 `s2` 中出现过,两字符串必不接近,返回 `False`
2. 字符频次是否相等:对 `c1` 和 `c2` 进行排序,并逐项检查,若存在 `c1[i] != c2[i]`,说明存在词频为 `c1[i]` 的字符种类数在 `s1` 和 `s2` 间并不相等,返回 `False`

若上述两项检查无误,说明两字符串接近,返回 `True`。

Java 代码:
```Java
class Solution {
    public boolean closeStrings(String s1, String s2) {
        int[] c1 = new int[26], c2 = new int[26];
        for (char c : s1.toCharArray()) c1[c - 'a']++;
        for (char c : s2.toCharArray()) c2[c - 'a']++;
        for (int i = 0; i < 26; i++) {
            if (c1[i] + c2[i] == 0) continue;
            if (c1[i] == 0 || c2[i] == 0) return false;
        }
        Arrays.sort(c1); Arrays.sort(c2);
        for (int i = 0; i < 26; i++) {
            if (c1[i] != c2[i]) return false;
        }
        return true;
    }
}
```
Python 代码:
```Python
class Solution:
    def closeStrings(self, s1: str, s2: str) -> bool:
        c1, c2 = [0] * 26, [0] * 26
        for c in s1:
            c1[ord(c) - ord('a')] += 1
        for c in s2:
            c2[ord(c) - ord('a')] += 1
        for i in range(26):
            if c1[i] + c2[i] == 0:
                continue
            if c1[i] == 0 or c2[i] == 0:
                return False
        c1.sort()
        c2.sort()
        for i in range(26):
            if c1[i] != c2[i]:
                return False
        return True
```
C++ 代码:
```C++
class Solution {
public:
    bool closeStrings(string s1, string s2) {
        vector<int> c1(26, 0), c2(26, 0);
        for (char c : s1) c1[c - 'a']++;
        for (char c : s2) c2[c - 'a']++;
        for (int i = 0; i < 26; i++) {
            if (c1[i] + c2[i] == 0) continue;
            if (c1[i] == 0 || c2[i] == 0) return false;
        }
        sort(c1.begin(), c1.end());
        sort(c2.begin(), c2.end());
        for (int i = 0; i < 26; i++) {
            if (c1[i] != c2[i]) return false;
        }
        return true;
    }
};
```
TypeScirpt 代码:
```TypeScript
function closeStrings(s1: string, s2: string): boolean {
    const c1: number[] = Array(26).fill(0);
    const c2: number[] = Array(26).fill(0);
    for (const c of s1) c1[c.charCodeAt(0) - 'a'.charCodeAt(0)]++;
    for (const c of s2) c2[c.charCodeAt(0) - 'a'.charCodeAt(0)]++;
    for (let i = 0; i < 26; i++) {
        if (c1[i] + c2[i] === 0) continue;
        if (c1[i] === 0 || c2[i] === 0) return false;
    }
    c1.sort((a, b) => a - b);
    c2.sort((a, b) => a - b);
    for (let i = 0; i < 26; i++) {
        if (c1[i] !== c2[i]) return false;
    }
    return true;
};
```
* 时间复杂度:统计词频复杂度为 $O(n + m)$;排序复杂度为 $O(C\log{C})$,其中 $C = 26$ 为字符集大小,进行合法性检查复杂度为 $O(C)$。整体复杂度为 $O(n + m + C\log{C})$
* 空间复杂度:$O(C)$

### 最后

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

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

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

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


LeetCode/1661-1670/1662. 检查两个字符串数组是否相等（简单）.md

1662. 检查两个字符串数组是否相等

Question:
给你两个字符串数组 `word1` 和 `word2`。如果两个数组表示的字符串相同,返回 `true` ;否则,返回 `false` 。

数组表示的字符串 是由数组中的所有元素 按顺序 连接形成的字符串。

示例 1:
```
输入:word1 = ["ab", "c"], word2 = ["a", "bc"]

输出:true

解释:
word1 表示的字符串为 "ab" + "c" -> "abc"
word2 表示的字符串为 "a" + "bc" -> "abc"
两个字符串相同,返回 true
```
示例 2:
```
输入:word1 = ["a", "cb"], word2 = ["ab", "c"]

输出:false
```
示例 3:
```
输入:word1  = ["abc", "d", "defg"], word2 = ["abcddefg"]

输出:true
```

提示:
* $1 <= word1.length, word2.length <= 10^3$
* $1 <= word1[i].length, word2[i].length <= 10^3$
* $1 <= sum(word1[i].length), sum(word2[i].length) <= 10^3$
* `word1[i]` 和 `word2[i]` 由小写字母组成

Solution:
### 双指针

将 `word1` 和 `word2` 的长度记为 $n$ 和 $m$。

题目本质是要我们比较两个数组扁平化后的字符串是否相同。

使用变量 `i` 和 `j` 代表当前处理到哪个 $word1[i]$ 和 $word2[j]$,使用变量 `p` 和 `q` 代表当前比较到 $word1[i]$ 和 $word2[j]$ 的哪一位。

最后根据是否能顺利比较完返回相应答案。

Java 代码:
```Java
class Solution {
    public boolean arrayStringsAreEqual(String[] word1, String[] word2) {
        int n = word1.length, m = word2.length;
        int i = 0, j = 0, p = 0, q = 0;
        while (i < n && j < m) {
            if (word1[i].charAt(p++) != word2[j].charAt(q++)) return false;
            if (p == word1[i].length()) {
                i++; p = 0;
            }
            if (q == word2[j].length()) {
                j++; q = 0;
            }
        }
        return i == n && j == m;
    }
}
```
TypeScript 代码:
```TypeScript
function arrayStringsAreEqual(word1: string[], word2: string[]): boolean {
    const n = word1.length, m = word2.length
    let i = 0, j = 0, p = 0, q = 0
    while (i < n && j < m) {
        if (word1[i][p++] != word2[j][q++]) return false
        if (p == word1[i].length) {
            i++; p = 0
        }
        if (q == word2[j].length) {
            j++; q = 0
        }
    }
    return i == n && j == m
}
```
Python 代码:
```Python
class Solution:
    def arrayStringsAreEqual(self, word1: List[str], word2: List[str]) -> bool:
        n, m = len(word1), len(word2)
        i, j, p, q = 0, 0, 0, 0
        while i < n and j < m:
            if word1[i][p] != word2[j][q]:
                return False
            p, q = p + 1, q + 1
            if p == len(word1[i]):
                i, p = i + 1, 0
            if q == len(word2[j]):
                j, q = j + 1, 0
        return i == n and j == m
```
* 时间复杂度:$O(\sum_{i = 0}^{n - 1}word1[i].length + \sum_{j = 0}^{m - 1}word2[j].length)$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


LeetCode/1661-1670/1668. 最大重复子字符串（简单）.md

1668. 最大重复子字符串

Question:
给你一个字符串 `sequence`,如果字符串 `word` 连续重复 `k` 次形成的字符串是 `sequence` 的一个子字符串,那么单词 `word` 的 重复值为 `k` 。

单词 `word` 的 最大重复值 是单词 `word` 在 `sequence` 中最大的重复值。如果 `word` 不是 `sequence` 的子串,那么重复值 `k` 为 `0` 。

给你一个字符串 `sequence` 和 `word` ,请你返回 最大重复值 `k` 。

示例 1:
```
输入:sequence = "ababc", word = "ab"

输出:2

解释:"abab" 是 "ababc" 的子字符串。
```
示例 2:
```
输入:sequence = "ababc", word = "ba"

输出:1

解释:"ba" 是 "ababc" 的子字符串,但 "baba" 不是 "ababc" 的子字符串。
```
示例 3:
```
输入:sequence = "ababc", word = "ac"

输出:0

解释:"ac" 不是 "ababc" 的子字符串。
```

提示:
* $1 <= sequence.length <= 100$
* $1 <= word.length <= 100$
* `sequence` 和 `word` 都只包含小写英文字母。

Solution:
### 序列 DP

为了方便,我们记 `sequence` 为 `ss`,记 `word` 为 `pp`,将两者长度分别记为 `n` 和 `m`。

同时我们调整「字符串」以及「将要用到的动规数组」的下标从 $1$ 开始。

这是一道入门级的「序列 DP」运用题,容易想到 **定义 $f[i]$ 为了考虑以 `ss[i]` 结尾时的最大重复值**。

不失一般性考虑 $f[i]$ 该如何转移:由于 `pp` 的长度已知,每次转移 $f[i]$ 时我们可以从 `ss` 中截取 **以 $ss[i]$ 为结尾,长度为 $m$ 的后缀字符串** `sub` 并与 `pp` 匹配,若两者相等,说明 `sub` 贡献了大小为 $1$ 的重复度,同时该重复度可累加在 $f[i - m]$ 上(好好回想我们的状态定义),即有状态转移方程:$f[i] = f[i - m] + 1$。

最终所有 $f[i]$ 的最大值即为答案。

Java 代码:
```Java
class Solution {
    public int maxRepeating(String ss, String pp) {
        int n = ss.length(), m = pp.length(), ans = 0;
        int[] f = new int[n + 10];
        for (int i = 1; i <= n; i++) {
            if (i - m < 0) continue;
            if (ss.substring(i - m, i).equals(pp)) f[i] = f[i - m] + 1;
            ans = Math.max(ans, f[i]);
        }
        return ans;
    }
}
```
TypeScript 代码:
```TypeScript
function maxRepeating(ss: string, pp: string): number {
    let n = ss.length, m = pp.length, ans = 0
    const f = new Array<number>(n + 10).fill(0)
    for (let i = 1; i <= n; i++) {
        if (i - m < 0) continue
        if (ss.substr(i - m, i) == pp) f[i] = f[i - m] + 1
        ans = Math.max(ans, f[i])
    }
    return ans
}
```
Python 代码:
```Python
class Solution:
    def maxRepeating(self, ss: str, pp: str) -> int:
        n, m, ans = len(ss), len(pp), 0
        f = [0] * (n + 10)
        for i in range(1, n + 1):
            if i - m < 0:
                continue
            if ss[i - m:i] == pp:
                f[i] = f[i - m] + 1
            ans = max(ans, f[i])
        return ans
```
* 时间复杂度:$O(n \times m)$
* 空间复杂度:$O(n)$

---

### 字符串哈希

解法一的转移瓶颈在于:每次需要花费 $O(m)$ 的复杂度来生成子串,并进行字符串比较。

该过程可用「字符串哈希」进行优化:将 `ss` 和 `pp` 进行拼接得到完整字符串,并计算完整字符串的哈希数组和次方数组。随后从前往后检查 `ss`,若「某个以 $ss[i]$ 结尾长度为 `m` 的后缀字符串哈希值」与「 `pp` 字符串的哈希值」相等,说明找到了前驱状态值 $f[i - m]$,可进行转移。

我们通过 $O(n + m)$ 复杂度预处理了字符串哈希,将转移过程中「复杂度为 $O(m)$ 的子串截取与字符串比较」替换成了「复杂度为 $O(1)$ 的数值对比」,整体复杂度从 $O(n \times m)$ 下降到 $O(n + m)$。

> **不了解「字符串哈希」的同学可见前置 🧀 : [字符串哈希入门](https://mp.weixin.qq.com/s?__biz=MzU4NDE3MTEyMA==&mid=2247489813&idx=1&sn=7f3bc18ca390d85b17655f7164d8e660)。里面详解字符串哈希基本原理以及哈希冲突简单处理方式**

Java 代码:
```Java
class Solution {
    public int maxRepeating(String ss, String pp) {
        int n = ss.length(), m = pp.length(), ans = 0;
        int[] f = new int[n + 10];

        String s = ss + pp;
        int P = 1313131, N = s.length();
        long[] h = new long[N + 10], p = new long[N + 10];
        p[0] = 1;
        for (int i = 1; i <= N; i++) {
            h[i] = h[i - 1] * P + s.charAt(i - 1);
            p[i] = p[i - 1] * P;
        }
        long phash = h[N] - h[N - m] * p[m];

        for (int i = 1; i <= n; i++) {
            if (i - m < 0) continue;
            long cur = h[i] - h[i - m] * p[m];
            if (cur == phash) f[i] = f[i - m] + 1;
            ans = Math.max(ans, f[i]);
        }
        return ans;
    }
}
```
Python 代码:
```Python
class Solution:
    def maxRepeating(self, ss: str, pp: str) -> int:
        n, m, ans = len(ss), len(pp), 0
        f = [0] * (n + 10)

        s = ss + pp
        P, N, MOD = 131, len(s), 987654321
        h, p = [0] * (N + 10), [0] * (N + 10)
        p[0] = 1
        for i in range(1, N + 1):
            h[i] = (h[i - 1] * P + ord(s[i - 1])) % MOD
            p[i] = (p[i - 1] * P) % MOD
        phash = (h[N] - h[N - m] * p[m]) % MOD

        for i in range(1, n + 1):
            if i - m < 0:
                continue
            cur = (h[i] - h[i - m] * p[m]) % MOD
            if cur == phash:
                f[i] = f[i - m] + 1
            ans = max(ans, f[i])
        return ans
```
* 时间复杂度:$O(n + m)$
* 空间复杂度:$O(n + m)$

---

### 总结

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

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

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

序列 DP 通常需要结合题意来寻找前驱状态,即需要自身寻找拓扑序关系(例如本题,需要自己结合题意来找到可转移的前驱状态 $f[i - m]$)。

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

### 最后

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

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

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

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


1669. 合并两个链表

Question:
给你两个链表 `list1` 和 `list2`,它们包含的元素分别为 `n` 个和 `m` 个。

请你将 `list1` 中下标从 `a` 到 `b` 的全部节点都删除,并将 `list2` 接在被删除节点的位置。

下图中蓝色边和节点展示了操作后的结果:

请你返回结果链表的头指针。

示例 1:

```
输入:list1 = [0,1,2,3,4,5], a = 3, b = 4, list2 = [1000000,1000001,1000002]

输出:[0,1,2,1000000,1000001,1000002,5]

解释:我们删除 list1 中下标为 3 和 4 的两个节点,并将 list2 接在该位置。上图中蓝色的边和节点为答案链表。
```
示例 2:

```
输入:list1 = [0,1,2,3,4,5,6], a = 2, b = 5, list2 = [1000000,1000001,1000002,1000003,1000004]

输出:[0,1,1000000,1000001,1000002,1000003,1000004,6]

解释:上图中蓝色的边和节点为答案链表。
```

提示:
* $3 <= list1.length <= 10^4$
* $1 <= a <= b < list1.length - 1$
* $1 <= list2.length <= 10^4$

Solution:
### 模拟

根据题意进行模拟即可。

使用两个变量 `A` 和 `B` 分别指向 `list1` 中两个断联的位置,分别将 `A`  指向 `list2` 的开头,将 `list2` 的 `next` 指针指向 `B`。

Java 代码:
```Java
class Solution {
    public ListNode mergeInBetween(ListNode list1, int a, int b, ListNode list2) {
        ListNode ans = list1;
        ListNode A = null, B = null;
        while (--a > 0 && --b > 0) list1 = list1.next;
        A = list1;
        while (b-- > 0) list1 = list1.next;
        B = list1;
        A.next = list2;
        while (list2.next != null) list2 = list2.next;
        list2.next = B.next;
        return ans;
    }
}
```
TypeScript 代码:
```TypeScript
function mergeInBetween(list1: ListNode | null, a: number, b: number, list2: ListNode | null): ListNode | null {
    const ans = list1
    let A = null, B = null
    while (--a > 0 && --b > 0) list1 = list1.next
    A = list1
    while (b-- > 0) list1 = list1.next
    B = list1
    A.next = list2
    while (list2.next != null) list2 = list2.next
    list2.next = B.next
    return ans
}
```
* 时间复杂度:$O(n + m)$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


1670. 设计前中后队列

Question:
请你设计一个队列,支持在前,中,后三个位置的 `push` 和 `pop` 操作。

请你完成 `FrontMiddleBack` 类:

* `FrontMiddleBack()` 初始化队列。
* `void pushFront(int val)` 将 `val` 添加到队列的 最前面 。
* `void pushMiddle(int val)` 将 `val` 添加到队列的 正中间 。
* `void pushBack(int val)` 将 `val` 添加到队里的 最后面 。
* `int popFront()` 将最前面的元素从队列中删除并返回值,如果删除之前队列为空,那么返回 `-1`。
* `int popMiddle()` 将正中间的元素从队列中删除并返回值,如果删除之前队列为空,那么返回 `-1`。
* `int popBack()` 将 最后面 的元素从队列中删除并返回值,如果删除之前队列为空,那么返回 `-1`。

请注意当有 两个 中间位置的时候,选择靠前面的位置进行操作。比方说:

* 将 `6` 添加到 `[1, 2, 3, 4, 5]` 的中间位置,结果数组为 `[1, 2, 6, 3, 4, 5]`。
* 从 `[1, 2, 3, 4, 5, 6]` 的中间位置弹出元素,返回 `3`,数组变为 `[1, 2, 4, 5, 6]`。

示例 1:
```
输入:
["FrontMiddleBackQueue", "pushFront", "pushBack", "pushMiddle", "pushMiddle", "popFront", "popMiddle", "popMiddle", "popBack", "popFront"]
[[], [1], [2], [3], [4], [], [], [], [], []]

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

解释:
FrontMiddleBackQueue q = new FrontMiddleBackQueue();
q.pushFront(1);   // [1]
q.pushBack(2);    // [1, 2]
q.pushMiddle(3);  // [1, 3, 2]
q.pushMiddle(4);  // [1, 4, 3, 2]
q.popFront();     // 返回 1 -> [4, 3, 2]
q.popMiddle();    // 返回 3 -> [4, 2]
q.popMiddle();    // 返回 4 -> [2]
q.popBack();      // 返回 2 -> []
q.popFront();     // 返回 -1 -> [] (队列为空)
```

提示:
* $1 <= val <= 10^9$
* 最多调用 $1000$ 次 `pushFront`, `pushMiddle`, `pushBack`, `popFront`, `popMiddle` 和 `popBack`。

Solution:
### 双端队列

只要求在头部或尾部高效插入/弹出元素的话,容易联想到双端队列。

还需要考虑往中间插入/弹出元素的话,会想到使用两个双端队列。

将两个双端队列分别称为 `l` 和 `r`,把 `l` 和 `r` 拼接起来就是完整元素列表:

由于双端队列本身支持 $O(1)$ 首尾操作,问题的关键在于如何确保涉及 `Middle` 操作的高效性。

我们可以设计一个 `update` 方法,用于确保两队列的相对平衡:

* 当元素总个数为偶数时,确保两队列元素相等
* 当元素总个数为奇数时,确保 `r` 队列比 `l` 队列元素多一个

如此一来,当我们需要往 `Middle` 插入元素时,始终往 `l` 的尾部插入即可;而当需要读取 `Middle` 位置元素时,根据两队列的元素个数关系决定是从 `l` 的尾部还是从 `r` 的头部取元素。

以下是对上述代码中几个操作的简短实现说明:

* `pushFront`:将元素添加到 `l` 队列的头部,调用 `update` 保持队列平衡
* `pushMiddle`:将元素添加到 `l` 队列的尾部,调用 `update` 保持队列平衡
* `pushBack`:将元素添加到 `r` 队列的尾部,调用 `update` 保持队列平衡
* `popFront`:若 `l` 队列不为空,从 `l` 队列的头部弹出一个元素;否则,从 `r` 队列的头部弹出一个元素(当且仅当元素个数为 $1$ 时,队列 `l` 为空,唯一元素在队列 `r` 中),调用 `update` 保持队列平衡
* `popMiddle`:若 `l` 队列和 `r` 队列的大小相等,则从 `l` 队列的尾部弹出一个元素;否则,从 `r` 队列的头部弹出一个元素。调用 `update` 保持队列平衡
* `popBack`:从 `r` 队列的尾部弹出一个元素,调用 `update` 保持队列平衡

双端队列的实现,可通过「数组 + 首尾坐标指针」来实现。为方便大家理清脉络,先使用语言自带的 `Deque` 实现一版。

Java 代码(`Deque` 版):
```Java
class FrontMiddleBackQueue {
    Deque<Integer> l = new ArrayDeque<>(1010), r = new ArrayDeque<>(1010);
    public void pushFront(int val) {
        l.addFirst(val);
        update();
    }
    public void pushMiddle(int val) {
        l.addLast(val);
        update();
    }
    public void pushBack(int val) {
        r.addLast(val);
        update();
    }
    public int popFront() {
        if (l.size() + r.size() == 0) return -1;
        int ans = l.size() != 0 ? l.pollFirst() : r.pollFirst();
        update();
        return ans;
    }
    public int popMiddle() {
        if (l.size() + r.size() == 0) return -1;
        int ans = l.size() == r.size() ? l.pollLast() : r.pollFirst();
        update();
        return ans;
    }
    public int popBack() {
        if (l.size() + r.size() == 0) return -1;
        int ans = r.pollLast();
        update();
        return ans;
    }
    void update() {
        while (l.size() > r.size()) r.addFirst(l.pollLast());
        while (r.size() - l.size() > 1) l.addLast(r.pollFirst());
    }
}
```

看过 `Deque` 实现版本,考虑如何使用数组实现。

各类操作的总调用次数最多为 $1000$ 次,我们可创建大小为 $2010$ 的数组,并从下标  $1010$(接近中间位置)开始进行存储,这样无论是从前还是往后存数都不会越界。

使用 `lhe` 和 `lta` 代表队列 `l` 的头部和尾部坐标,使用 `rhe` 和 `rta` 代表队列 `r` 的头部和尾部坐标,所有坐标初始值均为 $1100$。

需要注意的是,`ta`(无论是 `lta` 还是 `rta`)是严格指向尾部,因此如果要往尾部插数的话,需要先对指针自增(移到下一个空闲位置),再赋值;而 `he`(无论是 `lhe` 还是 `rhe`)是指向实际队列头部的前一位置,需要先赋值再前移。当 `he = ta` 代表队列为空。

Java 代码(纯数组版):

```Java
class FrontMiddleBackQueue {
    int[] l = new int[2010], r = new int[2010];
    int lhe = 1010, lta = 1010, rhe = 1010, rta = 1010;
    public void pushFront(int val) {
        l[lhe--] = val;
        update();
    }
    public void pushMiddle(int val) {
        l[++lta] = val;
        update();
    }
    public void pushBack(int val) {
        r[++rta] = val;
        update();
    }
    public int popFront() {
        if (getSize(lhe, lta) == 0 && getSize(rhe, rta) == 0) return -1;
        int ans = getSize(lhe, lta) != 0 ? l[++lhe] : r[++rhe];
        update();
        return ans;
    }
    public int popMiddle() {
        if (getSize(lhe, lta) + getSize(rhe, rta) == 0) return -1;
        int ans = getSize(lhe, lta) == getSize(rhe, rta) ? l[lta--] : r[++rhe];
        update();
        return ans;
    }
    public int popBack() {
        if (getSize(lhe, lta) == 0 && getSize(rhe, rta) == 0) return -1;
        int ans = r[rta--];
        update();
        return ans;
    }
    int getSize(int he, int ta) {
        return ta - he;
    }
    void update() {
        while (getSize(lhe, lta) > getSize(rhe, rta)) r[rhe--] = l[lta--];
        while (getSize(rhe, rta) - getSize(lhe, lta) > 1) l[++lta] = r[++rhe];
    }
}
```
C++ 代码:
```C++
class FrontMiddleBackQueue {
public:
    int l[2010], r[2010], lhe = 1010, lta = 1010, rhe = 1010, rta = 1010;
    void pushFront(int val) {
        l[lhe--] = val;
        update();
    }
    void pushMiddle(int val) {
        l[++lta] = val;
        update();
    }
    void pushBack(int val) {
        r[++rta] = val;
        update();
    }
    int popFront() {
        if (getSize(lhe, lta) == 0 && getSize(rhe, rta) == 0) return -1;
        int ans = getSize(lhe, lta) != 0 ? l[++lhe] : r[++rhe];
        update();
        return ans;
    }
    int popMiddle() {
        if (getSize(lhe, lta) == 0 && getSize(rhe, rta) == 0) return -1;
        int ans = getSize(lhe, lta) == getSize(rhe, rta) ? l[lta--] : r[++rhe];
        update();
        return ans;
    }
    int popBack() {
        if (getSize(lhe, lta) == 0 && getSize(rhe, rta) == 0) return -1;
        int ans = r[rta--];
        update();
        return ans;
    }
    int getSize(int he, int ta) {
        return ta - he;
    }
    void update() {
        while (getSize(lhe, lta) > getSize(rhe, rta)) r[rhe--] = l[lta--];
        while (getSize(rhe, rta) - getSize(lhe, lta) > 1) l[++lta] = r[++rhe];
    }
};
```
Python 代码:
```Python
class FrontMiddleBackQueue:
    def __init__(self):
        self.l, self.r = [0] * 2010, [0] * 2010
        self.r = [0] * 2010
        self.lhe, self.lta, self.rhe, self.rta = 1010, 1010, 1010, 1010

    def pushFront(self, val: int) -> None:
        self.l[self.lhe] = val
        self.lhe -= 1
        self.update()

    def pushMiddle(self, val: int) -> None:
        self.lta += 1
        self.l[self.lta] = val
        self.update()

    def pushBack(self, val: int) -> None:
        self.rta += 1
        self.r[self.rta] = val
        self.update()

    def popFront(self) -> int:
        if self.getSize(self.lhe, self.lta) + self.getSize(self.rhe, self.rta) == 0:
            return -1
        if self.getSize(self.lhe, self.lta) != 0:
            self.lhe += 1
            ans = self.l[self.lhe]
        else:
            self.rhe += 1
            ans = self.r[self.rhe]
        self.update()
        return ans

    def popMiddle(self) -> int:
        if self.getSize(self.lhe, self.lta) + self.getSize(self.rhe, self.rta) == 0:
            return -1
        if self.getSize(self.lhe, self.lta) == self.getSize(self.rhe, self.rta):
            ans = self.l[self.lta]
            self.lta -= 1
        else:
            self.rhe += 1
            ans = self.r[self.rhe]
        self.update()
        return ans

    def popBack(self) -> int:
        if self.getSize(self.lhe, self.lta) + self.getSize(self.rhe, self.rta) == 0:
            return -1
        ans = self.r[self.rta]
        self.rta -= 1
        self.update()
        return ans

    def getSize(self, he: int, ta: int) -> int:
        return ta - he

    def update(self) -> None:
        while self.getSize(self.lhe, self.lta) > self.getSize(self.rhe, self.rta):
            self.r[self.rhe] = self.l[self.lta]
            self.rhe -= 1
            self.lta -= 1
        while self.getSize(self.rhe, self.rta) - self.getSize(self.lhe, self.lta) > 1:
            self.lta += 1
            self.rhe += 1
            self.l[self.lta] = self.r[self.rhe]
```
TypeScript 代码:
```TypeScript
class FrontMiddleBackQueue {
    private l: number[];
    private r: number[];
    private lhe: number;
    private lta: number;
    private rhe: number;
    private rta: number;
    constructor() {
        this.l = Array(2010).fill(0), this.r = Array(2010).fill(0);
        this.lhe = 1010, this.lta = 1010, this.rhe = 1010, this.rta = 1010;
    }
    pushFront(val: number): void {
        this.l[this.lhe--] = val;
        this.update();
    }
    pushMiddle(val: number): void {
        this.l[++this.lta] = val;
        this.update();
    }
    pushBack(val: number): void {
        this.r[++this.rta] = val;
        this.update();
    }
    popFront(): number {
        if (this.getSize(this.lhe, this.lta) + this.getSize(this.rhe, this.rta) == 0) return -1;
        const ans = this.getSize(this.lhe, this.lta) != 0 ? this.l[++this.lhe] : this.r[++this.rhe];
        this.update();
        return ans;
    }
    popMiddle(): number {
        if (this.getSize(this.lhe, this.lta) + this.getSize(this.rhe, this.rta) == 0) return -1;
        const ans = this.getSize(this.lhe, this.lta) == this.getSize(this.rhe, this.rta) ? this.l[this.lta--] : this.r[++this.rhe];
        this.update();
        return ans;
    }
    popBack(): number {
        if (this.getSize(this.lhe, this.lta) + this.getSize(this.rhe, this.rta) == 0) return -1;
        const ans = this.r[this.rta--];
        this.update();
        return ans;
    }
    private getSize(he: number, ta: number): number {
        return ta - he;
    }
    private update(): void {
        while (this.getSize(this.lhe, this.lta) > this.getSize(this.rhe, this.rta)) this.r[this.rhe--] = this.l[this.lta--];
        while (this.getSize(this.rhe, this.rta) - this.getSize(this.lhe, this.lta) > 1) this.l[++this.lta] = this.r[++this.rhe];
    }
}
```
* 时间复杂度:所有操作复杂度均为 $O(1)$
* 空间复杂度:$O(n)$

---

### 进阶

更进一步,使用双向链表并与实现 `update` 类似效果,维护 `Middle` 位置的元素节点,同样可实现 $O(1)$ 各项操作,你能完成吗?

与纯数组版相比,使用链表好处在于可严格按需创建。

```Java
class Node {
    Node prev, next;
    int val;
    Node (int _val) {
        val = _val;
    }
}
class FrontMiddleBackQueue {
    Node he, ta, mid;
    int lsz, rsz;
    public FrontMiddleBackQueue() {
        he = new Node(-1); ta = new Node(-1);
        he.next = ta; ta.prev = he;
        mid = ta;
        lsz = rsz = 0;
    }
    public void pushFront(int val) {
        Node cur = new Node(val);
        cur.next = he.next;
        cur.prev = he;
        he.next.prev = cur;
        he.next = cur;
        lsz++;
        update();
    }
    public void pushMiddle(int val) {
        Node cur = new Node(val);
        cur.next = mid;
        cur.prev = mid.prev;
        mid.prev.next = cur;
        mid.prev = cur;
        lsz++;
        update();
    }
    public void pushBack(int val) {
        Node cur = new Node(val);
        cur.next = ta;
        cur.prev = ta.prev;
        ta.prev.next = cur;
        ta.prev = cur;
        rsz++;
        update();
    }
    public int popFront() {
        if (lsz + rsz == 0) return -1;
        int ans = he.next.val;
        he.next.next.prev = he;
        he.next = he.next.next;
        lsz--;
        update();
        return ans;
    }
    public int popMiddle() {
        if (lsz + rsz == 0) return -1;
        Node realMid = null;
        if (lsz == rsz) {
            realMid = mid.prev;
            lsz--;
        } else {
            realMid = mid;
            mid = mid.next;
            rsz--;
        }
        int ans = realMid.val;
        realMid.prev.next = realMid.next;
        realMid.next.prev = realMid.prev;
        realMid = realMid.next;
        update();
        return ans;
    }
    public int popBack() {
        if (lsz + rsz == 0) return -1;
        int ans = ta.prev.val;
        ta.prev.prev.next = ta;
        ta.prev = ta.prev.prev;
        rsz--;
        update();
        return ans;
    }
    void update() {
        while (lsz > rsz) {
            mid = mid.prev;
            lsz--; rsz++;
        }
        while (rsz - lsz > 1) {
            mid = mid.next;
            lsz++; rsz--;
        }
        if (lsz + rsz == 1) mid = ta.prev;
        if (lsz + rsz == 0) mid = ta;
    }
}
```

### 最后

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

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

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

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


LeetCode/1671-1680/1672. 最富有客户的资产总量（简单）.md

1672. 最富有客户的资产总量

Question:
给你一个 $m * n$ 的整数网格 `accounts`,其中 $accounts[i][j]$ 是第 $i$ 位客户在第 $j$ 家银行托管的资产数量。返回最富有客户所拥有的 资产总量 。

客户的 资产总量 就是他们在各家银行托管的资产数量之和。最富有客户就是 资产总量 最大的客户。

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

输出:6

解释:
第 1 位客户的资产总量 = 1 + 2 + 3 = 6
第 2 位客户的资产总量 = 3 + 2 + 1 = 6
两位客户都是最富有的,资产总量都是 6 ,所以返回 6 。
```
示例 2:
```
输入:accounts = [[1,5],[7,3],[3,5]]

输出:10

解释:
第 1 位客户的资产总量 = 6
第 2 位客户的资产总量 = 10
第 3 位客户的资产总量 = 8
第 2 位客户是最富有的,资产总量是 10
```
示例 3:
```
输入:accounts = [[2,8,7],[7,1,3],[1,9,5]]

输出:17
```

提示:
* $m == accounts.length$
* $n == accounts[i].length$
* $1 <= m, n <= 50$
* $1 <= accounts[i][j] <= 100$

Solution:
### 模拟

根据题意进行模拟即可。

```Java
class Solution {
    public int maximumWealth(int[][] accounts) {
        int m = accounts.length, n = accounts[0].length, ans = 0;
        for (int i = 0; i < m; i++) {
            int cur = 0;
            for (int j = 0; j < n; j++) cur += accounts[i][j];
            ans = Math.max(ans, cur);
        }
        return ans;
    }
}
```
* 时间复杂度:$O(m * n)$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


LeetCode/1671-1680/1678. 设计 Goal 解析器（简单）.md

Question:
请你设计一个可以解释字符串 `command` 的 `Goal` 解析器 。

`command` 由 `"G"`、`"()"` 或 `"(al)"` 按某种顺序组成。`Goal` 解析器会将 `"G"` 解释为字符串 `"G"`、`"()"` 解释为字符串 `"o"` ,`"(al)"` 解释为字符串 `"al"` 。

然后,按原顺序将经解释得到的字符串连接成一个字符串。

给你字符串 `command`,返回 `Goal` 解析器 对 `command` 的解释结果。

示例 1:
```
输入:command = "G()(al)"

输出:"Goal"

解释:Goal 解析器解释命令的步骤如下所示:
G -> G
() -> o
(al) -> al
最后连接得到的结果是 "Goal"
```
示例 2:
```
输入:command = "G()()()()(al)"

输出:"Gooooal"
```
示例 3:
```
输入:command = "(al)G(al)()()G"

输出:"alGalooG"
```

提示:
* $1 <= command.length <= 100$
* `command` 由 `"G"`、`"()"` 或 `"(al)"` 按某种顺序组成

Solution:
### 模拟

根据题意进行模拟即可。

Java 代码:
```Java
class Solution {
    public String interpret(String s) {
        StringBuilder sb = new StringBuilder();
        int n = s.length();
        for (int i = 0; i < n; ) {
            if (s.charAt(i) == 'G') {
                sb.append('G'); i++;
            } else if (i + 1 < n && s.charAt(i + 1) == ')') {
                sb.append('o'); i += 2;
            } else {
                sb.append("al"); i += 4;
            }
        }
        return sb.toString();
    }
}
```
TypeScript 代码:
```TypeScript
function interpret(s: string): string {
    const n = s.length
    let ans = ''
    for (let i = 0; i < n; ) {
        if (s[i] == 'G') {
            ans += 'G'; i++
        } else if (i + 1 < n && s[i + 1] == ')') {
            ans += 'o'; i += 2
        } else {
            ans += 'al'; i += 4
        }
    }
    return ans
}
```
Python 代码:
```Python
class Solution:
    def interpret(self, s: str) -> str:
        ans = ''
        n, i = len(s), 0
        while i < n:
            if s[i] == 'G':
                ans, i = ans + s[i], i + 1
            elif i + 1 < n and s[i + 1] == ')':
                ans, i = ans + 'o', i + 2
            else:
                ans, i = ans + 'al', i + 4
        return ans
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


LeetCode/1681-1690/1684. 统计一致字符串的数目（简单）.md

1684. 统计一致字符串的数目

Question:
给你一个由不同字符组成的字符串 `allowed` 和一个字符串数组 `words`。如果一个字符串的每一个字符都在 `allowed` 中,就称这个字符串是 一致字符串 。

请你返回 `words` 数组中 一致字符串 的数目。

示例 1:
```
输入:allowed = "ab", words = ["ad","bd","aaab","baa","badab"]

输出:2

解释:字符串 "aaab" 和 "baa" 都是一致字符串,因为它们只包含字符 'a' 和 'b' 。
```
示例 2:
```
输入:allowed = "abc", words = ["a","b","c","ab","ac","bc","abc"]

输出:7

解释:所有字符串都是一致的。
```
示例 3:
```
输入:allowed = "cad", words = ["cc","acd","b","ba","bac","bad","ac","d"]

输出:4

解释:字符串 "cc","acd","ac" 和 "d" 是一致字符串。
```

提示:
* $1 <= words.length <= 10^4$
* $1 <= allowed.length <= 26$
* $1 <= words[i].length <= 10$
* `allowed` 中的字符 互不相同 。
* `words[i]` 和 `allowed` 只包含小写英文字母。

Solution:
### 模拟

根据题意模拟即可:为了快速判断某个字符是否在 `allowed` 出现过,我们可以使用 `Set` 结构、定长数组或是一个 `int` 变量(搭配位运算)来对 `allowed` 中出现的字符进行转存。

随后遍历所有的 $words[i]$,统计符合要求的字符串个数。

Java 代码:
```Java
class Solution {
    public int countConsistentStrings(String allowed, String[] words) {
        boolean[] hash = new boolean[26];
        for (char c : allowed.toCharArray()) hash[c - 'a'] = true;
        int ans = 0;
        out:for (String s : words) {
            for (char c : s.toCharArray()) {
                if (!hash[c - 'a']) continue out;
            }
            ans++;
        }
        return ans;
    }
}
```
Java 代码:
```Java
class Solution {
    public int countConsistentStrings(String allowed, String[] words) {
        int hash = 0, ans = 0;
        for (char c : allowed.toCharArray()) hash |= (1 << (c - 'a'));
        out:for (String s : words) {
            for (char c : s.toCharArray()) {
                if (((hash >> (c - 'a')) & 1) == 0) continue out;
            }
            ans++;
        }
        return ans;
    }
}
```
TypeScript 代码:
```TypeScript
function countConsistentStrings(allowed: string, words: string[]): number {
    const sset = new Set<string>()
    for (const c of allowed) sset.add(c)
    let ans = 0
    out:for (const s of words) {
        for (const c of s) {
            if (!sset.has(c)) continue out
        }
        ans++
    }
    return ans
}
```
TypeScript 代码:
```TypeScript
function countConsistentStrings(allowed: string, words: string[]): number {
    let hash = 0, ans = 0
    for (const c of allowed) hash |= (1 << (c.charCodeAt(0) - 'a'.charCodeAt(0)))
    out:for (const s of words) {
        for (const c of s) {
            if (((hash >> (c.charCodeAt(0) - 'a'.charCodeAt(0))) & 1) == 0) continue out
        }
        ans++
    }
    return ans
}
```
Python3 代码:
```Python3
class Solution:
    def countConsistentStrings(self, allowed: str, words: List[str]) -> int:
        sset = set([c for c in allowed])
        ans = 0
        for s in words:
            ok = True
            for c in s:
                if c not in sset:
                    ok = False
                    break
            ans += 1 if ok else 0
        return ans
```
Python3 代码:
```Python3
class Solution:
    def countConsistentStrings(self, allowed: str, words: List[str]) -> int:
        num, ans = 0, 0
        for c in allowed:
            num |= (1 << (ord(c) - ord('a')))
        for s in words:
            ok = True
            for c in s:
                if not (num >> (ord(c) - ord('a')) & 1):
                    ok = False
                    break
            ans += 1 if ok else 0
        return ans
```
* 时间复杂度:$O(m + \sum_{i = 0}^{n - 1}words[i].length)$,其中 $m$ 为 `allowed` 长度,$n$ 为 `words` 长度
* 空间复杂度:$O(m)$

### 最后

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

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

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

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


LeetCode/1681-1690/1688. 比赛中的配对次数（简单）.md

1688. 比赛中的配对次数

Question:
给你一个整数 `n` ,表示比赛中的队伍数。比赛遵循一种独特的赛制:

* 如果当前队伍数是 偶数 ,那么每支队伍都会与另一支队伍配对。总共进行 `n / 2` 场比赛,且产生 `n / 2` 支队伍进入下一轮。
* 如果当前队伍数为 奇数 ,那么将会随机轮空并晋级一支队伍,其余的队伍配对。总共进行 `(n - 1) / 2` 场比赛,且产生 `(n - 1) / 2 + 1` 支队伍进入下一轮。

返回在比赛中进行的配对次数,直到决出获胜队伍为止。

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

输出:6

解释:比赛详情:
- 第 1 轮:队伍数 = 7 ,配对次数 = 3 ,4 支队伍晋级。
- 第 2 轮:队伍数 = 4 ,配对次数 = 2 ,2 支队伍晋级。
- 第 3 轮:队伍数 = 2 ,配对次数 = 1 ,决出 1 支获胜队伍。
总配对次数 = 3 + 2 + 1 = 6
```
示例 2:
```
输入:n = 14

输出:13

解释:比赛详情:
- 第 1 轮:队伍数 = 14 ,配对次数 = 7 ,7 支队伍晋级。
- 第 2 轮:队伍数 = 7 ,配对次数 = 3 ,4 支队伍晋级。
- 第 3 轮:队伍数 = 4 ,配对次数 = 2 ,2 支队伍晋级。
- 第 4 轮:队伍数 = 2 ,配对次数 = 1 ,决出 1 支获胜队伍。
总配对次数 = 7 + 3 + 2 + 1 = 13
```

提示:
* 1 <= n <= 200

Solution:
### 脑筋急转弯

这个题不能浪费了,吃个鱼啥的吧 🤣

* [全鱼宴 I](https://leetcode-cn.com/problems/largest-number-at-least-twice-of-others/solution/gong-shui-san-xie-jian-dan-mo-ni-ti-by-a-8179/)
* [全鱼宴 II](https://leetcode-cn.com/problems/calculate-money-in-leetcode-bank/solution/quan-yu-yan-di-er-can-jian-dan-de-shu-xu-zux4/)

共有 $n$ 支队伍,每场比赛淘汰一支,最终剩下一支冠军队,即有 $n - 1$ 支队伍需要通过 $n - 1$ 场比赛来被淘汰。

**代码(感谢 [@5cm/s 🌸](/u/megurine/) 烟花总提供的所有语言版本 🤣 🤣 ):**
```Java
class Solution {
    public int numberOfMatches(int n) {
        return n - 1;
    }
}
```
-
```C++
class Solution {
public:
    int numberOfMatches(int n) {
        return n - 1;
    }
};
```
-
```Python
class Solution(object):
    def numberOfMatches(self, n):
        return n - 1
```
-
```Python3
class Solution:
    def numberOfMatches(self, n: int) -> int:
        return n - 1
```
-
```C
int numberOfMatches(int n){
    return n - 1;
}
```
-
```C#
public class Solution {
    public int NumberOfMatches(int n) {
        return n - 1;
    }
}
```
-
```JS
var numberOfMatches = function(n) {
    return n - 1
};
```
-
```TS
function numberOfMatches(n: number): number {
    return n - 1
};
```
-
```Ruby
def number_of_matches(n)
    n - 1
end
```
-
```Swift
class Solution {
    func numberOfMatches(_ n: Int) -> Int {
        return n - 1
    }
}
```
-
```Go
func numberOfMatches(n int) int {
    return n - 1
}
```
-
```Scala
object Solution {
    def numberOfMatches(n: Int): Int = {
        return n - 1
    }
}
```
-
```Kotlin
class Solution {
    fun numberOfMatches(n: Int): Int {
        return n - 1
    }
}
```
-
```Rust
impl Solution {
    pub fn number_of_matches(n: i32) -> i32 {
        n - 1
    }
}
```
-
```PHP
class Solution {
    function numberOfMatches($n) {
        return $n - 1;
    }
}
```
-
```Racket
(define/contract (number-of-matches n)
  (-> exact-integer? exact-integer?)
    (- n 1)
  )
```
-
```Erlang
-spec number_of_matches(N :: integer()) -> integer().
number_of_matches(N) ->
  N - 1.
```
-
```Elixir
defmodule Solution do
  @spec number_of_matches(n :: integer) :: integer
  def number_of_matches(n) do
    n - 1
  end
end
```
* 时间复杂度:$O(1)$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


LeetCode/1691-1700/1691. 堆叠长方体的最大高度（困难）.md

1691. 堆叠长方体的最大高度

Question:
给你 `n` 个长方体 `cuboids`,其中第 `i` 个长方体的长宽高表示为 $cuboids[i] = [width_i, length_i, height_i]$(下标从 `0` 开始)。

请你从 `cuboids` 选出一个「子集」,并将它们堆叠起来。

如果 $width_{i} <= width_{j}$ 且 $length_{i} <= length_{j}$ 且 $height_{i} <= height_{j}$ ,你就可以将长方体 `i` 堆叠在长方体 `j` 上。你可以通过旋转把长方体的长宽高重新排列,以将它放在另一个长方体上。

返回 堆叠长方体 `cuboids` 可以得到的 最大高度 。

示例 1:

```
输入:cuboids = [[50,45,20],[95,37,53],[45,23,12]]

输出:190

解释:
第 1 个长方体放在底部,53x37 的一面朝下,高度为 95 。
第 0 个长方体放在中间,45x20 的一面朝下,高度为 50 。
第 2 个长方体放在上面,23x12 的一面朝下,高度为 45 。
总高度是 95 + 50 + 45 = 190 。
```
示例 2:
```
输入:cuboids = [[38,25,45],[76,35,3]]

输出:76

解释:
无法将任何长方体放在另一个上面。
选择第 1 个长方体然后旋转它,使 35x3 的一面朝下,其高度为 76 。
```
示例 3:
```
输入:cuboids = [[7,11,17],[7,17,11],[11,7,17],[11,17,7],[17,7,11],[17,11,7]]

输出:102

解释:
重新排列长方体后,可以看到所有长方体的尺寸都相同。
你可以把 11x7 的一面朝下,这样它们的高度就是 17 。
堆叠长方体的最大高度为 6 * 17 = 102 。
```

提示:
* $n = cuboids.length$
* $1 <= n <= 100$
* $1 <= width_{i}, length_{i}, height_{i} <= 100$

Solution:
### 排序 + 序列 DP

定义 $f[i]$ 为考虑前 $i$ 个箱子,且必须使用到第 $i$ 个箱子的最大高度。

不难发现,$f[i]$ 至少为当前箱子中的最大边长,而所有 $f[i]$ 的最大值即是答案。

由于题目规定了只有三边都不超过盒子 `j` 的盒子 `i` 才能放置在上面,因此我们可以先对三边总和进行「降序」排序。

这样当我们处理到盒子 `i` 时,所有能够位于盒子 `i` 底部的盒子 `j` 均位于 `i` 左侧,我们可以通过枚举 $j \in [0, i - 1]$ 的盒子,用满足条件的盒子 `j` 来更新 $f[i]$,即有 $f[i] = \max(f[j] + height_i)$。

实现上,当我们在判定某个盒子 `j` 是否能够位于盒子 `i` 的底部时,并不需要枚举两盒子的旋转情况,可以在进行 DP 之前先对所有盒子进行排序预处理(按照升序排序)。

Java 代码:
```Java
class Solution {
    public int maxHeight(int[][] cs) {
        int n = cs.length;
        for (int i = 0; i < n; i++) Arrays.sort(cs[i]);
        Arrays.sort(cs, (a, b)->(b[0]+b[1]+b[2])-(a[0]+a[1]+a[2]));
        int ans = 0;
        int[] f = new int[n + 10];
        for (int i = 0; i < n; i++) {
            f[i] = cs[i][2];
            for (int j = 0; j < i; j++) {
                if (cs[j][0] >= cs[i][0] && cs[j][1] >= cs[i][1] && cs[j][2] >= cs[i][2]) {
                    f[i] = Math.max(f[i], f[j] + cs[i][2]);
                }
            }
            ans = Math.max(ans, f[i]);
        }
        return ans;
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    int maxHeight(vector<vector<int>>& cs) {
        int n = cs.size();
        for(int i = 0; i < n; i++) sort(cs[i].begin(), cs[i].end());
        sort(cs.begin(), cs.end(), [](vector<int>& a, vector<int>& b){
            return a[0] + a[1] + a[2] > b[0] + b[1] + b[2];
        });
        vector<int> f(n + 10, 0);
        int ans = 0;
        for(int i = 0; i < n; i++) {
            f[i] = cs[i][2];
            for(int j = 0; j < i; j++) {
                if(cs[j][0] >= cs[i][0] && cs[j][1] >= cs[i][1] && cs[j][2] >= cs[i][2]) {
                    f[i] = max(f[i], f[j] + cs[i][2]);
                }
            }
            ans = max(ans, f[i]);
        }
        return ans;
    }
};
```
Python 代码:
```Python
class Solution:
    def maxHeight(self, cs):
        n = len(cs)
        for i in range(n):
            cs[i].sort()
        cs.sort(key=lambda x: -(x[0]+x[1]+x[2]))
        f, ans = [0] * (n+10), 0
        for i in range(n):
            f[i] = cs[i][2]
            for j in range(i):
                if cs[j][0] >= cs[i][0] and cs[j][1] >= cs[i][1] and cs[j][2] >= cs[i][2]:
                    f[i] = max(f[i], f[j] + cs[i][2])
            ans = max(ans, f[i])
        return ans
```
TypeScript 代码:
```TypeScript
function maxHeight(cs: number[][]): number {
    const n = cs.length;
    for (let i = 0; i < n; i++) cs[i].sort((a, b) => a-b);
    cs.sort((a, b) => (b[0] + b[1] + b[2]) - (a[0] + a[1] + a[2]));
    const f: number[] = new Array(n + 10).fill(0);
    let ans = 0;
    for (let i = 0; i < n; i++) {
        f[i] = cs[i][2];
        for (let j = 0; j < i; j++) {
            if (cs[j][0] >= cs[i][0] && cs[j][1] >= cs[i][1] && cs[j][2] >= cs[i][2]) {
                f[i] = Math.max(f[i], f[j] + cs[i][2]);
            }
        }
        ans = Math.max(ans, f[i]);
    }
    return ans;
};
```
* 时间复杂度:$O(n^2)$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


LeetCode/1691-1700/1694. 重新格式化电话号码（简单）.md

1694. 重新格式化电话号码

Question:
给你一个字符串形式的电话号码 `number`。`number` 由数字、空格 `' '`、和破折号 `'-'` 组成。

请你按下述方式重新格式化电话号码。

首先,删除 所有的空格和破折号。

其次,将数组从左到右 每 `3` 个一组 分块,直到 剩下 `4` 个或更少数字。剩下的数字将按下述规定再分块:
* `2` 个数字:单个含 `2` 个数字的块。
* `3` 个数字:单个含 `3` 个数字的块。
* `4` 个数字:两个分别含 `2` 个数字的块。

最后用破折号将这些块连接起来。注意,重新格式化过程中 不应该 生成仅含 `1` 个数字的块,并且 最多 生成两个含 `2` 个数字的块。

返回格式化后的电话号码。

示例 1:
```
输入:number = "1-23-45 6"

输出:"123-456"

解释:数字是 "123456"
步骤 1:共有超过 4 个数字,所以先取 3 个数字分为一组。第 1 个块是 "123" 。
步骤 2:剩下 3 个数字,将它们放入单个含 3 个数字的块。第 2 个块是 "456" 。
连接这些块后得到 "123-456" 。
```
示例 2:
```
输入:number = "123 4-567"

输出:"123-45-67"

解释:数字是 "1234567".
步骤 1:共有超过 4 个数字,所以先取 3 个数字分为一组。第 1 个块是 "123" 。
步骤 2:剩下 4 个数字,所以将它们分成两个含 2 个数字的块。这 2 块分别是 "45" 和 "67" 。
连接这些块后得到 "123-45-67" 。
```
示例 3:
```
输入:number = "123 4-5678"

输出:"123-456-78"

解释:数字是 "12345678" 。
步骤 1:第 1 个块 "123" 。
步骤 2:第 2 个块 "456" 。
步骤 3:剩下 2 个数字,将它们放入单个含 2 个数字的块。第 3 个块是 "78" 。
连接这些块后得到 "123-456-78" 。
```
示例 4:
```
输入:number = "12"

输出:"12"
```
示例 5:
```
输入:number = "--17-5 229 35-39475 "

输出:"175-229-353-94-75"
```

提示:
* $2 <= number.length <= 100$
* `number` 由数字和字符 `'-'` 及 `' '` 组成。
* `number` 中至少含 `2` 个数字。

Solution:
### 模拟

根据题意进行模拟即可。

Java 代码:
```Java
class Solution {
    public String reformatNumber(String number) {
        String s = number.replace(" ", "").replace("-", "");
        int n = s.length();
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < n; i += 3) {
            if (sb.length() != 0) sb.append("-");
            if (i + 5 > n) {
                if (i + 3 >= n) sb.append(s.substring(i));
                else sb.append(s.substring(i, i + 2)).append("-").append(s.substring(i + 2));
                break;
            }
            sb.append(s.substring(i, i + 3));
        }
        return sb.toString();
    }
}
```
Python 代码:
```Python
class Solution:
    def reformatNumber(self, number: str) -> str:
        s = number.replace(" ", "").replace("-", "")
        n, idx = len(s), 0
        ans = ""
        while idx < n:
            if ans:
                ans += "-"
            if idx + 5 > n:
                ans += s[idx:] if idx + 3 >= n else f"{s[idx:idx+2]}-{s[idx+2:]}"
                break
            ans += s[idx:idx+3]
            idx += 3
        return ans
```
TypeScript 代码:
```TypeScript
function reformatNumber(number: string): string {
    let s = ""
    for (let c of number) {
        if (c != " " && c != "-") s += c
    }
    const n = s.length
    let ans = ""
    for (let i = 0; i < n; i += 3) {
        if (ans.length != 0) ans += "-"
        if (i + 5 > n) {
            if (i + 3 >= n) ans += s.substring(i)
            else ans += s.substring(i, i + 2) + "-" + s.substring(i + 2)
            break
        }
        ans += s.substring(i, i + 3)
    }
    return ans
};
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


LeetCode/1691-1700/1697. 检查边长度限制的路径是否存在（困难）.md

1697. 检查边长度限制的路径是否存在

Question:
给你一个 `n` 个点组成的无向图边集 `edgeList`,其中 $edgeList[i] = [u_i, v_i, dis_i]$ 表示点 $u_i$ 和点 $v_i$ 之间有一条长度为 $dis_i$ 的边。请注意,两个点之间可能有 超过一条边 。

给你一个查询数组 `queries`,其中 $queries[j] = [p_j, q_j, limit_j]$ ,你的任务是对于每个查询 $queries[j]$ ,判断是否存在从 $p_j$ 到 $q_j$ 的路径,且这条路径上的每一条边都 严格小于 $limit_j$ 。

请你返回一个 布尔数组 `answer`,其中 `answer.length == queries.length`,当 $queries[j]$ 的查询结果为 `true` 时, `answer` 第 `j` 个值为 `true` ,否则为 `false` 。

示例 1:

```
输入:n = 3, edgeList = [[0,1,2],[1,2,4],[2,0,8],[1,0,16]], queries = [[0,1,2],[0,2,5]]

输出:[false,true]

解释:上图为给定的输入数据。注意到 0 和 1 之间有两条重边,分别为 2 和 16 。
对于第一个查询,0 和 1 之间没有小于 2 的边,所以我们返回 false 。
对于第二个查询,有一条路径(0 -> 1 -> 2)两条边都小于 5 ,所以这个查询我们返回 true 。
```
示例 2:

```
输入:n = 5, edgeList = [[0,1,10],[1,2,5],[2,3,9],[3,4,13]], queries = [[0,4,14],[1,4,13]]

输出:[true,false]

解释:上图为给定数据。
```

提示:
* $2 <= n <= 10^5$
* $1 <= edgeList.length, queries.length <= 10^5$
* $edgeList[i].length == 3$
* $queries[j].length == 3$
* $0 <= u_i, v_i, p_j, q_j <= n - 1$
* $u_i != v_i$
* $p_j != q_j$
* $1 <= dis_i, limit_j <= 10^9$
* 两个点之间可能有多条边。

Solution:
### 排序 + 并查集 + 双指针

为了方便,我们将点数记为 `n`,边数记为 `m`,询问数量记为 `k`,将 `edgeList` 简化为 `es`,将 `queries` 简化为 `qs`。

对于点边数量都在 $10^5$,同时询问次数也在 $10^5$ 的问题,不可能对于每个询问执行最短路算法,尤其还需考虑边权限制。

**对于一个询问 $(a, b, limit)$ 而言,等价于问我们使用所有边权小于 `limit` 的边,能否使得 `a` 和 `b` 两点联通。**

关于回答连通性问题,容易想到并查集。同时我们可以通过「调整回答询问的顺序」来降低复杂度(避免重复重置并查集和添加某些边),即转换为离线问题来处理。

> 何为离线问题?预先知道所有询问,能够通过调整回答询问的顺序,来降低算法复杂度。同时不同询问相互独立,不会因为调整询问顺序,对每个询问的结果造成影响。例如莫队算法。

具体的,我们可以对边集 `es` 和所有询问 `qs` 分别按照「边权」以及「限制」排升序。为了排序后,仍能知道当前询问的原编号,我们要将所有的 `qs[i]` 转换为四元组。

随后从前往后处理每个询问 `qs[i] = (a, b, t, idx)`,同时使用变量 `j` 来记录当前处理到的边。在查询 `a` 和 `b` 是否连通前,先将边集 `es` 中所有所有边权小于 `t` 的边应用到并查集上,从而实现每次 `ans[idx] = query(a, b)` 查询到的是原图中所有边权小于限制值 `t` 的子图。

Java 代码:
```Java
class Solution {
    static int N = 100010;
    static int[] p = new int[N];
    int find(int x) {
        if (p[x] != x) p[x] = find(p[x]);
        return p[x];
    }
    void union(int a, int b) {
        p[find(a)] = p[find(b)];
    }
    boolean query(int a, int b) {
        return find(a) == find(b);
    }
    public boolean[] distanceLimitedPathsExist(int n, int[][] es, int[][] _qs) {
        for (int i = 0; i < n; i++) p[i] = i;
        int m = es.length, k = _qs.length;
        int[][] qs = new int[k][4];
        for (int i = 0; i < k; i++) qs[i] = new int[]{_qs[i][0], _qs[i][1], _qs[i][2], i};
        Arrays.sort(qs, (a,b)->a[2]-b[2]);
        Arrays.sort(es, (a,b)->a[2]-b[2]);
        boolean[] ans = new boolean[k];
        for (int i = 0, j = 0; i < k; i++) {
            int a = qs[i][0], b = qs[i][1], t = qs[i][2], idx = qs[i][3];
            while (j < m && es[j][2] < t) {
                union(es[j][0], es[j][1]);
                j++;
            }
            ans[idx] = query(a, b);
        }
        return ans;
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    int p[100010];
    int find(int x) {
        if (p[x] != x) p[x] = find(p[x]);
        return p[x];
    }
    void unite(int a, int b) {
        p[find(a)] = find(b);
    }
    bool query(int a, int b) {
        return find(a) == find(b);
    }
    vector<bool> distanceLimitedPathsExist(int n, vector<vector<int>>& es, vector<vector<int>>& qs) {
        for (int i = 0; i < n; i++) p[i] = i;
        int m = es.size(), k = qs.size();
        vector<vector<int>> qss(k, vector<int>(4, 0));
        for (int i = 0; i < k; i++) qss[i] = {qs[i][0], qs[i][1], qs[i][2], i};
        sort(qss.begin(), qss.end(), [](const vector<int>& a, const vector<int>& b) {
            return a[2] < b[2];
        });
        sort(es.begin(), es.end(), [](const vector<int>& a, const vector<int>& b) {
            return a[2] < b[2];
        });
        vector<bool> ans(k, false);
        for (int i = 0, j = 0; i < k; i++) {
            int a = qss[i][0], b = qss[i][1], t = qss[i][2], idx = qss[i][3];
            while (j < m && es[j][2] < t) {
                unite(es[j][0], es[j][1]);
                j++;
            }
            ans[idx] = query(a, b);
        }
        return ans;
    }
};
```
Python 代码:
```Python
class Solution:
    def distanceLimitedPathsExist(self, n: int, es: List[List[int]], _qs: List[List[int]]) -> List[bool]:
        p = [i for i in range(n)]
        def find(x):
            if p[x] != x:
                p[x] = find(p[x])
            return p[x]
        def union(a, b):
            p[find(a)] = p[find(b)]
        def query(a, b):
            return find(a) == find(b)
        m, k = len(es), len(_qs)
        qs = [(a, b, c, i) for i, (a, b, c) in enumerate(_qs)]
        es.sort(key=lambda x: x[2])
        qs.sort(key=lambda x: x[2])
        j = 0
        ans = [False] * k
        for i in range(k):
            a, b, t, idx = qs[i]
            while j < m and es[j][2] < t:
                union(es[j][0], es[j][1])
                j += 1
            ans[idx] = query(a, b)
        return ans
```
TypeScript 代码:
```TypeScript
function distanceLimitedPathsExist(n: number, es: number[][], _qs: number[][]): boolean[] {
    const p = new Array<number>(n).fill(0)
    for (let i = 0; i < n; i++) p[i] = i;
    function find(x: number): number {
        if (p[x] != x) p[x] = find(p[x])
        return p[x]
    }
    function union(a: number, b: number): void {
        p[find(a)] = p[find(b)]
    }
    function query(a: number, b: number): boolean {
        return find(a) == find(b)
    }
    const m = es.length, k = _qs.length
    const qs = []
    for (let i = 0; i < k; i++) qs.push([_qs[i][0], _qs[i][1], _qs[i][2], i])
    qs.sort((a, b)=>a[2]-b[2])
    es.sort((a, b)=>a[2]-b[2])
    const ans = new Array<boolean>(k).fill(false)
    for (let i = 0, j = 0; i < k; i++) {
        const a = qs[i][0], b = qs[i][1], t = qs[i][2], idx = qs[i][3]
        while (j < m && es[j][2] < t) {
            union(es[j][0], es[j][1])
            j++
        }
        ans[idx] = query(a, b)
    }
    return ans
}
```
* 时间复杂度:初始化并查集的复杂度为 $O(n)$;对所有边进行排序复杂度为 $O(m\log{m})$;对所有询问进行排序复杂度为 $O(k\log{k})$;统计答案时使用双指针的方式将所有边运用到并查集上,整体复杂度为 $O(k + m)$。整体复杂度为 $O(n + m\log{m} + k\log{k})$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


LeetCode/1691-1700/1700. 无法吃午餐的学生数量（简单）.md

1700. 无法吃午餐的学生数量

Question:
学校的自助午餐提供圆形和方形的三明治,分别用数字 $0$ 和 $1$ 表示。所有学生站在一个队列里,每个学生要么喜欢圆形的要么喜欢方形的。

餐厅里三明治的数量与学生的数量相同。所有三明治都放在一个 栈 里,每一轮:

* 如果队列最前面的学生 喜欢 栈顶的三明治,那么会 拿走它 并离开队列。
* 否则,这名学生会 放弃这个三明治 并回到队列的尾部。

这个过程会一直持续到队列里所有学生都不喜欢栈顶的三明治为止。

给你两个整数数组 `students` 和 `sandwiches`,其中 `sandwiches[i]` 是栈里面第 `i` 个三明治的类型(`i = 0` 是栈的顶部), `students[j]` 是初始队列里第 `j` 名学生对三明治的喜好(`j = 0` 是队列的最开始位置)。请你返回无法吃午餐的学生数量。

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

输出:0

解释:
- 最前面的学生放弃最顶上的三明治,并回到队列的末尾,学生队列变为 students = [1,0,0,1]。
- 最前面的学生放弃最顶上的三明治,并回到队列的末尾,学生队列变为 students = [0,0,1,1]。
- 最前面的学生拿走最顶上的三明治,剩余学生队列为 students = [0,1,1],三明治栈为 sandwiches = [1,0,1]。
- 最前面的学生放弃最顶上的三明治,并回到队列的末尾,学生队列变为 students = [1,1,0]。
- 最前面的学生拿走最顶上的三明治,剩余学生队列为 students = [1,0],三明治栈为 sandwiches = [0,1]。
- 最前面的学生放弃最顶上的三明治,并回到队列的末尾,学生队列变为 students = [0,1]。
- 最前面的学生拿走最顶上的三明治,剩余学生队列为 students = [1],三明治栈为 sandwiches = [1]。
- 最前面的学生拿走最顶上的三明治,剩余学生队列为 students = [],三明治栈为 sandwiches = []。
所以所有学生都有三明治吃。
```
示例 2:
```
输入:students = [1,1,1,0,0,1], sandwiches = [1,0,0,0,1,1]

输出:3
```

提示:
* $1 <= students.length, sandwiches.length <= 100$
* $students.length == sandwiches.length$
* `sandwiches[i]` 要么是 $0$ ,要么是 $1$ 。
* `students[i]` 要么是 $0$ ,要么是 $1$ 。

Solution:
### 模拟

根据题意进行模拟即可 : 当学生遇到喜欢的种类会进行匹配,否则会轮到队列尾部,而面包则是一直停在栈顶位置等待匹配。

因此当且仅当栈顶的面包种类没有待匹配的学生种类与之相对应时,整个匹配过程结束。

Java 代码:
```Java
class Solution {
    public int countStudents(int[] a, int[] b) {
        int[] cnts = new int[2];
        for (int x : a) cnts[x]++;
        for (int i = 0; i < b.length; i++) {
            if (--cnts[b[i]] == -1) return b.length - i;
        }
        return 0;
    }
}
```
TypeScript 代码:
```TypeScript
function countStudents(a: number[], b: number[]): number {
    const cnts = new Array<number>(2).fill(0)
    for (const x of a) cnts[x]++
    for (let i = 0; i < b.length; i++) {
        if (--cnts[b[i]] == -1) return b.length - i
    }
    return 0
}
```
Python 代码:
```Python
class Solution:
    def countStudents(self, a: List[int], b: List[int]) -> int:
        cnts = Counter(a)
        for i, x in enumerate(b):
            if cnts[x] == 0:
                return len(b) - i
            cnts[x] -= 1
        return 0
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(C)$,其中 $C = 2$ 为种类数

### 最后

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

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

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

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


LeetCode/1701-1710/1704. 判断字符串的两半是否相似（简单）.md

1704. 判断字符串的两半是否相似

Question:
给你一个偶数长度的字符串 `s` 。将其拆分成长度相同的两半,前一半为 `a` ,后一半为 `b` 。

两个字符串 相似 的前提是它们都含有相同数目的元音`('a','e','i','o','u','A','E','I','O','U')`。注意,`s` 可能同时含有大写和小写字母。

如果 `a` 和 `b` 相似,返回 `true`;否则,返回 `false` 。

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

输出:true

解释:a = "bo" 且 b = "ok" 。a 中有 1 个元音,b 也有 1 个元音。所以,a 和 b 相似。
```
示例 2:
```
输入:s = "textbook"

输出:false

解释:a = "text" 且 b = "book" 。a 中有 1 个元音,b 中有 2 个元音。因此,a 和 b 不相似。
注意,元音 o 在 b 中出现两次,记为 2 个。
```

提示:
* $2 <= s.length <= 1000$
* `s.length` 是偶数
* `s` 由 大写和小写 字母组成

Solution:
### 模拟

根据题意进行模拟即可。

为了快速判断某个字符是否为元音字母,起始先对所有元音字母进行转存。

随后对 `s` 进行遍历,使用单个变量 `cnt` 记录元音字母数量。若当前遍历到的 `c` 所在下标属于前半部分,对 `cnt` 进行自增操作,若属于后半部分,对 `cnt` 进行自减操作。

当处理完整个 `s` 后满足 `cnt = 0` 说明前半部分元音字母和后半部分元音字母数量相同。

Java 代码:
```Java
class Solution {
    public boolean halvesAreAlike(String s) {
        Set<Character> set = new HashSet<>();
        for (char c : "aeiouAEIOU".toCharArray()) set.add(c);
        int n = s.length(), cnt = 0;
        for (int i = 0; i < n; i++) {
            if (!set.contains(s.charAt(i))) continue;
            cnt += i < n / 2 ? 1 : -1;
        }
        return cnt == 0;
    }
}
```
TypeScript 代码:
```TypeScript
function halvesAreAlike(s: string): boolean {
    let n = s.length, cnt = 0
    const set = new Set<String>()
    for (const c of "aeiouAEIOU") set.add(c)
    for (let i = 0; i < n; i++) {
        if (!set.has(s[i])) continue
        cnt += i < n / 2 ? 1 : -1
    }
    return cnt == 0
}
```
Python 代码:
```Python
class Solution:
    def halvesAreAlike(self, s: str) -> bool:
        cnt = 0
        ss = set('aeiouAEIOU')
        for idx, c in enumerate(s):
            if c not in ss:
                continue
            cnt += 1 if idx < len(s) / 2 else -1
        return cnt == 0
```
* 时间复杂度:$O(C + n)$,其中 $C$ 为元音字母数量
* 空间复杂度:$O(C)$

### 最后

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

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

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

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


LeetCode/1701-1710/1705. 吃苹果的最大数目（中等）.md

1705. 吃苹果的最大数目

Question:
有一棵特殊的苹果树,一连 `n` 天,每天都可以长出若干个苹果。

在第 `i` 天,树上会长出 `apples[i]` 个苹果,这些苹果将会在 `days[i]` 天后(也就是说,第 `i + days[i]` 天时)腐烂,变得无法食用。

也可能有那么几天,树上不会长出新的苹果,此时用 `apples[i] == 0` 且 `days[i] == 0` 表示。

你打算每天 最多 吃一个苹果来保证营养均衡。注意,你可以在这 n 天之后继续吃苹果。

给你两个长度为 `n` 的整数数组 `days` 和 `apples` ,返回你可以吃掉的苹果的最大数目。

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

输出:7

解释:你可以吃掉 7 个苹果:
- 第一天,你吃掉第一天长出来的苹果。
- 第二天,你吃掉一个第二天长出来的苹果。
- 第三天,你吃掉一个第二天长出来的苹果。过了这一天,第三天长出来的苹果就已经腐烂了。
- 第四天到第七天,你吃的都是第四天长出来的苹果。
```
示例 2:
```
输入:apples = [3,0,0,0,0,2], days = [3,0,0,0,0,2]

输出:5

解释:你可以吃掉 5 个苹果:
- 第一天到第三天,你吃的都是第一天长出来的苹果。
- 第四天和第五天不吃苹果。
- 第六天和第七天,你吃的都是第六天长出来的苹果。
```

提示:
* $apples.length == n$
* $days.length == n$
* $1 <= n <= 2 * 10^4$
* $0 <= apples[i], days[i] <= 2 * 10^4$
* 只有在 `apples[i] = 0` 时,`days[i] = 0` 才成立

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

这题是一道经典的结合优先队列(堆)的贪心题,与结合排序的贪心题一样,属于最为常见的贪心题型。

**直觉上,我们会觉得「优先吃掉最快过期的苹果」会是最优,而这个维护苹果过期的过程,可以使用「小根堆」来实现。**

苹果数量很大,但产生苹果的天数最多为 $2 * 10^4$,因此我们以二元组 `(最后食用日期, 当日产生苹果数量)` 的形式存入「小根堆」进行维护。

具体的,我们可以按照如下逻辑进行模拟(令 $n$ 为数组长度,$time$ 为当前时间,$ans$ 为吃到的苹果数量):

* 首先,如果「$time < n$」或者「堆不为空」,说明「还有苹果未被生成」或者「未必吃掉」,继续模拟;
* 在当日模拟中,如果「$time < n$」,说明当天有苹果生成,先将苹果 **以二元组 $(time + days[time] - 1, apples[time])$ 形式加入小根堆中**;

    > 其中二元组表示 $(最后食用日期, 当日产生苹果数量)$,同时需要过滤 $apples[time] = 0$ 的情况。
* 然后尝试从堆中取出「最后食用日期」最早「可食用」的苹果 `cur`,如果堆顶元素已过期,则抛弃;
* 如果吃掉 `cur` 一个苹果后,仍有剩余,并且最后时间大于当前时间(尚未过期),将 `cur` 重新入堆;
* 循环上述逻辑,直到所有苹果出堆。

**直观感受往往会骗人,我们使用「归纳法 + 反证法」证明上述猜想的正确性**

假设使用上述吃法得到的苹果序列为 $(a_1, a_2, ... ,a_n)$,而真实最优解对应序列为 $(b_1, b_2, ..., b_m)$。

我们目的是证明 $n = m$,即吃掉苹果数量一致(贪心解是最优解的具体方案之一)。

起始,我们先证明边界成立,即 $b_1$ 可以为 $a_1$。

首先该替换操作必然不会使最优序列变长,否则就违背了最优解是长度最长的合法序列这一前提。

由于贪心解中每次总是取「过期时间最早的」苹果来吃,因此有 $a_1$ 过期时间小于等于 $b_1$ 过期时间,需要证明如果将最优解中的 $b_1$ 替换为 $a_1$,并不会影响整个序列的长度。

重点在与该操作是否会使得最优序列长度变短,这需要分情况讨论:

* $a_1$ 不存在与 $(b_2, b_3, ..., b_m)$ 当中,此时直接用 $a_1$ 替换 $b_1$ 自然不会对后面的序列产生影响,也就是说替换后,最优序列合法性仍有保证,同时长度不变,结果不会变差;

* $a_1$ 为 $(b_2, b_3, ..., b_m)$ 中的某个,假设为 $b_i = a_1$,由于 $b_i/a_1$ 在贪心解中的先出堆(过期时间最早),因此必然也有 $b_i/a_1$ 过期时间早于等于 $b_1$ 的过期时间,此时将 $b_1$ 放到 $b_i$ 的位置仍是合法(过期时间比 $b_i/a_1$ 要晚),即将 $b_1$ 和 $b_i/a_1$ 交换位置,最优序列合法性仍有保证,同时长度不变,结果不会变差。

当贪心解和最优解的首个位置决策相同(吃掉的苹果一样),下一位置决策所面临的条件完全相同,归纳推理所依赖的结构没有发生改变,上述分析同样成立。

也就是说,上述推理可以推广到最优解序列中的任意位置。

**至此,我们证明出了最优解可以调整为我们的贪心序列,同时长度不变,即贪心解为最优解的具体方案之一。**

代码:
```Java
class Solution {
    public int eatenApples(int[] apples, int[] days) {
        PriorityQueue<int[]> q = new PriorityQueue<>((a,b)->a[0]-b[0]);
        int n = apples.length, time = 0, ans = 0;
        while (time < n || !q.isEmpty()) {
            if (time < n && apples[time] > 0) q.add(new int[]{time + days[time] - 1, apples[time]});
            while (!q.isEmpty() && q.peek()[0] < time) q.poll();
            if (!q.isEmpty()) {
                int[] cur = q.poll();
                if (--cur[1] > 0 && cur[0] > time) q.add(cur);
                ans++;
            }
            time++;
        }
        return ans;
    }
}
```
* 时间复杂度:令 $n$ 为数组长度,最多有 $n$ 组苹果入堆/出堆。复杂度为 $O(n\log{n})$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


Question:
用一个大小为 `m x n` 的二维网格 $grid$ 表示一个箱子。你有 $n$ 颗球。箱子的顶部和底部都是开着的。

箱子中的每个单元格都有一个对角线挡板,跨过单元格的两个角,可以将球导向左侧或者右侧。

* 将球导向右侧的挡板跨过左上角和右下角,在网格中用 $1$ 表示。
* 将球导向左侧的挡板跨过右上角和左下角,在网格中用 $-1$ 表示。

在箱子每一列的顶端各放一颗球。每颗球都可能卡在箱子里或从底部掉出来。如果球恰好卡在两块挡板之间的 `"V"` 形图案,或者被一块挡导向到箱子的任意一侧边上,就会卡住。

返回一个大小为 $n$ 的数组 $answer$ ,其中 $answer[i]$ 是球放在顶部的第 $i$ 列后从底部掉出来的那一列对应的下标,如果球卡在盒子里,则返回 $-1$ 。

示例 1:

```
输入:grid = [[1,1,1,-1,-1],[1,1,1,-1,-1],[-1,-1,-1,1,1],[1,1,1,1,-1],[-1,-1,-1,-1,-1]]

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

解释:示例如图:
b0 球开始放在第 0 列上,最终从箱子底部第 1 列掉出。
b1 球开始放在第 1 列上,会卡在第 2、3 列和第 1 行之间的 "V" 形里。
b2 球开始放在第 2 列上,会卡在第 2、3 列和第 0 行之间的 "V" 形里。
b3 球开始放在第 3 列上,会卡在第 2、3 列和第 0 行之间的 "V" 形里。
b4 球开始放在第 4 列上,会卡在第 2、3 列和第 1 行之间的 "V" 形里。
```
示例 2:
```
输入:grid = [[-1]]

输出:[-1]

解释:球被卡在箱子左侧边上。
```
示例 3:
```
输入:grid = [[1,1,1,1,1,1],[-1,-1,-1,-1,-1,-1],[1,1,1,1,1,1],[-1,-1,-1,-1,-1,-1]]

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

提示:
* $m == grid.length$
* $n == grid[i].length$
* $1 <= m, n <= 100$
* $grid[i][j]$ 为 $1$ 或 $-1$

Solution:
### 模拟

数据范围只有 $100$,直接模拟每个球从顶部的某列出发,最终到底底部哪列即可(如果可以到达的话)。

我们使用 `r` 和 `c` 表示小球当前所处的位置,受重力影响,在不被卡住的情况下,`r` 会持续自增,直到到达底部,而 `c` 的变化,则是取决于当前挡板 `grid[r][c]` 的方向,若 `grid[r][c]` 为 $1$,则小球的下一个位置为 $(r + 1, c + 1)$;若 `grid[r][c]` 为 $-1$,则下一位置为 $(r + 1, c - 1)$,即可以统一表示为 $(r + 1, c + grid[r][c])$。当且仅当小球在本次移动过程中没被卡住,才能继续移动。即只有 $c + grid[r][c]$ 没有超过矩阵的左右边界(没有被边界卡住),或者 $grid[r][c]$ 和 $grid[r][c + grid[r][c]]$ 同向(不形成夹角),小球方能继续移动。

代码:
```Java
class Solution {
    int m, n;
    int[][] g;
    public int[] findBall(int[][] grid) {
        g = grid;
        m = g.length; n = g[0].length;
        int[] ans = new int[n];
        for (int i = 0; i < n; i++) ans[i] = getVal(i);
        return ans;
    }
    int getVal(int x) {
        int r = 0, c = x;
        while (r < m) {
            int ne = c + g[r][c];
            if (ne < 0 || ne >= n) return -1;
            if (g[r][c] != g[r][ne]) return -1;
            r++; c = ne;
        }
        return c;
    }
}
```
* 时间复杂度:$O(m * n)$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


LeetCode/1701-1710/1707. 与数组中元素的最大异或值（困难）.md

1707. 与数组中元素的最大异或值

Question:
给你一个由非负整数组成的数组 $nums$ 。另有一个查询数组 $queries$,其中 $queries[i] = [xi, mi]$ 。

第 $i$ 个查询的答案是 $x_i$ 和任何 $nums$ 数组中不超过 $m_i$ 的元素按位异或(XOR)得到的最大值。

换句话说,答案是 `max(nums[j] \{XOR} x_i)` ,其中所有 $j$ 均满足 $nums[j] <= m_i$ 。如果 $nums$ 中的所有元素都大于 $m_i$,最终答案就是 $-1$ 。

返回一个整数数组 $answer$ 作为查询的答案,其中 $answer.length == queries.length$ 且 $answer[i]$ 是第 $i$ 个查询的答案。

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

输出:[3,3,7]

解释:
1) 0 和 1 是仅有的两个不超过 1 的整数。0 XOR 3 = 3 而 1 XOR 3 = 2 。二者中的更大值是 3 。
2) 1 XOR 2 = 3.
3) 5 XOR 2 = 7.
```
示例 2:
```
输入:nums = [5,2,4,6,6,3], queries = [[12,4],[8,1],[6,3]]

输出:[15,-1,5]
```

提示:
* 1 <= nums.length, queries.length <= $10^5$
* queries[i].length == 2
* 0 <= nums[j], xi, mi <= $10^9$

Solution:
### 基本分析

**在做本题之前,请先确保已经完成 [421. 数组中两个数的最大异或值](https://leetcode-cn.com/problems/maximum-xor-of-two-numbers-in-an-array/solution/gong-shui-san-xie-noxiang-xin-ke-xue-xi-bmjdg/)。**

这种提前给定了所有询问的题目,我们可以运用离线思想(调整询问的回答顺序)进行求解。

对于本题有两种离线方式可以进行求解。

---

### 普通 Trie

第一种方法基本思路是:**不一次性地放入所有数,而是每次将需要参与筛选的数字放入 $Trie$,再进行与 [421. 数组中两个数的最大异或值](https://leetcode-cn.com/problems/maximum-xor-of-two-numbers-in-an-array/solution/gong-shui-san-xie-noxiang-xin-ke-xue-xi-bmjdg/) 类似的贪心查找逻辑。**

具体的,我们可以按照下面的逻辑进行处理:

1. 对 `nums` 进行「从小到大」进行排序,对 `queries` 的第二维进行「从小到大」排序(排序前先将询问原本的下标映射关系存下来)。
2. 按照排序顺序处理所有的 `queries[i]`:
    1. 在回答每个询问前,将小于等于 `queries[i][1]` 的数值存入 $Trie$。由于我们已经事先对 `nums` 进行排序,因此这个过程只需要维护一个在 `nums` 上有往右移动的指针即可。
    2. 然后利用贪心思路,查询每个 `queries[i][0]` 所能找到的最大值是多少,计算异或和(此过程与 [421. 数组中两个数的最大异或值](https://leetcode-cn.com/problems/maximum-xor-of-two-numbers-in-an-array/solution/gong-shui-san-xie-noxiang-xin-ke-xue-xi-bmjdg/) 一致)。
    3. 找到当前询问在原询问序列的下标,将答案存入。

代码:
```Java
class Solution {
    static int N = (int)1e5 * 32;
    static int[][] trie = new int[N][2];
    static int idx = 0;
    public Solution() {
        for (int i = 0; i <= idx; i++) {
            Arrays.fill(trie[i], 0);
        }
        idx = 0;
    }
    void add(int x) {
        int p = 0;
        for (int i = 31; i >= 0; i--) {
            int u = (x >> i) & 1;
            if (trie[p][u] == 0) trie[p][u] = ++idx;
            p = trie[p][u];
        }
    }
    int getVal(int x) {
        int ans = 0;
        int p = 0;
        for (int i = 31; i >= 0; i--) {
            int a = (x >> i) & 1, b = 1 - a;
            if (trie[p][b] != 0) {
                p = trie[p][b];
                ans = ans | (b << i);
            } else {
                p = trie[p][a];
                ans = ans | (a << i);
            }
        }
        return ans ^ x;
    }
    public int[] maximizeXor(int[] nums, int[][] qs) {
        int m = nums.length, n = qs.length;

        // 使用哈希表将原本的顺序保存下来
        Map<int[], Integer> map = new HashMap<>();
        for (int i = 0; i < n; i++) map.put(qs[i], i);

        // 将 nums 与 queries[x][1] 进行「从小到大」进行排序
        Arrays.sort(nums);
        Arrays.sort(qs, (a, b)->a[1]-b[1]);

        int[] ans = new int[n];
        int loc = 0; // 记录 nums 中哪些位置之前的数已经放入 Trie
        for (int[] q : qs) {
            int x = q[0], limit = q[1];
            // 将小于等于 limit 的数存入 Trie
            while (loc < m && nums[loc] <= limit) add(nums[loc++]);
            if (loc == 0) {
                ans[map.get(q)] = -1;
            } else {
                ans[map.get(q)] = getVal(x);
            }
        }
        return ans;
    }
}
```
* 时间复杂度:令 `nums` 的长度为 `m`,`qs` 的长度为 `n`。两者排序的复杂度为 $O(m\log{m})$ 和 $O(n\log{n})$;将所有数插入 $Trie$ 和从 $Trie$ 中查找的复杂度均为 $O(Len)$,$Len$ 为 $32$。
整体复杂度为 $O(m\log{m} + n\log{n} + (m + n) * Len)$ = $O(m * \max(\log{m}, Len) + n * \max(\log{n}, Len))$。
* 空间复杂度:$O(C)$。其中 $C$ 为常数,固定为 $1e5 * 32 * 2$。

---

### 计数 Trie & 二分

另外一个比较「增加难度」的做法是,将整个过程翻转过来:**一次性存入所有的 $Trie$ 中,然后每次将不再参与的数从 $Trie$ 中移除。相比于解法一,这就要求我们为 $Trie$ 增加一个「删除/计数」功能,并且需要实现二分来找到移除元素的上界下标是多少。**

具体的,我们可以按照下面的逻辑进行处理:

1. 对 `nums` 进行「从大到小」进行排序,对 `queries` 的第二维进行「从大到小」排序(排序前先将询问原本的下标映射关系存下来)。
2. 按照排序顺序处理所有的 `queries[i]`:
    1. 在回答每个询问前,通过「二分」找到在 `nums` 中第一个满足「小于等于 `queries[i][1]` 的下标在哪」,然后将该下标之前的数从 $Trie$ 中移除。同理,这个过程我们需要使用一个指针来记录上一次删除的下标位置,避免重复删除。
    2. 然后利用贪心思路,查询每个 `queries[i][0]` 所能找到的最大值是多少。注意这是要判断当前节点是否有被计数,如果没有则返回 $-1$。
    3. 找到当前询问在原询问序列的下标,将答案存入。

代码:
```Java
class Solution {
    static int N = (int)1e5 * 32;
    static int[][] trie = new int[N][2];
    static int[] cnt = new int[N];
    static int idx = 0;
    public Solution() {
        for (int i = 0; i <= idx; i++) {
            Arrays.fill(trie[i], 0);
            cnt[i] = 0;
        }
        idx = 0;
    }
    // 往 Trie 存入(v = 1)/删除(v = -1) 某个数 x
    void add(int x, int v) {
        int p = 0;
        for (int i = 31; i >= 0; i--) {
            int u = (x >> i) & 1;
            if (trie[p][u] == 0) trie[p][u] = ++idx;
            p = trie[p][u];
            cnt[p] += v;
        }
    }
    int getVal(int x) {
        int ans = 0;
        int p = 0;
        for (int i = 31; i >= 0; i--) {
            int a = (x >> i) & 1, b = 1 - a;
            if (cnt[trie[p][b]] != 0) {
                p = trie[p][b];
                ans = ans | (b << i);
            } else if (cnt[trie[p][a]] != 0) {
                p = trie[p][a];
                ans = ans | (a << i);
            } else {
                return -1;
            }
        }
        return ans ^ x;
    }
    public int[] maximizeXor(int[] nums, int[][] qs) {
        int n = qs.length;

        // 使用哈希表将原本的顺序保存下来
        Map<int[], Integer> map = new HashMap<>();
        for (int i = 0; i < n; i++) map.put(qs[i], i);

        // 对两者排降序
        sort(nums);
        Arrays.sort(qs, (a, b)->b[1]-a[1]);

        // 将所有数存入 Trie
        for (int i : nums) add(i, 1);

        int[] ans = new int[n];
        int left = -1; // 在 nums 中下标「小于等于」left 的值都已经从 Trie 中移除
        for (int[] q : qs) {
            int x = q[0], limit = q[1];
            // 二分查找到待删除元素的右边界,将其右边界之前的所有值从 Trie 中移除。
            int right = getRight(nums, limit);
            for (int i = left + 1; i < right; i++) add(nums[i], -1);
            left = right - 1;
            ans[map.get(q)] = getVal(x);
        }
        return ans;
    }
    // 二分找到待删除的右边界
    int getRight(int[] nums, int limit) {
        int l = 0, r = nums.length - 1;
        while (l < r) {
            int mid = l + r >> 1;
            if (nums[mid] <= limit) {
                r = mid;
            } else {
                l = mid + 1;
            }
        }
        return nums[r] <= limit ? r : r + 1;
    }
    // 对 nums 进行降序排序(Java 没有 Api 直接支持对基本类型 int 排倒序,其他语言可忽略)
    void sort(int[] nums) {
        Arrays.sort(nums);
        int l = 0, r = nums.length - 1;
        while (l < r) {
            int c = nums[r];
            nums[r--] = nums[l];
            nums[l++] = c;
        }
    }
}
```
* 时间复杂度:令 `nums` 的长度为 `m`,`qs` 的长度为 `n`,常数 $Len = 32$。两者排序的复杂度为 $O(m\log{m})$ 和 $O(n\log{n})$;将所有数插入 $Trie$ 的复杂度为 $O(m * Len)$;每个查询都需要经过「二分」找边界,复杂度为 $O(n\log{m})$;最坏情况下所有数都会从 $Trie$ 中被标记删除,复杂度为 $O(m * Len)$。
整体复杂度为 $O(m\log{m} + n\log{n} + n\log{m} + mLen)$ = $O(m * \max(\log{m}, Len) + n * \max(\log{m}, \log{n}))$。
* 空间复杂度:$O(C)$。其中 $C$ 为常数,固定为 $1e5 * 32 * 3$。

---

### 说明

这两种方法我都是采取「数组实现」,而且由于数据范围较大,都使用了 `static` 来优化大数组创建,具体的「优化原因」与「类实现 Trie 方式」可以在题解 [421. 数组中两个数的最大异或值](https://leetcode-cn.com/problems/maximum-xor-of-two-numbers-in-an-array/solution/gong-shui-san-xie-noxiang-xin-ke-xue-xi-bmjdg/) 查看,这里不再赘述。

### 最后

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

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

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

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


LeetCode/1701-1710/1710. 卡车上的最大单元数（简单）.md

1710. 卡车上的最大单元数

Question:
请你将一些箱子装在 一辆卡车 上。给你一个二维数组 `boxTypes`,其中 $boxTypes[i] = [numberOfBoxes_i, numberOfUnitsPerBox_i]$ :

* $numberOfBoxes_i$ 是类型 `i` 的箱子的数量。
* $numberOfUnitsPerBox_i$ 是类型 `i` 每个箱子可以装载的单元数量。

整数 `truckSize` 表示卡车上可以装载 箱子 的 最大数量 。只要箱子数量不超过 `truckSize`,你就可以选择任意箱子装到卡车上。

返回卡车可以装载 单元 的 最大 总数。

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

输出:8

解释:箱子的情况如下:
- 1 个第一类的箱子,里面含 3 个单元。
- 2 个第二类的箱子,每个里面含 2 个单元。
- 3 个第三类的箱子,每个里面含 1 个单元。
可以选择第一类和第二类的所有箱子,以及第三类的一个箱子。
单元总数 = (1 * 3) + (2 * 2) + (1 * 1) = 8
```
示例 2:
```
输入:boxTypes = [[5,10],[2,5],[4,7],[3,9]], truckSize = 10

输出:91
```

提示:
* $1 <= boxTypes.length <= 1000$
* $1 <= numberOfBoxes_i, numberOfUnitsPerBox_i <= 1000$
* $1 <= truckSize <= 10^6$

Solution:
### 贪心

为了方便,我们令 `boxTypes` 为 `bs`,令 `truckSize` 为 `k`。

由于每个箱子成本相同(均占用一个箱子位置),因此在 `k` 给定的情况下,我们应当尽可能选择装载单元数量大的箱子。

这引导我们可以将 `bs` 根据「装载单元数量」排倒序,然后从前往后 `pick` 箱子,直到没有空闲位置。

Java 代码:
```Java
class Solution {
    public int maximumUnits(int[][] bs, int k) {
        int n = bs.length, ans = 0;
        Arrays.sort(bs, (a,b)->b[1]-a[1]);
        for (int i = 0, cnt = 0; i < n && cnt < k; i++) {
            int a = bs[i][0], b = bs[i][1], c = Math.min(a, k - cnt);
            cnt += c; ans += c * b;
        }
        return ans;
    }
}
```
TypeScript 代码:
```TypeScript
function maximumUnits(bs: number[][], k: number): number {
    let n = bs.length, ans = 0
    bs.sort((a,b)=>b[1]-a[1])
    for (let i = 0, cnt = 0; i < n && cnt < k; i++) {
        const a = bs[i][0], b = bs[i][1], c = Math.min(a, k - cnt)
        cnt += c; ans += c * b
    }
    return ans
}
```
Python 代码:
```Python
class Solution:
    def maximumUnits(self, bs: List[List[int]], k: int) -> int:
        bs.sort(key=lambda x: -x[1])
        n, ans = len(bs), 0
        idx, cnt = 0, 0
        while idx < n and cnt < k:
            a, b, c = bs[idx][0], bs[idx][1], min(bs[idx][0], k - cnt)
            cnt, ans = cnt + c, ans + b * c
            idx += 1
        return ans
```
* 时间复杂度:排序复杂度为 $O(n\log{n})$,同时答案复杂度为 $O(n)$。整体复杂度为 $O(n\log{n})$
* 空间复杂度:$O(\log{n})$

### 最后

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

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

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

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


Question:
给你一个字符串 columnTitle ,表示 Excel 表格中的列名称。返回该列名称对应的列序号。

例如,
```
A -> 1
B -> 2
C -> 3
...
Z -> 26
AA -> 27
AB -> 28
...
```
示例 1:
```
输入: columnTitle = "A"

输出: 1
```
示例 2:
```
输入: columnTitle = "AB"

输出: 28
```
示例 3:
```
输入: columnTitle = "ZY"

输出: 701
```
示例 4:
```
输入: columnTitle = "FXSHRXW"

输出: 2147483647
```

提示:
* 1 <= columnTitle.length <= 7
* columnTitle 仅由大写英文组成
* columnTitle 在范围 ["A", "FXSHRXW"] 内

Solution:
### 进制转换

也许你没专门做过「进制转换」类的题目,但相信你确实在 $10$ 进制中使用过如下的「进制转换」手段。

如果题目是 $10$ 进制转换,那么你会很容易想到如下转换过程:**从高位向低位处理,起始让 $ans$ 为 $0$,每次使用当前位数值更新 $ans$,更新规则为 $ans = ans * 10 + val_i$。**

举个🌰,假设存在某个十进制数字,编码为 $ABCD$(字母与数字的映射关系与本题相同),转换过程如下:

> $ans$ = 0
$ans = ans * 10 + 1$ => A
$ans = ans * 10 + 2$ => B
$ans = ans * 10 + 3$ => C
$ans = ans * 10 + 4$ => D

同理,本题只是将 $10$ 进制换成 $26$ 进制。

代码:
```Java
class Solution {
    public int titleToNumber(String s) {
        char[] cs = s.toCharArray();
        int n = cs.length;
        int ans = 0;
        for (int i = 0; i < n; i++) {
            ans = ans * 26 + (cs[i] - 'A' + 1);
        }
        return ans;
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:因为 `toCharArray` 会创建与 `s` 等长的数组,因此使用 `charAt` 代替 `toCharArray` 的话为 $O(1)$,否则为 $O(n)$

---

### 拓展

与本题互逆的题目,你可以一同复习一下 ~

* [168. Excel表列名称](https://leetcode-cn.com/problems/excel-sheet-column-title/) : [题解](https://leetcode-cn.com/problems/excel-sheet-column-title/solution/gong-shui-san-xie-cong-1-kai-shi-de-26-j-g2ur/)

### 最后

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

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

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

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


Question:
给定一个整数 $n$ ,返回 $n!$ 结果中尾随零的数量。

提示 `n! = n * (n - 1) * (n - 2) * ... * 3 * 2 * 1`

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

输出:0

解释:3! = 6 ,不含尾随 0
```
示例 2:
```
输入:n = 5

输出:1

解释:5! = 120 ,有一个尾随 0
```
示例 3:
```
输入:n = 0

输出:0
```

提示:
* $0 <= n <= 10^4$

进阶:你可以设计并实现对数时间复杂度的算法来解决此问题吗?

Solution:
### 数学

对于任意一个 $n!$ 而言,其尾随零的个数取决于展开式中 $10$ 的个数,而 $10$ 可由质因数 $2 * 5$ 而来,因此 $n!$ 的尾随零个数为展开式中各项分解质因数后 $2$ 的数量和 $5$ 的数量中的较小值。

即问题转换为对 $[1, n]$ 中的各项进行分解质因数,能够分解出来的 $2$ 的个数和 $5$ 的个数分别为多少。

为了更具一般性,我们分析对 $[1, n]$ 中各数进行分解质因数,能够分解出质因数 $p$ 的个数为多少。根据每个数能够分解出 $p$ 的个数进行分情况讨论:

* 能够分解出至少一个 $p$ 的个数为 $p$ 的倍数,在 $[1, n]$ 范围内此类数的个数为 $c_1 = \left \lfloor \frac{n}{p} \right \rfloor$
* 能够分解出至少两个 $p$ 的个数为 $p^2$ 的倍数,在 $[1, n]$ 范围内此类数的个数为 $c_2 = \left \lfloor \frac{n}{p^2} \right \rfloor$
* ...
* 能够分解出至少 $k$ 个 $p$ 的个数为 $p^k$ 的倍数,在 $[1, n]$ 范围内此类数的个数为 $c_k = \left \lfloor \frac{n}{p^k} \right \rfloor$

**我们定义一个合法的 $k$ 需要满足 $p^k \leqslant n$,上述的每一类数均是前一类数的「子集」(一个数如果是 $p^k$ 的倍数,必然是 $p^{k-1}$ 的倍数),因此如果一个数是 $p^k$ 的倍数,其出现在的集合数量为 $k$,与其最终贡献的 $p$  的数量相等。**

回到本题,$n!$ 中质因数 $2$ 的数量为 :
$$
\sum_{i = 1}^{k_1}\left \lfloor \frac{n}{2^i} \right \rfloor = \left \lfloor \frac{n}{2} \right \rfloor + \left \lfloor \frac{n}{2^2} \right \rfloor + ... + \left \lfloor \frac{n}{2^{k_1}} \right \rfloor
$$

$n!$ 中质因数 $5$ 的数量为 :
$$
\sum_{i = 1}^{k_2}\left \lfloor \frac{n}{5^i} \right \rfloor = \left \lfloor \frac{n}{5} \right \rfloor + \left \lfloor \frac{n}{5^2} \right \rfloor + ... + \left \lfloor \frac{n}{5^{k_2}} \right \rfloor
$$

由 $2 < 5$,可知 $k_2 \leqslant k_1$,同时 $i$  相同的每一项满足 $\left \lfloor \frac{n}{5^i} \right \rfloor \leqslant \left \lfloor \frac{n}{2^i} \right \rfloor$,可知最终 $\sum_{i = 1}^{k_2}\left \lfloor \frac{n}{5^i} \right \rfloor \leqslant \sum_{i = 1}^{k_1}\left \lfloor \frac{n}{2^i} \right \rfloor$,即质因数 $5$ 的个数必然不会超过质因数 $2$ 的个数。我们只需要统计质因数 $5$ 的个数即可。

代码:
```Java
class Solution {
    public int trailingZeroes(int n) {
        return n == 0 ? 0 : n / 5 + trailingZeroes(n / 5);
    }
}
```

```Python
class Solution:
    def trailingZeroes(self, n: int) -> int:
        return n // 5 + self.trailingZeroes(n // 5) if n else 0
```
* 时间复杂度:$O(\log{n})$
* 空间复杂度:忽略递归带来的额外空间开销,复杂度为 $O(1)$

### 最后

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

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

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

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


173. 二叉搜索树迭代器

Question:
实现一个二叉搜索树迭代器类 `BSTIterator`,表示一个按中序遍历二叉搜索树(`BST`)的迭代器:
* `BSTIterator(TreeNode root)` 初始化 `BSTIterator` 类的一个对象。`BST` 的根节点 `root` 会作为构造函数的一部分给出。指针应初始化为一个不存在于 `BST` 中的数字,且该数字小于 `BST` 中的任何元素。
* `boolean hasNext()` 如果向指针右侧遍历存在数字,则返回 `true`;否则返回 `false`。
* `int next()` 将指针向右移动,然后返回指针处的数字。

注意,指针初始化为一个不存在于 `BST` 中的数字,所以对 `next()` 的首次调用将返回 `BST` 中的最小元素。

你可以假设 `next()` 调用总是有效的,也就是说,当调用 `next()` 时,`BST` 的中序遍历中至少存在一个下一个数字。

示例:

```
输入
["BSTIterator", "next", "next", "hasNext", "next", "hasNext", "next", "hasNext", "next", "hasNext"]
[[[7, 3, 15, null, null, 9, 20]], [], [], [], [], [], [], [], [], []]
输出
[null, 3, 7, true, 9, true, 15, true, 20, false]

解释
BSTIterator bSTIterator = new BSTIterator([7, 3, 15, null, null, 9, 20]);
bSTIterator.next();    // 返回 3
bSTIterator.next();    // 返回 7
bSTIterator.hasNext(); // 返回 True
bSTIterator.next();    // 返回 9
bSTIterator.hasNext(); // 返回 True
bSTIterator.next();    // 返回 15
bSTIterator.hasNext(); // 返回 True
bSTIterator.next();    // 返回 20
bSTIterator.hasNext(); // 返回 False
```

提示:
* 树中节点的数目在范围 [1, $10^5$] 内
* $0 <= Node.val <= 10^6$
* 最多调用 $10^5$ 次 `hasNext` 和 `next` 操作

进阶:
* 你可以设计一个满足下述条件的解决方案吗?`next()` 和 `hasNext()` 操作均摊时间复杂度为 $O(1)$ ,并使用 $O(h)$ 内存。其中 `h` 是树的高度。

Solution:
### 基本思路

**这道题本质上考的是「将迭代版的中序遍历代码」做等价拆分。**

我们知道,中序遍历的基本逻辑是:处理左子树 -> 处理当前节点 -> 处理右子树。

其中迭代做法是利用「栈」进行处理:

1. 先将当前节点的所有左子树压入栈,压到没有为止
2. 将最后一个压入的节点弹出(栈顶元素),加入答案
3. 将当前弹出的节点作为当前节点,重复步骤一

相应的裸题在这里:[94. 二叉树的中序遍历](https://leetcode-cn.com/problems/binary-tree-inorder-traversal/)

中序遍历的迭代代码:
```java
class Solution {
    List<Integer> ans = new ArrayList<>();
    Deque<TreeNode> d = new ArrayDeque<>();
    public List<Integer> inorderTraversal(TreeNode root) {
        while (root != null || !d.isEmpty()) {
            // 步骤 1
            while (root != null) {
                d.addLast(root);
                root = root.left;
            }

            // 步骤 2
            root = d.pollLast();
            ans.add(root.val);

            // 步骤 3
            root = root.right;
        }
        return ans;
    }
}
```

总的来说是这么一个迭代过程:步骤 1 -> 步骤 2 -> 步骤 3 -> 步骤 1 ...

***

### 「中序遍历」代码的「等价拆分」

首先因为 `next()` 方法中我们需要输出一个值,执行的的是「步骤 2」的逻辑,同时我们需要在其前后添加「步骤 1」和「步骤 3」。

另外,我们还有一个 `hasNext()` 要处理,显然 `hasNext()` 应该对应我们的栈是否为空。

为此,我们**需要确保每次输出之后「步骤 1」被及时执行。**

综上,我们应该在初始化时,走一遍「步骤 1」,然后在 `next()` 方法中走「步骤 2」、「步骤 3」和「步骤 1」。

代码:
```java []
class BSTIterator {
    Deque<TreeNode> d = new ArrayDeque<>();
    public BSTIterator(TreeNode root) {
        // 步骤 1
        dfsLeft(root);
    }

    public int next() {
        // 步骤 2
        TreeNode root = d.pollLast();
        int ans = root.val;
        // 步骤 3
        root = root.right;
        // 步骤 1
        dfsLeft(root);
        return ans;
    }

    void dfsLeft(TreeNode root) {
        while (root != null) {
            d.addLast(root);
            root = root.left;
        }
    }

    public boolean hasNext() {
        return !d.isEmpty();
    }
}
```
* 时间复杂度:由于每个元素都是严格「进栈」和「出栈」一次,复杂度为均摊 $O(1)$
* 空间复杂度:栈内最多保存与深度一致的节点数量,复杂度为 $O(h)$

***

### 进阶

事实上,我们空间复杂度也能做到 $O(1)$,该如何做呢?

### 最后

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

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

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

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


Question:
给定一组非负整数 `nums`,重新排列每个数的顺序(每个数不可拆分)使之组成一个最大的整数。

注意:输出结果可能非常大,所以你需要返回一个字符串而不是整数。

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

输出:"210"
```
示例 2:
```
输入:nums = [3,30,34,5,9]

输出:"9534330"
```
示例 3:
```
输入:nums = [1]

输出:"1"
```
示例 4:
```
输入:nums = [10]

输出:"10"
```

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

Solution:
### 贪心

对于 $nums$ 中的任意两个值 $a$ 和 $b$,我们无法直接从常规角度上确定其大小/先后关系。

但我们可以根据「结果」来决定 $a$ 和 $b$ 的排序关系:

如果拼接结果 $ab$ 要比 $ba$ 好,那么我们会认为 $a$ 应该放在 $b$ 前面。

另外,注意我们需要处理前导零(最多保留一位)。

Java 代码:
```java
class Solution {
    public String largestNumber(int[] nums) {
        int n = nums.length;
        String[] ss = new String[n];
        for (int i = 0; i < n; i++) ss[i] = "" + nums[i];
        Arrays.sort(ss, (a, b) -> {
            String sa = a + b, sb = b + a ;
            return sb.compareTo(sa);
        });
        StringBuilder sb = new StringBuilder();
        for (String s : ss) sb.append(s);
        int len = sb.length();
        int k = 0;
        while (k < len - 1 && sb.charAt(k) == '0') k++;
        return sb.substring(k);
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    string largestNumber(vector<int>& nums) {
        int n = nums.size();
        vector<string> ss(n);
        for (int i = 0; i < n; i++) ss[i] = to_string(nums[i]);
        sort(ss.begin(), ss.end(), [](const string& a, const string& b) {
            return a + b > b + a;
        });
        string result;
        for (const string& s : ss) result += s;
        int len = result.length(), k = 0;
        while (k < len - 1 && result[k] == '0') k++;
        return result.substr(k);
    }
};
```
Python 代码:
```Python
class Solution:
    def largestNumber(self, nums: List[int]) -> str:
        nums = list(map(str, nums))
        nums.sort(key=lambda x: x * 10, reverse=True)
        result = ''.join(nums)
        k = 0
        while k < len(result) - 1 and result[k] == '0':
            k += 1
        return result[k:]
```
TypeScript 代码:
```TypeScript
function largestNumber(nums: number[]): string {
    const ss = nums.map(num => num.toString());
    ss.sort((a, b) => (b + a).localeCompare(a + b));
    let result = '';
    for (const s of ss) result += s;
    let k = 0;
    while (k < result.length - 1 && result[k] === '0') k++;
    return result.substring(k);
};
```
* 时间复杂度:由于是对 $String$ 进行排序,当排序对象不是 $Java$ 中的基本数据类型时,不会使用快排(考虑排序稳定性问题)。`Java` 中的 `Arrays.sort()` 的底层实现会「元素数量/元素是否大致有序」决定是使用插入排序还是归并排序。这里直接假定使用的是「插入排序」。复杂度为 $O(n^2)$
* 空间复杂度:$O(n)$

---

### 证明

上述解法,我们需要证明两个内容:

* 该贪心策略能取到全局最优解。
* 这样的「排序比较逻辑」应用在集合 $nums$ 上具有[「全序关系」](https://baike.baidu.com/item/%E5%85%A8%E5%BA%8F%E5%85%B3%E7%B3%BB)。

#### 1. 该贪心策略能取到全局最优解

令我们经过这样的贪心操作得到的贪心解为 $ans$,真实最优解为 $max$。

由于真实最优解为全局最大值,而我们的贪心解至少是一个合法解(一个数),因此天然有 $ans \leqslant max$。

**接下来我们只需要证明 $ans \geqslant max$,即可得 $ans = max$(贪心解即为最优解)。**

我们使用「反证法」来证明 $ans \geqslant max$ 成立:

假设 $ans \geqslant max$ 不成立,即有 $ans < max$。

$ans$ 和 $max$ 都是由同样一批数字凑成的,如果有 $ans < max$。

这意味着我们可以将 $ans$ 中的某些低位数字和高位数字互换,使得 $ans$ 更大(调整为 $max$),这与我们根据「结果」进行排序的逻辑冲突。

因此 $ans < max$ 必然不成立,得证 $ans \geqslant max$ 成立,结合 $ans \leqslant max$ 可得贪心解为最优。

举个🌰,如果有 $ans < max$,那么意味着在 $ans$ 中至少有一对数字互换可以使得 $ans$ 变大,

那么在排序逻辑中 $x$ 所在的整体(可能不只有 $x$ 一个数)应该被排在 $y$ 所在的整体(可能不只有 $y$ 一个数)前面。

#### 2. 全序关系

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

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

**2.1 完全性**

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

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

**2.2 反对称性**

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

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

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

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

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

**2.3 传递性**

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

这里的「传递性」其实也可以使用与 [官方题解](https://leetcode-cn.com/problems/largest-number/solution/zui-da-shu-by-leetcode-solution-sid5/) 类似的手法来证明。

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

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

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

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

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

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

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

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

---

### 找 $i$ 的越界问题

考虑

(1) a = 304 b = 30
(2) a = 301 b = 30

两种情况。

显然,(1)下我们会得到 $a@b$,而(2)下我们会得到 $b@a$

但是,在这种情况下 $i$ 实际上位于 $b$ 界外,那我们还能不能找 $i$ 呢?$b[i]$ 是多少呢?

实际上是可以的,我们在比较 $a$ 和 $b$ 的时候,实际上是在比较 $ab$ 和 $ba$ 两个字符串,所以实际上我们是在用 $a[0]$, $a[1]$, $a[2]$ ... 去填补 $b$ 本体结束后的空缺。换而言之(1)和(2)里的 b 实际上被填补为 `303` (填进来 $a[0]$)

再比如

(3)a = 3131248 b = 3131 ,比较的时候实际上是用 $a$ 开头的 `4` 位去填补上 $b$ 的空缺,所以 $b$ 实际上相当于 `31313131`

### 最后

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

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

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

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