Question:
给你一个整数 `n` ,请你找出并返回第 `n` 个 丑数 。

丑数 就是只包含质因数 `2`、`3` 和 `5` 的正整数。

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

输出:12

解释:[1, 2, 3, 4, 5, 6, 8, 9, 10, 12] 是由前 10 个丑数组成的序列。
```
示例 2:
```
输入:n = 1

输出:1

解释:1 通常被视为丑数。
```

提示:
* $1 <= n <= 1690$

Solution:
### 基本思路

根据丑数的定义,我们有如下结论:

* $1$ 是最小的丑数。
* 对于任意一个丑数 $x$,其与任意的质因数($2$、$3$、$5$)相乘,结果($2x$、$3x$、$5x$)仍为丑数。

---

### 优先队列(小根堆)

有了基本的分析思路,一个简单的解法是使用优先队列:

1. 起始先将最小丑数 $1$ 放入队列
2. 每次从队列取出最小值 $x$,然后将 $x$ 所对应的丑数 $2x$、$3x$ 和 $5x$ 进行入队。
3. 对步骤 2 循环多次,第 $n$ 次出队的值即是答案。

为了防止同一丑数多次进队,我们需要使用数据结构 $Set$ 来记录入过队列的丑数。

Java 代码:
```Java
class Solution {
    int[] nums = new int[]{2,3,5};
    public int nthUglyNumber(int n) {
        Set<Long> set = new HashSet<>();
        Queue<Long> pq = new PriorityQueue<>();
        set.add(1L);
        pq.add(1L);
        for (int i = 1; i <= n; i++) {
            long x = pq.poll();
            if (i == n) return (int)x;
            for (int num : nums) {
                long t = num * x;
                if (!set.contains(t)) {
                    set.add(t);
                    pq.add(t);
                }
            }
        }
        return -1;
    }
}
```
Python 代码:
```Python
class Solution(object):
    def nthUglyNumber(self, n):
        """
        :type n: int
        :rtype: int
        """
        nums = [2,3,5]
        explored = {1}
        pq = [1]
        for i in range(1, n+1):
            x = heapq.heappop(pq)
            if i == n:
                return x
            for num in nums:
                t = num * x
                if t not in explored:
                    explored.add(t)
                    heapq.heappush(pq,t)
        return -1
```
C++ 代码:
```C++
class Solution {
public:
    int nthUglyNumber(int n) {
        int nums[] = {2, 3, 5};
        set<long> s;
        priority_queue<long, vector<long>, greater<long>> q;
        s.insert(1);
        q.push(1);
        for (int i = 1; i <= n; i++)
        {
            long x = q.top();
            q.pop();
            if (i == n)
                return (int)x;
            for (int num = 0; num < 3; num++)
            {
                long t = nums[num] * x;
                if (!s.count(t))
                {
                    s.insert(t);
                    q.push(t);
                }
            }
        }
        return -1;
    }
};
```
* 时间复杂度:从优先队列中取最小值为 $O(1)$,往优先队列中添加元素复杂度为 $O(\log{n})$。整体复杂度为 $O(n\log{n})$
* 空间复杂度:$O(n)$

---

### 多路归并(多指针)

从解法一中不难发现,我们「往后产生的丑数」都是基于「已有丑数」而来(使用「已有丑数」乘上「质因数」$2$、$3$、$5$)。

因此,如果我们所有丑数的有序序列为 $a1,a2,a3,...,an$ 的话,序列中的每一个数都必然能够被以下三个序列(中的至少一个)覆盖:

* 由丑数 $\times 2$ 所得的有序序列:$1 \times 2$、$2 \times 2$、$3 \times 2$、$4 \times 2$、$5 \times 2$、$6 \times 2$、$8 \times 2$ ...
* 由丑数 $ \times 3$ 所得的有序序列:$1 \times 3$、$2 \times 3$、$3 \times 3$、$4 \times 3$、$5 \times 3$、$6 \times 3$、$8 \times 3$ ...
* 由丑数 $\times 5$ 所得的有序序列:$1 \times 5$、$2 \times 5$、$3 \times 5$、$4 \times 5$、$5 \times 5$、$6 \times 5$、$8 \times 5$ ...

举个🌰,假设我们需要求得 $[1, 2, 3, ... , 10, 12]$ 丑数序列 $arr$ 的最后一位,那么该序列可以看作以下三个有序序列归并而来:

* $1 \times 2, 2 \times 2, 3 \times 2, ... , 10 \times 2, 12 \times 2$ ,将 $2$ 提出,即 $arr \times 2$
* $1 \times 3, 2 \times 3, 3 \times 3, ... , 10 \times 3, 12 \times 3$ ,将 $3$ 提出,即 $arr \times 3$
* $1 \times 5, 2 \times 5, 3 \times 5, ... , 10 \times 5, 12 \times 5$ ,将 $5$ 提出,即 $arr \times 5$

因此我们可以使用三个指针来指向目标序列 $arr$ 的某个下标(下标 $0$ 作为哨兵不使用,起始都为 $1$),使用 $arr[下标] \times 质因数$ 代表当前使用到三个有序序列中的哪一位,同时使用 $idx$ 表示当前生成到 $arr$ 哪一位丑数。

Java 代码:
```Java
class Solution {
    public int nthUglyNumber(int n) {
        // ans 用作存储已有丑数(从下标 1 开始存储,第一个丑数为 1)
        int[] ans = new int[n + 1];
        ans[1] = 1;
        // 由于三个有序序列都是由「已有丑数」*「质因数」而来
        // i2、i3 和 i5 分别代表三个有序序列当前使用到哪一位「已有丑数」下标(起始都指向 1)
        for (int i2 = 1, i3 = 1, i5 = 1, idx = 2; idx <= n; idx++) {
            // 由 ans[iX] * X 可得当前有序序列指向哪一位
            int a = ans[i2] * 2, b = ans[i3] * 3, c = ans[i5] * 5;
            // 将三个有序序列中的最小一位存入「已有丑数」序列,并将其下标后移
            int min = Math.min(a, Math.min(b, c));
            // 由于可能不同有序序列之间产生相同丑数,因此只要一样的丑数就跳过(不能使用 else if )
            if (min == a) i2++;
            if (min == b) i3++;
            if (min == c) i5++;
            ans[idx] = min;
        }
        return ans[n];
    }
}
```
Python 代码:
```Python
class Solution(object):
    def nthUglyNumber(self, n):
        """
        :type n: int
        :rtype: int
        """
        # ans 用作存储已有丑数(从下标 1 开始存储,第一个丑数为 1)
        ans = [0] * (n+1)
        ans[1] = 1
        # 由于三个有序序列都是由「已有丑数」*「质因数」而来
        # i2、i3 和 i5 分别代表三个有序序列当前使用到哪一位「已有丑数」下标(起始都指向 1)
        i2 = i3 = i5 = 1
        idx = 2
        while idx <= n:
            # 由 ans[iX] * X 可得当前有序序列指向哪一位
            a,b,c = ans[i2] *2,ans[i3]*3,ans[i5]*5
            # 将三个有序序列中的最小一位存入「已有丑数」序列,并将其下标后移
            m = min(a,b,c)
            # 由于可能不同有序序列之间产生相同丑数,因此只要一样的丑数就跳过(不能使用 else if )
            if m == a:
                i2 += 1
            if m == b:
                i3 += 1
            if m == c:
                i5 += 1
            ans[idx] = m
            idx += 1
        return ans[n]
```
JavaScript 代码:
```JavaScript
/**
 * @param {number} n
 * @return {number}
 */
var nthUglyNumber = function(n) {
    const ans=new Array(n+1);
    ans[1]=1;
    for(let i2=1, i3=1, i5=1, idx=2;idx<=n;idx++){
        let a=ans[i2]*2, b=ans[i3]*3, c=ans[i5]*5;
        let min=Math.min(a, b, c);
        if(min===a) i2++;
        if(min===b) i3++;
        if(min===c) i5++;
        ans[idx]=min;
    }
    return ans[n];
};
```
C++ 代码:
```C++
class Solution {
public:
    int nthUglyNumber(int n) {
        // 存储丑数
        int *arr = new int[n + 1];
        arr[1] = 1;

        for (int x = 1, y = 1, z = 1, index = 2; index <= n; index++){
            int a = arr[x] * 2, b = arr[y] * 3, c = arr[z] * 5;
            int m = min(a, min(b, c));
            if (m == a)x++;
            if (m == b)y++;
            if (m == c)z++;
            arr[index] = m;
        }
        int ans = arr[n];
        delete[] arr;
        return ans;
    }
};
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


Question:
给定一个包含 $[0, n]$ 中 $n$ 个数的数组 $nums$ ,找出 $[0, n]$ 这个范围内没有出现在数组中的那个数。

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

输出:2

解释:n = 3,因为有 3 个数字,所以所有的数字都在范围 [0,3] 内。2 是丢失的数字,因为它没有出现在 nums 中。
```
示例 2:
```
输入:nums = [0,1]

输出:2

解释:n = 2,因为有 2 个数字,所以所有的数字都在范围 [0,2] 内。2 是丢失的数字,因为它没有出现在 nums 中。
```
示例 3:
```
输入:nums = [9,6,4,2,3,5,7,0,1]

输出:8

解释:n = 9,因为有 9 个数字,所以所有的数字都在范围 [0,9] 内。8 是丢失的数字,因为它没有出现在 nums 中。
```
示例 4:
```
输入:nums = [0]

输出:1

解释:n = 1,因为有 1 个数字,所以所有的数字都在范围 [0,1] 内。1 是丢失的数字,因为它没有出现在 nums 中。
```

提示:
* n == nums.length
* $1 <= n <= 10^4$
* $0 <= nums[i] <= n$
* nums 中的所有数字都 独一无二

进阶:你能否实现线性时间复杂度、仅使用额外常数空间的算法解决此问题?

Solution:
### 排序

一个简单的做法是直接对 $nums$ 进行排序,找到符合 $nums[i] \neq i$ 的位置即是答案,如果不存在 $nums[i] \neq i$ 的位置,则 $n$ 为答案。

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

---

### 数组哈希

利用 $nums$ 的数值范围为 $[0,n]$,且只有一个值缺失,我们可以直接开一个大小为 $n + 1$ 的数组充当哈希表,进行计数,没被统计到的数值即是答案。

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

---

### 原地哈希

事实上,我们可以将 $nums$ 本身作为哈希表进行使用,将 $nums[i]$ 放到其应该出现的位置 $i$ 上( $i < n$ ),然后对 $nums$ 进行检查,找到满足 $nums[i] \neq i$ 的位置即是答案,如果不存在 $nums[i] \neq i$ 的位置,则 $n$ 为答案。

代码:
```Java
class Solution {
    public int missingNumber(int[] nums) {
        int n = nums.length;
        for (int i = 0; i < n; i++) {
            if (nums[i] != i && nums[i] < n) swap(nums, nums[i], i--);
        }
        for (int i = 0; i < n; i++) {
            if (nums[i] != i) return i;
        }
        return n;
    }
    void swap(int[] nums, int i, int j) {
        int c = nums[i];
        nums[i] = nums[j];
        nums[j] = c;
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(1)$

---

### 作差法

利用 $nums$ 的数值范围为 $[1, n]$,我们可以先计算出 $[1, n]$ 的总和 $sum$(利用等差数列求和公式),再计算 $nums$ 的总和 $cur$,两者之间的差值即是 $nums$ 中缺失的数字。

代码:
```Java
class Solution {
    public int missingNumber(int[] nums) {
        int n = nums.length;
        int cur = 0, sum = n * (n + 1) / 2;
        for (int i : nums) cur += i;
        return sum - cur;
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(1)$

---

### 异或

找缺失数、找出现一次数都是异或的经典应用。

我们可以先求得 $[1, n]$ 的异或和 $ans$,然后用 $ans$ 对各个 $nums[i]$ 进行异或。

这样最终得到的异或和表达式中,只有缺失元素出现次数为 $1$ 次,其余元素均出现两次($x ⊕x = 0$),即最终答案 $ans$ 为缺失元素。

代码:
```Java
class Solution {
    public int missingNumber(int[] nums) {
        int n = nums.length;
        int ans = 0;
        for (int i = 0; i <= n; i++) ans ^= i;
        for (int i : nums) ans ^= i;
        return ans;
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


LeetCode/2641-2650/2646. 最小化旅行的价格总和（困难）.md

2646. 最小化旅行的价格总和

Question:
现有一棵无向、无根的树,树中有 `n` 个节点,按从 `0` 到 `n - 1` 编号。

给你一个整数 `n` 和一个长度为 `n - 1` 的二维整数数组 `edges`,其中 $edges[i] = [a_{i}, b_{i}]$ 表示树中节点 $a_{i}$ 和 $b_{i}$ 之间存在一条边。

每个节点都关联一个价格。给你一个整数数组 `price`,其中 `price[i]` 是第 `i` 个节点的价格。

给定路径的价格总和是该路径上所有节点的价格之和。

另给你一个二维整数数组 `trips`,其中 $trips[i] = [start_{i}, end_{i}]$ 表示您从节点 $start_{i}$ 开始第 $i$ 次旅行,并通过任何你喜欢的路径前往节点 $end_{i}$。

在执行第一次旅行之前,你可以选择一些非相邻节点并将价格减半。

返回执行所有旅行的最小价格总和。

示例 1:

```
输入:n = 4, edges = [[0,1],[1,2],[1,3]], price = [2,2,10,6], trips = [[0,3],[2,1],[2,3]]

输出:23

解释:
上图表示将节点 2 视为根之后的树结构。第一个图表示初始树,第二个图表示选择节点 0 、2 和 3 并使其价格减半后的树。
第 1 次旅行,选择路径 [0,1,3] 。路径的价格总和为 1 + 2 + 3 = 6 。
第 2 次旅行,选择路径 [2,1] 。路径的价格总和为 2 + 5 = 7 。
第 3 次旅行,选择路径 [2,1,3] 。路径的价格总和为 5 + 2 + 3 = 10 。
所有旅行的价格总和为 6 + 7 + 10 = 23 。可以证明,23 是可以实现的最小答案。
```
示例 2:

```
输入:n = 2, edges = [[0,1]], price = [2,2], trips = [[0,0]]

输出:1

解释:
上图表示将节点 0 视为根之后的树结构。第一个图表示初始树,第二个图表示选择节点 0 并使其价格减半后的树。
第 1 次旅行,选择路径 [0] 。路径的价格总和为 1 。
所有旅行的价格总和为 1 。可以证明,1 是可以实现的最小答案。
```

提示:
* $1 <= n <= 50$
* $edges.length = n - 1$
* $0 <= a_{i}, b_{i} <= n - 1$
* `edges` 表示一棵有效的树
* $price.length = n$
* $price[i]$ 是一个偶数
* $1 <= price[i] <= 1000$
* $1 <= trips.length <= 100$
* $0 <= start_{i}, end_{i} <= n - 1$

Solution:
### DFS

Java 代码:

```Java
class Solution {
    int N = 55, M = N * 2, idx = 0;
    int[] he = new int[N], e = new int[M], ne = new int[M];
    void add(int a, int b) {
        e[idx] = b;
        ne[idx] = he[a];
        he[a] = idx++;
    }
    public int minimumTotalPrice(int n, int[][] edges, int[] price, int[][] trips) {
        Arrays.fill(he, -1);
        for (int[] info : edges) {
            add(info[0], info[1]); add(info[1], info[0]);
        }
        int[] cnt = new int[n];
        for (int[] trip : trips) dfs1(trip[0], -1, trip[1], cnt);
        int[] ans = dfs2(0, -1, price, cnt);
        return Math.min(ans[0], ans[1]);
    }
    int[] dfs2(int u, int fa, int[] price, int[] cnt) {
        int a = price[u] / 2 * cnt[u], b = price[u] * cnt[u];
        for (int i = he[u]; i != -1; i = ne[i]) {
            int j = e[i];
            if (j == fa) continue;
            int[] info = dfs2(j, u, price, cnt);
            int c = info[0], d = info[1];
            a += d; b += Math.min(c, d);
        }
        return new int[]{a, b};
    }
    boolean dfs1(int u, int fa, int end, int[] cnt) {
        if (u == end) {
            cnt[u]++;
            return true;
        }
        for (int i = he[u]; i != -1; i = ne[i]) {
            int j = e[i];
            if (j == fa) continue;
            if (dfs1(j, u, end, cnt)) {
                cnt[u]++;
                return true;
            }
        }
        return false;
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    int he[55], e[55 * 2], ne[55 * 2], idx = 0;
    void add(int a, int b) {
        e[idx] = b;
        ne[idx] = he[a];
        he[a] = idx++;
    }
    int minimumTotalPrice(int n, vector<vector<int>>& edges, vector<int>& price, vector<vector<int>>& trips) {
        memset(he, -1, sizeof(he));
        for (auto& info : edges) {
            add(info[0], info[1]); add(info[1], info[0]);
        }
        vector<int> cnt(n, 0);
        for (auto& trip : trips) dfs1(trip[0], -1, trip[1], cnt);
        vector<int> ans = dfs2(0, -1, price, cnt);
        return min(ans[0], ans[1]);
    }
    vector<int> dfs2(int u, int fa, vector<int>& price, vector<int>& cnt) {
        int a = price[u] / 2 * cnt[u], b = price[u] * cnt[u];
        for (int i = he[u]; i != -1; i = ne[i]) {
            int j = e[i];
            if (j == fa) continue;
            vector<int> info = dfs2(j, u, price, cnt);
            int c = info[0], d = info[1];
            a += d; b += min(c, d);
        }
        return {a, b};
    }
    bool dfs1(int u, int fa, int end, vector<int>& cnt) {
        if (u == end) {
            cnt[u]++;
            return true;
        }
        for (int i = he[u]; i != -1; i = ne[i]) {
            int j = e[i];
            if (j == fa) continue;
            if (dfs1(j, u, end, cnt)) {
                cnt[u]++;
                return true;
            }
        }
        return false;
    }
};
```
Python 代码:
```Python
class Solution:
    def minimumTotalPrice(self, n: int, edges: List[List[int]], price: List[int], trips: List[List[int]]) -> int:
        idx = 0
        he, e, ne = [-1] * n, [0] * n * 2, [0] * n * 2

        def add(a, b):
            nonlocal idx
            e[idx], ne[idx], he[a] = b, he[a], idx
            idx += 1

        def dfs1(u, fa, end):
            if u == end:
                cnt[u] += 1
                return True
            i = he[u]
            while i != -1:
                j, i = e[i], ne[i]
                if j == fa: continue
                if dfs1(j, u, end):
                    cnt[u] += 1
                    return True
            return False

        def dfs2(u, fa):
            a, b = price[u] // 2 * cnt[u], price[u] * cnt[u]
            i = he[u]
            while i != -1:
                j, i = e[i], ne[i]
                if j == fa: continue
                c, d = dfs2(j, u)
                a += d
                b += min(c, d)
            return [a, b]

        for info in edges:
            add(info[0], info[1])
            add(info[1], info[0])
        cnt = [0] * n
        for trip in trips:
            dfs1(trip[0], -1, trip[1])
        ans = dfs2(0, -1)
        return min(ans[0], ans[1])
```
TypeScript 代码:
```TypeScript
function minimumTotalPrice(n: number, edges: number[][], price: number[], trips: number[][]): number {
    let N = n + 10, M = N * 2, idx = 0;
    const he = Array(N).fill(-1), e = Array(N).fill(0), ne = Array(N).fill(0);
    const add = function(a: number, b: number): void {
        e[idx] = b;
        ne[idx] = he[a];
        he[a] = idx++;
    };
    const dfs1 = function(u: number, fa: number, end: number): boolean {
        if (u == end) {
            cnt[u]++;
            return true;
        }
        for (let i = he[u]; i != -1; i = ne[i]) {
            const j = e[i];
            if (j == fa) continue;
            if (dfs1(j, u, end)) {
                cnt[u]++;
                return true;
            }
        }
        return false;
    };
    const dfs2 = function(u: number, fa: number): number[] {
        let a = price[u] / 2 * cnt[u], b = price[u] * cnt[u];
        for (let i = he[u]; i != -1; i = ne[i]) {
            const j = e[i];
            if (j == fa) continue;
            const [c, d] = dfs2(j, u);
            a += d; b += Math.min(c, d);
        }
        return [a, b];
    }
    for (const [a, b] of edges) {
        add(a, b); add(b, a);
    }
    const cnt = Array(N).fill(0)
    for (const [a, b] of trips) dfs1(a, -1, b);
    const [a, b] = dfs2(0, -1);
    return Math.min(a, b);
};
```
* 时间复杂度:令 `m` 为 `trips` 的大小,建图复杂度为 $O(n)$;统计完成 `trips` 时,每个点的经过次数 `cnt` 的复杂度为 $O(m \times n)$;最后统计答案复杂度为 $O(n)$。整体复杂度为 $O(m \times n)$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


LeetCode/2651-2660/2656. K 个元素的最大和（简单）.md

2656. K 个元素的最大和

Question:
给你一个下标从 `0` 开始的整数数组 `nums` 和一个整数 `k` 。

你需要执行以下操作恰好 `k` 次,最大化你的得分:

1. 从 `nums` 中选择一个元素 `m` 。
2. 将选中的元素 `m` 从数组中删除。
3. 将新元素 `m + 1` 添加到数组中。
4. 你的得分增加 `m` 。

请你返回执行以上操作恰好 `k` 次后的最大得分。

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

输出:18

解释:我们需要从 nums 中恰好选择 3 个元素并最大化得分。
第一次选择 5 。和为 5 ,nums = [1,2,3,4,6] 。
第二次选择 6 。和为 6 ,nums = [1,2,3,4,7] 。
第三次选择 7 。和为 5 + 6 + 7 = 18 ,nums = [1,2,3,4,8] 。
所以我们返回 18 。
18 是可以得到的最大答案。
```
示例 2:
```
输入:nums = [5,5,5], k = 2

输出:11

解释:我们需要从 nums 中恰好选择 2 个元素并最大化得分。
第一次选择 5 。和为 5 ,nums = [5,5,6] 。
第二次选择 6 。和为 6 ,nums = [5,5,7] 。
所以我们返回 11 。
11 是可以得到的最大答案。
```

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

Solution:
### 数学

为了使得分最高,每次应从 `nums` 中选最大值,选完后重放仍为最大值。

假设原始 `nums` 中的最大值为 `max`,那么问题转换为「等差数列」求和:首项为 `max`,末项为 `max + k - 1`,项数为 $k$,公差为 $1$。

Java 代码:
```Java
class Solution {
    public int maximizeSum(int[] nums, int k) {
        int max = 0;
        for (int x : nums) max = Math.max(max, x);
        return k * (max + max + k - 1) / 2;
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    int maximizeSum(vector<int>& nums, int k) {
        int maxv = 0;
        for (auto x : nums) maxv = max(maxv, x);
        return k * (maxv + maxv + k - 1) / 2;
    }
};
```
Python 代码:
```Python
class Solution:
    def maximizeSum(self, nums: List[int], k: int) -> int:
        return k * (2 * max(nums) + k - 1) // 2
```
TypeScript 代码:
```TypeScript
function maximizeSum(nums: number[], k: number): number {
    let max = 0;
    for (const x of nums) max = Math.max(max, x);
    return k * (max + max + k - 1) / 2;
};
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


2661. 找出叠涂元素

Question:
给你一个下标从 $0$ 开始的整数数组 `arr` 和一个 $m \times n$ 的整数矩阵 `mat`。

`arr` 和 `mat` 都包含范围 $[1,m \times n]$ 内的所有整数。

从下标 $0$ 开始遍历 `arr` 中的每个下标 `i` ,并将包含整数 `arr[i]` 的 `mat` 单元格涂色。

请你找出 `arr` 中在 `mat` 的某一行或某一列上都被涂色且下标最小的元素,并返回其下标 $i$ 。

示例 1:

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

输出:2

解释:遍历如上图所示,arr[2] 在矩阵中的第一行或第二列上都被涂色。
```
示例 2:

```
image explanation for example 2

输入:arr = [2,8,7,4,1,3,5,6,9], mat = [[3,2,5],[1,4,6],[8,7,9]]

输出:3

解释:遍历如上图所示,arr[3] 在矩阵中的第二列上都被涂色。
```

提示:
* $m = mat.length$
* $n = mat[i].length$
* $arr.length = m \times n$
* $1 <= m, n <= 10^5$
* $1 <= m \times n <= 105$
* $1 <= arr[i], mat[r][c] <= m \times n$
* `arr` 中的所有整数互不相同
* `mat` 中的所有整数互不相同

Solution:
### 哈希表

利用 `mat` 的数值各不相同,先使用「哈希表」对 `mat` 进行转存,以 $mat[i][j]$ 为键,$(i, j)$ 为值,方便后续快速查询某个值所在位置。

创建数组 `c1` 和 `c2`,分别记录某行某列有多少单元格被涂色,如 `c1[x] = a` 代表第 $x$ 行被涂色单元格数量为 $a$ 个,`c2[y] = b` 代表第 $y$ 列被涂色单元格数量为 $b$ 个。

遍历所有的 $arr[i]$,查询到 $arr[i]$ 的所在位置 $(x, y)$ 后,更新 `c1` 和 `c2`,若某行或某列被完全涂色,返回当前下标。

Java 代码:

```Java
class Solution {
    public int firstCompleteIndex(int[] arr, int[][] mat) {
        int n = mat.length, m = mat[0].length;
        Map<Integer, int[]> map = new HashMap<>();
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < m; j++) {
                map.put(mat[i][j], new int[]{i, j});
            }
        }
        int[] c1 = new int[n], c2 = new int[m];
        for (int i = 0; i < n * m; i++) {
            int[] info = map.get(arr[i]);
            int x = info[0], y = info[1];
            if (++c1[x] == m || ++c2[y] == n) return i;
        }
        return -1; // never
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    int firstCompleteIndex(vector<int>& arr, vector<vector<int>>& mat) {
        int n = mat.size(), m = mat[0].size();
        unordered_map<int, pair<int, int>> map;
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < m; j++) {
                map[mat[i][j]] = make_pair(i, j);
            }
        }
        vector<int> c1(n), c2(m);
        for (int i = 0; i < n * m; i++) {
            pair<int, int> info = map[arr[i]];
            int x = info.first, y = info.second;
            if (++c1[x] == m || ++c2[y] == n) return i;
        }
        return -1; // never
    }
};
```
Python 代码:
```Python
class Solution:
    def firstCompleteIndex(self, arr: List[int], mat: List[List[int]]) -> int:
        n, m = len(mat), len(mat[0])
        mapping = {mat[i][j]: (i, j) for i in range(n) for j in range(m)}
        c1, c2 = [0] * n, [0] * m
        for i in range(n * m):
            x, y = mapping[arr[i]]
            c1[x], c2[y] = c1[x] + 1, c2[y] + 1
            if c1[x] == m or c2[y] == n: return i
        return -1  # never
```
TypeScript 代码:
```TypeScript
function firstCompleteIndex(arr: number[], mat: number[][]): number {
    const n = mat.length, m = mat[0].length;
    const map: { [key: number]: [number, number] } = {};
    for (let i = 0; i < n; i++) {
        for (let j = 0; j < m; j++) {
            map[mat[i][j]] = [i, j];
        }
    }
    const c1 = new Array(n).fill(0), c2 = new Array(m).fill(0);
    for (let i = 0; i < n * m; i++) {
        const [x, y] = map[arr[i]];
        if (++c1[x] == m || ++c2[y] === n) return i;
    }
    return -1; // never
};
```
* 时间复杂度:$O(n \times m)$
* 空间复杂度:$O(n \times m)$

### 最后

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

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

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

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


LeetCode/2698-2700/2698. 求一个整数的惩罚数（中等）.md

2698. 求一个整数的惩罚数

Question:
给你一个正整数 $n$,请你返回 $n$ 的 惩罚数 。

$n$ 的 惩罚数 定义为所有满足以下条件 $i$ 的数的平方和:

* $1 <= i <= n$

* $i \times i$ 的十进制表示的字符串可以分割成若干连续子字符串,且这些子字符串对应的整数值之和等于 $i$ 。

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

输出:182

解释:总共有 3 个整数 i 满足要求:
- 1 ,因为 1 * 1 = 1
- 9 ,因为 9 * 9 = 81 ,且 81 可以分割成 8 + 1 。
- 10 ,因为 10 * 10 = 100 ,且 100 可以分割成 10 + 0 。
因此,10 的惩罚数为 1 + 81 + 100 = 182
```
示例 2:
```
输入:n = 37

输出:1478

解释:总共有 4 个整数 i 满足要求:
- 1 ,因为 1 * 1 = 1
- 9 ,因为 9 * 9 = 81 ,且 81 可以分割成 8 + 1 。
- 10 ,因为 10 * 10 = 100 ,且 100 可以分割成 10 + 0 。
- 36 ,因为 36 * 36 = 1296 ,且 1296 可以分割成 1 + 29 + 6 。
因此,37 的惩罚数为 1 + 81 + 100 + 1296 = 1478
```

提示:
* $1 <= n <= 1000$

Solution:
### 递归

一个朴素的做法是遍历 $[1, i]$,若当前数值 $i$ 满足要求,则将 $i \times i$ 累加到答案中。

问题关键转为:如何判定 $i \times i$ 是否能够分割成多个整数,使其累加值为 $i$。

简单做法是通过递归来做:每次从当前值的低位开始截取,通过「取余」和「地板除」操作,得到截取部分和剩余部分,再继续递归处理。

Java 代码:

```Java
class Solution {
    public int punishmentNumber(int n) {
        int ans = 0;
        for (int i = 1; i <= n; i++) {
            if (check(i * i, i)) ans += i * i;
        }
        return ans;
    }
    boolean check(int t, int x) {
        if (t == x) return true;
        int d = 10;
        while (t >= d && t % d <= x) {
            if (check(t / d, x - (t % d))) return true;
            d *= 10;
        }
        return false;
    }
}
```
Python 代码:
```Python
class Solution:
    def punishmentNumber(self, n: int) -> int:
        def check(t, x):
            if t == x:
                return True
            d = 10
            while t >= d and t % d <= x:
                if check(t // d, x - (t % d)):
                    return True
                d *= 10
            return False
        return sum([i * i if check(i * i, i) else 0 for i in range(1, n + 1)])
```
C++ 代码:
```C++
class Solution {
public:
    bool check(int t, int x) {
        if (t == x) return true;
        int d = 10;
        while (t >= d && t % d <= x) {
            if (check(t / d, x - (t % d))) return true;
            d *= 10;
        }
        return false;
    }
    int punishmentNumber(int n) {
        int ans = 0;
        for (int i = 1; i <= n; i++) {
            if (check(i * i, i)) ans += i * i;
        }
        return ans;
    }
};
```
TypeScript 代码:
```TypeScript
function punishmentNumber(n: number): number {
    function check(t: number, x: number): boolean {
        if (t === x) return true;
        let d = 10;
        while (t >= d && t % d <= x) {
            if (check(Math.floor(t / d), x - (t % d))) return true;
            d *= 10;
        }
        return false;
    }
    let ans = 0;
    for (let i = 1; i <= n; i++) {
        if (check(i * i, i)) ans += i * i;
    }
    return ans;
};
```
* 时间复杂度:$O(n \log{n^2})$
* 空间复杂度:$O(\log{n^2})$

---

### 打表

更进一步,对于 $[1, x]$ 范围内的惩罚数必然包含了 $[1, y]$(其中 $y < x$)中的惩罚数。

即多个样例之间必然存在重复计算,我们可通过「打表」进行预处理:定义 $f[i]$ 为 $n = i$ 时的答案(惩罚数),对于 $f[i]$ 而言,起始为 $f[i - 1]$,若数值 $i$ 本身满足要求,则将 $i \times i$ 累加到 $f[i]$ 当中。

Java 代码:
```Java
class Solution {
    static int[] f = new int[1010];
    static {
        for (int i = 1; i <= 1000; i++) {
            f[i] = f[i - 1];
            if (check(i * i, i)) f[i] += i * i;
        }
    }
    static boolean check(int t, int x) {
        if (t == x) return true;
        int d = 10;
        while (t >= d && t % d <= x) {
            if (check(t / d, x - (t % d))) return true;
            d *= 10;
        }
        return false;
    }
    public int punishmentNumber(int n) {
        return f[n];
    }
}
```
Python 代码:
```Python
def check(t, x):
    if t == x:
        return True
    d = 10
    while t >= d and t % d <= x:
        if check(t // d, x - (t % d)):
            return True
        d *= 10
    return False
f = [0] * 1010
for i in range(1, 1010):
    f[i] = f[i - 1] + (i * i if check(i * i, i) else 0)
class Solution:
    def punishmentNumber(self, n: int) -> int:
        return f[n]
```
C++ 代码:
```C++
inline bool check(int t, int x) {
    if (t == x) return true;
    int d = 10;
    while (t >= d && t % d <= x) {
        if (check(t / d, x - (t % d))) return true;
        d *= 10;
    }
    return false;
}
int f[1010];
int _ = []() {
    for (int i = 1; i < 1010; i++) {
        f[i] = f[i - 1];
        if (check(i * i, i)) f[i] += i * i;
    }
    return 0;
}();
class Solution {
public:
    int punishmentNumber(int n) {
        return f[n];
    }
};
```
TypeScript 代码:
```TypeScript
function check(t: number, x: number): boolean {
    if (t === x) return true;
    let d = 10;
    while (t >= d && t % d <= x) {
        if (check(Math.floor(t / d), x - (t % d))) return true;
        d *= 10;
    }
    return false;
}
const f = new Array(1010).fill(0);
for (let i = 1; i < 1010; i++) {
    f[i] = f[i - 1];
    if (check(i * i, i)) f[i] += i * i;
}
function punishmentNumber(n: number): number {
    return f[n];
};
```
* 时间复杂度:$O(1)$
* 空间复杂度:$O(C)$,其中 $C = 1000$

### 最后

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

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

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

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


273. 整数转换英文表示

Question:
将非负整数 `num` 转换为其对应的英文表示。

示例 1:
```
输入:num = 123

输出:"One Hundred Twenty Three"
```
示例 2:
```
输入:num = 12345

输出:"Twelve Thousand Three Hundred Forty Five"
```
示例 3:
```
输入:num = 1234567

输出:"One Million Two Hundred Thirty Four Thousand Five Hundred Sixty Seven"
```
示例 4:
```
输入:num = 1234567891

输出:"One Billion Two Hundred Thirty Four Million Five Hundred Sixty Seven Thousand Eight Hundred Ninety One"
```

提示:
* $0 <= num <= 2^{31} - 1$

Solution:
### 模拟

字符串大模拟,重点之一是考察大家对数字英文单词的熟练程度。🤣

首先,英文好的同学自然知道数字表示是每三位一组进行的,英文不好的可能需要通过样例来找找规律。

由于是每三位一组进行表示,首要考虑实现一个 `num2Str` 函数,将十进制长度小于等于 $3$ 位的数字表示出来,然后在后面配合 `Billion`、`Million` 和 `Thousand` 即可表示出范围不超过 $2^{32}-1$ 的任意数字。

从定义出发 `num2Str` 需要解决 $[0, 999]$ 范围内的所有整数,但由于该函数需要复用到更大的位数来配合 `Billion`、`Million` 和 `Thousand`,而 `Zero Billion` 并不是一个合法的描述,因此我们需要将 $0$ 抠出来特判,让 `num2Str` 对范围在 $[1, 999]$ 的数值进行转换。

考虑如何实现 `num2Str`,假设当前需要转换的数字为 $x$,我们可以对 $x$ 的大小进行分情况讨论:

1. $x >= 100$:此时首先需要表示成 `??? hundred`,表示完后考虑更小的位数;
2. $x >= 20$:此时需要表示成 `??? XXX-ty` 的形式,表示完后考虑更小的位数;
3. $x < 20$:直接描述成具体的单词。

实现完 `num2Str` 后,剩下的只需要考虑如何将入参 $num$ 拆分成每三位一组处理即可。

代码:
```Java
class Solution {
    static String[] num2str_small = {
        "Zero",
        "One", "Two", "Three", "Four", "Five", "Six", "Seven", "Eight", "Nine", "Ten",
        "Eleven", "Twelve", "Thirteen", "Fourteen", "Fifteen", "Sixteen", "Seventeen", "Eighteen", "Nineteen"
    };
    static String[] num2str_medium = {
        "", "", "Twenty", "Thirty", "Forty", "Fifty", "Sixty", "Seventy", "Eighty", "Ninety"
    };
    static String[] num2str_large = {
        "Billion", "Million", "Thousand", "",
    };
    String num2Str(int x) {
        String ans = "";
        if (x >= 100) {
            ans += num2str_small[x / 100] + " Hundred ";
            x %= 100;
        }
        if (x >= 20) {
            ans += num2str_medium[x / 10] + " ";
            x %= 10;
        }
        if (x != 0) ans += num2str_small[x] + " ";
        return ans;
    }
    public String numberToWords(int num) {
        if (num == 0) return num2str_small[0];
        StringBuilder sb = new StringBuilder();
        for (int i = (int)1e9, j = 0; i >= 1; i /= 1000, j++) {
            if (num < i) continue;
            sb.append(num2Str(num / i) + num2str_large[j] + " ");
            num %= i;
        }
        while (sb.charAt(sb.length() - 1) == ' ') sb.deleteCharAt(sb.length() - 1);
        return sb.toString();
    }
}
```
* 时间复杂度:令 $n$ 为 $num$ 数值大小,复杂度取决于最终构建的答案的长度,由于是每三位一组进行处理,同时每三位一组所转换的英文描述有明确的长度上界,因此最终答案长度与 $num$ 的十进制长度成线性关系,再根据 $num$ 的长度与 $num$ 数值的关系,可得最终复杂度为 $O(\log{n})$
* 空间复杂度:令 $n$ 为 $num$ 数值大小,复杂度取决于最终构建的答案的长度,由于是每三位一组进行处理,同时每三位一组所转换的英文描述有明确的长度上界,因此最终答案长度与 $num$ 的十进制长度成线性关系,再根据 $num$ 的长度与 $num$ 数值的关系,可得最终复杂度为 $O(\log{n})$

### 最后

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

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

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

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


Question:
给你一个整数数组 `citations`,其中 `citations[i]` 表示研究者的第 `i` 篇论文被引用的次数。计算并返回该研究者的 h 指数。

根据维基百科上 `h` 指数的定义:`h` 代表“高引用次数”,一名科研人员的 h指数是指他(她)的 (`n` 篇论文中)总共有 `h` 篇论文分别被引用了至少 `h` 次。且其余的 `n - h` 篇论文每篇被引用次数 不超过 `h` 次。

如果 `h` 有多种可能的值,`h` 指数是其中最大的那个。

示例 1:
```
输入:citations = [3,0,6,1,5]

输出:3

解释:给定数组表示研究者总共有 5 篇论文,每篇论文相应的被引用了 3, 0, 6, 1, 5 次。
由于研究者有 3 篇论文每篇 至少 被引用了 3 次,其余两篇论文每篇被引用 不多于 3 次,所以她的 h 指数是 3。
```
示例 2:
```
输入:citations = [1,3,1]

输出:1
```

提示:
* $n == citations.length$
* $1 <= n <= 5000$
* $0 <= citations[i] <= 1000$

Solution:
### 基本分析

为了方便,将 `citations` 记为 `cs`。

所谓的 `h` 指数是指一个具体的数值,**该数值为“最大”的满足「至少发表了 `x` 篇论文,且每篇论文至少被引用 `x` 次」定义的合法数,重点是“最大”**。

用题面的实例 $1$ 来举个 🌰,给定所有论文的引用次数情况为 `cs = [3,0,6,1,5]`,可统计满足定义的数值有哪些:

* $h = 0$,含义为「至少发表了 $0$ 篇,且这 $0$ 篇论文至少被引用 $0$ 次」,空集即满足,恒成立;

* $h = 1$,含义为「至少发表了 $1$ 篇,且这 $1$ 篇论文至少被引用 $1$ 次」,可以找到这样的组合,如 `[3]`,成立;

* $h = 2$,含义为「至少发表了 $2$ 篇,且这 $2$ 篇论文至少被引用 $2$ 次」,可以找到这样的组合,如 `[3, 6]`,成立;

* $h = 3$,含义为「至少发表了 $3$ 篇,且这 $3$ 篇论文至少被引用 $3$ 次」,可以找到这样的组合,如 `[3, 6, 5]`,成立;

* $h = 4$,含义为「至少发表了 $4$ 篇,且这 $4$ 篇论文至少被引用 $4$ 次」,找不到这样的组合,不成立;

* ...

实际上,**当遇到第一个无法满足的数时,更大的数值就没必要找了**。一个简单的推导:

至少出现 $k$ 次的论文数不足 $k$ 篇 => 至少出现 $k + 1$ 次的论文必然不足 $k$ 篇 => 至少出现 $k + 1$ 次的论文必然不足 $k + 1$ 篇(即更大的 $h$ 不满足)。

----

### 二分

基于此分析,我们发现对于任意的 `cs`(论文总数量为该数组长度 $n$),都必然对应了一个最大的 `h` 值,且**小于等于该 `h` 值的情况均满足,大于该 `h` 值的均不满足**。

那么,**在以最大 `h` 值为分割点的数轴上具有「二段性」**,可通过「二分」求解该分割点(答案)。

最后考虑在什么值域范围内进行二分?

一个合格的二分范围,仅需确保答案在此范围内即可。

再回看我们关于 `h` 的定义「至少发表了 `x` 篇论文,且每篇论文至少被引用 `x` 次」,满足条件除了引用次数,还有论文数量,而总的论文数量只有 $n$,因此最大的 `h` 只能是 $n$ 本身,而不能是比 $n$ 大的数,否则论文数量就不够了。

综上,我们只需要在 $[0, n]$ 范围进行二分即可。对于任意二分值 `mid`,只需线性扫描 `cs` 即可知道其是否合法。

Java 代码:
```Java
class Solution {
    public int hIndex(int[] cs) {
        int n = cs.length;
        int l = 0, r = n;
        while (l < r) {
            int mid = l + r + 1 >> 1;
            if (check(cs, mid)) l = mid;
            else r = mid - 1;
        }
        return r;
    }
    boolean check(int[] cs, int mid) {
        int ans = 0;
        for (int i : cs) if (i >= mid) ans++;
        return ans >= mid;
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    int hIndex(vector<int>& cs) {
        int n = cs.size();
        int l = 0, r = n;
        while (l < r) {
            int mid = (l + r + 1) / 2;
            if (check(cs, mid)) l = mid;
            else r = mid - 1;
        }
        return r;
    }
    bool check(vector<int>& cs, int x) {
        int cnt = 0;
        for (int c : cs) {
            if (c >= x) cnt++;
        }
        return cnt >= x;
    }
};
```
Python 代码:
```Python
class Solution:
    def hIndex(self, cs: List[int]) -> int:
        n = len(cs)
        l, r = 0, n
        while l < r:
            mid = (l + r + 1) // 2
            if sum(c >= mid for c in cs) >= mid:
                l = mid
            else:
                r = mid - 1
        return r
```
TypeScript 代码:
```TypeScript
function hIndex(cs: number[]): number {
    const check = function (cs: number[], x: number): boolean {
        let cnt: number = 0;
        for (let c of cs) {
            if (c >= x) cnt++;
        }
        return cnt >= x;
    }
    const n = cs.length;
    let l = 0, r = n;
    while (l < r) {
        const mid = Math.floor((l + r + 1) / 2);
        if (check(cs, mid)) l = mid;
        else r = mid - 1;
    }
    return r;
};
```
* 时间复杂度:对 $[0, n]$ 做二分,复杂度为 $O(\log{n})$;`check` 函数需要对数组进行线性遍历,复杂度为 $O(n)$。整体复杂度为 $O(n\log{n})$
* 空间复杂度:$O(1)$

---

### 计数

根据分析,利用最大的 `h` 不超过 $n$,我们可以再进一步,利用空间换时间,将复杂度降低到 $O(n)$。

假设我们预处理出引用次数所对应的论文数量 `cnt`,其中 `cnt[a] = b` 含义为引用次数 **恰好** 为 `a` 的论文数量有 `b` 篇。

那么再利用 `h` 是“最大”的满足定义的合法数,我们从 $n$ 开始往前找,找到的第一个满足条件的数,即是答案。

具体的,创建 `cnt` 数组,对 `cs` 进行计数,由于最大 `h` 不超过 $n$,因此对于引用次数超过 $n$ 的论文,可等价为引用次数为 $n$,即有计数逻辑 `cnt[min(c, n)]++`。

再根据处理好的 `cnt`,从 $n$ 开始倒序找 `h`。

由于我们处理的 `cnt[a]` 含义为引用次数 **恰好** 为 `a`,但题目定义则是 **至少**。同时「至少出现 $k + 1$ 次」的集合必然慢「至少出现 $k$ 次」要求(子集关系),我们可以使用变量 `tot`,对处理过的 `cnt[i]` 进行累加,从而实现从 **恰好** 到 **至少** 的转换。

Java 代码:
```Java
class Solution {
    public int hIndex(int[] cs) {
        int n = cs.length;
        int[] cnt = new int[n + 10];
        for (int c : cs) cnt[Math.min(c, n)]++;
        for (int i = n, tot = 0; i >= 0; i--) {
            tot += cnt[i];
            if (tot >= i) return i;
        }
        return -1; // never
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    int hIndex(vector<int>& cs) {
        int n = cs.size();
        vector<int> cnt(n + 10, 0);
        for (int c : cs) cnt[min(c, n)]++;
        for (int i = n, tot = 0; i >= 0; i--) {
            tot += cnt[i];
            if (tot >= i) return i;
        }
        return -1; // never
    }
};
```
Python 代码:
```Python
class Solution:
    def hIndex(self, cs: List[int]) -> int:
        n = len(cs)
        cnt = [0] * (n + 10)
        for c in cs:
            cnt[min(c, n)] += 1
        tot = 0
        for i in range(n, -1, -1):
            tot += cnt[i]
            if tot >= i:
                return i
        return -1  # never
```
TypeScript 代码:
```TypeScript
function hIndex(cs: number[]): number {
    const n = cs.length;
    const cnt = new Array(n + 10).fill(0);
    for (let c of cs) cnt[Math.min(c, n)]++;
    for (let i = n, tot = 0; i >= 0; i--) {
        tot += cnt[i];
        if (tot >= i) return i;
    }
    return -1; // never
};
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


Question:
给你一个整数数组 `citations`,其中 `citations[i]` 表示研究者的第 `i` 篇论文被引用的次数,`citations` 已经按照 **升序排列 **。计算并返回该研究者的 `h` 指数。

`h` 指数的定义:`h` 代表“高引用次数”(high citations),一名科研人员的 `h` 指数是指他(她)的 (`n` 篇论文中)总共有 `h` 篇论文分别被引用了至少 `h` 次。

请你设计并实现对数时间复杂度的算法解决此问题。

示例 1:
```
输入:citations = [0,1,3,5,6]

输出:3

解释:给定数组表示研究者总共有 5 篇论文,每篇论文相应的被引用了 0, 1, 3, 5, 6 次。
     由于研究者有 3 篇论文每篇 至少 被引用了 3 次,其余两篇论文每篇被引用 不多于 3 次,所以她的 h 指数是 3 。
```
示例 2:
```
输入:citations = [1,2,100]

输出:2
```

提示:
* $n = citations.length$
* $1 <= n <= 10^5$
* $0 <= citations[i] <= 1000$
* `citations` 按升序排列

Solution:
### 与前置题关系

本题与前置题 [274. H 指数](https://leetcode.cn/problems/h-index/) 的不同,主要体现在两个方面:

1. 数据范围不同:在 [274. H 指数](https://leetcode.cn/problems/h-index/) 里 $n$ 的范围为 $5000$,而本题 $n$ 的范围为 $10^5$;
2. 数组是否有序:在 [274. H 指数](https://leetcode-cn.com/problems/h-index/) 中数组不确保有序,本题则是有序。

增加了数组有序特性,又扩大了数据范围。

容易联想,利用此特性,存在复杂度更低的算法。

---

### 基本分析

为了方便,将 `citations` 记为 `cs`。

所谓的 `h` 指数是指一个具体的数值,**该数值为“最大”的满足「至少发表了 `x` 篇论文,且每篇论文至少被引用 `x` 次」定义的合法数,重点是“最大”**。

用题面的实例 $1$ 来举个 🌰,给定所有论文的引用次数情况为 `cs = [0,1,3,5,6]`,可统计满足定义的数值有哪些:

* $h = 0$,含义为「至少发表了 $0$ 篇,且这 $0$ 篇论文至少被引用 $0$ 次」,空集即满足,恒成立;

* $h = 1$,含义为「至少发表了 $1$ 篇,且这 $1$ 篇论文至少被引用 $1$ 次」,可以找到这样的组合,如 `[1]`,成立;

* $h = 2$,含义为「至少发表了 $2$ 篇,且这 $2$ 篇论文至少被引用 $2$ 次」,可以找到这样的组合,如 `[3, 5]`,成立;

* $h = 3$,含义为「至少发表了 $3$ 篇,且这 $3$ 篇论文至少被引用 $3$ 次」,可以找到这样的组合,如 `[3, 5, 6]`,成立;

* $h = 4$,含义为「至少发表了 $4$ 篇,且这 $4$ 篇论文至少被引用 $4$ 次」,找不到这样的组合,不成立;

* ...

实际上,**当遇到第一个无法满足的数时,更大的数值就没必要找了**。一个简单的推导:

至少出现 $k$ 次的论文数不足 $k$ 篇 => 至少出现 $k + 1$ 次的论文必然不足 $k$ 篇 => 至少出现 $k + 1$ 次的论文必然不足 $k + 1$ 篇(即更大的 $h$ 不满足)。

---

### 计数

首先,**仍能使用「计数」的方式进行求解,该求解为线性复杂度,且不要求数组有序**。

根据分析,最大的 `h` 不超过 $n$。

假设我们预处理出引用次数所对应的论文数量 `cnt`,其中 `cnt[a] = b` 含义为引用次数 **恰好** 为 `a` 的论文数量有 `b` 篇。

那么再利用 `h` 是“最大”的满足定义的合法数,我们从 $n$ 开始往前找,找到的第一个满足条件的数,即是答案。

具体的,创建 `cnt` 数组,对 `cs` 进行计数,由于最大 `h` 不超过 $n$,因此对于引用次数超过 $n$ 的论文,可等价为引用次数为 $n$,即有计数逻辑 `cnt[min(c, n)]++`。

再根据处理好的 `cnt`,从 $n$ 开始倒序找 `h`。

由于我们处理的 `cnt[a]` 含义为引用次数 **恰好** 为 `a`,但题目定义则是 **至少**。同时「至少出现 $k + 1$ 次」的集合必然慢「至少出现 $k$ 次」要求(子集关系),我们可以使用变量 `tot`,对处理过的 `cnt[i]` 进行累加,从而实现从 **恰好** 到 **至少** 的转换。

Java 代码:
```Java
class Solution {
    public int hIndex(int[] cs) {
        int n = cs.length;
        int[] cnt = new int[n + 10];
        for (int c : cs) cnt[Math.min(c, n)]++;
        for (int i = n, tot = 0; i >= 0; i--) {
            tot += cnt[i];
            if (tot >= i) return i;
        }
        return -1; // never
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    int hIndex(vector<int>& cs) {
        int n = cs.size();
        vector<int> cnt(n + 10, 0);
        for (int c : cs) cnt[min(c, n)]++;
        for (int i = n, tot = 0; i >= 0; i--) {
            tot += cnt[i];
            if (tot >= i) return i;
        }
        return -1; // never
    }
};
```
Python 代码:
```Python
class Solution:
    def hIndex(self, cs: List[int]) -> int:
        n = len(cs)
        cnt = [0] * (n + 10)
        for c in cs:
            cnt[min(c, n)] += 1
        tot = 0
        for i in range(n, -1, -1):
            tot += cnt[i]
            if tot >= i:
                return i
        return -1  # never
```
TypeScript 代码:
```TypeScript
function hIndex(cs: number[]): number {
    const n = cs.length;
    const cnt = new Array(n + 10).fill(0);
    for (let c of cs) cnt[Math.min(c, n)]++;
    for (let i = n, tot = 0; i >= 0; i--) {
        tot += cnt[i];
        if (tot >= i) return i;
    }
    return -1; // never
};
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n)$

---

### 二分答案(线性 `check`)

除了线性复杂度的「计数」做法,我们还容易想到「二分」。

其中**最容易想到的是「二分答案」,该做法复杂度为 $O(n\log{n})$,同样并不要求数组有序。**

我们发现对于任意的 `cs`(论文总数量为该数组长度 $n$),都必然对应了一个最大的 `h` 值,且小于等于该 `h` 值的情况均满足,大于该 `h` 值的均不满足。

那么,在以最大 `h` 值为分割点的数轴上具有「二段性」,可通过「二分」求解该分割点(答案)。

最后考虑在什么值域范围内进行二分?

一个合格的二分范围,仅需确保答案在此范围内即可。

再回看我们关于 `h` 的定义「至少发表了 `x` 篇论文,且每篇论文至少被引用 `x` 次」,满足条件除了引用次数,还有论文数量,而总的论文数量只有 $n$,因此最大的 `h` 只能是 $n$ 本身,而不能是比 $n$ 大的数,否则论文数量就不够了。

综上,我们只需要在 $[0, n]$ 范围进行二分即可。对于任意二分值 `mid`,只需线性扫描 `cs` 即可知道其是否合法。

Java 代码:
```Java
class Solution {
    public int hIndex(int[] cs) {
        int n = cs.length;
        int l = 0, r = n;
        while (l < r) {
            int mid = l + r + 1 >> 1;
            if (check(cs, mid)) l = mid;
            else r = mid - 1;
        }
        return r;
    }
    boolean check(int[] cs, int mid) {
        int ans = 0;
        for (int i : cs) if (i >= mid) ans++;
        return ans >= mid;
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    int hIndex(vector<int>& cs) {
        int n = cs.size();
        int l = 0, r = n;
        while (l < r) {
            int mid = (l + r + 1) / 2;
            if (check(cs, mid)) l = mid;
            else r = mid - 1;
        }
        return r;
    }
    bool check(vector<int>& cs, int x) {
        int cnt = 0;
        for (int c : cs) {
            if (c >= x) cnt++;
        }
        return cnt >= x;
    }
};
```
Python 代码:
```Python
class Solution:
    def hIndex(self, cs: List[int]) -> int:
        n = len(cs)
        l, r = 0, n
        while l < r:
            mid = (l + r + 1) // 2
            if sum(c >= mid for c in cs) >= mid:
                l = mid
            else:
                r = mid - 1
        return r
```
TypeScript 代码:
```TypeScript
function hIndex(cs: number[]): number {
    const check = function (cs: number[], x: number): boolean {
        let cnt: number = 0;
        for (let c of cs) {
            if (c >= x) cnt++;
        }
        return cnt >= x;
    }
    const n = cs.length;
    let l = 0, r = n;
    while (l < r) {
        const mid = Math.floor((l + r + 1) / 2);
        if (check(cs, mid)) l = mid;
        else r = mid - 1;
    }
    return r;
};
```
* 时间复杂度:对 $[0, n]$ 做二分,复杂度为 $O(\log{n})$;`check` 函数需要对数组进行线性遍历,复杂度为 $O(n)$。整体复杂度为 $O(n\log{n})$
* 空间复杂度:$O(1)$

---

### 二分下标(根据与 $cs[i]$ 关系)

在上述二分中,我们没有利用本题的「数组有序」的特性。

根据对 `h` 定义,若 $cs$ 升序,我们可推导出:
* 在最大的符合条件的分割点 $x$ 的右边(包含分割点),必然满足 $cs[i] >= x$
* 在最大的符合条件的分割点 $x$ 的左边,必然不满足 $cs[i] >= x$

因此,我们可以利用 **分割点右边数的个数与分割点 $cs[x]$ 的大小关系进行二分** 。

假设存在真实分割点下标 $x$,其值大小为 $cs[x]$,分割点右边的数值个数为 $n - x$,根据 `H 指数` 的定义,必然有 $cs[x] >= n - x$ 关系:

* 在分割点 $x$ 的右边:$cs[i]$ 非严格单调递增,数的个数严格单调递减,仍然满足 $cs[i] >= n - i$ 关系;
* 在分割点 $x$ 的左边:$cs[i]$ 非严格单调递减,数的个数严格单调递增,$x$ 作为真实分割点,必然不满足 $cs[i] >= n - i$ 关系。

利用此「二段性」进行二分即可,二分出下标后,再计算出数的个数。

Java 代码:
```Java
class Solution {
    public int hIndex(int[] cs) {
        int n = cs.length;
        int l = 0, r = n - 1;
        while (l < r) {
            int mid = l + r >> 1;
            if (cs[mid] >= n - mid) r = mid;
            else l = mid + 1;
        }
        return cs[r] >= n - r ? n - r : 0;
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    int hIndex(vector<int>& cs) {
        int n = cs.size();
        int l = 0, r = n - 1;
        while (l < r) {
            int mid = l + r >> 1;
            if (cs[mid] >= n - mid) r = mid;
            else l = mid + 1;
        }
        return cs[r] >= n - r ? n - r : 0;
    }
};
```
Python 代码:
```Python
class Solution:
    def hIndex(self, cs: List[int]) -> int:
        n = len(cs)
        l, r = 0, n - 1
        while l < r:
            mid = l + r >> 1
            if cs[mid] >= n - mid:
                r = mid
            else:
                l = mid + 1
        return n - r if cs[r] >= n - r else 0
```
TypeScript 代码:
```TypeScript
function hIndex(cs: number[]): number {
    const n = cs.length;
    let l = 0, r = n - 1;
    while (l < r) {
        const mid = l + r >> 1;
        if (cs[mid] >= n - mid) r = mid;
        else l = mid + 1;
    }
    return cs[r] >= n - r ? n - r : 0;
};
```
* 时间复杂度:$O(\log{n})$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


278. 第一个错误的版本

Question:
你是产品经理,目前正在带领一个团队开发新的产品。不幸的是,你的产品的最新版本没有通过质量检测。由于每个版本都是基于之前的版本开发的,所以错误的版本之后的所有版本都是错的。

假设你有 `n` 个版本 `[1, 2, ..., n]`,你想找出导致之后所有版本出错的第一个错误的版本。

你可以通过调用 `bool isBadVersion(version)` 接口来判断版本号 `version` 是否在单元测试中出错。实现一个函数来查找第一个错误的版本。你应该尽量减少对调用 `API` 的次数。

示例:
```
给定 n = 5,并且 version = 4 是第一个错误的版本。

调用 isBadVersion(3) -> false
调用 isBadVersion(5) -> true
调用 isBadVersion(4) -> true

所以,4 是第一个错误的版本。
```

Solution:
### 二分

一道交互题,根据题意可知,是尽可能少调用 `isBadVersion` 方法来找到分割点。

考虑存在「**没有错误版本**」和「**全是错误版本**」的情况,但如果往头部插入一个正确版本,往尾部插入一个错误版本作为哨兵,仍然具有「**二段性**」。

实际上,只需要进行这样的思考即可,不需要真正插入这样的哨兵,把这个哨兵逻辑放到最后返回的时候判断一下即可。

那么只需要将 `isBadVersion` 当做 `check` 函数进行二分即可。

**二分通常有以下两种写法,分别代表「找到最靠近中心的 `True`」 和「找到最靠近中心的 `False`」。**

另外根据数据范围需要注意计算 `mid` 时的爆 `int` 问题,可以通过使用类似 `l + (r - l) / 2` 的做法解决,也可以通过一个临时 `long` 来解决。

代码:
```Java
public class Solution extends VersionControl {
    public int firstBadVersion(int n) {
        int l = 1, r = n;
        while (l < r) {
            long tmp = (long)l + r >> 1;
            int mid = (int)tmp;
            if (isBadVersion(mid)) {
                r = mid;
            } else {
                l = mid + 1;
            }
        }
        return r;
    }
}
```
```Java
public class Solution extends VersionControl {
    public int firstBadVersion(int n) {
        int l = 1, r = n;
        while (l < r) {
            long tmp = (long)l + r + 1 >> 1;
            int mid = (int)tmp;
            if (!isBadVersion(mid)) {
                l = mid;
            } else {
                r = mid - 1;
            }
        }
        return !isBadVersion(r) ? r + 1 : r;
    }
}
```
* 时间复杂度:$O(\log{n})$
* 空间复杂度:$O(1)$

---

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

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

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

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

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

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

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

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

### 最后

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

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

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

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


Question:
给定正整数 n,找到若干个完全平方数(比如 1, 4, 9, 16, ...)使得它们的和等于 n。你需要让组成和的完全平方数的个数最少。

给你一个整数 n ,返回和为 n 的完全平方数的 最少数量 。

完全平方数 是一个整数,其值等于另一个整数的平方;换句话说,其值等于一个整数自乘的积。例如,1、4、9 和 16 都是完全平方数,而 3 和 11 不是。

示例 1:
```
输入:n = 12
输出:3
解释:12 = 4 + 4 + 4
```
示例 2:
```
输入:n = 13
输出:2
解释:13 = 4 + 9
```

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

Solution:
### 完全背包(朴素解法)

首先「完全平方数」有无限个,但要凑成的数字是给定的。

因此第一步可以将范围在 $[1, n]$ 内的「完全平方数」预处理出来。

这一步其实就是把所有可能用到的「物品」预处理出来。

从而将问题转换为:**给定了若干个数字,每个数字可以被使用无限次,求凑出目标值 $n$ 所需要用到的是最少数字个数是多少。**

由于题目没有限制我们相同的「完全平方数」只能使用一次,属于「完全背包」模型。

目前我们学过的两类背包问题(01 背包 & 完全背包)的原始状态定义都是两维:

* 第一维 $i$ 代表物品编号

* 第二维 $j$ 代表容量

其中第二维 $j$ 又有「不超过容量 $j$」和「容量恰好为 $j$」两种定义,本题要我们求「恰好」凑出 $n$ 所需要的最少个数。

因此我们可以调整我们的「状态定义」:

**$f[i][j]$ 为考虑前 $i$ 个数字,凑出数字总和 $j$ 所需要用到的最少数字数量。**

不失一般性的分析 $f[i][j]$,对于第 $i$ 个数字(假设数值为 $t$),我们有如下选择:

* 选 $0$ 个数字 $i$,此时有 $f[i][j] = f[i - 1][j]$
* 选 $1$ 个数字 $i$,此时有 $f[i][j] = f[i - 1][j - t] + 1$
* 选 $2$ 个数字 $i$,此时有 $f[i][j] = f[i - 1][j - 2 * t] + 2$
    ...
* 选 $k$ 个数字 $i$,此时有 $f[i][j] = f[i - 1][j - k * t] + k$

因此我们的状态转移方程为:

$$
f[i][j] = min(f[i-1][j-k*t]+k),0 \leqslant k * t \leqslant j
$$

当然,能够选择 $k$ 个数字 $i$ 的前提是,剩余的数字 $j - k * t$ 也能够被其他「完全平方数」凑出,即 $f[i - 1][j - k * t]$ 为有意义的值。

代码(朴素完全背包问题的复杂度是 $O(n^2 * \sqrt{n})$ 的,有超时风险,让物品下标从 $0$ 开始,单独处理第一个物品的 $P2$ 代码勉强能过):
```Java
class Solution {
    int INF = 0x3f3f3f3f;
    public int numSquares(int n) {
        // 预处理出所有可能用到的「完全平方数」
        List<Integer> list = new ArrayList<>();
        int t = 1;
        while (t * t <= n) {
            list.add(t * t);
            t++;
        }

        // f[i][j] 代表考虑前 i 个物品,凑出 j 所使用到的最小元素个数
        int m = list.size();
        int[][] f = new int[m + 1][n + 1];

        // 当没有任何数时,除了 f[0][0] 为 0(花费 0 个数值凑出 0),其他均为无效值
        Arrays.fill(f[0], INF);
        f[0][0] = 0;

        // 处理剩余数的情况
        for (int i = 1; i <= m ; i++) {
            int x = list.get(i - 1);
            for (int j = 0; j <= n; j++) {
                // 对于不选第 i 个数的情况
                f[i][j] = f[i - 1][j];
                // 对于选 k 次第 i 个数的情况
                for (int k = 1; k * x <= j; k++) {
                    // 能够选择 k 个 x 的前提是剩余的数字 j - k * x 也能被凑出
                    if (f[i - 1][j - k * x] != INF) {
                        f[i][j] = Math.min(f[i][j], f[i - 1][j - k * x] + k);
                    }
                }
            }
        }
        return f[m][n];
    }
}
```
```Java
class Solution {
    int INF = -1;
    public int numSquares(int n) {
        // 预处理出所有可能用到的「完全平方数」
        List<Integer> list = new ArrayList<>();
        int idx = 1;
        while (idx * idx <= n) {
            list.add(idx * idx);
            idx++;
        }

        // f[i][j] 代表考虑前 i 个物品,凑出 j 所使用到的最小元素个数
        int len = list.size();
        int[][] f = new int[len][n + 1];

        // 处理第一个数的情况
        for (int j = 0; j <= n; j++) {
            int t = list.get(0);
            int k = j / t;
            if (k * t == j) { // 只有容量为第一个数的整数倍的才能凑出
                f[0][j] = k;
            } else { // 其余则为无效值
                f[0][j] = INF;
            }
        }

        // 处理剩余数的情况
        for (int i = 1; i < len; i++) {
            int t = list.get(i);
            for (int j = 0; j <= n; j++) {
                // 对于不选第 i 个数的情况
                f[i][j] = f[i - 1][j];
                // 对于选 k 次第 i 个数的情况
                for (int k = 1; k * t <= j; k++) {
                    // 能够选择 k 个 t 的前提是剩余的数字 j - k * t 也能被凑出
                    // 使用 0x3f3f3f3f 作为最大值(预留累加空间)可以省去该判断
                    if (f[i - 1][j - k * t] != INF) {
                        f[i][j] = Math.min(f[i][j], f[i - 1][j - k * t] + k);
                    }
                }

            }
        }
        return f[len - 1][n];
    }
}
```
* 时间复杂度:预处理出所有可能用到的数字复杂度为 $O(\sqrt{n})$,共有 $n * \sqrt{n}$ 个状态需要转移,每个状态转移最多遍历 $n$ 次,因此转移完所有状态复杂度为 $O(n^2 * \sqrt{n})$。整体复杂度为 $O(n^2 * \sqrt{n})$。
* 空间复杂度:$O(n * \sqrt{n})$。

***

### 完全背包(进阶)

显然朴素版的完全背包进行求解复杂度有点高。

这次我们还是按照同样的思路再进行一次推导,加强对这种「一维空间优化」方式的理解。

从二维的状态转移方程入手进行分析(假设第 $i$ 个数字为 $t$):

至此,我们得到了最终的状态转移方程:

$$
f[j] = min(f[j], f[j - t] + 1)
$$

同时,预处理「物品」的逻辑也能直接下放到转移过程去做。

代码:
```Java
class Solution {
    public int numSquares(int n) {
        int[] f = new int[n + 1];
        Arrays.fill(f, 0x3f3f3f3f);
        f[0] = 0;
        for (int t = 1; t * t <= n; t++) {
            int x = t * t;
            for (int j = x; j <= n; j++) {
                f[j] = Math.min(f[j], f[j - x] + 1);
            }
        }
        return f[n];
    }
}
```
* 时间复杂度:共有 $n * \sqrt{n}$ 个状态需要转移,复杂度为 $O(n * \sqrt{n})$。
* 空间复杂度:$O(n)$。

### 最后

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

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

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

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


Question:
给你两个长度为 `n`、下标从 `0` 开始的整数数组 `nums1` 和 `nums2`,另给你一个下标从 `1` 开始的二维数组 `queries`,其中 $queries[i] = [x_{i}, y_{i}]$ 。

对于第 `i` 个查询,在所有满足 $nums1[j] >= x_{i}$ 且 $nums2[j] >= y_{i}$ 的下标 `j` ($0 <= j < n$) 中,找出 $nums1[j] + nums2[j]$ 的 最大值 ,如果不存在满足条件的 `j` 则返回 $-1$。

返回数组 `answer`,其中 `answer[i]` 是第 `i` 个查询的答案。

示例 1:
```
输入:nums1 = [4,3,1,2], nums2 = [2,4,9,5], queries = [[4,1],[1,3],[2,5]]

输出:[6,10,7]

解释:
对于第 1 个查询:xi = 4 且 yi = 1 ,可以选择下标 j = 0 ,此时 nums1[j] >= 4 且 nums2[j] >= 1 。nums1[j] + nums2[j] 等于 6 ,可以证明 6 是可以获得的最大值。
对于第 2 个查询:xi = 1 且 yi = 3 ,可以选择下标 j = 2 ,此时 nums1[j] >= 1 且 nums2[j] >= 3 。nums1[j] + nums2[j] 等于 10 ,可以证明 10 是可以获得的最大值。
对于第 3 个查询:xi = 2 且 yi = 5 ,可以选择下标 j = 3 ,此时 nums1[j] >= 2 且 nums2[j] >= 5 。nums1[j] + nums2[j] 等于 7 ,可以证明 7 是可以获得的最大值。
因此,我们返回 [6,10,7] 。
```
示例 2:
```
输入:nums1 = [3,2,5], nums2 = [2,3,4], queries = [[4,4],[3,2],[1,1]]

输出:[9,9,9]

解释:对于这个示例,我们可以选择下标 j = 2 ,该下标可以满足每个查询的限制。
```
示例 3:
```
输入:nums1 = [2,1], nums2 = [2,3], queries = [[3,3]]

输出:[-1]

解释:示例中的查询 xi = 3 且 yi = 3 。对于每个下标 j ,都只满足 nums1[j] < xi 或者 nums2[j] < yi 。因此,不存在答案。
```

提示:
* $nums1.length = nums2.length$
* $n = nums1.length$
* $1 <= n <= 10^5$
* $1 <= nums1[i], nums2[i] <= 10^9$
* $1 <= queries.length <= 10^5$
* $queries[i].length = 2$
* $x_{i} = queries[i][1]$
* $y_{i} = queries[i][2]$
* $1 <= x_{i}, y_{i} <= 10^9$

Solution:
### 离散化 + 排序 + 树状数组

根据题意,两个等长数组 `num1` 和 `nums2`,只能是相同下标的元素凑成一对。

不妨用两个一维数组 `nums1` 和 `nums2` 构建出一个二维数组 `nums`,方便后续处理,其中 $nums[i] = [nums1[i], nums2[i]]$。

同时对 `queries` 进行简单拓展,构建新的二维数组 `nq`,目的是对原有下标信息进行记录,其中 $nq[i] = [queries[i][0], queries[i][1], i]$(此处构建新 `nq` 的作用,下文会说)。

好了,现在我们有两个新的数组 `nums` 和 `nq`,接下来所有讨论都会针对新数组进行。

希望你牢牢记住:**$nums[i] = [nums1[i], nums2[i]]$ 是对 `nums1` 和 `nums2` 的简单合并;而 $nq[i] = [queries[i][0], queries[i][1], i]$ 是对 `queries` 原有下标的拓展记录**。

做完简单的预处理工作后,来思考一个前置问题:

假设其他内容不变,要求从满足「$nums[j][0] \geq nq[i][0]$ 且 $nums[j][1] \geq nq[i][1]$」调整为仅需满足「$nums[j][0] \geq nq[i][0]$」,如何求解?

一个简单的做法:

1. 对 `nums` 中的第一维(即 $nums[i][0] = nums1[i]$)和 `nq` 中第一维(即 $nq[i][0] = queries[i][0] = x_{i}$)排倒序;

2. 从前往后处理每个询问 $nq[i]$,同时使用变量 `idx` 对 `nums` 进行扫描,若满足 $nums[idx][0] \geq nq[i][0]$ 时,将 `idx` 右移,同时记录已被扫描的数对和 $nums[idx'][0] + nums[idx'][1]$ 。

   当 `idx` 不能再后移时,说明当前所有满足 $nums[idx][0] \geq nq[i][0]$ 要求的数对已被扫描完,在记录的数对和中取最大值,即是当前询问 $nq[i]$ 的答案 $ans[nq[i][2]]$(此处解释了为什么需要构造新数组 `nq`,因为询问处理是按照排序后进行,但在 `ans` 中需要映射回原有顺序)。

3. 重复步骤 $2$,直到处理完所有询问。

搞懂前置问题后,回到原问题,需要满足「$nums[j][0] \geq nq[i][0]$ 且 $nums[j][1] \geq nq[i][1]$」要求。

进一步思考,排序 + 调整询问顺序,只能解决其中一维限制要求,另一维该如何处理?

或更直接的问题:如何在被记录的所有数对和 $nums[idx'][0] + nums[idx'][1]$ 中找出那些满足  $nums[j][1] \geq nq[i][1]$ 的最大数对和。

不失一般性,假设当前处理到的是 $nums[idx]$,其中 $nums[idx][0]$ 的限制要求,通过前置问题的排序方式解决了。另外的 **$nums[idx][1]$ 我们希望作为“位置信息”,数对和 $sum = nums[idx][0] + nums[idx][1]$ 作为“值信息”进行记录**。

由于条件 $1 \leq x_{i}, y_{i} \leq 1e9$,我们需要对将要作为“位置信息”添加到树状数组的 $nums[idx][1]$ 和 $nq[i][1]$ 进行离散化,将其映射到 $[0, m - 1]$ 范围内。

对于每个询问,都需要找到已遍历过的大于 $nq[i][1]$ 的位置上的最大值,把离散化后的值域换成数组坐标,相当于求后缀最大值,后缀最大值可通过相反数,变成求前缀最大值。

能够实现类似的前缀操作,支持“单点修改”和“区间查询”的数据结构是「树状数组」。

Java 代码:

```Java
class Solution {
    int sz;
    int[] tr;
    int lowbit(int x) {
        return x & -x;
    }
    void add(int a, int b) {
        for (int i = a; i <= sz; i += lowbit(i)) tr[i] = Math.max(tr[i], b);
    }
    int query(int x) {
        int ans = -1;
        for (int i = x; i > 0; i -= lowbit(i)) ans = Math.max(ans, tr[i]);
        return ans;
    }
    public int[] maximumSumQueries(int[] nums1, int[] nums2, int[][] queries) {
        int n = nums1.length, m = queries.length;
        // 构建新的 nums 和 nq
        int[][] nums = new int[n][2];
        for (int i = 0; i < n; i++) nums[i] = new int[]{nums1[i], nums2[i]};
        int[][] nq = new int[m][3];
        for (int i = 0; i < m; i++) nq[i] = new int[]{queries[i][0], queries[i][1], i};

        // 对要添加到树状数组的 nums[i][1] 和 nq[i][1] 进行离散化(构建映射字典, 将原值映射到 [0, m - 1])
        Set<Integer> set = new HashSet<>();
        for (int[] x : nums) set.add(x[1]);
        for (int[] q : nq) set.add(q[1]);
        List<Integer> list = new ArrayList<>(set);
        Collections.sort(list);
        sz = list.size();
        Map<Integer, Integer> map = new HashMap<>();
        for (int i = 0; i < sz; i++) map.put(list.get(i), i);

        // 调整询问顺序, 解决其中一维限制
        Arrays.sort(nums, (a,b)->b[0]-a[0]);
        Arrays.sort(nq, (a,b)->b[0]-a[0]);

        tr = new int[sz + 10];
        Arrays.fill(tr, -1);

        int[] ans = new int[m];
        int idx = 0;
        for (int[] q : nq) {
            int x = q[0], y = q[1], i = q[2];
            // 扫描所有满足 nums[idx][0] >= x 的数对, 添加到树状数组中(其中离散值作为位置信息, 数对和作为值信息)
            while (idx < n && nums[idx][0] >= x) {
                add(sz - map.get(nums[idx][1]), nums[idx][0] + nums[idx][1]);
                idx++;
            }
            ans[i] = query(sz - map.get(y)); // 查询树状数组中的最值
        }
        return ans;
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    int sz;
    vector<int> tr;
    int lowbit(int x) {
        return x & -x;
    }
    void add(int a, int b) {
        for (int i = a; i <= sz; i += lowbit(i)) tr[i] = max(tr[i], b);
    }
    int query(int x) {
        int ans = -1;
        for (int i = x; i > 0; i -= lowbit(i)) ans = max(ans, tr[i]);
        return ans;
    }
    vector<int> maximumSumQueries(vector<int>& nums1, vector<int>& nums2, vector<vector<int>>& queries) {
        int n = nums1.size(), m = queries.size();
        // 构建新的 nums 和 nq
        vector<vector<int>> nums(n, vector<int>(2));
        vector<vector<int>> nq(m, vector<int>(3));
        for (int i = 0; i < n; i++) {
            nums[i][0] = nums1[i]; nums[i][1] = nums2[i];
        }
        for (int i = 0; i < m; i++) {
            nq[i][0] = queries[i][0]; nq[i][1] = queries[i][1]; nq[i][2] = i;
        }

        // 对要添加到树状数组的 nums[i][1] 和 nq[i][1] 进行离散化(构建映射字典, 将原值映射到 [0, m - 1])
        unordered_set<int> set;
        for (auto& x : nums) set.insert(x[1]);
        for (auto& q : nq) set.insert(q[1]);
        vector<int> list(set.begin(), set.end());
        sort(list.begin(), list.end());
        sz = list.size();
        map<int, int> map;
        for (int i = 0; i < sz; i++) map[list[i]] = i;

        // 调整询问顺序, 解决其中一维限制
        sort(nums.begin(), nums.end(), [](const vector<int>& a, const vector<int>& b) {
            return a[0] > b[0];
        });
        sort(nq.begin(), nq.end(), [](const vector<int>& a, const vector<int>& b) {
            return a[0] > b[0];
        });

        tr.resize(sz + 10, -1);

        vector<int> ans(m);
        int idx = 0;
        for (auto& q : nq) {
            int x = q[0], y = q[1], i = q[2];
            // 扫描所有满足 nums[idx][0] >= x 的数对, 添加到树状数组中(其中离散值作为位置信息, 数对和作为值信息)
            while (idx < n && nums[idx][0] >= x) {
                add(sz - map[nums[idx][1]], nums[idx][0] + nums[idx][1]);
                idx++;
            }
            ans[i] = query(sz - map[y]); // 查询树状数组中的最值
        }
        return ans;
    }
};
```
Python 代码:
```Python
class Solution:
    def maximumSumQueries(self, nums1: List[int], nums2: List[int], queries: List[List[int]]) -> List[int]:
        sz = 0
        tr = []

        def lowbit(x):
            return x & -x
        def add(a, b):
            i = a
            while i <= sz:
                tr[i] = max(tr[i], b)
                i += lowbit(i)
        def query(x):
            ans, i = -1, x
            while i > 0:
                ans = max(ans, tr[i])
                i -= lowbit(i)
            return ans

        n, m = len(nums1), len(queries)
        # 构建新的 nums 和 nq
        nums = [(nums1[i], nums2[i]) for i in range(n)]
        nq = [(queries[i][0], queries[i][1], i) for i in range(m)]

        #  对要添加到树状数组的 nums[i][1] 和 nq[i][1] 进行离散化(构建映射字典, 将原值映射到 [0, m - 1])
        unique_set = set()
        for x in nums:
            unique_set.add(x[1])
        for q in nq:
            unique_set.add(q[1])
        unique_list = list(unique_set)
        unique_list.sort()
        sz = len(unique_list)
        mapping = {val: idx for idx, val in enumerate(unique_list)}

        # 调整询问顺序, 解决其中一维限制
        nums.sort(key=lambda x: x[0], reverse=True)
        nq.sort(key=lambda x: x[0], reverse=True)

        tr = [-1] * (sz + 10)

        ans = [0] * m
        idx = 0
        for x, y, i in nq:
            # 扫描所有满足 nums[idx][0] >= x 的数对, 添加到树状数组中(其中离散值作为位置信息, 数对和作为值信息)
            while idx < n and nums[idx][0] >= x:
                add(sz - mapping[nums[idx][1]], nums[idx][0] + nums[idx][1])
                idx += 1
            ans[i] = query(sz - mapping[y])  # 查询树状数组中的最值
        return ans
```
TypeScript 代码:
```TypeScript
function maximumSumQueries(nums1: number[], nums2: number[], queries: number[][]): number[] {
    let sz = 0;
    let tr = [];

    const lowbit = function(x: number): number {
        return x & -x;
    }
    const add = function(a: number, b: number): void {
        for (let i = a; i <= sz; i += lowbit(i)) tr[i] = Math.max(tr[i], b);
    }
    const query = function(x: number): number {
        let ans = -1;
        for (let i = x; i > 0; i -= lowbit(i)) ans = Math.max(ans, tr[i]);
        return ans;
    }

    const n = nums1.length, m = queries.length;
    // 构建新的 nums 和 nq
    const nums = Array.from({ length: n }, (_, i) => [nums1[i], nums2[i]]);
    const nq = Array.from({ length: m }, (_, i) => [...queries[i], i]);

    // 对要添加到树状数组的 nums[i][1] 和 nq[i][1] 进行离散化(构建映射字典, 将原值映射到 [0, m - 1])
    const set: Set<number> = new Set();
    for (const x of nums) set.add(x[1]);
    for (const q of nq) set.add(q[1]);
    const list: number[] = [...set];
    list.sort((a, b) => a - b);
    sz = list.length;
    const mapping = new Map();
    for (let i = 0; i < sz; i++) mapping.set(list[i], i);

    // 调整询问顺序, 解决其中一维限制
    nums.sort((a, b) => b[0] - a[0]);
    nq.sort((a, b) => b[0] - a[0]);

    tr = Array(sz + 10).fill(-1);
    const ans = Array(m).fill(0);
    let idx = 0;
    for (let [x, y, i] of nq) {
        // 扫描所有满足 nums[idx][0] >= x 的数对, 添加到树状数组中(其中离散值作为位置信息, 数对和作为值信息)
        while (idx < n && nums[idx][0] >= x) {
            add(sz - mapping.get(nums[idx][1])!, nums[idx][0] + nums[idx][1]);
            idx++;
        }
        ans[i] = query(sz - mapping.get(y)!); // 查询树状数组中的最值
    }
    return ans;
};
```
* 时间复杂度:令 `nums1` 长度为 $n$,`queries` 长度为 $m$,构建 `nums` 和 `nq` 的复杂度为 $O(n + m)$;离散化复杂度为 $O((n + m) \log{(n + m)})$;对 `nums` 和 `nq` 的排序复杂度为 $O(n\log{n} + m\log{m})$;构建答案复杂度为 $O(m + n\log{n})$。整体复杂度为 $O((n + m) \log {(n + m)})$
* 空间复杂度:$O(n + m)$

---

### 进阶

本题解讲述了「从一维到二维偏序问题」时,可通过「一维排序,一维树状数组」求通解。

那三维偏序问题呢?是否存在通用的解决思路。

答:一维排序,一维归并,一维树状数组。

### 最后

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

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

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

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


2698. 求一个整数的惩罚数

Question:
给你一个下标从 $0$ 开始的整数数组 `nums` 和一个整数 `threshold`。

请你从 `nums` 的子数组中找出以下标 `l` 开头、下标 `r` 结尾 ($0 <= l <= r < nums.length$) 且满足以下条件的 最长子数组 :

* `nums[l] % 2 == 0`
* 对于范围 $[l, r - 1]$ 内的所有下标 `i`,`nums[i] % 2 != nums[i + 1] % 2`
* 对于范围 $[l, r]$ 内的所有下标 `i`,`nums[i] <= threshold`

以整数形式返回满足题目要求的最长子数组的长度。

注意:子数组 是数组中的一个连续非空元素序列。

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

输出:3

解释:在这个示例中,我们选择从 l = 1 开始、到 r = 3 结束的子数组 => [2,5,4] ,满足上述条件。
因此,答案就是这个子数组的长度 3 。可以证明 3 是满足题目要求的最大长度。
```
示例 2:
```
输入:nums = [1,2], threshold = 2

输出:1

解释:
在这个示例中,我们选择从 l = 1 开始、到 r = 1 结束的子数组 => [2] 。
该子数组满足上述全部条件。可以证明 1 是满足题目要求的最大长度。
```
示例 3:
```
输入:nums = [2,3,4,5], threshold = 4

输出:3

解释:
在这个示例中,我们选择从 l = 0 开始、到 r = 2 结束的子数组 => [2,3,4] 。
该子数组满足上述全部条件。
因此,答案就是这个子数组的长度 3 。可以证明 3 是满足题目要求的最大长度。
```

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

Solution:
### 双指针

整体题意:找 `nums` 中的最长的子数组 $[l, r]$,对于任意 $nums[i]$ 不超过 `threshold`,且从 $nums[l]$ 开始按照「先偶后奇」顺序交替。

假设子数组的左端点为 `i`,且“最远的”合法右端点为 `j`,那么在 $[i, j]$ 之间的任意右端点 `k`,即使能够使得 $nums[i...k]$ 合法,对统计答案而言,也是没有意义的,因为我们求的是最长。

基于此,我们容易想到:**找到所有的合法左端点 `i`,并统计该合法左端点的最远右端点 `j`。跳过 $[i, j]$ 之间的点作为左端点的情况,直接从结束位置 `j` 开始找下一个合法左端点。**

该做法可将朴素的 $O(n^2)$ 做法优化至 $O(n)$。

但,这做法为什么是正确的?

我们只考虑了 $[i, j]$ 中间点作为右端点的情况,那作为左端点呢?为什么跳过 $[i, j]$ 之间的 $k$ 作为左端点,正确性也不受影响?我们不是漏到了某些方案吗?

答案:**是漏掉了,但也只是漏掉了那些必不可能是最长子数组的方案**。

具体的,我们重新整理上述的「双指针」做法:

* 从前往后扫描 `nums`,变量 `i` 作为当前子数组左端点,首先确保 `i` 的合法性(跳过不满足 `nums[i] % 2 = 0` 和 `nums[i] <= threshold` 的位置)
* 随后在固定左端点 `i` 前提下,找最远的(第一个不满足要求的)右端点 `j`(值不超过 `threshold`,且奇偶性与前值交替)
* 得到当前连续段长度 $[i, j - 1]$,更新 `ans`,从当前结束位置 `j` 开始,重复上述过程,直到处理完 `nums`

Java 代码
```Java
class Solution {
    public int longestAlternatingSubarray(int[] nums, int threshold) {
        int n = nums.length, ans = 0, i = 0;
        while (i < n) {
            if ((nums[i] % 2 != 0 || nums[i] > threshold) && ++i >= 0) continue;
            int j = i + 1, cur = nums[i] % 2;
            while (j < n) {
                if (nums[j] > threshold || nums[j] % 2 == cur) break;
                cur = nums[j++] % 2;
            }
            ans = Math.max(ans, j - i);
            i = j;
        }
        return ans;
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    int longestAlternatingSubarray(vector<int>& nums, int threshold) {
        int n = nums.size(), ans = 0, i = 0;
        while (i < n) {
            if ((nums[i] % 2 != 0 || nums[i] > threshold) && ++i >= 0) continue;
            int j = i + 1, cur = nums[i] % 2;
            while (j < n) {
                if (nums[j] > threshold || nums[j] % 2 == cur) break;
                cur = nums[j++] % 2;
            }
            ans = max(ans, j - i);
            i = j;
        }
        return ans;
    }
};
```
Python 代码:
```Python
class Solution:
    def longestAlternatingSubarray(self, nums: List[int], threshold: int) -> int:
        n, ans, i = len(nums), 0, 0
        while i < n:
            if nums[i] % 2 != 0 or nums[i] > threshold:
                i += 1
                continue
            j, cur = i + 1, nums[i] % 2
            while j < n:
                if nums[j] > threshold or nums[j] % 2 == cur: break
                cur, j = nums[j] % 2, j + 1
            ans = max(ans, j - i)
            i = j
        return ans
```
TypeScript 代码:
```TypeScript
function longestAlternatingSubarray(nums: number[], threshold: number): number {
    let n = nums.length, ans = 0, i = 0
    while (i < n) {
        if ((nums[i] % 2 != 0 || nums[i] > threshold) && ++i >= 0) continue;
        let j = i + 1, cur = nums[i] % 2;
        while (j < n) {
            if (nums[j] > threshold || nums[j] % 2 == cur) break;
            cur = nums[j++] % 2;
        }
        ans = Math.max(ans, j - i);
        i = j;
    }
    return ans;
};
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


282. 给表达式添加运算符

Question:
给定一个仅包含数字 `0-9` 的字符串 `num` 和一个目标值整数 `target` ,在 `num` 的数字之间添加 二元 运算符(不是一元)`+`、`-` 或 `*` ,返回所有能够得到目标值的表达式。

示例 1:
```
输入: num = "123", target = 6

输出: ["1+2+3", "1*2*3"]
```
示例 2:
```
输入: num = "232", target = 8

输出: ["2*3+2", "2+3*2"]
```
示例 3:
```
输入: num = "105", target = 5

输出: ["1*0+5","10-5"]
```
示例 4:
```
输入: num = "00", target = 0

输出: ["0+0", "0-0", "0*0"]
```
示例 5:
```
输入: num = "3456237490", target = 9191

输出: []
```

提示:
* 1 <= num.length <= 10
* num 仅含数字
* $-2^{31} <= target <= 2^{31} - 1$

Solution:
### 回溯算法

最开始的想法是先使用 `DFS` 搜出来所有的表达式,然后套用 [(题解)227. 基本计算器 II](https://leetcode-cn.com/problems/basic-calculator-ii/solution/shi-yong-shuang-zhan-jie-jue-jiu-ji-biao-c65k/) 方案,计算所有表达式的结果,并将计算结果为 $target$ 的表达式加到结果集。

假设原字符串 $num$ 的长度为 $n$,由于每个位置之间存在四种插入决策(不插入符号、`+`、`-` 和 `*`),共有 $n - 1$ 个位置需要决策,因此搜索所有表达式的复杂度为 $O(4^{n - 1})$;同时需要对所有的表达式执行计算,复杂度为 $O(n)$,整体复杂度为 $O(n * 4^{n - 1})$。

添加运算符后的表达式长度不会超过 $20$,因此总的计算量应该是在 $10^7$ 以内,但可能是因为常数问题超时了(各种优化双栈操作也还是 TLE,在这上面浪费了好多时间 QWQ)。

**因此,我们需要考虑在搜索过程中进行计算,以避免使用 [(题解)227. 基本计算器 II](https://leetcode-cn.com/problems/basic-calculator-ii/solution/shi-yong-shuang-zhan-jie-jue-jiu-ji-biao-c65k/) 这种常数较大的计算方式。**

我们考虑如果只有 `+` 和 `-` 的话,可以很容易将运算和回溯搜索所有表达进行结合。但当存在 `*` 时,由于存在运算优先级的问题,我们需要记录形如 `a + b * c` 中的乘法部分。

实现上,除了记录当前决策到原串 $num$ 的哪一位 $u$,以及当前的运算结果 $cur$ 以外,还需要额外记录最后一次的计算结果 $prev$,然后在决策表达式中的第 $k$ 个部分时,对本次添加的运算符合做分情况讨论:

* 如果本次添加的 `+` 操作,且第 $k$ 项的值是 $next$:那么直接使用 $cur + next$ 来更新 $cur$,同时 $next$ 作为下一次的 $prev$;
* 如果本次添加的 `-` 操作,且第 $k$ 项的值是 $next$:同理,那么直接使用 $cur - next$ 来更新 $cur$,同时 $-next$ 作为下一次的 $prev$;
* 如果本次添加的 `*` 操作,且第 $k$ 项的值是 $next$:此时需要考虑运算符的优先级问题,由于本次的 $next$ 是与上一次的操作数 $prev$ 执行乘法,而 $cur$ 已经累加了 $prev$ 的影响,因此需要先减去 $prev$,再加上 $prev * next$,以此来更新 $cur$,同时 $prev * next$ 也作为下一次的 $prev$。

一些细节:需要注意前导零($0$ 单独作为一位是被允许的,但是多位且首部为 $0$ 是不允许的)以及 `+` 和 `-` 不作为一元运算符(运算符不能出现在表达式的首部)的情况。

代码:
```Java
class Solution {
    List<String> ans = new ArrayList<>();
    String s;
    int n, t;
    public List<String> addOperators(String num, int target) {
        s = num;
        n = s.length();
        t = target;
        dfs(0, 0, 0, "");
        return ans;
    }
    void dfs(int u, long prev, long cur, String ss) {
        if (u == n) {
            if (cur == t) ans.add(ss);
            return ;
        }
        for (int i = u; i < n; i++) {
            if (i != u && s.charAt(u) == '0') break;
            long next = Long.parseLong(s.substring(u, i + 1));
            if (u == 0) {
                dfs(i + 1, next, next, "" + next);
            } else {
                dfs(i + 1,  next, cur + next, ss + "+" + next);
                dfs(i + 1, -next, cur - next, ss + "-" + next);
                long x = prev * next;
                dfs(i + 1, x, cur - prev + x, ss + "*" + next);
            }
        }
    }
}
```
* 时间复杂度:$O(4^{n})$
* 空间复杂度:$O(4^{n})$

---

### 总结

**该做法表面上,只是实现了边搜索边计算的功能,但其本质上是利用了实数集 $S$ 和运算符 `+`(`-` 的本质也是 `+`)和 `*` 能够组成代数系统。**

利用代数系统 $(S, +, *)$,我们可以确保运算过程中的任意一个中间结果,都能使用形如 `a + b * c` 的形式进行表示,因此我们只需要多维护一个后缀串结果即可。

而代数系统的相关知识,以及如何确定能否作为一个代数系统,有兴趣的同学可以尝试去了解一下。

### 最后

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

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

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

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


Question:
请你设计一个迭代器,除了支持 `hasNext` 和 `next` 操作外,还支持 `peek` 操作。

实现 `PeekingIterator` 类:
* `PeekingIterator(int[] nums)` 使用指定整数数组 `nums` 初始化迭代器。
* `int next()` 返回数组中的下一个元素,并将指针移动到下个元素处。
* `bool hasNext()` 如果数组中存在下一个元素,返回 `true` ;否则,返回 `false`。
* `int peek()` 返回数组中的下一个元素,但 **不** 移动指针。

示例:
```
输入:
["PeekingIterator", "next", "peek", "next", "next", "hasNext"]
[[[1, 2, 3]], [], [], [], [], []]

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

解释:
PeekingIterator peekingIterator = new PeekingIterator([1, 2, 3]); // [1,2,3]
peekingIterator.next();    // 返回 1 ,指针移动到下一个元素 [1,2,3]
peekingIterator.peek();    // 返回 2 ,指针未发生移动 [1,2,3]
peekingIterator.next();    // 返回 2 ,指针移动到下一个元素 [1,2,3]
peekingIterator.next();    // 返回 3 ,指针移动到下一个元素 [1,2,3]
peekingIterator.hasNext(); // 返回 False
```

提示:
* $1 <= nums.length <= 1000$
* $1 <= nums[i] <= 1000$
* 对 `next` 和 `peek` 的调用均有效
* `next`、`hasNext` 和 `peek` 最多调用  $1000$ 次

**进阶:你将如何拓展你的设计?使之变得通用化,从而适应所有的类型,而不只是整数型?**

Solution:
### 迭代器基本认识 + 模拟

常规的迭代器的「访问」只支持两种操作:

* `hasNext()` 操作:如果存在下一元素,返回 `True`,否则返回 `False`。实现上,就是判断游标是否到达结尾位置;
* `next()` 操作:返回下一元素(当不存在下一元素时,返回 `null`)。实现上,就是返回游标指向的元素,并让游标后移。

在本题,还需要我们额外支持 `peek()` 操作,即在移动游标的前提下,返回游标指向的元素。

实现上,我们可以让操作提前一步进行,事先调用一次 `next()` 并使用该变量 $next$ 存起该元素,通过外部调用 `peek()` 还是 `next()` 来决定是否要更新 $next$;同时由于我们事先存起了下一访问位置的元素,我们可以通过判断 $next$ 是否为 `null` 来得知是否到达迭代器结尾(`hasNext()` 实现)。

代码:
```Java
class PeekingIterator implements Iterator<Integer> {
    Iterator<Integer> iter;
    Integer next;
	public PeekingIterator(Iterator<Integer> iterator) {
        iter = iterator;
        if (iter.hasNext()) next = iter.next();
	}

	public Integer peek() {
        return next;
	}

	@Override
	public Integer next() {
        Integer ans = next;
        next = iter.hasNext() ? iter.next() : null;
	    return ans;
	}

	@Override
	public boolean hasNext() {
        return next != null;
	}
}
```
* 时间复杂度:$O(1)$
* 空间复杂度:$O(1)$

---

### 进阶

* 你将如何拓展你的设计?使之变得通用化,从而适应所有的类型,而不只是整数型?

得益于 Java 的「泛型」设计,我们可以很轻松地支持任意类型:只需要将 `Integer` 修改成代指泛型的标识即可,例如 `E`。

代码:
```Java
class PeekingIterator implements Iterator<E> {
    Iterator<E> iter;
    E next;
	public PeekingIterator(Iterator<E> iterator) {
        iter = iterator;
        if (iter.hasNext()) next = iter.next();
	}

	public E peek() {
        return next;
	}

	@Override
	public E next() {
        E ans = next;
        next = iter.hasNext() ? iter.next() : null;
	    return ans;
	}

	@Override
	public boolean hasNext() {
        return next != null;
	}
}
```

Java 的泛型实现原理是「擦除法」。即实际上,都是以 `Object` 的顶层类型来存储,只不过在编译期,编译器会自动增加强制类型转换的代码,而在增加了强制类型转换的逻辑后,泛型信息也就不再需要,于是在编译过后,泛型信息会被直接擦除,而不会带到运行时。

其他不支持「泛型」的语言,可以采用类似的思路来实现:保存一个数据类型,在实现使用到泛型的接口时,先手动强转一下,再接收进来/返回出去。

### 最后

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

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

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

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


Question:
给定一个包含 `n + 1` 个整数的数组 `nums`,其数字都在 $[1, n]$ 范围内(包括 $1$ 和 $n$),可知至少存在一个重复的整数。

假设 `nums` 只有 一个重复的整数 ,返回 这个重复的数 。

你设计的解决方案必须 不修改 数组 `nums` 且只用常量级 $O(1)$ 的额外空间。

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

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

输出:3
```

提示:
* $1 <= n <= 10^5$
* $nums.length = n + 1$
* $1 <= nums[i] <= n$
* `nums` 中 只有一个整数 出现 两次或多次 ,其余整数均只出现 一次

进阶:
* 如何证明 `nums` 中至少存在一个重复的数字?
* 你可以设计一个线性级时间复杂度 $O(n)$ 的解决方案吗?

Solution:
### 原地哈希

数组长度为 $n + 1$,同时给定的 $nums[i]$ 都在 $[1, n]$ 范围内,因此我们可以设定哈希规则为 $nums[idx] = idx + 1$,即数值 `x` 会放在下标 $x - 1$ 的位置。

如此一来,对于只出现一次的数值而言,必然能够顺利放在目标位置,而出现多次的数值必然会因为位置冲突而被找出。

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

### 最后

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

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

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

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


LeetCode/2821-2830/2824. 统计和小于目标的下标对数目（简单）.md

2824. 统计和小于目标的下标对数目

Question:
给你一个下标从 `0` 开始长度为 `n` 的整数数组 `nums` 和一个整数 `target`,请你返回满足 `0 <= i < j < n` 且 `nums[i] + nums[j] < target` 的下标对 $(i, j)$ 的数目。

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

输出:3

解释:总共有 3 个下标对满足题目描述:
- (0, 1) ,0 < 1 且 nums[0] + nums[1] = 0 < target
- (0, 2) ,0 < 2 且 nums[0] + nums[2] = 1 < target
- (0, 4) ,0 < 4 且 nums[0] + nums[4] = 0 < target
注意 (0, 3) 不计入答案因为 nums[0] + nums[3] 不是严格小于 target 。
```
示例 2:
```
输入:nums = [-6,2,5,-2,-7,-1,3], target = -2

输出:10

解释:总共有 10 个下标对满足题目描述:
- (0, 1) ,0 < 1 且 nums[0] + nums[1] = -4 < target
- (0, 3) ,0 < 3 且 nums[0] + nums[3] = -8 < target
- (0, 4) ,0 < 4 且 nums[0] + nums[4] = -13 < target
- (0, 5) ,0 < 5 且 nums[0] + nums[5] = -7 < target
- (0, 6) ,0 < 6 且 nums[0] + nums[6] = -3 < target
- (1, 4) ,1 < 4 且 nums[1] + nums[4] = -5 < target
- (3, 4) ,3 < 4 且 nums[3] + nums[4] = -9 < target
- (3, 5) ,3 < 5 且 nums[3] + nums[5] = -3 < target
- (4, 5) ,4 < 5 且 nums[4] + nums[5] = -8 < target
- (4, 6) ,4 < 6 且 nums[4] + nums[6] = -4 < target
```

提示:
* $1 <= nums.length = n <= 50$
* $-50 <= nums[i], target <= 50$

Solution:
### 基本分析

为了方便,先对 `nums` 进行排序。

当 `nums` 有了有序特性后,剩下的便是「遍历右端点,在右端点左侧找最大合法左端点」或「遍历左端点,在左端点右侧找最大合法右端点」过程。

---

### 排序 + 二分

这是一种「遍历右端点,在右端点左侧找最大合法左端点」做法。

遍历右端点 `i`,然后在 $[0, i - 1]$ 范围内进行二分,找到最大的满足 $nums[j] + nums[i] < target$ 的位置 `j`。

若存在这样左端点 `j`,说明以 $nums[i]$ 为右端点时,共有 $j + 1$ 个(范围为 $[0, j]$ )个合法左端点,需要被统计。

Java 代码:

```Java
class Solution {
    public int countPairs(List<Integer> nums, int target) {
        Collections.sort(nums);
        int n = nums.size(), ans = 0;
        for (int i = 1; i < n; i++) {
            int l = 0, r = i - 1;
            while (l < r) {
                int mid = l + r + 1 >> 1;
                if (nums.get(mid) + nums.get(i) < target) l = mid;
                else r = mid - 1;
            }
            if (nums.get(r) + nums.get(i) < target) ans += r + 1;
        }
        return ans;
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    int countPairs(vector<int>& nums, int target) {
        sort(nums.begin(), nums.end());
        int n = nums.size(), ans = 0;
        for (int i = 1; i < n; i++) {
            int l = 0, r = i - 1;
            while (l < r) {
                int mid = l + r + 1 >> 1;
                if (nums[mid] + nums[i] < target) l = mid;
                else r = mid - 1;
            }
            if (nums[r] + nums[i] < target) ans += r + 1;
        }
        return ans;
    }
};

```
Python 代码:
```Python
class Solution:
    def countPairs(self, nums: List[int], target: int) -> int:
        nums.sort()
        n, ans = len(nums), 0
        for i in range(1, n):
            l, r = 0, i - 1
            while l < r:
                mid = l + r + 1 >> 1
                if nums[mid] + nums[i] < target: l = mid
                else: r = mid - 1
            if nums[r] + nums[i] < target: ans += r + 1
        return ans
```
TypeScript 代码:
```TypeScript
function countPairs(nums: number[], target: number): number {
    nums.sort((a,b)=>a-b);
    let n = nums.length, ans = 0;
    for (let i = 1; i < n; i++) {
        let l = 0, r = i - 1;
        while (l < r) {
            const mid = l + r + 1 >> 1;
            if (nums[mid] + nums[i] < target) l = mid;
            else r = mid - 1;
        }
        if (nums[r] + nums[i] < target) ans += r + 1;
    }
    return ans;
};
```
* 时间复杂度:排序复杂度为 $O(n\log{n})$;构造答案复杂度为 $O(n\log{n})$。整体复杂度为 $O(n\log{n})$
* 空间复杂度:$O(\log{n})$

---

### 排序 + 双指针

这是一种「遍历左端点,在左端点右侧找最大合法右端点」做法。

使用 `l` 和 `r` 分别指向排序好的 `nums` 的首尾。

若当前 $nums[l] + nums[r] \geq target$,说明此时对于 `l` 来说,`r` 并不合法,对 `r` 自减(左移)。

直到满足 $nums[l] + nums[r] < target$,此时对于 `l` 来说,找到了最右侧的合法右端点 `r`,在 $[l + 1, r]$ 期间的数必然仍满足 $nums[l] + nums[r] < target$,共有 $r - l$ 个(范围为 $[l + 1, r]$ )个合法右端点,需要被统计。

Java 代码:
```Java
class Solution {
    public int countPairs(List<Integer> nums, int target) {
        Collections.sort(nums);
        int n = nums.size(), ans = 0;
        for (int l = 0, r = n - 1; l < r; l++) {
            while (r >= 0 && nums.get(l) + nums.get(r) >= target) r--;
            if (l < r) ans += r - l;
        }
        return ans;
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    int countPairs(vector<int>& nums, int target) {
        sort(nums.begin(), nums.end());
        int n = nums.size(), ans = 0;
        for (int l = 0, r = n - 1; l < r; l++) {
            while (r >= 0 && nums[l] + nums[r] >= target) r--;
            if (l < r) ans += r - l;
        }
        return ans;
    }
};
```
Python 代码:
```Python
class Solution:
    def countPairs(self, nums: List[int], target: int) -> int:
        nums.sort()
        n, ans = len(nums), 0
        l, r = 0, n - 1
        while l < r:
            while r >= 0 and nums[l] + nums[r] >= target: r -= 1
            if l < r: ans += r - l
            l += 1
        return ans
```
TypeScript 代码:
```TypeScript
function countPairs(nums: number[], target: number): number {
    nums.sort((a,b)=>a-b);
    let n = nums.length, ans = 0;
    for (let l = 0, r = n - 1; l < r; l++) {
        while (r >= 0 && nums[l] + nums[r] >= target) r--;
        if (l < r) ans += r - l;
    }
    return ans;
};
```
* 时间复杂度:排序复杂度为 $O(n\log{n})$;构造答案复杂度为 $O(n)$。整体复杂度为 $O(n\log{n})$
* 空间复杂度:$O(\log{n})$

### 最后

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

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

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

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


2876. 有向图访问计数

Question:
现有一个有向图,其中包含 `n` 个节点,节点编号从 `0` 到 `n - 1`。此外,该图还包含了 `n` 条有向边。

给你一个下标从 `0` 开始的数组 `edges`,其中 `edges[i]` 表示存在一条从节点 `i` 到节点 `edges[i]` 的边。

想象在图上发生以下过程:

你从节点 `x` 开始,通过边访问其他节点,直到你在 此过程 中再次访问到之前已经访问过的节点。

返回数组 `answer` 作为答案,其中 `answer[i]` 表示如果从节点 `i` 开始执行该过程,你可以访问到的不同节点数。

示例 1:

```
输入:edges = [1,2,0,0]

输出:[3,3,3,4]

解释:从每个节点开始执行该过程,记录如下:
- 从节点 0 开始,访问节点 0 -> 1 -> 2 -> 0 。访问的不同节点数是 3 。
- 从节点 1 开始,访问节点 1 -> 2 -> 0 -> 1 。访问的不同节点数是 3 。
- 从节点 2 开始,访问节点 2 -> 0 -> 1 -> 2 。访问的不同节点数是 3 。
- 从节点 3 开始,访问节点 3 -> 0 -> 1 -> 2 -> 0 。访问的不同节点数是 4 。
```
示例 2:

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

输出:[5,5,5,5,5]

解释:无论从哪个节点开始,在这个过程中,都可以访问到图中的每一个节点。
```

提示:
* $n = edges.length$
* $2 <= n <= 10^5$
* $0 <= edges[i] <= n - 1$
* $edges[i] != i$

Solution:
### 内向基环森林 + 拓扑排序

根据题意,共 `n` 个点,`n` 条边,利用 `edges`,将 `i` 向 `edges[i]` 连有向边,可知每个点有唯一的出边,因此这是一张可能包含多棵「内向基环树」的「基环森林」。

基环树是指其具有 $n$ 个点 $n$ 条边的联通块,而「内向」是指树中任意节点有且只有一条出边,对应的「外向」是指树中任意节点有且只有一条入边。

例如,左图内向,右图外向:

显然,可根据当前节点是否在“环内”进行分情况讨论:

* 对于「环内」节点来说,其答案为环节点个数;
* 对于「环外」节点来说,直观感受应该是由环上节点转移而来。但由于本题给定的是「内向基环树」,因此我们需要对原图进行“反向”,然后从环内节点开始,进行 `BFS` ,从而更新其余非环节点答案。

具体的,我们使用如下思路进行求解:

1. 创建大小为 `n` 的数组 `in`,进行入度统计;
2. 根据入度进行「拓扑排序」,剩余满足 $in[i] \neq 0$ 的点,为「环内」的点。我们可处理出每个点所在环的大小,环的大小为这些点的答案。处理过程中收集这些「环内」的点(将来要从它们出发,更新其他「环外」节点)
3. 对原图进行“反向”,从收集好的「环内」点进行出发,运用 `BFS` 得出剩余点答案。

Java 代码:
```Java
class Solution {
    int N = 200010, M = N, idx = 0;
    int[] he = new int[N], e = new int[M], ne = new int[M];
    void add(int a, int b) {
        e[idx] = b;
        ne[idx] = he[a];
        he[a] = idx++;
    }
    public int[] countVisitedNodes(List<Integer> edges) {
        int n = edges.size();
        int[] in = new int[n], ans = new int[n];
        for (int x : edges) in[x]++;
        Deque<Integer> d = new ArrayDeque<>();
        for (int i = 0; i < n; i++) {
            if (in[i] == 0) d.addLast(i);
        }
        while (!d.isEmpty()) {
            int t = edges.get(d.pollFirst());
            if (--in[t] == 0) d.addLast(t);
        }
        // 处理环上的
        Set<Integer> set = new HashSet<>();
        for (int i = 0; i < n; i++) {
            if (in[i] == 0) continue;
            List<Integer> list = new ArrayList<>();
            list.add(i);
            int j = edges.get(i), val = 1;
            while (j != i) {
                list.add(j);
                j = edges.get(j);
                val++;
            }
            for (int x : list) {
                set.add(x);
                in[x] = 0;
                ans[x] = val;
            }
        }
        // 建立反向图, 处理非环上的, 从环内点出发进行往外更新
        Arrays.fill(he, -1);
        for (int i = 0; i < n; i++) add(edges.get(i), i);
        for (int u : set) {
            int val = ans[u];
            Deque<Integer> de = new ArrayDeque<>();
            de.addLast(u);
            while (!de.isEmpty()) {
                int sz = de.size();
                while (sz-- > 0) {
                    int t = de.pollFirst();
                    ans[t] = val;
                    for (int i = he[t]; i != -1; i = ne[i]) {
                        int j = e[i];
                        if (ans[j] != 0) continue;
                        de.addLast(j);
                    }
                }
                val++;
            }
        }
        return ans;
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    int he[200010], e[200010], ne[200010], idx;
    void add(int a, int b) {
        e[idx] = b;
        ne[idx] = he[a];
        he[a] = idx++;
    }
    vector<int> countVisitedNodes(vector<int>& edges) {
        int n = edges.size();
        vector<int> in(n, 0), ans(n, 0);
        for (int x : edges) in[x]++;
        queue<int> d;
        for (int i = 0; i < n; i++) {
            if (in[i] == 0) d.push(i);
        }
        while (!d.empty()) {
            int t = edges[d.front()];
            d.pop();
            if (--in[t] == 0) d.push(t);
        }
        set<int> s;
        for (int i = 0; i < n; i++) {
            if (in[i] == 0) continue;
            vector<int> list;
            list.push_back(i);
            int j = edges[i], val = 1;
            while (j != i) {
                list.push_back(j);
                j = edges[j];
                val++;
            }
            for (int x : list) {
                s.insert(x);
                in[x] = 0;
                ans[x] = val;
            }
        }
        memset(he, -1, sizeof(he));
        for (int i = 0; i < n; i++) add(edges[i], i);
        for (int u : s) {
            int val = ans[u];
            queue<int> de;
            de.push(u);
            while (!de.empty()) {
                int sz = de.size();
                while (sz-- > 0) {
                    int t = de.front();
                    de.pop();
                    ans[t] = val;
                    for (int i = he[t]; i != -1; i = ne[i]) {
                        int j = e[i];
                        if (ans[j] != 0) continue;
                        de.push(j);
                    }
                }
                val++;
            }
        }
        return ans;
    }
};
```
* 时间复杂度:统计入度复杂度为 $O(n)$;拓扑排序复杂度为 $O(n)$;统计「环内」节点答案复杂度为 $O(n)$;统计「环外」答案复杂度为 $O(n)$。整体复杂度为 $O(n)$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


Question:
你和你的朋友,两个人一起玩 `Nim` 游戏:

* 桌子上有一堆石头。
* 你们轮流进行自己的回合,你作为先手。
* 每一回合,轮到的人拿掉 `1-3` 块石头。
* 拿掉最后一块石头的人就是获胜者。

假设你们每一步都是最优解。请编写一个函数,来判断你是否可以在给定石头数量为 `n` 的情况下赢得游戏。如果可以赢,返回 `true`;否则,返回 `false`。

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

输出:false

解释:如果堆中有 4 块石头,那么你永远不会赢得比赛;
     因为无论你拿走 1 块、2 块 还是 3 块石头,最后一块石头总是会被你的朋友拿走。
```
示例 2:
```
输入:n = 1

输出:true
```
示例 3:
```
输入:n = 2

输出:true
```

提示:
* $1 <= n <= 2^{31} - 1$

Solution:
### 博弈论

这是一道 Nim 游戏的简化版。

**在不知晓博弈论结论前,可以先通过找规律得到猜想,然后再从「何种情况下,先手会处于必胜态」的角度来进行分析。**

根据题意,我们尝试从小范围数据的情况进行讨论:

1. 如果落到先手的局面为「**石子数量为 `1-3`**」的话,那么**先手必胜**;
2. 如果落到先手的局面为「**石子数量为 `4`**」的话,那么先手决策完(无论何种决策),交到后手的局面为「石子数量为 `1-3`」,即此时后手必胜,对应**先手必败**(到这里我们有一个推论:如果交给先手的局面为 `4` 的话,那么先手必败);
3. 如果落到先手的局面为「**石子数量为 `5-7`**」的话,那么先手可以通过控制选择石子的数量,来使得后手处于「石子数量为 `4`」的局面(此时后手必败),因此**先手必胜**;
4. 如果落到先手的局面为「**石子数量为 `8`**」的话,由于每次只能选 `1-3` 个石子,因此交由后手的局面为 `5-7`,根据流程 `3` 我们知道此时**先手必败**;
...

到这里,我们猜想 **当起始局面石子数量为 `4` 的倍数,则先手必败,否则先手必胜(即 `n % 4 != 0` 时,先手必胜)。**

然后我们通过「归纳法」证明一下该猜想的正确性。

在上面的「找规律」分析中,我们分情况讨论了最后一个决胜回合(我们称「剩余石子数量少于等于 `4` 的局面」为最后回合)的情况:**如果交由先手的石子数量为 `4`,那么先手必败,否则先手必胜。**

而对于「最后回合」前的任意回合(石子数量大于 `4`),我们需要证明 **先手可以通过调整所选石子数量,来维持「`n % 4 != 0`」直到最后回合。**

如果起始对先手而言满足「`n % 4 != 0`」,此时先手可以通过选择石子数量为「`n % 4`」来确保交到后手的局面为 `4` 的倍数。

那么根据推论,此时的原始后手作为下一回合的先手角色,且面临石子数量为 `4` 的倍数的局面,为必败态。

进一步的解释就是,由于原始后手面临石子数量为 `4` 的倍数的局面,且只能选 `1-3` 个石子,因此无论如何选择,重新回到原始先手的仍然满足「`n % 4 != 0`」(非 $4$ 的倍数)。

因此 **原始先手只需要确保每次都选择「`x % 4`」个石子($x$ 为当前石子数量),就可以确保交由自己的局面一直满足「`x % 4 != 0`」,交由对方的局面一直满足「`x % 4 == 0`」,直到最后回合的到来。**

至此,我们证明了 **如果起始石子数量 $n$ 满足「`n % 4 != 0`」条件,那么先手必胜。**

代码:
```Java
class Solution {
    public boolean canWinNim(int n) {
        return n % 4 != 0;
    }
}
```
* 时间复杂度:$O(1)$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


295. 数据流的中位数

Question:
中位数是有序列表中间的数。如果列表长度是偶数,中位数则是中间两个数的平均值。

例如,

* `[2,3,4]` 的中位数是 $3$
* `[2,3]` 的中位数是 $(2 + 3) / 2 = 2.5$

设计一个支持以下两种操作的数据结构:
* `void addNum(int num)` - 从数据流中添加一个整数到数据结构中。
* `double findMedian()` - 返回目前所有元素的中位数。

示例:
```
addNum(1)
addNum(2)
findMedian() -> 1.5
addNum(3)
findMedian() -> 2
```

进阶:
* 如果数据流中所有整数都在 $0$ 到 $100$ 范围内,你将如何优化你的算法?
* 如果数据流中 `99%` 的整数都在 $0$ 到 $100$ 范围内,你将如何优化你的算法?

Solution:
### 优先队列(堆)

这是一道经典的数据结构运用题。

具体的,**我们可以使用两个优先队列(堆)来维护整个数据流数据,令维护数据流左半边数据的优先队列(堆)为 `l`,维护数据流右半边数据的优先队列(堆)为 `r`**。

显然,**为了可以在 $O(1)$ 的复杂度内取得当前中位数,我们应当令 `l` 为大根堆,`r` 为小根堆,并人为固定 `l` 和 `r` 之前存在如下的大小关系**:

* **当数据流元素数量为偶数:`l` 和 `r` 大小相同,此时动态中位数为两者堆顶元素的平均值;**
* **当数据流元素数量为奇数:`l` 比 `r` 多一,此时动态中位数为 `l` 的堆顶原数。**

为了满足上述说的奇偶性堆大小关系,在进行 `addNum` 时,我们应当分情况处理:

* 插入前两者大小相同,说明插入前数据流元素个数为偶数,插入后变为奇数。我们期望操作完达到「`l` 的数量为 `r` 多一,同时双堆维持有序」,进一步分情况讨论:
    * 如果 `r` 为空,说明当前插入的是首个元素,直接添加到 `l` 即可;
    * 如果 `r` 不为空,且 `num <= r.peek()`,说明 `num` 的插入位置不会在后半部分(不会在 `r` 中),直接加到 `l` 即可;
    * 如果 `r` 不为空,且 `num > r.peek()`,说明 `num` 的插入位置在后半部分,此时将 `r` 的堆顶元素放到 `l` 中,再把 `num` 放到 `r`(相当于从 `r` 中置换一位出来放到 `l` 中)。

* 插入前两者大小不同,说明前数据流元素个数为奇数,插入后变为偶数。我们期望操作完达到「`l` 和 `r` 数量相等,同时双堆维持有序」,进一步分情况讨论(此时 `l` 必然比 `r` 元素多一):
    * 如果 `num >= l.peek()`,说明 `num` 的插入位置不会在前半部分(不会在 `l` 中),直接添加到 `r` 即可。
    * 如果 `num < l.peek()`,说明 `num` 的插入位置在前半部分,此时将 `l` 的堆顶元素放到 `r` 中,再把 `num` 放入 `l` 中(相等于从 `l` 中替换一位出来当到 `r` 中)。

代码:
```Java
class MedianFinder {
    PriorityQueue<Integer> l = new PriorityQueue<>((a,b)->b-a);
    PriorityQueue<Integer> r = new PriorityQueue<>((a,b)->a-b);
    public void addNum(int num) {
        int s1 = l.size(), s2 = r.size();
        if (s1 == s2) {
            if (r.isEmpty() || num <= r.peek()) {
                l.add(num);
            } else {
                l.add(r.poll());
                r.add(num);
            }
        } else {
            if (l.peek() <= num) {
                r.add(num);
            } else {
                r.add(l.poll());
                l.add(num);
            }
        }
    }
    public double findMedian() {
        int s1 = l.size(), s2 = r.size();
        if (s1 == s2) {
            return (l.peek() + r.peek()) / 2.0;
        } else {
            return l.peek();
        }
    }
}
```
* 时间复杂度:`addNum` 函数的复杂度为 $O(\log{n})$;`findMedian` 函数的复杂度为 $O(1)$
* 空间复杂度:$O(n)$

---

### 进阶

* 如果数据流中所有整数都在 $0$ 到 $100$ 范围内,你将如何优化你的算法?

可以使用建立长度为 $101$ 的桶,每个桶分别统计每个数的出现次数,同时记录数据流中总的元素数量,每次查找中位数时,先计算出中位数是第几位,从前往后扫描所有的桶得到答案。

**这种做法相比于对顶堆做法,计算量上没有优势,更多的是空间上的优化。**

**对顶堆解法两个操作中耗时操作复杂度为 $O(\log{n})$,$\log$ 操作常数不会超过 $3$,在极限数据 $10^7$ 情况下计算量仍然低于耗时操作复杂度为 $O(C)$($C$ 固定为 $101$)桶计数解法。**

* 如果数据流中 `99%` 的整数都在 $0$ 到 $100$ 范围内,你将如何优化你的算法?

和上一问解法类似,对于 $1$% 采用哨兵机制进行解决即可,在常规的最小桶和最大桶两侧分别维护一个有序序列,即建立一个代表负无穷和正无穷的桶。

上述两个进阶问题的代码如下,但注意由于真实样例的数据分布不是进阶所描述的那样(不是绝大多数都在 $[0,100]$ 范围内),会 `TLE`。

代码:
```Java
class MedianFinder {
    TreeMap<Integer, Integer> head = new TreeMap<>(), tail = new TreeMap<>();
    int[] arr = new int[101];
    int a, b, c;
    public void addNum(int num) {
        if (num >= 0 && num <= 100) {
            arr[num]++;
            b++;
        } else if (num < 0) {
            head.put(num, head.getOrDefault(num, 0) + 1);
            a++;
        } else if (num > 100) {
            tail.put(num, tail.getOrDefault(num, 0) + 1);
            c++;
        }
    }
    public double findMedian() {
        int size = a + b + c;
        if (size % 2 == 0) return (find(size / 2) + find(size / 2 + 1)) / 2.0;
        return find(size / 2 + 1);
    }
    int find(int n) {
        if (n <= a) {
            for (int num : head.keySet()) {
                n -= head.get(num);
                if (n <= 0) return num;
            }
        } else if (n <= a + b) {
            n -= a;
            for (int i = 0; i <= 100; i++) {
                n -= arr[i];
                if (n <= 0) return i;
            }
        } else {
            n -= a + b;
            for (int num : tail.keySet()) {
                n -= tail.get(num);
                if (n <= 0) return num;
            }
        }
        return -1; // never
    }
}
```

### 最后

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

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

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

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


LeetCode/291-300/297. 二叉树的序列化与反序列化（困难）.md

297. 二叉树的序列化与反序列化

Question:
序列化是将一个数据结构或者对象转换为连续的比特位的操作,进而可以将转换后的数据存储在一个文件或者内存中,同时也可以通过网络传输到另一个计算机环境,采取相反方式重构得到原数据。

请设计一个算法来实现二叉树的序列化与反序列化。这里不限定你的序列 / 反序列化算法执行逻辑,你只需要保证一个二叉树可以被序列化为一个字符串并且将这个字符串反序列化为原始的树结构。

提示: 输入输出格式与 LeetCode 目前使用的方式一致,详情请参阅 LeetCode 序列化二叉树的格式。你并非必须采取这种方式,你也可以采用其他的方法解决这个问题。

示例 1:

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

提示:
* 树中结点数在范围 $[0, 10^4]$ 内
* -1000 <= Node.val <= 1000

Solution:
### 基本思路

无论使用何种「遍历方式」进行二叉树存储,为了方便,我们都需要对空节点有所表示。

其实题目本身的样例就给我们提供了很好的思路:**使用层序遍历的方式进行存储,对于某个叶子节点的空节点进行存储,同时确保不递归存储空节点对应的子节点。**

---

### 层序遍历

根据节点值的数据范围 `-1000 <= Node.val <= 1000`(我是在 [297. 二叉树的序列化与反序列化](https://leetcode-cn.com/problems/serialize-and-deserialize-binary-tree/) 看的,你也可以不使用数字,使用某个特殊字符进行表示,只要能在反序列时有所区分即可),我们可以建立占位节点 `emptyNode` 用来代指空节点(`emptyNode.val = INF`)。

序列化:先特判掉空树的情况,之后就是常规的层序遍历逻辑:

1. 起始时,将 `root` 节点入队;
2. 从队列中取出节点,检查节点是否有左/右节点:
    * 如果有的话,将值追加序列化字符中(注意使用分隔符),并将节点入队;
    * 如果没有,检查当前节点是否为 `emptyNode` 节点,如果不是 `emptyNode` 说明是常规的叶子节点,需要将其对应的空节点进行存储,即将 `emptyNode` 入队;
3. 循环流程 $2$,直到整个队列为空。

反序列:同理,怎么「序列化」就怎么进行「反序列」即可:

1. 起始时,构造出 `root` 并入队;
2. 每次从队列取出元素时,同时从序列化字符中截取两个值(对应左右节点),检查是否为 `INF`,若不为 `INF` 则构建对应节点;
3. 循环流程 $2$,直到整个序列化字符串被处理完(注意跳过最后一位分隔符)。

代码:
```Java
public class Codec {
    int INF = -2000;
    TreeNode emptyNode = new TreeNode(INF);
    public String serialize(TreeNode root) {
        if (root == null) return "";

        StringBuilder sb = new StringBuilder();
        Deque<TreeNode> d = new ArrayDeque<>();
        d.addLast(root);
        while (!d.isEmpty()) {
            TreeNode poll = d.pollFirst();
            sb.append(poll.val + "_");
            if (!poll.equals(emptyNode)) {
                d.addLast(poll.left != null ? poll.left : emptyNode);
                d.addLast(poll.right != null ? poll.right : emptyNode);
            }
        }
        return sb.toString();
    }

    public TreeNode deserialize(String data) {
        if (data.equals("")) return null;

        String[] ss = data.split("_");
        int n = ss.length;
        TreeNode root = new TreeNode(Integer.parseInt(ss[0]));
        Deque<TreeNode> d = new ArrayDeque<>();
        d.addLast(root);
        for (int i = 1; i < n - 1; i += 2) {
            TreeNode poll = d.pollFirst();
            int a = Integer.parseInt(ss[i]), b = Integer.parseInt(ss[i + 1]);
            if (a != INF) {
                poll.left = new TreeNode(a);
                d.addLast(poll.left);
            }
            if (b != INF) {
                poll.right = new TreeNode(b);
                d.addLast(poll.right);
            }
        }
        return root;
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


Question:
你在和朋友一起玩 猜数字(`Bulls and Cows`)游戏,该游戏规则如下:

写出一个秘密数字,并请朋友猜这个数字是多少。朋友每猜测一次,你就会给他一个包含下述信息的提示:

* 猜测数字中有多少位属于数字和确切位置都猜对了(称为 `"Bulls"`, 公牛),
* 有多少位属于数字猜对了但是位置不对(称为 `"Cows"`, 奶牛)。也就是说,这次猜测中有多少位非公牛数字可以通过重新排列转换成公牛数字。

给你一个秘密数字 `secret` 和朋友猜测的数字 `guess`,请你返回对朋友这次猜测的提示。

提示的格式为 `"xAyB"` ,`x` 是公牛个数, `y` 是奶牛个数,`A` 表示公牛,`B` 表示奶牛。

请注意秘密数字和朋友猜测的数字都可能含有重复数字。

示例 1:
```
输入: secret = "1807", guess = "7810"

输出: "1A3B"

解释: 数字和位置都对(公牛)用 '|' 连接,数字猜对位置不对(奶牛)的采用斜体加粗标识。
"1807"
  |
"7810"
```
示例 2:
```
输入: secret = "1123", guess = "0111"

输出: "1A1B"

解释: 数字和位置都对(公牛)用 '|' 连接,数字猜对位置不对(奶牛)的采用斜体加粗标识。
"1123"        "1123"
  |      or     |
"0111"        "0111"
注意,两个不匹配的 1 中,只有一个会算作奶牛(数字猜对位置不对)。通过重新排列非公牛数字,其中仅有一个 1 可以成为公牛数字。
```
示例 3:
```
输入:secret = "1", guess = "0"

输出:"0A0B"
```
示例 4:
```
输入:secret = "1", guess = "1"

输出:"1A0B"
```

提示:
* 1 <= secret.length, guess.length <= 1000
* secret.length == guess.length
* secret 和 guess 仅由数字组成

Solution:
### 模拟

根据题意,我们可以对 $secret$ 和 $guess$ 进行诸位比较,统计公牛数量 $a$ 和奶牛数量 $b$。

对于字符相同的位置,我们可以直接对 $a$ 进行自增;对于字符不同的位置,使用「哈希表」进行分别统计 $secret$ 和 $guess$ 的词频,某个数字 $x$ 在两者词频中的较小值,即为该数字对应的奶牛数量,统计所有数字 $[0, 9]$ 的奶牛数量总和即为 $b$。

代码:
```Java
class Solution {
    public String getHint(String secret, String guess) {
        int n = secret.length();
        int a = 0, b = 0;
        int[] cnt1 = new int[10], cnt2 = new int[10];
        for (int i = 0; i < n; i++) {
            int c1 = secret.charAt(i) - '0', c2= guess.charAt(i) - '0';
            if (c1 == c2) {
                a++;
            } else {
                cnt1[c1]++;
                cnt2[c2]++;
            }
        }
        for (int i = 0; i < 10; i++) b += Math.min(cnt1[i], cnt2[i]);
        return a + "A" + b + "B";
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:令 $C$ 为字符集大小,$C$ 固定为 $10$。复杂度为 $O(C)$

### 最后

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

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

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

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