LeetCode/151-160/154. 寻找旋转排序数组中的最小值 II（困难）.md

154. 寻找旋转排序数组中的最小值 II

Question:
已知一个长度为 `n` 的数组,预先按照升序排列,经由 $1$ 到 $n$ 次 旋转 后,得到输入数组。

例如,原数组 `nums = [0,1,4,4,5,6,7]` 在变化后可能得到:

* 若旋转 $4$ 次,则可以得到 `[4,5,6,7,0,1,4]`
* 若旋转 $74 次,则可以得到 `[0,1,4,4,5,6,7]`

注意,数组 `[a[0], a[1], a[2], ..., a[n-1]]` 旋转一次 的结果为数组 `[a[n-1], a[0], a[1], a[2], ..., a[n-2]]` 。

给你一个可能存在「重复」元素值的数组 `nums` ,它原来是一个升序排列的数组,并按上述情形进行了多次旋转。

请你找出并返回数组中的「最小元素」。

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

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

输出:0
```

提示:
* $n == nums.length$
* $1 <= n <= 5000$
* $-5000 <= nums[i] <= 5000$
* `nums` 原来是一个升序排序的数组,并进行了 $1$ 至 $n$ 次旋转

进阶:
* 这道题是 寻找旋转排序数组中的最小值 的延伸题目。
* 允许重复会影响算法的时间复杂度吗?会如何影响,为什么?

Solution:
### 二分

根据题意,我们知道,所谓的旋转其实就是「将某个下标前面的所有数整体移到后面,使得数组从整体有序变为分段有序」。

但和 [153. 寻找旋转排序数组中的最小值](https://leetcode-cn.com/problems/find-minimum-in-rotated-sorted-array/solution/gong-shui-san-xie-yan-ge-olognyi-qi-kan-6d969/) 不同的是,本题元素并不唯一。

**这意味着我们无法直接根据与** $nums[0]$ **的大小关系,将数组划分为两段,即无法通过「二分」来找到旋转点。**

**因为「二分」的本质是二段性,并非单调性。只要一段满足某个性质,另外一段不满足某个性质,就可以用「二分」。**

如果你有看过我 [严格 O(logN),一起看清二分的本质](https://leetcode-cn.com/problems/search-in-rotated-sorted-array/solution/shua-chuan-lc-yan-ge-ologn100yi-qi-kan-q-xifo/) 这篇题解,你应该很容易就理解上句话的意思。如果没有也没关系,我们可以先解决本题,在理解后你再去做 [153. 寻找旋转排序数组中的最小值](https://leetcode-cn.com/problems/find-minimum-in-rotated-sorted-array/),我认为这两题都是一样的,不存在先后关系。

举个🌰,我们使用数据 [0,1,2,2,2,3,4,5] 来理解为什么不同的旋转点会导致「二段性丢失」:

Java 代码:
```java
class Solution {
    public int minArray(int[] nums) {
        int n = nums.length;
        int l = 0, r = n - 1;
        while (l < r && nums[0] == nums[r]) r--;
        while (l < r) {
            int mid = l + r + 1 >> 1;
            if (nums[mid] >= nums[0]) l = mid;
            else r = mid - 1;
        }
        return r + 1 < n ? nums[r + 1] : nums[0];
    }
}
```
TypeScript 代码:
```TypeScript
function minArray(nums: number[]): number {
    const n = nums.length
    let l = 0, r = n - 1
    while (l < r && nums[0] == nums[r]) r--
    while (l < r) {
        const mid = l + r + 1 >> 1
        if (nums[mid] >= nums[0]) l = mid
        else r = mid - 1
    }
    return r + 1 < n ? nums[r + 1] : nums[0]
};
```
* 时间复杂度:恢复二段性处理中,最坏的情况下(考虑整个数组都是同一个数)复杂度是 $O(n)$,而之后的找旋转点是「二分」,复杂度为 $O(\log{n})$。整体复杂度为 $O(n)$
* 空间复杂度:$O(1)$

---

### 进阶

如果真正理解「二分」的话,本题和 [153. 寻找旋转排序数组中的最小值](https://leetcode-cn.com/problems/find-minimum-in-rotated-sorted-array/) 区别不大。

建议大家在完成两题的基础上试试 [面试题 10.03. 搜索旋转数组](https://leetcode-cn.com/problems/search-rotate-array-lcci/) 。

---

### 其他「二分」相关题解

* 二分模板
    [29. 两数相除](https://leetcode-cn.com/problems/divide-two-integers/) : [二分 + 倍增乘法解法(含模板)](https://leetcode-cn.com/problems/divide-two-integers/solution/shua-chuan-lc-er-fen-bei-zeng-cheng-fa-j-m73b/)

* 二分本质 & 恢复二段性处理

    [33. 搜索旋转排序数组(找目标值)](https://leetcode-cn.com/problems/search-in-rotated-sorted-array/) : [严格 O(logN),一起看清二分的本质](https://leetcode-cn.com/problems/search-in-rotated-sorted-array/solution/shua-chuan-lc-yan-ge-ologn100yi-qi-kan-q-xifo/)

    [81. 搜索旋转排序数组 II(找目标值)](https://leetcode-cn.com/problems/search-in-rotated-sorted-array-ii/) : [详解为何元素相同会导致 O(n),一起看清二分的本质](https://leetcode-cn.com/problems/search-in-rotated-sorted-array-ii/solution/gong-shui-san-xie-xiang-jie-wei-he-yuan-xtam4/)

    [153. 寻找旋转排序数组中的最小值(找最小值)](https://leetcode-cn.com/problems/find-minimum-in-rotated-sorted-array/) : [严格 O(logN),一起看清二分的本质](https://leetcode-cn.com/problems/find-minimum-in-rotated-sorted-array/solution/gong-shui-san-xie-yan-ge-olognyi-qi-kan-6d969/)

    [154. 寻找旋转排序数组中的最小值 II(找最小值)](https://leetcode-cn.com/problems/find-minimum-in-rotated-sorted-array-ii/) : [详解为何元素相同会导致 O(n),一起看清二分的本质](https://leetcode-cn.com/problems/find-minimum-in-rotated-sorted-array-ii/solution/gong-shui-san-xie-xiang-jie-wei-he-yuan-7xbty/)

* 二分 check 函数如何确定
    [34. 在排序数组中查找元素的第一个和最后一个位置](https://leetcode-cn.com/problems/find-first-and-last-position-of-element-in-sorted-array/) : [考察对「二分」的理解,以及 check 函数的「大于 小于」怎么写](https://leetcode-cn.com/problems/find-first-and-last-position-of-element-in-sorted-array/solution/gong-shui-san-xie-kao-cha-dui-er-fen-de-86bk0/)

* 二分答案的题目
    [1482. 制作 m 束花所需的最少天数](https://leetcode-cn.com/problems/minimum-number-of-days-to-make-m-bouquets/) : [利用「二段性」找分割点,以及优化 check 函数](https://leetcode-cn.com/problems/minimum-number-of-days-to-make-m-bouquets/solution/gong-shui-san-xie-li-yong-er-duan-xing-z-ysv4/)
    [1011. 在 D 天内送达包裹的能力](https://leetcode-cn.com/problems/capacity-to-ship-packages-within-d-days/) : [利用「二段性」找分割点,以及如何确定「二分范围」](https://leetcode-cn.com/problems/capacity-to-ship-packages-within-d-days/solution/gong-shui-san-xie-li-yong-er-duan-xing-z-95zj/)

### 最后

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

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

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

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


Question:
设计一个支持 push ,pop ,top 操作,并能在常数时间内检索到最小元素的栈。
* push(x) —— 将元素 x 推入栈中。
* pop() —— 删除栈顶的元素。
* top() —— 获取栈顶元素。
* getMin() —— 检索栈中的最小元素。

示例:
```
输入:
["MinStack","push","push","push","getMin","pop","top","getMin"]
[[],[-2],[0],[-3],[],[],[],[]]
```
输出:
```
[null,null,null,null,-3,null,0,-2]

解释:
MinStack minStack = new MinStack();
minStack.push(-2);
minStack.push(0);
minStack.push(-3);
minStack.getMin();   --> 返回 -3.
minStack.pop();
minStack.top();      --> 返回 0.
minStack.getMin();   --> 返回 -2.
```

提示:
* pop、top 和 getMin 操作总是在 非空栈 上调用。

Solution:
### 双栈解法

为了快速找到栈中最小的元素,我们可以使用一个辅助栈 help。

通过控制 help 的压栈逻辑来实现:*help 栈顶中始终存放着栈内元素的最小值。*

代码:
```java
class MinStack {
    Deque<Integer> data = new ArrayDeque<>();
    Deque<Integer> help = new ArrayDeque<>();

    public void push(int val) {
        data.addLast(val);
        if (help.isEmpty() || help.peekLast() >= val) {
            help.addLast(val);
        } else {
            help.addLast(help.peekLast());
        }
    }

    public void pop() {
        data.pollLast();
        help.pollLast();
    }

    public int top() {
        return data.peekLast();
    }

    public int getMin() {
        return help.peekLast();
    }
}
```
* 时间复杂度:所有的操作均为 $O(1)$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


Question:
给你两个单链表的头节点 headA 和 headB ,请你找出并返回两个单链表相交的起始节点。如果两个链表没有交点,返回 null 。

图示两个链表在节点 c1 开始相交:

题目数据 保证 整个链式结构中不存在环。

注意,函数返回结果后,链表必须 保持其原始结构 。

示例 1:

```
输入:intersectVal = 8, listA = [4,1,8,4,5], listB = [5,0,1,8,4,5], skipA = 2, skipB = 3
输出:Intersected at '8'
解释:相交节点的值为 8 (注意,如果两个链表相交则不能为 0)。
从各自的表头开始算起,链表 A 为 [4,1,8,4,5],链表 B 为 [5,0,1,8,4,5]。
在 A 中,相交节点前有 2 个节点;在 B 中,相交节点前有 3 个节点。
```
示例 2:

```
输入:intersectVal = 2, listA = [0,9,1,2,4], listB = [3,2,4], skipA = 3, skipB = 1
输出:Intersected at '2'
解释:相交节点的值为 2 (注意,如果两个链表相交则不能为 0)。
从各自的表头开始算起,链表 A 为 [0,9,1,2,4],链表 B 为 [3,2,4]。
在 A 中,相交节点前有 3 个节点;在 B 中,相交节点前有 1 个节点。
```
示例 3:

```
输入:intersectVal = 0, listA = [2,6,4], listB = [1,5], skipA = 3, skipB = 2
输出:null
解释:从各自的表头开始算起,链表 A 为 [2,6,4],链表 B 为 [1,5]。
由于这两个链表不相交,所以 intersectVal 必须为 0,而 skipA 和 skipB 可以是任意值。
这两个链表不相交,因此返回 null 。
```

提示:
* listA 中节点数目为 m
* listB 中节点数目为 n
* 0 <= m, n <= 3 * $10^4$
* 1 <= Node.val <= $10^5$
* 0 <= skipA <= m
* 0 <= skipB <= n
* 如果 listA 和 listB 没有交点,intersectVal 为 0
* 如果 listA 和 listB 有交点,intersectVal == listA[skipA + 1] == listB[skipB + 1]

进阶:你能否设计一个时间复杂度 $O(n)$ 、仅用 $O(1)$ 内存的解决方案?

Solution:
### 朴素解法

一个朴素的做法是两层循环:当遇到第一个相同的节点时说明找到了;全都走完了还没遇到相同,说明不存在交点。

代码:
```Java
public class Solution {
    public ListNode getIntersectionNode(ListNode a, ListNode b) {
        for (ListNode h1 = a; h1 != null ; h1 = h1.next) {
            for (ListNode h2 = b; h2 != null ; h2 = h2.next) {
                if (h1.equals(h2)) return h1;
            }
        }
        return null;
    }
}
```
* 时间复杂度:$O(n * m)$
* 空间复杂度:$O(1)$

---

### 栈解法

将两条链表分别压入两个栈中,然后循环比较两个栈的栈顶元素,同时记录上一位栈顶元素。

当遇到第一个不同的节点时,结束循环,上一位栈顶元素即是答案。

代码:
```Java
public class Solution {
    public ListNode getIntersectionNode(ListNode a, ListNode b) {
        Deque<ListNode> d1 = new ArrayDeque(), d2 = new ArrayDeque();
        while (a != null) {
            d1.addLast(a);
            a = a.next;
        }
        while (b != null) {
            d2.addLast(b);
            b = b.next;
        }
        ListNode ans = null;
        while (!d1.isEmpty() && !d2.isEmpty() && d1.peekLast().equals(d2.peekLast())) {
            ListNode c1 = d1.pollLast(), c2 = d2.pollLast();
            ans = c1;
        }
        return ans;
    }
}
```
* 时间复杂度:$O(n + m)$
* 空间复杂度:$O(n + m)$

---

### 差值解法

先对两条链表扫描一遍,取得两者长度,然后让长的链表先走「两者的长度差值」,然后再同时走,遇到第一个节点即是答案。

代码:
```Java
public class Solution {
    public ListNode getIntersectionNode(ListNode a, ListNode b) {
        int c1 = 0, c2 = 0;
        ListNode t1 = a, t2 = b;
        while (t1 != null && ++c1 > 0) t1 = t1.next;
        while (t2 != null && ++c2 > 0) t2 = t2.next;
        int t = Math.abs(c1 - c2);
        while (t-- > 0) {
            if (c1 > c2) a = a.next;
            else b = b.next;
        }
        while (a != null && b != null) {
            if (a.equals(b)) {
                return a;
            } else {
                a = a.next;
                b = b.next;
            }
        }
        return null;
    }
}
```
* 时间复杂度:$O(n + m)$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


Question:
小区便利店正在促销,用 `numExchange` 个空酒瓶可以兑换一瓶新酒。你购入了 `numBottles` 瓶酒。

如果喝掉了酒瓶中的酒,那么酒瓶就会变成空的。

请你计算**最多**能喝到多少瓶酒。

示例 1:

```
输入:numBottles = 9, numExchange = 3

输出:13

解释:你可以用 3 个空酒瓶兑换 1 瓶酒。
所以最多能喝到 9 + 3 + 1 = 13 瓶酒。
```
示例 2:

```
输入:numBottles = 15, numExchange = 4

输出:19

解释:你可以用 4 个空酒瓶兑换 1 瓶酒。
所以最多能喝到 15 + 3 + 1 = 19 瓶酒。
```
示例 3:
```
输入:numBottles = 5, numExchange = 5

输出:6
```
示例 4:
```
输入:numBottles = 2, numExchange = 3

输出:2
```

提示:
* $1 <= numBottles <= 100$
* $2 <= numExchange <= 100$

Solution:
### 模拟

根据题意进行模拟即可,使用 $ans$ 统计答案,$n$ 为空瓶子个数。

起始有 $n$ 瓶酒,因此 $ans = n$,此时空瓶子个数为 $n$,当且仅当空瓶子个数 $n$ 大于等于兑换个数 $m$ 时,可以继续喝到酒(能够更新 $ans$),兑换后得到酒的个数为 $a = \left \lfloor \frac{n}{m} \right \rfloor$,剩余空瓶子个数等于「兑换酒的个数 $a$」和「兑换后剩余的酒瓶子个数 $b = n \pmod m$」之和。

代码:
```Java
class Solution {
    public int numWaterBottles(int n, int m) {
        int ans = n;
        while (n >= m) {
            int a = n / m, b = n % m;
            ans += a;
            n = a + b;
        }
        return ans;
    }
}
```
* 时间复杂度:循环次数「不超过」能换新酒的数量,能够新酒的数量最多为 $\left \lfloor \frac{n}{m - 1}\right \rfloor$ 瓶。复杂度为 $O(\left \lfloor \frac{n}{m - 1}\right \rfloor)$。进一步,当 $m = 2$ 时,兑换酒的数量最多,此时复杂度为 $O(\log{n})$
* 空间复杂度:$O(1)$

---

### 数学

起始有 $n$ 瓶酒,使用 $m$ 个空酒瓶能够换得一瓶新酒(饮用数量加一,且新瓶子数量加一)。即对于每次交换而言,会损失掉 $m - 1$ 个瓶子。

利用每个回合损失的瓶子个数 $m - 1$ 为定值,可直接算出最大交换次数(额外饮用次数)$cnt = \left \lfloor \frac{n}{m - 1}\right \rfloor$,加上起始酒的个数即是答案。

注意边界条件:当 $n$ 为 $m - 1$ 的倍数时,最后一个回合不满足兑换条件。

代码:
```Java
class Solution {
    public int numWaterBottles(int n, int m) {
        int cnt = n / (m  - 1);
        return n % (m - 1) == 0 ? n + cnt - 1 : n + cnt;
    }
}
```
* 时间复杂度:$O(1)$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


Question:
你有两个 有序 且数组内元素互不相同的数组 `nums1` 和 `nums2` 。

一条 合法路径 定义如下:

* 选择数组 `nums1` 或者 `nums2` 开始遍历(从下标 $0$ 处开始)。
* 从左到右遍历当前数组。
* 如果你遇到了 `nums1` 和 `nums2` 中都存在的值,那么你可以切换路径到另一个数组对应数字处继续遍历(但在合法路径中重复数字只会被统计一次)。

得分定义为合法路径中不同数字的和。

请你返回所有可能合法路径中的最大得分。

由于答案可能很大,请你将它对 $10^9 + 7$ 取余后返回。

示例 1:

```
输入:nums1 = [2,4,5,8,10], nums2 = [4,6,8,9]

输出:30

解释:合法路径包括:
[2,4,5,8,10], [2,4,5,8,9], [2,4,6,8,9], [2,4,6,8,10],(从 nums1 开始遍历)
[4,6,8,9], [4,5,8,10], [4,5,8,9], [4,6,8,10]  (从 nums2 开始遍历)
最大得分为上图中的绿色路径 [2,4,6,8,10] 。
```
示例 2:
```
输入:nums1 = [1,3,5,7,9], nums2 = [3,5,100]

输出:109

解释:最大得分由路径 [1,3,5,100] 得到。
```
示例 3:
```
输入:nums1 = [1,2,3,4,5], nums2 = [6,7,8,9,10]

输出:40

解释:nums1 和 nums2 之间无相同数字。
最大得分由路径 [6,7,8,9,10] 得到。
```
示例 4:
```
输入:nums1 = [1,4,5,8,9,11,19], nums2 = [2,3,4,11,12]

输出:61
```

提示:
* $1 <= nums1.length <= 10^5$
* $1 <= nums2.length <= 10^5$
* $1 <= nums1[i], nums2[i] <= 10^7$
* `nums1` 和 `nums2` 都是严格递增的数组。

Solution:
### 前缀和 + 构造(分段计算)

一个简单且正确的做法,是我们构造一种决策方案,使得能够直接计算出最大得分。

首先,在最佳路径中所有的公共点都必然会经过,因此我们可以将值相等的点进行合并,即看作同一个点。

利用两个数组均满足「单调递增」,我们可以通过 $O(n + m)$ 的复杂度统计出那些公共点,以二元组 $(i, j)$ 的形式存储到 `list` 数组(二元组含义为 $nums1[i] = nums2[j]$)。

对于 `list` 中的每对相邻元素(相邻公共点),假设为 $(a_i, b_i)$ 和 $(c_i, d_i)$,我们可以通过「前缀和」计算出 $nums1[a_i ... c_i]$ 以及 $nums2[b_i ... d_i]$ 的和,从而决策出在 $nums1[a_i]$(或者说是 $nums2[b_i]$,这两个是同一个点)时,我们应当走哪一段。

当计算完所有公共点之间的得分后,对于最佳路线的首位两端,也是结合「前缀和」做同样的逻辑处理即可。

代码:
```Java
class Solution {
    int MOD = (int)1e9 + 7;
    public int maxSum(int[] nums1, int[] nums2) {
        int n = nums1.length, m = nums2.length;
        long[] s1 = new long[n + 10], s2 = new long[m + 10];
        for (int i = 1; i <= n; i++) s1[i] = s1[i - 1] + nums1[i - 1];
        for (int i = 1; i <= m; i++) s2[i] = s2[i - 1] + nums2[i - 1];
        List<int[]> list = new ArrayList<>();
        for (int i = 0, j = 0; i < n && j < m; ) {
            if (nums1[i] == nums2[j]) list.add(new int[]{i, j});
            if (nums1[i] < nums2[j]) i++;
            else j++;
        }
        long ans = 0;
        for (int i = 0, p1 = -1, p2 = -1; i <= list.size(); i++) {
            int idx1 = 0, idx2 = 0;
            if (i < list.size()) {
                int[] info = list.get(i);
                idx1 = info[0]; idx2 = info[1];
            } else {
                idx1 = n - 1; idx2 = m - 1;
            }
            long t1 = s1[idx1 + 1] - s1[p1 + 1], t2 = s2[idx2 + 1] - s2[p2 + 1];
            ans += Math.max(t1, t2);
            p1 = idx1; p2 = idx2;
        }
        return (int)(ans % MOD);
    }
}
```
* 时间复杂度:$O(n + m)$
* 空间复杂度:$O(n + m)$

---

### 序列 DP

另外一个较为常见的做法是「序列 DP」做法。

定义 $f[i]$ 代表在 `nums1` 上进行移动,到达 $nums1[i]$ 的最大得分;定义 $g[j]$ 代表在 `nums2` 上进行移动,到达 $nums[j]$ 的最大得分。

由于两者的分析是类似的,我们以 $f[i]$ 为例进行分析即可。

不失一般性考虑 $f[i]$ 如何转移,假设当前处理到的是 $nums1[i]$,根据 $nums1[i]$ 是否为公共点,进行分情况讨论:

* $nums1[i]$ 不为公共点,此时只能由 $nums[i - 1]$ 转移而来,即有 $f[i] = f[i - 1] + nums[i]$;
* $nums1[i]$ 为公共点(假设与 $nums2[j]$ 公共),此时能够从 $nums1[i - 1]$ 或 $nums2[j - 1]$ 转移而来,我们需要取 $f[i - 1]$ 和 $g[j - 1]$ 的最大值,即有 $f[i] = g[j] = \max(f[i - 1], g[j - 1]) + nums1[i]$。

更重要的是,我们需要确保计算 $f[i]$ 时,$g[j - 1]$ 已被计算完成。

由于最佳路线必然满足「单调递增」,因此我们可以使用「双指针」来对 $f[i]$ 和 $g[j]$ 同时进行转移,每次取值小的进行更新,从而确保更新过程也是单调的,即当需要计算 $f[i]$ 时,比 $nums1[i]$ 小的 $f[X]$ 和 $g[X]$ 均被转移完成。

代码:
```Java
class Solution {
    int MOD = (int)1e9 + 7;
    public int maxSum(int[] nums1, int[] nums2) {
        int n = nums1.length, m = nums2.length;
        long[] f = new long[n + 1], g = new long[m + 1];
        int i = 1, j = 1;
        while (i <= n || j <= m) {
            if (i <= n && j <= m) {
                if (nums1[i - 1] < nums2[j - 1]) {
                    f[i] = f[i - 1] + nums1[i - 1];
                    i++;
                } else if (nums2[j - 1] < nums1[i - 1]) {
                    g[j] = g[j - 1] + nums2[j - 1];
                    j++;
                } else {
                    f[i] = g[j] = Math.max(f[i - 1], g[j - 1]) + nums1[i - 1];
                    i++; j++;
                }
            } else if (i <= n) {
                f[i] = f[i - 1] + nums1[i - 1];
                i++;
            } else {
                g[j] = g[j - 1] + nums2[j - 1];
                j++;
            }
        }
        return (int) (Math.max(f[n], g[m]) % MOD);
    }
}
```
* 时间复杂度:$O(n + m)$
* 空间复杂度:$O(n + m)$

### 最后

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

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

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

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


1576. 替换所有的问号

Question:
给你一个仅包含小写英文字母和 `'?'` 字符的字符串 `s`,请你将所有的 `'?'` 转换为若干小写字母,使最终的字符串不包含任何 **连续重复** 的字符。

注意:你 不能 修改非 `'?'` 字符。

题目测试用例保证 除 `'?'` 字符 之外,不存在连续重复的字符。

在完成所有转换(可能无需转换)后返回最终的字符串。如果有多个解决方案,请返回其中任何一个。可以证明,在给定的约束条件下,答案总是存在的。

示例 1:
```
输入:s = "?zs"

输出:"azs"

解释:该示例共有 25 种解决方案,从 "azs" 到 "yzs" 都是符合题目要求的。只有 "z" 是无效的修改,因为字符串 "zzs" 中有连续重复的两个 'z' 。
```
示例 2:
```
输入:s = "ubv?w"

输出:"ubvaw"

解释:该示例共有 24 种解决方案,只有替换成 "v" 和 "w" 不符合题目要求。因为 "ubvvw" 和 "ubvww" 都包含连续重复的字符。
```
示例 3:
```
输入:s = "j?qg??b"

输出:"jaqgacb"
```
示例 4:
```
输入:s = "??yw?ipkj?"

输出:"acywaipkja"
```

提示:
* $1 <= s.length <= 100$
* `s` 仅包含小写英文字母和 `'?'` 字符

Solution:
### 模拟

根据题意进行模拟,尝试对每个 $s[i]$ 进行替换,能够替换的前提是 $s[i]$ 为 `?`,且替换字符与前后字符(若存在)不同,由于只需要确保与前后字符不同,因此必然最多在 $3$ 个字符内找到可替换的值。

代码:
```Java
class Solution {
    public String modifyString(String s) {
        char[] cs = s.toCharArray();
        int n = cs.length;
        for (int i = 0; i < n; i++) {
            for (int c = 0; c < 3 && cs[i] == '?'; c++) {
                boolean ok = true;
                if (i - 1 >= 0 && cs[i - 1] == c + 'a') ok = false;
                if (i + 1 < n && cs[i + 1] == c + 'a') ok = false;
                if (ok) cs[i] = (char)(c + 'a');
            }
        }
        return String.valueOf(cs);
    }
}
```
* 时间复杂度:会处理 `s` 中的所有字符,若当前字符为 `?` 时,会检查与前后字符的关系,最多在 $3$ 个字符内找到可替换的字符,复杂度为 $O(n * C)$,其中 $C = 3$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


LeetCode/1581-1590/1582. 二进制矩阵中的特殊位置（简单）.md

1582. 二进制矩阵中的特殊位置

Question:
给你一个大小为 `rows x cols` 的矩阵 `mat`,其中 `mat[i][j]` 是 `0` 或 `1`,请返回 矩阵 `mat` 中特殊位置的数目 。

特殊位置 定义:如果 `mat[i][j] = 1` 并且第 `i` 行和第 `j` 列中的所有其他元素均为 `0`(行和列的下标均 从 `0` 开始 ),则位置 `(i, j)` 被称为特殊位置。

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

输出:1

解释:(1,2) 是一个特殊位置,因为 mat[1][2] == 1 且所处的行和列上所有其他元素都是 0
```
示例 2:
```
输入:mat = [[1,0,0],
            [0,1,0],
            [0,0,1]]

输出:3

解释:(0,0), (1,1) 和 (2,2) 都是特殊位置
```
示例 3:
```
输入:mat = [[0,0,0,1],
            [1,0,0,0],
            [0,1,1,0],
            [0,0,0,0]]

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

输出:3
```

提示:
* $rows = mat.length$
* $cols = mat[i].length$
* $1 <= rows, cols <= 100$
* $mat[i][j]$ 是 `0` 或 `1`

Solution:
### 模拟

根据题意,使用数组 `r` 和 `c` 分别预处理除每行和每列所含 $1$ 的个数,复杂度为 $O(m \times n)$。

随后分别统计特殊位置的个数:满足 $mat[i][j] = 1$ 且 $r[i] = c[j] = 1$ 的位置。

Java 代码:
```Java
class Solution {
    public int numSpecial(int[][] mat) {
        int n = mat.length, m = mat[0].length, ans = 0;
        int[] r = new int[n], c = new int[m];
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < m; j++) {
                r[i] += mat[i][j]; c[j] += mat[i][j];
            }
        }
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < m; j++) {
                if (mat[i][j] == 1 && r[i] == 1 && c[j] == 1) ans++;
            }
        }
        return ans;
    }
}
```
TypeScript 代码:
```TypeScript
function numSpecial(mat: number[][]): number {
    let n = mat.length, m = mat[0].length, ans = 0
    const r = new Array<number>(n).fill(0), c = new Array<number>(m).fill(0)
    for (let i = 0; i < n; i++) {
        for (let j = 0; j < m; j++) {
            r[i] += mat[i][j]; c[j] += mat[i][j]
        }
    }
    for (let i = 0; i < n; i++) {
        for (let j = 0; j < m; j++) {
            if (mat[i][j] == 1 && r[i] == 1 && c[j] == 1) ans++
        }
    }
    return ans
};
```
* 时间复杂度:$O(m \times n)$
* 空间复杂度:$O(m + n)$

### 最后

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

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

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

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


LeetCode/1581-1590/1583. 统计不开心的朋友（中等）.md

1583. 统计不开心的朋友

Question:
给你一份 `n` 位朋友的亲近程度列表,其中 `n` 总是**偶数** 。

对每位朋友 `i`,`preferences[i]` 包含一份 按亲近程度从高到低排列 的朋友列表。

换句话说,排在列表前面的朋友与 `i` 的亲近程度比排在列表后面的朋友更高。每个列表中的朋友均以 `0` 到 `n-1` 之间的整数表示。

所有的朋友被分成几对,配对情况以列表 `pairs` 给出,其中 `pairs[i] = [xi, yi]` 表示 `xi` 与 `yi` 配对,且 `yi` 与 `xi` 配对。

但是,这样的配对情况可能会是其中部分朋友感到不开心。在 `x` 与 `y` 配对且 `u` 与 `v` 配对的情况下,如果同时满足下述两个条件,`x` 就会不开心:

* `x` 与 `u` 的亲近程度胜过 `x` 与 `y`,且
* `u` 与 `x` 的亲近程度胜过 `u` 与 `v`

返回 不开心的朋友的数目 。

示例 1:
```
输入:n = 4, preferences = [[1, 2, 3], [3, 2, 0], [3, 1, 0], [1, 2, 0]], pairs = [[0, 1], [2, 3]]

输出:2

解释:
朋友 1 不开心,因为:
- 1 与 0 配对,但 1 与 3 的亲近程度比 1 与 0 高,且
- 3 与 1 的亲近程度比 3 与 2 高。
朋友 3 不开心,因为:
- 3 与 2 配对,但 3 与 1 的亲近程度比 3 与 2 高,且
- 1 与 3 的亲近程度比 1 与 0 高。
朋友 0 和 2 都是开心的。
```
示例 2:
```
输入:n = 2, preferences = [[1], [0]], pairs = [[1, 0]]

输出:0

解释:朋友 0 和 1 都开心。
```
示例 3:
```
输入:n = 4, preferences = [[1, 3, 2], [2, 3, 0], [1, 3, 0], [0, 2, 1]], pairs = [[1, 3], [0, 2]]

输出:4
```

提示:
* 2 <= n <= 500
* n 是偶数
* preferences.length == n
* preferences[i].length == n - 1
* 0 <= preferences[i][j] <= n - 1
* preferences[i] 不包含 i
* preferences[i] 中的所有值都是独一无二的
* pairs.length == n/2
* pairs[i].length == 2
* xi != yi
* 0 <= xi, yi <= n - 1
* 每位朋友都 恰好 被包含在一对中

Solution:
### 模拟

模拟题,先将所有的 $preferences$ 使用「哈希表套哈希表」的形式进行存储,存储格式为 `{x : {y : score1}, {z : score12}, ... }`。

如果 $x$ 和 $y$ 的亲密度要比 $x$ 和 $z$ 的亲密度要高,则有 $score1 > score2$。利用原本 $preferences[i]$ 就是按照亲密度进行排序,我们可以对下标进行转换作为亲密数得分即可。

然后对所有的 $pairs$ 进行遍历,统计所有的答案,注意一个小朋友只能被统计一次。

> 当然利用 $n$ 的数据范围,直接使用二维数组充当哈希表也是可以的(见 $P2$)

代码:
```Java
class Solution {
    Map<Integer, Map<Integer, Integer>> map = new HashMap<>();
    public int unhappyFriends(int n, int[][] preferences, int[][] pairs) {
        int m = pairs.length;
        for (int i = 0; i < n; i++) {
            int[] p = preferences[i];
            Map<Integer, Integer> cur = new HashMap<>();
            for (int j = 0; j < n - 1; j++) cur.put(p[j], n - j);
            map.put(i, cur);
        }
        int ans = 0;
        for (int i = 0; i < m; i++) {
            int x = pairs[i][0], y = pairs[i][1];
            boolean xok = false, yok = false;
            for (int j = 0; j < m; j++) {
                if (i == j) continue;
                int u = pairs[j][0], v = pairs[j][1];
                if (!xok && check(x, y, u, v)) xok = true;
                if (!yok && check(y, x, u, v)) yok = true;
                if (xok && yok) break;
            }
            if (xok) ans++;
            if (yok) ans++;
        }
        return ans;
    }
    boolean check(int x, int y, int u, int v) {
        Map<Integer, Integer> xmap = map.get(x), ymap = map.get(y);
        Map<Integer, Integer> umap = map.get(u), vmap = map.get(v);
        if (xmap.get(u) > xmap.get(y) && umap.get(x) > umap.get(v)) return true;
        if (xmap.get(v) > xmap.get(y) && vmap.get(x) > vmap.get(u)) return true;
        return false;
    }
}
```

代码:
```Java
class Solution {
    int N = 510;
    int[][] map = new int[N][N];
    public int unhappyFriends(int n, int[][] preferences, int[][] pairs) {
        int m = pairs.length;
        for (int i = 0; i < n; i++) {
            int[] p = preferences[i];
            for (int j = 0; j < n - 1; j++) map[i][p[j]] = n - j;
        }
        int ans = 0;
        for (int i = 0; i < m; i++) {
            int x = pairs[i][0], y = pairs[i][1];
            boolean xok = false, yok = false;
            for (int j = 0; j < m; j++) {
                if (i == j) continue;
                int u = pairs[j][0], v = pairs[j][1];
                if (!xok && check(x, y, u, v)) xok = true;
                if (!yok && check(y, x, u, v)) yok = true;
                if (xok && yok) break;
            }
            if (xok) ans++;
            if (yok) ans++;
        }
        return ans;
    }
    boolean check(int x, int y, int u, int v) {
        if (map[x][u] > map[x][y] && map[u][x] > map[u][v]) return true;
        if (map[x][v] > map[x][y] && map[v][x] > map[v][u]) return true;
        return false;
    }
}
```
* 时间复杂度:预处理出 `map` 的复杂度为 $O(n^2)$;遍历统计答案的复杂度为 $O(n^2)$。整体复杂度为 $O(n^2)$
* 空间复杂度:$O(n^2)$

### 最后

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

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

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

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


LeetCode/1581-1590/1588. 所有奇数长度子数组的和（简单）.md

1588. 所有奇数长度子数组的和

Question:
给你一个正整数数组 `arr` ,请你计算所有可能的奇数长度子数组的和。

子数组 定义为原数组中的一个连续子序列。

请你返回 `arr` 中 所有奇数长度子数组的和 。

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

输出:58

解释:所有奇数长度子数组和它们的和为:
[1] = 1
[4] = 4
[2] = 2
[5] = 5
[3] = 3
[1,4,2] = 7
[4,2,5] = 11
[2,5,3] = 10
[1,4,2,5,3] = 15
我们将所有值求和得到 1 + 4 + 2 + 5 + 3 + 7 + 11 + 10 + 15 = 58
```
示例 2:
```
输入:arr = [1,2]

输出:3

解释:总共只有 2 个长度为奇数的子数组,[1] 和 [2]。它们的和为 3 。
```
示例 3:
```
输入:arr = [10,11,12]

输出:66
```

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

Solution:
### 前缀和

枚举所有长度为奇数的子数组,我们可以通过「枚举长度 - 枚举左端点,并计算右端点」的两层循环来做。

而对于区间 $[l, r]$ 的和问题,可以直接再加一层循环来做,这样复杂度来到了 $O(n^3)$,但本题数据范围只有 $100$,也是可以过的。

对于此类区间求和问题,我们应当想到使用「前缀和」进行优化:使用 $O(n)$ 的复杂度预处理出前缀和数组,每次查询 $[l, r]$ 区间和可以在 $O(1)$ 返回。

代码:
```Java
class Solution {
    public int sumOddLengthSubarrays(int[] arr) {
        int n = arr.length;
        int[] sum = new int[n + 1];
        for (int i = 1; i <= n; i++) sum[i] = sum[i - 1] + arr[i - 1];
        int ans = 0;
        for (int len = 1; len <= n; len += 2) {
            for (int l = 0; l + len - 1 < n; l++) {
                int r = l + len - 1;
                ans += sum[r + 1] - sum[l];
            }
        }
        return ans;
    }
}
```
* 时间复杂度:$O(n^2)$
* 空间复杂度:$O(n)$

----

### 数学

事实上,我们可以统计任意值 $arr[i]$ 在奇数子数组的出现次数。

对于原数组的任意位置 $i$ 而言,其左边共有 $i$ 个数,右边共有 $n - i - 1$ 个数。

**$arr[i]$ 作为某个奇数子数组的成员的充要条件为:其所在奇数子数组左右两边元素个数奇偶性相同。**

于是问题转换为如何求得「$arr[i]$ 在原数组中两边连续一段元素个为奇数的方案数」和「$arr[i]$ 在原数组两边连续一段元素个数为偶数的方案数」。

由于我们已经知道 $arr[i]$ 左边共有 $i$ 个数,右边共有 $n - i - 1$ 个数,因此可以算得组合数:

* 位置 $i$ 左边奇数个数的方案数为 $(i + 1) / 2$,右边奇数个数的方案数为 $(n - i) / 2$;
* 位置 $i$ 左边偶数(非零)个数的方案数为 $i / 2$,右边偶数(非零)个数的方案数为 $(n - i - 1) / 2$;
    * 考虑左右两边不选也属于合法的偶数个数方案数,因此在上述分析基础上对偶数方案数自增 $1$。

至此,我们得到了位置 $i$ 左右奇数和偶数的方案数个数,根据「如果 $arr[i]$ 位于奇数子数组中,其左右两边元素个数奇偶性相同」以及「乘法原理」,我们知道 $arr[i]$ 同出现在多少个奇数子数组中,再乘上 $arr[i]$ 即是 $arr[i]$ 对答案的贡献。

代码:
```Java
class Solution {
    public int sumOddLengthSubarrays(int[] arr) {
        int n = arr.length;
        int ans = 0;
        for (int i = 0; i < n; i++) {
            int l1 = (i + 1) / 2, r1 = (n - i) / 2; // 奇数
            int l2 = i / 2, r2 = (n - i - 1) / 2; // 偶数
            l2++; r2++;
            ans += (l1 * r1 + l2 * r2) * arr[i];
        }
        return ans;
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


LeetCode/1591-1600/1592. 重新排列单词间的空格（简单）.md

1592. 重新排列单词间的空格

Question:
给你一个字符串 `text`,该字符串由若干被空格包围的单词组成。每个单词由一个或者多个小写英文字母组成,并且两个单词之间至少存在一个空格。题目测试用例保证 `text` 至少包含一个单词 。

请你重新排列空格,使每对相邻单词之间的空格数目都 相等 ,并尽可能 最大化 该数目。如果不能重新平均分配所有空格,请 将多余的空格放置在字符串末尾 ,这也意味着返回的字符串应当与原 `text` 字符串的长度相等。

返回 重新排列空格后的字符串 。

示例 1:
```
输入:text = "  this   is  a sentence "

输出:"this   is   a   sentence"

解释:总共有 9 个空格和 4 个单词。可以将 9 个空格平均分配到相邻单词之间,相邻单词间空格数为:9 / (4-1) = 3 个。
```
示例 2:
```
输入:text = " practice   makes   perfect"

输出:"practice   makes   perfect "

解释:总共有 7 个空格和 3 个单词。7 / (3-1) = 3 个空格加上 1 个多余的空格。多余的空格需要放在字符串的末尾。
```
示例 3:
```
输入:text = "hello   world"

输出:"hello   world"
```
示例 4:
```
输入:text = "  walks  udp package   into  bar a"

输出:"walks  udp  package  into  bar  a "
```
示例 5:
```
输入:text = "a"

输出:"a"
```

提示:
* $1 <= text.length <= 100$
* `text` 由小写英文字母和 `' '` 组成
* `text` 中至少包含一个单词

Solution:
### 模拟

根据题意模拟即可:使用「双指针」统计空格数量和分割出所有单词。

Java 代码:
```Java
class Solution {
    public String reorderSpaces(String s) {
        int n = s.length(), cnt = 0;
        List<String> list = new ArrayList<>();
        for (int i = 0; i < n; ) {
            if (s.charAt(i) == ' ' && ++i >= 0 && ++cnt >= 0) continue;
            int j = i;
            while (j < n && s.charAt(j) != ' ') j++;
            list.add(s.substring(i, j));
            i = j;
        }
        StringBuilder sb = new StringBuilder();
        int m = list.size(), t = cnt / Math.max(m - 1, 1);
        String k = "";
        while (t-- > 0) k += " ";
        for (int i = 0; i < m; i++) {
            sb.append(list.get(i));
            if (i != m - 1) sb.append(k);
        }
        while (sb.length() != n) sb.append(" ");
        return sb.toString();
    }
}
```
TypeScript 代码:
```TypeScript
function reorderSpaces(s: string): string {
    let n = s.length, cnt = 0
    const list = new Array<string>()
    for (let i = 0; i < n; ) {
        if (s[i] == ' ' && ++i >= 0 && ++cnt >= 0) continue
        let j = i + 1
        while (j < n && s[j] != ' ') j++
        list.push(s.substring(i, j))
        i = j
    }
    let ans = '', k = ''
    let m = list.length, t = Math.floor(cnt / Math.max(m - 1, 1))
    while (t-- > 0) k += ' '
    for (let i = 0; i < m; i++) {
        ans += list[i]
        if (i != m - 1) ans += k
    }
    while (ans.length != n) ans += ' '
    return ans
};
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


LeetCode/1591-1600/1598. 文件夹操作日志搜集器（简单）.md

1598. 文件夹操作日志搜集器

Question:
每当用户执行变更文件夹操作时,LeetCode 文件系统都会保存一条日志记录。

下面给出对变更操作的说明:

* `"../"` :移动到当前文件夹的父文件夹。如果已经在主文件夹下,则 继续停留在当前文件夹 。
* `"./"` :继续停留在当前文件夹。
* `"x/"` :移动到名为 `x` 的子文件夹中。题目数据 保证总是存在文件夹 x 。

给你一个字符串列表 `logs` ,其中 `logs[i]` 是用户在 `ith` 步执行的操作。

文件系统启动时位于主文件夹,然后执行 `logs` 中的操作。

执行完所有变更文件夹操作后,请你找出 返回主文件夹所需的最小步数 。

示例 1:

```
输入:logs = ["d1/","d2/","../","d21/","./"]

输出:2

解释:执行 "../" 操作变更文件夹 2 次,即可回到主文件夹
```
示例 2:

```
输入:logs = ["d1/","d2/","./","d3/","../","d31/"]

输出:3
```
示例 3:
```
输入:logs = ["d1/","../","../","../"]

输出:0
```

提示:
* $1 <= logs.length <= 10^3$
* $2 <= logs[i].length <= 10$
* `logs[i]` 包含小写英文字母,数字,`'.'` 和 `'/'`
* `logs[i]` 符合语句中描述的格式
* 文件夹名称由小写英文字母和数字组成

Solution:
### 模拟

根据题意进行模拟即可。

Java 代码:
```Java
class Solution {
    public int minOperations(String[] logs) {
        int depth = 0;
        for (String s : logs) {
            if (s.equals("../")) depth = Math.max(0, depth - 1);
            else if (!s.equals("./")) depth++;
        }
        return depth;
    }
}
```
TypeScript 代码:
```TypeScript
function minOperations(logs: string[]): number {
    let depth = 0
    for (const s of logs) {
        if (s == "../") depth = Math.max(0, depth - 1)
        else if (s != "./") depth++
    }
    return depth
};
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


1600. 皇位继承顺序

Question:
一个王国里住着国王、他的孩子们、他的孙子们等等。每一个时间点,这个家庭里有人出生也有人死亡。

这个王国有一个明确规定的皇位继承顺序,第一继承人总是国王自己。我们定义递归函数 Successor(x, curOrder) ,给定一个人 x 和当前的继承顺序,该函数返回 x 的下一继承人。
```
Successor(x, curOrder):
    如果 x 没有孩子或者所有 x 的孩子都在 curOrder 中:
        如果 x 是国王,那么返回 null
        否则,返回 Successor(x 的父亲, curOrder)
    否则,返回 x 不在 curOrder 中最年长的孩子
```
比方说,假设王国由国王,他的孩子 Alice 和 Bob (Alice 比 Bob 年长)和 Alice 的孩子 Jack 组成。

1. 一开始, curOrder 为 ["king"].
2. 调用 Successor(king, curOrder) ,返回 Alice ,所以我们将 Alice 放入 curOrder 中,得到 ["king", "Alice"] 。
3. 调用 Successor(Alice, curOrder) ,返回 Jack ,所以我们将 Jack 放入 curOrder 中,得到 ["king", "Alice", "Jack"] 。
4. 调用 Successor(Jack, curOrder) ,返回 Bob ,所以我们将 Bob 放入 curOrder 中,得到 ["king", "Alice", "Jack", "Bob"] 。
5. 调用 Successor(Bob, curOrder) ,返回 null 。最终得到继承顺序为 ["king", "Alice", "Jack", "Bob"] 。
通过以上的函数,我们总是能得到一个唯一的继承顺序。

请你实现 ThroneInheritance 类:

* ThroneInheritance(string kingName) 初始化一个 ThroneInheritance 类的对象。国王的名字作为构造函数的参数传入。
* void birth(string parentName, string childName) 表示 parentName 新拥有了一个名为 childName 的孩子。
* void death(string name) 表示名为 name 的人死亡。一个人的死亡不会影响 Successor 函数,也不会影响当前的继承顺序。你可以只将这个人标记为死亡状态。
* string[] getInheritanceOrder() 返回 除去 死亡人员的当前继承顺序列表。

示例:
```
输入:
["ThroneInheritance", "birth", "birth", "birth", "birth", "birth", "birth", "getInheritanceOrder", "death", "getInheritanceOrder"]
[["king"], ["king", "andy"], ["king", "bob"], ["king", "catherine"], ["andy", "matthew"], ["bob", "alex"], ["bob", "asha"], [null], ["bob"], [null]]

输出:
[null, null, null, null, null, null, null, ["king", "andy", "matthew", "bob", "alex", "asha", "catherine"], null, ["king", "andy", "matthew", "alex", "asha", "catherine"]]

解释:
ThroneInheritance t= new ThroneInheritance("king"); // 继承顺序:king
t.birth("king", "andy"); // 继承顺序:king > andy
t.birth("king", "bob"); // 继承顺序:king > andy > bob
t.birth("king", "catherine"); // 继承顺序:king > andy > bob > catherine
t.birth("andy", "matthew"); // 继承顺序:king > andy > matthew > bob > catherine
t.birth("bob", "alex"); // 继承顺序:king > andy > matthew > bob > alex > catherine
t.birth("bob", "asha"); // 继承顺序:king > andy > matthew > bob > alex > asha > catherine
t.getInheritanceOrder(); // 返回 ["king", "andy", "matthew", "bob", "alex", "asha", "catherine"]
t.death("bob"); // 继承顺序:king > andy > matthew > bob(已经去世)> alex > asha > catherine
t.getInheritanceOrder(); // 返回 ["king", "andy", "matthew", "alex", "asha", "catherine"]
```

提示:
* 1 <= kingName.length, parentName.length, childName.length, name.length <= 15
* kingName,parentName, childName 和 name 仅包含小写英文字母。
* 所有的参数 childName 和 kingName 互不相同。
* 所有 death 函数中的死亡名字 name 要么是国王,要么是已经出生了的人员名字。
* 每次调用 birth(parentName, childName) 时,测试用例都保证 parentName 对应的人员是活着的。
* 最多调用 $10^5$ 次birth 和 death 。
* 最多调用 10 次 getInheritanceOrder 。

Solution:
### 单向链表 & 标记删除

根据题意,我们需要将「新儿子」插入到「父亲」的「最后一个儿子」的「儿子们」的后面(**注意这是个递归过程**);如果该「父亲」还没有任何儿子,则直接插到「父亲」后面。

因此,我们需要在节点 `Node` 中使用一个 `last` 记录该节点的「最后一个儿子」,同时因为删除的时候,我们无法在 $O(1)$ 的复杂度内更新 `last` 信息,所以只能使用「标记删除」的方式。

代码:
```Java
class ThroneInheritance {
    class Node {
        String name;
        Node next;
        Node last; // 记录最后一个儿子
        boolean isDeleted = false;
        Node (String _name) {
            name = _name;
        }
    }
    Map<String, Node> map = new HashMap<>();
    Node head = new Node(""), tail = new Node("");
    public ThroneInheritance(String name) {
        Node root = new Node(name);
        root.next = tail;
        head.next = root;
        map.put(name, root);
    }

    public void birth(String pname, String cname) {
        Node node = new Node(cname);
        map.put(cname, node);
        Node p = map.get(pname);
        Node tmp = p;
        while (tmp.last != null) tmp = tmp.last;
        node.next = tmp.next;
        tmp.next = node;
        p.last = node;
    }

    public void death(String name) {
        Node node = map.get(name);
        node.isDeleted = true;
    }

    public List<String> getInheritanceOrder() {
        List<String> ans = new ArrayList<>();
        Node tmp = head.next;
        while (tmp.next != null) {
            if (!tmp.isDeleted) ans.add(tmp.name);
            tmp = tmp.next;
        }
        return ans;
    }
}
```
* 时间复杂度:`birth` 和 `getInheritanceOrder` 操作为 $O(n)$;其余操作为 $O(1)$
* 时间复杂度:$O(n)$

### 最后

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

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

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

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


LeetCode/1601-1610/1601. 最多可达成的换楼请求数目（困难）.md

1601. 最多可达成的换楼请求数目

Question:
我们有 `n` 栋楼,编号从 `0` 到 `n - 1` 。每栋楼有若干员工。由于现在是换楼的季节,部分员工想要换一栋楼居住。

给你一个数组 $requests$ ,其中 $requests[i] = [from_i, to_i]$ ,表示一个员工请求从编号为 $from_i$ 的楼搬到编号为 $to_i$ 的楼。

一开始 所有楼都是满的,所以从请求列表中选出的若干个请求是可行的需要满足 每栋楼员工净变化为 $0$ 。意思是每栋楼 离开 的员工数目 等于 该楼 搬入 的员工数数目。比方说 $n = 3$ 且两个员工要离开楼 $0$ ,一个员工要离开楼 $1$ ,一个员工要离开楼 $2$ ,如果该请求列表可行,应该要有两个员工搬入楼 $0$ ,一个员工搬入楼 $1$ ,一个员工搬入楼 $2$ 。

请你从原请求列表中选出若干个请求,使得它们是一个可行的请求列表,并返回所有可行列表中最大请求数目。

示例 1:

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

输出:5
解释:请求列表如下:
从楼 0 离开的员工为 x 和 y ,且他们都想要搬到楼 1 。
从楼 1 离开的员工为 a 和 b ,且他们分别想要搬到楼 2 和 0 。
从楼 2 离开的员工为 z ,且他想要搬到楼 0 。
从楼 3 离开的员工为 c ,且他想要搬到楼 4 。
没有员工从楼 4 离开。
我们可以让 x 和 b 交换他们的楼,以满足他们的请求。
我们可以让 y,a 和 z 三人在三栋楼间交换位置,满足他们的要求。
所以最多可以满足 5 个请求。
```
示例 2:

```
输入:n = 3, requests = [[0,0],[1,2],[2,1]]

输出:3

解释:请求列表如下:
从楼 0 离开的员工为 x ,且他想要回到原来的楼 0 。
从楼 1 离开的员工为 y ,且他想要搬到楼 2 。
从楼 2 离开的员工为 z ,且他想要搬到楼 1 。
我们可以满足所有的请求。
```
示例 3:
```
输入:n = 4, requests = [[0,3],[3,1],[1,2],[2,0]]

输出:4
```

提示:
* $1 <= n <= 20$
* $1 <= requests.length <= 16$
* $requests[i].length == 2$
* $0 <= fromi, toi < n$

Solution:
### 二进制枚举

为了方便,我们令 $requests$ 的长度为 $m$。

数据范围很小,$n$ 的范围为 $20$,而 $m$ 的范围为 $16$。

根据每个 $requests[i]$ 是否选择与否,共有 $2^m$ 种状态(不超过 $70000$ 种状态)。我们可以采用「二进制枚举」的思路来求解,使用二进制数 $state$ 来表示对 $requests[i]$ 的选择情况,当 $state$ 的第 $k$ 位为 $1$,代表 $requests[k]$ 被选择。

我们枚举所有的 $state$ 并进行合法性检查,从中选择出包含请求数的最多(二进制表示中包含 $1$ 个数最多)的合法 $state$,其包含的请求数量即是答案。

其中统计 $state$ 中 $1$ 的个数可以使用 `lowbit`,复杂度为 $O(m)$,判断合法性则直接模拟即可(统计每座建筑的进出数量,最后判定进出数不相等的建筑数量是为 $0$),复杂度为 $O(m)$,整体计算量为不超过 $2 \times  10^6$,可以过。

代码:
```Java
class Solution {
    int[][] rs;
    public int maximumRequests(int n, int[][] requests) {
        rs = requests;
        int m = rs.length, ans = 0;
        for (int i = 0; i < (1 << m); i++) {
            int cnt = getCnt(i);
            if (cnt <= ans) continue;
            if (check(i)) ans = cnt;
        }
        return ans;
    }
    boolean check(int s) {
        int[] cnt = new int[20];
        int sum = 0;
        for (int i = 0; i < 16; i++) {
            if (((s >> i) & 1) == 1) {
                int a = rs[i][0], b = rs[i][1];
                if (++cnt[a] == 1) sum++;
                if (--cnt[b] == 0) sum--;
            }
        }
        return sum == 0;
    }
    int getCnt(int s) {
        int ans = 0;
        for (int i = s; i > 0; i -= (i & -i)) ans++;
        return ans;
    }
}
```
* 时间复杂度:令 $m$ 为 $requests$ 长度,共有 $2^m$ 种选择状态,计算每个状态的所包含的问题数量复杂度为 $O(m)$,计算某个状态是否合法复杂度为 $O(m)$;整体复杂度为 $O(2^m \times m)$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


1603. 设计停车系统

Question:
请你给一个停车场设计一个停车系统。停车场总共有三种不同大小的车位:大,中和小,每种尺寸分别有固定数目的车位。

请你实现 ParkingSystem 类:

* ParkingSystem(int big, int medium, int small) 初始化 ParkingSystem 类,三个参数分别对应每种停车位的数目。
* bool addCar(int carType) 检查是否有 carType 对应的停车位。 carType 有三种类型:大,中,小,分别用数字 1, 2 和 3 表示。一辆车只能停在  carType 对应尺寸的停车位中。如果没有空车位,请返回 false ,否则将该车停入车位并返回 true 。

示例 1:
```
输入:
["ParkingSystem", "addCar", "addCar", "addCar", "addCar"]
[[1, 1, 0], [1], [2], [3], [1]]
输出:
[null, true, true, false, false]

解释:
ParkingSystem parkingSystem = new ParkingSystem(1, 1, 0);
parkingSystem.addCar(1); // 返回 true ,因为有 1 个空的大车位
parkingSystem.addCar(2); // 返回 true ,因为有 1 个空的中车位
parkingSystem.addCar(3); // 返回 false ,因为没有空的小车位
parkingSystem.addCar(1); // 返回 false ,因为没有空的大车位,唯一一个大车位已经被占据了
```

提示:
* 0 <= big, medium, small <= 1000
* carType 取值为 1, 2 或 3
* 最多会调用 addCar 函数 1000 次

Solution:
### 简单变量

一个简单的做法是,直接使用几个成员变量来记录。

```Java
class ParkingSystem {
    int big, medium, small;
    public ParkingSystem(int _big, int _medium, int _small) {
        big = _big;
        medium = _medium;
        small = _small;
    }

    public boolean addCar(int ct) {
        if (ct == 1 && big > 0) return big-- > 0;
        else if (ct == 2 && medium > 0) return medium-- > 0;
        else if (ct == 3 && small > 0) return small-- > 0;
        return false;
    }
}
```
* 时间复杂度:$O(1)$
* 空间复杂度:$O(1)$

***

### 哈希表

另外一个更好拓展的方法,使用哈希表来进行记录。

这样做的好处是,当增加车类型,只需要重载一个构造方法即可。

```Java
class ParkingSystem {
    Map<Integer, Integer> map = new HashMap<>();
    public ParkingSystem(int _big, int _medium, int _small) {
        map.put(1, _big);
        map.put(2, _medium);
        map.put(3, _small);
    }

    public boolean addCar(int ct) {
        if (map.get(ct) > 0) {
            map.put(ct, map.get(ct) - 1);
            return true;
        }
        return false;
    }
}
```
* 时间复杂度:$O(1)$
* 空间复杂度:$O(1)$

***

### 二进制分段

事实上,由于 $1000$ 的二进制表示只有 $10$ 位,而 $int$ 有 $32$ 位。

我们可以使用一个 $int$ 配合「位运算」来分段做。

使用 $[0,10)$ 代表 big,$[10,20)$ 表示 medium,$[20,30)$ 表示 small

*PS. 这样 $int$ 分段的做法,在工程源码上也有体现:`JDK` 中的 `ThreadPoolExecutor` 使用了一个 $ctl$ 变量 ($int$ 类型) 的前 $3$ 位记录线程池的状态,后 $29$ 位记录程池中线程个数。*

**这样的「二进制分段压缩存储」的主要目的,不是为了减少使用一个 $int$,而是为了让「非原子性操作」变为「原子性操作」。**

我们可以分析下为什么 `ThreadPoolExecutor` 要这么做。

当线程数量变化为某个特定值时,要修改的就不仅仅是「线程数量」,还需要修改「线程池的状态」。

由于并发环境下,如果要做到「原子性」地同时需要修改两个 $int$ 的话。只能上「重量级锁」,「重量级锁」就会涉及到「内核态」的系统调用,通常是耗时是「用户态」的上百倍。

但是如果我们将「线程数量」和「线程池的状态」合二为一之后,我们只需要修改一个 $int$,这时候只需要使用 CAS 做法(用户态)即可保证线程安全与原子性。

那么对应到该题,如果我们允许同时停入不同类型的车,在不引入重量级锁的前提下,想要真正做到「同时」修改两种类型的车的车位的话,只能采用这样的「二进制分段」做法 ~

```Java
class ParkingSystem {
    int cnt; // [small medium big]
    public ParkingSystem(int _big, int _medium, int _small) {
        for (int i = 0; i < 30; i++) {
            int cur = 0;
            if (i < 10) {
                cur = (_big >> i) & 1;
            } else if (i < 20) {
                cur = (_medium >> (i - 10)) & 1;
            } else if (i < 30) {
                cur = (_small >> (i - 20)) & 1;
            }
            cnt += cur == 1 ? (1 << i) : 0;
        }
    }

    public boolean addCar(int ct) {
        int cur = countOfType(ct);
        if (cur > 0) {
            setCount(ct, cur - 1);
            return true;
        }
        return false;
    }

    int countOfType(int ct) {
        int ans = 0;
        int start = --ct * 10, end = start + 10;
        for (int i = start; i < end; i++) {
            if (((cnt >> i) & 1) == 1) {
                ans += (1 << (i - start));
            }
        }
        return ans;
    }

    void setCount(int ct, int pc) {
        int start = --ct * 10, end = start + 10;
        for (int i = start; i < end; i++) {
            if (((pc >> (i - start)) & 1) == 1) {
                cnt |= (1 << i);
            } else {
                cnt &= ~(1 << i);
            }
        }
    }
}
```
* 时间复杂度:$O(1)$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


LeetCode/1601-1610/1606. 找到处理最多请求的服务器（困难）.md

1606. 找到处理最多请求的服务器

Question:
你有 $k$ 个服务器,编号为 $0$ 到 $k-1$ ,它们可以同时处理多个请求组。

每个服务器有无穷的计算能力但是不能同时处理超过一个请求。

请求分配到服务器的规则如下:
* 第 $i$(序号从 $0$ 开始)个请求到达。
* 如果所有服务器都已被占据,那么该请求被舍弃(完全不处理)。
* 如果第 ( `i % k` ) 个服务器空闲,那么对应服务器会处理该请求。
* 否则,将请求安排给下一个空闲的服务器(服务器构成一个环,必要的话可能从第 $0$ 个服务器开始继续找下一个空闲的服务器)。比方说,如果第 $i$ 个服务器在忙,那么会查看第 ( $i+1$ ) 个服务器,第 ( $i+2$ ) 个服务器等等。
* 给你一个严格递增的正整数数组 `arrival`,表示第 $i$ 个任务的到达时间,和另一个数组 `load` ,其中 $load[i]$ 表示第 $i$ 个请求的工作量(也就是服务器完成它所需要的时间)。你的任务是找到 最繁忙的服务器 。最繁忙定义为一个服务器处理的请求数是所有服务器里最多的。

请你返回包含所有最繁忙服务器序号的列表,你可以以任意顺序返回这个列表。

示例 1:

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

输出:[1]

解释:
所有服务器一开始都是空闲的。
前 3 个请求分别由前 3 台服务器依次处理。
请求 3 进来的时候,服务器 0 被占据,所以它呗安排到下一台空闲的服务器,也就是服务器 1 。
请求 4 进来的时候,由于所有服务器都被占据,该请求被舍弃。
服务器 0 和 2 分别都处理了一个请求,服务器 1 处理了两个请求。所以服务器 1 是最忙的服务器。
```
示例 2:
```
输入:k = 3, arrival = [1,2,3,4], load = [1,2,1,2]

输出:[0]

解释:
前 3 个请求分别被前 3 个服务器处理。
请求 3 进来,由于服务器 0 空闲,它被服务器 0 处理。
服务器 0 处理了两个请求,服务器 1 和 2 分别处理了一个请求。所以服务器 0 是最忙的服务器。
```
示例 3:
```
输入:k = 3, arrival = [1,2,3], load = [10,12,11]

输出:[0,1,2]

解释:每个服务器分别处理了一个请求,所以它们都是最忙的服务器。
```
示例 4:
```
输入:k = 3, arrival = [1,2,3,4,8,9,10], load = [5,2,10,3,1,2,2]

输出:[1]
```
示例 5:
```
输入:k = 1, arrival = [1], load = [1]

输出:[0]
```

提示:
* $1 <= k <= 10^5$
* $1 <= arrival.length, load.length <= 10^5$
* $arrival.length == load.length$
* $1 <= arrival[i], load[i] <= 10^9$
* `arrival` 保证严格递增。

Solution:
### 数据结构

题目要统计处理任务数最多的机器,首先容易想到使用「哈希表」统计每个机台处理的任务数,利用机台数量 $k$ 最多不超过 $10^5$,我们可以开一个静态数组 `cnts` 来充当哈希表,同时维护一个当前处理的最大任务数量 `max`,最终所有满足 $cnst[i] = \max$ 的机台集合即是答案。

再根据「每个任务有对应的开始时间和持续时间」以及「任务分配规则」,容易想到使用优先队列(堆)和有序集合(红黑树)来进行维护。

具体的,利用「每个任务有对应的开始时间和持续时间」,我们使用优先队列(堆)维护二元组 $(idx, endTime)$,其中 $idx$ 为机器编号,$endTime$ 为当前机台所处理任务的结束时间(也就是该机台最早能够接受新任务的时刻),对于每个 $arrival[i]$ 而言(新任务),我们先从优先队列中取出所有 $endTime \leqslant arrival[i]$ 的机台 $idx$,加入「空闲池」,然后再按照「任务分配规则」从空闲池子中取机台,若取不到,则丢弃该任务。

由于「任务分配规则」是优先取大于等于 `i % k` 的最小值,若取不到,再取大于等于 $0$ 的最小值。因此我们的「空闲池」最好是支持「二分」的有序集合,容易想到基于「红黑树」的 `TreeSet` 结构。

Java 代码:
```Java
class Solution {
    static int N = 100010;
    static int[] cnts = new int[N];
    public List<Integer> busiestServers(int k, int[] arrival, int[] load) {
        Arrays.fill(cnts, 0);
        int n = arrival.length, max = 0;
        PriorityQueue<int[]> busy = new PriorityQueue<>((a,b)->a[1]-b[1]);
        TreeSet<Integer> free = new TreeSet<>();
        for (int i = 0; i < k; i++) free.add(i);
        for (int i = 0; i < n; i++) {
            int start = arrival[i], end = start + load[i];
            while (!busy.isEmpty() && busy.peek()[1] <= start) free.add(busy.poll()[0]);
            Integer u = free.ceiling(i % k);
            if (u == null) u = free.ceiling(0);
            if (u == null) continue;
            free.remove(u);
            busy.add(new int[]{u, end});
            max = Math.max(max, ++cnts[u]);
        }
        List<Integer> ans = new ArrayList<>();
        for (int i = 0; i < k; i++) {
            if (cnts[i] == max) ans.add(i);
        }
        return ans;
    }
}
```
Python 代码:
```Python
from sortedcontainers import SortedList

class Solution:
    def busiestServers(self, k: int, arrival: List[int], load: List[int]) -> List[int]:
        cnts = [0] * k
        n, m = len(arrival), 0
        busy, free = [], SortedList(range(k))
        for i in range(n):
            start, end = arrival[i], arrival[i] + load[i]
            while busy and busy[0][0] <= start:
                free.add(busy[0][1])
                heappop(busy)
            if (idx := free.bisect_left(i % k)) == len(free) == (idx := free.bisect_left(0)):
                continue
            u = free[idx]
            free.remove(u)
            heappush(busy, (end, u))
            cnts[u] += 1
            m = max(m, cnts[u])
        return [i for i in range(k) if cnts[i] == m]
```
* 时间复杂度:令任务数量为 $n$,机台数量为 $k$,起始将所有机台存入 `TreeSet`,复杂度为 $O(k\log{k})$;每次处理新的 $arrival[i]$ 时,先从优先队列取出可接受新任务的机台,存入 `TreeSet`,然后从 `TreeSet` 中取出最多一个的机台来完成任务,其中从 `TreeSet` 中取出机台最多调用两次的 `ceiling` 操作,复杂度为 $O(\log{k})$,这部分的整体复杂度为 $O(n\log{k})$;统计处理任务数达到 `max` 的机台集合复杂度为 $O(k)$;整体复杂度为 $O((k + n)\log{k})$
* 空间复杂度:$O(k)$

### 最后

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

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

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

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


LeetCode/1601-1610/1608. 特殊数组的特征值（简单）.md

1608. 特殊数组的特征值

Question:
给你一个非负整数数组 `nums`。如果存在一个数 `x` ,使得 `nums` 中恰好有 `x` 个元素 大于或者等于 `x` ,那么就称 `nums` 是一个 特殊数组 ,而 `x` 是该数组的 特征值 。

注意: `x` 不必 是 `nums` 的中的元素。

如果数组 `nums` 是一个 特殊数组 ,请返回它的特征值 `x` 。否则,返回 `-1` 。可以证明的是,如果 `nums` 是特殊数组,那么其特征值 `x` 是 唯一的 。

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

输出:2

解释:有 2 个元素(3 和 5)大于或等于 2 。
```
示例 2:
```
输入:nums = [0,0]

输出:-1

解释:没有满足题目要求的特殊数组,故而也不存在特征值 x 。
如果 x = 0,应该有 0 个元素 >= x,但实际有 2 个。
如果 x = 1,应该有 1 个元素 >= x,但实际有 0 个。
如果 x = 2,应该有 2 个元素 >= x,但实际有 0 个。
x 不能取更大的值,因为 nums 中只有两个元素。
```
示例 3:
```
输入:nums = [0,4,3,0,4]

输出:3

解释:有 3 个元素大于或等于 3 。
```
示例 4:
```
输入:nums = [3,6,7,7,0]

输出:-1
```

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

Solution:
### 排序 + 枚举 + 二分

根据题意并结合 $nums[i]$ 的数据范围为 $1e3$,我们可以通过「枚举」的方式找到 `x`,而对于每个 `x` 的合法性检查,我们需要快速知道 `nums` 中比 `x` 大的数的个数,这可以通过「排序 + 二分」来做。

Java 代码:
```Java
class Solution {
    public int specialArray(int[] nums) {
        Arrays.sort(nums);
        int n = nums.length;
        for (int x = 0; x < 1010; x++) {
            int l = 0, r = n - 1;
            while (l < r) {
                int mid = l + r >> 1;
                if (nums[mid] >= x) r = mid;
                else l = mid + 1;
            }
            if (nums[r] >= x && x == n - r) return x;
        }
        return -1;
    }
}
```
TypeScript 代码:
```TypeScript
function specialArray(nums: number[]): number {
    const n = nums.length
    nums.sort((a,b)=>a-b)
    for (let x = 0; x < 1010; x++) {
        let l = 0, r = n - 1
        while (l < r) {
            const mid = l + r >> 1
            if (nums[mid] >= x) r = mid
            else l = mid + 1
        }
        if (nums[r] >= x && x == n - r) return x
    }
    return -1
};
```
* 时间复杂度:排序的复杂度为 $O(n\log{n})$;枚举找 `x` 的复杂度为 $O(C\log{n})$,其中 $C = 1e3$ 为 $nums[i]$ 的值域大小
* 空间复杂度:$O(\log{n})$

---

### 排序 + 二分

若不利用 $nums[i]$ 的值域偏小(即值域放大到 $1e9$),我们可以通过「排序 + 两次二分」的操作来做:第一次二分找分割点 `x`(二分范围为 $[0, 1e9]$);第二次则是在 `getCnt` 函数内部中使用二分来对 `x` 进行合法性检查。

我们知道若真实的特殊值为 `x`,那么在以 `x` 为分割点的数轴上具有「二段性」,假设当前值为 `k`:
* 小于真实特殊值 `x` 的 `k` 值满足「`nums` 中大于等于 `k` 的元素个数超过 `k` 个」
* 大于等于真实特殊值 `x` 的 `k` 值满足「`nums` 中大于等于 `k` 的元素个数不超过 `k` 个」

因此可以通过「二分」来找真实特殊值为 `x`,至于 `x` 的合法检查则和解法一相同,先通过排序确保数组有序,再通过二分的方式来统计大于等于 `x` 的数的个数。

Java 代码:
```Java
class Solution {
    int[] nums;
    public int specialArray(int[] _nums) {
        nums = _nums;
        Arrays.sort(nums);
        int l = 0, r = (int) 1e9;
        while (l < r) {
            int mid = l + r >> 1;
            if (getCnt(mid) <= mid) r = mid;
            else l = mid + 1;
        }
        return getCnt(r) == r ? r : -1;
    }
    int getCnt(int x) {
        int n = nums.length, l = 0, r = n - 1;
        while (l < r) {
            int mid = l + r >> 1;
            if (nums[mid] >= x) r = mid;
            else l = mid + 1;
        }
        return nums[r] >= x ? n - r : 0;
    }
}
```
TypeScript 代码:
```TypeScript
let nums: number[]
function specialArray(_nums: number[]): number {
    nums = _nums
    nums.sort((a,b)=>a-b)
    let l = 0, r = 1e9
    while (l < r) {
        const mid = l + r >> 1
        if (getCnt(mid) <= mid) r = mid
        else l = mid + 1
    }
    return getCnt(r) == r ? r : -1
};
function getCnt(x: number): number {
    let n = nums.length, l = 0, r = n - 1
    while (l < r) {
        const mid = l + r >> 1
        if (nums[mid] >= x) r = mid
        else l = mid + 1
    }
    return nums[r] >= x ? n - r : 0
}
```
* 时间复杂度:排序复杂度为 $O(n\log{n})$,二分找答案复杂度为 $O(\log{C} \times \log{n})$
* 空间复杂度:$O(\log{n})$

---

### 模拟(计数 + 枚举)

另外一种除非过大缩放 $nums$ 的长度 $n$,否则仅有代码量优势的做法是使用「计数 + 枚举」的模拟做法。

先使用静态数组对 $nums$ 进行词频统计,随后通过逆序枚举的方式找特殊值 `x`,同时使用 `tot` 统计遍历过的桶的总元素个数,当满足 `tot = x` 时,返回结果。

Java 代码:
```Java
class Solution {
    public int specialArray(int[] nums) {
        int[] cnts = new int[1010];
        for (int x : nums) cnts[x]++;
        for (int i = 1009, tot = 0; i >= 0; i--) {
            tot += cnts[i];
            if (i == tot) return i;
        }
        return -1;
    }
}
```
TypeScript 代码:
```TypeScript
function specialArray(nums: number[]): number {
    const cnts = new Array<number>(1010).fill(0)
    for (let x of nums) cnts[x]++
    for (let i = 1009, tot = 0; i >= 0; i--) {
        tot += cnts[i]
        if (tot == i) return i
    }
    return -1
};
```
* 时间复杂度:$O(n + C)$,其中 $C = 1e3$ 为 $nums[i]$ 的值域大小
* 空间复杂度:$O(C)$

### 最后

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

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

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

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


Question:
如果一棵二叉树满足下述几个条件,则可以称为 **奇偶树** :

二叉树根节点所在层下标为 $0$ ,根的子节点所在层下标为 $1$ ,根的孙节点所在层下标为 $2$ ,依此类推。

* **偶数下标** 层上的所有节点的值都是 **奇** 整数,从左到右按顺序 **严格递增**
* **奇数下标** 层上的所有节点的值都是 **偶** 整数,从左到右按顺序 **严格递减**

给你二叉树的根节点,如果二叉树为 奇偶树 ,则返回 `true` ,否则返回 `false` 。

示例 1:

```
输入:root = [1,10,4,3,null,7,9,12,8,6,null,null,2]

输出:true

解释:每一层的节点值分别是:
0 层:[1]
1 层:[10,4]
2 层:[3,7,9]
3 层:[12,8,6,2]
由于 0 层和 2 层上的节点值都是奇数且严格递增,而 1 层和 3 层上的节点值都是偶数且严格递减,因此这是一棵奇偶树。
```
示例 2:

```
输入:root = [5,4,2,3,3,7]

输出:false

解释:每一层的节点值分别是:
0 层:[5]
1 层:[4,2]
2 层:[3,3,7]
2 层上的节点值不满足严格递增的条件,所以这不是一棵奇偶树。
```
示例 3:

```
输入:root = [5,9,1,3,5,7]

输出:false

解释:1 层上的节点值应为偶数。
```
示例 4:
```
输入:root = [1]

输出:true
```
示例 5:
```
输入:root = [11,8,6,1,3,9,11,30,20,18,16,12,10,4,2,17]

输出:true
```

提示:
* 树中节点数在范围 $[1, 10^5]$ 内
* $1 <= Node.val <= 10^6$

Solution:
### BFS

考察「层序遍历」,在遍历过程中需要记录层下标,并根据层下标检查进行「节点值的奇偶性」和「是否满足递增/递减」。

额外使用一个布尔变量 `flag` 记录层序是否为偶数(判断节点值的奇偶性),使用 `prev` 记录当前层的上一节点的值(判断是否满足递增/递减)即可,`prev` 起始值可根据数据范围设置为哨兵值。

代码:
```Java
class Solution {
    public boolean isEvenOddTree(TreeNode root) {
        Deque<TreeNode> d = new ArrayDeque<>();
        boolean flag = true;
        d.addLast(root);
        while (!d.isEmpty()) {
            int size = d.size(), prev = flag ? 0 : 0x3f3f3f3f;
            while (size-- > 0) {
                TreeNode node = d.pollFirst();
                int cur = node.val;
                if (flag && (cur % 2 == 0 || cur <= prev)) return false;
                if (!flag && (cur % 2 != 0 || cur >= prev)) return false;
                prev = cur;
                if (node.left != null) d.addLast(node.left);
                if (node.right != null) d.addLast(node.right);
            }
            flag = !flag;
        }
        return true;
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n)$

---

### DFS

同样的思路,自然也能够使用 `DFS` 进行求解。

由于 `DFS`  是深度优先,因此我们在 `DFS` 过程中除了要记录当前层编号(判断节点值的奇偶性),还要记录每层最后上一次遍历到的节点值为多少(判断是否满足递增/递减)。

代码:
```Java
class Solution {
    Map<Integer, Integer> map = new HashMap<>();
    public boolean isEvenOddTree(TreeNode root) {
        return dfs(root, 0);
    }
    boolean dfs(TreeNode root, int idx) {
        boolean flag = idx % 2 == 0;
        int prev = map.getOrDefault(idx, flag ? 0 : 0x3f3f3f3f), cur = root.val;
        if (flag && (cur % 2 == 0 || cur <= prev)) return false;
        if (!flag && (cur % 2 != 0 || cur >= prev)) return false;
        map.put(idx, root.val);
        if (root.left != null && !dfs(root.left, idx + 1)) return false;
        if (root.right != null && !dfs(root.right, idx + 1)) return false;
        return true;
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


LeetCode/1601-1610/1610. 可见点的最大数目（困难）.md

1610. 可见点的最大数目

Question:
给你一个点数组 `points` 和一个表示角度的整数 `angle` ,你的位置是 `location`,其中 $location = [pos_x, pos_y]$ 且 $points[i] = [x_i, y_i]$ 都表示 `X-Y` 平面上的整数坐标。

最开始,你面向东方进行观测。你 不能 进行移动改变位置,但可以通过 自转 调整观测角度。换句话说,$pos_x$ 和 $pos_y$ 不能改变。你的视野范围的角度用 `angle` 表示, 这决定了你观测任意方向时可以多宽。设 d 为你逆时针自转旋转的度数,那么你的视野就是角度范围 $[d - angle/2, d + angle/2]$ 所指示的那片区域。

对于每个点,如果由该点、你的位置以及从你的位置直接向东的方向形成的角度 位于你的视野中 ,那么你就可以看到它。

同一个坐标上可以有多个点。你所在的位置也可能存在一些点,但不管你的怎么旋转,总是可以看到这些点。同时,点不会阻碍你看到其他点。

返回你能看到的点的最大数目。

示例 1:

```
输入:points = [[2,1],[2,2],[3,3]], angle = 90, location = [1,1]

输出:3

解释:阴影区域代表你的视野。在你的视野中,所有的点都清晰可见,尽管 [2,2] 和 [3,3]在同一条直线上,你仍然可以看到 [3,3] 。
```
示例 2:
```
输入:points = [[2,1],[2,2],[3,4],[1,1]], angle = 90, location = [1,1]

输出:4

解释:在你的视野中,所有的点都清晰可见,包括你所在位置的那个点。
```
示例 3:

```
输入:points = [[1,0],[2,1]], angle = 13, location = [1,1]

输出:1

解释:如图所示,你只能看到两点之一。
```

提示:
* $1 <= points.length <= 10^5$
* $points[i].length == 2$
* $location.length == 2$
* $0 <= angle < 360$
* $0 <= posx, posy, xi, yi <= 100$

Solution:
### 数学

这是一道思维难度不大,但细节繁多的题目。

题目要我们旋转出一个角度为 $angle$ 的、可无限延伸的覆盖面,使得该覆盖面所能覆盖 $points$ 中的点最多。

我们所在的位置为 $location = (x,y)$,我们可以将 $(x,y)$ 作为「极点」,求所有 $points[i]$ 的「极角」。

令 $points[i] =(a,b)$,与极点关系 $dx = a - x;dy = b - y$。

求极角的方式有两种:

1. 使用 $atan(\frac{dy}{dx})$:值域范围为 [-90°,90°],需要对 $dx$ 与 $dy$ 进行象限讨论,从而将值域范围转化为我们希望的 [0°,360°],同时需要注意 $dx = 0$ 的边界情况;

2. 使用 $atan2(dy, dx)$:值域范围为 [-180°,180°],与我们期望的 [0°,360°] 相差一个固定的值,可进行统一转换,也可以直接使用。

得到夹角数组 $list$ 后,对其进行排序,问题初步转换为:在夹角数组中找到连续一段 $[i, j]$,使得 $list[i]$ 和 $list[j]$ 的角度差不超过 $angle$。

但直接在原数组 $list$ 操作,会漏掉夹角横跨一四象限的情况:

因此,另外一个细节是,在求连续段长度时,先对夹角数组进行拷贝拼接,并对拼接部分增加偏移量(确保数组仍具有单调性)。

具体的,设夹角数组长度为 $n$,此时令 $list[n + i] = list[i] + 2 * PI$,从而将问题彻底转换为求连续段问题。

求解最长合法连续段 $[i,j]$ 可用「双指针」实现「滑动窗口」来做。

一些细节:题目规定了与 $location$ 重合的点在任意角度都能看到,因此我们需要对这些点进行特殊处理,

代码:
```Java
class Solution {
    public int visiblePoints(List<List<Integer>> points, int angle, List<Integer> location) {
        int x = location.get(0), y = location.get(1);
        List<Double> list = new ArrayList<>();
        int cnt = 0;
        double pi = Math.PI, t = angle * pi / 180;
        for (List<Integer> p : points) {
            int a = p.get(0), b = p.get(1);
            if (a == x && b == y && ++cnt >= 0) continue;
            list.add(Math.atan2(b - y, a - x) + pi);
        }
        Collections.sort(list);
        int n = list.size(), max = 0;
        for (int i = 0; i < n; i++) list.add(list.get(i) + 2 * pi);
        for (int i = 0, j = 0; j < 2 * n; j++) {
            while (i < j && list.get(j) - list.get(i) > t) i++;
            max = Math.max(max, j - i + 1);
        }
        return cnt + max;
    }
}
```
* 时间复杂度:令 $n$ 为 `points` 数组的长度,预处理出 `points` 的所有角度复杂度为 $O(n)$;对所有角度进行排序的复杂度为 $O(n\log{n})$;使用双指针实现滑动窗口得出最大合法子数组的复杂度为 $O(n)$;整体复杂度为 $O(n\log{n})$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


Question:
峰值元素是指其值严格大于左右相邻值的元素。

给你一个整数数组 `nums`,找到峰值元素并返回其索引。数组可能包含多个峰值,在这种情况下,返回 任何一个峰值 所在位置即可。

你可以假设 `nums[-1] = nums[n] = -∞` 。

你必须实现时间复杂度为 $O(\log{n})$ 的算法来解决此问题。

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

输出:2

解释:3 是峰值元素,你的函数应该返回其索引 2。
```
示例 2:
```
输入:nums = [1,2,1,3,5,6,4]

输出:1 或 5

解释:你的函数可以返回索引 1,其峰值元素为 2;
     或者返回索引 5, 其峰值元素为 6。
```

提示:
* $1 <= nums.length <= 1000$
* $-2^{31} <= nums[i] <= 2^{31} - 1$
* 对于所有有效的 `i` 都有 `nums[i] != nums[i + 1]`

Solution:
### 模拟

由于数据范围只有 $1000$,使用线性扫描找峰值的模拟做法也是没有问题。

代码:
```Java
class Solution {
    public int findPeakElement(int[] nums) {
        int n = nums.length;
        for (int i = 0; i < n; i++) {
            boolean ok = true;
            if (i - 1 >= 0) {
                if (nums[i - 1] >= nums[i]) ok = false;
            }
            if (i + 1 < n) {
                if (nums[i + 1] >= nums[i]) ok = false;
            }
            if (ok) return i;
        }
        return -1;
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(1)$

---

### 二分

题目让我们实现一个 $O(\log{n})$ 算法,这是对使用「二分」的强烈暗示。

和往常的题目一样,**我们应当从是否具有「二段性」来考虑是否可以进行「二分」**。

不难发现,**如果** 在确保有解的情况下,我们可以根据当前的分割点 $mid$ 与左右元素的大小关系来指导 $l$ 或者 $r$ 的移动。

假设当前分割点 $mid$ 满足关系 $num[mid] > nums[mid + 1]$ 的话,一个很简单的想法是 $num[mid]$ 可能为峰值,而 $nums[mid + 1]$ 必然不为峰值,于是让 $r = mid$,从左半部分继续找峰值。

估计不少同学靠这个思路 AC 了,只能说做法对了,分析没对。

上述做法正确的前提有两个:

1. 对于任意数组而言,一定存在峰值(一定有解);
2. 二分不会错过峰值。

我们分别证明一下。

**证明 $1$ :对于任意数组而言,一定存在峰值(一定有解)**

根据题意,我们有「数据长度至少为 $1$」、「越过数组两边看做负无穷」和「相邻元素不相等」的起始条件。

我们可以根据数组长度是否为 $1$ 进行分情况讨论:

1. 数组长度为 $1$,由于边界看做负无穷,此时峰值为该唯一元素的下标;

2. 数组长度大于 $1$,从最左边的元素 $nums[0]$ 开始出发考虑:
    * 如果 $nums[0] > nums[1]$,那么最左边元素 $nums[0]$ 就是峰值(结合左边界为负无穷);
    * 如果 $nums[0] < nums[1]$,由于已经存在明确的 $nums[0]$ 和 $nums[1]$ 大小关系,我们将 $nums[0]$ 看做边界, $nums[1]$ 看做新的最左侧元素,继续往右进行分析:
        * 如果在到达数组最右侧前,出现 $nums[i] > nums[i + 1]$,说明存在峰值位置 $i$(当我们考虑到 $nums[i]$,必然满足 $nums[i]$ 大于前一元素的前提条件,当然前一元素可能是原始左边界);
        * 到达数组最右侧,还没出现 $nums[i] > nums[i + 1]$,说明数组严格递增。此时结合右边界可以看做负无穷,可判定 $nums[n - 1]$ 为峰值。

**综上,我们证明了无论何种情况,数组必然存在峰值。**

**证明 $2$ :二分不会错过峰值**

其实基于「证明 $1$」,我们很容易就可以推理出「证明 $2$」的正确性。

整理一下由「证明 $1$」得出的推理:**如果当前位置大于其左边界或者右边界,那么在当前位置的右边或左边必然存在峰值。**

**换句话说,对于一个满足 $nums[x] > nums[x - 1]$ 的位置,$x$ 的右边一定存在峰值;或对于一个满足 $nums[x] > nums[x + 1]$ 的位置,$x$ 的左边一定存在峰值。**

因此这里的「二段性」其实是指:**在以 $mid$ 为分割点的数组上,根据 $nums[mid]$ 与 $nums[mid \pm 1]$ 的大小关系,可以确定其中一段满足「必然有解」,另外一段不满足「必然有解」(可能有解,可能无解)。**

> 如果不理解为什么「证明 $2$」的正确性可以由「证明 $1$」推导而出的话,可以重点看看「证明 $1$」的第 $2$ 点的证明。

**至此,我们证明了始终选择大于边界一端进行二分,可以确保选择的区间一定存在峰值,并随着二分过程不断逼近峰值位置。**

另外,为了照顾还在纠结使用什么“模板”的同学,特意写了两个版本。但其实只要搞清楚我们「二分」什么内容,根本不会存在说用哪种方式才能写过的情况。

代码:
```Java
class Solution {
    public int findPeakElement(int[] nums) {
        int n = nums.length;
        int l = 0, r = n - 1;
        while (l < r) {
            int mid = l + r >> 1;
            if (nums[mid] > nums[mid + 1]) r = mid;
            else l = mid + 1;
        }
        return r;
    }
}
```

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

---

### 总结

**通过本题,我们可以对「二分」有进一步的认识。**

**最早在 [33. 搜索旋转排序数组](https://mp.weixin.qq.com/s?__biz=MzU4NDE3MTEyMA==&mid=2247485864&idx=1&sn=e5482b2cf55962cd0c5384698d4d0fde&chksm=fd9ca2b7caeb2ba152ef1b900dce805ccfc73cf2a1595fa62eba8a6c5c5212d2d5b3e9f752ba&token=1232059512&lang=zh_CN#rd) 中,我们强调,二分的本质是「二段性」而非「单调性」,而经过本题,我们进一步发现「二段性」还能继续细分,不仅仅只有满足 $01$ 特性(满足/不满足)的「二段性」可以使用二分,满足 $1?$ 特性(一定满足/不一定满足)也可以二分。**

### 最后

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

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

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

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


Question:
给你两个版本号 version1 和 version2 ,请你比较它们。

版本号由一个或多个修订号组成,各修订号由一个 '.' 连接。每个修订号由 多位数字 组成,可能包含 前导零 。每个版本号至少包含一个字符。修订号从左到右编号,下标从 0 开始,最左边的修订号下标为 0 ,下一个修订号下标为 1 ,以此类推。例如,2.5.33 和 0.1 都是有效的版本号。

比较版本号时,请按从左到右的顺序依次比较它们的修订号。比较修订号时,只需比较 忽略任何前导零后的整数值 。也就是说,修订号 1 和修订号 001 相等 。如果版本号没有指定某个下标处的修订号,则该修订号视为 0 。例如,版本 1.0 小于版本 1.1 ,因为它们下标为 0 的修订号相同,而下标为 1 的修订号分别为 0 和 1 ,0 < 1 。

返回规则如下:
* 如果 version1 > version2 返回 1,
* 如果 version1 < version2 返回 -1,
* 除此之外返回 0。

示例 1:
```
输入:version1 = "1.01", version2 = "1.001"

输出:0

解释:忽略前导零,"01" 和 "001" 都表示相同的整数 "1"
```
示例 2:
```
输入:version1 = "1.0", version2 = "1.0.0"

输出:0

解释:version1 没有指定下标为 2 的修订号,即视为 "0"
```
示例 3:
```
输入:version1 = "0.1", version2 = "1.1"

输出:-1

解释:version1 中下标为 0 的修订号是 "0",version2 中下标为 0 的修订号是 "1" 。0 < 1,所以 version1 < version2
```
示例 4:
```
输入:version1 = "1.0.1", version2 = "1"

输出:1
```
示例 5:
```
输入:version1 = "7.5.2.4", version2 = "7.5.3"

输出:-1
```

提示:
* 1 <= version1.length, version2.length <= 500
* version1 和 version2 仅包含数字和 '.'
* version1 和 version2 都是 有效版本号
* version1 和 version2 的所有修订号都可以存储在 32 位整数 中

Solution:
### 模拟

根据题意,对字符串进行分割,诸位比较「修订号」大小即可。

对于缺省的修订号位置,使用 $0$ 进行代指。

代码:
```Java
class Solution {
    public int compareVersion(String v1, String v2) {
        String[] ss1 = v1.split("\\."), ss2 = v2.split("\\.");
        int n = ss1.length, m = ss2.length;
        int i = 0, j = 0;
        while (i < n || j < m) {
            int a = 0, b = 0;
            if (i < n) a = Integer.parseInt(ss1[i++]);
            if (j < m) b = Integer.parseInt(ss2[j++]);
            if (a != b) return a > b ? 1 : -1;
        }
        return 0;
    }
}
```
* 时间复杂度:令 `v1` 长度为 $n$,`v2` 长度为 $m$。整体复杂度为 $O(\max(n, m))$
* 空间复杂度:$O(n + m)$

### 最后

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

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

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

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


Question:
给定两个整数,分别表示分数的分子 `numerator` 和分母 `denominator`,以**字符串形式返回小数**。

如果小数部分为循环小数,则将循环的部分括在括号内。

如果存在多个答案,只需返回 **任意一个**。

对于所有给定的输入,保证答案字符串的长度小于 $10^4$ 。

示例 1:
```
输入:numerator = 1, denominator = 2

输出:"0.5"
```
示例 2:
```
输入:numerator = 2, denominator = 1

输出:"2"
```
示例 3:
```
输入:numerator = 2, denominator = 3

输出:"0.(6)"
```
示例 4:
```
输入:numerator = 4, denominator = 333

输出:"0.(012)"
```
示例 5:
```
输入:numerator = 1, denominator = 5

输出:"0.2"
```

提示:
* $-2^{31}$ <= numerator, denominator <= $2^{31} - 1$
* denominator != 0

Solution:
### 模拟

这是一道模拟 [竖式计算(除法)](https://baike.baidu.com/item/%E7%AB%96%E5%BC%8F%E8%AE%A1%E7%AE%97)的题目。

首先可以明确,两个数相除要么是「有限位小数」,要么是「无限循环小数」,而不可能是「无限不循环小数」。

然后考虑人工计算两数相除是如何进行:

这引导我们可以在模拟竖式计算(除法)过程中,使用「哈希表」记录某个余数最早在什么位置出现过,一旦出现相同余数,则将「出现位置」到「当前结尾」之间的字符串抠出来,即是「循环小数」部分。

> PS. 到这里,从人工模拟除法运算的过程,我们就可以知道「为什么不会出现“无限不循环小数”」,因为始终是对余数进行补零操作,再往下进行运算,而余数个数具有明确的上限(有限集)。所以一直往下计算,最终结果要么是「出现相同余数」,要么是「余数为 $0$,运算结束」。

一些细节:

* 一个显然的条件是,如果本身两数能够整除,直接返回即可;
* 如果两个数有一个为“负数”,则最终答案为“负数”,因此可以起始先判断两数相乘是否小于 $0$,如果是,先往答案头部追加一个负号 `-`;
* 两者范围为 `int`,但计算结果可以会超过 `int` 范围,考虑 $numerator = -2^{31}$ 和 $denominator = -1$ 的情况,其结果为 $2^{31}$,超出 `int` 的范围 $[-2^{31}, 2^{31} - 1]$。因此起始需要先使用 `long` 对两个入参类型转换一下。

代码:
```Java
class Solution {
    public String fractionToDecimal(int numerator, int denominator) {
        // 转 long 计算,防止溢出
        long a = numerator, b = denominator;
        // 如果本身能够整除,直接返回计算结果
        if (a % b == 0) return String.valueOf(a / b);
        StringBuilder sb = new StringBuilder();
        // 如果其一为负数,先追加负号
        if (a * b < 0) sb.append('-');
        a = Math.abs(a); b = Math.abs(b);
        // 计算小数点前的部分,并将余数赋值给 a
        sb.append(String.valueOf(a / b) + ".");
        a %= b;
        Map<Long, Integer> map = new HashMap<>();
        while (a != 0) {
            // 记录当前余数所在答案的位置,并继续模拟除法运算
            map.put(a, sb.length());
            a *= 10;
            sb.append(a / b);
            a %= b;
            // 如果当前余数之前出现过,则将 [出现位置 到 当前位置] 的部分抠出来(循环小数部分)
            if (map.containsKey(a)) {
                int u = map.get(a);
                return String.format("%s(%s)", sb.substring(0, u), sb.substring(u));
            }
        }
        return sb.toString();
    }
}
```
* 时间复杂度:复杂度取决于最终答案的长度,题目规定了最大长度不会超过 $10^4$,整体复杂度为 $O(M)$
* 空间复杂度:复杂度取决于最终答案的长度,题目规定了最大长度不会超过 $10^4$,整体复杂度为 $O(M)$

### 最后

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

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

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

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


Question:
给定一个正整数,返回它在 Excel 表中相对应的列名称。

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

Solution:
### 模拟

这是一道从 $1$ 开始的的 $26$ 进制转换题。

对于一般性的进制转换题目,只需要不断地对 $columnNumber$ 进行 `%` 运算取得最后一位,然后对 $columnNumber$ 进行 `/` 运算,将已经取得的位数去掉,直到 $columnNumber$ 为 $0$ 即可。

一般性的进制转换题目无须进行额外操作,是因为我们是在「每一位数值范围在 $[0,x)$」的前提下进行「逢 $x$ 进一」。

但本题需要我们将从 $1$ 开始,因此在执行「进制转换」操作前,我们需要先对 $columnNumber$ 执行减一操作,从而实现整体偏移。

Java 代码:
```Java
class Solution {
    public String convertToTitle(int columnNumber) {
        StringBuilder sb = new StringBuilder();
        while (columnNumber > 0) {
            columnNumber--;
            sb.append((char)(columnNumber % 26 + 'A'));
            columnNumber /= 26;
        }
        sb.reverse();
        return sb.toString();
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    string convertToTitle(int columnNumber) {
        string sb;
        while (columnNumber > 0) {
            columnNumber--;
            sb.push_back((char)(columnNumber % 26 + 'A'));
            columnNumber /= 26;
        }
        reverse(sb.begin(), sb.end());
        return sb;
    }
};
```
Python 代码:
```Python
class Solution:
    def convertToTitle(self, columnNumber: int) -> str:
        sb = ""
        while columnNumber > 0:
            columnNumber -= 1
            sb += chr((columnNumber % 26) + ord('A'))
            columnNumber //= 26
        return sb[::-1]
```
* 时间复杂度:$O(\log_{26}{cn})$
* 空间复杂度:不算构造答案所消耗的空间,复杂度为 $O(1)$

### 最后

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

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

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

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


LeetCode/1611-1620/1614. 括号的最大嵌套深度（简单）.md

1614. 括号的最大嵌套深度

Question:
如果字符串满足以下条件之一,则可以称之为 有效括号字符串(valid parentheses string,可以简写为 VPS):

* 字符串是一个空字符串 `""`,或者是一个不为 `"("` 或 `")"` 的单字符。
* 字符串可以写为 `AB`(`A` 与 `B` 字符串连接),其中 `A` 和 `B` 都是 有效括号字符串 。
* 字符串可以写为 `(A)`,其中 `A` 是一个 有效括号字符串 。

类似地,可以定义任何有效括号字符串 `S` 的 嵌套深度 `depth(S)`:

* `depth("") = 0`
* `depth(C) = 0`,其中 `C` 是单个字符的字符串,且该字符不是 `"("` 或者 `")"`
* `depth(A + B) = max(depth(A), depth(B))`,其中 `A` 和 `B` 都是 有效括号字符串
* `depth("(" + A + ")") = 1 + depth(A)`,其中 `A` 是一个 有效括号字符串

例如:`""`、`"()()"`、`"()(()())"` 都是 有效括号字符串(嵌套深度分别为 0、1、2),而 `")("` 、`"(()"` 都不是 有效括号字符串 。

给你一个 有效括号字符串 `s`,返回该字符串的 `s` 嵌套深度 。

示例 1:
```
输入:s = "(1+(2*3)+((8)/4))+1"

输出:3

解释:数字 8 在嵌套的 3 层括号中。
```
示例 2:
```
输入:s = "(1)+((2))+(((3)))"

输出:3
```
示例 3:
```
输入:s = "1+(2*3)/(2-1)"
输出:1
```
示例 4:
```
输入:s = "1"

输出:0
```

提示:
* $1 <= s.length <= 100$
* `s` 由数字 `0-9` 和字符 `'+'`、`'-'`、`'*'`、`'/'`、`'('`、`')'` 组成
* 题目数据保证括号表达式 `s` 是 有效的括号表达式

Solution:
### 模拟

根据题意,其实就是求最大的连续左括号的数量(跳过普通字符,且与 `)` 抵消后),只需要边遍历边统计即可。

代码:
```Java
class Solution {
    public int maxDepth(String s) {
        int n = s.length(), ans = 0;
        for (int i = 0, cnt = 0; i < n; i++) {
            if (s.charAt(i) == '(') cnt++;
            else if (s.charAt(i) == ')') cnt--;
            ans = Math.max(ans, cnt);
        }
        return ans;
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


LeetCode/1611-1620/1619. 删除某些元素后的数组均值（简单）.md

1619. 删除某些元素后的数组均值

Question:
给你一个整数数组 `arr`,请你删除最小 `5%` 的数字和最大 `5%` 的数字后,剩余数字的平均值。

与 标准答案 误差在 $10^{-5}$ 的结果都被视为正确结果。

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

输出:2.00000

解释:删除数组中最大和最小的元素后,所有元素都等于 2,所以平均值为 2 。
```
示例 2:
```
输入:arr = [6,2,7,5,1,2,0,3,10,2,5,0,5,5,0,8,7,6,8,0]

输出:4.00000
```
示例 3:
```
输入:arr = [6,0,7,0,7,5,7,8,3,4,0,7,8,1,6,8,1,1,2,4,8,1,9,5,4,3,8,5,10,8,6,6,1,0,6,10,8,2,3,4]

输出:4.77778
```
示例 4:
```
输入:arr = [9,7,8,7,7,8,4,4,6,8,8,7,6,8,8,9,2,6,0,0,1,10,8,6,3,3,5,1,10,9,0,7,10,0,10,4,1,10,6,9,3,6,0,0,2,7,0,6,7,2,9,7,7,3,0,1,6,1,10,3]

输出:5.27778
```
示例 5:
```
输入:arr = [4,8,4,10,0,7,1,3,7,8,8,3,4,1,6,2,1,1,8,0,9,8,0,3,9,10,3,10,1,10,7,3,2,1,4,9,10,7,6,4,0,8,5,1,2,1,6,2,5,0,7,10,9,10,3,7,10,5,8,5,7,6,7,6,10,9,5,10,5,5,7,2,10,7,7,8,2,0,1,1]

输出:5.29167
```

提示:
* $20 <= arr.length <= 1000$
* `arr.length` 是 `20` 的 倍数
* $0 <= arr[i] <= 10^5$

Solution:
### 模拟

根据题意进行模拟即可:先对 `arr` 进行排序,再计算出待统计的左右端点(端点以外的数值为 `5%` 最值),最后计算出相应均值。

Java 代码:
```Java
class Solution {
    public double trimMean(int[] arr) {
        Arrays.sort(arr);
        int n = arr.length, tot = 0;
        for (int i = n / 20; i < n - n / 20; i++) tot += arr[i];
        return tot * 1.0 / (n * 0.9);
    }
}
```
TypeScript 代码:
```TypeScript
function trimMean(arr: number[]): number {
    arr.sort((a,b)=>a-b)
    let n = arr.length, tot = 0
    for (let i = n / 20; i < n - n / 20; i++) tot += arr[i]
    return tot / (n * 0.9)
};
```
* 时间复杂度:$O(n\log{n})$
* 空间复杂度:$O(\log{n})$

### 最后

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

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

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

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


LeetCode/1611-1620/1620. 网络信号最好的坐标（中等）.md

1620. 网络信号最好的坐标

Question:
给你一个数组 `towers` 和一个整数 `radius` 。

数组  `towers`  中包含一些网络信号塔,其中 $towers[i] = [x_{i}, y_{i}, q_{i}]$ 表示第 `i` 个网络信号塔的坐标是 $(x_{i}, y_{i})$ 且信号强度参数为 $q_{i}$ 。所有坐标都是在  `X-Y` 坐标系内的 **整数** 坐标。两个坐标之间的距离用 **欧几里得距离** 计算。

整数 `radius` 表示一个塔 能到达 的 最远距离 。如果一个坐标跟塔的距离在 `radius` 以内,那么该塔的信号可以到达该坐标。在这个范围以外信号会很微弱,所以 `radius` 以外的距离该塔是 不能到达的 。

如果第 `i` 个塔能到达 $(x, y)$ ,那么该塔在此处的信号为 `⌊q / (1 + d)⌋` ,其中 `d` 是塔跟此坐标的距离。一个坐标的 信号强度 是所有 能到达 该坐标的塔的信号强度之和。

请你返回数组 $[c_{x}, c_{y}]$ ,表示 信号强度 最大的 整数 坐标点 $(c_{x}, c_{y})$ 。如果有多个坐标网络信号一样大,请你返回字典序最小的 **非负** 坐标。

注意:

* 坐标 `(x1, y1)` 字典序比另一个坐标 `(x2, y2)` 小,需满足以下条件之一:
	* 要么 `x1 < x2` ,
	* 要么 `x1 == x2` 且 `y1 < y2` 。
* `⌊val⌋` 表示小于等于 `val` 的最大整数(向下取整函数)。

示例 1:

```
输入:towers = [[1,2,5],[2,1,7],[3,1,9]], radius = 2

输出:[2,1]

解释:
坐标 (2, 1) 信号强度之和为 13
- 塔 (2, 1) 强度参数为 7 ,在该点强度为 ⌊7 / (1 + sqrt(0)⌋ = ⌊7⌋ = 7
- 塔 (1, 2) 强度参数为 5 ,在该点强度为 ⌊5 / (1 + sqrt(2)⌋ = ⌊2.07⌋ = 2
- 塔 (3, 1) 强度参数为 9 ,在该点强度为 ⌊9 / (1 + sqrt(1)⌋ = ⌊4.5⌋ = 4
没有别的坐标有更大的信号强度。
```
示例 2:
```
输入:towers = [[23,11,21]], radius = 9

输出:[23,11]

解释:由于仅存在一座信号塔,所以塔的位置信号强度最大。
```
示例 3:
```
输入:towers = [[1,2,13],[2,1,7],[0,1,9]], radius = 2

输出:[1,2]

解释:坐标 (1, 2) 的信号强度最大。
```

提示:
* $1 <= towers.length <= 50$
* $towers[i].length = 3$
* $0 <= x_{i}, y_{i}, q_{i} <= 50$
* $1 <= radius <= 50$

Solution:
### 模拟

观察数据范围:无论是 `towers` 数组大小、坐标 $(x, y)$ 的值域大小,还是最远距离 `k = radius`,取值均不超过 $50$。

因此我们可以直接采用「模拟」的方式进行求解,而不会面临 `TLE` 或 `MLE` 的风险。

具体的,我们建立一个大小为 $110 \times 110$ 的棋盘 `g`,用于记录每个坐标点的信号值,即 $g[i][j] = x$ 代表坐标 $(i, j)$ 的信号值为 $x$。

> 其中 $110$ 的大小是利用了「任意坐标 $(x, y)$ 的取值范围不超过 $50$」,同时「最远距离 $k$ 不超过 $50$」并且「最终答案为非负坐标」而定。

随后,我们可以枚举所有 $towers[i] = (a, b, q)$,并检查以该塔为中心点,大小为 $(k + k)^2$ 的矩阵中的所有点(该塔所能贡献信号的所有坐标均落在矩阵中),枚举过程中使用变量 `val` 记录最大信号值,使用 `x` 和 `y` 记录答案坐标。

Java 代码:
```Java
class Solution {
    public int[] bestCoordinate(int[][] towers, int k) {
        int[][] g = new int[110][110];
        int x = 0, y = 0, val = 0;
        for (int[] t : towers) {
            int a = t[0], b = t[1], q = t[2];
            for (int i = Math.max(0, a - k); i <= a + k; i++) {
                for (int j = Math.max(0, b - k); j <= b + k; j++) {
                    double d = Math.sqrt((a - i) * (a - i) + (b - j) * (b - j));
                    if (d > k) continue;
                    g[i][j] += Math.floor(q / (1 + d));
                    if (g[i][j] > val) {
                        x = i; y = j; val = g[i][j];
                    } else if (g[i][j] == val && (i < x || (i == x && j < y))) {
                        x = i; y = j;
                    }
                }
            }
        }
        return new int[]{x, y};
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    vector<int> bestCoordinate(vector<vector<int>>& towers, int k) {
        const int n = 110;
        vector<vector<int>> g(n, vector<int>(n, 0));
        int x = 0, y = 0, val = 0;
        for (const auto& t : towers) {
            int a = t[0], b = t[1], q = t[2];
            for (int i = max(0, a - k); i <= a + k; ++i) {
                for (int j = max(0, b - k); j <= b + k; ++j) {
                    double d = sqrt((a - i) * (a - i) + (b - j) * (b - j));
                    if (d > k) continue;
                    g[i][j] += floor(q / (1.0 + d));
                    if (g[i][j] > val) {
                        x = i; y = j; val = g[i][j];
                    } else if (g[i][j] == val && (i < x || (i == x && j < y))) {
                        x = i; y = j;
                    }
                }
            }
        }
        return vector<int>{x, y};
    }
};
```
Python 代码:
```Python
class Solution:
    def bestCoordinate(self, towers: List[List[int]], k: int) -> List[int]:
        g = [[0] * 110 for _ in range(110)]
        x, y, val = 0, 0, 0
        for (a, b, q) in towers:
            for i in range(max(0, a - k), a + k + 1):
                for j in range(max(0, b - k), b + k + 1):
                    d = math.sqrt((a - i) * (a - i) + (b - j) * (b - j))
                    if d > k:
                        continue
                    g[i][j] += int(q / (1 + d))
                    if g[i][j] > val:
                        val, x, y = g[i][j], i, j
                    elif g[i][j] == val and ((i < x or (i == x and j < y))):
                        x, y = i, j
        return [x, y]
```
TypeScript 代码:
```TypeScript
function bestCoordinate(towers: number[][], k: number): number[] {
    const g = new Array<Array<number>>(110)
    for (let i = 0; i < 110; i++) g[i] = new Array<number>(110).fill(0)
    let x = 0, y = 0, val = 0
    for (const t of towers) {
        const a = t[0], b = t[1], q = t[2]
        for (let i = Math.max(0, a - k); i <= a + k; i++) {
            for (let j = Math.max(0, b - k); j <= b + k; j++) {
                const d = Math.sqrt((a - i) * (a - i) + (b - j) * (b - j))
                if (d > k) continue
                g[i][j] += Math.floor(q / (1 + d))
                if (g[i][j] > val) {
                    x = i; y = j; val = g[i][j]
                } else if (g[i][j] == val && ((i < x) || (i == x && j < y))) {
                    x = i; y = j
                }
            }
        }
    }
    return [x, y]
}
```
* 时间复杂度:需要 $O(n)$ 的复杂度枚举所有的塔 $towers[i]$;对于每座塔,我们需要枚举以该塔为中心点,大小为 $(k + k)^2$ 的矩阵中的所有坐标。整体复杂度为 $O(n \times k^2)$
* 空间复杂度:$O(M^2)$,其中 $M = 110$ 为棋盘大小

### 最后

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

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

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

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


Question:
请你实现三个 API `append`,`addAll` 和 `multAll` 来实现奇妙序列。

请实现 `Fancy` 类 :

* `Fancy()` 初始化一个空序列对象。
* `void append(val)` 将整数 `val` 添加在序列末尾。
* `void addAll(inc)` 将所有序列中的现有数值都增加 `inc` 。
* `void multAll(m)` 将序列中的所有现有数值都乘以整数 `m` 。
* `int getIndex(idx)` 得到下标为 `idx` 处的数值(下标从 $0$ 开始),并将结果对 $10^9 + 7$ 取余。如果下标大于等于序列的长度,请返回 $-1$ 。

示例:
```
输入:
["Fancy", "append", "addAll", "append", "multAll", "getIndex", "addAll", "append", "multAll", "getIndex", "getIndex", "getIndex"]
[[], [2], [3], [7], [2], [0], [3], [10], [2], [0], [1], [2]]

输出:
[null, null, null, null, null, 10, null, null, null, 26, 34, 20]

解释:
Fancy fancy = new Fancy();
fancy.append(2);   // 奇妙序列:[2]
fancy.addAll(3);   // 奇妙序列:[2+3] -> [5]
fancy.append(7);   // 奇妙序列:[5, 7]
fancy.multAll(2);  // 奇妙序列:[5*2, 7*2] -> [10, 14]
fancy.getIndex(0); // 返回 10
fancy.addAll(3);   // 奇妙序列:[10+3, 14+3] -> [13, 17]
fancy.append(10);  // 奇妙序列:[13, 17, 10]
fancy.multAll(2);  // 奇妙序列:[13*2, 17*2, 10*2] -> [26, 34, 20]
fancy.getIndex(0); // 返回 26
fancy.getIndex(1); // 返回 34
fancy.getIndex(2); // 返回 20
```

提示:
* $1 <= val, inc, m <= 100$
* $0 <= idx <= 10^5$
* 总共最多会有 $10^5$ 次对 `append`,`addAll`,`multAll` 和 `getIndex` 的调用。

Solution:
### 线段树(多个懒标记)

使用多个懒标记来解决 `add` 和 `mul` 问题。

代码:
```Java
class Fancy {
    class Node {
        int ls, rs;
        long val, add, mul = 1;
    }
    int N = (int) 1e8 + 10, M = 1000010, loc = 1, cnt = 0, mod = (int)1e9+7;
    Node[] tr = new Node[M];
    void add(int u, int lc, int rc, int l, int r, int v) {
        int len = rc - lc + 1;
        if (l <= lc && rc <= r) {
            tr[u].val += len * v; tr[u].val %= mod;
            tr[u].add += v; tr[u].add %= mod;
            return ;
        }
        pushdown(u, len);
        int mid = lc + rc >> 1;
        if (l <= mid) add(tr[u].ls, lc, mid, l, r, v);
        if (r > mid) add(tr[u].rs, mid + 1, rc, l, r, v);
        pushup(u);
    }
    void mul(int u, int lc, int rc, int l, int r, int v) {
        if (l <= lc && rc <= r) {
            tr[u].val *= v; tr[u].val %= mod;
            tr[u].mul *= v; tr[u].mul %= mod;
            tr[u].add *= v; tr[u].add %= mod;
            return ;
        }
        pushdown(u, rc - lc + 1);
        int mid = lc + rc >> 1;
        if (l <= mid) mul(tr[u].ls, lc, mid, l, r, v);
        if (r > mid) mul(tr[u].rs, mid + 1, rc, l, r, v);
        pushup(u);
    }
    long query(int u, int lc, int rc, int l, int r) {
        if (l <= lc && rc <= r) return tr[u].val;
        pushdown(u, rc - lc + 1);
        int mid = lc + rc >> 1;
        long ans = 0;
        if (l <= mid) ans = query(tr[u].ls, lc, mid, l, r);
        if (r > mid) ans += query(tr[u].rs, mid + 1, rc, l, r);
        return ans;
    }
    void pushdown(int u, int len) {
        if (tr[u] == null) tr[u] = new Node();
        if (tr[u].ls == 0) {
            tr[u].ls = ++loc;
            tr[tr[u].ls] = new Node();
        }
        if (tr[u].rs == 0) {
            tr[u].rs = ++loc;
            tr[tr[u].rs] = new Node();
        }
        long mul = tr[u].mul, add = tr[u].add;
        tr[tr[u].ls].val = tr[tr[u].ls].val * mul + (len / 2) * add; tr[tr[u].rs].val = tr[tr[u].rs].val * mul + (len / 2) * add;
        tr[tr[u].ls].mul *= mul; tr[tr[u].rs].mul *= mul;
        tr[tr[u].ls].add = tr[tr[u].ls].add * mul + add; tr[tr[u].rs].add = tr[tr[u].rs].add * mul + add;
        tr[tr[u].ls].val %= mod; tr[tr[u].rs].val %= mod;
        tr[tr[u].ls].mul %= mod; tr[tr[u].rs].mul %= mod;
        tr[tr[u].ls].add %= mod; tr[tr[u].rs].add %= mod;
        tr[u].add = 0; tr[u].mul = 1;
    }
    void pushup(int u) {
        tr[u].val = tr[tr[u].ls].val + tr[tr[u].rs].val;
        tr[u].val %= mod;
    }
    public void append(int val) {
        cnt++;
        add(1, 1, N, cnt, cnt, val);
    }
    public void addAll(int inc) {
        if (cnt == 0) return ;
        add(1, 1, N, 1, cnt, inc);
    }
    public void multAll(int m) {
        if (cnt == 0) return ;
        mul(1, 1, N, 1, cnt, m);
    }
    public int getIndex(int idx) {
        return idx + 1 > cnt ? -1 : (int)(query(1, 1, N, idx + 1, idx + 1) % mod);
    }
}
```
* 时间复杂度:查询次数为 $m$,值域大小为 $n$,插入和查询复杂度均为 $O(\log{n})$
* 空间复杂度:$O(m \times \log{n})$

### 最后

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

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

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

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


LeetCode/1621-1630/1624. 两个相同字符之间的最长子字符串（简单）.md

1624. 两个相同字符之间的最长子字符串

Question:
给你一个字符串 `s`,请你返回 两个相同字符之间的最长子字符串的长度 ,计算长度时不含这两个字符。如果不存在这样的子字符串,返回 `-1` 。

子字符串 是字符串中的一个连续字符序列。

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

输出:0

解释:最优的子字符串是两个 'a' 之间的空子字符串。
```
示例 2:
```
输入:s = "abca"

输出:2

解释:最优的子字符串是 "bc" 。
```
示例 3:
```
输入:s = "cbzxy"

输出:-1

解释:s 中不存在出现出现两次的字符,所以返回 -1 。
```
示例 4:
```
输入:s = "cabbac"

输出:4

解释:最优的子字符串是 "abba" ,其他的非最优解包括 "bb" 和 "" 。
```

提示:
* $1 <= s.length <= 300$
* `s` 只含小写英文字母

Solution:
### 模拟

根据题意继续模拟即可:使用数组 `idxs` 记录下每个字符最开始出现的下标即可。

Java 代码:
```Java
class Solution {
    public int maxLengthBetweenEqualCharacters(String s) {
        int[] idxs = new int[26];
        Arrays.fill(idxs, 310);
        int n = s.length(), ans = -1;
        for (int i = 0; i < n; i++) {
            int u = s.charAt(i) - 'a';
            idxs[u] = Math.min(idxs[u], i);
            ans = Math.max(ans, i - idxs[u] - 1);
        }
        return ans;
    }
}
```
Typescript 代码:
```Typescript
function maxLengthBetweenEqualCharacters(s: string): number {
    const idxs = new Array<number>(26).fill(310)
    let n = s.length, ans = -1
    for (let i = 0; i < n; i++) {
        const u = s.charCodeAt(i) - 'a'.charCodeAt(0)
        idxs[u] = Math.min(idxs[u], i)
        ans = Math.max(ans, i - idxs[u] - 1)
    }
    return ans
};
```
Python 代码:
```Python
class Solution:
    def maxLengthBetweenEqualCharacters(self, s: str) -> int:
        idxs = {}
        ans = -1
        for i, c in enumerate(s):
            idxs[c] = idxs[c] if c in idxs else i
            ans = max(ans, i - idxs[c] - 1)
        return ans
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(C)$,其中 $C = 26$ 为字符集大小

### 最后

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

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

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

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


LeetCode/1621-1630/1629. 按键持续时间最长的键（简单）.md

1629. 按键持续时间最长的键

Question:
LeetCode 设计了一款新式键盘,正在测试其可用性。测试人员将会点击一系列键(总计 `n` 个),每次一个。

给你一个长度为 `n` 的字符串 `keysPressed` ,其中 `keysPressed[i]` 表示测试序列中第 i 个被按下的键。`releaseTimes` 是一个升序排列的列表,其中 `releaseTimes[i]` 表示松开第 `i` 个键的时间。字符串和数组的 下标都从 `0` 开始 。第 `0` 个键在时间为 `0` 时被按下,接下来每个键都 恰好 在前一个键松开时被按下。

测试人员想要找出按键 持续时间最长 的键。第 `i` 次按键的持续时间为 `releaseTimes[i] - releaseTimes[i - 1]` ,第 `0` 次按键的持续时间为 `releaseTimes[0]` 。

注意,测试期间,同一个键可以在不同时刻被多次按下,而每次的持续时间都可能不同。

请返回按键 持续时间最长 的键,如果有多个这样的键,则返回 按字母顺序排列最大 的那个键。

示例 1:
```
输入:releaseTimes = [9,29,49,50], keysPressed = "cbcd"

输出:"c"

解释:按键顺序和持续时间如下:
按下 'c' ,持续时间 9(时间 0 按下,时间 9 松开)
按下 'b' ,持续时间 29 - 9 = 20(松开上一个键的时间 9 按下,时间 29 松开)
按下 'c' ,持续时间 49 - 29 = 20(松开上一个键的时间 29 按下,时间 49 松开)
按下 'd' ,持续时间 50 - 49 = 1(松开上一个键的时间 49 按下,时间 50 松开)
按键持续时间最长的键是 'b' 和 'c'(第二次按下时),持续时间都是 20
'c' 按字母顺序排列比 'b' 大,所以答案是 'c'
```
示例 2:
```
输入:releaseTimes = [12,23,36,46,62], keysPressed = "spuda"

输出:"a"

解释:按键顺序和持续时间如下:
按下 's' ,持续时间 12
按下 'p' ,持续时间 23 - 12 = 11
按下 'u' ,持续时间 36 - 23 = 13
按下 'd' ,持续时间 46 - 36 = 10
按下 'a' ,持续时间 62 - 46 = 16
按键持续时间最长的键是 'a' ,持续时间 16
```

提示:
* `releaseTimes.length == n`
* `keysPressed.length == n`
* `2 <= n <= 1000`
* `1 <= releaseTimes[i] <= 10^9`
* `releaseTimes[i] < releaseTimes[i+1]`
* `keysPressed` 仅由小写英文字母组成

Solution:
### 模拟

为了方便,我们用 $rt$ 来代指 $releaseTimes$,用 $kp$ 来代指 $keysPressed$。

根据题意,从先往后处理每个 $kp[i]$,计算每次的持续时间(当前结束时间与上次时间的差值)$rt[i] - rt[i - 1]$,遍历过程中维护最大持续时间和对应的字符下标。

代码:
```Java
class Solution {
    public char slowestKey(int[] rt, String kp) {
        int n = rt.length, idx = 0, max = rt[0];
        for (int i = 1; i < n; i++) {
            int cur = rt[i] - rt[i - 1];
            if (cur > max) {
                idx = i; max = cur;
            } else if (cur == max && kp.charAt(i) > kp.charAt(idx)) {
                idx = i;
            }
        }
        return kp.charAt(idx);
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


LeetCode/1631-1640/1631. 最小体力消耗路径（中等）.md

1631. 最小体力消耗路径

Question:
你准备参加一场远足活动。

给你一个二维 `rows x columns` 的地图 heights ,其中 `heights[row][col]` 表示格子 `(row, col)` 的高度。

一开始你在最左上角的格子 `(0, 0`) ,且你希望去最右下角的格子 `(rows-1, columns-1)` (注意下标从 0 开始编号)。

你每次可以往 上,下,左,右 四个方向之一移动,你想要找到耗费 体力 最小的一条路径。

一条路径耗费的「体力值」是路径上相邻格子之间「高度差绝对值」的「最大值」决定的。

请你返回从左上角走到右下角的最小 体力消耗值 。

示例 1:

```
输入:heights = [[1,2,2],[3,8,2],[5,3,5]]

输出:2

解释:路径 [1,3,5,3,5] 连续格子的差值绝对值最大为 2 。
这条路径比路径 [1,2,2,2,5] 更优,因为另一条路径差值最大值为 3 。
```
示例 2:
```
输入:heights = [[1,2,3],[3,8,4],[5,3,5]]

输出:1

解释:路径 [1,2,3,4,5] 的相邻格子差值绝对值最大为 1 ,比路径 [1,3,5,3,5] 更优。
```
示例 3:
```
输入:heights = [[1,2,1,1,1],[1,2,1,2,1],[1,2,1,2,1],[1,2,1,2,1],[1,1,1,2,1]]

输出:0

解释:上图所示路径不需要消耗任何体力。
```

提示:
* rows == heights.length
* columns == heights[i].length
* 1 <= rows, columns <= 100
* 1 <= heights[i][j] <= $10^6$

Solution:
### 基本分析

对于这道题,可能会有同学想这是不是应该用 DP 呀?

特别是接触过[「路径问题」](https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzU4NDE3MTEyMA==&action=getalbum&album_id=1773144264147812354&scene=173&from_msgid=2247485319&from_itemidx=1&count=3#wechat_redirect)但又还没系统学完的同学。

**事实上,当题目允许往任意方向移动时,考察的往往就不是 DP 了,而是图论。**

**从本质上说,DP 问题是一类特殊的图论问题。**

**那为什么有一些 DP 题目简单修改条件后,就只能彻底转化为图论问题来解决了呢?**

**这是因为修改条件后,导致我们 DP 状态展开不再是一个拓扑序列,也就是我们的图不再是一个拓扑图。**

**换句话说,DP 题虽然都属于图论范畴。**

**但对于不是拓扑图的图论问题,我们无法使用 DP 求解。**

**而此类看似 DP,实则图论的问题,通常是最小生成树或者最短路问题。**

---

### Kruskal

**当一道题我们决定往「图论」方向思考时,我们的重点应该放在「如何建图」上。**

因为解决某个特定的图论问题(最短路/最小生成树/二分图匹配),我们都是使用特定的算法。

由于使用到的算法都有固定模板,因此编码难度很低,而「如何建图」的思维难度则很高。

对于本题,我们可以按照如下分析进行建图:

因为在任意格子可以往「任意方向」移动,所以相邻的格子之间存在一条无向边。

题目要我们求的就是**从起点到终点的最短路径中,边权最大的值**。

我们可以先遍历所有的格子,将所有的边加入集合。

存储的格式为数组 $[a, b, w]$ ,代表编号为 $a$ 的点和编号为 $b$ 的点之间的权重为 $w$。

按照题意,$w$ 为两者的高度差的绝对值。

对集合进行排序,按照 $w$ 进行从小到大排序(Kruskal 部分)。

当我们有了所有排好序的候选边集合之后,我们可以对边进行从前往后处理,每次加入一条边之后,使用并查集来查询「起点」和「终点」是否连通(并查集部分)。

**当第一次判断「起点」和「终点」联通时,说明我们「最短路径」的所有边都已经应用到并查集上了,而且由于我们的边是按照「从小到大」进行排序,因此最后一条添加的边就是「最短路径」上权重最大的边。**

代码:
```Java
class Solution {
    int N = 10009;
    int[] p = new int[N];
    int row, col;
    void union(int a, int b) {
        p[find(a)] = p[find(b)];
    }
    boolean query(int a, int b) {
        return p[find(a)] == p[find(b)];
    }
    int find(int x) {
        if (p[x] != x) p[x] = find(p[x]);
        return p[x];
    }
    public int minimumEffortPath(int[][] heights) {
        row = heights.length;
        col = heights[0].length;

        // 初始化并查集
        for (int i = 0; i < row * col; i++) p[i] = i;

        // 预处理出所有的边
        // edge 存的是 [a, b, w]:代表从 a 到 b 的体力值为 w
        // 虽然我们可以往四个方向移动,但是只要对于每个点都添加「向右」和「向下」两条边的话,其实就已经覆盖了所有边了
        List<int[]> edges = new ArrayList<>();
        for (int i = 0; i < row; i++) {
            for (int j = 0; j < col; j++) {
                int idx = getIndex(i, j);
                if (i + 1 < row) {
                    int a = idx, b = getIndex(i + 1, j);
                    int w = Math.abs(heights[i][j] - heights[i + 1][j]);
                    edges.add(new int[]{a, b, w});
                }
                if (j + 1 < col) {
                    int a = idx, b = getIndex(i, j + 1);
                    int w = Math.abs(heights[i][j] - heights[i][j + 1]);
                    edges.add(new int[]{a, b, w});
                }
            }
        }

        // 根据权值 w 降序
        Collections.sort(edges, (a,b)->a[2]-b[2]);

        // 从「小边」开始添加,当某一条边别应用之后,恰好使用得「起点」和「结点」联通
        // 那么代表找到了「最短路径」中的「权重最大的边」
        int start = getIndex(0, 0), end = getIndex(row - 1, col - 1);
        for (int[] edge : edges) {
            int a = edge[0], b = edge[1], w = edge[2];
            union(a, b);
            if (query(start, end)) {
                return w;
            }
        }
        return 0;
    }
    int getIndex(int x, int y) {
        return x * col  + y;
    }
}
```

令行数为 $r$,列数为 $c$,那么节点的数量为 $r * c$,无向边的数量严格为 $r * (c - 1) + c * (r - 1)$,数量级上为 $r * c$。

* 时间复杂度:获取所有的边复杂度为 $O(r * c)$,排序复杂度为 $O((r * c)\log{(r * c)})$,遍历得到最终解复杂度为 $O(r * c)$。整体复杂度为 $O((r * c)\log{(r * c)})$。
* 空间复杂度:使用了并查集数组。复杂度为 $O(r * c)$。

---

### 证明

**我们之所以能够这么做,是因为「跳出循环前所遍历的最后一条边必然是最优路径上的边,而且是 $w$ 最大的边」。**

我们可以用「反证法」来证明这个结论为什么是正确的。

我们先假设「跳出循环前所遍历的最后一条边必然是最优路径上的边,而且是 $w$ 最大的边」不成立:

我们令循环终止前的最后一条边为 `a`

1. *假设 a 不在最优路径内*:如果 $a$ 并不在最优路径内,即最优路径是由 $a$ 边之前的边构成,那么 $a$ 边不会对左上角和右下角节点的连通性产生影响。也就是在遍历到该边之前,左上角和右下角应该是联通的,逻辑上循环会在遍历到该边前终止。与我们循环的决策逻辑冲突。

2. *$a$ 在最优路径内,但不是 $w$ 最大的边*:我们在遍历之前就已经排好序。与排序逻辑冲突。

因此,我们的结论是正确的。`a` 边必然属于「最短路径」并且是权重最大的边。

### 最后

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

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

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

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


LeetCode/1631-1640/1636. 按照频率将数组升序排序（简单）.md

1636. 按照频率将数组升序排序

Question:
给你一个整数数组 `nums`,请你将数组按照每个值的频率 升序 排序。如果有多个值的频率相同,请你按照数值本身将它们 降序 排序。

请你返回排序后的数组。

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

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

解释:'3' 频率为 1,'1' 频率为 2,'2' 频率为 3 。
```
示例 2:
```
输入:nums = [2,3,1,3,2]

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

解释:'2' 和 '3' 频率都为 2 ,所以它们之间按照数值本身降序排序。
```
示例 3:
```
输入:nums = [-1,1,-6,4,5,-6,1,4,1]

输出:[5,-1,4,4,-6,-6,1,1,1]
```

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

Solution:
### 哈希表 + 排序 + 模拟

根据题意,先使用哈希表进行词频统计,再以二元组 $(x, cnt)$ 的形式转存到数组 `list` 中(其中 $x$ 为对应的 $nums$ 中的数值,$cnt$ 为数值 $x$ 在 `nums` 中的出现次数),再根据题目给定对 `list` 进行排序,最后再构造出答案。

Java 代码:
```Java
class Solution {
    public int[] frequencySort(int[] nums) {
        int n = nums.length;
        Map<Integer, Integer> map = new HashMap<>();
        for (int i = 0; i < n; i++) map.put(nums[i], map.getOrDefault(nums[i], 0) + 1);
        List<int[]> list = new ArrayList<>();
        for (int key : map.keySet()) list.add(new int[]{key, map.get(key)});
        Collections.sort(list, (a, b)->{
            return a[1] != b[1] ? a[1] - b[1] : b[0] - a[0];
        });
        int[] ans = new int[n];
        int idx = 0;
        for (int[] info : list) {
            int a = info[0], b = info[1];
            while (b-- > 0) ans[idx++] = a;
        }
        return ans;
    }
}
```
TypeScript 代码:
```TypeScript
function frequencySort(nums: number[]): number[] {
    const map = new Map<number, number>()
    for (const x of nums) {
        if (!map.has(x)) map.set(x, 0)
        map.set(x, map.get(x) + 1)
    }
    nums.sort((a,b)=>{
        return map.get(a) != map.get(b) ? map.get(a) - map.get(b) : b - a
    })
    return nums
};
```
* 时间复杂度:使用哈希表进行统计复杂度为 $O(n)$;根据规则进行排序复杂度为 $O(n\log{n})$;构造答案复杂度为 $O(n)$。整体复杂度为 $O(n\log{n})$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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