LeetCode/1771-1780/1780. 判断一个数字是否可以表示成三的幂的和（中等）.md

1780. 判断一个数字是否可以表示成三的幂的和

Question:
给你一个整数 `n`,如果你可以将 `n` 表示成若干个不同的三的幂之和,请你返回 `true`,否则请返回 `false`。

对于一个整数 `y`,如果存在整数 `x` 满足 $y = 3^x$,我们称这个整数 `y` 是三的幂。

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

输出:true

解释:12 = 31 + 32
```
示例 2:
```
输入:n = 91

输出:true

解释:91 = 30 + 32 + 34
```
示例 3:
```
输入:n = 21

输出:false
```

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

Solution:
### 数学

这是一道考察「进制转换」基本认识的题目。

将 `n` 看作一个三进制数,例如对于 $(210)_{3}$ 而言,其代表的是 $2 \times 3^2 + 1 \times 3^1 + 0 \times 3^0$ 十进制数。

由于题目规定组成和的三的幂需要满足「不同」的条件,因此 `n` 所代表的三进制表示中的系数只能是 $1$ 或 $0$,而不是能是 $2$。

Java 代码:
```Java
class Solution {
    public boolean checkPowersOfThree(int n) {
        while (n != 0) {
            if (n % 3 == 2) return false;
            n /= 3;
        }
        return true;
    }
}
```
C++ 代码
```C++
class Solution {
public:
    bool checkPowersOfThree(int n) {
        while (n != 0) {
            if (n % 3 == 2) return false;
            n /= 3;
        }
        return true;
    }
};
```
Python 代码
```Python
class Solution:
    def checkPowersOfThree(self, n: int) -> bool:
        while n != 0:
            if n % 3 == 2:
                return False
            n //= 3
        return True
```
TypeScript  代码:
```TypeScript
function checkPowersOfThree(n: number): boolean {
    while (n != 0) {
        if (n % 3 == 2) return false
        n = Math.floor(n / 3)
    }
    return true
}
```
* 时间复杂度:$O(\log{n})$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


LeetCode/1781-1790/1781. 所有子字符串美丽值之和（中等）.md

1781. 所有子字符串美丽值之和

Question:
一个字符串的 美丽值 定义为:出现频率最高字符与出现频率最低字符的出现次数之差。

比方说,`"abaacc"` 的美丽值为 `3 - 1 = 2`。

给你一个字符串 `s` ,请你返回它所有子字符串的 美丽值 之和。

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

输出:5

解释:美丽值不为零的字符串包括 ["aab","aabc","aabcb","abcb","bcb"] ,每一个字符串的美丽值都为 1 。
```
示例 2:
```
输入:s = "aabcbaa"

输出:17
```

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

Solution:
### 模拟 + 哈希表

数据范围只有 $500$,我们可以通过两层循环的方式枚举所有子串,当枚举子串左端点 `i` 的时候,可以同步开一个大小为 $C = 26$ 的数组来记录每个字母的出现次数,随后通过遍历该数组来得知最大和最小频次,将当前子串对应的美丽值累加到答案。

该做法复杂度为 $O(n^2 \times C)$,计算量约为 $500 \times 500 \times 26 = 6.5 \times 10^6$,可以过。

在确定了子串的左端点 `i`,枚举右端点 `j` 的过程中,维护最大频次是简单的,关键在于如果知晓最小频次,我们可以额外起一个哈希表 `map` 来记录出现频次为 `x` 的字符有多少个,`map[x] = cnt` 含义为频次为 `x` 的字符类型有 `cnt` 种。

假设当前我们处理的字符为 `c`,根据字符 `c` 原来的频次进行分情况讨论(使用 `max` 和 `min` 分别记录当前最大最小频次):

* 若字符 `c` 为首次出现,即原频次为 $0$,此时有最小频次 `min = 1`
* 当字符 `c` 为并非首次出现,假设原频次数为 `x`,此时频次为 `x` 的字符数量减一;频次为 `x + 1` 的字符数量加一,若频次为 `x` 的字符数量在减一后 $map[min] \leq 0$,说明没有频次为 `min` 的字符了,此时最小频次为 `min + 1`

Java 代码:
```Java
class Solution {
    public int beautySum(String s) {
        int n = s.length(), ans = 0;
        for (int i = 0; i < n; i++) {
            int[] cnts = new int[26];
            Map<Integer, Integer> map = new HashMap<>();
            int min = -1, max = -1;
            for (int j = i; j < n; j++) {
                int c = s.charAt(j) - 'a';
                map.put(cnts[c], map.getOrDefault(cnts[c], 0) - 1);
                map.put(cnts[c] + 1, map.getOrDefault(cnts[c] + 1, 0) + 1);
                cnts[c]++;
                if (cnts[c] == 1) min = 1;
                else if (map.get(min) <= 0) min++;
                max = Math.max(max, cnts[c]);
                ans += max - min;
            }
        }
        return ans;
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    int beautySum(string s) {
        int n = s.size(), cnts[26], ans = 0;
        unordered_map<int, int> map;
        for(int i = 0; i < n; ++i) {
            memset(cnts, 0, sizeof(cnts));
            map.clear();
            int min = -1, maxv = -1;
            for(int j = i; j < n; ++j) {
                int c = s[j] - 'a';
                map[cnts[c]]--; map[cnts[c] + 1]++;
                cnts[c]++;
                if(cnts[c] == 1) min = 1;
                else if(map[min] <= 0) min++;
                maxv = max(maxv, cnts[c]);
                ans += maxv - min;
            }
        }
        return ans;
    }
};
```
Python 代码:
```Python
class Solution:
    def beautySum(self, s: str) -> int:
        n, ans = len(s), 0
        for i in range(n):
            cnts = [0] * 26
            dmap = defaultdict(int)
            maxv, minv = -1, -1
            for j in range(i, n):
                c = ord(s[j]) - ord('a')
                dmap[cnts[c]] -= 1
                dmap[cnts[c] + 1] += 1
                cnts[c] += 1
                if cnts[c] == 1:
                    minv = 1
                elif dmap[minv] <= 0:
                    minv += 1
                maxv = max(maxv, cnts[c])
                ans += maxv - minv
        return ans
```
TypeScript 代码:
```TypeScript
function beautySum(s: string): number {
    let n = s.length, ans = 0
    for (let i = 0; i < n; i++) {
        const cnts = new Array<number>(26).fill(0)
        const map = new Map()
        let min = 0, max = 0
        for (let j = i; j < n; j++) {
            const c = s.charCodeAt(j) - 'a'.charCodeAt(0)
            if (!map.has(cnts[c])) map.set(cnts[c], 0)
            map.set(cnts[c], map.get(cnts[c]) - 1)
            if (!map.has(cnts[c] + 1)) map.set(cnts[c] + 1, 0)
            map.set(cnts[c] + 1, map.get(cnts[c] + 1) + 1)
            cnts[c]++
            if (cnts[c] == 1) min = 1
            else if (map.get(min) <= 0) min++
            max = Math.max(max, cnts[c])
            ans += max - min
        }
    }
    return ans
}
```
* 时间复杂度:$O(n^2)$
* 空间复杂度:$O(C)$,其中 $C = 26$ 为字符集大小

### 最后

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

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

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

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


LeetCode/1781-1790/1784. 检查二进制字符串字段（简单）.md

1784. 检查二进制字符串字段

Question:
给你一个二进制字符串 `s` ,该字符串 不含前导零 。

如果 `s` 包含 零个或一个由连续的 `'1'` 组成的字段 ,返回 `true`。否则,返回 `false`。

如果 `s` 中 由连续若干个 `'1'` 组成的字段 数量不超过 `1`,返回 `true`。否则,返回 `false`。

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

输出:false

解释:由连续若干个 '1' 组成的字段数量为 2,返回 false
```
示例 2:
```
输入:s = "110"

输出:true
```

提示:
* $1 <= s.length <= 100$
* `s[i]` 为 `'0'` 或 `'1'`
* `s[0]` 为 `'1'`

Solution:
### 模拟

根据题意进行模拟即可。

Java 代码:
```Java
class Solution {
    public boolean checkOnesSegment(String s) {
        int n = s.length(), cnt = 0, idx = 0;
        while (idx < n && cnt <= 1) {
            while (idx < n && s.charAt(idx) == '0') idx++;
            if (idx < n) {
                while (idx < n && s.charAt(idx) == '1') idx++;
                cnt++;
            }
        }
        return cnt <= 1;
    }
}
```
TypeScript 代码:
```TypeScript
function checkOnesSegment(s: string): boolean {
    let n = s.length, cnt = 0, idx = 0
    while (idx < n && cnt <= 1) {
        while (idx < n && s[idx] == '0') idx++
        if (idx < n) {
            while (idx < n && s[idx] == '1') idx++
            cnt++
        }
    }
    return cnt <= 1
};
```
Python 代码:
```Python
class Solution:
    def checkOnesSegment(self, s: str) -> bool:
        n, cnt, idx = len(s), 0, 0
        while idx < n and cnt <= 1:
            while idx < n and s[idx] == '0':
                idx += 1
            if idx < n:
                while idx < n and s[idx] == '1':
                    idx += 1
                cnt += 1
        return cnt <= 1
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


LeetCode/1781-1790/1785. 构成特定和需要添加的最少元素（中等）.md

1785. 构成特定和需要添加的最少元素

Question:
给你一个整数数组 `nums`,和两个整数 `limit` 与 `goal`。

数组 `nums` 有一条重要属性:`abs(nums[i]) <= limit`。

返回使数组元素总和等于 `goal` 所需要向数组中添加的 最少元素数量 ,添加元素 不应改变 数组中 `abs(nums[i]) <= limit` 这一属性。

注意,如果 `x >= 0`,那么 `abs(x)` 等于 `x` ;否则,等于 `-x`。

示例 1:
```
输入:nums = [1,-1,1], limit = 3, goal = -4

输出:2

解释:可以将 -2 和 -3 添加到数组中,数组的元素总和变为 1 - 1 + 1 - 2 - 3 = -4 。
```
示例 2:
```
输入:nums = [1,-10,9,1], limit = 100, goal = 0

输出:1
```

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

Solution:
### 贪心

对于 `nums` 而言,我们可以先通过 $O(n)$ 的遍历求得其原总和 `sum` 为何值。

若 `sum` 与 `goal` 不等,我们可以按照「贪心」的方式里添加元素。

由于添加的元素需要满足 `abs(x) <= limit` 要求,因此我们添加数的范围在 $[-limit, limit]$ 之间。

为确保添加的元素最小,我们应当优先添加能够有效抵消两者差值的数值(添加 $limit$ 或 $-limit$),添加个数为 $\left \lceil \frac{\left | sum - goal \right |}{limit} \right \rceil$。

Java 代码:
```Java
class Solution {
    public int minElements(int[] nums, int limit, int goal) {
        long sum = 0;
        for (int x : nums) sum += x;
        return (int) ((Math.abs(sum - goal) + limit - 1) / limit);
    }
}
```
TypeScript 代码:
```TypeScript
function minElements(nums: number[], limit: number, goal: number): number {
    let sum = 0
    for (const x of nums) sum += x
    return Math.ceil(Math.abs(sum - goal) / limit)
}
```
Python 代码:
```Python
class Solution:
    def minElements(self, nums: List[int], limit: int, goal: int) -> int:
        sumv = sum(nums)
        return (abs(sumv - goal) + limit - 1) // limit
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


LeetCode/1781-1790/1786. 从第一个节点出发到最后一个节点的受限路径数（中等）.md

1786. 从第一个节点出发到最后一个节点的受限路径数

Question:
现有一个加权无向连通图。给你一个正整数 `n` ,表示图中有 `n` 个节点,并按从 `1` 到 `n` 给节点编号;另给你一个数组 `edges`,其中每个 $edges[i] = [u_{i}, v_{i}, weight_{i}]$ 表示存在一条位于节点 $u_{i}$ 和 $v_{i}$ 之间的边,这条边的权重为 $weight_{i}$ 。

从节点 start 出发到节点 `end` 的路径是一个形如 $[z_{0}, z_{1}, z_{2}, ..., z_{k}]$ 的节点序列,满足 $z_{0} = start$ 、$z_{k} = end$ 且在所有符合 $0 <= i <= k-1$ 的节点 $z_{i}$ 和 $z_{i}+1$ 之间存在一条边。

路径的距离定义为这条路径上所有边的权重总和。用 `distanceToLastNode(x)` 表示节点 `n` 和 `x` 之间路径的最短距离。受限路径 为满足 $distanceToLastNode(z_{i}) > distanceToLastNode(z_{i}+1)$ 的一条路径,其中 $0 <= i <= k-1$ 。

返回从节点 `1` 出发到节点 `n` 的 受限路径数 。由于数字可能很大,请返回对 $10^9 + 7$ 取余 的结果。

示例 1:

```
输入:n = 5, edges = [[1,2,3],[1,3,3],[2,3,1],[1,4,2],[5,2,2],[3,5,1],[5,4,10]]
输出:3
解释:每个圆包含黑色的节点编号和蓝色的 distanceToLastNode 值。三条受限路径分别是:
1) 1 --> 2 --> 5
2) 1 --> 2 --> 3 --> 5
3) 1 --> 3 --> 5
```
示例 2:

```
输入:n = 7, edges = [[1,3,1],[4,1,2],[7,3,4],[2,5,3],[5,6,1],[6,7,2],[7,5,3],[2,6,4]]
输出:1
解释:每个圆包含黑色的节点编号和蓝色的 distanceToLastNode 值。唯一一条受限路径是:1 --> 3 --> 7 。
```

提示:
* $1 <= n <= 2 \times 10^4$
* $n - 1 <= edges.length <= 4 \times 10^4$
* $edges[i].length == 3$
* $1 <= ui, vi <= n$
* $u_i != v_i$
* $1 <= weighti <= 10^5$
* 任意两个节点之间至多存在一条边
* 任意两个节点之间至少存在一条路径

Solution:
### 堆优化 Dijkstra + 动态规划

`n` 为点的数量,`m` 为边的数量。

为了方便理解,我们将第 `n` 个点称为「起点」,第 `1` 个点称为「结尾」。

按照题意,我们需要先求每个点到结尾的「最短路」,求最短路的算法有很多,通常根据「有无负权边」& 「稠密图还是稀疏图」进行选择。

该题只有正权变,而且“边”和“点”的数量在一个数量级上,属于稀疏图。

因此我们可以采用「最短路」算法:堆优化的 Dijkstra,复杂度为 $O(m\log{n})$。

> PS. 通常会优先选择 SPFA,SPFA 通常情况下复杂度为 $O(m)$,但最坏情况下复杂度为 $O(n \times m)$。从数据上来说 SPFA 也会超,而且本题还结合了 DP,因此可能会卡掉图论部分的 SPFA。出于这些考虑,我直接使用堆优化 Dijkstra。

当我们求得了每个点到结尾的「最短路」之后,接下来我们需要求得从「起点」到「结尾」的**受限路径数量**。

这显然可以用 DP 来做。

我们定义 `f(i)` 为从第 `i` 个点到结尾的受限路径数量,`f(1)` 就是我们的答案,而 `f(n) = 1` 是一个显而易见的起始条件。

因为题目的**受限路径数**的定义,我们需要找的路径所包含的点,必须是其距离结尾的最短路越来越近的。

举个🌰,对于示例 `1`,其中一条符合要求的路径为 `1 --> 2 --> 3 --> 5`。

这条路径的搜索过程可以看做,从结尾(第 5 个点)出发,逆着走,每次选择一个点(例如 a)之后,再选择下一个点(例如 b)时就必须**满足最短路距离比上一个点(点 a)要远**,如果最终能选到起点(第一个点),说明统计出一条有效路径。

我们的搜索方式决定了需要先按照最短路距离进行从小到大排序。

**不失一般性,当我们要求 `f(i)` 的时候,其实找的是 `i` 点可以到达的点 `j`,并且 `j` 点到结尾的最短路要严格小于 `i` 点到结尾的最短路。**

符合条件的点 `j` 有很多个,将所有的 `f(j)` 累加即是 `f(i)`。

代码:
```java
class Solution {
    int mod = 1000000007;
    public int countRestrictedPaths(int n, int[][] es) {
        // 预处理所有的边权。 a b w -> a : { b : w } + b : { a : w }
        Map<Integer, Map<Integer, Integer>> map = new HashMap<>();
        for (int[] e : es) {
            int a = e[0], b = e[1], w = e[2];
            Map<Integer, Integer> am = map.getOrDefault(a, new HashMap<Integer, Integer>());
            am.put(b, w);
            map.put(a, am);
            Map<Integer, Integer> bm = map.getOrDefault(b, new HashMap<Integer, Integer>());
            bm.put(a, w);
            map.put(b, bm);
        }

        // 堆优化 Dijkstra:求 每个点 到 第n个点 的最短路
        int[] dist = new int[n + 1];
        boolean[] st = new boolean[n + 1];
        Arrays.fill(dist, Integer.MAX_VALUE);
        dist[n] = 0;
        Queue<int[]> q = new PriorityQueue<int[]>((a, b)->a[1]-b[1]); // 点编号,点距离。根据点距离从小到大
        q.add(new int[]{n, 0});
        while (!q.isEmpty()) {
            int[] e = q.poll();
            int idx = e[0], cur = e[1];
            if (st[idx]) continue;
            st[idx] = true;
            Map<Integer, Integer> mm = map.get(idx);
            if (mm == null) continue;
            for (int i : mm.keySet()) {
                dist[i] = Math.min(dist[i], dist[idx] + mm.get(i));
                q.add(new int[]{i, dist[i]});
            }
        }

        // dp 过程
        int[][] arr = new int[n][2];
        for (int i = 0; i < n; i++) arr[i] = new int[]{i + 1, dist[i + 1]}; // 点编号,点距离
        Arrays.sort(arr, (a, b)->a[1]-b[1]); // 根据点距离从小到大排序

        // 定义 f(i) 为从第 i 个点到结尾的受限路径数量
        // 从 f[n] 递推到 f[1]
        int[] f = new int[n + 1];
        f[n] = 1;
        for (int i = 0; i < n; i++) {
            int idx = arr[i][0], cur = arr[i][1];
            Map<Integer, Integer> mm = map.get(idx);
            if (mm == null) continue;
            for (int next : mm.keySet()) {
                if (cur > dist[next]) {
                    f[idx] += f[next];
                    f[idx] %= mod;
                }
            }
            // 第 1 个节点不一定是距离第 n 个节点最远的点,但我们只需要 f[1],可以直接跳出循环
            if (idx == 1) break;
        }
        return f[1];
    }
}
```
* 时间复杂度:求最短路的复杂度为 $O(m\log{n})$,DP 过程坏情况下要扫完所有的边,复杂度为 $O(m)$。整体复杂度为 $O(m\log{n})$
* 空间复杂度:$O(n + m)$

### 最后

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

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

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

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


LeetCode/1781-1790/1787. 使所有区间的异或结果为零（困难）.md

1787. 使所有区间的异或结果为零

Question:
给你一个整数数组 `nums` 和一个整数 `k` 。
区间 `[left, right]``(left <= right)`的 异或结果 是对下标位于 `left` 和 `right`(包括 `left` 和 `right` )之间所有元素进行 XOR 运算的结果:`nums[left] XOR nums[left+1] XOR ... XOR nums[right]` 。

返回数组中**要更改的最小元素数** ,以使所有长度为 `k` 的区间异或结果等于零。

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

输出:3

解释:将数组 [1,2,0,3,0] 修改为 [0,0,0,0,0]
```
示例 2:
```
输入:nums = [3,4,5,2,1,7,3,4,7], k = 3

输出:3

解释:将数组 [3,4,5,2,1,7,3,4,7] 修改为 [3,4,7,3,4,7,3,4,7]
```
示例 3:
```
输入:nums = [1,2,4,1,2,5,1,2,6], k = 3

输出:3

解释:将数组[1,2,4,1,2,5,1,2,6] 修改为 [1,2,3,1,2,3,1,2,3]
```

提示:

* 1 <= k <= nums.length <= 2000
* 0 <= nums[i] < $2^{10}$

Solution:
### 基本分析

题目示例所包含的提示过于明显了,估计很多同学光看三个样例就猜出来了:**答案数组必然是每 $k$ 个一组进行重复的。**

这样的性质是可由「答案数组中所有长度为 $k$ 的区间异或结果为 $0$」推导出来的:

* 假设区间 $[i, j]$ 长度为 $k$,其异或结果为 $0$。即 $nums[i] ⊕ nums[i + 1] ⊕ ... ⊕ nums[j] = 0$

* 长度不变,将区间整体往后移动一位 $[i + 1, j + 1]$,其异或结果为 $0$。即 $nums[i + 1] ⊕ ... ⊕ nums[j] ⊕ nums[j + 1] = 0$

* 两式结合,中间 $[i + 1, j]$ 区间的数值出现两次,抵消掉最终剩下 $nums[i] ⊕ nums[j + 1] = 0$,即推出 $nums[i]$ 必然等于 $num[j + 1]$。

即答案数组必然每 $k$ 个一组进行重复。

也可以从滑动窗口的角度分析:窗口每滑动一格,会新增和删除一个值。对于异或而言,新增和删除都是对值本身做异或,因此新增值和删除值相等(保持窗口内异或值为 $0$)。

因此我们将这个一维的输入看成二维,从而将问题转化为:**使得最终每列相等,同时「首行」的异或值为 $0$ 的最小修改次数。**

当然 $n$ 和 $k$ 未必成倍数关系,这时候最后一行可能为不足 $k$ 个。这也是为什么上面没有说「每行异或结果为 $0$」,而是说「首行异或结果为 $0$」的原因:

***

### 动态规划

定义 $f[i][xor]$ 为考虑前 $i$ 列,且首行前 $i$ 列异或值为 $xor$ 的最小修改次数,最终答案为 $f[k - 1][0]$。

第一维的范围为 $[0, k)$,由输入参数给出;第二维为 $[0, 2^{10})$,根据题目给定的数据范围 `0 <= nums[i] < 2^10` 可得(异或为不进位加法,因此最大异或结果不会超过 $2^{10}$)。

为了方便,我们需要使用哈希表 $map$ 记录每一列的数字分别出现了多少次,使用变量 $cnt$ 统计该列有多少数字(因为最后一行可能不足 $k$ 个)。

不失一般性的考虑 $f[i][xor]$ 如何转移:

* 当前处于第 $0$ 列:由于没有前一列,这时候只需要考虑怎么把该列变成 $xor$ 即可:

$$
f[0][xor] = cnt - map.get(xor)
$$

* 当前处于其他列:需要考虑与前面列的关系。

    我们知道最终的 $f[i][xor]$ 由两部分组成:一部分是前 $i - 1$ 列的修改次数,一部分是当前列的修改次数。

    这时候需要分情况讨论:

    * **仅修改当列的部分数**:这时候需要从哈希表中遍历已存在的数,在所有方案中取 $min$:
    $$
    f[i][xor] = f[i - 1][xor ⊕ cur] + cnt - map.get(cur)
    $$

    * **对整列进行修改替换**:此时当前列的修改成本固定为 $cnt$,只需要取前 $i - 1$ 列的最小修改次数过来即可:
    $$
    f[i][xor] = f[i - 1][xor'] + cnt
    $$

最终 $f[i][xor]$ 在所有上述方案中取 $min$。为了加速「取前 $i - 1$ 列的最小修改次数」的过程,我们可以多开一个 $g[]$ 数组来记录前一列的最小状态值。

代码:
```Java
class Solution {
    public int minChanges(int[] nums, int k) {
        int n = nums.length;
        int max = 1024;
        int[][] f = new int[k][max];
        int[] g = new int[k];
        for (int i = 0; i < k; i++) {
            Arrays.fill(f[i], 0x3f3f3f3f);
            g[i] = 0x3f3f3f3f;
        }
        for (int i = 0, cnt = 0; i < k; i++, cnt = 0) {
            // 使用 map 和 cnt 分别统计当前列的「每个数的出现次数」和「有多少个数」
            Map<Integer, Integer> map = new HashMap<>();
            for (int j = i; j < n; j += k) {
                map.put(nums[j], map.getOrDefault(nums[j], 0) + 1);
                cnt++;
            }
            if (i == 0) { // 第 0 列:只需要考虑如何将该列变为 xor 即可
                for (int xor = 0; xor < max; xor++) {
                    f[0][xor] = Math.min(f[0][xor], cnt - map.getOrDefault(xor, 0));
                    g[0] = Math.min(g[0], f[0][xor]);
                }
            } else { // 其他列:考虑与前面列的关系
                for (int xor = 0; xor < max; xor++) {
                    f[i][xor] = g[i - 1] + cnt; // 整列替换
                    for (int cur : map.keySet()) { // 部分替换
                        f[i][xor] = Math.min(f[i][xor], f[i - 1][xor ^ cur] + cnt - map.get(cur));
                    }
                    g[i] = Math.min(g[i], f[i][xor]);
                }
            }
        }
        return f[k - 1][0];
    }
}
```
* 时间复杂度:共有 $O(C * k)$ 个状态需要被转移(其中 $C$ 固定为 $2^{10}$),每个状态的转移需要遍历哈希表,最多有 $\frac{n}{k}$ 个数,复杂度为 $O(\frac{n}{k})$。整体复杂度为 $O(C * n)$
* 空间复杂度:$O(C * k)$,其中 $C$ 固定为 $2^{10} + 1$

### 最后

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

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

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

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


LeetCode/1781-1790/1790. 仅执行一次字符串交换能否使两个字符串相等（简单）.md

1790. 仅执行一次字符串交换能否使两个字符串相等

Question:
给你长度相等的两个字符串 `s1` 和 `s2` 。一次 字符串交换 操作的步骤如下:选出某个字符串中的两个下标(不必不同),并交换这两个下标所对应的字符。

如果对 其中一个字符串 执行 最多一次字符串交换 就可以使两个字符串相等,返回 `true` ;否则,返回 `false` 。

示例 1:
```
输入:s1 = "bank", s2 = "kanb"

输出:true

解释:例如,交换 s2 中的第一个和最后一个字符可以得到 "bank"
```
示例 2:
```
输入:s1 = "attack", s2 = "defend"

输出:false

解释:一次字符串交换无法使两个字符串相等
```
示例 3:
```
输入:s1 = "kelb", s2 = "kelb"

输出:true

解释:两个字符串已经相等,所以不需要进行字符串交换
```
示例 4:
```
输入:s1 = "abcd", s2 = "dcba"

输出:false
```

提示:
* $1 <= s1.length, s2.length <= 100$
* $s1.length = s2.length$
* `s1` 和 `s2` 仅由小写英文字母组成

Solution:
### 模拟

根据题意进行模拟即可 : 使用 `a` 和 `b` 记录不同的位置下标,初始值为 `-1`,若「不同位置超过 $2$ 个」或「只有 $1$ 个」直接返回 `false`,若「不存在不同位置」或「不同位置字符相同」,则返回 `true`。

Java 代码:
```Java
class Solution {
    public boolean areAlmostEqual(String s1, String s2) {
        int n = s1.length(), a = -1, b = -1;
        for (int i = 0; i < n; i++) {
            if (s1.charAt(i) == s2.charAt(i)) continue;
            if (a == -1) a = i;
            else if (b == -1) b = i;
            else return false;
        }
        if (a == -1) return true;
        if (b == -1) return false;
        return s1.charAt(a) == s2.charAt(b) && s1.charAt(b) == s2.charAt(a);
    }
}
```
TypeScript 代码:
```TypeScript
function areAlmostEqual(s1: string, s2: string): boolean {
    let n = s1.length, a = -1, b = -1
    for (let i = 0; i < n; i++) {
        if (s1[i] == s2[i]) continue
        if (a == -1) a = i
        else if (b == -1) b = i
        else return false
    }
    if (a == -1) return true
    if (b == -1) return false
    return s1[a] == s2[b] && s1[b] == s2[a]
}
```
Python 代码:
```Python
class Solution:
    def areAlmostEqual(self, s1: str, s2: str) -> bool:
        n, a, b = len(s1), -1, -1
        for i in range(n):
            if s1[i] == s2[i]:
                continue
            if a == -1:
                a = i
            elif b == -1:
                b = i
            else:
                return False
        if a == -1:
            return True
        if b == -1:
            return False
        return s1[a] == s2[b] and s1[b] == s2[a]
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


LeetCode/1791-1800/1791. 找出星型图的中心节点（简单）.md

1791. 找出星型图的中心节点

Question:
有一个无向的 星型 图,由 $n$ 个编号从 $1$ 到 $n$ 的节点组成。星型图有一个 中心 节点,并且恰有 $n - 1$ 条边将中心节点与其他每个节点连接起来。

给你一个二维整数数组 `edges` ,其中 $edges[i] = [u_i, v_i]$ 表示在节点 $u_i$ 和 $v_i$ 之间存在一条边。请你找出并返回 `edges` 所表示星型图的中心节点。

示例 1:

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

输出:2

解释:如上图所示,节点 2 与其他每个节点都相连,所以节点 2 是中心节点。
```
示例 2:
```
输入:edges = [[1,2],[5,1],[1,3],[1,4]]

输出:1
```

提示:
* $3 <= n <= 10^5$
* $edges.length == n - 1$
* $edges[i].length == 2$
* $1 <= u_i, v_i <= n$
* $u_i != v_i$
* 题目数据给出的 `edges` 表示一个有效的星型图

Solution:
### 模拟

根据题意,中心节点必然出现在所有的 $edges[i]$ 中,因此使用前两条边即可确定答案。

起始让 $edges[0][0]$ 和 $edges[0][1]$ 作为答案候选,然后在 $edges[1]$ 关系中检查哪个候选出现过。

代码:
```Java
class Solution {
    public int findCenter(int[][] edges) {
        int a = edges[0][0], b = edges[0][1];
        if (a == edges[1][0] || a == edges[1][1]) return a;
        else return b;
    }
}
```
* 时间复杂度:$O(1)$
* 空间复杂度:$O(1)$

---

### 进阶

**显然,如果将每个 $edges[i]$ 看做两点之间的「双向边」的话,那么星型图为「欧拉图」,所有点的出度均等于入度。**

容易将题拓展为求欧拉回路的问题:

>**给定星与星之间的距离,从某个星的位置出发,经过所有的边(可重复经过)并回到起点的最短距离,输出能够取得最短距离的路径(无解输出 $-1$)**。

答案就是求「欧拉回路」,其中「可重复经过边」包含了「可重复经过点」的含义。

由于星星图存在中心点,必然有解(但为了题目描述的完整性,出题都会预设一个无解返回值);同时也不会重复经过某条边(仍然是为了题目描述完整性才这样写)。

### 最后

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

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

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

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


LeetCode/1791-1800/1797. 设计一个验证系统（中等）.md

1797. 设计一个验证系统

Question:
你需要设计一个包含验证码的验证系统。

每一次验证中,用户会收到一个新的验证码,这个验证码在 `ct` 时刻之后 `timeToLive` 秒过期。

如果验证码被更新了,那么它会在 `ct` (可能与之前的 `ct` 不同)时刻延长 `timeToLive` 秒。

请你实现 `AuthenticationManager` 类:

* `AuthenticationManager(int timeToLive)` 构造 `AuthenticationManager` 并设置 `timeToLive` 参数。

* `generate(string id, int ct)` 给定 `id`,在当前时间 `ct` 生成一个新的验证码。

* `renew(string id, int ct)` 将给定 `id` 且未过期的验证码在 `ct` 时刻更新。

  如果给定 `id` 对应的验证码不存在或已过期,请你忽略该操作,不会有任何更新操作发生。

* `countUnexpiredTokens(int ct)` 请返回在给定 `ct` 时刻,未过期的验证码数目。

如果一个验证码在时刻 `t` 过期,且另一个操作恰好在时刻 `t` 发生(`renew` 或者 `countUnexpiredTokens` 操作),过期事件优先于其他操作。

示例 1:

```
输入:
["AuthenticationManager", "renew", "generate", "countUnexpiredTokens", "generate", "renew", "renew", "countUnexpiredTokens"]
[[5], ["aaa", 1], ["aaa", 2], [6], ["bbb", 7], ["aaa", 8], ["bbb", 10], [15]]

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

解释:
AuthenticationManager authenticationManager = new AuthenticationManager(5); // 构造 AuthenticationManager ,设置 timeToLive = 5 秒。
authenticationManager.renew("aaa", 1); // 时刻 1 时,没有验证码的 tokenId 为 "aaa" ,没有验证码被更新。
authenticationManager.generate("aaa", 2); // 时刻 2 时,生成一个 tokenId 为 "aaa" 的新验证码。
authenticationManager.countUnexpiredTokens(6); // 时刻 6 时,只有 tokenId 为 "aaa" 的验证码未过期,所以返回 1 。
authenticationManager.generate("bbb", 7); // 时刻 7 时,生成一个 tokenId 为 "bbb" 的新验证码。
authenticationManager.renew("aaa", 8); // tokenId 为 "aaa" 的验证码在时刻 7 过期,且 8 >= 7 ,所以时刻 8 的renew 操作被忽略,没有验证码被更新。
authenticationManager.renew("bbb", 10); // tokenId 为 "bbb" 的验证码在时刻 10 没有过期,所以 renew 操作会执行,该 token 将在时刻 15 过期。
authenticationManager.countUnexpiredTokens(15); // tokenId 为 "bbb" 的验证码在时刻 15 过期,tokenId 为 "aaa" 的验证码在时刻 7 过期,所有验证码均已过期,所以返回 0 。
```

提示:
* $1 <= timeToLive <= 10^8$
* $1 <= currentTime <= 10^8$
* $1 <= tokenId.length <= 5$
* `id` 只包含小写英文字母。
* 所有 `generate` 函数的调用都会包含独一无二的 `id` 值。
* 所有函数调用中,`ct` 的值严格递增。
* 所有函数的调用次数总共不超过 `2000` 次。

Solution:
### 哈希表

数据范围只有 `20`,我们使用哈希表记录每个 `id` 的过期时间 `ct`,在每次查询时遍历整个哈希表来统计未过期的验证码数量。

Java 代码:
```Java
class AuthenticationManager {
    int d;
    Map<String, Integer> map = new HashMap<>();
    public AuthenticationManager(int timeToLive) {
        d = timeToLive;
    }
    public void generate(String id, int ct) {
        map.put(id, ct + d);
    }
    public void renew(String id, int ct) {
        if (!map.containsKey(id) || map.get(id) <= ct) return ;
        map.put(id, ct + d);
    }
    public int countUnexpiredTokens(int ct) {
        int ans = 0;
        for (String id : map.keySet()) {
            if (map.get(id) > ct) ans++;
        }
        return ans;
    }
}
```
C++ 代码:
```C++
class AuthenticationManager {
    int d;
    unordered_map<string, int> map;
public:
    AuthenticationManager(int timeToLive) : d(timeToLive){}
    void generate(string id, int ct) {
        map[id] = ct + d;
    }
    void renew(string id, int ct) {
        if (!map.count(id) || map[id] <= ct) return;
        map[id] = ct + d;
    }
    int countUnexpiredTokens(int ct) {
        int ans = 0;
        for (auto it = map.begin(); it != map.end(); ++it) {
            if (it->second > ct) ans++;
        }
        return ans;
    }
};
```
Python 代码:
```Python
class AuthenticationManager:
    def __init__(self, timeToLive):
        self.d = timeToLive
        self.map = {}

    def generate(self, id: str, ct: int) -> None:
        self.map[id] = ct + self.d

    def renew(self, id: str, ct: int) -> None:
        if id not in self.map or self.map[id] <= ct:
            return
        self.map[id] = ct + self.d

    def countUnexpiredTokens(self, ct: int) -> int:
        ans = 0
        for id in self.map:
            if self.map[id] > ct:
                ans += 1
        return ans
```
TypeScript 代码:
```TypeScript
class AuthenticationManager {
    d: number;
    map: { [id: string]: number } = {};

    constructor(timeToLive: number) {
        this.d = timeToLive;
    }

    generate(id: string, ct: number): void {
        this.map[id] = ct + this.d;
    }

    renew(id: string, ct: number): void {
        if (!this.map.hasOwnProperty(id) || this.map[id] <= ct) {
            return;
        }
        this.map[id] = ct + this.d;
    }

    countUnexpiredTokens(ct: number): number {
        let ans = 0;
        for (const id in this.map) {
            if (this.map[id] > ct) {
                ans++;
            }
        }
        return ans;
    }
}
```
* 时间复杂度:`generate` 操作和 `renew` 操作的复杂度为 $O(1)$;`countUnexpiredTokens` 操作的复杂度为 $O(n)$
* 空间复杂度:$O(n)$

---

### 双向链表

在所有函数的调用过程中,`timeToLive` 都在单调递增。

在哈希表的做法里,我们没有清理旧验证码的操作,同时每次执行 `countUnexpiredTokens` 时,需要对整个哈希表进行遍历。

实际上,如果我们引入 **双向链表**,并将哈希表的键值对定义从 `{验证码:过期时间值}` 调整为 `{验证码:链表节点}` 时(链表节点属性仅包含验证码字符串 `id` 及其过期时间 `t`),我们便能实现如下优化:

* **减少统计未过期验证码时的无效遍历**:由于构建的双向链表严格按照 `timeToLive` 递增,因此可以从尾部出发,从后往前利用链表节点的 `prev` 指针进行遍历统计。

  如此一来,有多少未过期的验证码,我们就会遍历多少个链表节点,其余已过期的节点对象并不会被访问;

* **引入清理时机**:由于上述的统计过程,我们会找到最靠前的一个未过期节点。可以将其作为新的双向链表新头结点,从而将整段的过期节点从双向链表中删除

最后,我们像对其他链表题目一样,为了方便,引入 `he` 和 `ta` 的头尾链表哨兵节点以减少边界处理。

Java 代码:
```Java
class AuthenticationManager {
    class Node {
        String id;
        int t;
        Node prev, next;
        Node (String _id, int _t) {
            id = _id; t = _t;
        }
    }
    int d;
    Node he, ta;
    Map<String, Node> map = new HashMap<>();
    public AuthenticationManager(int timeToLive) {
        he = new Node("", -1); ta = new Node("", (int)1e9);
        he.next = ta; ta.prev = he;
        d = timeToLive;
    }
    public void generate(String id, int ct) {
        Node node = new Node(id, ct + d);
        node.prev = ta.prev;
        node.next = ta;
        ta.prev.next = node;
        ta.prev = node;
        map.put(id, node);
    }
    public void renew(String id, int ct) {
        if (!map.containsKey(id) || map.get(id).t <= ct) return ;
        Node node = map.get(id);
        node.prev.next = node.next;
        node.next.prev = node.prev;
        generate(id, ct);
    }
    public int countUnexpiredTokens(int ct) {
        int ans = 0;
        Node cur = ta.prev;
        while (cur.t > ct && ++ans >= 0) cur = cur.prev;
        he.next = cur.next;
        cur.next.prev = he;
        return ans;
    }
}
```
C++ 代码:
```C++
class AuthenticationManager {
    struct Node {
        string id;
        int t;
        Node *prev, *next;
        Node (string _id, int _t) : id(_id), t(_t), prev(nullptr), next(nullptr) {}
    };
    int d;
    Node *he, *ta;
    unordered_map<string, Node*> map;
public:
    AuthenticationManager(int timeToLive) : d(timeToLive) {
        he = new Node("", -1);
        ta = new Node("", (int)1e9);
        he->next = ta;
        ta->prev = he;
    }
    void generate(string id, int ct) {
        Node* node = new Node(id, ct + d);
        node->prev = ta->prev;
        node->next = ta;
        ta->prev->next = node;
        ta->prev = node;
        map[id] = node;
    }
    void renew(string id, int ct) {
        if (!map.count(id) || map[id]->t <= ct) return;
        Node* node = map[id];
        node->prev->next = node->next;
        node->next->prev = node->prev;
        generate(id, ct);
    }
    int countUnexpiredTokens(int ct) {
        int ans = 0;
        Node* cur = ta->prev;
        while (cur->t > ct && ++ans >= 0) cur = cur->prev;
        he->next = cur->next;
        cur->next->prev = he;
        return ans;
    }
};
```
TypeScript 代码:
```TypeScript
class Node {
    id: string;
    t: number;
    prev: Node | null;
    next: Node | null;
    constructor(_id: string, _t: number) {
        this.id = _id;
        this.t = _t;
        this.prev = null
        this.next = null
    }
}
class AuthenticationManager {
    d: number;
    he: Node;
    ta: Node;
    map: {[id: string]: Node};
    constructor(timeToLive: number) {
        this.d = timeToLive;
        this.he = new Node("", -1)
        this.ta = new Node("", 1e9)
        this.he.next = this.ta
        this.ta.prev = this.he
        this.map = {};
    }

    generate(id: string, ct: number) {
        let node : Node = new Node(id, ct + this.d) ;
        node.prev = this.ta.prev;
        node.next = this.ta;
        this.ta.prev!.next = node;
        this.ta.prev = node;
        this.map[id] = node;
    }

    renew(id: string, ct: number) {
        if (!this.map.hasOwnProperty(id) || this.map[id].t <= ct) {
            return;
        }
        var node : Node = this.map[id];
        node.prev!.next = node.next;
        node.next!.prev = node.prev;
        this.generate(id, ct);
    }

    countUnexpiredTokens(ct: number) {
        let ans = 0;
        let cur : Node = this.ta.prev!;
        while (cur.t > ct && ++ans >= 0)
            cur = cur.prev!;
        this.he.next = cur.next;
        cur.next!.prev = this.he;
        return ans;
    }
}
```
* 时间复杂度:`generate` 操作和 `renew` 操作的复杂度为 $O(1)$;`countUnexpiredTokens` 操作的复杂度为 $O(n)$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


LeetCode/1791-1800/1798. 你能构造出连续值的最大数目（中等）.md

1798. 你能构造出连续值的最大数目

Question:
给你一个长度为 `n` 的整数数组 `coins`,它代表你拥有的 `n` 个硬币。第 `i` 个硬币的值为 `coins[i]`。如果你从这些硬币中选出一部分硬币,它们的和为 `x` ,那么称,你可以构造出 `x` 。

请返回从 `0` 开始(包括 `0` ),你最多能构造出多少个连续整数。

你可能有多个相同值的硬币。

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

输出:2

解释:你可以得到以下这些值:
- 0:什么都不取 []
- 1:取 [1]
从 0 开始,你可以构造出 2 个连续整数。
```
示例 2:
```
输入:coins = [1,1,1,4]

输出:8

解释:你可以得到以下这些值:
- 0:什么都不取 []
- 1:取 [1]
- 2:取 [1,1]
- 3:取 [1,1,1]
- 4:取 [4]
- 5:取 [4,1]
- 6:取 [4,1,1]
- 7:取 [4,1,1,1]
从 0 开始,你可以构造出 8 个连续整数。
```
示例 3:
```
输入:nums = [1,4,10,3,1]

输出:20
```

提示:
* $coins.length = n$
* $1 <= n <= 4 \times 10^4$
* $1 <= coins[i] <= 4 \times 10^4$

Solution:
### 数学

`n` 的数据范围为 $4 \times 10^4$,必然不是考察我们使用 `coins` 来构造单个数值 `x` 的逻辑,因为「遍历 + 逐个构造验证」的做法会超时,**因此只能是考察我们能否推导出整段构造的相关性质**。

假设我们已经用前 `k` 个数值构造出连段 $[0, x]$ 中的任意数,当增加第 $k + 1$ 个数值时,还能否进行连续构造:

* 若不能,则连续构造中断,答案为 $[0, x]$,共 $x + 1$ 个
* 若能,则再考虑连续构造的右边界会到哪个地方

由于题目允许我们任意使用 `coins` 中的数,同时整段构造又是不断扩大 $[0, x]$ 中右边界的过程(从小到大),为了方便,我们可以先对 `coins` 进行排序。

不失一般性,假设我们已经使用 `coins` 中的前  `k` 个数构造出了范围 $[0, x]$ 中的任意数。当考虑增加一个 $coins[k]$ 后,我们可在每一个原有构造方案中增加 $coins[k]$,这样由 $coins[k]$ 所拓展出的构造范围便是 $[coins[k], coins[k] + x]$。

原来的连续数是 $[0, x]$,若要保证连续,我们需要保证 $coins[k] <= x + 1$,此时构造连续段也从 $[0, x]$ 变为 $[0, coins[k] + x]$。

即 $coins[k] > k + 1$ 是中断构造必要条件,再结合我们实现对 `coins` 进行了排序,容易证明如果 $coins[k]$ 都不能满足 $coins[k] <= x + 1$,排在 $coins[k]$ 后面比其大的数均不能满足要求。

一些细节:起始时,我们可以不选 `coins` 中的任何数,即此时连续构造范围为 $[0, 0]$,随后从小到大遍历 `coins`,检查当前 $coins[i]$ 是否会中断构造。

Java 代码:
```Java
class Solution {
    public int getMaximumConsecutive(int[] coins) {
        Arrays.sort(coins);
        int ans = 0;
        for (int c : coins) {
            if (c > ans + 1) break;
            ans += c;
        }
        return ans + 1;
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    int getMaximumConsecutive(vector<int>& coins) {
        sort(coins.begin(), coins.end());
        int ans = 0;
        for (int c : coins) {
            if (c > ans + 1) break;
            ans += c;
        }
        return ans + 1;
    }
};
```
Python 代码:
```Python
class Solution:
    def getMaximumConsecutive(self, coins: List[int]) -> int:
        coins.sort()
        ans = 0
        for c in coins:
            if c > ans + 1: break
            ans += c
        return ans + 1
```
TypeScript 代码:
```TypeScript
function getMaximumConsecutive(coins: number[]): number {
    coins.sort((a,b)=>a-b)
    let ans = 0
    for (const c of coins) {
        if (c > ans + 1) break
        ans += c
    }
    return ans + 1
}
```
* 时间复杂度:$O(n\log{n})$
* 空间复杂度:$O(\log{n})$

### 最后

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

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

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

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


LeetCode/1801-1810/1801. 积压订单中的订单总数（中等）.md

1801. 积压订单中的订单总数

Question:
给你一个二维整数数组 $orders$ ,其中每个 $orders[i] = [price_i, amount_i, orderType_i]$ 表示有 $amount_i$ 笔类型为 $orderType_i$、价格为 $price_i$ 的订单。

订单类型 $orderType_i$ 可以分为两种:

* $0$ 表示这是一批采购订单 `buy`
* $1$ 表示这是一批销售订单 `sell`

注意,$orders[i]$ 表示一批共计 $amount_i$ 笔的独立订单,这些订单的价格和类型相同。

对于所有有效的 $i$ ,由 $orders[i]$ 表示的所有订单提交时间均早于 $orders[i+1]$ 表示的所有订单。

存在由未执行订单组成的积压订单。积压订单最初是空的。

提交订单时,会发生以下情况:

* 如果该订单是一采购订单 `buy`,则可以查看积压订单中价格 **最低** 的销售订单 `sell`。如果该销售订单 `sell` 的价格 **低于或等于** 当前采购订单 `buy` 的价格,则匹配并执行这两笔订单,并将销售订单 `sell` 从积压订单中删除。否则,采购订单 `buy` 将会添加到积压订单中。
* 反之亦然,如果该订单是一笔销售订单 `sell`,则可以查看积压订单中价格 **最高** 的采购订单 `buy`。如果该采购订单 `buy` 的价格 **高于或等于** 当前销售订单 `sell` 的价格,则匹配并执行这两笔订单,并将采购订单 `buy` 从积压订单中删除。否则,销售订单 `sell` 将会添加到积压订单中。

输入所有订单后,返回积压订单中的订单总数。

由于数字可能很大,所以需要返回对 $10^9 + 7$ 取余的结果。

示例 1:

```
输入:orders = [[10,5,0],[15,2,1],[25,1,1],[30,4,0]]

输出:6

解释:输入订单后会发生下述情况:
- 提交 5 笔采购订单,价格为 10 。没有销售订单,所以这 5 笔订单添加到积压订单中。
- 提交 2 笔销售订单,价格为 15 。没有采购订单的价格大于或等于 15 ,所以这 2 笔订单添加到积压订单中。
- 提交 1 笔销售订单,价格为 25 。没有采购订单的价格大于或等于 25 ,所以这 1 笔订单添加到积压订单中。
- 提交 4 笔采购订单,价格为 30 。前 2 笔采购订单与价格最低(价格为 15)的 2 笔销售订单匹配,从积压订单中删除这 2 笔销售订单。第 3 笔采购订单与价格最低的 1 笔销售订单匹配,销售订单价格为 25 ,从积压订单中删除这 1 笔销售订单。积压订单中不存在更多销售订单,所以第 4 笔采购订单需要添加到积压订单中。
最终,积压订单中有 5 笔价格为 10 的采购订单,和 1 笔价格为 30 的采购订单。所以积压订单中的订单总数为 6 。
```
示例 2:

```
输入:orders = [[7,1000000000,1],[15,3,0],[5,999999995,0],[5,1,1]]

输出:999999984

解释:输入订单后会发生下述情况:
- 提交 109 笔销售订单,价格为 7 。没有采购订单,所以这 109 笔订单添加到积压订单中。
- 提交 3 笔采购订单,价格为 15 。这些采购订单与价格最低(价格为 7 )的 3 笔销售订单匹配,从积压订单中删除这 3 笔销售订单。
- 提交 999999995 笔采购订单,价格为 5 。销售订单的最低价为 7 ,所以这 999999995 笔订单添加到积压订单中。
- 提交 1 笔销售订单,价格为 5 。这笔销售订单与价格最高(价格为 5 )的 1 笔采购订单匹配,从积压订单中删除这 1 笔采购订单。
最终,积压订单中有 (1000000000-3) 笔价格为 7 的销售订单,和 (999999995-1) 笔价格为 5 的采购订单。所以积压订单中的订单总数为 1999999991 ,等于 999999984 % (109 + 7) 。
```

提示:
* $1 <= orders.length <= 10^5$
* $orders[i].length == 3$
* $1 <= pricei, amounti <= 10^9$
* `orderTypei` 为 $0$ 或 $1$

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

整理题意:从前往后处理所有的 $orders[i]$,对于 `buy` 类型的订单,从积压订单中找价格低于等于当前价格的 `sell` 订单进行抵消;同理,对于 `sell` 类型的订单,从积压订单中找价格高于等于当前价格的 `buy` 订单进行抵消。问最终有多少积压订单。

这个找「最低/最高」价格的操作可以利用优先队列(堆)来做,对于积压的 `buy` 类型订单,我们总是要找价格高的,使用大根堆维护;对于积压的 `sell` 类型订单,我们总是要找价格低的,使用小根堆维护。

两个优先队列(堆)均维护形如 $(price_i, amount_i)$ 的二元组信息,代表价格为 $price_i$ 的订单积压了 $amount_i$ 单。

当处理(尝试匹配)了所有的 $orders[i]$ 之后,统计两个优先队列(堆)中订单数量即是答案。

代码:
```Java
class Solution {
    public int getNumberOfBacklogOrders(int[][] orders) {
        PriorityQueue<int[]> buy = new PriorityQueue<>((a,b)->b[0]-a[0]), sell = new PriorityQueue<>((a,b)->a[0]-b[0]);
        PriorityQueue<int[]> from = null, to = null;
        boolean fromIsSell = false;
        for (int[] order : orders) {
            int p = order[0], a = order[1], t = order[2];
            if (t == 0) {
                from = sell; to = buy; fromIsSell = true;
            } else {
                from = buy; to = sell; fromIsSell = false;
            }
            while (a > 0 && !from.isEmpty() && (fromIsSell ? from.peek()[0] <= p : from.peek()[0] >= p)) {
                int[] cur = from.poll();
                int cnt = Math.min(cur[1], a);
                cur[1] -= cnt; a -= cnt;
                if (cur[1] > 0) from.add(cur);
            }
            if (a > 0) to.add(new int[]{p, a});
        }
        int ans = 0;
        for (PriorityQueue<int[]> q : new PriorityQueue[]{buy, sell}) {
            while (!q.isEmpty()) {
                ans += q.poll()[1];
                ans %= (int)1e9+7;
            }
        }
        return ans;
    }
}
```

-

```Python
class Solution:
    def getNumberOfBacklogOrders(self, orders: List[List[int]]) -> int:
        buy, sell = [], []
        for p, a, t in orders:
            if t == 0:
                from_heap, to_heap, from_is_sell = sell, buy, True
            else:
                from_heap, to_heap, from_is_sell = buy, sell, False
            while a and from_heap and (from_heap[0][0] <= p if from_is_sell else -from_heap[0][0] >= p):
                cur = heapq.heappop(from_heap)
                if cur[1] > a:
                    heapq.heappush(from_heap, (cur[0], cur[1] - a))
                    a = 0
                else:
                    a -= cur[1]
            if a:
                heapq.heappush(to_heap, (-p if from_is_sell else p, a))
        ans, mod = 0, int(1e9)+7
        for _, a in buy + sell:
            ans += a
        return ans % mod

```
* 时间复杂度:令$n$ 为数组 `orders` 的长度,复杂度为 $O(n\log{n})$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


LeetCode/1801-1810/1802. 有界数组中指定下标处的最大值（中等）.md

1802. 有界数组中指定下标处的最大值

Question:
给你三个正整数 `n`、`index` 和 `maxSum`。

你需要构造一个同时满足下述所有条件的数组 `nums`(下标 从 `0` 开始 计数):

* `nums.length == n`
* `nums[i]` 是 正整数 ,其中 `0 <= i < n`
* `abs(nums[i] - nums[i+1]) <= 1` ,其中 `0 <= i < n-1`
* `nums` 中所有元素之和不超过 `maxSum`
* `nums[index]` 的值被 最大化
* 返回你所构造的数组中的 `nums[index]`

注意:`abs(x)` 等于 `x` 的前提是 `x >= 0`;否则,`abs(x)` 等于 `-x`。

示例 1:
```
输入:n = 4, index = 2,  maxSum = 6

输出:2

解释:数组 [1,1,2,1] 和 [1,2,2,1] 满足所有条件。不存在其他在指定下标处具有更大值的有效数组。
```
示例 2:
```
输入:n = 6, index = 1,  maxSum = 10

输出:3
```

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

Solution:
### 二分 + 贪心 + 数学

根据题意,容易想到以 `ans` 为分割点的正整数数组具有二段性,其中 `ans` 为最大的 $nums[idx]$。

小于等于 `ans` 的值均能通过直接调整 $nums[idx]$ 来构造,不会违反总和不超过 `max` 的限制;大于 `ans` 的值则无法满足 `max` 限制。基于此我们可通过「二分」的方式来找分割点。

假设当前二分到的值为 `x`,考虑如何实现一个 `check` 函数,该函数用于判断 `x` 能否作为 $nums[idx]$:

为了令 $nums[idx] = x$ 时,数组总和 `sum` 不超过 `max` 限制,我们应当贪心构造 $nums$ 的剩余元素:从 $idx$ 开始往两侧构造,按照递减的方式进行逐个构造,若递减到 $1$ 则维持不变。

这样可确保构造出来的 $nums$ 既满足 $nums[idx] = x$ 同时元素总和最小。

位置 `idx` 的值为 `x`,其左边有 `idx` 个元素,其右边有 `n - idx - 1` 个元素。

利用「等差数列求和」公式分别从 `x - 1` 开始构造(注意:这里说的构造仅是计算 $nums$ 总和),若总和不超过 `max` 说明 $nums[idx] = x$ 满足要求,我们令 $l = mid$,否则令 $r = mid - 1$。

Java 代码:
```Java
class Solution {
    public int maxValue(int n, int index, int max) {
        long l = 1, r = max;
        while (l < r) {
            long mid = l + r + 1 >> 1;
            if (check(n, mid, index, max)) l = mid;
            else r = mid - 1;
        }
        return (int) r;
    }
    boolean check(int n, long x, int idx, int max) {
        long sum = x;
        if (idx > x - 1) {
            long an = x - 1, a1 = 1, cnt = x - 1;
            sum += cnt * (a1 + an) / 2;
            sum += idx - cnt;
        } else {
            long cnt = idx, an = x - 1, a1 = an - cnt + 1;
            sum += cnt * (a1 + an) / 2;
        }
        if (n - idx - 1 > x - 1) {
            long an = x - 1, a1 = 1, cnt = x - 1;
            sum += cnt * (a1 + an) / 2;
            sum += n - idx - 1 - cnt;
        } else {
            long cnt = n - idx - 1, an = x - 1, a1 = an - cnt + 1;
            sum += cnt * (a1 + an) / 2;
        }
        return sum <= max;
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    int maxValue(int n, int index, int maxSum) {
        long l = 1, r = maxSum;
        while (l < r) {
            long mid = (l + r + 1) >> 1;
            if (check(n, mid, index, maxSum)) l = mid;
            else r = mid - 1;
        }
        return (int) r;
    }
    bool check(int n, long x, int idx, int maxSum) {
        long sum = x;
        if (idx > x - 1) {
            long an = x - 1, a1 = 1, cnt = x - 1;
            sum += cnt * (a1 + an) / 2;
            sum += idx - cnt;
        } else {
            long cnt = idx, an = x - 1, a1 = an - cnt + 1;
            sum += cnt * (a1 + an) / 2;
        }
        if (n - idx - 1 > x - 1) {
            long an = x - 1, a1 = 1, cnt = x - 1;
            sum += cnt * (a1 + an) / 2;
            sum += n - idx - 1 - cnt;
        } else {
            long cnt = n - idx - 1, an = x - 1, a1 = an - cnt + 1;
            sum += cnt * (a1 + an) / 2;
        }
        return sum <= maxSum;
    }
};
```
Python 代码:
```Python
class Solution:
    def maxValue(self, n: int, index: int, maxSum: int) -> int:
        l, r = 1, maxSum
        while l < r:
            mid = (l + r + 1) >> 1
            if self.check(n, mid, index, maxSum):
                l = mid
            else:
                r = mid - 1
        return int(r)

    def check(self, n, x, idx, maxSum):
        sumv = x
        if idx > x - 1:
            an, a1, cnt = x - 1, 1, x - 1
            sumv += cnt * (a1 + an) // 2
            sumv += idx - cnt
        else:
            cnt, an, a1 = idx, x - 1, x - idx
            sumv += cnt * (a1 + an) // 2
        if n - idx - 1 > x - 1:
            an, a1, cnt = x - 1, 1, x - 1
            sumv += cnt * (a1 + an) // 2
            sumv += n - idx - 1 - cnt
        else:
            cnt, an, a1 = n - idx - 1, x - 1, x - n + idx + 1
            sumv += cnt * (a1 + an) // 2
        return sumv <= maxSum
```
TypeScript 代码:
```TypeScript
function maxValue(n: number, index: number, maxSum: number): number {
    const check = function(n: number, x: number, idx: number, maxSum: number): boolean {
        let sum = x;
        if (idx > x - 1) {
            let an = x - 1, a1 = 1, cnt = x - 1;
            sum += cnt * (a1 + an) / 2;
            sum += idx - cnt;
        } else {
            let cnt = idx, an = x - 1, a1 = an - cnt + 1;
            sum += cnt * (a1 + an) / 2;
        }
        if (n - idx - 1 > x - 1) {
            let an = x - 1, a1 = 1, cnt = x - 1;
            sum += cnt * (a1 + an) / 2;
            sum += n - idx - 1 - cnt;
        } else {
            let cnt = n - idx - 1, an = x - 1, a1 = an - cnt + 1;
            sum += cnt * (a1 + an) / 2;
        }
        return sum <= maxSum;
    };
    let l = 1, r = maxSum;
    while (l < r) {
        let mid = (l + r + 1) >> 1;
        if (check(n, mid, index, maxSum)) l = mid;
        else r = mid - 1;
    }
    return r;
};
```
* 时间复杂度:$O(\log{n})$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


Question:
所有 DNA 都由一系列缩写为 `'A'`,`'C'`,`'G'` 和 `'T'` 的核苷酸组成,例如:`"ACGAATTCCG"`。在研究 DNA 时,识别 DNA 中的重复序列有时会对研究非常有帮助。

编写一个函数来找出所有目标子串,目标子串的长度为 $10$,且在 DNA 字符串 `s` 中出现次数超过一次。

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

输出:["AAAAACCCCC","CCCCCAAAAA"]
```
示例 2:
```
输入:s = "AAAAAAAAAAAAA"

输出:["AAAAAAAAAA"]
```

提示:
* 0 <= s.length <= $10^5$
* `s[i]` 为 `'A'`、`'C'`、`'G'` 或 `'T'`

Solution:
### 滑动窗口 + 哈希表

数据范围只有 $10^5$,一个朴素的想法是:从左到右处理字符串 $s$,使用滑动窗口得到每个以 $s[i]$ 为结尾且长度为 $10$ 的子串,同时使用哈希表记录每个子串的出现次数,如果该子串出现次数超过一次,则加入答案。

为了防止相同的子串被重复添加到答案,而又不使用常数较大的 `Set` 结构。我们可以规定:当且仅当该子串在之前出现过一次(加上本次,当前出现次数为两次)时,将子串加入答案。

代码:
```Java
class Solution {
    public List<String> findRepeatedDnaSequences(String s) {
        List<String> ans = new ArrayList<>();
        int n = s.length();
        Map<String, Integer> map = new HashMap<>();
        for (int i = 0; i + 10 <= n; i++) {
            String cur = s.substring(i, i + 10);
            int cnt = map.getOrDefault(cur, 0);
            if (cnt == 1) ans.add(cur);
            map.put(cur, cnt + 1);
        }
        return ans;
    }
}
```
* 时间复杂度:每次检查以 $s[i]$ 为结尾的子串,需要构造出新的且长度为 $10$ 的字符串。令 $C = 10$,复杂度为 $O(n * C)$
* 空间复杂度:长度固定的子串数量不会超过 $n$ 个。复杂度为 $O(n)$

---

### 字符串哈希 + 前缀和

子串长度为 $10$,因此上述解法的计算量为 $10^6$。

若题目给定的子串长度大于 $100$ 时,加上生成子串和哈希表本身常数操作,那么计算量将超过 $10^7$,会 TLE。

因此一个能够做到严格 $O(n)$ 的做法是使用「字符串哈希 + 前缀和」。

具体做法为,我们使用一个与字符串 $s$ 等长的哈希数组 $h[]$,以及次方数组 $p[]$。

由字符串预处理得到这样的哈希数组和次方数组复杂度为 $O(n)$。当我们需要计算子串 $s[i...j]$ 的哈希值,只需要利用前缀和思想 $h[j] - h[i - 1] * p[j - i + 1]$ 即可在 $O(1)$ 时间内得出哈希值(与子串长度无关)。

**到这里,还有一个小小的细节需要注意:如果我们期望做到严格 $O(n)$,进行计数的「哈希表」就不能是以 `String` 作为 `key`,只能使用 `Integer`(也就是 hash 结果本身)作为 `key`。因为 Java 中的 `String` 的 `hashCode` 实现是会对字符串进行遍历的,这样哈希计数过程仍与长度有关,而 `Integer` 的 `hashCode` 就是该值本身,这是与长度无关的。**

代码:
```Java
class Solution {
    int N = (int)1e5+10, P = 131313;
    int[] h = new int[N], p = new int[N];
    public List<String> findRepeatedDnaSequences(String s) {
        int n = s.length();
        List<String> ans = new ArrayList<>();
        p[0] = 1;
        for (int i = 1; i <= n; i++) {
            h[i] = h[i - 1] * P + s.charAt(i - 1);
            p[i] = p[i - 1] * P;
        }
        Map<Integer, Integer> map = new HashMap<>();
        for (int i = 1; i + 10 - 1 <= n; i++) {
            int j = i + 10 - 1;
            int hash = h[j] - h[i - 1] * p[j - i + 1];
            int cnt = map.getOrDefault(hash, 0);
            if (cnt == 1) ans.add(s.substring(i - 1, i + 10 - 1));
            map.put(hash, cnt + 1);
        }
        return ans;
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n)$

---

### 我猜你问

1. 字符串哈希的「构造 $p$ 数组」和「计算哈希」的过程,不会溢出吗?

会溢出,溢出就会变为负数,当且仅当两个哈希值溢出程度与 `Integer.MAX_VALUE` 呈不同的倍数关系时,会产生错误结果(哈希冲突),此时考虑修改 $P$ 或者采用表示范围更大的 `long` 来代替 `int`。

2. $P = 131313$ 这个数字是怎么来的?

WA 出来的,刚开始使用的 $P = 131$,被卡在了 $30/31$ 个样例。

字符串哈希本身存在哈希冲突的可能,一般会在尝试 $131$ 之后尝试使用 $13131$,然后再尝试使用比 $13131$ 更大的质数。

### 最后

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

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

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

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

```

```


Question:
颠倒给定的 32 位无符号整数的二进制位。

提示:

* 请注意,在某些语言(如 Java)中,没有无符号整数类型。在这种情况下,输入和输出都将被指定为有符号整数类型,并且不应影响您的实现,因为无论整数是有符号的还是无符号的,其内部的二进制表示形式都是相同的。
* 在 Java 中,编译器使用二进制补码记法来表示有符号整数。因此,在上面的 示例 2 中,输入表示有符号整数 -3,输出表示有符号整数 -1073741825。

进阶:
* 如果多次调用这个函数,你将如何优化你的算法?

示例 1:
```
输入: 00000010100101000001111010011100

输出: 00111001011110000010100101000000

解释: 输入的二进制串 00000010100101000001111010011100 表示无符号整数 43261596,
     因此返回 964176192,其二进制表示形式为 00111001011110000010100101000000。
```
示例 2:
```
输入:11111111111111111111111111111101
输出:10111111111111111111111111111111
解释:输入的二进制串 11111111111111111111111111111101 表示无符号整数 4294967293,
     因此返回 3221225471 其二进制表示形式为 10111111111111111111111111111111 。
```

提示:
* 输入是一个长度为 32 的二进制字符串

Solution:
### 「对称位」构造

一个简单的做法是对输入的 $n$ 做诸位检查。

**如果某一位是 1 的话,则将答案相应的对称位置修改为 1。**

代码:
```Java
public class Solution {
    public int reverseBits(int n) {
        int ans = 0;
        for (int i = 0; i < 32; i++) {
            int t = (n >> i) & 1;
            if (t == 1) {
                ans |= (1 << (31 - i));
            }
        }
        return ans;
    }
}
```
* 时间复杂度:$int$ 固定 32 位,循环次数不随输入样本发生改变。复杂度为 $O(1)$
* 空间复杂度:$O(1)$

***

### 「逐位分离」构造

另外一种做法是,每次都使用 $n$ 的最低一位,使用 $n$ 的最低一位去更新答案的最低一位,使用完将 $n$ 进行右移一位,将答案左移一位。

**相当于每次都用 $n$ 的最低一位更新成 $ans$ 的最低一位。**

代码:
```Java
public class Solution {
    public int reverseBits(int n) {
        int ans = 0;
        int cnt = 32;
        while (cnt-- > 0) {
            ans <<= 1;
            ans += (n & 1);
            n >>= 1;
        }
        return ans;
    }
}
```
* 时间复杂度:$int$ 固定 32 位,循环次数不随输入样本发生改变。复杂度为 $O(1)$
* 空间复杂度:$O(1)$

***

### 「分组互换」

事实上,可以对于长度固定的 $int$ 类型,我们可以使用「分组构造」的方式进行。

**两位互换 -> 四位互换 -> 八位互换 -> 十六位互换。**

代码:
```Java
public class Solution {
    public int reverseBits(int n) {
        n = ((n & 0xAAAAAAAA) >>> 1)  | ((n & 0x55555555) << 1);
        n = ((n & 0xCCCCCCCC) >>> 2)  | ((n & 0x33333333) << 2);
        n = ((n & 0xF0F0F0F0) >>> 4)  | ((n & 0x0F0F0F0F) << 4);
        n = ((n & 0xFF00FF00) >>> 8)  | ((n & 0x00FF00FF) << 8);
        n = ((n & 0xFFFF0000) >>> 16) | ((n & 0x0000FFFF) << 16);
        return n;
    }
}
```
* 时间复杂度:如何进行互换操作取决于 $int$ 长度。复杂度为 $O(1)$
* 空间复杂度:$O(1)$

**PS. 类似的做法我在 [191. 位1的个数](https://leetcode-cn.com/problems/number-of-1-bits/solution/yi-ti-san-jie-wei-shu-jian-cha-you-yi-to-av1r/) 也介绍过。如果大家学有余力的话,建议大家在纸上模拟一下这个过程。如果不想深入,也可以当成模板背过(写法非常固定)。**
**但请不要认为「方法三」一定就比「方法一」等直接采用循环的方式更快。此类做法的最大作用,不是处理 int,而是处理更大位数的情况,在长度只有 32 位的 int 的情况下,该做法不一定就比循环要快(该做法会产生多个的中间结果,导致赋值发生多次,而且由于指令之间存在对 n 数值依赖,可能不会被优化为并行指令),这个道理和对于排序元素少的情况下,我们会选择「冒泡排序」而不是「归并排序」是一样的,因为「冒泡排序」常数更小。**

### 最后

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

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

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

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

```

```


Question:
句子 是一个单词列表,列表中的单词之间用单个空格隔开,且不存在前导或尾随空格。

每个单词仅由大小写英文字母组成(不含标点符号)。

例如,`"Hello World"`、`"HELLO"` 和 `"hello world hello world"` 都是句子。
给你一个句子 `s` 和一个整数 `k` ,请你将 `s` 截断 ,使截断后的句子仅含 前 `k` 个单词。返回 截断 `s` 后得到的句子。

示例 1:
```
输入:s = "Hello how are you Contestant", k = 4

输出:"Hello how are you"

解释:
s 中的单词为 ["Hello", "how" "are", "you", "Contestant"]
前 4 个单词为 ["Hello", "how", "are", "you"]
因此,应当返回 "Hello how are you"
```
示例 2:
```
输入:s = "What is the solution to this problem", k = 4

输出:"What is the solution"

解释:
s 中的单词为 ["What", "is" "the", "solution", "to", "this", "problem"]
前 4 个单词为 ["What", "is", "the", "solution"]
因此,应当返回 "What is the solution"
```
示例 3:
```
输入:s = "chopper is not a tanuki", k = 5

输出:"chopper is not a tanuki"
```

提示:
* `1 <= s.length <= 500`
* `k` 的取值范围是 `[1,  s 中单词的数目]`
* `s` 仅由大小写英文字母和空格组成
* `s` 中的单词之间由单个空格隔开
* 不存在前导或尾随空格

Solution:
### 模拟

根据题意进行模拟,在拼接答案时对「空格」进行计数即可。

可通过 `StringBuilder` 实现拼接,也可以使用变量 `idx` 记录答案的结尾位置。

代码:
```Java
class Solution {
    public String truncateSentence(String s, int k) {
        StringBuilder sb = new StringBuilder();
        int n = s.length();
        for (int i = 0, cnt = 0; i < n && cnt < k; i++) {
            char c = s.charAt(i);
            if (c == ' ') cnt++;
            if (cnt < k) sb.append(c);
        }
        return sb.toString();
    }
}
```

```Java
class Solution {
    public String truncateSentence(String s, int k) {
        int n = s.length(), idx = 0;
        while (idx < n) {
            if (s.charAt(idx) == ' ' && --k == 0) break;
            idx++;
        }
        return s.substring(0, idx);
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


Question:
给你两个正整数数组 `nums1` 和 `nums2` ,数组的长度都是 `n` 。

数组 `nums1` 和 `nums2` 的 **绝对差值和** 定义为所有 `|nums1[i] - nums2[i]|(0 <= i < n)`的 总和(下标从 $0$ 开始)。

你可以选用 `nums1` 中的 **任意一个** 元素来替换 `nums1` 中的 **至多** 一个元素,以 **最小化** 绝对差值和。

在替换数组 `nums1` 中最多一个元素 之后 ,返回最小绝对差值和。因为答案可能很大,所以需要对 $10^9 + 7$ 取余 后返回。

`|x|` 定义为:

* 如果 $x >= 0$,值为 `x` ,或者
* 如果 $x <= 0$,值为 `-x`

示例 1:

```
输入:nums1 = [1,7,5], nums2 = [2,3,5]

输出:3

解释:有两种可能的最优方案:
- 将第二个元素替换为第一个元素:[1,7,5] => [1,1,5] ,或者
- 将第二个元素替换为第三个元素:[1,7,5] => [1,5,5]
两种方案的绝对差值和都是 |1-2| + (|1-3| 或者 |5-3|) + |5-5| = 3
```
示例 2:
```
输入:nums1 = [2,4,6,8,10], nums2 = [2,4,6,8,10]

输出:0

解释:nums1 和 nums2 相等,所以不用替换元素。绝对差值和为 0
```
示例 3:
```
输入:nums1 = [1,10,4,4,2,7], nums2 = [9,3,5,1,7,4]

输出:20

解释:将第一个元素替换为第二个元素:[1,10,4,4,2,7] => [10,10,4,4,2,7]
绝对差值和为 |10-9| + |10-3| + |4-5| + |4-1| + |2-7| + |7-4| = 20
```

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

Solution:
### 二分

这是一道二分陈题,具体做法如下:

我们在进行处理前,先对 $nums1$ 进行拷贝并排序,得到 $sorted$ 数组。

然后 **在遍历 $nums1$ 和 $nums2$ 计算总的差值 $sum$ 时,通过对 $sorted$ 进行二分查找,找到最合适替换 $nums[i]$ 的值**。

具体的,当我们处理到第 $i$ 位时,假设该位的原差值为 $x = abs(nums1[i] - nums2[i])$,然后从 $sorted$ 数组中通过二分找到最接近 $nums2[i]$ 的值,计算一个新的差值 $nd$(注意要检查分割点与分割点的下一位),如果满足 $nd < x$ 说明存在一个替换方案使得差值变小,我们使用变量 $max$ 记下来这个替换方案所带来的变化,并不断更新 $max$。

当整个数组被处理完,$max$ 存储着最优方案对应的差值变化,此时 $sum - max$ 即是答案。

代码:
```Java
class Solution {
    int mod = (int)1e9+7;
    public int minAbsoluteSumDiff(int[] nums1, int[] nums2) {
        int n = nums1.length;
        int[] sorted = nums1.clone();
        Arrays.sort(sorted);
        long sum = 0, max = 0;
        for (int i = 0; i < n; i++) {
            int a = nums1[i], b = nums2[i];
            if (a == b) continue;
            int x = Math.abs(a - b);
            sum += x;
            int l = 0, r = n - 1;
            while (l < r) {
                int mid = l + r + 1 >> 1;
                if (sorted[mid] <= b) l = mid;
                else r = mid - 1;
            }
            int nd = Math.abs(sorted[r] - b);
            if (r + 1 < n) nd = Math.min(nd, Math.abs(sorted[r + 1] - b));
            if (nd < x) max = Math.max(max, x - nd);
        }
        return (int)((sum - max) % mod);
    }
}
```
* 时间复杂度:对 `sorted` 进行拷贝并排序的复杂度为 $O(n\log{n})$;遍历处理数组时会一边统计,一边尝试二分,找最合适的替换数值,复杂度为 $O(n\log{n})$。整体复杂度为 $O(n\log{n})$
* 空间复杂度:使用 `sorted` 数组需要 $O(n)$ 的空间复杂度,同时排序过程中会使用 $O(\log{n})$ 的空间复杂度;整体复杂度为 $O(n + \log{n})$

### 最后

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

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

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

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


LeetCode/1821-1830/1822. 数组元素积的符号（简单）.md

1822. 数组元素积的符号

Question:
已知函数 `signFunc(x)` 将会根据 `x` 的正负返回特定值:

* 如果 `x` 是正数,返回 `1` 。
* 如果 `x` 是负数,返回 `-1` 。
* 如果 `x` 是等于 `0` ,返回 `0` 。

给你一个整数数组 `nums`。令 `product` 为数组 `nums` 中所有元素值的乘积。

返回 `signFunc(product)` 。

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

输出:1

解释:数组中所有值的乘积是 144 ,且 signFunc(144) = 1
```
示例 2:
```
输入:nums = [1,5,0,2,-3]

输出:0

解释:数组中所有值的乘积是 0 ,且 signFunc(0) = 0
```
示例 3:
```
输入:nums = [-1,1,-1,1,-1]

输出:-1

解释:数组中所有值的乘积是 -1 ,且 signFunc(-1) = -1
```

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

Solution:
### 模拟

根据题意进行模拟。

Java 代码:
```Java
class Solution {
    public int arraySign(int[] nums) {
        int ans = 1;
        for (int x : nums) {
            if (x == 0) return 0;
            if (x < 0) ans *= -1;
        }
        return ans;
    }
}
```
TypeScript 代码:
```TypeScript
function arraySign(nums: number[]): number {
    let ans = 0
    for (const x of nums) {
        if (x == 0) return 0
        if (x < 0) ans *= -1
    }
    return ans
}
```
Python 代码:
```Python
class Solution:
    def arraySign(self, nums: List[int]) -> int:
        ans = 1
        for x in nums:
            if x == 0:
                return 0
            if x < 0:
                ans *= -1
        return ans
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


LeetCode/1821-1830/1823. 找出游戏的获胜者（中等）.md

1823. 找出游戏的获胜者

Question:
共有 $n$ 名小伙伴一起做游戏。小伙伴们围成一圈,按 顺时针顺序 从 $1$ 到 $n$ 编号。确切地说,从第 $i$ 名小伙伴顺时针移动一位会到达第 ($i+1$) 名小伙伴的位置,其中 $1 <= i < n$ ,从第 $n$ 名小伙伴顺时针移动一位会回到第 $1$ 名小伙伴的位置。

游戏遵循如下规则:

1. 从第 $1$ 名小伙伴所在位置 开始 。
2. 沿着顺时针方向数 $k$ 名小伙伴,计数时需要 包含 起始时的那位小伙伴。逐个绕圈进行计数,一些小伙伴可能会被数过不止一次。
3. 你数到的最后一名小伙伴需要离开圈子,并视作输掉游戏。
4. 如果圈子中仍然有不止一名小伙伴,从刚刚输掉的小伙伴的 顺时针下一位 小伙伴 开始,回到步骤 2 继续执行。
5. 否则,圈子中最后一名小伙伴赢得游戏。

给你参与游戏的小伙伴总数 $n$ ,和一个整数 $k$ ,返回游戏的获胜者。

示例 1:

```
输入:n = 5, k = 2

输出:3

解释:游戏运行步骤如下:
1) 从小伙伴 1 开始。
2) 顺时针数 2 名小伙伴,也就是小伙伴 1 和 2 。
3) 小伙伴 2 离开圈子。下一次从小伙伴 3 开始。
4) 顺时针数 2 名小伙伴,也就是小伙伴 3 和 4 。
5) 小伙伴 4 离开圈子。下一次从小伙伴 5 开始。
6) 顺时针数 2 名小伙伴,也就是小伙伴 5 和 1 。
7) 小伙伴 1 离开圈子。下一次从小伙伴 3 开始。
8) 顺时针数 2 名小伙伴,也就是小伙伴 3 和 5 。
9) 小伙伴 5 离开圈子。只剩下小伙伴 3 。所以小伙伴 3 是游戏的获胜者。
```
示例 2:
```
输入:n = 6, k = 5

输出:1

解释:小伙伴离开圈子的顺序:5、4、6、2、3 。小伙伴 1 是游戏的获胜者。
```

提示:
* $1 <= k <= n <= 500$

Solution:
### 模拟

利用数据范围 $1 <= k <= n <= 500$,我们可以直接根据规则进行模拟。

创建一个标记数组 $vis$,若有 $vis[idx] = true$ 则代表点编号为 $idx$ 已被淘汰,每次我们都从当前位置 $cur$ 开始,找到第 $k$ 个尚未淘汰的点($vis[idx] = false$),并将其进行标记($vis[idx] = true$),共有 $n - 1$ 个点需要被淘汰。

一些细节,为了方便取模,我们调整点编号从 $1$ 开始,在返回答案时再重新调整为从 $1$ 开始。

代码:
```Java
class Solution {
    public int findTheWinner(int n, int k) {
        boolean[] vis = new boolean[n + 10];
        int cnt = 0, cur = 0;
        while (cnt != n - 1) {
            for (int j = 0; j < k - 1; j++) {
                cur++;
                while (vis[cur % n]) cur++;
            }
            vis[cur % n] = true;
            cnt++; cur++;
            while (vis[cur % n]) cur++;
        }
        return (cur % n) + 1;
    }
}
```
* 时间复杂度:要消除(被标记)的点数量为 $n - 1$,每次找到要消除的点编号,最多遍历 $n$ 个点,复杂度为 $O(n^2)$
* 空间复杂度:$O(n)$

---

### 约瑟夫环

这还是一道约瑟夫环经典题。

> 另外一道同款题在 [这里](https://mp.weixin.qq.com/s?__biz=MzU4NDE3MTEyMA==&mid=2247490948&idx=1&sn=a71c4ec3529277ad119702d7dacf510e&chksm=fd9cb69bcaeb3f8d5461518a15d969b45a13558a4c5810100f40200a12aac66f1876f2ef6de7&token=831443013&lang=zh_CN#rd) 🎉🎉

每次往同一方向,以固定步长 $k$ 进行消数。由于下一次操作的发起点为消除位置的下一个点(即前后两次操作发起点在原序列下标中相差 $k$),同时问题规模会从 $n$ 变为 $n - 1$,因此原问题答案等价于 `findTheWinner(n - 1, k) + k`。

一些细节,由于编号从 $1$ 开始,在返回答案时我们需要将结果为 $0$ 的值映射回编号 $n$。

代码:
```Java
class Solution {
    public int findTheWinner(int n, int k) {
        if (n <= 1) return n;
        int ans = (findTheWinner(n - 1, k) + k) % n;
        return ans == 0 ? n : ans;
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


1833. 雪糕的最大数量

Question:
夏日炎炎,小男孩 Tony 想买一些雪糕消消暑。

商店中新到 `n` 支雪糕,用长度为 n 的数组 `costs` 表示雪糕的定价,其中 `costs[i]` 表示第 `i` 支雪糕的现金价格。

Tony 一共有 `coins` 现金可以用于消费,他想要买尽可能多的雪糕。

给你价格数组 `costs` 和现金量 `coins`,请你计算并返回 Tony 用 `coins` 现金能够买到的雪糕的 最大数量 。

注意:Tony 可以按任意顺序购买雪糕。

示例 1:
```
输入:costs = [1,3,2,4,1], coins = 7

输出:4

解释:Tony 可以买下标为 0、1、2、4 的雪糕,总价为 1 + 3 + 2 + 1 = 7
```
示例 2:
```
输入:costs = [10,6,8,7,7,8], coins = 5

输出:0

解释:Tony 没有足够的钱买任何一支雪糕。
```
```
示例 3:

输入:costs = [1,6,3,1,2,5],

输出:6

解释:Tony 可以买下所有的雪糕,总价为 1 + 6 + 3 + 1 + 2 + 5 = 18 。
```

提示:
* $costs.length = n$
* $1 <= n <= 10^5$
* $1 <= costs[i] <= 10^5$
* $1 <= coins <= 10^8$

Solution:
### 基本分析

从题面看,是一道「01 背包」问题,每个物品的成本为 $cost[i]$,价值为 $1$。

但「01 背包」的复杂度为 $O(N\times C)$,其中 $N$ 为物品数量(数量级为 $10^5$),$C$ 为背包容量(数量级为 $10^8$)。显然会 `TLE`。

换个思路发现,每个被选择的物品对答案的贡献都是 $1$,优先选择价格小的物品会使得我们剩余金额尽可能的多,将来能够做的决策方案也就相应变多。

因此一个直观的做法是,对物品数组进行「从小到大」排序,然后「从前往后」开始决策购买。

---

### 证明

直观上,这样的贪心思路可以使得最终选择的物品数量最多。

接下来证明一下该思路的正确性。

假定贪心思路取得的序列为 $[a1,a2,a3,...,an]$(长度为 $n$),真实最优解所取得的序列为 $[b1,b2,b3,..,bm]$(长度为 $m$)。

两个序列均为「单调递增序列」。

其中最优解所对应具体方案不唯一,即存在多种选择方案使得物品数量相同。

因此,**我们只需要证明两个序列长度一致即可**。

按照贪心逻辑,最终选择的方案总成本不会超过 $coins$,因此至少是一个合法的选择方案,天然有 $n \leq m$,只需要证明 $n \geq m$ 成立,即可得证 $n = m$。

通过反证法证明 $n \geq m$ 成立,假设 $n \geq m$ 不成立,即有 $n < m$。

根据贪心决策,**我们选择的物品序列在「排序好的 $cost$ 数组」里,必然是一段连续的前缀。并且再选择下一物品将会超过总费用 $coins$;而真实最优解的选择方案在「排序好的 $cost$ 数组」里分布不定。**

这时候我们可以利用「**每个物品对答案的贡献均为 $1$,将最优解中的分布靠后的物品,替换为分布较前的物品,不会使得费用增加,同时答案不会变差**」。

从而将真实最优解也调整为某段连续的前缀。

**综上,通过反证法得证 $n \geq m$ 成立,结合 $n \leq m$,可推出 $n = m$。**

**即贪心解必然能够取得与最优解一样的长度。**

---

### 贪心

排序,从前往后决策,直到不能决策为止。

Java 代码:
```Java
class Solution {
    public int maxIceCream(int[] costs, int coins) {
        int n = costs.length;
        Arrays.sort(costs);
        int ans = 0;
        for (int i = 0; i < n; i++) {
            if (coins >= costs[i]) {
                ans++; coins -= costs[i];
            }
        }
        return ans;
    }
}
```
Python 代码:
```Python
class Solution:
    def maxIceCream(self, costs: List[int], coins: int) -> int:
        costs.sort()
        ans = 0
        for i in range(len(costs)):
            if coins >= costs[i]:
                ans += 1
                coins -= costs[i]
        return ans
```
C++ 代码:
```C++
class Solution {
public:
    int maxIceCream(vector<int>& costs, int coins) {
        int n = costs.size();
        sort(costs.begin(), costs.end());
        int ans = 0;
        for (int i = 0; i < n; i++) {
            if (coins >= costs[i]) {
                ans++; coins -= costs[i];
            }
        }
        return ans;
    }
};
```
TypeScript 代码:
```TypeScript
function maxIceCream(costs: number[], coins: number): number {
    costs.sort((a, b) => a - b);
    let ans = 0;
    for (let i = 0; i < costs.length; i++) {
        if (coins >= costs[i]) {
            ans++; coins -= costs[i];
        }
    }
    return ans;
};
```
* 时间复杂度:排序复杂度为 $O(n\log{n})$;获取答案的复杂度为 $O(n)$。整体复杂度为 $O(n\log{n})$
* 空间复杂度:排序复杂度为 $O(\log{n})$。整体复杂度为 $O(\log{n})$

PS. 这里假定 `Arrays.sort` 使用的是「双轴排序」的实现。

### 最后

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

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

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

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


Question:
给你一个二维数组 $tasks$,用于表示 $n$ 项从 $0$ 到 $n - 1$ 编号的任务。

其中 $tasks[i] = [enqueueTime_i, processingTime_i]$ 意味着第 $i$ 项任务将会于 $enqueueTime_i$ 时进入任务队列,需要 $processingTime_i$ 的时长完成执行。

现有一个单线程 CPU ,同一时间只能执行**最多一项**任务,该 CPU 将会按照下述方式运行:
* 如果 CPU 空闲,且任务队列中没有需要执行的任务,则 CPU 保持空闲状态。
* 如果 CPU 空闲,但任务队列中有需要执行的任务,则 CPU 将会选择 执行时间最短 的任务开始执行。如果多个任务具有同样的最短执行时间,则选择下标最小的任务开始执行。
* 一旦某项任务开始执行,CPU 在 执行完整个任务 前都不会停止。
* CPU 可以在完成一项任务后,立即开始执行一项新任务。

返回 CPU 处理任务的顺序。

示例 1:

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

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

解释:事件按下述流程运行:
- time = 1 ,任务 0 进入任务队列,可执行任务项 = {0}
- 同样在 time = 1 ,空闲状态的 CPU 开始执行任务 0 ,可执行任务项 = {}
- time = 2 ,任务 1 进入任务队列,可执行任务项 = {1}
- time = 3 ,任务 2 进入任务队列,可执行任务项 = {1, 2}
- 同样在 time = 3 ,CPU 完成任务 0 并开始执行队列中用时最短的任务 2 ,可执行任务项 = {1}
- time = 4 ,任务 3 进入任务队列,可执行任务项 = {1, 3}
- time = 5 ,CPU 完成任务 2 并开始执行队列中用时最短的任务 3 ,可执行任务项 = {1}
- time = 6 ,CPU 完成任务 3 并开始执行任务 1 ,可执行任务项 = {}
- time = 10 ,CPU 完成任务 1 并进入空闲状态
```
示例 2:
```
输入:tasks = [[7,10],[7,12],[7,5],[7,4],[7,2]]

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

解释:事件按下述流程运行:
- time = 7 ,所有任务同时进入任务队列,可执行任务项  = {0,1,2,3,4}
- 同样在 time = 7 ,空闲状态的 CPU 开始执行任务 4 ,可执行任务项 = {0,1,2,3}
- time = 9 ,CPU 完成任务 4 并开始执行任务 3 ,可执行任务项 = {0,1,2}
- time = 13 ,CPU 完成任务 3 并开始执行任务 2 ,可执行任务项 = {0,1}
- time = 18 ,CPU 完成任务 2 并开始执行任务 0 ,可执行任务项 = {1}
- time = 28 ,CPU 完成任务 0 并开始执行任务 1 ,可执行任务项 = {}
- time = 40 ,CPU 完成任务 1 并进入空闲状态
```

提示:
* tasks.length == n
* 1 <= n <= $10^5$
* 1 <= $enqueueTime_i$, $processingTime_i$ <= $10^9$

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

先将 $tasks$ 按照「入队时间」进行升序排序,同时为了防止任务编号丢失,排序前需要先将二元组的 $tasks$ 转存为三元组,新增记录的是原任务编号。

然后可以按照「时间线」进行模拟:

1. 起始令 $time$ 从 $1$ 开始进行递增,每次将到达「入队时间」的任务进行入队;

2. 判断当前队列是否有可以执行的任务:
   1. 如果没有,说明还没到达下一个入队任务的入队时间,直接将 $times$ 快进到下一个入队任务的入队时间;
   2. 如果有,从队列中取出任务执行,同时由于是单线程执行,在该任务结束前,不会有新任务被执行,将 $times$ 快进到该任务的结束时间。

代码:
```Java
class Solution {
    public int[] getOrder(int[][] ts) {
        int n = ts.length;
        // 将 ts 转存成 nts,保留任务编号
        int[][] nts = new int[n][3];
        for (int i = 0; i < n; i++) nts[i] = new int[]{ts[i][0], ts[i][1], i};
        // 根据任务入队时间进行排序
        Arrays.sort(nts, (a,b)->a[0]-b[0]);
        // 根据题意,先按照「持续时间」排序,再根据「任务编号」排序
        PriorityQueue<int[]> q = new PriorityQueue<>((a,b)->{
            if (a[1] != b[1]) return a[1] - b[1];
            return a[2] - b[2];
        });
        int[] ans = new int[n];
        for (int time = 1, j = 0, idx = 0; idx < n; ) {
        	// 如果当前任务可以添加到「队列」中(满足入队时间)则进行入队
            while (j < n && nts[j][0] <= time) q.add(nts[j++]);
            if (q.isEmpty()) {
	            // 如果当前「队列」没有任务,直接跳到下个任务的入队时间
                time = nts[j][0];
            } else {
            	// 如果有可执行任务的话,根据优先级将任务出队(记录下标),并跳到该任务完成时间点
                int[] cur = q.poll();
                ans[idx++] = cur[2];
                time += cur[1];
            }
        }
        return ans;
    }
}
```
* 时间复杂度:将 $ts$ 转存成 $nts$ 的复杂度为 $O(n)$;对 $nts$ 排序复杂度为 $O(n\log{n})$;模拟时间线,将任务进行入队出队操作,并构造最终答案复杂度为 $O(n\log{n})$。整体复杂度为 $O(n\log{n})$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


LeetCode/1831-1840/1838. 最高频元素的频数（中等）.md

1838. 最高频元素的频数

Question:
元素的**频数**是该元素在一个数组中出现的次数。

给你一个整数数组 $nums$ 和一个整数 $k$ 。

在一步操作中,你可以选择 $nums$ 的一个下标,并将该下标对应元素的值增加 $1$ 。

执行最多 $k$ 次操作后,返回数组中最高频元素的**最大可能频数**。

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

输出:3

解释:对第一个元素执行 3 次递增操作,对第二个元素执 2 次递增操作,此时 nums = [4,4,4] 。
4 是数组中最高频元素,频数是 3 。
```
示例 2:
```
输入:nums = [1,4,8,13], k = 5

输出:2

解释:存在多种最优解决方案:
- 对第一个元素执行 3 次递增操作,此时 nums = [4,4,8,13] 。4 是数组中最高频元素,频数是 2 。
- 对第二个元素执行 4 次递增操作,此时 nums = [1,8,8,13] 。8 是数组中最高频元素,频数是 2 。
- 对第三个元素执行 5 次递增操作,此时 nums = [1,4,13,13] 。13 是数组中最高频元素,频数是 2 。
```
示例 3:
```
输入:nums = [3,9,6], k = 2

输出:1
```

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

Solution:
### 枚举

一个朴素的做法是,先对原数组 $nums$ 进行排序,然后枚举最终「频数对应值」是哪个。

利用每次操作只能对数进行加一,我们可以从「频数对应值」开始往回检查,从而得出在操作次数不超过 $k$ 的前提下,以某个值作为「频数对应值」最多能够凑成多少个。

算法整体复杂度为 $O(n^2)$,Java 2021/07/19 可过。

代码:
```Java
class Solution {
    public int maxFrequency(int[] nums, int k) {
        int n = nums.length;
        Map<Integer, Integer> map = new HashMap<>();
        for (int i : nums) map.put(i, map.getOrDefault(i, 0) + 1);
        List<Integer> list = new ArrayList<>(map.keySet());
        Collections.sort(list);
        int ans = 1;
        for (int i = 0; i < list.size(); i++) {
            int x = list.get(i), cnt = map.get(x);
            if (i > 0) {
                int p = k;
                for (int j = i - 1; j >= 0; j--) {
                    int y = list.get(j);
                    int diff = x - y;
                    if (p >= diff) {
                        int add = p / diff;
                        int min = Math.min(map.get(y), add);
                        p -= min * diff;
                        cnt += min;
                    } else {
                        break;
                    }
                }
            }
            ans = Math.max(ans, cnt);
        }
        return ans;
    }
}
```
* 时间复杂度:得到去重后的频数后选集合复杂度为 $O(n)$;最坏情况下去重后仍有 $n$ 个频数,且判断 $k$ 次操作内某个频数最多凑成多少复杂度为 $O(n)$。整体复杂度为 $O(n^2)$
* 空间复杂度:$O(n)$

---

### 排序 + 前缀和 + 二分 + 滑动窗口

**先对原数组 $nums$ 进行从小到大排序,如果存在真实最优解 $len$,意味着至少存在一个大小为 $len$ 的区间 $[l, r]$,使得在操作次数不超过 $k$ 的前提下,区间 $[l, r]$ 的任意值 $nums[i]$ 的值调整为 $nums[r]$。**

这引导我们利用「数组有序」&「前缀和」快速判断「某个区间 $[l, r]$ 是否可以在 $k$ 次操作内将所有值变为 $nums[r]$」:

具体的,我们可以二分答案 $len$ 作为窗口长度,利用前缀和我们可以在 $O(1)$ 复杂度内计算任意区间的和,同时由于每次操作只能对数进行加一,即窗口内的所有数最终变为 $nums[r]$ ,最终目标区间和为 $nums[r] * len$,通过比较目标区间和和真实区间和的差值,我们可以知道 $k$ 次操作是否能将当前区间变为 $nums[r]$。

上述判断某个值 $len$ 是否可行的 `check` 操作复杂度为 $O(n)$,因此算法复杂度为 $O(n\log{n})$。

代码:
```Java
class Solution {
    int[] nums, sum;
    int n, k;
    public int maxFrequency(int[] _nums, int _k) {
        nums = _nums;
        k = _k;
        n = nums.length;
        Arrays.sort(nums);
        sum = new int[n + 1];
        for (int i = 1; i <= n; i++) sum[i] = sum[i - 1] + nums[i - 1];
        int l = 0, r = n;
        while (l < r) {
            int mid = l + r + 1 >> 1;
            if (check(mid)) l = mid;
            else r = mid - 1;
        }
        return r;
    }
    boolean check(int len) {
        for (int l = 0; l + len - 1 < n; l++) {
            int r = l + len - 1;
            int cur = sum[r + 1] - sum[l];
            int t = nums[r] * len;
            if (t - cur <= k) return true;
        }
        return false;
    }
}
```
* 时间复杂度:排序的复杂度为 $O(n\log{n})$;计算前缀和数组复杂度为 $O(n)$;`check` 函数的复杂度为 $O(n)$,因此二分复杂度为 $O(n\log{n})$。整体复杂度为 $O(n\log{n})$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


LeetCode/1841-1850/1846. 减小和重新排列数组后的最大元素（中等）.md

1846. 减小和重新排列数组后的最大元素

Question:
给你一个正整数数组 `arr`。请你对 `arr` 执行一些操作(也可以不进行任何操作),使得数组满足以下条件:
* `arr` 中 第一个 元素必须为 $1$ 。
* 任意相邻两个元素的差的绝对值 小于等于 $1$ ,也就是说,对于任意的 $1 <= i < arr.length$ (数组下标从 $0$ 开始),都满足 `abs(arr[i] - arr[i - 1]) <= 1`。`abs(x)` 为 `x` 的绝对值。

你可以执行以下 $2$ 种操作任意次:
* 减小 `arr` 中任意元素的值,使其变为一个 更小的正整数 。
* 重新排列 `arr` 中的元素,你可以以任意顺序重新排列。

请你返回执行以上操作后,在满足前文所述的条件下,`arr` 中可能的 最大值 。

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

输出:2

解释:
我们可以重新排列 arr 得到 [1,2,2,2,1] ,该数组满足所有条件。
arr 中最大元素为 2 。
```
示例 2:
```
输入:arr = [100,1,1000]

输出:3

解释:
一个可行的方案如下:
1. 重新排列 arr 得到 [1,100,1000] 。
2. 将第二个元素减小为 2 。
3. 将第三个元素减小为 3 。
现在 arr = [1,2,3] ,满足所有条件。
arr 中最大元素为 3 。
```
示例 3:
```
输入:arr = [1,2,3,4,5]

输出:5

解释:数组已经满足所有条件,最大元素为 5 。
```

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

Solution:
### 基本分析 & 证明

根据题意,数组的第一位必须是 $1$,且每个数只能 **减小** 或 **不变**,数值位置可以任意调整。

求解经过调整后,符合要求的数组中的最大值是多少。

首先符合条件的数组相邻位差值绝对值不超过 $1$,这限定了数组的必然是如下三种分布之一:

* (非严格)单调递减
* 存在波段
* (非严格)单调递增

**证明一:取得最优解对应的数组「必然是」或者「可调整为」(非严格)单调递增的形式。**

我们使用反证法来证明另外两种分布不能取得最优解:

* (非严格)单调递减:题目限定了数的范围为正整数,且第一位为 $1$,这种情况不用讨论了,跳过;
* 存在波段:我们始终可以将波峰的右侧出现的值,纳入到波峰的左侧,从而消掉这个波峰,最终将整个分布调整为「(非严格)单调递增」的形式,结果不会变差:

多个波段的情况也是同理,可以自己在纸上画画。

都是利用 **波峰右侧的点可以调整成波峰左侧的点,从而使分布变为(非严格)单调递增。**

**至此,我们证明了最优解对应的数组必然符合(非严格)单调递增。**

这启发我们可以先对原数组排个序,在此基础上进行分析。

对原数组排序得到的有序数组,不一定是符合「相邻位差值绝对值不超过 $1$」的,同时由于每个数值可以选择 **减小** 或 **不变**。

**证明二:当必须要对当前位进行调整的时,优先选择调整为「与前一值差值为 $1$ 的较大数」不会比调整为「与前一差值为 $0$ 的较小数」更差。**

这可以使用归纳推理,假设采取「优先调整为与前一值差值为 $1$ 的较大数」得到的序列为 `a`,采用「优先调整与前一差值为 $0$ 的较小数」得到的序列为 `b`。

**根据「$a[0] = b[0] = 1$」、「`a` 和 `b` 长度一致」、「`a` 和 `b` 均为(非严格)单调递增」以及「`a` 和 `b` 均满足相邻位差值不超过 $1$」,可推导出 $sum(a) >= sum(b)$,和任意位置 $a[i] >= b[i]$,从而推导出 `a` 序列的最后一位必然大于等于 `b` 的最后一位。**

即 `b` 不会比 `a` 更优。

**证明三:调整大小的操作不会改变数组元素之间的相对位置关系。**

在证明二的分析中,我们会对某些元素进行“减小”操作,使得整个数组最终满足「相邻位差值绝对值不超过 $1$」。

但该证明成立的还有一个很重要的前提条件,就是调整操作不会出发元素的位置重排。

那么该前提条件是否必然成立呢?答案是必然成立。

假设原排序数组中存在需要调整的点 $i$ 和点 $j$,且 $nums[i] <= nums[j]$。

为了让数组满足条件,它们都进行了“减少”操作的调整,分别变为了 $p$ 和 $q$,如果触发位置重排的话,必然有 $nums[p] >= nums[q]$。

此时,我们能够通过调整它们的变化关系:点 $i$ 变为点 $q$、点 $j$ 变成点 $p$ 来确保同样满足条件,且不触发元素在有序数组中的位置重排。

---

### 贪心

排序,限定第一位值为 $1$,从前往后处理,根据每一位是否「必须修改(与上一位差值是否大于 $1$)」做决策,如果必须被修改,则修改为与前一值差值为 $1$ 的较大数。

代码:
```Java
class Solution {
    public int maximumElementAfterDecrementingAndRearranging(int[] arr) {
        int n = arr.length;
        Arrays.sort(arr);
        arr[0] = 1;
        for (int i = 1; i < n; i++) {
            if (arr[i] - arr[i - 1] > 1) {
                arr[i] = arr[i - 1] + 1;
            }
        }
        return arr[n - 1];
    }
}
```
* 时间复杂度:假定 `Arrays.sort` 使用的是双轴快排实现。复杂度为 $O(n\log{n})$
* 空间复杂度:假定 `Arrays.sort` 使用的是双轴快排实现。复杂度为 $O(\log{n})$

### 最后

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

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

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

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


LeetCode/1871-1880/1877. 数组中最大数对和的最小值（中等）.md

1877. 数组中最大数对和的最小值

Question:
一个数对 (a,b) 的 数对和 等于 a + b 。最大数对和 是一个数对数组中最大的 数对和 。

比方说,如果我们有数对 (1,5) ,(2,3) 和 (4,4),最大数对和 为 max(1+5, 2+3, 4+4) = max(6, 5, 8) = 8 。
给你一个长度为 偶数 n 的数组 nums ,请你将 nums 中的元素分成 n / 2 个数对,使得:

* nums 中每个元素 恰好 在 一个 数对中,且
* 最大数对和 的值 最小 。

请你在最优数对划分的方案下,返回最小的 最大数对和 。

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

输出:7

解释:数组中的元素可以分为数对 (3,3) 和 (5,2) 。
最大数对和为 max(3+3, 5+2) = max(6, 7) = 7 。
```
示例 2:
```
输入:nums = [3,5,4,2,4,6]

输出:8

解释:数组中的元素可以分为数对 (3,5),(4,4) 和 (6,2) 。
最大数对和为 max(3+5, 4+4, 6+2) = max(8, 8, 8) = 8 。
```

提示:
* n == nums.length
* 2 <= n <= $10^5$
* n 是 偶数 。
* 1 <= nums[i] <= $10^5$

Solution:
### 基本分析 & 证明

直觉上,我们会认为「**尽量让“较小数”和“较大数”组成数对,可以有效避免出现“较大数成对”的现象**」。

我们来证明一下该猜想是否成立。

假定 $nums$ 本身有序,由于我们要将 $nums$ 拆分成 $n / 2$ 个数对,根据猜想,我们得到的数对序列为:
$$
(nums[0], nums[n - 1]), (nums[1], nums[n - 2]), ... , (nums[(n / 2) - 1], nums[n / 2])
$$

**换句话说,构成答案的数对必然是较小数取自有序序列的左边,较大数取自有序序列的右边,且与数组中心对称**。

假设最大数对是 $(nums[i], nums[j])$,其中 $i < j$,记两者之和为 $ans = nums[i] + nums[j]$。

反证法证明,不存在别的数对组合会比 $(nums[i], nums[j])$ 更优:

假设存在数对 $(nums[p], nums[q])$ 与 $(nums[i], nums[j])$ 进行调整使答案更优。

接下来分情况讨论:

* 调整为 $(nums[i], nums[p])$ 和 $(nums[q], nums[j])$:此时最大数对答案为 $nums[q] + nums[j]$,显然 $nums[q] + nums[j] >= nums[i] + nums[j] = ans$。我们要最小化最大数对和,因此该调整方案不会让答案更好;
* 调整为 $(nums[i], nums[q])$ 和 $(nums[p], nums[j])$:此时最大数对答案为 $\max(nums[i] + nums[q], nums[p] + nums[j]) = nums[p] + nums[j] >= nums[i] + nums[j] = ans$。我们要最小化最大数对和,因此该调整方案不会让答案更好;

**上述分析可以归纳推理到每一个“非对称”的数对配对中。**

至此我们得证,将原本对称的数对调整为不对称的数对,不会使得答案更优,即贪心解可取得最优解。

---

### 贪心

对原数组 $nums$ 进行排序,然后从一头一尾开始往中间组「数对」,取所有数对中的最大值即是答案。

代码:
```Java
class Solution {
    public int minPairSum(int[] nums) {
        Arrays.sort(nums);
        int n = nums.length;
        int ans = nums[0] + nums[n - 1];
        for (int i = 0, j = n - 1; i < j; i++, j--) {
            ans = Math.max(ans, nums[i] + nums[j]);
        }
        return ans;
    }
}
```
* 时间复杂度:$O(n\log{n})$
* 空间复杂度:$O(\log{n})$

---

### 答疑

关于「证明」部分,不少小伙伴有一些疑问,觉得挺有代表性的,特意加到题解内。

Q1. 「证明」部分是不是缺少了“非对称”得最优的情况?

A1. 并没有,证明的基本思路如下:

1. 猜想对称组数对的方式,会得到最优解;

2. 证明非对称数组不会被对称数对方式更优。

然后我们证明了“非对称方式”不会比“对称方式”更优,因此“对称方式”可以取得最优解。

**至于非对称和非对称之间怎么调整,会更优还是更差,我不关心,也不需要证明,因为已经证明了非对称不会比对称更优。**

Q2. 证明部分的图 $p$、$q$ 是在 $i$、$j$ 内部,那么其他 $p$、$q$、$i$、$j$ 大小关系的情况呢?

A2. 有这个疑问,说明没有重点理解「证明」中的加粗部分(原话):

> **上述分析可以归纳推理到每一个“非对称”的数对配对中。**

也就是说,上述的分析是可以推广到每一步都成立的,包括第一步,当 $i$ 为排序数组的第一位原始,$j$ 为排序数组中最后一位时,任意 $p$ 和 $q$ 都是在 $i$、$j$ 内部的。

**因此,「证明」对边界情况成立,同时对任意不成“对称”关系的数对也成立(其实也就是「证明」部分中的原话)。**

**更大白话一点是:对于任意“非对称”的数对组合,将其调整为“对称”数对组合,结果不会变差。**

### 最后

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

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

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

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


LeetCode/1871-1880/1879. 两个数组最小的异或值之和（困难）.md

1879. 两个数组最小的异或值之和

Question:
给你两个整数数组 `nums1` 和 `nums2`,它们长度都为 `n`。

两个数组的 异或值之和 为 `(nums1[0] XOR nums2[0]) + (nums1[1] XOR nums2[1]) + ... + (nums1[n - 1] XOR nums2[n - 1])` (下标从 `0` 开始)。

比方说,`[1,2,3]` 和 `[3,2,1]` 的 异或值之和 等于 `(1 XOR 3) + (2 XOR 2) + (3 XOR 1) = 2 + 0 + 2 = 4`。

请你将 `nums2` 中的元素重新排列,使得异或值之和最小 。

请你返回重新排列之后的 异或值之和 。

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

输出:2

解释:将 nums2 重新排列得到 [3,2] 。
异或值之和为 (1 XOR 3) + (2 XOR 2) = 2 + 0 = 2 。
```
示例 2:
```
输入:nums1 = [1,0,3], nums2 = [5,3,4]

输出:8

解释:将 nums2 重新排列得到 [5,4,3] 。
异或值之和为 (1 XOR 5) + (0 XOR 4) + (3 XOR 3) = 4 + 4 + 0 = 8 。
```

提示:
* $n = nums1.length$
* $n == nums2.length$
* $1 <= n <= 14$
* $0 <= nums1[i], nums2[i] <= 10^7$

Solution:
### 状压 DP

这是一道「状压 DP」模板题。

为了方便,我们令下标从 $1$ 开始。

**定义 $f[i][s]$ 为考虑前 $i$ 个元素,且对 `nums2` 的使用情况为 $s$ 时的最小异或值**。其中 $s$ 是一个长度为 $n$ 的二进制数:若 $s$ 中的第 $k$ 位为 $1$,说明 `nums2[k]` 已被使用;若 $s$ 中的第 $k$ 位为 $0$,说明 `nums2[k]` 未被使用。

起始时,只有 $f[0][0] = 0$,其余均为无穷大 `INF`。$f[0][0]$ 含义为在不考虑任何数,对 `nums2` 没有任何占用情况时,最小异或值为 $0$。最终 $f[n][2^n - 1]$ 即为答案。

不失一般性考虑 $f[i][s]$ 该如何转移,可以以 `nums1[i]` 是与哪个 `nums2[j]` 进行配对作为切入点:

* 由于总共考虑了前 $i$ 个成员,因此 $s$ 中 $1$ 的数量必然为 $i$,否则 $f[i][s]$ 就不是一个合法状态,跳过转移

* 枚举 `nums1[i]` 是与哪一个 `nums2[j]` 进行配对的,且枚举的 $j$ 需满足在 $s$ 中的第 $j$ 位值为 $1$,若满足则有

$$
f[i][s] = \min(f[i][s], f[i - 1][prev] + nums1[i] ⊕ nums2[j])
$$

  其中 `prev` 为将 $s$ 中的第 $j$ 位进行置零后的二进制数,即 `prev = s ^ (1 << j)`,符号 ⊕ 代表异或操作。

Java 代码:
```Java
class Solution {
    public int minimumXORSum(int[] nums1, int[] nums2) {
        int n = nums1.length, mask = 1 << n, INF = 0x3f3f3f3f;
        int[][] f = new int[n + 10][mask];
        for (int i = 0; i <= n; i++) Arrays.fill(f[i], INF);
        f[0][0] = 0;
        for (int i = 1; i <= n; i++) {
            for (int s = 0; s < mask; s++) {
                if (getCnt(s, n) != i) continue;
                for (int j = 1; j <= n; j++) {
                    if (((s >> (j - 1)) & 1) == 0) continue;
                    f[i][s] = Math.min(f[i][s], f[i - 1][s ^ (1 << (j - 1))] + (nums1[i - 1] ^ nums2[j - 1]));
                }
            }
        }
        return f[n][mask - 1];
    }
    int getCnt(int s, int n) {
        int ans = 0;
        for (int i = 0; i < n; i++) ans += (s >> i) & 1;
        return ans;
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    int minimumXORSum(vector<int>& nums1, vector<int>& nums2) {
        int n = nums1.size(), mask = 1 << n, INF = 0x3f3f3f3f;
        vector<vector<int>> f(n + 10, vector<int>(mask, INF));
        f[0][0] = 0;
        auto getCnt = [&](int s, int n) {
            int ans = 0;
            for (int i = 0; i < n; i++) ans += (s >> i) & 1;
            return ans;
        };
        for (int i = 1; i <= n; i++) {
            for (int s = 0; s < mask; s++) {
                if (getCnt(s, n) != i) continue;
                for (int j = 1; j <= n; j++) {
                    if (((s >> (j - 1)) & 1) == 0) continue;
                    f[i][s] = min(f[i][s], f[i - 1][s ^ (1 << (j - 1))] + (nums1[i - 1] ^ nums2[j - 1]));
                }
            }
        }
        return f[n][mask - 1];
    }
};
```
Python 代码:
```Python
class Solution:
    def minimumXORSum(self, nums1: List[int], nums2: List[int]) -> int:
        n, mask, INF = len(nums1), 1 << len(nums1), 0x3f3f3f3f
        f = [[INF] * mask for _ in range(n + 10)]
        f[0][0] = 0
        for i in range(1, n + 1):
            for s in range(mask):
                if sum([1 for i in range(n) if (s >> i) & 1]) != i:
                    continue
                for j in range(1, n + 1):
                    if ((s >> (j - 1)) & 1) == 0:
                        continue
                    f[i][s] = min(f[i][s], f[i - 1][s ^ (1 << (j - 1))] + (nums1[i - 1] ^ nums2[j - 1]))
        return f[n][mask - 1]
```
TypeScript 代码:
```TypeScript
function minimumXORSum(nums1: number[], nums2: number[]): number {
    const n = nums1.length, mask = 1 << n, INF = 0x3f3f3f3f;
    const f: number[][] = new Array(n + 10).fill([]).map(() => new Array(mask).fill(INF));
    f[0][0] = 0;
    const getCnt = (s: number, n: number): number => {
        let ans = 0;
        for (let i = 0; i < n; i++) ans += (s >> i) & 1;
        return ans;
    };
    for (let i = 1; i <= n; i++) {
        for (let s = 0; s < mask; s++) {
            if (getCnt(s, n) !== i) continue;
            for (let j = 1; j <= n; j++) {
                if (((s >> (j - 1)) & 1) === 0) continue;
                f[i][s] = Math.min(f[i][s], f[i - 1][s ^ (1 << (j - 1))] + (nums1[i - 1] ^ nums2[j - 1]));
            }
        }
    }
    return f[n][mask - 1];
};
```
* 时间复杂度:$O(n^2 \times 2^n)$
* 空间复杂度:$O(n \times 2^n)$

---

### 模拟退火

事实上,这道题还能使用「模拟退火」进行求解。

由于我们可以无限次对 `nums2` 进行打乱互换,先来思考如何衡量一个 `nums2` 排列的“好坏”。

一个简单的方式:固定计算 `(nums1[0] XOR nums2[0]) + (nums1[1] XOR nums2[1]) + ... + (nums1[n - 1] XOR nums2[n - 1])` 作为衡量当前 `nums2` 的得分,得分越小,当前的 `nums2` 排列越好。

迭代开始前先对 `nums2` 进行一次随机打乱,随后每个回合随机选择 `nums2` 的两个成员进行互换,并比较互换前后的得分情况,若互换后变好,那么保留该互换操作;若变差,则以一定概率进行重置(重新换回来)。

重复迭代多次,使用一个全局变量 `ans` 保存下最小异或值之和。

即「模拟退火」的单次迭代基本流程:

1. 随机选择两个下标,计算「交换下标元素前对应序列的得分」&「交换下标元素后对应序列的得分」
2. 如果温度下降(交换后的序列更优),进入下一次迭代
3. 如果温度上升(交换前的序列更优),以「一定的概率」恢复现场(再交换回来)

> 对于一个能够运用模拟退火求解的问题,最核心的是如何实现 `calc` 方法(即如何定义一个具体方案的得分),其余均为模板内容。

Java 代码(2024/04/29 可过):
```Java
class Solution {
    int N = 400;
    double hi = 1e5, lo = 1e-5, fa = 0.90;
    Random random = new Random(20230823);
    void swap(int[] n, int a, int b) {
        int c = n[a];
        n[a] = n[b];
        n[b] = c;
    }
    int calc() {
        int res = 0;
        for (int i = 0; i < n; i++) res += n1[i] ^ n2[i];
        ans = Math.min(ans, res);
        return res;
    }
    void shuffle(int[] nums) {
        for (int i = n; i > 0; i--) swap(nums, random.nextInt(i), i - 1);
    }
    void sa() {
        shuffle(n2);
        for (double t = hi; t > lo; t *= fa) {
            int a = random.nextInt(n), b = random.nextInt(n);
            int prev = calc();
            swap(n2, a, b);
            int cur = calc();
            int diff = cur - prev;
            if (Math.log(diff / t) >= random.nextDouble()) swap(n2, a, b);
        }
    }
    int[] n1, n2;
    int n;
    int ans = Integer.MAX_VALUE;
    public int minimumXORSum(int[] nums1, int[] nums2) {
        n1 = nums1; n2 = nums2;
        n = n1.length;
        while (N-- > 0) sa();
        return ans;
    }
}
```
* 时间复杂度:启发式搜索不讨论时空复杂度
* 空间复杂度:启发式搜索不讨论时空复杂度

### 最后

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

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

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

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


LeetCode/1891-1900/1893. 检查是否区域内所有整数都被覆盖（简单）.md

1893. 检查是否区域内所有整数都被覆盖

Question:
给你一个二维整数数组 `ranges` 和两个整数 `left` 和 `right`。每个 $ranges[i] = [start_i, end_i]$ 表示一个从 $start_i$ 到 $end_i$ 的 闭区间 。

如果闭区间 $[left, right]$ 内每个整数都被 `ranges` 中 至少一个 区间覆盖,那么请你返回 `true`,否则返回 `false`。

已知区间 $ranges[i] = [start_i, end_i]$,如果整数 `x` 满足 $start_i <= x <= end_i$,那么我们称整数 `x` 被覆盖了。

示例 1:
```
输入:ranges = [[1,2],[3,4],[5,6]], left = 2, right = 5

输出:true

解释:2 到 5 的每个整数都被覆盖了:
- 2 被第一个区间覆盖。
- 3 和 4 被第二个区间覆盖。
- 5 被第三个区间覆盖。
```
示例 2:
```
输入:ranges = [[1,10],[10,20]], left = 21, right = 21

输出:false

解释:21 没有被任何一个区间覆盖。
```

提示:
* $1 <= ranges.length <= 50$
* $1 <= start_i <= end_i <= 50$
* $1 <= left <= right <= 50$

Solution:
### 模拟

一个简单的想法是根据题意进行模拟,检查 $[left, right]$ 中的每个整数,如果检查过程中发现某个整数没被 $ranges$ 中的闭区间所覆盖,那么直接返回 `False`,所有数值通过检查则返回 `True`。

代码:
```Java
class Solution {
    public boolean isCovered(int[][] rs, int l, int r) {
        for (int i = l; i <= r; i++) {
            boolean ok = false;
            for (int[] cur : rs) {
                int a = cur[0], b = cur[1];
                if (a <= i && i <= b) {
                    ok = true;
                    break;
                }
            }
            if (!ok) return false;
        }
        return true;
    }
}
```
* 时间复杂度:令 $[left, right]$ 之间整数数量为 $n$,$ranges$ 长度为 $m$。整体复杂度为 $O(n * m)$
* 空间复杂度:$O(1)$

---

### 树状数组

**针对此题,可以有一个很有意思的拓展,将本题难度提升到【中等】甚至是【困难】。**

将查询 $[left, right]$ 修改为「四元查询数组」$querys$,每个 $querys[i]$ 包含四个指标 $(a,b,l,r)$:代表询问 $[l, r]$ 中的每个数是否在 $range$ 中 $[a, b]$ 的闭区间所覆盖过。

**如果进行这样的拓展的话,那么我们需要使用「持久化树状数组」或者「主席树」来配合「容斥原理」来做。**

基本思想都是使用 $range[0,b]$ 的计数情况减去 $range[0, a-1]$ 的计数情况来得出 $[a, b]$ 的计数情况。

回到本题,由于数据范围很小,只有 $50$,我们可以使用「树状数组」进行求解:

* `void add(int x, int u)`:对于数值 $x$ 出现次数进行 $+u$ 操作;
* `int query(int x)`:查询某个满足 $<= x$ 的数值的个数。

那么显然,如果我们需要查询一个数值 $x$ 是否出现过,可以通过查询 $cnt = query(x) - query(x - 1)$ 来得知。

代码:
```Java
class Solution {
    int n = 55;
    int[] tr = new int[n];
    int lowbit(int x) {
        return x & -x;
    }
    void add(int x, int u) {
        for (int i = x; i <= n; i += lowbit(i)) tr[i] += u;
    }
    int query(int x) {
        int ans = 0;
        for (int i = x; i > 0; i -= lowbit(i)) ans += tr[i];
        return ans;
    }
    public boolean isCovered(int[][] rs, int l, int r) {
        for (int[] cur : rs) {
            int a = cur[0], b = cur[1];
            for (int i = a; i <= b; i++) {
                add(i, 1);
            }
        }
        for (int i = l; i <= r; i++) {
            int cnt = query(i) - query(i - 1);
            if (cnt == 0) return false;
        }
        return true;
    }
}
```
* 时间复杂度:令 $[left, right]$ 之间整数数量为 $n$,$\sum_{i = 0}^{range.legth - 1}ranges[i].length$ 为 $sum$,常数 $C$ 固定为 $55$。建树复杂度为 $O(sum\log C)$,查询查询复杂度为 $O(n\log{C})$。整体复杂度为 $O(sum\log{C} + n\log{C})$
* 空间复杂度:$O(C)$

---

### 树状数组(去重优化)

在朴素的「树状数组」解法中,我们无法直接查询 $[l, r]$ 区间中被覆盖过的个数的根本原因是「某个值可能会被重复添加到树状数组中」。

因此,一种更加优秀的做法:**在往树状数组中添数的时候进行去重,然后通过 $cnt = query(r) - query(l - 1)$ 直接得出 $[l, r]$ 范围内有多少个数被添加过。**

这样的 `Set` 去重操作可以使得我们查询的复杂度从 $O(n\log{C})$ 下降到 $O(\log{C})$。

> 由于数值范围很小,自然也能够使用数组来代替 `Set` 进行标记(见 P2)

代码:
```Java
class Solution {
    int n = 55;
    int[] tr = new int[n];
    int lowbit(int x) {
        return x & -x;
    }
    void add(int x, int u) {
        for (int i = x; i <= n; i += lowbit(i)) tr[i] += u;
    }
    int query(int x) {
        int ans = 0;
        for (int i = x; i > 0; i -= lowbit(i)) ans += tr[i];
        return ans;
    }
    public boolean isCovered(int[][] rs, int l, int r) {
        Set<Integer> set = new HashSet<>();
        for (int[] cur : rs) {
            int a = cur[0], b = cur[1];
            for (int i = a; i <= b; i++) {
                if (!set.contains(i)) {
                    add(i, 1);
                    set.add(i);
                }
            }
        }
        int tot = r - l + 1, cnt = query(r) - query(l - 1);
        return tot == cnt;
    }
}
```
```Java
class Solution {
    int n = 55;
    int[] tr = new int[n];
    boolean[] vis = new boolean[n];
    int lowbit(int x) {
        return x & -x;
    }
    void add(int x, int u) {
        for (int i = x; i <= n; i += lowbit(i)) tr[i] += u;
    }
    int query(int x) {
        int ans = 0;
        for (int i = x; i > 0; i -= lowbit(i)) ans += tr[i];
        return ans;
    }
    public boolean isCovered(int[][] rs, int l, int r) {
        for (int[] cur : rs) {
            int a = cur[0], b = cur[1];
            for (int i = a; i <= b; i++) {
                if (!vis[i]) {
                    add(i, 1);
                    vis[i] = true;
                }
            }
        }
        int tot = r - l + 1, cnt = query(r) - query(l - 1);
        return tot == cnt;
    }
}
```
* 时间复杂度:令 $[left, right]$ 之间整数数量为 $n$,$\sum_{i = 0}^{range.legth - 1}ranges[i].length$ 为 $sum$,常数 $C$ 固定为 $55$。建树复杂度为 $O(sum\log C)$,查询查询复杂度为 $O(\log{C})$。整体复杂度为 $O(sum\log{C} + \log{C})$
* 空间复杂度:$O(C + \sum_{i = 0}^{range.legth - 1}ranges[i].length)$

---

### 线段树(不含“懒标记”)

更加进阶的做法是使用「线段树」来做,与「树状数组(优化)」解法一样,线段树配合持久化也可以用于求解「在线」问题。

**与主要解决「单点修改 & 区间查询」的树状数组不同,线段树能够解决绝大多数「区间修改(区间修改/单点修改)& 区间查询」问题。**

对于本题,由于数据范围只有 $55$,因此我们可以使用与「树状数组(优化)」解法相同的思路,实现一个不包含“懒标记”的线段树来做(仅支持单点修改 & 区间查询)。

代码:
```Java
class Solution {
    // 代表 [l, r] 区间有 cnt 个数被覆盖
    class Node {
        int l, r, cnt;
        Node (int _l, int _r, int _cnt) {
            l = _l; r = _r; cnt = _cnt;
        }
    }
    int N = 55;
    Node[] tr = new Node[N * 4];
    void pushup(int u) {
        tr[u].cnt = tr[u << 1].cnt + tr[u << 1 | 1].cnt;
    }
    void build(int u, int l, int r) {
        if (l == r) {
            tr[u] = new Node(l, r, 0);
        } else {
            tr[u] = new Node(l, r, 0);
            int mid = l + r >> 1;
            build(u << 1, l, mid);
            build(u << 1 | 1, mid + 1, r);
            pushup(u);
        }
    }
    // 从 tr 数组的下标 u 开始,在数值 x 的位置进行标记
    void update(int u, int x) {
        if (tr[u].l == x && tr[u].r == x) {
            tr[u].cnt = 1;
        } else {
            int mid = tr[u].l + tr[u].r >> 1;
            if (x <= mid) update(u << 1, x);
            else update(u << 1 | 1, x);
            pushup(u);
        }
    }
    // 从 tr 数组的下标 u 开始,查询 [l,r] 范围内有多少个数值被标记
    int query(int u, int l, int r) {
        if (l <= tr[u].l && tr[u].r <= r) return tr[u].cnt;
        int mid = tr[u].l + tr[u].r >> 1;
        int ans = 0;
        if (l <= mid) ans += query(u << 1, l, r);
        if (r > mid) ans += query(u << 1 | 1, l, r);
        return ans;
    }
    public boolean isCovered(int[][] rs, int l, int r) {
        build(1, 1, N);
        for (int[] cur : rs) {
            int a = cur[0], b = cur[1];
            for (int i = a; i <= b; i++) {
                update(1, i);
            }
        }
        int tot = r - l + 1 , cnt = query(1, l, r);
        return tot == cnt;
    }
}
```
* 时间复杂度:令 $[left, right]$ 之间整数数量为 $n$,$\sum_{i = 0}^{range.legth - 1}ranges[i].length$ 为 $sum$,常数 $C$ 固定为 $55$。建树复杂度为 $O(sum\log C)$,查询查询复杂度为 $O(\log{C})$。整体复杂度为 $O(sum\log{C} + \log{C})$
* 空间复杂度:$O(C * 4)$

### 最后

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

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

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

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


LeetCode/1891-1900/1894. 找到需要补充粉笔的学生编号（中等）.md

1894. 找到需要补充粉笔的学生编号

Question:
一个班级里有 n 个学生,编号为 0 到 n - 1 。每个学生会依次回答问题,编号为 0 的学生先回答,然后是编号为 1 的学生,以此类推,直到编号为 n - 1 的学生,然后老师会重复这个过程,重新从编号为 0 的学生开始回答问题。

给你一个长度为 n 且下标从 0 开始的整数数组 chalk 和一个整数 k 。一开始粉笔盒里总共有 k 支粉笔。当编号为 i 的学生回答问题时,他会消耗 chalk[i] 支粉笔。如果剩余粉笔数量 严格小于 chalk[i] ,那么学生 i 需要 补充 粉笔。

请你返回需要 **补充** 粉笔的学生 **编号** 。

示例 1:
```
输入:chalk = [5,1,5], k = 22

输出:0

解释:学生消耗粉笔情况如下:
- 编号为 0 的学生使用 5 支粉笔,然后 k = 17 。
- 编号为 1 的学生使用 1 支粉笔,然后 k = 16 。
- 编号为 2 的学生使用 5 支粉笔,然后 k = 11 。
- 编号为 0 的学生使用 5 支粉笔,然后 k = 6 。
- 编号为 1 的学生使用 1 支粉笔,然后 k = 5 。
- 编号为 2 的学生使用 5 支粉笔,然后 k = 0 。
编号为 0 的学生没有足够的粉笔,所以他需要补充粉笔。
```
示例 2:
```
输入:chalk = [3,4,1,2], k = 25

输出:1

解释:学生消耗粉笔情况如下:
- 编号为 0 的学生使用 3 支粉笔,然后 k = 22 。
- 编号为 1 的学生使用 4 支粉笔,然后 k = 18 。
- 编号为 2 的学生使用 1 支粉笔,然后 k = 17 。
- 编号为 3 的学生使用 2 支粉笔,然后 k = 15 。
- 编号为 0 的学生使用 3 支粉笔,然后 k = 12 。
- 编号为 1 的学生使用 4 支粉笔,然后 k = 8 。
- 编号为 2 的学生使用 1 支粉笔,然后 k = 7 。
- 编号为 3 的学生使用 2 支粉笔,然后 k = 5 。
- 编号为 0 的学生使用 3 支粉笔,然后 k = 2 。
编号为 1 的学生没有足够的粉笔,所以他需要补充粉笔。
```

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

Solution:
### 前缀和 + 二分

根据题意,每个学生消耗的粉笔为定值,所有粉笔最终会像老师的教导一样孜孜不倦地循环投入在所有的学生身上。

因此我们可以预处理出前缀和数组 $sum$,将 $k$ 对所有学生一次循环所消耗总粉笔数($sum[n]$)进行取模,得到最后一轮开始前的粉笔数量。

然后对前缀和数组进行二分,找到最后一位满足粉笔要求的学生,其往后一位的同学编号即是答案。

代码:
```Java
class Solution {
    public int chalkReplacer(int[] chalk, int k) {
        int n = chalk.length;
        long[] sum = new long[n + 1];
        for (int i = 1; i <= n; i++) sum[i] = sum[i - 1] + chalk[i - 1];
        k = (int)(k % sum[n]);
        int l = 1, r = n;
        while (l < r) {
            int mid = l + r + 1 >> 1;
            if (sum[mid] <= k) l = mid;
            else r = mid - 1;
        }
        return sum[r] <= k ? r : r - 1;
    }
}
```
* 时间复杂度:预处理前缀和的复杂度为 $O(n)$;二分求答案的复杂度为 $O(\log{n})$。整体复杂度为 $O(n)$
* 空间复杂度:$O(n)$

---

### 模拟

通过解法一,我们发现复杂度的上界为预处理前缀和的复杂度 $O(n)$,同时「对单次循环消耗的总粉笔数取模操作」确保了剩余的粉笔数必然会在单次遍历中消耗完成。

因此 $O(\log{n})$ 的二分其实是没有必要的,只需要再对 `chalk` 进行最后一轮的遍历模拟即可。

代码:
```Java
class Solution {
    public int chalkReplacer(int[] chalk, int k) {
        int n = chalk.length;
        long max = 0;
        for (int i : chalk) max += i;
        k = (int)(k % max);
        for (int i = 0; i < n; i++) {
            k -= chalk[i];
            if (k < 0) return i;
        }
        return -1; // never
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


Question:
编写一个函数,输入是一个无符号整数(以二进制串的形式),返回其二进制表达式中数字位数为 '1' 的个数(也被称为汉明重量)。

提示:

* 请注意,在某些语言(如 Java)中,没有无符号整数类型。在这种情况下,输入和输出都将被指定为有符号整数类型,并且不应影响您的实现,因为无论整数是有符号的还是无符号的,其内部的二进制表示形式都是相同的。
* 在 Java 中,编译器使用二进制补码记法来表示有符号整数。因此,在上面的 示例 3 中,输入表示有符号整数 -3。

示例 1:
```
输入:00000000000000000000000000001011
输出:3
解释:输入的二进制串 00000000000000000000000000001011 中,共有三位为 '1'。
```
示例 2:
```
输入:00000000000000000000000010000000
输出:1
解释:输入的二进制串 00000000000000000000000010000000 中,共有一位为 '1'。
```
示例 3:
```
输入:11111111111111111111111111111101
输出:31
解释:输入的二进制串 11111111111111111111111111111101 中,共有 31 位为 '1'。
```

提示:
* 输入必须是长度为 32 的 二进制串 。

进阶:
* 如果多次调用这个函数,你将如何优化你的算法?

Solution:
### 「位数检查」解法

一个朴素的做法是,对 `int` 的每一位进行检查,并统计 $1$ 的个数。

代码:
```Java
public class Solution {
    public int hammingWeight(int n) {
        int ans = 0;
        for (int i = 0; i < 32; i++) {
            ans += ((n >> i) & 1);
        }
        return ans;
    }
}
```
* 时间复杂度:$O(k)$,$k$ 为 `int` 的位数,固定为 $32$ 位
* 空间复杂度:$O(1)$

***

### 「右移统计」解法

对于方法一,即使 $n$ 的高位均为是 $0$,我们也会对此进行循环检查。

因此另外一个做法是:通过 `n & 1` 来统计当前 $n$ 的最低位是否为 $1$,同时每次直接对 $n$ 进行右移并高位补 0。

当 $n = 0$ 代表,我们已经将所有的 $1$ 统计完成。

这样的做法,可以确保只会循环到最高位的 $1$。

代码:
```Java
public class Solution {
    public int hammingWeight(int n) {
        int ans = 0;
        while (n != 0) {
            ans += (n & 1);
            n >>>= 1;
        }
        return ans;
    }
}
```
* 时间复杂度:$O(k)$,$k$ 为 `int` 的位数,固定为 $32$ 位,最坏情况 $n$ 的二进制表示全是 $1$
* 空间复杂度:$O(1)$

---

### 「lowbit」解法

对于方法二,如果最高位 $1$ 和 最低位 $1$ 之间全是 $0$,我们仍然会诸次右移,直到处理到最高位的 $1$ 为止。

那么是否有办法,只对位数为 $1$ 的二进制位进行处理呢?

使用 `lowbit` 即可做到,`lowbit` 会在 $O(1)$ 复杂度内返回二进制表示中最低位 $1$ 所表示的数值。

例如 $(0000...111100)_2$ 传入 `lowbit` 返回 $(0000...000100)_2$;$(0000...00011)_2$ 传入 `lowbit` 返回 $(0000...00001)_2$ ...

代码:
```Java
public class Solution {
    public int hammingWeight(int n) {
        int ans = 0;
        for (int i = n; i != 0; i -= lowbit(i)) ans++;
        return ans;
    }
    int lowbit(int x) {
        return x & -x;
    }
}
```
* 时间复杂度:$O(k)$,$k$ 为 `int` 的位数,固定为 $32$ 位,最坏情况 $n$ 的二进制表示全是 $1$
* 空间复杂度:$O(1)$

---

### 「分组统计」解法

以上三种解法都是 $O(k)$ 的,事实上我们可以通过分组统计的方式,做到比 $O(k)$ 更低的复杂度。

代码:
```Java
public class Solution {
    public int hammingWeight(int n) {
        n = (n & 0x55555555) + ((n >>> 1)  & 0x55555555);
        n = (n & 0x33333333) + ((n >>> 2)  & 0x33333333);
        n = (n & 0x0f0f0f0f) + ((n >>> 4)  & 0x0f0f0f0f);
        n = (n & 0x00ff00ff) + ((n >>> 8)  & 0x00ff00ff);
        n = (n & 0x0000ffff) + ((n >>> 16) & 0x0000ffff);
        return n;
    }
}
```
* 时间复杂度:$O(\log{k})$,$k$ 为 `int` 的位数,固定为 $32$ 位
* 空间复杂度:$O(1)$

PS. 对于该解法,如果大家学有余力的话,还是建议大家在纸上模拟一下这个过程。如果不想深入,也可以当成模板背过(写法非常固定),但通常如果不是写底层框架,你几乎不会遇到需要一个 $O(\log{k})$ 解法的情况。

而且这个做法的最大作用,不是处理 `int`,而是处理更大位数的情况,在长度只有 $32$ 位的 `int` 的情况下,该做法不一定就比循环要快(该做法会产生多个的中间结果,导致赋值发生多次,而且由于指令之间存在对 $n$ 数值依赖,可能不会被优化为并行指令),这个道理和对于排序元素少的情况下,我们会选择「选择排序」而不是「归并排序」是一样的。

### 最后

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

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

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

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