LeetCode/851-860/857. 雇佣 K 名工人的最低成本（困难）.md

857. 雇佣 K 名工人的最低成本

Question:
有 `n` 名工人。 给定两个数组 `quality` 和 `wage` ,其中,`quality[i]` 表示第 `i` 名工人的工作质量,其最低期望工资为 `wage[i]` 。

现在我们想雇佣 `k` 名工人组成一个工资组。在雇佣 一组 `k` 名工人时,我们必须按照下述规则向他们支付工资:

* 对工资组中的每名工人,应当按其工作质量与同组其他工人的工作质量的比例来支付工资。
* 工资组中的每名工人至少应当得到他们的最低期望工资。

给定整数 `k`,返回 组成满足上述条件的付费群体所需的最小金额 。在实际答案的 $10^{-5}$ 以内的答案将被接受。。

示例 1:
```
输入: quality = [10,20,5], wage = [70,50,30], k = 2

输出: 105.00000

解释: 我们向 0 号工人支付 70,向 2 号工人支付 35。
```
示例 2:
```
输入: quality = [3,1,10,10,1], wage = [4,8,2,2,7], k = 3

输出: 30.66667

解释: 我们向 0 号工人支付 4,向 2 号和 3 号分别支付 13.33333。
```

提示:
* $n = quality.length = wage.length$
* $1 <= k <= n <= 10^4$
* $1 <= quality[i], wage[i] <= 10^4$

Solution:
### 枚举 + 优先队列(堆)

为了方便,我们令 `quality` 为 `qs`,`wage` 为 `ws`。

从 $n$ 个工人中选 $k$ 个,使这 $k$ 个工人实际工资与其 $qs[i]$ 成比例,且实际工资不低于 $ws[i]$。

根据条件一,假设实际工资与能力值比值为 $t$,则所选工人的实际工资为 $qs[i] \times t$,再结合条件二可知 $qs[i] \times t >= ws[i]$,变形可得 $t >= \frac{ws[i]}{qs[i]}$。

那么在选定若干工人的情况下,为使得总支出最小,我们可以取 $t$ 为所有被选工人中的最大 $\frac{ws[i]}{qs[i]}$ 即可。

**因此最终的 $t$ 值必然是取自某个工人的实际比值 $\frac{ws[i]}{qs[i]}$,这引导我们可以通过「枚举」哪个工人的实际比值为实际 $t$ 值来做。**

假设我们已经预处理出所有工人的 $\frac{ws[i]}{qs[i]}$ 比值信息,并「从小到大」进行枚举(该过程可看做是以比值信息作为维度来枚举每个工人):假设当前处理到的比值为最终使用到的 $t$,我们能够选的工人必然是在该工人的左边(根据上述分析可知,被选的工人满足 $\frac{ws[i]}{qs[i]} <= t$ 条件)。

因此,我们可以使用二维数组 `ds` 记录下每个工人的 $\frac{ws[i]}{qs[i]}$ 比值信息:$ds[i][0] = \frac{ws[i]}{qs[i]}$,$ds[i][1] = i$。并对其进行排升序,枚举每个工人的实际比值,同时动态维护枚举过程中的最小 $k$ 个 $qs[i]$(使用「大根堆」处理),并使用单变量 `tot` 记录当前堆中的 $qs[i]$ 总和,$tot \times \frac{ws[i]}{qs[i]}$ 即是以当前比值作为实际 $t$ 值时的总成本,在所有总成本中取最小值即是答案。

Java 代码:
```Java
class Solution {
    public double mincostToHireWorkers(int[] qs, int[] ws, int k) {
        int n = qs.length;
        double[][] ds = new double[n][2];
        for (int i = 0; i < n; i++) {
            ds[i][0] = ws[i] * 1.0 / qs[i]; ds[i][1] = i * 1.0;
        }
        Arrays.sort(ds, (a,b)->Double.compare(a[0], b[0]));
        PriorityQueue<Integer> q = new PriorityQueue<>((a,b)->b-a);
        double ans = 1e18;
        for (int i = 0, tot = 0; i < n; i++) {
            int cur = qs[(int)ds[i][1]];
            tot += cur; q.add(cur);
            if (q.size() > k) tot -= q.poll();
            if (q.size() == k) ans = Math.min(ans, tot * ds[i][0]);
        }
        return ans;
    }
}
```
TypeScript 代码:
```TypeScript
function mincostToHireWorkers(qs: number[], ws: number[], k: number): number {
    const n = qs.length
    const ds: number[][] = new Array<Array<number>>()
    for (let i = 0; i < n; i++) ds.push([ws[i] / qs[i], i])
    ds.sort((a,b)=>a[0]-b[0])
    const q = new MaxPriorityQueue()
    let ans = 1e18
    for (let i = 0, tot = 0; i < n; i++) {
        const cur = qs[ds[i][1]]
        tot += cur
        q.enqueue(cur)
        if (q.size() > k) tot -= q.dequeue().element
        if (q.size() == k) ans = Math.min(ans, tot * ds[i][0])
    }
    return ans
};
```
* 时间复杂度:构造系数并排序复杂度为 $O(n\log{n})$;枚举并计算答案复杂度为 $O(n\log{k})$。整体复杂度为 $O(n\log{n})$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


Question:
给你两个字符串 `s` 和 `goal` ,只要我们可以通过交换 `s` 中的两个字母得到与 `goal` 相等的结果,就返回 `true` ;否则返回 `false` 。

交换字母的定义是:取两个下标 `i` 和 `j` (下标从 $0$ 开始)且满足 `i != j` ,接着交换 `s[i]` 和 `s[j]` 处的字符。

* 例如,在 `"abcd"` 中交换下标 `0` 和下标 `2` 的元素可以生成 `"cbad"` 。

示例 1:
```
输入:s = "ab", goal = "ba"

输出:true

解释:你可以交换 s[0] = 'a' 和 s[1] = 'b' 生成 "ba",此时 s 和 goal 相等。
```
示例 2:
```
输入:s = "ab", goal = "ab"

输出:false

解释:你只能交换 s[0] = 'a' 和 s[1] = 'b' 生成 "ba",此时 s 和 goal 不相等。
```
示例 3:
```
输入:s = "aa", goal = "aa"

输出:true

解释:你可以交换 s[0] = 'a' 和 s[1] = 'a' 生成 "aa",此时 s 和 goal 相等。
```
示例 4:
```
输入:s = "aaaaaaabc", goal = "aaaaaaacb"

输出:true
```

提示:
* $1 <= s.length, goal.length <= 2 * 10^4$
* `s` 和 `goal` 由小写英文字母组成

Solution:
### 模拟

根据题意进行模拟即可,搞清楚什么情况下两者为「亲密字符」:

1. 当 $s$ 与 $goal$ 长度 或 词频不同,必然不为亲密字符;
2. 当「$s$ 与 $goal$ 不同的字符数量为 $2$(能够相互交换)」或「$s$ 与 $goal$ 不同的字符数量为 $0$,但同时 $s$ 中有出现数量超过 $2$ 的字符(能够相互交换)」时,两者必然为亲密字符。

代码:
```Java
class Solution {
    public boolean buddyStrings(String s, String goal) {
        int n = s.length(), m = goal.length();
        if (n != m) return false;
        int[] cnt1 = new int[26], cnt2 = new int[26];
        int sum = 0;
        for (int i = 0; i < n; i++) {
            int a = s.charAt(i) - 'a', b = goal.charAt(i) - 'a';
            cnt1[a]++; cnt2[b]++;
            if (a != b) sum++;
        }
        boolean ok = false;
        for (int i = 0; i < 26; i++) {
            if (cnt1[i] != cnt2[i]) return false;
            if (cnt1[i] > 1) ok = true;
        }
        return sum == 2 || (sum == 0 && ok);
    }
}
```
* 时间复杂度:令 $n$ 为两字符串之间的最大长度,$C$ 为字符集大小,$C$ 固定为 $26$,复杂度为 $O(n + C)$
* 空间复杂度:$O(C)$

### 最后

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

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

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

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


LeetCode/861-870/862. 和至少为 K 的最短子数组（困难）.md

862. 和至少为 K 的最短子数组

Question:
给你一个整数数组 `nums` 和一个整数 `k` ,找出 `nums` 中和至少为 `k` 的 最短非空子数组 ,并返回该子数组的长度。如果不存在这样的子数组,返回 `-1` 。

子数组是数组中连续 的一部分。

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

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

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

输出:3
```

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

Solution:
### 前缀和 + 离散化 + 权值树状数组

由于求解的对象是子数组,容易联想到求连续段之和,容易联想到「前缀和」,假设我们预处理出的前缀和数组为 $sum$(为了方便,我们令前缀和数组坐标从 $1$ 开始)。

即每个 $nums[i]$ 而言,本质上是找满足「$sum[i] - sum[j] \geqslant k$」条件的最大下标 $j$,其中 $j$ 的取值范围为 $[0, i - 1]$,从而知道以 $i$ 作为右端点时,满足条件的最短子数组长度为 $i - j$。

先考虑存在负数域的问题,由于我们需要使用 $sum[X]$,以及对应的 $sum[X] + k$,同时 $k$ 的取值为 $1e9$(过大),我们可以通过「离散化」手段将其映射到 $2$ 倍的数组长度,即大小为 $2 \times 10^5$ 的正数域。

随后来考虑如何求解「满足条件的最大下标」问题,可以通过「权值树状数组」来做:对于每个 $sum[i]$ 而言,我们利用「权值树状数组」来维护满足大于等于 $sum[i] + k$ 的最大下标。起始我们先初始化树状数组为 $-1$,遍历过程中,查询是否存在满足条件的下标(若不为 `-1` 则更新 `ans`),并更新权值树状数组对应的最大下标即可。

Java 代码:
```Java
class Solution {
    static int N = 200010;
    static int[] tr = new int[N], sum = new int[N];
    int n, m, ans;
    int lowbit(int x) {
        return x & -x;
    }
    void update(int val, int loc) {
        for (int i = val; i < m; i += lowbit(i)) tr[i] = Math.max(tr[i], loc);
    }
    int query(int x) {
        int ans = -1;
        for (int i = x; i > 0; i -= lowbit(i)) ans = Math.max(ans, tr[i]);
        return ans;
    }
    int getIdx(List<Long> list, long x) {
        int l = 0, r = list.size() - 1;
        while (l < r) {
            int mid = l + r >> 1;
            if (list.get(mid) >= x) r = mid;
            else l = mid + 1;
        }
        return r + 1;
    }
    public int shortestSubarray(int[] nums, int k) {
        n = nums.length; m = 2 * n + 10; ans = n + 10;
        Arrays.fill(tr, -1);
        long[] temp = new long[m];
        List<Long> list = new ArrayList<>();
        list.add(0L);
        for (int i = 1; i <= 2 * n + 1; i++) {
            if (i <= n) temp[i] = temp[i - 1] + nums[i - 1];
            else temp[i] = temp[i - (n + 1)] + k;
            list.add(temp[i]);
        }
        Collections.sort(list);
        for (int i = 0; i <= 2 * n + 1; i++) sum[i] = getIdx(list, temp[i]);
        update(sum[n + 1], 0);
        for (int i = 1; i <= n; i++) {
            int j = query(sum[i]);
            if (j != -1) ans = Math.min(ans, i - j);
            update(sum[n + 1 + i], i);
        }
        return ans == n + 10 ? -1 : ans;
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    static const int N = 200010;
    vector<int> tr, sum;
    int n, m, ans;
    int lowbit(int x) {
        return x & -x;
    }
    void update(int val, int loc) {
        for (int i = val; i < m; i += lowbit(i)) tr[i] = max(tr[i], loc);
    }
    int query(int x) {
        int ans = -1;
        for (int i = x; i > 0; i -= lowbit(i)) ans = max(ans, tr[i]);
        return ans;
    }
    int shortestSubarray(vector<int>& nums, int k) {
        n = nums.size(); m = 2 * n + 10; ans = n + 10;
        tr.resize(m, -1); sum.resize(m + 10, 0);
        vector<long long> temp(m);
        vector<long long> list;
        for (int i = 1; i <= 2 * n + 1; i++) {
            if (i <= n) temp[i] = temp[i - 1] + nums[i - 1];
            else temp[i] = temp[i - (n + 1)] + k;
            list.push_back(temp[i]);
        }
        sort(list.begin(), list.end());
        for (int i = 0; i <= 2 * n + 1; i++) {
            sum[i] = lower_bound(list.begin(), list.end(), temp[i]) - list.begin() + 1;
        }
        update(sum[n + 1], 0);
        for (int i = 1; i <= n; i++) {
            int j = query(sum[i]);
            if (j != -1) ans = min(ans, i - j);
            update(sum[n + 1 + i], i);
        }
        return ans == n + 10 ? -1 : ans;
    }
};
```
* 时间复杂度:预处理前缀和的的复杂度为 $O(n)$,排序并进行离散化的复杂度为 $O(n\log{n})$;构造答案的复杂度为 $O(n\log{n})$。整体复杂度为 $O(n\log{n})$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


LeetCode/861-870/863. 二叉树中所有距离为 K 的结点（中等）.md

863. 二叉树中所有距离为 K 的结点

Question:
给定一个二叉树(具有根结点 root), 一个目标结点 target ,和一个整数值 K 。

返回到目标结点 target 距离为 K 的所有结点的值的列表。 答案可以以任何顺序返回。

示例 1:
```
输入:root = [3,5,1,6,2,0,8,null,null,7,4], target = 5, K = 2

输出:[7,4,1]

解释:
所求结点为与目标结点(值为 5)距离为 2 的结点,
值分别为 7,4,以及 1
```

注意,输入的 "root" 和 "target" 实际上是树上的结点。
上面的输入仅仅是对这些对象进行了序列化描述。

提示:
* 给定的树是非空的。
* 树上的每个结点都具有唯一的值 0 <= node.val <= 500 。
* 目标结点 target 是树上的结点。
* 0 <= K <= 1000.

Solution:
### 基本分析

显然,如果题目是以图的形式给出的话,我们可以很容易通过「`BFS` / 迭代加深」找到距离为 $k$ 的节点集。

而树是一类特殊的图,我们可以通过将二叉树转换为图的形式,再进行「`BFS` / 迭代加深」。

由于二叉树每个点最多有 $2$ 个子节点,点和边的数量接近,属于稀疏图,因此我们可以使用「邻接表」的形式进行存储。

建图方式为:对于二叉树中相互连通的节点(`root` 与 `root.left`、`root` 和 `root.right`),建立一条无向边。

建图需要遍历整棵树,使用 `DFS` 或者 `BFS` 均可。

由于所有边的权重均为 $1$,我们可以使用 「`BFS` / 迭代加深」 找到从目标节点 `target` 出发,与目标节点距离为 $k$ 的节点,然后将其添加到答案中。

>一些细节:利用每个节点具有唯一的值,我们可以直接使用节点值进行建图和搜索。

---

### 建图 + `BFS`

由「基本分析」,可写出「建图 + `BFS`」的实现。

代码:
```Java
class Solution {
    int N = 1010, M = N * 2;
    int[] he = new int[N], e = new int[M], ne = new int[M];
    int idx;
    void add(int a, int b) {
        e[idx] = b;
        ne[idx] = he[a];
        he[a] = idx++;
    }
    boolean[] vis = new boolean[N];
    public List<Integer> distanceK(TreeNode root, TreeNode t, int k) {
        List<Integer> ans = new ArrayList<>();
        Arrays.fill(he, -1);
        dfs(root);
        Deque<Integer> d = new ArrayDeque<>();
        d.addLast(t.val);
        vis[t.val] = true;
        while (!d.isEmpty() && k >= 0) {
            int size = d.size();
            while (size-- > 0) {
                int poll = d.pollFirst();
                if (k == 0) {
                    ans.add(poll);
                    continue;
                }
                for (int i = he[poll]; i != -1 ; i = ne[i]) {
                    int j = e[i];
                    if (!vis[j]) {
                        d.addLast(j);
                        vis[j] = true;
                    }
                }
            }
            k--;
        }
        return ans;
    }
    void dfs(TreeNode root) {
        if (root == null) return;
        if (root.left != null) {
            add(root.val, root.left.val);
            add(root.left.val, root.val);
            dfs(root.left);
        }
        if (root.right != null) {
            add(root.val, root.right.val);
            add(root.right.val, root.val);
            dfs(root.right);
        }
    }
}
```
* 时间复杂度:通过 `DFS` 进行建图的复杂度为 $O(n)$;通过 `BFS` 找到距离 $target$ 为 $k$ 的节点,复杂度为 $O(n)$。整体复杂度为 $O(n)$
* 空间复杂度:$O(n)$

---

### 建图 + 迭代加深

由「基本分析」,可写出「建图 + 迭代加深」的实现。

迭代加深的形式,我们只需要结合题意,搜索深度为 $k$ 的这一层即可。

代码:
```Java
class Solution {
    int N = 1010, M = N * 2;
    int[] he = new int[N], e = new int[M], ne = new int[M];
    int idx;
    void add(int a, int b) {
        e[idx] = b;
        ne[idx] = he[a];
        he[a] = idx++;
    }
    boolean[] vis = new boolean[N];
    public List<Integer> distanceK(TreeNode root, TreeNode t, int k) {
        List<Integer> ans = new ArrayList<>();
        Arrays.fill(he, -1);
        dfs(root);
        vis[t.val] = true;
        find(t.val, k, 0, ans);
        return ans;
    }
    void find(int root, int max, int cur, List<Integer> ans) {
        if (cur == max) {
            ans.add(root);
            return ;
        }
        for (int i = he[root]; i != -1; i = ne[i]) {
            int j = e[i];
            if (!vis[j]) {
                vis[j] = true;
                find(j, max, cur + 1, ans);
            }
        }
    }
    void dfs(TreeNode root) {
        if (root == null) return;
        if (root.left != null) {
            add(root.val, root.left.val);
            add(root.left.val, root.val);
            dfs(root.left);
        }
        if (root.right != null) {
            add(root.val, root.right.val);
            add(root.right.val, root.val);
            dfs(root.right);
        }
    }
}
```
* 时间复杂度:通过 `DFS` 进行建图的复杂度为 $O(n)$;通过迭代加深找到距离 $target$ 为 $k$ 的节点,复杂度为 $O(n)$。整体复杂度为 $O(n)$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


LeetCode/861-870/864. 获取所有钥匙的最短路径（困难）.md

864. 获取所有钥匙的最短路径

Question:
给定一个二维网格 `g`,其中:

* `'.'` 代表一个空房间
* `'#'` 代表一堵墙
* `'@'` 是起点
* 小写字母代表钥匙
* 大写字母代表锁

我们从起点开始出发,一次移动是指向四个基本方向之一行走一个单位空间。

我们不能在网格外面行走,也无法穿过一堵墙。

如果途经一个钥匙,我们就把它捡起来,除非我们手里有对应的钥匙,否则无法通过锁。

假设 `k` 为 钥匙/锁 的个数,且满足 $1 <= k <= 6$,字母表中的前 `k` 个字母在网格中都有自己对应的一个小写和一个大写字母。

换言之,每个锁有唯一对应的钥匙,每个钥匙也有唯一对应的锁。另外,代表钥匙和锁的字母互为大小写并按字母顺序排列。

返回获取所有钥匙所需要的移动的最少次数。

如果无法获取所有钥匙,返回 `-1` 。

示例 1:

```
输入:g = ["@.a.#","###.#","b.A.B"]

输出:8

解释:目标是获得所有钥匙,而不是打开所有锁。
```
示例 2:

```
输入:g = ["@..aA","..B#.","....b"]

输出:6
```
示例 3:

```
输入: g = ["@Aa"]

输出: -1
```

提示:
* $m = g.length$
* $n = g[i].length$
* $1 <= m, n <= 30$
* `g[i][j]` 只含有 `'.'`,`'#'`, `'@'`, `'a'-'f'` 以及 `'A'-'F'`
* 钥匙的数目范围是 $[1, 6]$
* 每个钥匙都对应一个不同的字母
* 每个钥匙正好打开一个对应的锁

Solution:
### BFS + 状态压缩

**一道常规的 `BFS` 运用题,只不过需要在 `BFS` 过程中记录收集到的钥匙状态。**

利用「钥匙数量不超过 $6$,并按字母顺序排列」,我们可以使用一个 `int` 类型二进制数 `state` 来代指当前收集到钥匙情况:

* 若 `state` 的二进制中的第 $k$ 位为 `1`,代表当前种类编号为 $k$ 的钥匙 **已被收集**,后续移动若遇到对应的锁则 **能通过**
* 若 `state` 的二进制中的第 $k$ 位为 `0`,代表当前种类编号为 $k$ 的钥匙 **未被收集**,后续移动若遇到对应的锁则 **无法通过**

其中「钥匙种类编号」则按照小写字母先后顺序,从 $0$ 开始进行划分对应:即字符为 `a` 的钥匙编号为 `0`,字符为 `b` 的钥匙编号为 `1`,字符为 `c` 的钥匙编号为 `2` ...

当使用了这样的「状态压缩」技巧后,我们可以很方便通过「位运算」进行 **钥匙检测** 和 **更新钥匙收集状态**:

* 钥匙检测:`(state >> k) & 1`,若返回 `1` 说明第 $k$ 位为 `1`,当前持有种类编号为 `k` 的钥匙
* 更新钥匙收集状态:`state |= 1 << k`,将 `state` 的第 $k$ 位设置为 `1`,代表当前新收集到种类编号为 `k` 的钥匙

搞明白如何记录当前收集到的钥匙状态后,剩下的则是常规 `BFS` 过程:

1. 起始遍历一次棋盘,找到起点位置,并将其进行入队,队列维护的是 $(x, y, state)$ 三元组状态(其中 $(x, y)$ 代表当前所在的棋盘位置,$state$ 代表当前的钥匙收集情况)
    同时统计整个棋盘所包含的钥匙数量 `cnt`,并使用 数组/哈希表 记录到达每个状态所需要消耗的最小步数 `step`

2. 进行四联通方向的 `BFS`,转移过程中需要注意「遇到锁时,必须有对应钥匙才能通过」&「遇到钥匙时,需要更新对应的 `state` 再进行入队」

3. 当 `BFS` 过程中遇到 `state = (1 << cnt) - 1` 时,代表所有钥匙均被收集完成,可结束搜索

Java 代码:
```Java
class Solution {
    static int N = 35, K = 10, INF = 0x3f3f3f3f;
    static int[][][] dist = new int[N][N][1 << K];
    static int[][] dirs = new int[][]{{1,0},{-1,0},{0,1},{0,-1}};
    public int shortestPathAllKeys(String[] g) {
        int n = g.length, m = g[0].length(), cnt = 0;
        Deque<int[]> d = new ArrayDeque<>();
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < m; j++) {
                Arrays.fill(dist[i][j], INF);
                char c = g[i].charAt(j);
                if (c == '@') {
                    d.addLast(new int[]{i, j, 0});
                    dist[i][j][0] = 0;
                } else if (c >= 'a' && c <= 'z') cnt++;
            }
        }
        while (!d.isEmpty()) {
            int[] info = d.pollFirst();
            int x = info[0], y = info[1], cur = info[2], step = dist[x][y][cur];
            for (int[] di : dirs) {
                int nx = x + di[0], ny = y + di[1];
                if (nx < 0 || nx >= n || ny < 0 || ny >= m) continue;
                char c = g[nx].charAt(ny);
                if (c == '#') continue;
                if ((c >= 'A' && c <= 'Z') && (cur >> (c - 'A') & 1) == 0) continue;
                int ncur = cur;
                if (c >= 'a' && c <= 'z') ncur |= 1 << (c - 'a');
                if (ncur == (1 << cnt) - 1) return step + 1;
                if (step + 1 >= dist[nx][ny][ncur]) continue;
                dist[nx][ny][ncur] = step + 1;
                d.addLast(new int[]{nx, ny, ncur});
            }
        }
        return -1;
    }
}
```
C++ 代码:
```C++
class Solution {
    int N = 35, K = 10, INF = 0x3f3f3f3f;
    vector<vector<vector<int>>> dist = vector<vector<vector<int>>>(N, vector<vector<int>>(N, vector<int>(1<<K, INF)));
    vector<vector<int>> dirs = {{1,0}, {-1,0}, {0,1}, {0,-1}};
public:
    int shortestPathAllKeys(vector<string>& g) {
        int n = g.size(), m = g[0].size(), cnt = 0;
        queue<vector<int>> d;
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < m; j++) {
                fill(dist[i][j].begin(), dist[i][j].end(), INF);
                char c = g[i][j];
                if (c == '@') {
                    d.push({i, j, 0});
                    dist[i][j][0] = 0;
                } else if (c >= 'a' && c <= 'z') cnt++;
            }
        }
        while (!d.empty()) {
            vector<int> info = d.front();
            d.pop();
            int x = info[0], y = info[1], cur = info[2], step = dist[x][y][cur];
            for (vector<int> di : dirs) {
                int nx = x + di[0], ny = y + di[1];
                if (nx < 0 || nx >= n || ny < 0 || ny >= m) continue;
                char c = g[nx][ny];
                if (c == '#') continue;
                if ((c >= 'A' && c <= 'Z') && (cur >> (c - 'A') & 1) == 0) continue;
                int ncur = cur;
                if (c >= 'a' && c <= 'z') ncur |= 1 << (c - 'a');
                if (ncur == (1 << cnt) - 1) return step + 1;
                if (step + 1 >= dist[nx][ny][ncur]) continue;
                dist[nx][ny][ncur] = step + 1;
                d.push({nx, ny, ncur});
            }
        }
        return -1;
    }
};
```
Python3 代码:
```Python
class Solution:
    def shortestPathAllKeys(self, g: List[str]) -> int:
        dirs = [[0,1], [0,-1], [1,0], [-1,0]]
        n, m, cnt = len(g), len(g[0]), 0
        dist = defaultdict(lambda : 0x3f3f3f3f)
        for i in range(n):
            for j in range(m):
                c = g[i][j]
                if c == '@':
                    d = deque([(i, j, 0)])
                    dist[(i, j, 0)] = 0
                elif 'a' <= c <= 'z':
                    cnt += 1
        while d:
            x, y, cur = d.popleft()
            step = dist[(x, y, cur)]
            for di in dirs:
                nx, ny = x + di[0], y + di[1]
                if nx < 0 or nx >= n or ny < 0 or ny >= m:
                    continue
                c = g[nx][ny]
                if c == '#':
                    continue
                if 'A' <= c <= 'Z' and (cur >> (ord(c) - ord('A')) & 1) == 0:
                    continue
                ncur = cur
                if 'a' <= c <= 'z':
                    ncur |= (1 << (ord(c) - ord('a')))
                if ncur == (1 << cnt) - 1:
                    return step + 1
                if step + 1 >= dist[(nx, ny, ncur)]:
                    continue
                dist[(nx, ny, ncur)] = step + 1
                d.append((nx, ny, ncur))
        return -1
```
TypeScript 代码:
```TypeScript
function shortestPathAllKeys(g: string[]): number {
    const dirs = [[1,0],[-1,0],[0,1],[0,-1]]
    let n = g.length, m = g[0].length, cnt = 0
    const dist = new Array<Array<Array<number>>>()
    for (let i = 0; i < n; i++) {
        dist[i] = new Array<Array<number>>(m)
        for (let j = 0; j < m; j++) {
            dist[i][j] = new Array<number>(1 << 10).fill(0x3f3f3f3f)
        }
    }
    const d = []
    for (let i = 0; i < n; i++) {
        for (let j = 0; j < m; j++) {
            if (g[i][j] == '@') {
                d.push([i, j, 0]); dist[i][j][0] = 0
            } else if (g[i][j] >= 'a' && g[i][j] <= 'z') cnt++
        }
    }
    while (d.length > 0) {
        const info = d.shift()
        const x = info[0], y = info[1], cur = info[2], step = dist[x][y][cur]
        for (const di of dirs) {
            const nx = x + di[0], ny = y + di[1]
            if (nx < 0 || nx >= n || ny < 0 || ny >= m) continue
            const c = g[nx][ny]
            if (c == '#') continue
            if ('A' <= c && c <= 'Z' && ((cur >> (c.charCodeAt(0) - 'A'.charCodeAt(0)) & 1) == 0)) continue
            let ncur = cur
            if ('a' <= c && c <= 'z') ncur |= 1 << (c.charCodeAt(0) - 'a'.charCodeAt(0))
            if (ncur == (1 << cnt) - 1) return step + 1
            if (step + 1 >= dist[nx][ny][ncur]) continue
            d.push([nx, ny, ncur])
            dist[nx][ny][ncur] = step + 1
        }
    }
    return -1
}
```
* 时间复杂度:$O(n \times m \times 2^k)$
* 空间复杂度:$O(n \times m \times 2^k)$

### 最后

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

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

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

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


Question:
给你一个二维整数数组 matrix, 返回 matrix 的 转置矩阵 。

矩阵的 转置 是指将矩阵的主对角线翻转,交换矩阵的行索引与列索引。

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

提示:
* m == matrix.length
* n == matrix[i].length
* 1 <= m, n <= 1000
* 1 <= m * n <= $10^5$
* -$10^9$ <= matrix[i][j] <= $10^9$

Solution:
### 模拟

对角线翻转,只需要从头模拟一遍即可。

```java
class Solution {
    public int[][] transpose(int[][] matrix) {
        int n = matrix.length, m = matrix[0].length;
        int[][] ans = new int[m][n];
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                ans[i][j] = matrix[j][i];
            }
        }
        return ans;
    }
}
```
* 时间复杂度:$O(n * m)$
* 空间复杂度:使用同等空间保存答案。复杂度为 $O(n * m)$

***

### 矩阵类题目

题目过于简单,让人怀疑 LeetCode 团队是不是还在放假。

我在 LeetCode 上挑了几道难度适中,与矩阵变换相关的题目,如果大家感觉今天的「每日一题」不够尽兴的话,可以试着做一下:

[566. 重塑矩阵(简单)](https://leetcode-cn.com/problems/reshape-the-matrix/)

[54. 螺旋矩阵(中等)](https://leetcode-cn.com/problems/spiral-matrix/)

[59. 螺旋矩阵 II(中等)](https://leetcode-cn.com/problems/spiral-matrix-ii/)

***

#### 关于复杂度的说明

无论是时间还是空间都是用作描述算法运行所需要的资源消耗

我是将「算法本身占用的内存」和「输入数据」以外的内存作为“额外”内存,所以是 $O(n * m)$

官方的我看了一下,如果说明了忽略输出数据的内存占用的话,说成 $O(1)$ 也是可以的

就像我们使用递归求解问题的时候,栈帧本身也会带来内存消耗。因此进行复杂度分析的时候,出于严谨应该是要说明忽略递归带来的空间消耗

### 最后

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

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

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

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


Question:
给定一个正整数 $n$,找到并返回 $n$ 的二进制表示中两个 相邻 $1$ 之间的 最长距离 。如果不存在两个相邻的 $1$,返回 $0$ 。

如果只有 $0$ 将两个 $1$ 分隔开(可能不存在 $0$ ),则认为这两个 $1$ 彼此 相邻 。两个 $1$ 之间的距离是它们的二进制表示中位置的绝对差。例如,`"1001"` 中的两个 $1$ 的距离为 $3$ 。

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

输出:2

解释:22 的二进制是 "10110" 。
在 22 的二进制表示中,有三个 1,组成两对相邻的 1 。
第一对相邻的 1 中,两个 1 之间的距离为 2 。
第二对相邻的 1 中,两个 1 之间的距离为 1 。
答案取两个距离之中最大的,也就是 2 。
```
示例 2:
```
输入:n = 8

输出:0

解释:8 的二进制是 "1000" 。
在 8 的二进制表示中没有相邻的两个 1,所以返回 0 。
```
示例 3:
```
输入:n = 5

输出:2

解释:5 的二进制是 "101" 。
```

提示:
* $1 <= n <= 10^9$

Solution:
### 模拟

根据题意进行模拟即可,遍历 $n$ 的二进制中的每一位 $i$,同时记录上一位 $1$ 的位置 $j$,即可得到所有相邻 $1$ 的间距,所有间距取 $\max$ 即是答案。

代码:
```Java
class Solution {
    public int binaryGap(int n) {
        int ans = 0;
        for (int i = 31, j = -1; i >= 0; i--) {
            if (((n >> i) & 1) == 1) {
                if (j != -1) ans = Math.max(ans, j - i);
                j = i;
            }
        }
        return ans;
    }
}
```
* 时间复杂度:$O(\log{n})$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


LeetCode/861-870/869. 重新排序得到 2 的幂（中等）.md

869. 重新排序得到 2 的幂

Question:
给定正整数 `N` ,我们按任何顺序(包括原始顺序)将数字重新排序,注意其前导数字不能为零。

如果我们可以通过上述方式得到 $2$ 的幂,返回 `true`;否则,返回 `false`。

示例 1:
```
输入:1

输出:true
```
示例 2:
```
输入:10

输出:false
```
示例 3:
```
输入:16

输出:true
```
示例 4:
```
输入:24

输出:false
```
示例 5:
```
输入:46

输出:true
```

提示:
$1 <= N <= 10^9$

Solution:
### 打表 + DFS

一个朴素的做法是对 $n$ 进行重排,然后检查重排后的数值是否属于 $2$ 的幂。

由于 $2$ 的幂数固定,我们可以先通过「打表」将范围落在 $[1, 1e9]$ 以内的 $2$ 的幂预处理出来,这样我们可以在 $O(1)$ 的复杂度内判断某个数是否为 $2$ 的幂。

重排的过程则是 `DFS` 实现。

代码:
```Java
class Solution {
    static Set<Integer> set = new HashSet<>();
    static {
        for (int i = 1; i < (int)1e9+10; i *= 2) set.add(i);
    }
    int m;
    int[] cnts = new int[10];
    public boolean reorderedPowerOf2(int n) {
        while (n != 0) {
            cnts[n % 10]++;
            n /= 10;
            m++;
        }
        return dfs(0, 0);
    }
    boolean dfs(int u, int cur) {
        if (u == m) return set.contains(cur);
        for (int i = 0; i < 10; i++) {
            if (cnts[i] != 0) {
                cnts[i]--;
                if ((i != 0 || cur != 0) && dfs(u + 1, cur * 10 + i)) return true;
                cnts[i]++;
            }
        }
        return false;
    }
}
```
* 时间复杂度:打表预处理所有 $2$ 的幂放到本地运行为 $O(1)$,放到 $OJ$ 运行则是 $O(C)$,$C$ 为常数,约为 $30$。处理处 $cnts$ 数组的复杂度为 $O(\log{n})$;重排的复杂度为 $O((\log{n})!)$,判断是否为 $2$ 的幂的复杂度为 $O(1)$。整体复杂度为 $O((\log{n})!)$。
* 空间复杂度:$O(C)$,其中 $C$ 为常数,约为 $40$。

---

### 打表 + 词频统计

解法一,我们发现复杂度上界取决于对 $n$ 的重排,同时数据范围内的 $2$ 的幂数量很少。

因此有效降低复杂度(避免重排)的做法是,直接枚举所有的 $2$ 的幂 $x$,检查 $x$ 的词频是否与 $n$ 相同。

代码:
```Java
class Solution {
    static Set<Integer> set = new HashSet<>();
    static {
        for (int i = 1; i < (int)1e9+10; i *= 2) set.add(i);
    }
    public boolean reorderedPowerOf2(int n) {
        int[] cnts = new int[10];
        while (n != 0) {
            cnts[n % 10]++;
            n /= 10;
        }
        int[] cur = new int[10];
        out:for (int x : set) {
            Arrays.fill(cur, 0);
            while (x != 0) {
                cur[x % 10]++;
                x /= 10;
            }
            for (int i = 0; i < 10; i++) {
                if (cur[i] != cnts[i]) continue out;
            }
            return true;
        }
        return false;
    }
}
```
* 时间复杂度:打表预处理所有 $2$ 的幂放到本地运行为 $O(1)$,放到 $OJ$ 运行则是 $O(C)$,$C$ 为常数,约为 $30$。检查所有 $2$ 的幂的词频是否与 $n$ 词频相同复杂度为 $O(C * \log{n})$。整体复杂度为 $O(C * \log{n})$。
* 空间复杂度:$O(C)$,其中 $C$ 为常数,约为 $40$。

### 最后

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

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

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

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


Question:
给定两个大小相等的数组 `nums1` 和 `nums2`,`nums1` 相对于 `nums` 的优势可以用满足 `nums1[i] > nums2[i]` 的索引 `i` 的数目来描述。

返回 `nums1` 的任意排列,使其相对于 `nums2` 的优势最大化。

示例 1:
```
输入:nums1 = [2,7,11,15], nums2 = [1,10,4,11]

输出:[2,11,7,15]
```
示例 2:
```
输入:nums1 = [12,24,8,32], nums2 = [13,25,32,11]

输出:[24,32,8,12]
```

提示:
* $1 <= nums1.length <= 10^5$
* $nums2.length = nums1.length$
* $0 <= nums1[i], nums2[i] <= 10^9$

Solution:
### 数据结构

显然,对于任意一个 $t = nums2[i]$ 而言,我们应当在候选集合中选择**比其大的最小数**,若不存在这样的数字,则选择候选集合中的**最小值**。

同时,由于 $nums1$ 相同数会存在多个,我们还要对某个具体数字的可用次数进行记录。

也就是我们总共涉及两类操作:

1. 实时维护一个候选集合,该集合支持高效查询比某个数大的数值操作;
2. 对候选集合中每个数值的可使用次数进行记录,当使用到了候选集合中的某个数后,要对其进行计数减一操作,若计数为 $0$,则将该数值从候选集合中移除。

计数操作容易想到哈希表,而实时维护候选集合并高效查询可以使用基于红黑树的 `TreeSet` 数据结构。

Java 代码:
```Java
class Solution {
    public int[] advantageCount(int[] nums1, int[] nums2) {
        int n = nums1.length;
        TreeSet<Integer> tset = new TreeSet<>();
        Map<Integer, Integer> map = new HashMap<>();
        for (int x : nums1) {
            map.put(x, map.getOrDefault(x, 0) + 1);
            if (map.get(x) == 1) tset.add(x);
        }
        int[] ans = new int[n];
        for (int i = 0; i < n; i++) {
            Integer cur = tset.ceiling(nums2[i] + 1);
            if (cur == null) cur = tset.ceiling(-1);
            ans[i] = cur;
            map.put(cur, map.get(cur) - 1);
            if (map.get(cur) == 0) tset.remove(cur);
        }
        return ans;
    }
}
```
Python 代码:
```Python
from sortedcontainers import SortedList

class Solution:
    def advantageCount(self, nums1: List[int], nums2: List[int]) -> List[int]:
        n = len(nums1)
        cnts, tset = defaultdict(int), SortedList()
        for i in range(n):
            cnts[nums1[i]] += 1
            if cnts[nums1[i]] == 1:
                tset.add(nums1[i])
        ans = [0] * n
        for i in range(n):
            t = nums2[i]
            if (idx := tset.bisect_left(t + 1)) == len(tset):
                idx = tset.bisect_left(-1)
            ans[i] = tset[idx]
            cnts[ans[i]] -= 1
            if cnts[ans[i]] == 0:
                tset.remove(ans[i])
        return ans
```
* 时间复杂度:$O(n\log{n})$
* 空间复杂度:$O(n)$

---

### 排序 + 双指针

在解法一中,我们是从每个 $nums2[i]$ 出发考虑,使用哪个 $nums1[i]$ 去匹配最为合适。

实际上,我们也能从 $nums1[i]$ 出发,考虑将其与哪个 $nums2[i]$ 进行匹配。

为了让每个决策回合具有独立性,我们需要对两数组进行排序,同时为了在构造答案时,能够对应回 `nums2` 的原下标,排序前我们需要使用「哈希表」记录每个 $nums2[i]$ 的下标为何值。

使用变量 `l1` 代表当前决策将 $nums1[l1]$ 分配到哪个 `nums2` 的位置,使用 `l2` 和 `r2` 代表当前 `nums2` 中还有 $[l2, r2]$ 位置还待填充。

可以证明我们在从前往后给每个 $nums1[l1]$ 分配具体位置时,分配的位置只会在 `l2` 和 `r2` 两者之间产生。

Java 代码:
```Java
class Solution {
    public int[] advantageCount(int[] nums1, int[] nums2) {
        int n = nums1.length;
        Map<Integer, List<Integer>> map = new HashMap<>();
        for (int i = 0; i < n; i++) {
            List<Integer> list = map.getOrDefault(nums2[i], new ArrayList<>());
            list.add(i);
            map.put(nums2[i], list);
        }
        Arrays.sort(nums1); Arrays.sort(nums2);
        int[] ans = new int[n];
        for (int l1 = 0, l2 = 0, r2 = n - 1; l1 < n; l1++) {
            int t = nums1[l1] > nums2[l2] ? l2 : r2;
            List<Integer> list = map.get(nums2[t]);
            int idx = list.remove(list.size() - 1);
            ans[idx] = nums1[l1];
            if (t == l2) l2++;
            else r2--;
        }
        return ans;
    }
}
```
Python 代码:
```Python
class Solution:
    def advantageCount(self, nums1: List[int], nums2: List[int]) -> List[int]:
        n = len(nums1)
        mapping = defaultdict(list)
        for i in range(n):
            mapping[nums2[i]].append(i)
        nums1.sort()
        nums2.sort()
        ans = [0] * n
        l2, r2 = 0, n - 1
        for l1 in range(n):
            t = l2 if nums1[l1] > nums2[l2] else r2
            ans[mapping[nums2[t]].pop()] = nums1[l1]
            if t == l2:
                l2 += 1
            else:
                r2 -= 1
        return ans
```
* 时间复杂度:$O(n\log{n})$
* 空间复杂度:$O(n)$

---

### 加餐

**加餐一道同类型题目 : [难度 2.5/5,敲醒沉睡心灵的数据结构运用题](https://mp.weixin.qq.com/s?__biz=MzU4NDE3MTEyMA==&mid=2247493710&idx=1&sn=2d6d4ee9f4b1ff1ea766ddf7ea57bf55) 🎉🎉**

### 最后

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

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

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

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


Question:
汽车从起点出发驶向目的地,该目的地位于出发位置东面 `target` 英里处。

沿途有加油站,每个 `station[i]` 代表一个加油站,它位于出发位置东面 `station[i][0]` 英里处,并且有 `station[i][1]` 升汽油。

假设汽车油箱的容量是无限的,其中最初有 `startFuel` 升燃料。它每行驶 $1$ 英里就会用掉 $1$ 升汽油。

当汽车到达加油站时,它可能停下来加油,将所有汽油从加油站转移到汽车中。

为了到达目的地,汽车所必要的最低加油次数是多少?如果无法到达目的地,则返回 $-1$ 。

注意:如果汽车到达加油站时剩余燃料为 $0$,它仍然可以在那里加油。如果汽车到达目的地时剩余燃料为 $0$,仍然认为它已经到达目的地。

示例 1:
```
输入:target = 1, startFuel = 1, stations = []

输出:0

解释:我们可以在不加油的情况下到达目的地。
```
示例 2:
```
输入:target = 100, startFuel = 1, stations = [[10,100]]

输出:-1

解释:我们无法抵达目的地,甚至无法到达第一个加油站。
```
示例 3:
```
输入:target = 100, startFuel = 10, stations = [[10,60],[20,30],[30,30],[60,40]]

输出:2

解释:
我们出发时有 10 升燃料。
我们开车来到距起点 10 英里处的加油站,消耗 10 升燃料。将汽油从 0 升加到 60 升。
然后,我们从 10 英里处的加油站开到 60 英里处的加油站(消耗 50 升燃料),
并将汽油从 10 升加到 50 升。然后我们开车抵达目的地。
我们沿途在1两个加油站停靠,所以返回 2 。
```

提示:
* $1 <= target, startFuel, stations[i][1] <= 10^9$
* $0 <= stations.length <= 500$
* $1 <= position_{i} < position_{i+1} < target$
* $1 <= fuel_{i} < 10^9$

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

我们可以模拟行进过程,使用 `n` 代表加油站总个数,`idx` 代表经过的加油站下标,使用 `remain` 代表当前有多少油(起始有 `remain = startFuel`),`loc` 代表走了多远,`ans` 代表答案(至少需要的加油次数)。

只要 `loc < target`,代表还没到达(经过)目标位置,我们可以继续模拟行进过程。

每次将 `remain` 累加到 `loc` 中,含义为使用完剩余的油量,可以去到的最远距离,同时将所在位置 `stations[idx][0] <= loc` 的加油站数量加入优先队列(大根堆,根据油量排倒序)中。

再次检查是否满足 `loc < target`(下次循环),此时由于清空了剩余油量 `remain`,我们尝试从优先队列(大根堆)中取出过往油量最大的加油站并进行加油(同时对加油次数 `ans` 进行加一操作)。

使用新的剩余油量 `remain` 重复上述过程,直到满足 `loc >= target` 或无油可加。

容易证明该做法的正确性:同样是消耗一次加油次数,始终选择油量最大的加油站进行加油,可以确保不存在更优的结果。

Java 代码:
```Java
class Solution {
    public int minRefuelStops(int target, int startFuel, int[][] stations) {
        PriorityQueue<Integer> q = new PriorityQueue<>((a,b)->b-a);
        int n = stations.length, idx = 0;
        int remain = startFuel, loc = 0, ans = 0;
        while (loc < target) {
            if (remain == 0) {
                if (!q.isEmpty() && ++ans >= 0) remain += q.poll();
                else return -1;
            }
            loc += remain; remain = 0;
            while (idx < n && stations[idx][0] <= loc) q.add(stations[idx++][1]);
        }
        return ans;
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    int minRefuelStops(int target, int startFuel, vector<vector<int>>& stations) {
        priority_queue<int> q;
        int n = stations.size(), idx = 0;
        int remain = startFuel, loc = 0, ans = 0;
        while (loc < target) {
            if (remain == 0) {
                if (!q.empty() && ++ans >= 0) {
                    remain += q.top();
                    q.pop();
                } else {
                    return -1;
                }
            }
            loc += remain; remain = 0;
            while (idx < n && stations[idx][0] <= loc) q.push(stations[idx++][1]);
        }
        return ans;
    }
};
```
Python 代码:
```Python
class Solution:
    def minRefuelStops(self, target: int, startFuel: int, stations: List[List[int]]) -> int:
        q = []
        n, idx = len(stations), 0
        remain, loc, ans = startFuel, 0, 0
        while loc < target:
            if remain == 0:
                if q:
                    ans += 1
                    remain += -heappop(q)
                else:
                    return -1
            loc, remain = loc + remain, 0
            while idx < n and stations[idx][0] <= loc:
                heappush(q, -stations[idx][1])
                idx += 1
        return ans
```
* 时间复杂度:$O(n\log{n})$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


Question:
请考虑一棵二叉树上所有的叶子,这些叶子的值按从左到右的顺序排列形成一个 叶值序列 。

举个例子,如上图所示,给定一棵叶值序列为 (6, 7, 4, 9, 8) 的树。

如果有两棵二叉树的叶值序列是相同,那么我们就认为它们是 叶相似 的。

如果给定的两个根结点分别为 root1 和 root2 的树是叶相似的,则返回 true;否则返回 false 。

示例 1:

```
输入:
root1 = [3,5,1,6,2,9,8,null,null,7,4],
root2 = [3,5,1,6,7,4,2,null,null,null,null,null,null,9,8]

输出:true
```
示例 2:
```
输入:root1 = [1], root2 = [1]

输出:true
```
示例 3:
```
输入:root1 = [1], root2 = [2]

输出:false
```
示例 4:
```
输入:root1 = [1,2], root2 = [2,2]

输出:true
```
示例 5:

```
输入:root1 = [1,2,3], root2 = [1,3,2]

输出:false
```

提示:
* 给定的两棵树可能会有 1 到 200 个结点。
* 给定的两棵树上的值介于 0 到 200 之间。

Solution:
### 递归

递归写法十分简单,属于树的遍历中最简单的实现方式。

代码:
```Java
class Solution {
    public boolean leafSimilar(TreeNode t1, TreeNode t2) {
        List<Integer> l1 = new ArrayList<>(), l2 = new ArrayList<>();
        dfs(t1, l1);
        dfs(t2, l2);
        if (l1.size() == l2.size()) {
            for (int i = 0; i < l1.size(); i++) {
                if (!l1.get(i).equals(l2.get(i))) return false;
            }
            return true;
        }
        return false;
    }
    void dfs(TreeNode root, List<Integer> list) {
        if (root == null) return;
        if (root.left == null && root.right == null) {
            list.add(root.val);
            return;
        }
        dfs(root.left, list);
        dfs(root.right, list);
    }
}
```
* 时间复杂度:`n` 和 `m` 分别代表两棵树的节点数量。复杂度为 $O(n + m)$
* 空间复杂度:`n` 和 `m` 分别代表两棵树的节点数量,当两棵树都只有一层的情况,所有的节点值都会被存储在 $list$ 中。复杂度为 $O(n + m)$

---

### 迭代

迭代其实就是使用「栈」来模拟递归过程,也属于树的遍历中的常见实现形式。

一般简单的面试中如果问到树的遍历,面试官都不会对「递归」解法感到满意,因此掌握「迭代/非递归」写法同样重要。

代码:
```Java
class Solution {
    public boolean leafSimilar(TreeNode t1, TreeNode t2) {
        List<Integer> l1 = new ArrayList<>(), l2 = new ArrayList<>();
        process(t1, l1);
        process(t2, l2);
        if (l1.size() == l2.size()) {
            for (int i = 0; i < l1.size(); i++) {
                if (!l1.get(i).equals(l2.get(i))) return false;
            }
            return true;
        }
        return false;
    }
    void process(TreeNode root, List<Integer> list) {
        Deque<TreeNode> d = new ArrayDeque<>();
        while (root != null || !d.isEmpty()) {
            while (root != null) {
                d.addLast(root);
                root = root.left;
            }
            root = d.pollLast();
            if (root.left == null && root.right == null) list.add(root.val);
            root = root.right;
        }
    }
}
```
* 时间复杂度:`n` 和 `m` 分别代表两棵树的节点数量。复杂度为 $O(n + m)$
* 空间复杂度:`n` 和 `m` 分别代表两棵树的节点数量,当两棵树都只有一层的情况,所有的节点值都会被存储在 $list$ 中。复杂度为 $O(n + m)$

### 最后

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

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

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

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


LeetCode/871-880/873. 最长的斐波那契子序列的长度（中等）.md

873. 最长的斐波那契子序列的长度

Question:
如果序列 $X_1, X_2, ..., X_n$ 满足下列条件,就说它是 斐波那契式 的:

* `n >= 3`
* 对于所有 `i + 2 <= n`,都有 $X_i + X_{i+1} = X_{i+2}$

给定一个严格递增的正整数数组形成序列 `arr`,找到 `arr` 中最长的斐波那契式的子序列的长度。如果一个不存在,返回  $0$ 。

(回想一下,子序列是从原序列 `arr` 中派生出来的,它从 `arr` 中删掉任意数量的元素(也可以不删),而不改变其余元素的顺序。例如, `[3, 5, 8]` 是 `[3, 4, 5, 6, 7, 8]` 的一个子序列)

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

输出: 5

解释: 最长的斐波那契式子序列为 [1,2,3,5,8] 。
```
示例 2:
```
输入: arr = [1,3,7,11,12,14,18]

输出: 3

解释: 最长的斐波那契式子序列有 [1,11,12]、[3,11,14] 以及 [7,11,18] 。
```

提示:
* $3 <= arr.length <= 1000$
* $1 <= arr[i] < arr[i + 1] <= 10^9$

Solution:
### 序列 DP

定义 $f[i][j]$ 为使用 $arr[i]$ 为斐波那契数列的最后一位,使用 $arr[j]$ 为倒数第二位(即 $arr[i]$ 的前一位)时的最长数列长度。

不失一般性考虑 $f[i][j]$ 该如何计算,首先根据斐波那契数列的定义,我们可以直接算得 $arr[j]$ 前一位的值为 $arr[i] - arr[j]$,而快速得知 $arr[i] - arr[j]$ 值的坐标 $t$,可以利用 `arr` 的严格单调递增性质,使用「哈希表」对坐标进行转存,若坐标 $t$ 存在,并且符合 $t < j$,说明此时至少凑成了长度为 $3$ 的斐波那契数列,同时结合状态定义,可以使用 $f[j][t]$ 来更新 $f[i][j]$,即有状态转移方程:

$$
f[i][j] = \max(3, f[j][t] + 1)
$$

同时,当我们「从小到大」枚举 $i$,并且「从大到小」枚举 $j$ 时,我们可以进行如下的剪枝操作:

* 可行性剪枝:当出现 $arr[i] - arr[j] >= arr[j]$,说明即使存在值为 $arr[i] - arr[j]$ 的下标 $t$,根据 `arr` 单调递增性质,也不满足 $t < j < i$ 的要求,且继续枚举更小的 $j$,仍然有 $arr[i] - arr[j] >= arr[j]$,仍不合法,直接 `break` 掉当前枚举 $j$ 的搜索分支;
* 最优性剪枝:假设当前最大长度为 `ans`,只有当 $j + 2 > ans$,我们才有必要往下搜索,$j + 2$ 的含义为以 $arr[j]$ 为斐波那契数列倒数第二个数时的理论最大长度。

代码:
```Java
class Solution {
    public int lenLongestFibSubseq(int[] arr) {
        int n = arr.length, ans = 0;
        Map<Integer, Integer> map = new HashMap<>();
        for (int i = 0; i < n; i++) map.put(arr[i], i);
        int[][] f = new int[n][n];
        for (int i = 0; i < n; i++) {
            for (int j = i - 1; j >= 0 && j + 2 > ans; j--) {
                if (arr[i] - arr[j] >= arr[j]) break;
                int t = map.getOrDefault(arr[i] - arr[j], -1);
                if (t == -1) continue;
                f[i][j] = Math.max(3, f[j][t] + 1);
                ans = Math.max(ans, f[i][j]);
            }
        }
        return ans;
    }
}
```
* 时间复杂度:存入哈希表复杂度为 $O(n)$;`DP` 过程复杂度为 $O(n^2)$。整体复杂度为 $O(n^2)$
* 空间复杂度:$O(n^2)$

### 最后

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

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

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

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


875. 爱吃香蕉的珂珂

Question:
珂珂喜欢吃香蕉。这里有 `n` 堆香蕉,第 `i` 堆中有 `piles[i]` 根香蕉。警卫已经离开了,将在 `h` 小时后回来。

珂珂可以决定她吃香蕉的速度 `k` (单位:根/小时)。每个小时,她将会选择一堆香蕉,从中吃掉 `k` 根。如果这堆香蕉少于 `k` 根,她将吃掉这堆的所有香蕉,然后这一小时内不会再吃更多的香蕉。

珂珂喜欢慢慢吃,但仍然想在警卫回来前吃掉所有的香蕉。

返回她可以在 `h` 小时内吃掉所有香蕉的最小速度 `k`(`k` 为整数)。

示例 1:
```
输入:piles = [3,6,7,11], h = 8

输出:4
```
示例 2:
```
输入:piles = [30,11,23,4,20], h = 5

输出:30
```
示例 3:
```
输入:piles = [30,11,23,4,20], h = 6

输出:23
```

提示:
* $1 <= piles.length <= 10^4$
* $piles.length <= h <= 10^9$
* $1 <= piles[i] <= 10^9$

Solution:
### 二分

由于存在「吃完这堆的所有香蕉,然后这一小时内不会再吃更多的香蕉」的条件,因此不会存在多堆香蕉共用一个小时的情况,即每堆香蕉都是相互独立,同时可以明确每堆香蕉的耗时为 $\left \lceil \frac{piles[i]}{k} \right \rceil$(其中 $k$ 为速度)。

因此我们可以二分 $k$ 值,在以 $k$ 为分割点的数组上具有「二段性」:

* 小于 $k$ 的值,总耗时 $ans$ 必然不满足 $ans \leq h$
* 大于等于 $k$ 的值,总耗时 $ans$ 必然满足 $ans \leq h$

然后我们需要确定二分的范围,每堆香蕉至少消耗一个小时,因此大于 $\max(piles[i])$ 的速度值 $k$ 是没有意义的(与 $k = \max(piles[i])$ 等价),因此我们可以先对 `piles` 进行一次遍历,找最大值,再二分;也可以直接利用数据范围 $1 <= piles[i] <= 10^9$ 确定一个粗略边界进行二分。

最后的 `check` 函数,只需要计算当前速率 $k$ 所对应的总耗时 $ans$,再与 $h$ 做比较即可。

代码:
```Java
class Solution {
    public int minEatingSpeed(int[] piles, int h) {
        int l = 0, r = (int)1e9;
        while (l < r) {
            int mid = l + r >> 1;
            if (check(piles, mid, h)) r = mid;
            else l = mid + 1;
        }
        return r;
    }
    boolean check(int[] p, int k, int h) {
        int ans = 0;
        for (int x : p) ans += Math.ceil(x * 1.0 / k);
        return ans <= h;
    }
}
```
* 时间复杂度:令 $n$ 数组长度,$m = 1e9$ 为最大的 $piles[i]$,复杂度为 $O(n\log{m})$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


Question:
亚历克斯和李用几堆石子在做游戏。偶数堆石子排成一行,每堆都有正整数颗石子 piles[i] 。

游戏以谁手中的石子最多来决出胜负。石子的总数是奇数,所以没有平局。

亚历克斯和李轮流进行,亚历克斯先开始。 每回合,玩家从行的开始或结束处取走整堆石头。 这种情况一直持续到没有更多的石子堆为止,此时手中石子最多的玩家获胜。

假设亚历克斯和李都发挥出最佳水平,当亚历克斯赢得比赛时返回 true ,当李赢得比赛时返回 false 。

示例:
```
输入:[5,3,4,5]
输出:true
解释:
亚历克斯先开始,只能拿前 5 颗或后 5 颗石子 。
假设他取了前 5 颗,这一行就变成了 [3,4,5] 。
如果李拿走前 3 颗,那么剩下的是 [4,5],亚历克斯拿走后 5 颗赢得 10 分。
如果李拿走后 5 颗,那么剩下的是 [3,4],亚历克斯拿走后 4 颗赢得 9 分。
这表明,取前 5 颗石子对亚历克斯来说是一个胜利的举动,所以我们返回 true 。
```

提示:
* 2 <= piles.length <= 500
* piles.length 是偶数。
* 1 <= piles[i] <= 500
* sum(piles) 是奇数。

Solution:
### 动态规划

**定义 $f[l][r]$ 为考虑区间 $[l,r]$,在双方都做最好选择的情况下,先手与后手的最大得分差值为多少。**

那么 $f[1][n]$ 为考虑所有石子,先手与后手的得分差值:

* $f[1][n] > 0$,则先手必胜,返回 `True`
* $f[1][n] < 0$,则先手必败,返回 `False`

不失一般性的考虑 $f[l][r]$ 如何转移。根据题意,只能从两端取石子(令 $piles$ 下标从 $1$ 开始),共两种情况:

* 从左端取石子,价值为 $piles[l - 1]$;取完石子后,原来的后手变为先手,从 $[l + 1, r]$ 区间做最优决策,所得价值为 $f[l + 1][r]$。**因此本次先手从左端点取石子的话,双方差值为**:

$$
piles[l - 1] - f[l + 1][r]
$$

* 从右端取石子,价值为 $piles[r - 1]$;取完石子后,原来的后手变为先手,从 $[l, r - 1]$ 区间做最优决策,所得价值为 $f[l][r - 1]$。**因此本次先手从右端点取石子的话,双方差值为**:

$$
piles[r - 1] - f[l][r - 1]
$$

双方都想赢,都会做最优决策(即使自己与对方分差最大)。因此 $f[l][r]$ 为上述两种情况中的最大值。

根据状态转移方程,我们发现大区间的状态值依赖于小区间的状态值,典型的区间 DP 问题。

按照从小到大「枚举区间长度」和「区间左端点」的常规做法进行求解即可。

代码:
```Java
class Solution {
    public boolean stoneGame(int[] ps) {
        int n = ps.length;
        int[][] f = new int[n + 2][n + 2];
        for (int len = 1; len <= n; len++) { // 枚举区间长度
            for (int l = 1; l + len - 1 <= n; l++) { // 枚举左端点
                int r = l + len - 1; // 计算右端点
                int a = ps[l - 1] - f[l + 1][r];
                int b = ps[r - 1] - f[l][r - 1];
                f[l][r] = Math.max(a, b);
            }
        }
        return f[1][n] > 0;
    }
}
```
* 时间复杂度:$O(n^2)$
* 空间复杂度:$O(n^2)$

---

### 博弈论

事实上,这还是一道很经典的博弈论问题,也是最简单的一类博弈论问题。

为了方便,我们称「石子序列」为石子在原排序中的编号,下标从 $1$ 开始。

由于石子的堆数为偶数,且只能从两端取石子。**因此先手后手所能选择的石子序列,完全取决于先手每一次决定。**

由于石子的堆数为偶数,对于先手而言:**每一次的决策局面,都能「自由地」选择奇数还是偶数的序列,从而限制后手下一次「只能」奇数还是偶数石子。**

具体的,对于本题,**由于石子堆数为偶数,因此先手的最开始局面必然是 $[奇数, 偶数]$,即必然是「奇偶性不同的局面」;当先手决策完之后,交到给后手的要么是 $[奇数,奇数]$ 或者 $[偶数,偶数]$,即必然是「奇偶性相同的局面」;后手决策完后,又恢复「奇偶性不同的局面」交回到先手** ...

不难归纳推理,这个边界是可以应用到每一个回合。

**因此先手只需要在进行第一次操作前计算原序列中「奇数总和」和「偶数总和」哪个大,然后每一次决策都「限制」对方只能选择「最优奇偶性序列」的对立面即可。**

同时又由于所有石子总和为奇数,堆数为偶数,即没有平局,所以先手必胜。

代码:
```Java
class Solution {
    public boolean stoneGame(int[] piles) {
        return true;
    }
}
```
* 时间复杂度:$O(1)$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


878. 第 N 个神奇数字

Question:
一个正整数如果能被 `a` 或 `b` 整除,那么它是神奇的。

给定三个整数 `n` , `a` , `b` ,返回第 `n` 个神奇的数字。因为答案可能很大,所以返回答案 对 $10^9 + 7$ 取模 后的值。

示例 1:
```
输入:n = 1, a = 2, b = 3

输出:2
```
示例 2:
```
输入:n = 4, a = 2, b = 3

输出:6
```

提示:
* $1 <= n <= 10^9$
* $2 <= a, b <= 4 \times 10^4$

Solution:
### 数学

#### 提示一 : 从题面分析常见做法,从常见做法复杂度出发考虑其他做法

若不看数据范围,只看题面,容易想到的做法是「多路归并」:起始使用两个指针指向 `[a, 2a, 3a, ... ]` 和 `[b, 2b, 3b, ...]` 的开头,不断比较两指针所指向的数值大小,从而决定将谁后移,并不断更新顺位计数。

该做法常见,但其复杂度为 $O(n)$,对于本题 $n = 1e9$ 来说并不可行。

确定线性复杂度的做法不可行后,我们考虑是否存在对数复杂度的做法。

#### 提示二 : 如何考虑常见的对数复杂度做法,如何定义二段性

题目要我们求第 $n$ 个符合要求的数,假设我们想要通过「二分」来找该数值,那么我们需要分析其是否存在「二段性」。

假设在所有「能够被 `a` 或 `b` 整除的数」形成的数轴上,我们要找的分割点是 `k`,我们期望通过「二分」来找到 `k` 值,那么需要定义某种性质,使得 `k` 左边的数均满足该性质,`k` 右边的数均不满足该性质。

不难想到可根据题意来设定该性质:小于 `k` 的任意数字 `x` 满足在 $[0, x]$ 范围数的个数不足 `k` 个,而大于等于 `k` 的任意数字 `x` 则不满足该性质。

#### 提示三 : 如何实现高效的 `check` 函数

当确定使用「二分」来做时,剩下问题转化为:**如何快速得知某个 $[0, n]$ 中满足要求的数的个数。**

容易联想到「容斥原理」:**能被 `a` 或 `b` 整除的数的个数 = 能够被 `a` 整除的数的个数 + 能够被 `b` 整除的数的个数 - 既能被 `a` 又能被 `b` 整除的数的个数**。

$$
\left \lfloor \frac{n}{a} \right \rfloor + \left \lfloor \frac{n}{b} \right \rfloor - \left \lfloor \frac{n}{c} \right \rfloor
$$

其中 `c` 为 `a` 和 `b` 的最小公倍数。

求解最小公倍数 `lcm` 需要实现最大公约数 `gcd`,两者模板分别为:

```Java []
int gcd(int a, int b) {
    return b == 0 ? a : gcd(b, a % b);
}
int lcm(int a, int b) {
    return a * b / gcd(a, b);
}
```

#### 提示四 : 如何确定值域

一个合格的值域只需要确定答案在值域范围即可,因此我们可以直接定值域大小为 $1e18$。

或是根据 `a` 和 `b` 的取值来大致确定:假设两者中的较大值为 $m$,此时第 $n$ 个符合要求的数最大不会超过 $n \times m$,因此也可以设定值域大小为 $[0, 40000n]$。

Java 代码:
```Java
class Solution {
    int n, a, b, c;
    int gcd(int a, int b) {
        return b == 0 ? a : gcd(b, a % b);
    }
    public int nthMagicalNumber(int _n, int _a, int _b) {
        n = _n; a = _a; b = _b; c = a * b / gcd(a, b);
        long l = 0, r = (long)1e18;
        while (l < r) {
            long mid = l + r >> 1;
            if (check(mid) >= n) r = mid;
            else l = mid + 1;
        }
        return (int)(r % 1000000007);
    }
    long check(long x) {
        return x / a + x / b - x / c;
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    int n, a, b, c;
    int gcd(int a, int b) {
        return b == 0 ? a : gcd(b, a % b);
    }
    long check(long x) {
        return x / a + x / b - x / c;
    }
    int nthMagicalNumber(int _n, int _a, int _b) {
        n = _n; a = _a; b = _b; c = a * b / gcd(a, b);
        long l = 0, r = 1e18;
        while (l < r) {
            long mid = l + r >> 1;
            if (check(mid) >= n) r = mid;
            else l = mid + 1;
        }
        return (int)(r % 1000000007);
    }
};
```
Python3 代码:
```Python
class Solution:
    def nthMagicalNumber(self, n: int, a: int, b: int) -> int:
        def gcd(a, b):
            return a if b == 0 else gcd(b, a % b)
        def check(x):
            return x // a + x // b - x // c
        c = a * b // gcd(a, b)
        l, r = 0, 1e18
        while l < r:
            mid = (l + r) // 2
            if check(mid) >= n:
                r = mid
            else:
                l = mid + 1
        return int(r % 1000000007)
```
TypeScript 代码:
```TypeScript
function nthMagicalNumber(n: number, a: number, b: number): number {
    function gcd(a: number, b: number): number {
        return b == 0 ? a : gcd(b, a % b)
    }
    function check(x: number): number {
        return Math.floor(x / a) + Math.floor(x / b) - Math.floor(x / c)
    }
    const c = Math.floor(a * b / gcd(a, b))
    let l = 0, r = 1e18
    while (l < r) {
        const mid = Math.floor((l + r) / 2)
        if (check(mid) >= n) r = mid
        else l = mid + 1
    }
    return r % 1000000007
}
```
* 时间复杂度:$O(\log{N})$,其中 $N = 1e18$ 为值域大小
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


Question:
集团里有 `n` 名员工,他们可以完成各种各样的工作创造利润。

第 `i` 种工作会产生 `profit[i]` 的利润,它要求 `group[i]` 名成员共同参与。如果成员参与了其中一项工作,就不能参与另一项工作。

工作的任何至少产生 `minProfit` 利润的子集称为 盈利计划 。并且工作的成员总数最多为 `n` 。

有多少种计划可以选择?因为答案很大,所以 返回结果模 $10^9 + 7 $ 的值。

示例 1:
```
输入:n = 5, minProfit = 3, group = [2,2], profit = [2,3]
输出:2
解释:至少产生 3 的利润,该集团可以完成工作 0 和工作 1 ,或仅完成工作 1 。
总的来说,有两种计划。
```
示例 2:
```
输入:n = 10, minProfit = 5, group = [2,3,5], profit = [6,7,8]
输出:7
解释:至少产生 5 的利润,只要完成其中一种工作就行,所以该集团可以完成任何工作。
有 7 种可能的计划:(0),(1),(2),(0,1),(0,2),(1,2),以及 (0,1,2) 。
```

提示:
* 1 <= n <= 100
* 0 <= minProfit <= 100
* 1 <= group.length <= 100
* 1 <= group[i] <= 100
* profit.length == group.length
* 0 <= profit[i] <= 100

Solution:
### 动态规划

这是一类特殊的多维费用背包问题。

将每个任务看作一个「物品」,完成任务所需要的人数看作「成本」,完成任务得到的利润看作「价值」。

其特殊在于存在一维容量维度需要满足「不低于」,而不是常规的「不超过」。这需要我们对于某些状态作等价变换。

**定义 $f[i][j][k]$ 为考虑前 $i$ 件物品,使用人数不超过 $j$,所得利润至少为 $k$ 的方案数。**

对于每件物品(令下标从 $1$ 开始),我们有「选」和「不选」两种决策:

* 不选:显然有:

$$
f[i - 1][j][k]
$$

* 选:首先需要满足人数达到要求( $j >= group[i - 1]$ ),还需要考虑「至少利润」负值问题:
    如果直接令「利润维度」为 $k - profit[i - 1]$ 可能会出现负值,那么负值是否为合法状态呢?这需要结合「状态定义」来看,由于是「利润至少为 $k$」,因此属于「合法状态」,需要参与转移。
    **由于我们没有设计动规数组存储「利润至少为负权」状态,我们需要根据「状态定义」做一个等价替换,将这个「状态」映射到 $f[i][j][0]$。这主要是利用所有的任务利润都为“非负数”,所以不可能出现利润为负的情况,这时候「利润至少为某个负数 $k$」的方案数其实是完全等价于「利润至少为 $0$」的方案数。**

$$
f[i - 1][j - group[i - 1]][\max(k - profit[i - 1], 0)]
$$

最终 $f[i][j][k]$ 为上述两种情况之和.

然后考虑「如何构造有效起始值」问题,还是结合我们的「状态定义」来考虑:

当不存在任何物品(任务)时,所得利用利润必然为 $0$(满足至少为 $0$),同时对人数限制没有要求。

因此可以让所有 $f[0][x][0] = 1$。

代码(一维空间优化代码见 P2):
```Java
class Solution {
    int mod = (int)1e9+7;
    public int profitableSchemes(int n, int min, int[] gs, int[] ps) {
        int m = gs.length;
        long[][][] f = new long[m + 1][n + 1][min + 1];
        for (int i = 0; i <= n; i++) f[0][i][0] = 1;
        for (int i = 1; i <= m; i++) {
            int a = gs[i - 1], b = ps[i - 1];
            for (int j = 0; j <= n; j++) {
                for (int k = 0; k <= min; k++) {
                    f[i][j][k] = f[i - 1][j][k];
                    if (j >= a) {
                        int u = Math.max(k - b, 0);
                        f[i][j][k] += f[i - 1][j - a][u];
                        f[i][j][k] %= mod;
                    }
                }
            }
        }
        return (int)f[m][n][min];
    }
}
```
```Java
class Solution {
    int mod = (int)1e9+7;
    public int profitableSchemes(int n, int min, int[] gs, int[] ps) {
        int m = gs.length;
        int[][] f = new int[n + 1][min + 1];
        for (int i = 0; i <= n; i++) f[i][0] = 1;
        for (int i = 1; i <= m; i++) {
            int a = gs[i - 1], b = ps[i - 1];
            for (int j = n; j >= a; j--) {
                for (int k = min; k >= 0; k--) {
                    int u = Math.max(k - b, 0);
                    f[j][k] += f[j - a][u];
                    if (f[j][k] >= mod) f[j][k] -= mod;
                }
            }
        }
        return f[n][min];
    }
}
```
* 时间复杂度:$O(m * n * min)$
* 空间复杂度:$O(m * n * min)$

---

### 动态规划(作差法)

这个方案足足调了快一个小时 🤣

先是爆 `long`,然后转用高精度后被卡内存,最终改为滚动数组后~~勉强过了~~(不是,稳稳的过了,之前调得久是我把 `N` 多打了一位,写成 1005 了,`N` 不打错的话,不滚动也是能过的 😭😭😭 )

基本思路是先不考虑最小利润 `minProfit`,求得所有只受「人数限制」的方案数 `a`,然后求得考虑「人数限制」同时,利润低于 `minProfit`(不超过 `minProfit - 1`)的所有方案数 `b`。

由 `a` - `b` 即是答案。

代码:
```Java
import java.math.BigInteger;
class Solution {
    static int N = 105;
    static BigInteger[][] f = new BigInteger[2][N];
    static BigInteger[][][] g = new BigInteger[2][N][N];
    static BigInteger mod = new BigInteger("1000000007");

    public int profitableSchemes(int n, int min, int[] gs, int[] ps) {
        int m = gs.length;

        for (int j = 0; j <= n; j++) {
            f[0][j] = new BigInteger("1");
            f[1][j] = new BigInteger("0");
        }
        for (int j = 0; j <= n; j++) {
            for (int k = 0; k <= min; k++) {
                g[0][j][k] = new BigInteger("1");
                g[1][j][k] = new BigInteger("0");
            }
        }

        for (int i = 1; i <= m; i++) {
            int a = gs[i - 1], b = ps[i - 1];
            int x = i & 1, y = (i - 1) & 1;
            for (int j = 0; j <= n; j++) {
                f[x][j] = f[y][j];
                if (j >= a) {
                    f[x][j] = f[x][j].add(f[y][j - a]);
                }
            }
        }
        if (min == 0) return (f[m&1][n]).mod(mod).intValue();

        for (int i = 1; i <= m; i++) {
            int a = gs[i - 1], b = ps[i - 1];
            int x = i & 1, y = (i - 1) & 1;
            for (int j = 0; j <= n; j++) {
                for (int k = 0; k < min; k++) {
                    g[x][j][k] = g[y][j][k];
                    if (j - a >= 0 && k - b >= 0) {
                        g[x][j][k] = g[x][j][k].add(g[y][j - a][k - b]);
                    }
                }
            }
        }

        return f[m&1][n].subtract(g[m&1][n][min - 1]).mod(mod).intValue();
    }
}
```
* 时间复杂度:第一遍 `DP` 复杂度为 $O(m * n)$;第二遍 `DP` 复杂度为 $O(m * n * min)$。整体复杂度为 $O(m * n * min)$
* 空间复杂度:$O(m * n * min)$

### 最后

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

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

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

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


Question:
第 `i` 个人的体重为 `people[i]`,每艘船可以承载的最大重量为 `limit`。

每艘船最多可同时载两人,但条件是这些人的重量之和最多为 `limit`。

返回载到每一个人所需的最小船数。(保证每个人都能被船载)。

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

输出:1

解释:1 艘船载 (1, 2)
```
示例 2:
```
输入:people = [3,2,2,1], limit = 3

输出:3

解释:3 艘船分别载 (1, 2), (2) 和 (3)
```
示例 3:
```
输入:people = [3,5,3,4], limit = 5

输出:4

解释:4 艘船分别载 (3), (3), (4), (5)
```

提示:
* 1 <= people.length <= 50000
* 1 <= people[i] <= limit <= 30000

Solution:
### 贪心

一个直观的想法是:由于一个船要么载两人要么载一人,在人数给定的情况下,为了让使用的总船数最小,要当尽可能让更多船载两人,即尽可能多的构造出数量之和不超过 $limit$ 的二元组。

先对 $people$ 进行排序,然后使用两个指针 `l` 和 `r` 分别从首尾开始进行匹配:

* 如果 $people[l] + people[r] <= limit$,说明两者可以同船,此时船的数量加一,两个指针分别往中间靠拢;
* 如果 $people[l] + people[r] > limit$,说明不能成组,由于题目确保人的重量不会超过 $limit$,此时让 $people[r]$ 独立成船,船的数量加一,`r` 指针左移。

**我们猜想这样「最重匹配最轻、次重匹配次轻」的做法能使双人船的重量之和尽可能平均,从而使双人船的数量最大化。**

接下来,我们使用「归纳法」证明猜想的正确性。

假设最优成船组合中二元组的数量为 $c1$,我们贪心做法的二元组数量为 $c2$。

最终答案 = 符合条件的二元组的数量 + 剩余人数数量,而在符合条件的二元组数量固定的情况下,剩余人数也固定。因此我们只需要证明 $c1 = c2$ 即可。

通常使用「归纳法」进行证明,都会先从边界入手。

当我们处理最重的人 $people[r]$(此时 $r$ 为原始右边界 $n - 1$)时:

* 假设其与 $people[l]$(此时 $l$ 为原始左边界 $0$)之和超过 $limit$,说明 $people[r]$ 与数组任一成员组合都会超过 $limit$,即无论在最优组合还是贪心组合中,$people[r]$ 都会独立成船;
* 假设 $people[r] + people[l] <= limit$,说明数组中存在至少一个成员能够与 $people[l]$ 成船:

    * 假设在最优组合中 $people[l]$ 独立成船,此时如果将贪心组合 $(people[l], people[r])$ 中的 $people[l]$ 拆分出来独立成船,贪心二元组数量 $c2$ 必然不会变大(可能还会变差),即将「贪心解」调整成「最优解」结果不会变好;

    * 假设在最优组合中,$people[l]$ 不是独立成船,又因此当前 $r$ 处于原始右边界,因此与 $people[l]$ 成组的成员 $people[x]$ 必然满足 $people[x] <= people[r]$。
      此时我们将 $people[x]$ 和 $people[r]$ 位置进行交换(将贪心组合调整成最优组合),此时带来的影响包括:
        * 与 $people[l]$ 成组的对象从 $people[r]$ 变为 $people[x]$,但因为 $people[x] <= people[r]$,即有 $people[l] + people[x] <= people[l] + people[r] <= limit$,仍为合法二元组,消耗船的数量为 $1$;
        * 原本位置 $x$ 的值从 $people[x]$ 变大为 $people[r]$,如果调整后的值能组成二元组,那么原本更小的值也能组成二元组,结果没有变化;如果调整后不能成为组成二元组,那么结果可能会因此变差。

        综上,将 $people[x]$ 和 $people[r]$ 位置进行交换(将贪心组合调整成最优组合),贪心二元组数量 $c2$ 不会变大,即将「贪心解」调整成「最优解」结果不会变好。

**对于边界情况,我们证明了从「贪心解」调整为「最优解」不会使得结果更好,因此可以保留当前的贪心决策,然后将问题规模缩减为 $n - 1$ 或者 $n - 2$,同时数列仍然满足升序特性,即归纳分析所依赖的结构没有发生改变,可以将上述的推理分析推广到每一个决策的回合(新边界)中。**

**至此,我们证明了将「贪心解」调整为「最优解」结果不会变好,即贪心解是最优解之一。**

代码:
```Java
class Solution {
    public int numRescueBoats(int[] people, int limit) {
        Arrays.sort(people);
        int n = people.length;
        int l = 0, r = n - 1;
        int ans = 0;
        while (l <= r) {
            if (people[l] + people[r] <= limit) l++;
            r--;
            ans++;
        }
        return ans;
    }
}
```
* 时间复杂度:排序复杂度为 $O(n\log{n})$;双指针统计答案复杂度为 $O(n)$。整体复杂度为 $O(n\log{n})$
* 空间复杂度:$O(\log{n})$

### 最后

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

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

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

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


882. 细分图中的可到达节点

Question:
给你一个无向图(原始图),图中有 `n` 个节点,编号从 `0` 到 `n - 1` 。你决定将图中的每条边 细分 为一条节点链,每条边之间的新节点数各不相同。

图用由边组成的二维数组 `edges` 表示,其中 $edges[i] = [u_{i}, v_{i}, cnt_{i}]$ 表示原始图中节点 $u_{i}$ 和 $v_{i}$ 之间存在一条边,$cnt_{i}$ 是将边 细分 后的新节点总数。注意,$cnt_{i} = 0$ 表示边不可细分。

要 细分边 $[u_{i}, v_{i}]$ ,需要将其替换为 $(cnt_{i} + 1)$ 条新边,和 $cnt_{i}$ 个新节点。新节点为 $x_1, x_2, ..., x_{cnt_{i}}$ ,新边为 $[u_{i}, x_{1}], [x_{1}, x_{2}], [x_{2}, x_{3}], ..., [x_{cnt_{i}}+1, x_{cnt_{i}}], [x_{cnt_{i}}, v_{i}]$ 。

现在得到一个 新的细分图 ,请你计算从节点 `0` 出发,可以到达多少个节点?如果节点间距离是 `maxMoves` 或更少,则视为 可以到达 。

给你原始图和 `maxMoves` ,返回 新的细分图中从节点 `0` 出发 可到达的节点数 。

示例 1:
```
输入:edges = [[0,1,10],[0,2,1],[1,2,2]], maxMoves = 6, n = 3

输出:13

解释:边的细分情况如上图所示。
可以到达的节点已经用黄色标注出来。
```
示例 2:
```
输入:edges = [[0,1,4],[1,2,6],[0,2,8],[1,3,1]], maxMoves = 10, n = 4

输出:23
```
示例 3:
```
输入:edges = [[1,2,4],[1,4,5],[1,3,1],[2,3,4],[3,4,5]], maxMoves = 17, n = 5

输出:1

解释:节点 0 与图的其余部分没有连通,所以只有节点 0 可以到达。
```

提示:
* $0 <= edges.length <= \min(n * (n - 1) / 2, 10^4)$
* $edges[i].length = 3$
* $0 <= u_{i} < v_{i} < n$
* 图中 不存在平行边
* $0 <= cnt_{i} <= 10^4$
* $0 <= maxMoves <= 10^9$
* $1 <= n <= 3000$

Solution:
### 朴素 Dijkstra

为了方便,我们将原始图边的数量记为 `m`,因此对于原始图而言,点的数量 $3000$,边的数量为 $10000$。

题目要我们求新图上,从 `0` 点出发可到达的点的数量,我们将原图上存在的点称为「原点」,细分边上增加的点称为「细分点」,两类点中可达点的数量即是答案。

在分别考虑如何统计两类点之前,我们需要重新定义一下边的权重:**若原点 `u` 和原点 `v` 的边上存在 `c` 个细分点,我们将原点 `u` 和原点 `v` 之间的边看作是一条权重为 `c + 1` 的无向边(结合题意,`c` 个点存在 `c + 1` 个分段/距离)**。

重新定义边的权重后,因为该图是「稠密图」,我们可以使用「朴素 Dijkstra」来求解最短路,得到 $dist$ 数组,其中 $dist[x] = t$ 含义为从原点 `0` 点出发,到达原点 `x` 的最短距离为 `t`。

> **不了解最短路的同学可以看前置 🧀 : [涵盖所有的「存图方式」与「最短路算法(详尽注释)」](https://mp.weixin.qq.com/s?__biz=MzU4NDE3MTEyMA==&mid=2247488007&idx=1&sn=9d0dcfdf475168d26a5a4bd6fcd3505d)**

随后考虑如何统计答案(可达点的数量),根据统计点的类型分情况讨论:

1. 对于原点:若有 $dist[x] \leq max$ 的话,说明原点 `x` 可达,累加到答案中;

2. 对于细分点:由于所有的细分点都在原图边上,因此我们可以统计所有原图边上有多少细分点可达。
    对于任意一条边 $e(u, v)$ 而言,该边上可达点数量包含「经过原点 `u` 可达」以及「经过原点 `v` 可达」的并集,其中原点 `0` 到达原点 `u` 以及原点 `v` 的距离,我们是已知的。因此经过原点 `u` 可达的数量为 $\max(0, max - dist[u])$,经过原点 `v` 可达的数量为 $\max(0, max - dist[v])$,两者之和与该边上细分点的总数取 `min` 即是这条边可达点的数量。

代码:
```Java
class Solution {
    static int N = 3010, INF = 0x3f3f3f3f;
    static int[][] g = new int[N][N];
    static int[] dist = new int[N];
    static boolean[] vis = new boolean[N];
    public int reachableNodes(int[][] edges, int max, int n) {
        // 建图
        for (int i = 0; i < n; i++) Arrays.fill(g[i], INF);
        for (int[] info : edges) {
            int a = info[0], b = info[1], c = info[2] + 1;
            g[a][b] = g[b][a] = c;
        }
        // 朴素 Dijkstra
        Arrays.fill(dist, INF);
        Arrays.fill(vis, false);
        dist[0] = 0;
        for (int i = 0; i < n; i++) {
            int t = -1;
            for (int j = 0; j < n; j++) {
                if (!vis[j] && (t == -1 || dist[j] < dist[t])) t = j;
            }
            vis[t] = true;
            for (int j = 0; j < n; j++) dist[j] = Math.min(dist[j], dist[t] + g[t][j]);
        }
        // 统计答案
        int ans = 0;
        for (int i = 0; i < n; i++) {
            if (dist[i] <= max) ans++;
        }
        for (int[] info : edges) {
            int a = info[0], b = info[1], c = info[2];
            int c1 = Math.max(0, max - dist[a]), c2 = Math.max(0, max - dist[b]);
            ans += Math.min(c, c1 + c2);
        }
        return ans;
    }
}
```
* 时间复杂度:建图复杂度为 $O(m)$;使用朴素 Dijkstra 求最短路复杂度为 $O(n^2)$;统计答案复杂度为 $O(n + m)$。整体复杂度为 $O(m + n^2)$
* 空间复杂度:$O(n^2)$

---

### SPFA

从数据范围来看,无论是朴素 Dijkstra 还是堆优化版的 Dijkstra 都可以过,复杂度分别为 $O(n^2)$ 和 $O(m\log{n})$。

那 Bellman Ford 类的单源最短路就无法通过了吗?

理论上,无论是 Bellman Ford 还是 SPFA 复杂度均为 $O(n \times m)$,均无法通过本题。但实际上 SPFA 由于使用队列对松弛顺序进行了调整,因此在应对「非菊花图」时均表现良好,复杂度可视为 $O(k \times m)$,近似 $O(m)$。

代码:
```Java
class Solution {
    static int N = 3010, M = 20010, INF = 0x3f3f3f3f, idx = 0;
    static int[] he = new int[N], e = new int[M], ne = new int[M], w = new int[M];
    static int[] dist = new int[N];
    static boolean[] vis = new boolean[N];
    void add(int a, int b, int c) {
        e[idx] = b;
        ne[idx] = he[a];
        w[idx] = c;
        he[a] = idx++;
    }
    public int reachableNodes(int[][] edges, int max, int n) {
        // 建图
        Arrays.fill(he, -1);
        idx = 0;
        for (int[] info : edges) {
            int a = info[0], b = info[1], c = info[2] + 1;
            add(a, b, c); add(b, a, c);
        }
        // SPFA
        Arrays.fill(dist, INF);
        Arrays.fill(vis, false);
        Deque<Integer> d = new ArrayDeque<>();
        d.addLast(0);
        dist[0] = 0;
        vis[0] = true;
        while (!d.isEmpty()) {
            int t = d.pollFirst();
            vis[t] = false;
            for (int i = he[t]; i != -1; i = ne[i]) {
                int j = e[i];
                if (dist[j] > dist[t] + w[i]) {
                    dist[j] = dist[t] + w[i];
                    if (vis[j]) continue;
                    d.addLast(j);
                    vis[j] = true;
                }
            }
        }
        // 统计答案
        int ans = 0;
        for (int i = 0; i < n; i++) {
            if (dist[i] <= max) ans++;
        }
        for (int[] info : edges) {
            int a = info[0], b = info[1], c = info[2];
            int c1 = Math.max(0, max - dist[a]), c2 = Math.max(0, max - dist[b]);
            ans += Math.min(c, c1 + c2);
        }
        return ans;
    }
}
```
* 时间复杂度:建图复杂度为 $O(m)$;使用 SPFA 求最短路复杂度为 $O(n \times m)$;统计答案复杂度为 $O(n + m)$。整体复杂度为 $O(n \times m)$
* 空间复杂度:$O(n + m)$

### 最后

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

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

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

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


883. 三维形体投影面积

Question:
在 $n * n$ 的网格 `grid` 中,我们放置了一些与 `x`,`y`,`z` 三轴对齐的 $1 * 1 * 1$ 立方体。

每个值 $v = grid[i][j]$ 表示 $v$ 个正方体叠放在单元格 $(i, j)$ 上。

现在,我们查看这些立方体在 `xy`、`yz` 和 `zx` 平面上的投影。

投影 就像影子,将 三维 形体映射到一个 二维 平面上。从顶部、前面和侧面看立方体时,我们会看到“影子”。

返回 所有三个投影的总面积 。

示例 1:

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

输出:17

解释:这里有该形体在三个轴对齐平面上的三个投影(“阴影部分”)。
```
示例 2:
```
输入:grid = [[2]]

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

输出:8
```

提示:
* $n == grid.length == grid[i].length$
* $1 <= n <= 50$
* $0 <= grid[i][j] <= 50$

Solution:
### 模拟

根据题意进行模拟即可,使用三个变量分别统计三视图的阴影面积:

* `ans1`:统计俯视图的面积,共有 $n * n$ 个位置需要被统计,当任意格子 $g[i][j] > 0$,阴影面积加一;
* `ans2`:统计左视图的面积,共有 $n$ 行需要被统计,每一行对 `ans2` 的贡献为该行的最大高度;
* `ans3`:统计主视图的面积,共有 $n$ 列需要被统计,每一列对 `ans3` 的贡献为该列的最大高度。

代码:
```Java
class Solution {
    public int projectionArea(int[][] g) {
        int ans1 = 0, ans2 = 0, ans3 = 0;
        int n = g.length;
        for (int i = 0; i < n; i++) {
            int a = 0, b = 0;
            for (int j = 0; j < n; j++) {
                if (g[i][j] > 0) ans1++;
                a = Math.max(a, g[i][j]);
                b = Math.max(b, g[j][i]);
            }
            ans2 += a; ans3 += b;
        }
        return ans1 + ans2 + ans3;
    }
}
```
* 时间复杂度:$O(n^2)$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


884. 两句话中的不常见单词

Question:
句子 是一串由空格分隔的单词。每个 单词 仅由小写字母组成。

如果某个单词在其中一个句子中恰好出现一次,在另一个句子中却 没有出现 ,那么这个单词就是 不常见的 。

给你两个 句子 `s1` 和 `s2` ,返回所有 不常用单词 的列表。返回列表中单词可以按 任意顺序 组织。

示例 1:
```
输入:s1 = "this apple is sweet", s2 = "this apple is sour"

输出:["sweet","sour"]
```
示例 2:
```
输入:s1 = "apple apple", s2 = "banana"

输出:["banana"]
```

提示:
* $1 <= s1.length, s2.length <= 200$
* `s1` 和 `s2` 由小写英文字母和空格组成
* `s1` 和 `s2` 都不含前导或尾随空格
* `s1` 和 `s2` 中的所有单词间均由单个空格分隔

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

根据题意进行模拟即可,先使用「哈希表」对两字符串进行「词频统计」,然后分别从两边的「哈希表」出发,统计符合题意的字符串数量。

或是利用某个单词在一边出现一次,另外一边不曾出现,等价于「某个单词总共只出现一次」。

代码:
```Java
class Solution {
    public String[] uncommonFromSentences(String s1, String s2) {
        Map<String, Integer> map = new HashMap<>();
        String str = s1 + " " + s2;
        String[] ss = str.split(" ");
        for (String s : ss) map.put(s, map.getOrDefault(s, 0) + 1);
        List<String> list = new ArrayList<>();
        for (String s : map.keySet()) if (map.get(s) == 1) list.add(s);
        return list.toArray(new String[list.size()]);
    }
}
```
* 时间复杂度:令 `n` 和 `m` 分别为两字符串长度,复杂度为 $O(n + m)$
* 空间复杂度:$O(n + m)$

### 最后

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

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

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

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


Question:
给定一组 `n` 人(编号为 `1, 2, ..., n`), 我们想把每个人分进任意大小的两组。

每个人都可能不喜欢其他人,那么他们不应该属于同一组。

给定整数 `n` 和数组 `dislikes` ,其中 $dislikes[i] = [a_i, b_i]$ ,表示不允许将编号为 $a_i$ 和  $b_i$ 的人归入同一组。

当可以用这种方法将所有人分进两组时,返回 `true`;否则返回 `false`。

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

输出:true

解释:group1 [1,4], group2 [2,3]
```
示例 2:
```
输入:n = 3, dislikes = [[1,2],[1,3],[2,3]]

输出:false
```
示例 3:
```
输入:n = 5, dislikes = [[1,2],[2,3],[3,4],[4,5],[1,5]]

输出:false
```

提示:
* $1 <= n <= 2000$
* $0 <= dislikes.length <= 10^4$
* $dislikes[i].length = 2$
* $1 <= dislikes[i][j] <= n$
* $a_i < b_i$
* `dislikes` 中每一组都 不同

Solution:
### 染色法

无论是从题目描述和对点边的描述,这都是一道「染色法判定二分图」的模板题。

为了方便,我们令 `dislikes` 为 `ds`,将其长度记为 $m$。

题目要求我们将 $n$ 个点划分到两个集合中,同时我们将每个 $ds[i]$ 看做无向边的话,可知集合内部无边,即所有的边必然横跨两个集合之间。

使用 $ds[i]$ 进行建图,并将两个将要划分出的两个集合分别记为 `A` 和 `B`,我们可以采用「染色」的方式,尝试将所有点进行划分。

构建一个与点数相等的数组 `color`,我们人为规定划分到集合 `A` 的点满足 $color[i] = 1$,划分到集合 `B` 的点满足 $color[i] = 2$,起始有 $color[i] = 0$,代表该点尚未被划分。

随后我们可以实现 `DFS`  函数为 `boolean dfs(int u, int cur)` 含义为尝试将点 `u` 上 `cur` 色。根据定义可知,我们除了需要 `color[u] = cur` 以外,还需要遍历点 `u` 的所有出边(处理其邻点,将其划分到另一集合上),若在处理过程中发生冲突,则返回 `false`,若能顺利染色则返回 `true`。

由于我们固定了颜色编号为 `1` 和 `2`,因此 `cur` 的对立色可统一为 `3 - cur`。

最终,我们根据能否给所有点染色成功来决定答案。

Java 代码:
```Java
class Solution {
    int N = 2010, M = 2 * 10010;
    int[] he = new int[N], e = new int[M], ne = new int[M], color = new int[N];
    int idx;
    void add(int a, int b) {
        e[idx] = b;
        ne[idx] = he[a];
        he[a] = idx++;
    }
    boolean dfs(int u, int cur) {
        color[u] = cur;
        for (int i = he[u]; i != -1; i = ne[i]) {
            int j = e[i];
            if (color[j] == cur) return false;
            if (color[j] == 0 && !dfs(j, 3 - cur)) return false;
        }
        return true;
    }
    public boolean possibleBipartition(int n, int[][] ds) {
        Arrays.fill(he, -1);
        for (int[] info : ds) {
            int a = info[0], b = info[1];
            add(a, b); add(b, a);
        }
        for (int i = 1; i <= n; i++) {
            if (color[i] != 0) continue;
            if (!dfs(i, 1)) return false;
        }
        return true;
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    int he[2010], e[2 * 10010], ne[2 * 10010], color[2010], idx = 0;
    void add(int a, int b) {
        e[idx] = b;
        ne[idx] = he[a];
        he[a] = idx++;
    }
    bool dfs(int u, int cur) {
        color[u] = cur;
        for (int i = he[u]; i != -1; i = ne[i]) {
            int j = e[i];
            if (color[j] == cur) return false;
            if (color[j] == 0 && !dfs(j, 3 - cur)) return false;
        }
        return true;
    }
    bool possibleBipartition(int n, vector<vector<int>>& ds) {
        fill(he, he + n + 10, -1);
        for (const auto& info : ds) {
            int a = info[0], b = info[1];
            add(a, b); add(b, a);
        }
        for (int i = 1; i <= n; i++) {
            if (color[i] != 0) continue;
            if (!dfs(i, 1)) return false;
        }
        return true;
    }
};
```
Python 代码:
```Python
class Solution:
    def possibleBipartition(self, n: int, ds: List[List[int]]) -> bool:
        N, M = 2010, 20010
        he, e, ne, color = [-1] * N, [0] * M, [0] * M, [0] * N
        idx = 0
        def add(a, b):
            nonlocal idx
            e[idx], ne[idx], he[a] = b, he[a], idx
            idx += 1
        def dfs(u, cur):
            color[u] = cur
            i = he[u]
            while i != -1:
                j = e[i]
                if color[j] == cur:
                    return False
                if color[j] == 0 and not dfs(j, 3 - cur):
                    return False
                i = ne[i]
            return True
        for info in ds:
            a, b = info[0], info[1]
            add(a, b)
            add(b, a)
        for i in range(1, n + 1):
            if color[i] != 0:
                continue
            if not dfs(i, 1):
                return False
        return True
```
TypeScript 代码:
```TypeScript
function possibleBipartition(n: number, ds: number[][]): boolean {
    const N = 2010, M = 2 * 10010
    const he = new Array<number>(N).fill(-1), e = new Array<number>(M).fill(0), ne = new Array<number>(M).fill(0), color = new Array<number>(N).fill(0)
    let idx = 0
    function add(a: number, b: number): void {
        e[idx] = b
        ne[idx] = he[a]
        he[a] = idx++
    }
    function dfs(u: number, cur: number): boolean {
        color[u] = cur
        for (let i = he[u]; i != -1; i = ne[i]) {
            const j = e[i];
            if (color[j] == cur) return false
            if (color[j] == 0 && !dfs(j, 3 - cur)) return false
        }
        return true
    }
    for (const info of ds) {
        const a = info[0], b = info[1]
        add(a, b); add(b, a)
    }
    for (let i = 1; i <= n; i++) {
        if (color[i] != 0) continue
        if (!dfs(i, 1)) return false
    }
    return true
}
```
* 时间复杂度:$O(n + m)$
* 空间复杂度:$O(n + m)$

---

### 反向点 + 并查集

我们知道对于 $ds[i] = (a, b)$ 而言,点 `a` 和点 `b` 必然位于不同的集合中,同时由于只有两个候选集合,因此这样的关系具有推断性:即对于 $(a, b)$ 和 $(b, c)$ 可知 `a` 和 `c` 位于同一集合。

因此,我们可以对于每个点 `x` 而言,建议一个反向点 `x + n`:若点 `x` 位于集合 `A` 则其反向点 `x + n` 位于集合 `B`,反之同理。

基于此,我们可以使用「并查集」维护所有点的连通性:边维护变检查每个 $ds[i]$ 的联通关系,若 $ds[i] = (a, b)$ 联通,必然是其反向点联通所导致,必然是此前的其他 $ds[j]$ 导致的关系冲突,必然不能顺利划分成两个集合,返回 `false`,否则返回 `true`。

Java 代码:
```Java
class Solution {
    int[] p = new int[4010];
    int find(int x) {
        if (p[x] != x) p[x] = find(p[x]);
        return p[x];
    }
    void union(int a, int b) {
        p[find(a)] = p[find(b)];
    }
    boolean query(int a, int b) {
        return find(a) == find(b);
    }
    public boolean possibleBipartition(int n, int[][] ds) {
        for (int i = 1; i <= 2 * n; i++) p[i] = i;
        for (int[] info : ds) {
            int a = info[0], b = info[1];
            if (query(a, b)) return false;
            union(a, b + n); union(b, a + n);
        }
        return true;
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    vector<int> p;
    int find(int x) {
        if (p[x] != x) p[x] = find(p[x]);
        return p[x];
    }
    void unionp(int a, int b) {
        p[find(a)] = p[find(b)];
    }
    bool query(int a, int b) {
        return find(a) == find(b);
    }
    bool possibleBipartition(int n, vector<vector<int>>& ds) {
        p.resize(2 * n + 1);
        for (int i = 1; i <= 2 * n; ++i) p[i] = i;
        for (const auto& info : ds) {
            int a = info[0], b = info[1];
            if (query(a, b)) return false;
            unionp(a, b + n);
            unionp(b, a + n);
        }
        return true;
    }
};
```
Python 代码:
```Python
class Solution:
    def possibleBipartition(self, n: int, ds: List[List[int]]) -> bool:
        p = [i for i in range(0, 2 * n + 10)]
        def find(x):
            if p[x] != x:
                p[x] = find(p[x])
            return p[x]
        def union(a, b):
            p[find(a)] = p[find(b)]
        def query(a, b):
            return find(a) == find(b)
        for info in ds:
            a, b = info[0], info[1]
            if query(a, b):
                return False
            else:
                union(a, b + n)
                union(b, a + n)
        return True
```
TypeScript 代码:
```TypeScript
function possibleBipartition(n: number, ds: number[][]): boolean {
    const p = new Array<number>(4010).fill(0)
    function find(x: number): number {
        if (p[x] != x) p[x] = find(p[x])
        return p[x]
    }
    function union(a: number, b: number): void {
        p[find(a)] = p[find(b)]
    }
    function query(a: number, b: number): boolean {
        return find(a) == find(b)
    }
    for (let i = 1; i <= 2 * n; i++) p[i] = i
    for (const info of ds) {
        const a = info[0], b = info[1]
        if (query(a, b)) return false
        union(a, b + n); union(b, a + n)
    }
    return true
}
```
* 时间复杂度:$O(n + m)$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


888. 公平的糖果棒交换

Question:
爱丽丝和鲍勃有不同大小的糖果棒:A[i] 是爱丽丝拥有的第 i 根糖果棒的大小,B[j] 是鲍勃拥有的第 j 根糖果棒的大小。

因为他们是朋友,所以他们想交换一根糖果棒,这样交换后,他们都有相同的糖果总量。(一个人拥有的糖果总量是他们拥有的糖果棒大小的总和。)

返回一个整数数组 ans,其中 ans[0] 是爱丽丝必须交换的糖果棒的大小,ans[1] 是 Bob 必须交换的糖果棒的大小。

如果有多个答案,你可以返回其中任何一个。

保证答案存在。

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

输出:[1,2]
```
示例 2:
```
输入:A = [1,2], B = [2,3]

输出:[1,2]
```
示例 3:
```
输入:A = [2], B = [1,3]

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

输出:[5,4]
```

提示:
* 1 <= A.length <= 10000
* 1 <= B.length <= 10000
* 1 <= A[i] <= 100000
* 1 <= B[i] <= 100000
* 保证爱丽丝与鲍勃的糖果总量不同。
* 答案肯定存在。

Solution:
### 朴素解法

最终目的是让两个数组总和相等。

我们可以先分别求得两个数组总和为 $aSum$ 和 $bSum$。

即有数组总和 $total = aSum + bSum$。

同时得数组目标总和 $target = total / 2$。

当前两个数组与目标总和的差值分别为 $target - aSum$ 和 $target - bSum$。

我们记 $diff = target - aSum$。

对于某个 $a[i]$ 而言,如果 $a[i]$ 能构成答案,那么 `b` 数组中必然存在大小为 $a[i] + diff$ 的值,使得两者交换后,数组总和均为 $target$。

因此我们只需要遍历数组 `a`,查找哪一个 $a[i]$ 使得 $a[i] + diff$ 存在于数组 `b` 即可。

代码:
```Java
class Solution {
    public int[] fairCandySwap(int[] a, int[] b) {
        int aSum = 0, bSum = 0;
        for (int i : a) aSum += i;
        for (int i : b) bSum += i;
        int total = aSum + bSum, target = total / 2;
        int diff = target - aSum;
        int[] ans = new int[2];
        for (int i : a) {
            if (find(b, i + diff)) {
                ans[0] = i;
                ans[1] = i + diff;
            }
        }
        return ans;
    }
    boolean find(int[] nums, int target) {
        for (int i : nums) {
            if (i == target) return true;
        }
        return false;
    }
}
```

* 时间复杂度: 计算总和复杂度为 $O(n)$,找到最终解复杂度为 $O(n^2)$。整体复杂度为 $O(n^2)$
* 空间复杂度:$O(1)$

---

### 查找优化

上述解法之所以无法做到线性,是因为我们每次都要对数组 `b` 进行扫描,确定 $a[i] + diff$ 是否存在。

我们知道 map/set/数组 都可以实现 $O(1)$ 查找,由于这里明确给出了两个数组中出现的数的范围,因此可以使用数组进行计数。

同时可以优化一下变量的使用,使用一个变量 $diff$ 来计算最终的差异值。

这种优化,是典型的空间换时间做法。

代码:
```Java
class Solution {
    public int[] fairCandySwap(int[] a, int[] b) {
        // 先求得 a 的总和
        int diff = 0;
        for (int i : a) diff += i;

        // 使用 cnt 统计 b 中的数的出现次数,同时计算 a 总和与 b 总和的差值
        int[] cnt = new int[100009];
        for (int i : b) {
            diff -= i;
            cnt[i]++;
        }

        // 计算出 a 中具体的替换差值是多少
        diff /= -2;
        int[] ans = new int[2];
        for (int i : a) {
            int target = i + diff;
            // 如果目标替换量在合法范围,并且存在于 b 数组中。说明找到解了
            if (target >= 1 && target <= 100000 && cnt[target] > 0) {
                ans[0] = i;
                ans[1] = target;
                break;
            }
        }
        return ans;
    }
}
```
* 时间复杂度:计算总和复杂度为 $O(n)$,找到最终解复杂度为 $O(n)$。整体复杂度为 $O(n)$
* 空间复杂度:使用 `cnt` 数组进行计数。复杂度为 $O(n)$

### 最后

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

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

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

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


890. 查找和替换模式

Question:
你有一个单词列表 `words` 和一个模式  `pattern`,你想知道 `words` 中的哪些单词与模式匹配。

如果存在字母的排列 `p` ,使得将模式中的每个字母 `x` 替换为 `p(x)` 之后,我们就得到了所需的单词,那么单词与模式是匹配的。

(回想一下,字母的排列是从字母到字母的双射:每个字母映射到另一个字母,没有两个字母映射到同一个字母。)

返回 `words` 中与给定模式匹配的单词列表。

你可以按任何顺序返回答案。

示例:
```
输入:words = ["abc","deq","mee","aqq","dkd","ccc"], pattern = "abb"

输出:["mee","aqq"]

解释:
"mee" 与模式匹配,因为存在排列 {a -> m, b -> e, ...}。
"ccc" 与模式不匹配,因为 {a -> c, b -> c, ...} 不是排列。
因为 a 和 b 映射到同一个字母。
```

提示:
* $1 <= words.length <= 50$
* $1 <= pattern.length = words[i].length <= 20$

Solution:
### 哈希表 模拟

根据题意进行模拟即可,使用 `map` 记录具体的映射关系,使用 `vis` 记录哪些字符已被映射,利用字符集大小只有 $26$,我们可以使用数组充当哈希表。

代码:
```Java
class Solution {
    public List<String> findAndReplacePattern(String[] ws, String pe) {
        List<String> ans = new ArrayList<>();
        int[] map = new int[26], vis = new int[26];
        for (String s : ws) {
            Arrays.fill(map, -1);
            Arrays.fill(vis, 0);
            boolean ok = true;
            for (int i = 0; i < pe.length() && ok; i++) {
                int c1 = s.charAt(i) - 'a', c2 = pe.charAt(i) - 'a';
                if (map[c1] == -1 && vis[c2] == 0) {
                    map[c1] = c2; vis[c2] = 1;
                } else if (map[c1] != c2) ok = false;
            }
            if (ok) ans.add(s);
        }
        return ans;
    }
}
```
* 时间复杂度:$O(\sum_{i = 0}^{n}len(ws[i]) + n \times C)$,其中 $C = 26$ 代表字符集大小
* 空间复杂度:$O(C)$

### 最后

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

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

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

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


891. 子序列宽度之和

Question:
一个序列的 宽度 定义为该序列中最大元素和最小元素的差值。

给你一个整数数组 `nums`,返回 `nums` 的所有非空 子序列 的 宽度之和 。由于答案可能非常大,请返回对 $10^9 + 7$ 取余 后的结果。

子序列 定义为从一个数组里删除一些(或者不删除)元素,但不改变剩下元素的顺序得到的数组。例如,`[3,6,2,7]` 就是数组 `[0,3,1,6,2,2,7]` 的一个子序列。

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

输出:6

解释:子序列为 [1], [2], [3], [2,1], [2,3], [1,3], [2,1,3] 。
相应的宽度是 0, 0, 0, 1, 1, 2, 2 。
宽度之和是 6 。
```
示例 2:
```
输入:nums = [2]

输出:0
```

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

Solution:
### 数学

#### 提示一:每个子序列对答案的贡献

对于某个子序列而言,若其最大值为 $a$,最小值为 $b$,则该子序列对答案的贡献为 $(a - b)$。

我们有若干个子序列,即有若干个 $(a - b)$,答案为所有 $(a - b)$ 之和,我们称一个 $(a - b)$ 为 `item`。

#### 提示二:每个 $nums[i]$ 参与了多少个 `item` 的组成,在最终展开式中又是如何

对于每个 $(a - b)$ 而言,`a` 和 `b` 均必然是具体的 $nums[i]$。

同时易知若 $nums[i]$ 作为了 $k$ 个子序列的最小值,那么在最终表达式展开中,必然有 $k$ 个 $-nums[i]$;同理若 $nums[i]$ 作为了 $k$ 个子序列的最大值,那么在最终表达式展开中,必然有 $k$ 个 $nums[i]$。

#### 提示三:统计每个 $nums[i]$ 作为最值时,有多少个子序列

先不考虑 $nums[i]$ 的重复问题。

若 $nums[i]$ 作为子序列最小值时,首先 $nums[i]$ 必选,小于 $nums[i]$ 的必不选,而大于 $nums[i]$ 的可选可不选,组合个数取决于大于 $nums[i]$ 的数的个数,假设有 $k$ 个,那么根据组合数原理,共有 $2^k$ 个组合,即共有 $2^k$ 个子序列。此时 $nums[i]$ 对答案的贡献为 $2^k \times (-nums[i])$。

同理,$nums[i]$ 作为子序列最大值时,子序列个数取决于小于 $nums[i]$ 的数的个数,假设有 $k$ 个,此时 $nums[i]$ 对答案的贡献为 $2^k \times nums[i]$。

#### 提示四:如何快速得知比 $nums[i]$ 大/小 的数的个数

排序。

#### 提示五:$nums[i]$ 的重复问题

无论是将 $nums[i]$ 视作最大值还是最小值,我们的组合数均取决于某一侧的数的个数,因此不会答案正确性产生影响。

#### 提示六:$2^k$ 操作的重复计算问题

将 $nums[i]$ 视作最值,我们都需要统计两边数所产生的组合数个数,因此即使对于单个用例都会面临重复计算某个 $2^k$ 的问题(对称性)。

同时对于跨样例而言,我们仍会重复计算某些 $2^k$(尤其是较小的 $k$ 值),为避免重复计算,我们可以通过打表预处理的方式算得所有可能要用到 $2^k$ 结果,在使用的时候直接通过查表取得。

Java 代码:
```Java
class Solution {
    static int N = 100010, MOD = (int)1e9+7;
    static long[] p = new long[N];
    static {
        p[0] = 1;
        for (int i = 1; i < N; i++) p[i] = p[i - 1] * 2 % MOD;
    }
    public int sumSubseqWidths(int[] nums) {
        int n = nums.length;
        long ans = 0;
        Arrays.sort(nums);
        for (int i = 0; i < n; i++) {
            ans += (p[i] * nums[i]) % MOD;
            ans %= MOD;
            ans -= (p[n - i - 1] * nums[i]) % MOD;
            ans %= MOD;
        }
        return (int) ans;
    }
}
```
TypeScript 代码:
```TypeScript
function sumSubseqWidths(nums: number[]): number {
    let n = nums.length, mod = 1000000007, ans = 0
    const p = new Array<number>(n + 10).fill(1)
    for (let i = 1; i <= n; i++) p[i] = p[i - 1] * 2 % mod
    nums.sort((a,b)=>a-b)
    for (let i = 0; i < n; i++) {
        ans += p[i] * nums[i] % mod
        ans %= mod
        ans -= p[n - i - 1] * nums[i] % mod
        ans %= mod
    }
    return ans
}
```
Python3 代码:
```Python3
class Solution:
    def sumSubseqWidths(self, nums: List[int]) -> int:
        n, mod, ans = len(nums), 1000000007, 0
        p = [1] * (n + 10)
        for i in range(1, n + 1):
            p[i] = p[i - 1] * 2 % mod
        nums.sort()
        for i in range(n):
            ans = ans + p[i] * nums[i] % mod
            ans = ans - p[n - i - 1] * nums[i] % mod
        return ans % mod
```
* 时间复杂度:排序复杂度为 $O(n\log{n})$;统计答案复杂度为 $O(n)$。整体复杂度为 $O(n\log{n})$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


Question:
设计一个类似堆栈的数据结构,将元素推入堆栈,并从堆栈中弹出出现频率最高的元素。

实现 `FreqStack` 类:
* `FreqStack()` 构造一个空的堆栈。
* `void push(int val)` 将一个整数 `val` 压入栈顶。
* `int pop()` 删除并返回堆栈中出现频率最高的元素。

如果出现频率最高的元素不只一个,则移除并返回最接近栈顶的元素。

示例 1:
```
输入:
["FreqStack","push","push","push","push","push","push","pop","pop","pop","pop"],
[[],[5],[7],[5],[7],[4],[5],[],[],[],[]]

输出:[null,null,null,null,null,null,null,5,7,5,4]

解释:
FreqStack = new FreqStack();
freqStack.push (5);//堆栈为 [5]
freqStack.push (7);//堆栈是 [5,7]
freqStack.push (5);//堆栈是 [5,7,5]
freqStack.push (7);//堆栈是 [5,7,5,7]
freqStack.push (4);//堆栈是 [5,7,5,7,4]
freqStack.push (5);//堆栈是 [5,7,5,7,4,5]
freqStack.pop ();//返回 5 ,因为 5 出现频率最高。堆栈变成 [5,7,5,7,4]。
freqStack.pop ();//返回 7 ,因为 5 和 7 出现频率最高,但7最接近顶部。堆栈变成 [5,7,5,4]。
freqStack.pop ();//返回 5 ,因为 5 出现频率最高。堆栈变成 [5,7,4]。
freqStack.pop ();//返回 4 ,因为 4, 5 和 7 出现频率最高,但 4 是最接近顶部的。堆栈变成 [5,7]。
```

提示:
* $0 <= val <= 10^9$
* `push` 和 `pop` 的操作数不大于 $2 \times 10^4$
* 输入保证在调用 `pop` 之前堆栈中至少有一个元素

Solution:
### 哈希表

这是一道很纯的哈希表题儿。

首先,我们容易想到建立 **第一个哈希表 `cnts` 用于记录某个数值的出现次数,`cnts[val] = c` 含义为数值 `val` 当前在栈中的出现次数为 `c`。我们称该哈希表为「计数哈希表」**。

再结合每次 `pop` 需要返回「频率最大的元素,若有多个则返回最考虑栈顶的一个」的要求,我们还可以 **建立第二个哈希 `map`,该哈希表以「出现次数 `c`」为键,以「出现次数均为 `c` 的元素序列」为值,`map[c] = A = [...]` 含义为出现次数为 `c` 的序列为 `A`,并且序列 `A` 中的结尾元素为出现次数为 `c` 的所有元素中最靠近栈顶的元素。我们称该哈希表为「分桶哈希表」**。

最后再额外使用一个变量 `max` 记录当前最大出现频数,不难发现,`max` 必然是以步长 $\pm 1$ 进行变化(当出现次数为 `max` 的元素被 `pop` 掉了一个后,必然剩下 `max - 1` 个),因此当我们在某次 `pop` 操作后发现出现次数为 `max` 的集合为空时,对 `max` 进行自减操作即可。

将题目给的样例作为 🌰 ,大家可以看看 `cnts`、`map` 和 `max` 三者如何变化,以及 `pop` 的更新逻辑:

Java 代码:
```Java
class FreqStack {
    Map<Integer, List<Integer>> map = new HashMap<>();
    Map<Integer, Integer> cnts = new HashMap<>();
    int max;
    public void push(int val) {
        cnts.put(val, cnts.getOrDefault(val, 0) + 1);
        int c = cnts.get(val);
        List<Integer> list = map.getOrDefault(c, new ArrayList<>());
        list.add(val);
        map.put(c, list);
        max = Math.max(max, c);
    }
    public int pop() {
        List<Integer> list = map.get(max);
        int ans = list.remove(list.size() - 1);
        cnts.put(ans, cnts.get(ans) - 1);
        if (list.size() == 0) max--;
        return ans;
    }
}
```
C++ 代码:
```C++
class FreqStack {
public:
    unordered_map<int, int> freq;
    unordered_map<int, vector<int>> m;
    int maxv = 0;

    void push(int val) {
        maxv = max(maxv, ++freq[val]);
        m[freq[val]].push_back(val);
    }

    int pop() {
        int x = m[maxv].back();
        m[maxv].pop_back();
        if (m[freq[x]--].empty()) maxv--;
        return x;
    }
};
```
Python 代码:
```Python
class FreqStack:
    def __init__(self):
        self.cnts = defaultdict(int)
        self.map = defaultdict(list)
        self.mv = 0

    def push(self, val: int) -> None:
        self.cnts[val] += 1
        c = self.cnts[val]
        self.map[c].append(val)
        self.mv = max(self.mv, c)

    def pop(self) -> int:
        ans = self.map[self.mv].pop()
        self.cnts[ans] -= 1
        self.mv -= 0 if self.map[self.mv] else 1
        return ans
```
TypeScript 代码:
```TypeScript
class FreqStack {
    map: Map<number, Array<number>> = new Map<number, Array<number>>()
    cnst: Map<number, number> = new Map<number, number>()
    max: number = 0
    push(val: number): void {
        if (!this.cnst.has(val)) this.cnst.set(val, 0)
        this.cnst.set(val, this.cnst.get(val) + 1)
        const c = this.cnst.get(val)
        if (!this.map.has(c)) this.map.set(c, new Array<number>())
        this.map.get(c).push(val)
        this.max = Math.max(this.max, c)
    }
    pop(): number {
        const ans = this.map.get(this.max).pop()
        if (this.map.get(this.max).length == 0) this.max--
        this.cnst.set(ans, this.cnst.get(ans) - 1)
        return ans
    }
}
```
* 时间复杂度:所有操作均为 $O(1)$
* 空间复杂度:所有入栈的节点最多会被存储两次,一次在计数哈希表中,一次在分桶哈希表中,复杂度为 $O(n)$

### 最后

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

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

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

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


Question:
如果数组是单调递增或单调递减的,那么它是单调的。

如果对于所有 i <= j,A[i] <= A[j],那么数组 A 是单调递增的。 如果对于所有 i <= j,A[i]> = A[j],那么数组 A 是单调递减的。

当给定的数组 A 是单调数组时返回 true,否则返回 false。

示例 1:
```
输入:[1,2,2,3]
输出:true
```
示例 2:
```
输入:[6,5,4,4]
输出:true
```
示例 3:
```
输入:[1,3,2]
输出:false
```
示例 4:
```
输入:[1,2,4,5]
输出:true
```
示例 5:
```
输入:[1,1,1]
输出:true
```

提示:
* 1 <= A.length <= 50000
* -100000 <= A[i] <= 100000

Solution:
### 朴素解法(所谓的两次遍历)

两次遍历,分别检查是否为单调递增和单调递减。

```java []
class Solution {
    public boolean isMonotonic(int[] a) {
        return check(a, true) || check(a, false);
    }
    boolean check(int[] a, boolean flag) {
        for (int i = 0; i < a.length - 1; i++) {
            if (flag) {
                if (a[i] > a[i + 1]) return false;
            } else {
                if (a[i] < a[i + 1]) return false;
            }
        }
        return true;
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(1)$

***

### 朴素解法(所谓的一次遍历)

一次遍历。

同时为了防止扫完整个数组,增加一个提前 return 的逻辑:

```java
class Solution {
    public boolean isMonotonic(int[] a) {
        boolean up = true, down = true;
        for (int i = 0; i < a.length - 1; i++) {
            if (a[i] > a[i + 1]) up = false;
            if (a[i] < a[i + 1]) down = false;
            if (!up && !down) return false;
        }
        return up || down;
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(1)$

***

### 总结

事实上,上述两种解法其实并**不应该区分为「一次遍历」与「两次遍历」**。

**我们应该用「对数组的访问次数」来定义遍历多少次,而不是「利用 for 循环的个数」来定义。上述无论那种方法,对数组访问次数都是一样的。**

而在不对解法二进行剪枝的情况下,要比解法一慢。主要是因为解法一明确了是「递增」还是「递减」之后,在循环内部做了剪枝。

当我们对解法二进行同样的内部剪枝之后,其实和解法一应该是类似的。

前三次提交是保留 `if (!up && !down) return false;` 的提交 (1 ms)
后三次提交是不保留 `if (!up && !down) return false;` 的提交记录 (2 ms)

简单题,大家就看个乐呵 ~

***

### 有趣的实验

评论区有位同学提出了一个很有意思的问题:如果数据量很大,大到内存都无法一次完全读入,那么一个循环里两次重复读应该比两次循环要快得多了吧?

我理解 ta 的意思是,每次读取值都算一次 IO 成本的话,一个循环里两次重复读的的成本应该是要**小于**比两次循环的成本吧?

因此有了以下的测试代码:

```java
class Solution {
    // 统计「二次循环」的访问次数
    int cnt;
    public boolean isMonotonic(int[] a) {
        cnt = 0;
        // 这里不直接写成「短路与」进行返回,确保两个循环都会被执行
        boolean t = check(a, true), u = check(a, false);
        System.out.println(cnt);
        return t || u;
    }
    boolean check(int[] a, boolean flag) {
        for (int i = 0; i < a.length - 1; i++) {
            if (flag) {
                if (getVal(a, i) > getVal(a, i + 1)) return false;
            } else {
                if (getVal(a, i) < getVal(a, i + 1)) return false;
            }
        }
        return true;
    }
    int getVal(int[] a, int idx) {
        cnt++;
        return a[idx];
    }
}
```
对于样例数据的输出:8 8 6 8 8

```java
class Solution {
    // 统计「一次循环」的访问次数
    int cnt;
    public boolean isMonotonic(int[] a) {
        cnt = 0;
        boolean up = true, down = true;
        for (int i = 0; i < a.length - 1; i++) {
            if (getVal(a, i) > getVal(a, i + 1)) up = false;
            if (getVal(a, i) < getVal(a, i + 1)) down = false;
            if (!up && !down) break;
        }
        System.out.println(cnt);
        return up || down;
    }
    int getVal(int[] a, int idx) {
        cnt++;
        return a[idx];
    }
}
```
对于样例数据的输出:12 12 8 12 8

**结论:二次循环的剪枝效果应该是要比一次循环要更好点(更加直接)。如果还有人坚持「所谓的一次循环」要优于「所谓的二次循环」,实验代码就是最好的证明。**

### 最后

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

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

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

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


897. 递增顺序搜索树

Question:
给你一棵二叉搜索树,请你 按中序遍历 将其重新排列为一棵递增顺序搜索树,使树中最左边的节点成为树的根节点,并且每个节点没有左子节点,只有一个右子节点。

示例 1:

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

输出:[1,null,2,null,3,null,4,null,5,null,6,null,7,null,8,null,9]
```
示例 2:

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

输出:[1,null,5,null,7]
```

提示:
* 树中节点数的取值范围是 [1, 100]
* 0 <= Node.val <= 1000

Solution:
### 基本思路

由于给定的树是一棵「二叉搜索树」,因此只要对其进行「中序遍历」即可得到有序列表,再根据有序列表构建答案即可。

而二叉搜索树的「中序遍历」有「迭代」和「递归」两种形式。

---

### 递归

递归写法十分简单,属于树的遍历中最简单的实现方式。

代码:
```Java
class Solution {
    List<TreeNode> list = new ArrayList<>();
    public TreeNode increasingBST(TreeNode root) {
        dfs(root);
        TreeNode dummy = new TreeNode(-1);
        TreeNode cur = dummy;
        for (TreeNode node : list) {
            cur.right = node;
            node.left = null;
            cur = node;
        }
        return dummy.right;
    }
    void dfs(TreeNode root) {
        if (root == null) return ;
        dfs(root.left);
        list.add(root);
        dfs(root.right);
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n)$

---

### 迭代

迭代其实就是使用「栈」来模拟递归过程,也属于树的遍历中的常见实现形式。

一般简单的面试中如果问到树的遍历,面试官都不会对「递归」解法感到满意,因此掌握「迭代/非递归」写法同样重要。

代码:
```Java
class Solution {
    List<TreeNode> list = new ArrayList<>();
    public TreeNode increasingBST(TreeNode root) {
        Deque<TreeNode> d = new ArrayDeque<>();
        while (root != null || !d.isEmpty()) {
            while (root != null) {
                d.add(root);
                root = root.left;
            }
            root = d.pollLast();
            list.add(root);
            root = root.right;
        }
        TreeNode dummy = new TreeNode(-1);
        TreeNode cur = dummy;
        for (TreeNode node : list) {
            cur.right = node;
            node.left = null;
            cur = node;
        }
        return dummy.right;
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


Question:
给定一个字符串 `s` 和一个整数 `k` 。你可以从 `s` 的前 `k` 个字母中选择一个,并把它加到字符串的末尾。

返回 在应用上述步骤的任意数量的移动后,字典上最小的字符串 。

示例 1:
```
输入:s = "cba", k = 1

输出:"acb"

解释:
在第一步中,我们将第一个字符(“c”)移动到最后,获得字符串 “bac”。
在第二步中,我们将第一个字符(“b”)移动到最后,获得最终结果 “acb”。
```
示例 2:
```
输入:s = "baaca", k = 3

输出:"aaabc"

解释:
在第一步中,我们将第一个字符(“b”)移动到最后,获得字符串 “aacab”。
在第二步中,我们将第三个字符(“c”)移动到最后,获得最终结果 “aaabc”。
```

提示:
* $1 <= k <= S.length <= 1000$
* `s` 只由小写字母组成。

Solution:
### 最小表示法

当 $k > 1$ 时,我们能够构造出任意的字符串方案,因此当 $k > 1$ 时,我们可以直接通过对字符串排序来得到答案,复杂度为 $O(n\log{n})$。

当 $k = 1$ 时,我们共有 $n$ 种候选方案(将字符串 `s` 看作一个首尾相接的循环字符串,共有 $n$ 个起点可枚举),枚举过程中需要与当前最优的方案进行比较,比较复杂度为 $O(n)$,因此整体复杂度为 $O(n^2)$。

上述的做法已经可以通过本题,可以看出瓶颈在于对 $k = 1$ 的处理。

而实际上,对于给定字符串 `s`,求其循环同构的所有方案中字典序最小的方案,可以使用「最小表示法」来做,复杂度为 $O(n)$。

最小表示法将「方案比较」与「构造更优方案」进行结合:假设我们当前有两字符串 `a` 和 `b` 需要进行比较,其均为原串 `s` 的循环同构具体方案。假设 `a` 和 `b` 分别对应了原串下标为 `i` 和 `j` 的具体方案,且假设两字符串前 $k$ 个字符均相同。

当两字符串第一个不同的字符大小关系为 $cs[i + k] > cs[j + k]$ 时,可以发现在下标范围 $idx \in [i, i + k]$ 作为起点的新方案 `a'` 必然不会是最优方案,即必然存在下标范围 $idx - i + j$ 作为起点的新方案 `b'` 比其更优,因此我们可以直接从 $i + k + 1$ 位置构造新的更优方案,并与 `b` 再次比较。而 $cs[i + k] < cs[j + k]$ 的分析同理。

> 更为直白的表述为:分别从 `i` 和 `j` 作为起点的字符串 `a` 和 `b`,其前 $k$ 个字符相同,而当 $cs[i + k] > cs[j + k]$ 时,我们可以明确「以 $i + p$ 为起点的字符串」必不可能比「以 $j + p$ 为起点的字符串」更优,其中 $p \in [0, k]$。

Java 代码:
```Java
class Solution {
    public String orderlyQueue(String s, int _k) {
        char[] cs = s.toCharArray();
        if (_k == 1) {
            int i = 0, j = 1, k = 0, n = cs.length;
            while (i < n && j < n && k < n) {
                char a = cs[(i + k) % n], b = cs[(j + k) % n];
                if (a == b) k++;
                else {
                    if (a > b) i += k + 1;
                    else j += k + 1;
                    if (i == j) i++;
                    k = 0;
                }
            }
            i = Math.min(i, j);
            return s.substring(i) + s.substring(0, i);
        } else {
            Arrays.sort(cs);
            return String.valueOf(cs);
        }
    }
}
```
TypeScript 代码:
```TypeScript
function orderlyQueue(s: string, _k: number): string {
    if (_k == 1) {
        let i = 0, j = 1, k = 0, n = s.length
        while (i < n && j < n && k < n) {
            const a = s[(i + k) % n], b = s[(j + k) % n]
            if (a == b) k++;
            else {
                if (a > b) i += k + 1
                else j += k + 1
                if (i == j) i++
                k = 0
            }
        }
        i = Math.min(i, j)
        return s.substring(i) + s.substring(0, i)
    } else {
        return [...s].sort().join('');
    }
};
```
* 时间复杂度:当 $k = 1$ 时,复杂度为 $O(n)$;当 $k > 1$ 时,复杂度为 $O(n\log{n})$
* 空间复杂度:当 $k > 1$ 时,需要使用额外的排序空间 $O(\log{n})$

### 最后

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

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

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

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