Question:
你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。

给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。

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

输出:4

解释:偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。
     偷窃到的最高金额 = 1 + 3 = 4 。
```
示例 2:
```
输入:[2,7,9,3,1]

输出:12

解释:偷窃 1 号房屋 (金额 = 2), 偷窃 3 号房屋 (金额 = 9),接着偷窃 5 号房屋 (金额 = 1)。
     偷窃到的最高金额 = 2 + 9 + 1 = 12 。
```

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

Solution:
### 状态机 DP

这是一道入门级的状态机线性 DP 题。

定义 $f[i][j]$ 为考虑前 $i$ 间房子,且第 $i$ 间房子的状态为 $j$ 时所能取得的最大价值(其中 $j = 0$ 代表不偷该房子,$j = 1$ 代表偷该房子)。

再结合题意,因为相邻房子不能同时被偷,可推导出状态转移方程为:

* 当前房子不偷,则对前一间房子的状态无要求,状态值为前一状态的较大值:$f[i][0] = \max(f[i - 1][0], f[i - 1][1])$
* 当前房子偷,此时限定了前一间只能不偷,状态值为前一间房子不偷时的最大价值,加上当前房子的价值: $f[i][1] = f[i - 1][0] + nums[i - 1]$

最终 $\max(f[n][0], f[n][1])$ 即是答案。

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

---

### 优化

利用 $f[i][X]$ 仅依赖于 $f[i - 1][X]$,我们可以通过「滚动数组」的方式将空间优化至 $O(1)$。

Java 代码:
```Java
class Solution {
    public int rob(int[] nums) {
        int n = nums.length;
        int[][] f = new int[][]{{0, 0}, {0, 0}};
        for (int i = 1; i <= n; i++) {
            int a = (i - 1) & 1, b = i & 1;
            f[b][0] = Math.max(f[a][0], f[a][1]);
            f[b][1] = f[a][0] + nums[i - 1];
        }
        return Math.max(f[n & 1][0], f[n & 1][1]);
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    int rob(vector<int>& nums) {
        int n = nums.size();
        int f[][2] = {{0, 0}, {0, 0}};
        for (int i = 1; i <= n; i++) {
            int a = (i - 1) & 1, b = i & 1;
            f[b][0] = max(f[a][0], f[a][1]);
            f[b][1] = f[a][0] + nums[i - 1];
        }
        return max(f[n & 1][0], f[n & 1][1]);
    }
};
```
TypeScript 代码:
```TypeScript
function rob(nums: number[]): number {
    const n = nums.length
    const f = [[0, 0], [0, 0]]
    for (let i = 1; i <= n; i++) {
        const a = (i - 1) & 1, b = i & 1
        f[b][0] = Math.max(f[a][0], f[a][1])
        f[b][1] = f[a][0] + nums[i - 1]
    }
    return Math.max(f[n & 1][0], f[n & 1][1])
}
```
Python 代码:
```Python
class Solution:
    def rob(self, nums: List[int]) -> int:
        n = len(nums)
        f = [[0, 0], [0, 0]]
        for i in range(1, n + 1):
            a, b = (i - 1) & 1, i & 1
            f[b][0] = max(f[a][0], f[a][1])
            f[b][1] = f[a][0] + nums[i - 1]
        return max(f[n & 1][0], f[n & 1][1])
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


199. 二叉树的右视图

Question:
给定一个二叉树的 根节点 `root`,想象自己站在它的右侧,按照从顶部到底部的顺序,返回从右侧所能看到的节点值。

示例 1:

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

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

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

输出: []
```

提示:
* 二叉树的节点个数的范围是 $[0,100]$
* $-100 <= Node.val <= 100$

Solution:
### BFS

本质就是找层序遍历过程中,每层的最后一个节点。

Java 代码:

```Java
/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode() {}
 *     TreeNode(int val) { this.val = val; }
 *     TreeNode(int val, TreeNode left, TreeNode right) {
 *         this.val = val;
 *         this.left = left;
 *         this.right = right;
 *     }
 * }
 */
class Solution {
    public List<Integer> rightSideView(TreeNode root) {
        List<Integer> ans = new ArrayList<>();
        if (root == null) return ans;
        Deque<TreeNode> d = new ArrayDeque<>();
        d.addLast(root);
        while (!d.isEmpty()) {
            int sz = d.size();
            while (sz-- > 0) {
                TreeNode node = d.pollFirst();
                if (node.left != null) d.addLast(node.left);
                if (node.right != null) d.addLast(node.right);
                if (sz == 0) ans.add(node.val);
            }
        }
        return ans;
    }
}
```
C++ 代码:
```C++
/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode() : val(0), left(nullptr), right(nullptr) {}
 *     TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
 *     TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
 * };
 */
class Solution {
public:
    vector<int> rightSideView(TreeNode* root) {
        vector<int> ans;
        if (!root) return ans;
        queue<TreeNode*> d;
        d.push(root);
        while (!d.empty()) {
            int sz = d.size();
            while (sz-- > 0) {
                TreeNode* node = d.front();
                d.pop();
                if (node->left) d.push(node->left);
                if (node->right) d.push(node->right);
                if (sz == 0) ans.push_back(node->val);
            }
        }
        return ans;
    }
};
```
Python 代码:
```Python
# Definition for a binary tree node.
# class TreeNode:
#     def __init__(self, val=0, left=None, right=None):
#         self.val = val
#         self.left = left
#         self.right = right
class Solution:
    def rightSideView(self, root: Optional[TreeNode]) -> List[int]:
        ans = []
        if not root:
            return ans
        d = deque()
        d.append(root)
        while d:
            sz = len(d)
            while sz > 0:
                node = d.popleft()
                if node.left:
                    d.append(node.left)
                if node.right:
                    d.append(node.right)
                if sz == 1:
                    ans.append(node.val)
                sz -= 1
        return ans
```
TypeScript 代码:
```TypeScript
/**
 * Definition for a binary tree node.
 * class TreeNode {
 *     val: number
 *     left: TreeNode | null
 *     right: TreeNode | null
 *     constructor(val?: number, left?: TreeNode | null, right?: TreeNode | null) {
 *         this.val = (val===undefined ? 0 : val)
 *         this.left = (left===undefined ? null : left)
 *         this.right = (right===undefined ? null : right)
 *     }
 * }
 */
function rightSideView(root: TreeNode | null): number[] {
    const ans = [];
    if (!root) return ans;
    const d = [];
    d.push(root);
    while (d.length > 0) {
        let sz = d.length;
        while (sz-- > 0) {
            const node = d.shift()!;
            if (node.left) d.push(node.left);
            if (node.right) d.push(node.right);
            if (sz === 0) ans.push(node.val);
        }
    }
    return ans;
};
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n)$

---

### DFS

`DFS` 解法同理,规定好节点的遍历顺序(例如 `中-右-左`),并在 `DFS`  过程中传递当前层参数 `level`(根节点为第 `0` 层),同时使用一个 `Set` 数据结构记录处理过的层编号,从而实现只将当前层的首个节点添加到答案中。

Java 代码:
```Java
/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode() {}
 *     TreeNode(int val) { this.val = val; }
 *     TreeNode(int val, TreeNode left, TreeNode right) {
 *         this.val = val;
 *         this.left = left;
 *         this.right = right;
 *     }
 * }
 */
class Solution {
    List<Integer> ans = new ArrayList<>();
    Set<Integer> set = new HashSet<>();
    public List<Integer> rightSideView(TreeNode root) {
        dfs(root, 0);
        return ans;
    }
    void dfs(TreeNode node, int level) {
        if (node == null) return ;
        if (!set.contains(level)) {
            ans.add(node.val);
            set.add(level);
        }
        dfs(node.right, level + 1);
        dfs(node.left, level + 1);
    }
}
```
C++ 代码:
```C++
/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode() : val(0), left(nullptr), right(nullptr) {}
 *     TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
 *     TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
 * };
 */
class Solution {
public:
    vector<int> ans;
    unordered_set<int> levels;
    vector<int> rightSideView(TreeNode* root) {
        dfs(root, 0);
        return ans;
    }
    void dfs(TreeNode* node, int level) {
        if (!node) return;
        if (levels.find(level) == levels.end()) {
            ans.push_back(node->val);
            levels.insert(level);
        }
        dfs(node->right, level + 1);
        dfs(node->left, level + 1);
    }
};
```
Python 代码:
```Python
# Definition for a binary tree node.
# class TreeNode:
#     def __init__(self, val=0, left=None, right=None):
#         self.val = val
#         self.left = left
#         self.right = right
class Solution:
    def rightSideView(self, root: Optional[TreeNode]) -> List[int]:
        def dfs(node, level):
            if not node:
                return
            if level not in levels:
                ans.append(node.val)
                levels.add(level)
            dfs(node.right, level + 1)
            dfs(node.left, level + 1)
        ans = []
        levels = set()
        dfs(root, 0)
        return ans
```
TypeScript 代码:
```TypeScript
/**
 * Definition for a binary tree node.
 * class TreeNode {
 *     val: number
 *     left: TreeNode | null
 *     right: TreeNode | null
 *     constructor(val?: number, left?: TreeNode | null, right?: TreeNode | null) {
 *         this.val = (val===undefined ? 0 : val)
 *         this.left = (left===undefined ? null : left)
 *         this.right = (right===undefined ? null : right)
 *     }
 * }
 */
function rightSideView(root: TreeNode | null): number[] {
    const ans = [];
    const levels = new Set();
    function dfs(node: TreeNode | null, level: number) {
        if (!node) return;
        if (!levels.has(level)) {
            ans.push(node.val);
            levels.add(level);
        }
        dfs(node.right, level + 1);
        dfs(node.left, level + 1);
    }
    dfs(root, 0);
    return ans;
};
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


LeetCode/1971-1980/1976. 到达目的地的方案数（中等）.md

1976. 到达目的地的方案数

Question:
你在一个城市里,城市由 $n$ 个路口组成,路口编号为 $0$ 到 $n - 1$ ,某些路口之间有 双向 道路。输入保证你可以从任意路口出发到达其他任意路口,且任意两个路口之间最多有一条路。

给你一个整数 `n` 和二维整数数组 `roads`,其中 $roads[i] = [u_i, v_i, time_i]$ 表示在路口 $u_i$ 和 $v_i$ 之间有一条需要花费 $time_i$ 时间才能通过的道路。你想知道花费 最少时间 从路口 $0$ 出发到达路口 $n - 1$ 的方案数。

请返回花费 最少时间 到达目的地的 路径数目 。由于答案可能很大,将结果对 $10^9 + 7$ 取余 后返回。

示例 1:

```
输入:n = 7, roads = [[0,6,7],[0,1,2],[1,2,3],[1,3,3],[6,3,3],[3,5,1],[6,5,1],[2,5,1],[0,4,5],[4,6,2]]

输出:4

解释:从路口 0 出发到路口 6 花费的最少时间是 7 分钟。
四条花费 7 分钟的路径分别为:
- 0 ➝ 6
- 0 ➝ 4 ➝ 6
- 0 ➝ 1 ➝ 2 ➝ 5 ➝ 6
- 0 ➝ 1 ➝ 3 ➝ 5 ➝ 6
```
示例 2:
```
输入:n = 2, roads = [[1,0,10]]

输出:1

解释:只有一条从路口 0 到路口 1 的路,花费 10 分钟。
```

提示:
* $1 <= n <= 200$
* $n - 1 <= roads.length <= \frac{n \times (n - 1)}{2}$
* $roads[i].length == 3$
* $0 <= u_i, v_i <= n - 1$
* $1 <= time_i <= 10^9$
* $u_i != v_i$
* 任意两个路口之间至多有一条路。
* 从任意路口出发,你能够到达其他任意路口。

Solution:
### Dijkstra + 拓扑排序 + DP

为了方便,我们记 `roads` 为 `rs`,令点数为 `n`,边数为 `m`。

边数与点数不在一个数量级上($m \approx n^2$),属于「稠密图」,我们可以使用「邻接矩阵」进行存图,同时使用朴素 `Dijkstra` 求解从 $0$ 号点到其他点的最短路,记为 `dist` 数组,$dist[i] = x$ 代表以 $0$ 号点为起点到到 $i$ 点的最短路径为 $x$。

当我们预处理出 $0$ 点到其他点的最短距离后,考虑如何统计从 $0$ 点到 $n - 1$ 点,且路径和为 $dist[n - 1]$ 的方案数。

一个容易想到的性质:**在任意的合法方案中,途径的该路径中的每个点时,都是以最短路径的方式到达的。**

使用「反证法」证明该性质的正确性:假设其中一条合法路径为 `a -> ... -> k -> ... -> z`(其中 `a` 为 $0$ 号点,`z` 为 $n - 1$ 号点),其为合法路径,意味着从 `a` 到 `z` 的路径和为 $dist[n - 1]$。若我们在经过某个途经点,假设为 `k` 时,所途径的路径总和 $x$ 不是 $dist[k]$ 的话,意味着我们可以调整从 `a` 到 `k` 的路径,使其变为 $dist[k]$,而后续路径不变(从 `k` 到 `z` 的路径不变)来得到一条路径和比 $dist[n - 1]$ 要小的从 `a` 到 `z` 的路径,这与 $dist[n - 1]$ 为从 `a` 到 `z` 的最短路冲突。

至此,我们证明了「**在任意的合法方案中,途径的该路径中的每个点时,都是以最短路径的方式到达的**」这一性质。

利用该性质,我们可以对图进行「重建」,对于原图中点 $a$ 与点 $b$ 权重为 $c$ 的无向边,我们根据 $dist[a]$、$dist[b]$ 和 $c$ 三者关系建立有向边,并统计入度:

* 若有 $dist[b] = dist[a] + c$,在新图上增加从 $a$ 到 $b$ 的权重为 $c$ 的有向边,同时 $b$ 入度加一;
* 若有 $dist[a] = dist[b] + c$,在新图上增加从 $b$ 到 $a$ 的权重为 $c$ 的有向边,同时 $a$ 入度加一。

构建新图的目的是能够在跑「拓扑排序」过程中进行 `DP`,统计方案数。

**定义 $f[i]$ 为从 $0$ 到达 $i$ 点的方案数,$f[n - 1]$ 为答案,同时我们有显而易见的初始化条件 $f[0] = 1$。**

不失一般性考虑 $f[i]$ 如何计算,若我们存在一条从 $i$ 到 $j$ 的出边,并且 $f[i]$ 已确定更新完成(通过判断 $i$ 的入度是为 $0$ 得知,入度为 $0$ 意味着已经没有其他状态可以更新 $f[i]$),我们可以用 $f[i]$ 来更新 $f[j]$,即有 $f[j] = f[j] + f[i]$,含义将到达 $i$ 的路径数累加到到达 $j$ 的路径数中,同时更新 $j$ 的入度。

代码:
```Java
class Solution {
    int N = 210, MOD = (int)1e9+7;
    long INF = (long)1e12;
    int[][] g = new int[N][N];
    int[] in = new int[N];
    long[] dist = new long[N];
    boolean[] vis = new boolean[N];
    int n;
    public int countPaths(int _n, int[][] rs) {
        n = _n;
        for (int[] info : rs) {
            int a = info[0], b = info[1], c = info[2];
            g[a][b] = g[b][a] = c;
        }
        // 朴素 Dijkstra 求解从 0 点到其他点的最短路
        dijkstra();
        // 利用最短路重新建图,并统计入度
        for (int[] info : rs) {
            int a = info[0], b = info[1], c = info[2];
            g[a][b] = g[b][a] = 0;
            if (dist[a] + c == dist[b]) {
                g[a][b] = c; in[b]++;
            } else if (dist[b] + c == dist[a]) {
                g[b][a] = c; in[a]++;
            }
        }
        // 跑拓扑排序统计方案数
        Deque<Integer> d = new ArrayDeque<>();
        for (int i = 0; i < n; i++) {
            if (in[i] == 0) d.addLast(i);
        }
        int[] f = new int[n];
        f[0] = 1;
        while (!d.isEmpty()) {
            int x = d.pollFirst();
            for (int i = 0; i < n; i++) {
                if (g[x][i] == 0) continue;
                f[i] += f[x];
                f[i] %= MOD;
                if (--in[i] == 0) d.addLast(i);
            }
        }
        return f[n - 1];
    }
    void dijkstra() {
        Arrays.fill(dist, INF);
        dist[0] = 0;
        for (int i = 0; i < n; i++) {
            int t = -1;
            for (int j = 0; j < n; j++) {
                if (!vis[j] && (t == -1 || dist[j] < dist[t])) t = j;
            }
            vis[t] = true;
            for (int j = 0; j < n; j++) {
                if (g[t][j] == 0) continue;
                dist[j] = Math.min(dist[j], dist[t] + g[t][j]);
            }
        }
    }
}
```
* 时间复杂度:首次建图复杂度为 $O(m)$;Dijkstra 求最短路复杂度为 $O(n^2)$;再次建图复杂度为 $O(m)$,跑拓扑排序统计方案数复杂度为 $O(n + m)$。整体复杂度为 $O(n^2 + m)$
* 空间复杂度:$O(n^2)$

### 最后

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

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

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

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


LeetCode/1981-1990/1984. 学生分数的最小差值（简单）.md

1984. 学生分数的最小差值

Question:
给你一个 下标从 $0$ 开始 的整数数组 $nums$ ,其中 $nums[i]$ 表示第 $i$ 名学生的分数。另给你一个整数 $k$ 。

从数组中选出任意 $k$ 名学生的分数,使这 $k$ 个分数间 **最高分** 和 **最低分** 的 差值 达到 最小化 。

返回可能的 **最小差值** 。

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

输出:0

解释:选出 1 名学生的分数,仅有 1 种方法:
- [90] 最高分和最低分之间的差值是 90 - 90 = 0
可能的最小差值是 0
```
示例 2:
```
输入:nums = [9,4,1,7], k = 2

输出:2

解释:选出 2 名学生的分数,有 6 种方法:
- [9,4,1,7] 最高分和最低分之间的差值是 9 - 4 = 5
- [9,4,1,7] 最高分和最低分之间的差值是 9 - 1 = 8
- [9,4,1,7] 最高分和最低分之间的差值是 9 - 7 = 2
- [9,4,1,7] 最高分和最低分之间的差值是 4 - 1 = 3
- [9,4,1,7] 最高分和最低分之间的差值是 7 - 4 = 3
- [9,4,1,7] 最高分和最低分之间的差值是 7 - 1 = 6
可能的最小差值是 2
```

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

Solution:
### 排序 + 滑动窗口

从 $n$ 个元素里找 $k$ 个,使得 $k$ 个元素最大差值最小。

**最大值最小化问题容易想到「二分」,利用答案本身具有「二段性」,来将原本的求解问题转化为判断定问题。**

回到本题,容易证明,这 $k$ 个元素必然是有序数组中(排序后)的连续段。反证法,若最佳 $k$ 个选择不是连续段,能够调整为连续段,结果不会变差。

因此我们可以先对 $nums$ 进行排序,然后扫描所有大小为 $k$ 的窗口,直接找到答案,而无须使用「二分」。

代码(二分答案代码见 $P2$):
```Java
class Solution {
    public int minimumDifference(int[] nums, int k) {
        Arrays.sort(nums);
        int n = nums.length, ans = nums[k - 1] - nums[0];
        for (int i = k; i < n; i++) {
            ans = Math.min(ans, nums[i] - nums[i - k + 1]);
        }
        return ans;
    }
}
```
-
```Java
class Solution {
    int[] nums; int k;
    public int minimumDifference(int[] _nums, int _k) {
        nums = _nums; k = _k;
        Arrays.sort(nums);
        int l = 0, r = 100010;
        while (l < r) {
            int mid = l + r >> 1;
            if (check(mid)) r = mid;
            else l = mid + 1;
        }
        return r;
    }
    boolean check(int x) {
        int n = nums.length, ans = nums[k - 1] - nums[0];
        for (int i = k; i < n && ans > x; i++) {
            ans = Math.min(ans, nums[i] - nums[i - k + 1]);
        }
        return ans <= x;
    }
}
```
* 时间复杂度:排序复杂度为 $O(n\log{n})$;遍历得到答案复杂度为 $O(n)$。整体复杂度为 $O(n\log{n})$
* 空间复杂度:$O(\log{n})$

### 最后

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

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

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

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


1994. 好子集的数目

Question:
给你一个整数数组 `nums`。如果 `nums` 的一个子集中,所有元素的乘积可以表示为一个或多个 互不相同的质数 的乘积,那么我们称它为 好子集 。

* 比方说,如果 `nums = [1, 2, 3, 4]` :
	* `[2, 3]` ,`[1, 2, 3]` 和 `[1, 3]` 是 好 子集,乘积分别为 `6 = 2*3` ,`6 = 2*3` 和 `3 = 3` 。
	* `[1, 4]` 和 `[4]` 不是 好 子集,因为乘积分别为 `4 = 2*2` 和 `4 = 2*2` 。
	请你返回 `nums` 中不同的 好 子集的数目对 $10^9 + 7$ 取余 的结果。

`nums` 中的 子集 是通过删除 `nums` 中一些(可能一个都不删除,也可能全部都删除)元素后剩余元素组成的数组。如果两个子集删除的下标不同,那么它们被视为不同的子集。

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

输出:6

解释:好子集为:
- [1,2]:乘积为 2 ,可以表示为质数 2 的乘积。
- [1,2,3]:乘积为 6 ,可以表示为互不相同的质数 2 和 3 的乘积。
- [1,3]:乘积为 3 ,可以表示为质数 3 的乘积。
- [2]:乘积为 2 ,可以表示为质数 2 的乘积。
- [2,3]:乘积为 6 ,可以表示为互不相同的质数 2 和 3 的乘积。
- [3]:乘积为 3 ,可以表示为质数 3 的乘积。
```
示例 2:
```
输入:nums = [4,2,3,15]

输出:5

解释:好子集为:
- [2]:乘积为 2 ,可以表示为质数 2 的乘积。
- [2,3]:乘积为 6 ,可以表示为互不相同质数 2 和 3 的乘积。
- [2,15]:乘积为 30 ,可以表示为互不相同质数 2,3 和 5 的乘积。
- [3]:乘积为 3 ,可以表示为质数 3 的乘积。
- [15]:乘积为 15 ,可以表示为互不相同质数 3 和 5 的乘积。
```

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

Solution:
### 状压 DP

该问题属于 NP 完全问题,注定不存在多项式解决方案,只能通过「爆搜 + 剪枝」或「状压 DP」来求解。

对子集的乘积进行质数分解,等价于对子集每一位数进行质数分解。

一个显然的突破口是 $1 <= nums[i] <= 30$,再加上题目对于「好子集」的定义,我们可以进一步缩减可选数的数量,不超过 $30$ 的质数个数包括 $[2, 3, 5, 7, 11, 13, 17, 19, 23, 29]$(共 $10$ 个),将其记作 $p$,在一个好子集中,每个 $p[i]$ 最多出现一次。

同时,题目规定数值相同,下标不同均视为不同方案,因此我们可以先使用数组 $cnts$ 统计在 $nums$ 中每个数的出现次数,$cnts[val] = x$ 含义为数值 $val$ 在 $nums$ 中的出现次数为 $x$ 次。

使用的数有限(共 $10$ 个),并且使用到的数最多出现一次,容易想到使用「状压 DP」来求解:我们使用一个二进制数来表示好子集乘积最终能拆解成哪些质数,如果拆解结果中包含 $p[i]$,对应的二进制表示中的第 $i$ 位则为 $1$,否则为 $0$。

**定义 $f[state]$ 为当前子集乘积拆解结果的用到的质数为 $state$ 时的方案数,$state$ 为一个长度 $10$ 的二进制数,若 $state$ 中的第 $k$ 位二进制表示为 $1$,代表数值 $p[k]$ 在拆解结果中出现过;若第 $k$ 位二进制表示为 $0$ 代表 $p[k]$ 在拆解结果中没出现过。**

我们有起始化条件:空集,即 $f[0] = 1$。

不失一般性考虑 $f[s]$ 该如何计算:从前往后考虑每个数值(范围 $[2, 30]$ 的数,$1$ 添加与否不对好子集产生影响,最后讨论)是否可以加入到子集中,一个数值 $t$ 能够添加到子集中的充要条件为题目给定的条件:**该数不会被相同的质数相乘表示**。

如果一个数值 $t$ 能够添加到好子集中,我们通过「试除法」将其分解为 $p$ 中的多个质数,并使用二进制数 $cur$ 来表示用到了 $p$ 中的哪些质数,然后需要判断 $t$ 能够添加到那些子集中,其实就是枚举与 $cur$ 无交集的状态 $prev$,最终的 $f[s]$ 为「所有合法的 $prev$ 的状态数 $f[prev]$」与「数值 $t$ 的出现次数 $cnts[t]$ 」的乘积之和。

需要注意的是,由于我们是从范围 $[2, 30]$ 范围内从前往后考虑每个 $t$,因此**在枚举 $prev$ 时需要进行倒序遍历,确保计算 $f[s]$ 所依赖的 $f[prev]$ 为不考虑当前数值 $t$ 时的方案数**。

由「质数 $p$ 组成的好子集方案数」为 $ans = \sum_{state' = 1}^{{1024}} f[state']$,其中 $state'$ 对应一个合法的好子集方案。

在此基础上,再考虑数值 $1$ 对答案的影响:**在每个合法的 $state'$ 前提下,增加若干个 $1$ 并不影响子集乘积(即好子集增加 $1$ 后仍为好子集),因此每个合法子集 $state'$ 可以对应出 $2^{cnts[1]}$ 个具体方案(代表每个 $1$ 即可以选,也可以不选)**。

代码:
```Java
class Solution {
    int MOD = (int)1e9+7;
    int[] p = new int[]{2, 3, 5, 7, 11, 13, 17, 19, 23, 29};
    int[] cnts = new int[35];
    public int numberOfGoodSubsets(int[] nums) {
        int n = nums.length;
        for (int i : nums) cnts[i]++;
        int mask = 1 << 10;
        long[] f = new long[mask];
        f[0] = 1;
        for (int i = 2; i <= 30; i++) {
            if (cnts[i] == 0) continue;
            // 对 i 进行试除
            int cur = 0, x = i;
            boolean ok = true;
            for (int j = 0; j < 10 && ok; j++) {
                int t = p[j], c = 0;
                while (x % t == 0) {
                    cur |= (1 << j);
                    x /= t; c++;
                }
                // 如果 i 能够被同一质数试除多次,说明 i 不能加到子集,跳过
                if (c > 1) ok = false;
            }
            if (!ok) continue;
            // 枚举前一状态 prev
            //(确保考虑一个新数值 i 时,依赖的子集 prev 存储的为尚未考虑 i 的方案数)
            for (int prev = mask - 1; prev >= 0; prev--) {
                // 只有当前选择数与前一状态不冲突,则能够进行转移,将方案数进行累加
                if ((prev & cur) != 0) continue;
                f[prev | cur] = (f[prev | cur] + f[prev] * cnts[i]) % MOD;
            }
        }
        long ans = 0;
        // 统计所有非空集的方案数
        for (int i = 1; i < mask; i++) ans = (ans + f[i]) % MOD;
        // 在此基础上,考虑每个 1 选择与否对答案的影响
        for (int i = 0; i < cnts[1]; i++) ans = ans * 2 % MOD;
        return (int) ans;
    }
}
```
* 时间复杂度:预处理每个数值的出现次数复杂度为 $O(n)$;令数值范围 $C = 30$,状态数为 $M = 1024$,DP 部分复杂度为 $O(C \times M)$。整体复杂度为 $O(n + C \times M)$
* 空间复杂度:$O(C + M)$

---

### 关于「倒序遍历」状态的答疑

上述的分析推理基于我们进行了「一维空间优化」。

原始的状态定义应该是 **$f[i][state]$ 为考虑范围在 $[1,30]$ 的前 $i$ 个数,并且好子集拆解结果的用到的质数为 $state$ 时的方案数。**

我们有显然的初始化条件:$f[1][0] = 1$,代表当只有数值 $1$ 的话,只有空集为合法方案。

不失一般性的考虑 $f[i][s]$ 该如何计算,根据使用数值 $i$ 来进行分情况讨论:

* 不考虑数值 $i$(或是数值 $i$ 在 $nums$ 中没有出现,无法考虑),则有 $f[i][s] = f[i - 1][s]$;
* 考虑数值 $i$,分析方法同「一维空间优化」,对数值 $i$ 应用「试除法」拆解成对应的质数状态 $cur$,然后找到可以转移的前一状态 $prev$ 来尝试转移,则有 $f[i][cur | prev] = f[i][cur | prev] + f[i - 1][prev]$。

基于对「原始状态定义」的分析,我们发现 $f[i][cur | prev]$ 的更新(累加)依赖于 $f[i][prev]$,同时 $prev$ 是 $cur | prev$ 的子集,因此在进行「一维空间优化」,我们需要确保所依赖的状态没有被覆盖更新,即采用「倒序遍历」。

代码:
```Java
class Solution {
    int MOD = (int)1e9+7;
    int[] p = new int[]{2, 3, 5, 7, 11, 13, 17, 19, 23, 29};
    int[] cnts = new int[35];
    public int numberOfGoodSubsets(int[] nums) {
        for (int i : nums) cnts[i]++;
        int mask = 1 << 10;
        long[][] f = new long[35][mask];
        f[1][0] = 1;
        for (int i = 2; i <= 30; i++) {
            for (int s = 0; s < mask; s++) f[i][s] = f[i - 1][s];
            if (cnts[i] == 0) continue;
            int cur = 0, x = i;
            boolean ok = true;
            for (int j = 0; j < 10 && ok; j++) {
                int t = p[j], c = 0;
                while (x % t == 0) {
                    cur |= (1 << j);
                    x /= t; c++;
                }
                if (c > 1) ok = false;
            }
            if (!ok) continue;
            for (int prev = 0; prev < mask; prev++) {
                if ((prev & cur) != 0) continue;
                f[i][prev | cur] = (f[i][prev | cur] + f[i - 1][prev] * cnts[i]) % MOD;
            }
        }
        long ans = 0;
        for (int i = 1; i < mask; i++) ans = (ans + f[30][i]) % MOD;
        for (int i = 0; i < cnts[1]; i++) ans = ans * 2 % MOD;
        return (int) ans;
    }
}
```
* 时间复杂度:预处理每个数值的出现次数复杂度为 $O(n)$;令数值范围 $C = 30$,状态数为 $M = 1024$,DP 部分复杂度为 $O(C \times M)$。整体复杂度为 $O(n + C \times M)$
* 空间复杂度:$O(C \times M)$

### 最后

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

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

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

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


1995. 统计特殊四元组

Question:
给你一个 下标从 `0` 开始 的整数数组 `nums`,返回满足下述条件的**不同**四元组 `(a, b, c, d)` 的 数目 :

`nums[a] + nums[b] + nums[c] == nums[d]`,且 `a < b < c < d`。

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

输出:1

解释:满足要求的唯一一个四元组是 (0, 1, 2, 3) 因为 1 + 2 + 3 == 6 。
```
示例 2:
```
输入:nums = [3,3,6,4,5]

输出:0

解释:[3,3,6,4,5] 中不存在满足要求的四元组。
```
示例 3:
```
输入:nums = [1,1,1,3,5]

输出:4

解释:满足要求的 4 个四元组如下:
- (0, 1, 2, 3): 1 + 1 + 1 == 3
- (0, 1, 3, 4): 1 + 1 + 3 == 5
- (0, 2, 3, 4): 1 + 1 + 3 == 5
- (1, 2, 3, 4): 1 + 1 + 3 == 5
```

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

Solution:
### 朴素解法

利用数据范围只有 $50$,可直接根据题意进行模拟。

代码:
```Java
class Solution {
    public int countQuadruplets(int[] nums) {
        int n = nums.length, ans = 0;
        for (int a = 0; a < n; a++) {
            for (int b = a + 1; b < n; b++) {
                for (int c = b + 1; c < n; c++) {
                    for (int d = c + 1; d < n; d++) {
                        if (nums[a] + nums[b] + nums[c] == nums[d]) ans++;
                    }
                }
            }
        }
        return ans;
    }
}
```
* 时间复杂度:$O(n^4)$
* 空间复杂度:$O(1)$

---

### 哈希表

利用等式关系 $nums[a] + nums[b] + nums[c] = nums[d]$,可以调整枚举 $c$ 的顺序为「逆序」,每次 $c$ 往左移动一个单位,$d$ 的可取下标范围增加一个(即 $c + 1$ 位置),使用数组代替哈希表对 $nums[d]$ 的个数进行统计,可使复杂度下降到 $O(n^3)$。

代码:
```Java
class Solution {
    public int countQuadruplets(int[] nums) {
        int n = nums.length, ans = 0;
        int[] cnt = new int[10010];
        for (int c = n - 2; c >= 2; c--) {
            cnt[nums[c + 1]]++;
            for (int a = 0; a < n; a++) {
                for (int b = a + 1; b < c; b++) {
                    ans += cnt[nums[a] + nums[b] + nums[c]];
                }
            }
        }
        return ans;
    }
}
```
* 时间复杂度:$O(n^3)$
* 空间复杂度:$O(C)$

---

### 哈希表

更进一步,根据等式关系进行移项可得:$nums[a] + nums[b] = nums[d] - nums[c]$,其中各下标满足 $a < b < c < d$。

我们可在「逆序」枚举 $b$ 时,将新产生的 $c$(即 $b + 1$ 位置)所能产生的新 $nums[d] - nums[c]$ 的值存入哈希表(即 从 $[b + 2, n)$ 范围内枚举 $d$),最后通过枚举 $a$ 来统计答案。

> 一些细节:由于 $nums[d] - nums[c]$ 可能为负,在使用数组代替哈希表时,可利用 $1 <= nums[i] <= 100$ 做一个值偏移。

代码:
```Java
class Solution {
    public int countQuadruplets(int[] nums) {
        int n = nums.length, ans = 0;
        int[] cnt = new int[10010];
        for (int b = n - 3; b >= 1; b--) {
            for (int d = b + 2; d < n; d++) cnt[nums[d] - nums[b + 1] + 200]++;
            for (int a = 0; a < b; a++) ans += cnt[nums[a] + nums[b] + 200];
        }
        return ans;
    }
}
```
* 时间复杂度:$O(n^2)$
* 空间复杂度:$O(C)$

---

### 多维背包

利用等式关系 $nums[a] + nums[b] + nums[c] = nums[d]$,具有明确的「数值」和「个数」关系,可将问题抽象为组合优化问题求方案数。

限制组合个数的维度有两个,均为「恰好」限制,转换为「二维费用背包问题求方案数」问题。

**定义 $f[i][j][k]$ 为考虑前 $i$ 个物品(下标从 $1$ 开始),凑成数值恰好 $j$,使用个数恰好为 $k$ 的方案数。**

最终答案为 $\sum_{i = 3}^{n - 1}(f[i][nums[i]][3])$,起始状态 $f[0][0][0] = 1$ 代表不考虑任何物品时,所用个数为 $0$,凑成数值为 $0$ 的方案数为 $1$。

不失一般性考虑 $f[i][j][k]$ 该如何转移,根据 $nums[i - 1]$ 是否参与组合进行分情况讨论:

* $nums[i - 1]$ 不参与组成,此时有:$f[i - 1][j][k]$;
* $nums[i - 1]$ 参与组成,此时有:$f[i - 1][j - t][k - 1]$;

最终 $f[i][j][k]$ 为上述两种情况之和,最终统计 $\sum_{i = 3}^{n - 1}(f[i][nums[i]][3])$ 即是答案。

> 利用 $f[i][j][k]$ 仅依赖于 $f[i - 1][j][k]$ 和 `j` `k` 维度值更小的 $f[i - 1][X][X]$,可进行维度优化,并在转移过程中统计答案。

代码(维度优化见 $P2$):
```Java
class Solution {
    public int countQuadruplets(int[] nums) {
        int n = nums.length;
        int[][][] f = new int[n + 1][110][4];
        f[0][0][0] = 1;
        for (int i = 1; i <= n; i++) {
            int t = nums[i - 1];
            for (int j = 0; j < 110; j++) {
                for (int k = 0; k < 4; k++) {
                    f[i][j][k] += f[i - 1][j][k];
                    if (j - t >= 0 && k - 1 >= 0) f[i][j][k] += f[i - 1][j - t][k - 1];
                }
            }
        }
        int ans = 0;
        for (int i = 3; i < n; i++) ans += f[i][nums[i]][3];
        return ans;
    }
}
```

```Java
class Solution {
    public int countQuadruplets(int[] nums) {
        int n = nums.length, ans = 0;
        int[][] f = new int[110][4];
        f[0][0] = 1;
        for (int i = 1; i <= n; i++) {
            int t = nums[i - 1];
            ans += f[t][3];
            for (int j = 109; j >= 0; j--) {
                for (int k = 3; k >= 0; k--) {
                    if (j - t >= 0 && k - 1 >= 0) f[j][k] += f[j - t][k - 1];
                }
            }
        }
        return ans;
    }
}
```
* 时间复杂度:$O(n * 110 * 4)$
* 空间复杂度:$O(n * 110 * 4)$

### 最后

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

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

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

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


LeetCode/1991-2000/1996. 游戏中弱角色的数量（中等）.md

1996. 游戏中弱角色的数量

Question:
你正在参加一个多角色游戏,每个角色都有两个主要属性:攻击 和 防御 。

给你一个二维整数数组 `properties`,其中 $properties[i] = [attack_i, defense_i]$ 表示游戏中第 $i$ 个角色的属性。

如果存在一个其他角色的攻击和防御等级 都严格高于 该角色的攻击和防御等级,则认为该角色为 弱角色 。

更正式地,如果认为角色 `i` 弱于 存在的另一个角色 `j` ,那么 $attack_j > attack_i$ 且 $defense_j > defense_i$ 。

返回 **弱角色** 的数量。

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

输出:0

解释:不存在攻击和防御都严格高于其他角色的角色。
```
示例 2:
```
输入:properties = [[2,2],[3,3]]

输出:1

解释:第一个角色是弱角色,因为第二个角色的攻击和防御严格大于该角色。
```
示例 3:
```
输入:properties = [[1,5],[10,4],[4,3]]

输出:1

解释:第三个角色是弱角色,因为第二个角色的攻击和防御严格大于该角色。
```

提示:
* $2 <= properties.length <= 10^5$
* $properties[i].length == 2$
* $1 <= attacki, defensei <= 10^5$

Solution:
### 排序 + 贪心 + 计数

为了方便,我们使用 `ps` 来代指 `properties`。

决定角色「强弱」的维度有两个,同时由于我们只关心某个角色是否为弱角色,而不关心有多少比其(严格)强的角色有多少个。

因此我们先对 `ps` 进行排序:**优先根据第一维度(攻击力)排序,在第一维度(攻击力)相同时,根据第二维度(防御力)进行排序**。

由于我们统计的是「严格弱角色」,因此在从前往后处理 `ps` 过程中,要将第一维度(攻击力)相同的作为一组进行处理,假设 $[i, j)$ 为第一维度(攻击力)相同的连续段,假设当前处理到连续段 $[i, j)$ 中的第 $k$ 个角色 $ps[k]$,那么 $ps[k]$ 为弱角色的充要条件为:

1. **存在比 $ps[k][0]$ 攻击力高的角色,由于我们先按照了攻击力进行排序,同时又是按照攻击相同为一组进行处理,因此这等价于当前连续段 $[i, j)$ 不是第一组,即 $i \neq 0$;**
2. **在满足 $1$ 的前提下,存在防御力比 $ps[k][1]$ 高的角色,由于要求弱角色为「严格」,因此我们只能在之前的组(攻击力比 $ps[k][0]$ 大的相同连续段)去找。这意味着我们在遍历过程中需要贪心地维护一个防御力的最大值 $\max$,并在处理完相同连续段后尝试对其进行更新。**

代码:
```Java
class Solution {
    public int numberOfWeakCharacters(int[][] ps) {
        int n = ps.length, ans = 0;
        Arrays.sort(ps, (a, b)->{
            if (a[0] != b[0]) return b[0] - a[0];
            return b[1] - a[1];
        });
        for (int i = 0, max = ps[0][1]; i < n; ) {
            int j = i, cur = max;
            while (j < n && ps[j][0] == ps[i][0]) {
                if (i != 0 && ps[j][1] < max) ans++;
                cur = Math.max(cur, ps[j][1]);
                j++;
            }
            i = j; max = cur;
        }
        return ans;
    }
}
```
* 时间复杂度:排序的复杂度为 $O(n\log{n})$;统计答案复杂度为 $O(n)$。整体复杂度为 $O(n\log{n})$
* 空间复杂度:$O(\log{n})$

### 最后

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

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

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

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


2000. 反转单词前缀

Question:
给你一个下标从 $0$ 开始的字符串 `word` 和一个字符 `ch` 。找出 `ch` 第一次出现的下标 `i` ,反转 `word` 中从下标 $0$ 开始、直到下标 `i` 结束(含下标 `i` )的那段字符。如果 `word` 中不存在字符 `ch` ,则无需进行任何操作。

* 例如,如果 `word = "abcdefd"` 且 `ch = "d"` ,那么你应该 反转 从下标 $0$ 开始、直到下标 `3` 结束(含下标 `3` )。结果字符串将会是 `"dcbaefd"` 。

返回 结果字符串 。

示例 1:
```
输入:word = "abcdefd", ch = "d"

输出:"dcbaefd"

解释:"d" 第一次出现在下标 3 。
反转从下标 0 到下标 3(含下标 3)的这段字符,结果字符串是 "dcbaefd" 。
```
示例 2:
```
输入:word = "xyxzxe", ch = "z"

输出:"zxyxxe"

解释:"z" 第一次也是唯一一次出现是在下标 3 。
反转从下标 0 到下标 3(含下标 3)的这段字符,结果字符串是 "zxyxxe" 。
```
示例 3:
```
输入:word = "abcd", ch = "z"

输出:"abcd"

解释:"z" 不存在于 word 中。
无需执行反转操作,结果字符串是 "abcd" 。
```

提示:
* $1 <= word.length <= 250$
* `word` 由小写英文字母组成
* `ch` 是一个小写英文字母

Solution:
### 模拟

先从前往后遍历,找到第一个 `ch` 的下标 $idx$(初始值为 $-1$),然后对 $[0, idx]$ 应用双指针进行翻转(若没有 `ch` 字符,则 $idx = -1$,则 $[0, idx]$ 为不合法区间,翻转过程被跳过)。

代码:
```Java
class Solution {
    public String reversePrefix(String word, char ch) {
        char[] cs = word.toCharArray();
        int n = cs.length, idx = -1;
        for (int i = 0; i < n && idx == -1; i++) {
            if (cs[i] == ch) idx = i;
        }
        int l = 0, r = idx;
        while (l < r) {
            char c = cs[l];
            cs[l++] = cs[r];
            cs[r--] = c;
        }
        return String.valueOf(cs);
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:`toCharArray` 会产生新数组,复杂度为 $O(n)$

### 最后

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

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

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

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


LeetCode/2001-2010/2003. 每棵子树内缺失的最小基因值（困难）.md

2003. 每棵子树内缺失的最小基因值

Question:
有一棵根节点为 `0` 的 家族树 ,总共包含 `n` 个节点,节点编号为 `0` 到 `n - 1`。

给你一个下标从 `0` 开始的整数数组 `parents`,其中 $parents[i]$ 是节点 `i` 的父节点。由于节点 `0` 是根 ,所以 $parents[0] = -1$。

总共有 $10^5$ 个基因值,每个基因值都用 闭区间 $[1, 10^5]$ 中的一个整数表示。

给你一个下标从 `0` 开始的整数数组 `nums`,其中 $nums[i]$ 是节点 `i` 的基因值,且基因值 互不相同 。

请你返回一个数组 `ans`,长度为 `n`,其中 $ans[i]$ 是以节点 `i` 为根的子树内缺失的最小基因值。

节点 `x` 为根的子树包含节点 `x` 和它所有的后代节点。

示例 1:

```
输入:parents = [-1,0,0,2], nums = [1,2,3,4]

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

解释:每个子树答案计算结果如下:
- 0:子树包含节点 [0,1,2,3] ,基因值分别为 [1,2,3,4] 。5 是缺失的最小基因值。
- 1:子树只包含节点 1 ,基因值为 2 。1 是缺失的最小基因值。
- 2:子树包含节点 [2,3] ,基因值分别为 [3,4] 。1 是缺失的最小基因值。
- 3:子树只包含节点 3 ,基因值为 4 。1是缺失的最小基因值。
```
示例 2:

```
输入:parents = [-1,0,1,0,3,3], nums = [5,4,6,2,1,3]

输出:[7,1,1,4,2,1]

解释:每个子树答案计算结果如下:
- 0:子树内包含节点 [0,1,2,3,4,5] ,基因值分别为 [5,4,6,2,1,3] 。7 是缺失的最小基因值。
- 1:子树内包含节点 [1,2] ,基因值分别为 [4,6] 。 1 是缺失的最小基因值。
- 2:子树内只包含节点 2 ,基因值为 6 。1 是缺失的最小基因值。
- 3:子树内包含节点 [3,4,5] ,基因值分别为 [2,1,3] 。4 是缺失的最小基因值。
- 4:子树内只包含节点 4 ,基因值为 1 。2 是缺失的最小基因值。
- 5:子树内只包含节点 5 ,基因值为 3 。1 是缺失的最小基因值。
```
示例 3:
```
输入:parents = [-1,2,3,0,2,4,1], nums = [2,3,4,5,6,7,8]

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

解释:所有子树都缺失基因值 1 。
```

提示:
* $n = parents.length = nums.length$
* $2 <= n <= 10^5$
* 对于 `i != 0`,满足 $0 <= parents[i] <= n - 1$
* $parents[0] = -1$
* `parents` 表示一棵合法的树。
* $1 <= nums[i] <= 10^5$
* `nums[i]` 互不相同。

Solution:
### DFS

#### 破题

先用几句话破题。

共由 $n$ 个节点组成一棵树(节点编号从 $0$ 到 $n - 1$),`parents` 描述了该树的形态,同时每个节点有一个基因值 $nums[i]$。

题目要我们求:**以每个节点为根的子树中,权重集合在 $[1, n + 1]$ 范围内缺失的最小数**。

> 需要重点注意:是权重集合在 $[1, n + 1]$ 范围内缺失的最小数,而不是在 `nums` 中缺失的最小数。

举个 🌰,假设由 $4$ 个节点组成树,基因值 `nums = [2,3,4,5]`,那么对应的 `ans = [1,1,1,1]`。

再次强调:我们求的是每个节点为根的子树中,权重集合在 $[1, n + 1]$ 范围内的最小缺失值,而非在 `nums` 中的缺失值。

#### 结论一:当`nums` 中没有 $1$,所有节点答案为 $1$

由于我们是求 $[1, n + 1]$ 范围内的最小缺失值,当 `nums` 中不存在 $1$ 时,所有节点缺失的最小值为 $1$。

#### 结论二:`nums` 有 $1$,所有该节点的“非祖先”节点,答案为 $1$

基因值互不相同,同时统计的是,以每个节点为“根”时,子树的权值情况,因此节点 $1$ 只会对其“祖先”产生影响。

#### 结论三:从「$1$ 节点」到「根节点」的路径中,答案必然满足“非递减”性质

这个结论不明显,但不难理解。

先假设存在某个节点 `X`,其最小缺失值为 $k$:

再通过节点 `X` 的最小缺失值,推理出父节点 `Fa` 的情况:

综上,我们只需要考虑「节点 $1$」到「根节点」这一节点答案即可。

并且由于从下往上,答案非递减,我们采取「先算子节点,再算父节点」的方式。

具体的,用变量 `cur` 代指当前节点,使用 `ne` 代指当前节点的子节点,`vis` 数组记录在子树中出现过的基因值,`val` 代表当前的节点的最小缺失值。

一些细节:由于题目只说了 $1 \leq nums[i] \leq 1e5$,没说 $nums[i]$ 与 $n$ 的关系,因此我们开辟 `vis` 数组时需要开到 $100010$,或是干脆使用 `Set` 充当 `vis`。

Java 代码:

```Java
class Solution {
    // 考虑到有不懂「链式前向星」的同学, 这里使用最简单的存图方式 {1: [2, 3]} 代表节点一有两个子节点 2 和 3
    Map<Integer, List<Integer>> g = new HashMap<>();
    public int[] smallestMissingValueSubtree(int[] parents, int[] nums) {
        int n = nums.length, cur = -1;
        int[] ans = new int[n];
        Arrays.fill(ans, 1);
        // 找节点 1, 建图
        for (int i = 0; i < n; i++) {
            if (nums[i] == 1) cur = i;
            List<Integer> list = g.getOrDefault(parents[i], new ArrayList<>());
            list.add(i);
            g.put(parents[i], list);
        }

        // 若 nums 中没 1, 对应结论一
        if (cur == -1) return ans;

        boolean[] vis = new boolean[100010];
        // 从节点 1 开始往根找(从深到浅), idx 代表当前节点, ne 代表 cur 在该链路下的子节点
        int ne = cur, val = 1;
        while (cur != -1) {
            // 每次对当前节点所在子树的进行标记
            dfs(cur, ne, nums, vis);
            // 在 [val, n+1] 范围内找第一个未被标记基因值
            for (int i = val; i <= n + 1; i++) {
                if (vis[i]) continue;
                ans[cur] = val = i;
                break;
            }
            ne = cur; cur = parents[cur]; // 指针上移
        }
        return ans;
    }
    void dfs(int idx, int block, int[] nums, boolean[] vis) {
        vis[nums[idx]] = true;
        List<Integer> list = g.getOrDefault(idx, new ArrayList<>());
        for (int x : list) {
            if (x == block) continue;
            dfs(x, block, nums, vis);
        }
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    // 考虑到有不懂「链式前向星」的同学, 这里使用最简单的存图方式 {1: [2, 3]} 代表节点一有两个子节点 2 和 3
    unordered_map<int, vector<int>> g;
    vector<int> smallestMissingValueSubtree(std::vector<int>& parents, std::vector<int>& nums) {
        int n = nums.size(), cur = -1;
        vector<int> ans(n, 1);
        // 找节点 1, 建图
        for (int i = 0; i < n; i++) {
            if (nums[i] == 1) cur = i;
            g[parents[i]].push_back(i);
        }

        // 若 nums 中没 1, 对应结论一
        if (cur == -1) return ans;

        unordered_set<int> vis;
        // 从节点 1 开始往根找(从深到浅), idx 代表当前节点, ne 代表 cur 在该链路下的子节点
        int ne = cur, val = 1;
        while (cur != -1) {
            // 每次对当前节点所在子树的进行标记
            dfs(cur, ne, nums, vis);
            // 在 [val, n+1] 范围内找第一个未被标记基因值
            for (int i = val; i <= n + 1; i++) {
                if (vis.count(i)) continue;
                ans[cur] = val = i;
                break;
            }
            ne = cur; cur = parents[cur]; // 指针上移
        }
        return ans;
    }
    void dfs(int idx, int block, vector<int>& nums, unordered_set<int>& vis) {
        vis.insert(nums[idx]);
        for (int x : g[idx]) {
            if (x == block) continue;
            dfs(x, block, nums, vis);
        }
    }
};
```
Python 代码:
```Python
class Solution:
    def smallestMissingValueSubtree(self, parents: List[int], nums: List[int]) -> List[int]:
        # 虑到有不懂「链式前向星」的同学, 这里使用最简单的存图方式 {1: [2, 3]} 代表节点 1 有两个子节点 2 和 3
        g = defaultdict(list)

        def dfs(idx, block):
            nonlocal val
            vis.add(nums[idx])
            for x in g[idx]:
                if x == block:
                    continue
                dfs(x, block)

        n, cur = len(nums), -1
        ans = [1] * n
        # 找节点 1, 建图
        for i in range(n):
            if nums[i] == 1:
                cur = i
            g[parents[i]].append(i)

        # 若 nums 中没 1, 对应结论一
        if cur == -1:
            return ans

        vis = set()
        # 从节点 1 开始往根找(从深到浅), idx 代表当前节点, ne 代表 cur 在该链路下的子节点
        ne, val = cur, 1
        while cur != -1:
            # 每次对当前节点所在子树的进行标记
            dfs(cur, ne)
            # 在 [val, n+1] 范围内找第一个未被标记基因值
            for i in range(val, n + 2):
                if i in vis:
                    continue
                ans[cur] = val = i
                break
            ne, cur = cur, parents[cur]  # 指针上移

        return ans
```
TypeScript 代码:
```TypeScript
function smallestMissingValueSubtree(parents: number[], nums: number[]): number[] {
    // 考虑到有不懂「链式前向星」的同学, 这里使用最简单的存图方式 {1: [2, 3]} 代表节点 1 有两个子节点 2 和 3
    const g = {};

    const dfs = function (g: { [key: number]: number[] }, idx: number, block: number, nums: number[], vis: Set<number>): void {
        vis.add(nums[idx]);
        if (Array.isArray(g[idx])) {
            for (let x of g[idx]) {
                if (x == block) continue;
                dfs(g, x, block, nums, vis);
            }
        }
    }

    let n = nums.length, cur = -1;
    const ans = new Array(n).fill(1);

    // 找节点 1, 建图
    for (let i = 0; i < n; i++) {
        if (nums[i] === 1) cur = i;
        if (!g[parents[i]]) g[parents[i]] = [];
        g[parents[i]].push(i);
    }

    // 若 nums 中没 1, 对应结论一
    if (cur == -1) return ans;

    const vis = new Set<number>();
    // 从节点 1 开始往根找(从深到浅), idx 代表当前节点, ne 代表 cur 在该链路下的子节点
    let ne = cur, val = 1;
    while (cur !== -1) {
        // 每次对当前节点所在子树的进行标记
        dfs(g, cur, ne, nums, vis);
        // 在 [val, n+1] 范围内找第一个未被标记基因值
        for (let i = val; i <= n + 1; i++) {
            if (vis.has(i)) continue;
            ans[cur] = val = i;
            break;
        }
        ne = cur; cur = parents[cur];  // 指针上移
    }

    return ans;
}
```
Java 代码(链式向前星,使用 `Set` 充当 `vis`):
```Java
class Solution {
    int N = 100010, M = N, idx = 1;
    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[] smallestMissingValueSubtree(int[] parents, int[] nums) {
        Arrays.fill(he, -1);
        int n = parents.length, cur = -1, val = 1;
        int[] ans = new int[n];
        Arrays.fill(ans, 1);
        for (int i = 0; i < n; i++) {
            if (i >= 1) add(parents[i], i);
            if (nums[i] == 1) cur = i;
        }
        if (cur == -1) return ans;
        Set<Integer> vis = new HashSet();
        while (cur != -1) {
            dfs(cur, vis, nums);
            for (int i = val; i <= n + 1; i++) {
                if (vis.contains(i)) continue;
                ans[cur] = val = i;
                break;
            }
            cur = parents[cur];
        }
        return ans;
    }
    void dfs(int u, Set<Integer> vis, int[] nums) {
        vis.add(nums[u]);
        for (int i = he[u]; i != -1; i = ne[i]) {
            int j = e[i];
            if (vis.contains(nums[j])) continue;
            dfs(j, vis, nums);
        }
    }
}
```
* 时间复杂度:找 $1$ 和建图的复杂度为 $O(n)$;构造从根节点到 $1$节点的链条答案时,会对子树节点进行标记,同时每个节点的答案会从 $[val, n + 1]$ 范围内找缺失值,复杂度为 $O(n)$。 整体复杂度为 $O(n)$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


LeetCode/2001-2010/2006. 差的绝对值为 K 的数对数目（简单）.md

2006. 差的绝对值为 K 的数对数目

Question:
给你一个整数数组 `nums` 和一个整数 `k` ,请你返回数对 $(i, j)$ 的数目,满足 $i < j$ 且`|nums[i] - nums[j]| == k`。

`|x|` 的值定义为:

* 如果 $x >= 0$ ,那么值为 $x$ 。
* 如果 $x < 0$ ,那么值为 $-x$ 。

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

输出:4

解释:差的绝对值为 1 的数对为:
- [1,2,2,1]
- [1,2,2,1]
- [1,2,2,1]
- [1,2,2,1]
```
示例 2:
```
输入:nums = [1,3], k = 3

输出:0

解释:没有任何数对差的绝对值为 3 。
```
示例 3:
```
输入:nums = [3,2,1,5,4], k = 2

输出:3

解释:差的绝对值为 2 的数对为:
- [3,2,1,5,4]
- [3,2,1,5,4]
- [3,2,1,5,4]
```

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

Solution:
### 朴素解法

数据范围很小,直接根据题意模拟即可。

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

---

### 哈希表

对于每个 $t = nums[j]$ 而言,是要找与其相对差值为 $k$ 且下标比其小的数(即 $t - k$ 和 $t + k$),可以采取边遍历边记录某个数出现次数,从而将复杂度优化到 $O(n)$。

再利用数据范围 $1 <= nums[i] <= k$,可以直接使用数组充当哈希表。

代码:
```Java
class Solution {
    public int countKDifference(int[] nums, int k) {
        int[] cnts = new int[110];
        int n = nums.length, ans = 0;
        for (int i = 0; i < n; i++) {
            int t = nums[i];
            if (t - k >= 1) ans += cnts[t - k];
            if (t + k <= 100) ans += cnts[t + k];
            cnts[t]++;
        }
        return ans;
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(C)$

### 最后

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

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

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

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


Question:
给你一个链表的头节点 head 和一个整数 val ,请你删除链表中所有满足 Node.val == val 的节点,并返回 新的头节点 。

示例 1:

```
输入:head = [1,2,6,3,4,5,6], val = 6
输出:[1,2,3,4,5]
```
示例 2:
```
输入:head = [], val = 1
输出:[]
```
示例 3:
```
输入:head = [7,7,7,7], val = 7
输出:[]
```

提示:
* 列表中的节点在范围 [0, $10^4$] 内
* 1 <= Node.val <= 50
* 0 <= k <= 50

Solution:
### 递归

一个直观的做法是:写一个递归函数来将某个值为 `val` 的节点从链表中移除。

由于是单链表,无法通过某个节点直接找到「前一个节点」,因此为了方便,我们可以为递归函数多设置一个入参,代表「前一个节点」。

代码:
```Java
class Solution {
    public ListNode removeElements(ListNode head, int val) {
        ListNode dummy = new ListNode(-1);
        dummy.next = head;
        dfs(dummy, dummy.next, val);
        return dummy.next;
    }
    void dfs(ListNode prev, ListNode root, int val) {
        if (root == null) return ;
        if (root.val == val) {
            prev.next = root.next;
        } else {
            prev = root;
        }
        dfs(prev, prev.next, val);
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:忽略递归带来的额外空间开销。复杂度为 $O(1)$

---

### 迭代

同理,我们可以使用「迭代」方式来实现,而迭代有 `while` 和 `for` 两种写法。

代码:
```Java
class Solution {
    public ListNode removeElements(ListNode head, int val) {
        ListNode dummy = new ListNode(-1);
        dummy.next = head;
        for (ListNode tmp = dummy.next, prev = dummy; tmp != null; tmp = tmp.next) {
            if (tmp.val == val) {
                prev.next = tmp.next;
            } else {
                prev = tmp;
            }
        }
        return dummy.next;
    }
}
```
```Java
class Solution {
    public ListNode removeElements(ListNode head, int val) {
        ListNode dummy = new ListNode(-1);
        dummy.next = head;
        ListNode tmp = dummy.next, prev = dummy;
        while (tmp != null) {
            if (tmp.val == val) {
                prev.next = tmp.next;
            } else {
                prev = tmp;
            }
            tmp = tmp.next;
        }
        return dummy.next;
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


Question:
你这个学期必须选修 `numCourses` 门课程,记为 $0$ 到 $numCourses - 1$ 。

在选修某些课程之前需要一些先修课程。 先修课程按数组 `prerequisites` 给出,其中 $prerequisites[i] = [a_i, b_i]$ ,表示如果要学习课程 $a_i$ 则 必须 先学习课程  $b_i$ 。

* 例如,先修课程对 $[0, 1$] 表示:想要学习课程 $0$ ,你需要先完成课程 $1$ 。

请你判断是否可能完成所有课程的学习?如果可以,返回 `true`;否则,返回 `false`。

示例 1:
```
输入:numCourses = 2, prerequisites = [[1,0]]

输出:true

解释:总共有 2 门课程。学习课程 1 之前,你需要完成课程 0 。这是可能的。
```
示例 2:
```
输入:numCourses = 2, prerequisites = [[1,0],[0,1]]

输出:false

解释:总共有 2 门课程。学习课程 1 之前,你需要先完成课程 0 ;并且学习课程 0 之前,你还应先完成课程 1 。这是不可能的。
```

提示:
* $1 <= numCourses <= 10^5$
* $0 <= prerequisites.length <= 5000$
* $prerequisites[i].length == 2$
* $0 <= a_i, b_i < numCourses$
* `prerequisites[i]` 中的所有课程对 互不相同

Solution:
### 拓扑排序

为了方便,我们记 `numCourses` 为 `n`,`prerequisites` 为 `g`。

若课程 `a` 存在前置课程 `b` 的话,我们添加一条从 `b` 到 `a` 的有向边,同时统计所有点的入度。

当处理完所有的 $g[i]$ 后,将所有的入度为 $0$ 的课程(含义为没有前置课程要求的科目)进行入队操作,跑一遍「拓扑排序」,若所有课程都能顺利出队,说明所有课程都能使完成。

代码:
```Java
class Solution {
    int N = 100010, M = 5010;
    int[] in = new int[N], he = new int[N], e = new int[M], ne = new int[M];
    int idx;
    void add(int a, int b) {
        e[idx] = b;
        ne[idx] = he[a];
        he[a] = idx++;
        in[b]++;
    }
    public boolean canFinish(int n, int[][] g) {
        Arrays.fill(he, -1);
        for (int[] info : g) add(info[1], info[0]);
        int ans = 0;
        Deque<Integer> d = new ArrayDeque<>();
        for (int i = 0; i < n; i++) {
            if (in[i] == 0) d.addLast(i);
        }
        while (!d.isEmpty()) {
            int t = d.pollFirst();
            ans++;
            for (int i = he[t]; i != -1; i = ne[i]) {
                int j = e[i];
                if (--in[j] == 0) d.addLast(j);
            }
        }
        return ans == n;
    }
}
```
* 时间复杂度:$O(n + m)$
* 空间复杂度:$O(n + m)$

### 最后

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

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

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

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


LeetCode/201-210/208. 实现 Trie (前缀树)（中等）.md

Question:
Trie(发音类似 "try")或者说 前缀树 是一种树形数据结构,用于高效地存储和检索字符串数据集中的键。这一数据结构有相当多的应用情景,例如自动补完和拼写检查。

请你实现 Trie 类:

* Trie() 初始化前缀树对象。
* void insert(String word) 向前缀树中插入字符串 word 。
* boolean search(String word) 如果字符串 word 在前缀树中,返回 true(即,在检索之前已经插入);否则,返回 false 。
* boolean startsWith(String prefix) 如果之前已经插入的字符串 word 的前缀之一为 prefix ,返回 true ;否则,返回 false 。

示例:
```
输入
["Trie", "insert", "search", "search", "startsWith", "insert", "search"]
[[], ["apple"], ["apple"], ["app"], ["app"], ["app"], ["app"]]

输出
[null, null, true, false, true, null, true]

解释
Trie trie = new Trie();
trie.insert("apple");
trie.search("apple");   // 返回 True
trie.search("app");     // 返回 False
trie.startsWith("app"); // 返回 True
trie.insert("app");
trie.search("app");     // 返回 True
```

提示:
* 1 <= word.length, prefix.length <= 2000
* word 和 prefix 仅由小写英文字母组成
* insert、search 和 startsWith 调用次数 总计 不超过 3 * $10^4$ 次

Solution:
### Trie 树

$Trie$ 树(又叫「前缀树」或「字典树」)是一种用于快速查询「某个字符串/字符前缀」是否存在的数据结构。

其核心是使用「边」来代表有无字符,使用「点」来记录是否为「单词结尾」以及「其后续字符串的字符是什么」。

---

### 二维数组

一个朴素的想法是直接使用「二维数组」来实现 $Trie$ 树。

* 使用二维数组 $trie[]$ 来存储我们所有的单词字符。
* 使用 $index$ 来自增记录我们到底用了多少个格子(相当于给被用到格子进行编号)。
* 使用 $count[]$ 数组记录某个格子被「被标记为结尾的次数」(当 $idx$ 编号的格子被标记了 $n$ 次,则有 $cnt[idx] = n$)。

代码 :
```java []
class Trie {
    int N = 100009; // 直接设置为十万级
    int[][] trie;
    int[] count;
    int index;

    public Trie() {
        trie = new int[N][26];
        count = new int[N];
        index = 0;
    }

    public void insert(String s) {
        int p = 0;
        for (int i = 0; i < s.length(); i++) {
            int u = s.charAt(i) - 'a';
            if (trie[p][u] == 0) trie[p][u] = ++index;
            p = trie[p][u];
        }
        count[p]++;
    }

    public boolean search(String s) {
        int p = 0;
        for (int i = 0; i < s.length(); i++) {
            int u = s.charAt(i) - 'a';
            if (trie[p][u] == 0) return false;
            p = trie[p][u];
        }
        return count[p] != 0;
    }

    public boolean startsWith(String s) {
        int p = 0;
        for (int i = 0; i < s.length(); i++) {
            int u = s.charAt(i) - 'a';
            if (trie[p][u] == 0) return false;
            p = trie[p][u];
        }
        return true;
    }
}
```
* 时间复杂度:$Trie$ 树的每次调用时间复杂度取决于入参字符串的长度。复杂度为 $O(Len)$。
* 空间复杂度:二维数组的高度为 $n$,字符集大小为 $k$。复杂度为 $O(nk)$。

---

### TrieNode

相比二维数组,更加常规的做法是建立 $TrieNode$ 结构节点。

随着数据的不断插入,根据需要不断创建 $TrieNode$ 节点。

代码:
```java []
class Trie {
    class TrieNode {
        boolean end;
        TrieNode[] tns = new TrieNode[26];
    }

    TrieNode root;
    public Trie() {
        root = new TrieNode();
    }

    public void insert(String s) {
        TrieNode p = root;
        for(int i = 0; i < s.length(); i++) {
            int u = s.charAt(i) - 'a';
            if (p.tns[u] == null) p.tns[u] = new TrieNode();
            p = p.tns[u];
        }
        p.end = true;
    }

    public boolean search(String s) {
        TrieNode p = root;
        for(int i = 0; i < s.length(); i++) {
            int u = s.charAt(i) - 'a';
            if (p.tns[u] == null) return false;
            p = p.tns[u];
        }
        return p.end;
    }

    public boolean startsWith(String s) {
        TrieNode p = root;
        for(int i = 0; i < s.length(); i++) {
            int u = s.charAt(i) - 'a';
            if (p.tns[u] == null) return false;
            p = p.tns[u];
        }
        return true;
    }
}
```
* 时间复杂度:$Trie$ 树的每次调用时间复杂度取决于入参字符串的长度。复杂度为 $O(Len)$。
* 空间复杂度:结点数量为 $n$,字符集大小为 $k$。复杂度为 $O(nk)$。

---

### 两种方式的对比

使用「二维数组」的好处是写起来飞快,同时没有频繁 $new$ 对象的开销。但是需要根据数据结构范围估算我们的「二维数组」应该开多少行。

坏处是使用的空间通常是「$TrieNode$」方式的数倍,而且由于通常对行的估算会很大,导致使用的二维数组开得很大,如果这时候每次创建 $Trie$ 对象时都去创建数组的话,会比较慢,而且当样例多的时候甚至会触发 $GC$(因为 $OJ$ 每测试一个样例会创建一个 $Trie$ 对象)。

因此还有一个小技巧是将使用到的数组转为静态,然后利用 $index$ 自增的特性在初始化 $Trie$ 时执行清理工作 & 重置逻辑。

这样的做法能够使评测时间降低一半,运气好的话可以得到一个与「$TrieNode$」方式差不多的时间。

```java []
class Trie {
    // 以下 static 成员独一份,被创建的多个 Trie 共用
    static int N = 100009; // 直接设置为十万级
    static int[][] trie = new int[N][26];
    static int[] count = new int[N];
    static int index = 0;

    // 在构造方法中完成重置 static 成员数组的操作
    // 这样做的目的是为减少 new 操作(无论有多少测试数据,上述 static 成员只会被 new 一次)
    public Trie() {
        for (int row = index; row >= 0; row--) {
            Arrays.fill(trie[row], 0);
        }
        Arrays.fill(count, 0);
        index = 0;
    }

    public void insert(String s) {
        int p = 0;
        for (int i = 0; i < s.length(); i++) {
            int u = s.charAt(i) - 'a';
            if (trie[p][u] == 0) trie[p][u] = ++index;
            p = trie[p][u];
        }
        count[p]++;
    }

    public boolean search(String s) {
        int p = 0;
        for (int i = 0; i < s.length(); i++) {
            int u = s.charAt(i) - 'a';
            if (trie[p][u] == 0) return false;
            p = trie[p][u];
        }
        return count[p] != 0;
    }

    public boolean startsWith(String s) {
        int p = 0;
        for (int i = 0; i < s.length(); i++) {
            int u = s.charAt(i) - 'a';
            if (trie[p][u] == 0) return false;
            p = trie[p][u];
        }
        return true;
    }
}

```

---

### 关于「二维数组」是如何工作 & 1e5 大小的估算

要搞懂为什么行数估算是 1e5,首先要搞清楚「二维数组」是如何工作的。

在「二维数组」中,我们是通过 $index$ 自增来控制使用了多少行的。

当我们有一个新的字符需要记录,我们会将 $index$ 自增(代表用到了新的一行),然后将这新行的下标记录到当前某个前缀的格子中。

举个🌰,假设我们先插入字符串 `abc` 这时候,前面三行会被占掉。

* 第 0 行 `a` 所对应的下标有值,值为 1,代表前缀 `a` 后面接的字符串会被记录在下标为 1 的行内

* 第 1 行 `b` 所对应的下标有值,值为 2,代表前缀 `ab` 后面接的字符串会被记录在下标为 2 的行内

* 第 2 行 `c` 所对应的下标有值,值为 3,代表前缀 `abc` 后面接的字符串会被记录在下标为 3 的行内

当再插入 `abcl` 的时候,这时候会先定位到 `abl` 的前缀行(第 3 行),将 `l` 的下标更新为 4,代表 `abcl` 被加入前缀树,并且前缀 `abcl` 接下来会用到第 4 行进行记录。

但当插入 `abl` 的时候,则会定位到 `ab` 的前缀行(第 2 行),然后将 `l` 的下标更新为 5,代表 `abl` 被加入前缀树,并且前缀 `abl` 接下来会使用第 5 行进行记录。

当搞清楚了「二维数组」是如何工作之后,我们就能开始估算会用到多少行了,调用次数为 $10^4$,传入的字符串长度为 $10^3$,假设每一次的调用都是 $insert$,并且每一次调用都会使用到新的 $10^3$ 行。那么我们的行数需要开到 $10^7$。

**但由于我们的字符集大小只有 26,因此不太可能在 $10^4$ 次调用中都用到新的 $10^3$ 行。**

**而且正常的测试数据应该是 $search$ 和 $startsWith$ 调用次数大于 $insert$ 才有意义的,一个只有 $insert$ 调用的测试数据,任何实现方案都能 AC。**

**因此我设定了 $10^5$ 为行数估算,当然直接开到 $10^6$ 也没有问题。**

---

### 关于 Trie 的应用面

首先,在纯算法领域,前缀树算是一种较为常用的数据结构。

不过如果在工程中,不考虑前缀匹配的话,基本上使用 hash 就能满足。

如果考虑前缀匹配的话,工程也不会使用 Trie 。

一方面是字符集大小不好确定(题目只考虑 26 个字母,字符集大小限制在较小的 26 内)因此可以使用 Trie,但是工程一般兼容各种字符集,一旦字符集大小很大的话,Trie 将会带来很大的空间浪费。

另外,对于个别的超长字符 Trie 会进一步变深。

这时候如果 Trie 是存储在硬盘中,Trie 结构过深带来的影响是多次随机 IO,随机 IO 是成本很高的操作。

同时 Trie 的特殊结构,也会为分布式存储将会带来困难。

因此在工程领域中 Trie 的应用面不广。

至于一些诸如「联想输入」、「模糊匹配」、「全文检索」的典型场景在工程主要是通过 ES (ElasticSearch) 解决的。

而 ES 的实现则主要是依靠「倒排索引」

### 最后

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

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

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

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


209. 长度最小的子数组

Question:
给定一个含有 `n` 个正整数的数组和一个正整数 `target`。

找出该数组中满足其和 `≥ target` 的长度最小的 连续子数组 $[nums_l, nums_{l+1}, ..., nums_{r-1}, nums_r]$ ,并返回其长度。如果不存在符合条件的子数组,返回 $0$ 。

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

输出:2

解释:子数组 [4,3] 是该条件下的长度最小的子数组。
```
示例 2:
```
输入:target = 4, nums = [1,4,4]

输出:1
```
示例 3:
```
输入:target = 11, nums = [1,1,1,1,1,1,1,1]

输出:0
```

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

Solution:
### 前缀和 + 二分

利用 $nums[i]$ 的数据范围为 $[1, 10^5]$,可知前缀和数组满足「单调递增」。

我们先预处理出前缀和数组 `sum`(前缀和数组下标默认从 $1$ 开始),对于每个 $nums[i]$ 而言,假设其对应的前缀和值为 $s = sum[i + 1]$,我们将 $nums[i]$ 视为子数组的右端点,问题转换为:在前缀和数组下标 $[0, i]$ 范围内找到满足「**值小于等于 $s - t$**」的最大下标,充当子数组左端点的前一个值。

利用前缀和数组的「单调递增」(即具有二段性),该操作可使用「二分」来做。

Java 代码:
```Java
class Solution {
    public int minSubArrayLen(int t, int[] nums) {
        int n = nums.length, ans = n + 10;
        int[] sum = new int[n + 10];
        for (int i = 1; i <= n; i++) sum[i] = sum[i - 1] + nums[i - 1];
        for (int i = 1; i <= n; i++) {
            int d = sum[i] - t;
            int l = 0, r = i;
            while (l < r) {
                int mid = l + r + 1 >> 1;
                if (sum[mid] <= d) l = mid;
                else r = mid - 1;
            }
            if (sum[r] <= d) ans = Math.min(ans, i - r);
        }
        return ans == n + 10 ? 0 : ans;
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    int minSubArrayLen(int t, vector<int>& nums) {
        int n = nums.size(), ans = n + 10;
        vector<int> sum(n + 10, 0);
        for (int i = 1; i <= n; i++) sum[i] = sum[i - 1] + nums[i - 1];
        for (int i = 1; i <= n; i++) {
            int d = sum[i] - t;
            int l = 0, r = i;
            while (l < r) {
                int mid = (l + r + 1) >> 1;
                if (sum[mid] <= d) l = mid;
                else r = mid - 1;
            }
            if (sum[r] <= d) ans = min(ans, i - r);
        }
        return ans == n + 10 ? 0 : ans;
    }
};
```
Python 代码:
```Python
class Solution:
    def minSubArrayLen(self, t: int, nums: List[int]) -> int:
        n, ans = len(nums), len(nums) + 10
        s = [0] * (n + 10)
        for i in range(1, n + 1):
            s[i] = s[i - 1] + nums[i - 1]
        for i in range(1, n + 1):
            d = s[i] - t
            l, r = 0, i
            while l < r:
                mid = (l + r + 1) // 2
                if s[mid] <= d:
                    l = mid
                else:
                    r = mid - 1
            if s[r] <= d:
                ans = min(ans, i - r)
        return 0 if ans == n + 10 else ans
```
TypeScript 代码:
```TypeScript
function minSubArrayLen(t: number, nums: number[]): number {
    let n = nums.length, ans = n + 10;
    const sum = new Array(n + 10).fill(0);
    for (let i = 1; i <= n; i++) sum[i] = sum[i - 1] + nums[i - 1];
    for (let i = 1; i <= n; i++) {
        const d = sum[i] - t;
        let l = 0, r = i;
        while (l < r) {
            const mid = l + r + 1 >> 1;
            if (sum[mid] <= d) l = mid;
            else r = mid - 1;
        }
        if (sum[r] <= d) ans = Math.min(ans, i - r);
    }
    return ans == n + 10 ? 0 : ans;
};
```
* 时间复杂度:预处理前缀和数组的复杂度为 $O(n)$,遍历前缀和数组统计答案复杂度为 $O(n\log{n})$。整体复杂度为 $O(n\log{n})$
* 空间复杂度:$O(n)$

---

### 滑动窗口

另外一个,复杂度比 $O(n\log{n})$ 更低的做法,是滑动窗口。

在一次遍历过程中,使用 `j` 和 `i` 分别代表窗口的左右端点,变量 `c` 用于记录窗口内的数值总和。

遍历过程其实就是右端点 `i` 不断右移的过程,每次将当前右端点 `i` 的值累加到 `c` 上,若累加后,左端点右移仍能满足「总和大于等于 `t`」的要求,那么我们则让左端点 `j` 右移。

如此一来,我们便得到了每个右端点 `i` 固定时,下标最大的合法左端点 `j`(若有)。所有合法窗口长度的最小值即是答案。

代码:
```Java
class Solution {
    public int minSubArrayLen(int t, int[] nums) {
        int n = nums.length, ans = n + 10;
        for (int i = 0, j = 0, c = 0; i < n; i++) {
            c += nums[i];
            while (j < i && c - nums[j] >= t) c -= nums[j++];
            if (c >= t) ans = Math.min(ans, i - j + 1);
        }
        return ans > n ? 0 : ans;
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


Question:
给你一个在 X-Y 平面上的点构成的数据流。设计一个满足下述要求的算法:

* 添加 一个在数据流中的新点到某个数据结构中。可以添加 重复 的点,并会视作不同的点进行处理。
* 给你一个查询点,请你从数据结构中选出三个点,使这三个点和查询点一同构成一个 面积为正 的 轴对齐正方形 ,统计 满足该要求的方案数目。

轴对齐正方形 是一个正方形,除四条边长度相同外,还满足每条边都与 x-轴 或 y-轴 平行或垂直。

实现 `DetectSquares` 类:
* `DetectSquares()` 使用空数据结构初始化对象
* `void add(int[] point)` 向数据结构添加一个新的点 `point = [x, y]`
* `int count(int[] point)` 统计按上述方式与点 `point = [x, y]` 共同构造 轴对齐正方形 的方案数。

示例:

```
输入:
["DetectSquares", "add", "add", "add", "count", "count", "add", "count"]
[[], [[3, 10]], [[11, 2]], [[3, 2]], [[11, 10]], [[14, 8]], [[11, 2]], [[11, 10]]]
输出:
[null, null, null, null, 1, 0, null, 2]

解释:
DetectSquares detectSquares = new DetectSquares();
detectSquares.add([3, 10]);
detectSquares.add([11, 2]);
detectSquares.add([3, 2]);
detectSquares.count([11, 10]); // 返回 1 。你可以选择:
                               //   - 第一个,第二个,和第三个点
detectSquares.count([14, 8]);  // 返回 0 。查询点无法与数据结构中的这些点构成正方形。
detectSquares.add([11, 2]);    // 允许添加重复的点。
detectSquares.count([11, 10]); // 返回 2 。你可以选择:
                               //   - 第一个,第二个,和第三个点
                               //   - 第一个,第三个,和第四个点
```

提示:
* $point.length == 2$
* $0 <= x, y <= 1000$
* 调用 `add` 和 `count` 的 总次数 最多为 $5000$

Solution:
### 哈希表

对于 `add` 操作,我们可以使用「哈希表 套 哈希表」的方式,以 `{x, {y : 点 (x,y) 数量}}` 的形式对传入点进行存储。

对于 `count` 查询而言,假定传入的点为 $(x, y)$,我们可以先查询 $x$ 行都有哪些列,枚举这些列( 即枚举点 $(x, ny)$ ),由 $y$ 和 $ny$ 可得正方形边长 $len$,此时再检查唯一确定的两点 $(x \pm len, y)$ 和 $(x \pm len, ny)$ 的出现次数,应用乘法原理,即可知道该正方形的方案数,统计所有合法方案数即是该询问的答案。

利用题目范围给定的 `x` 和 `y` 具有明确的范围 `0 <= x, y <= 1000`,我们可以使用数组充当哈希表,但是为了拓展性和减少边界判断,即支持将平面拓展到任意大小,最好还是直接使用哈希表。

代码(数组充当哈希表代码见 $P2$):
```Java
class DetectSquares {
    Map<Integer, Map<Integer, Integer>> row2Col = new HashMap<>();

    public void add(int[] point) {
        int x = point[0], y = point[1];
        Map<Integer, Integer> col2Cnt = row2Col.getOrDefault(x, new HashMap<>());
        col2Cnt.put(y, col2Cnt.getOrDefault(y, 0) + 1);
        row2Col.put(x, col2Cnt);
    }

    public int count(int[] point) {
        int x = point[0], y = point[1];
        int ans = 0;
        Map<Integer, Integer> col2Cnt = row2Col.getOrDefault(x, new HashMap<>());
        for (int ny : col2Cnt.keySet()) {
            if (ny == y) continue;
            int c1 = col2Cnt.get(ny);
            int len = y - ny;
            int[] nums = new int[]{x + len, x - len};
            for (int nx : nums) {
                Map<Integer, Integer> temp = row2Col.getOrDefault(nx, new HashMap<>());
                int c2 = temp.getOrDefault(y, 0), c3 = temp.getOrDefault(ny, 0);
                ans += c1 * c2 * c3;
            }
        }
        return ans;
    }
}
```
-
```Java
class DetectSquares {
    int N  = 1010;
    int[][] cnts = new int[N][N];

    public void add(int[] point) {
        int x = point[0], y = point[1];
        cnts[x][y]++;
    }

    public int count(int[] point) {
        int x = point[0], y = point[1];
        int ans = 0;
        for (int ny = 0; ny < N; ny++) {
            if (y == ny) continue;
            int c1 = cnts[x][ny];
            if (c1 == 0) continue;
            int len = y - ny;
            int[] nums = new int[]{x + len, x - len};
            for (int nx : nums) {
                if (nx < 0 || nx >= N) continue;
                int c2 = cnts[nx][y], c3 = cnts[nx][ny];
                ans += c1 * c2 * c3;
            }
        }
        return ans;
    }
}
```
* 时间复杂度:`add` 操作的复杂度为 $O(1)$,`count` 最坏情况会扫描完所有此前加入的点,复杂度为 $O(n)$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


LeetCode/2011-2020/2016. 增量元素之间的最大差值（简单）.md

2016. 增量元素之间的最大差值

Question:
给你一个下标从 $0$ 开始的整数数组 $nums$ ,该数组的大小为 $n$ ,请你计算 $nums[j] - nums[i]$ 能求得的 最大差值 ,其中 $0 <= i < j < n$ 且 $nums[i] < nums[j]$ 。

返回 最大差值 。如果不存在满足要求的 $i$ 和 $j$ ,返回 $-1$ 。

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

输出:4

解释:
最大差值出现在 i = 1 且 j = 2 时,nums[j] - nums[i] = 5 - 1 = 4 。
注意,尽管 i = 1 且 j = 0 时 ,nums[j] - nums[i] = 7 - 1 = 6 > 4 ,但 i > j 不满足题面要求,所以 6 不是有效的答案。
```
示例 2:
```
输入:nums = [9,4,3,2]

输出:-1
解释:
不存在同时满足 i < j 和 nums[i] < nums[j] 这两个条件的 i, j 组合。
```
示例 3:
```
输入:nums = [1,5,2,10]

输出:9

解释:
最大差值出现在 i = 0 且 j = 3 时,nums[j] - nums[i] = 10 - 1 = 9 。
```

提示:
* $n == nums.length$
* $2 <= n <= 1000$
* $1 <= nums[i] <= 10^9$

Solution:
### 模拟

一个显然的做法是两层循环找合适的 `i` 和 `j`,这样的做法是 $O(n^2)$ 的。

利用我们目的是找到能够取得最大差值的数对,对于每个数对中的 $nums[i]$ 而言,对应的 $nums[j]$ 必然第是坐标 $i$ 左侧的最小值,因此可以通过边遍历边维护最小值 $min$ 的做法,从而将复杂度降到 $O(n)$。

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

### 最后

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

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

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

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


LeetCode/2021-2030/2022. 将一维数组转变成二维数组（简单）.md

2022. 将一维数组转变成二维数组

Question:
给你一个下标从 $0$ 开始的一维整数数组 `original` 和两个整数 `m` 和  `n` 。

你需要使用 `original` 中 所有 元素创建一个 `m` 行 `n` 列的二维数组。

`original` 中下标从 `0` 到 `n - 1` (都 包含 )的元素构成二维数组的第一行,下标从 `n` 到 `2 * n - 1` (都 包含 )的元素构成二维数组的第二行,依此类推。

请你根据上述过程返回一个 `m x n` 的二维数组。如果无法构成这样的二维数组,请你返回一个空的二维数组。

示例 1:

```
输入:original = [1,2,3,4], m = 2, n = 2

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

解释:
构造出的二维数组应该包含 2 行 2 列。
original 中第一个 n=2 的部分为 [1,2] ,构成二维数组的第一行。
original 中第二个 n=2 的部分为 [3,4] ,构成二维数组的第二行。
```
示例 2:
```
输入:original = [1,2,3], m = 1, n = 3

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

解释:
构造出的二维数组应该包含 1 行 3 列。
将 original 中所有三个元素放入第一行中,构成要求的二维数组。
```
示例 3:
```
输入:original = [1,2], m = 1, n = 1

输出:[]

解释:
original 中有 2 个元素。
无法将 2 个元素放入到一个 1x1 的二维数组中,所以返回一个空的二维数组。
```
示例 4:
```
输入:original = [3], m = 1, n = 2

输出:[]

解释:
original 中只有 1 个元素。
无法将 1 个元素放满一个 1x2 的二维数组,所以返回一个空的二维数组。
```

提示:
* $1 <= original.length <= 5 * 10^4$
* $1 <= original[i] <= 10^5$
* $1 <= m, n <= 4 * 10^4$

Solution:
### 模拟

**新年快乐,祝大家身体健康,各种上岸 🎉 🎉(带着 $2021$ 年的一切美好品质继续前行**

构造 $m * n$ 的新二维矩阵 `year2022`,并使用 $idx$ 对旧矩阵 `year2021` 进行遍历即可。

代码:
```Java
class Solution {
    public int[][] construct2DArray(int[] year2021, int m, int n) {
        if (year2021.length != m * n) return new int[0][0];
        int[][] year2022 = new int[m][n];
        for (int i = 0, idx = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                year2022[i][j] = year2021[idx++];
            }
        }
        return year2022;
    }
}
```
* 时间复杂度:$O(m * n)$
* 空间复杂度:$O(m * n)$

### 最后

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

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

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

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


LeetCode/2021-2030/2024. 考试的最大困扰度（中等）.md

2024. 考试的最大困扰度

Question:
一位老师正在出一场由 $n$ 道判断题构成的考试,每道题的答案为 `true` (用 `'T'` 表示)或者 `false` (用 `'F'` 表示)。老师想增加学生对自己做出答案的不确定性,方法是最大化有连续相同结果的题数。(也就是连续出现 `true` 或者连续出现 `false`)。

给你一个字符串 $answerKey$ ,其中 $answerKey[i]$ 是第 $i$ 个问题的正确结果。除此以外,还给你一个整数 $k$,表示你能进行以下操作的最多次数:

每次操作中,将问题的正确答案改为 `'T'` 或者 `'F'` (也就是将 $answerKey[i]$ 改为 `'T'` 或者 `'F'` )。
请你返回在不超过 $k$ 次操作的情况下,最大 连续 `'T'` 或者 `'F'` 的数目。

示例 1:
```
输入:answerKey = "TTFF", k = 2

输出:4

解释:我们可以将两个 'F' 都变为 'T' ,得到 answerKey = "TTTT" 。
总共有四个连续的 'T' 。
```
示例 2:
```
输入:answerKey = "TFFT", k = 1

输出:3

解释:我们可以将最前面的 'T' 换成 'F' ,得到 answerKey = "FFFT" 。
或者,我们可以将第二个 'T' 换成 'F' ,得到 answerKey = "TFFF" 。
两种情况下,都有三个连续的 'F' 。
```
示例 3:
```
输入:answerKey = "TTFTTFTT", k = 1

输出:5

解释:我们可以将第一个 'F' 换成 'T' ,得到 answerKey = "TTTTTFTT" 。
或者我们可以将第二个 'F' 换成 'T' ,得到 answerKey = "TTFTTTTT" 。
两种情况下,都有五个连续的 'T' 。
```

提示:
* $n == answerKey.length$
* $1 <= n <= 5 \times 10^4$
* $answerKey[i]$ 要么是 `'T'` ,要么是 `'F'`
* $1 <= k <= n$

Solution:
### 滑动窗口

题目求修改次数不超过 $k$ 的前提下,连续段 `'T'` 或 `'F'` 的最大长度。

等价于求一个包含 `'F'` 或者 `'T'` 的个数不超过 $k$ 的最大长度窗口。

假定存在一个 `int getCnt(char c)` 函数,返回包含字符 `c` 数量不超过 $k$ 的最大窗口长度,那么最终 `max(getCnt('T'), getCnt('F'))` 即是答案。

其中 `getCnt` 函数的实现可以使用「滑动窗口」:使用 $j$ 和 $i$ 分别代表窗口的左右端点,$cnt$ 为区间 $[j, i]$ 中的字符 `c` 的数量,每次右端点 $i$ 移动时,若满足 $s[i] = c$,让 $cnt$ 自增,当 $cnt > k$ 时,使左端点 $j$ 往右移动,同时更新 $cnt$,直到 $[j, i]$ 区间恢复合法性(包含字符 `c` 的数量 $cnt$ 不超过 $k$ 个)。

代码:
```Java
class Solution {
    String s;
    int n, k;
    public int maxConsecutiveAnswers(String answerKey, int _k) {
        s = answerKey;
        n = s.length(); k = _k;
        return Math.max(getCnt('T'), getCnt('F'));
    }
    int getCnt(char c) {
        int ans = 0;
        for (int i = 0, j = 0, cnt = 0; i < n; i++) {
            if (s.charAt(i) == c) cnt++;
            while (cnt > k) {
                if (s.charAt(j) == c) cnt--;
                j++;
            }
            ans = Math.max(ans, i - j + 1);
        }
        return ans;
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


LeetCode/2021-2030/2028. 找出缺失的观测数据（中等）.md

2028. 找出缺失的观测数据

Question:
现有一份 $n + m$ 次投掷单个「六面」骰子的观测数据,骰子的每个面从 $1$ 到 $6$ 编号。观测数据中缺失了 $n$ 份,你手上只拿到剩余 $m$ 次投掷的数据。幸好你有之前计算过的这 $n + m$ 次投掷数据的平均值。

给你一个长度为 $m$ 的整数数组 `rolls` ,其中 $rolls[i]$ 是第 $i$ 次观测的值。同时给你两个整数 $mean$ 和 $n$ 。

返回一个长度为 $n$ 的数组,包含所有缺失的观测数据,且满足这 $n + m$ 次投掷的平均值是 $mean$。

如果存在多组符合要求的答案,只需要返回其中任意一组即可。如果不存在答案,返回一个空数组。

$k$ 个数字的 平均值 为这些数字求和后再除以 $k$ 。

注意 $mean$ 是一个整数,所以 $n + m$ 次投掷的总和需要被 $n + m$ 整除。

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

输出:[6,6]

解释:所有 n + m 次投掷的平均值是 (3 + 2 + 4 + 3 + 6 + 6) / 6 = 4 。
```
示例 2:
```
输入:rolls = [1,5,6], mean = 3, n = 4

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

解释:所有 n + m 次投掷的平均值是 (1 + 5 + 6 + 2 + 3 + 2 + 2) / 7 = 3 。
```
示例 3:
```
输入:rolls = [1,2,3,4], mean = 6, n = 4

输出:[]

解释:无论丢失的 4 次数据是什么,平均值都不可能是 6 。
```
示例 4:
```
输入:rolls = [1], mean = 3, n = 1

输出:[5]

解释:所有 n + m 次投掷的平均值是 (1 + 5) / 2 = 3 。
```

提示:
* $m == rolls.length$
* $1 <= n, m <= 10^5$
* $1 <= rolls[i], mean <= 6$

Solution:
### 构造

根据题意,我们需要构造长度为 $n$ 的序列 $ans$,使得 $ans$ 和 $rolls$ 并集的平均值为 $mean$。

由于最终的平均值 $mean$ 已知,我们可以直接算得两序列之和为 $t = (m + n) \times mean$。

使用 $t$ 减去 $$\sum_{i = 0}^{m}rolls[i]$$ 可得 $$\sum_{i = 0}^{n}ans[i]$$。我们知道一个长度为 $n$ 的有效序列的元素和范围为 $[n, 6 \times n]$(骰子编号为 $[1, 6]$),根据 $\sum_{i = 0}^{m}rolls[i]$ 与 $[n, 6 \times n]$ 关系进行分情况讨论:

* 如果 $$\sum_{i = 0}^{n}ans[i]$$ 不落在 $[n, 6  \times n]$ 范围内,无解,直接返回空数组;

* 如果 $$\sum_{i = 0}^{m} rool[i]$$ 落在 $[n, 6 \times n]$ 范围内,有解,此时尝试构造一个合法的 $ans$ : 起始使用 $$\left \lfloor \frac{\sum_{i = 0}^{n}ans[i]}{n} \right \rfloor$$ 填充 $ans$,若 $$\left \lfloor \frac{\sum_{i = 0}^{n}ans[i]}{n} \right \rfloor \times n < \sum_{i = 0}^{n}ans[i]$$,计算两者差异值 $d$,并尝试将 $d$ 分摊到前 $d$ 个 $ans[i]$ 上(该过程一定可以顺利进行)。

代码:
```Java
class Solution {
    public int[] missingRolls(int[] rolls, int mean, int n) {
        int m = rolls.length, cnt = m + n;
        int t = mean * cnt;
        for (int i : rolls) t -= i;
        if (t < n || t > 6 * n) return new int[0];
        int[] ans = new int[n];
        Arrays.fill(ans, t / n);
        if (t / n * n < t) {
            int d = t - (t / n * n), idx = 0;
            while (d-- > 0) ans[idx++]++;
        }
        return ans;
    }
}
```
* 时间复杂度:$O(m + n)$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


Question:
`Alice` 和 `Bob` 轮流进行自己的回合,`Alice` 先手。每一回合,玩家需要从 `stones` 中移除任一石子。

如果玩家移除石子后,导致 所有已移除石子 的价值 总和 可以被 $3$ 整除,那么该玩家就 输掉游戏 。
如果不满足上一条,且移除后没有任何剩余的石子,那么 `Bob` 将会直接获胜(即便是在 `Alice` 的回合)。
假设两位玩家均采用 最佳 决策。如果 `Alice` 获胜,返回 `true` ;如果 `Bob` 获胜,返回 `false` 。

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

输出:true

解释:游戏进行如下:
- 回合 1:Alice 可以移除任意一个石子。
- 回合 2:Bob 移除剩下的石子。
已移除的石子的值总和为 1 + 2 = 3 且可以被 3 整除。因此,Bob 输,Alice 获胜。
```
示例 2:
```
输入:stones = [2]

输出:false

解释:Alice 会移除唯一一个石子,已移除石子的值总和为 2 。
由于所有石子都已移除,且值总和无法被 3 整除,Bob 获胜。
```
示例 3:
```
输入:stones = [5,1,2,4,3]

输出:false

解释:Bob 总会获胜。其中一种可能的游戏进行方式如下:
- 回合 1:Alice 可以移除值为 1 的第 2 个石子。已移除石子值总和为 1 。
- 回合 2:Bob 可以移除值为 3 的第 5 个石子。已移除石子值总和为 = 1 + 3 = 4 。
- 回合 3:Alices 可以移除值为 4 的第 4 个石子。已移除石子值总和为 = 1 + 3 + 4 = 8 。
- 回合 4:Bob 可以移除值为 2 的第 3 个石子。已移除石子值总和为 = 1 + 3 + 4 + 2 = 10.
- 回合 5:Alice 可以移除值为 5 的第 1 个石子。已移除石子值总和为 = 1 + 3 + 4 + 2 + 5 = 15.
Alice 输掉游戏,因为已移除石子值总和(15)可以被 3 整除,Bob 获胜。
```

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

Solution:
### 分情况讨论博弈

为了方便,我们用 `A` 来代指 `Alice`,用 `B` 带代指 `Bob`。

`A` 只有一种获胜方式,是使得 `B` 在选石子时凑成 $3$ 的倍数;而 `B` 除了能够通过让 `A` 凑成 $3$ 的倍数以外,还能通过让游戏常规结束来获胜。

因此整个游戏过程,我们只需要关心「已被移除的石子总和」和「剩余石子个数/价值情况」即可。

更进一步的,我们只需关心已被移除的石子总和是否为 $3$ 的倍数,以及剩余石子的价值与已移除石子总和相加是否凑成 $3$ 的倍数即可。

所以我们可以按照石子价值除以 $3$ 的余数分成三类,并统计相应数量。

不失一般性考虑,某个回合开始前,已移除的石子总和状态为 $x$(共三种,分别为除以 $3$ 余数为 $0$、$1$ 和 $2$,其中当状态为 $0$,且非首个回合时,说明凑成 $3$ 的倍数,游戏结束),剩余石子价值除以 $3$ 的余数 $s$ 分别为 $0$、$1$ 和 $2$。

**首先如果当前 $x = 1$ 时,不能选择 $s = 2$ 的石子,否则会导致凑成总和为 $3$ 的倍数而失败;同理 $x = 2$ 时,不能选择 $s = 1$ 的石子;而选择 $s = 0$ 的数字,不会改变 $x$ 的状态,可看做换手操作。**

**同时成对的 $s = 0$ 的等价于没有 $s = 0$ 的石子(双方只需要轮流选完这些 $s = 0$ 的石子,最终会回到先手最开始的局面);而选择与 $x$ 相同的 $s$ 会导致 $x$ 改变(即 $x = 1$ 时,选择 $s = 1$ 的石子,会导致 $x = 2$;而 $x = 2$ 时,选 $s = 2$ 的石子,会导致 $x = 1$)。**

明确规则后,是分情况讨论的过程:

* **$s = 0$ 的石子数量为偶数**:此时等价于没有 $s = 0$ 的石子,我们只需要关心 $s = 1$ 和 $s = 2$ 即可:

    * **$s = 1$ 的石子数量为 $0$**: 这意味着 `A` 开始选择的只能是 $s = 2$,此时交给 `B` 的局面为「$x = 2$、剩余石子只有 $s = 2$」,此时 `B` 只能选 $s = 2$ 的石子,由于 $x = 2$ 且选择的石子 $s = 2$,因此交由回 `A` 的局面为「$x = 1$,剩余是在只有 $s = 2$」,因此游戏继续的话 `A` 必败,同时如果在该过程的任何时刻石子被取完,也是 `B` 直接获胜,即 **`A` 仍为必败**;

    * **$s = 2$ 的石子数量为 $0$**:分析同理,`A` 只能选 $s = 1$,此时交给 `B` 的局面为「$x = 1$、剩余石子只有 $s = 1$」,此时 `B` 只能选 $s = 1$ 的石子,由于 $x = 1$ 且选择的石子 $s = 1$,因此交由回 `A` 的局面为「$x = 2$,剩余是在只有 $s = 1$」,因此游戏继续的话 `A` 必败,同时如果在该过程的任何时刻石子被取完,也是 `B` 直接获胜,即 **`A` 仍为必败**;

    * **$s = 1$ 和 $s = 2$ 的石子数量均不为 $0$**:`A` 选数量不是最多的一类石子,`B` 下一回合只能选择相同类型的石子(或是无从选择导致失败),然后游戏继续,最终 `B` 会先进入「只能凑成 $3$ 的倍数」的局面导致失败,即 **`A` 必胜。**

* **$s = 0$ 的石子数量为奇数**:此时等价于有一次换手机会,该换手机会必然应用在「对必败局面进行转移」才有意义,因此只有 $s = 1$ 和 $s = 2$ 的石子数量差大于 $2$,`A` 的先手优势不会因为存在换手机会而被转移:

    * **两者数量差不超过 $2$**:此时 `B` 可以利用「对方凑成 $3$ 的倍数必败」规则和「优先使用 $s = 0$ 石子」权利来进入确保自己为必胜态:

        * 举个 🌰,当 $s = 1$ 和 $s = 2$ 的石子数量相等,虽然有 $s = 0$ 的石子,`A` 先手,但是 `A` 的首个回合必然不能选 $s = 0$,否则马上失败结束,因此 `A` 只能选 $s = 1$ 或 $s = 2$,此时 `B`直接选择 $s = 0$ 的石子,交由给 `A` 的局面 $x$ 没有发生改变,`A` 只能选择与首个回合相同的 $s$ 游戏才能继续,因此局面会变为「`B` 先手、$s = 1$ 和 $s = 2$ 的石子数量差为 $2$」,游戏继续,最终 `A` 会先遇到「只能凑成 $3$ 的倍数」的局面,即 **`B` 必胜**。

        * 两者数量不等,但数量差不超过 $2$:此时无论 `A` 选择数量较少或较多的 `s`,`B` 都在第二回合马上使用 $s = 0$ 的石子进行换手,`A` 只能继续选与第一回合相同类型的的石子,游戏才能进行,最终 `A` 会先遇到「只能凑成 $3$ 的倍数」或「石子被取完」的局面,即 **`B` 必胜**。

    * **两者数量差超过 $2$** :此时即使 `A` 只要确保第一次选择数量较多的 $s$,不管 `B` 是否使用「优先使用 $s = 0$」的石子,`A` 都有足够次数数量多 $s$ 来抵消换手(或是在 `B` 放弃使用 $s = 0$ 之后马上使用),最终都是 `B` 最先遇到「只能凑成 $3$ 的倍数」的局面,即 **`A` 获胜**。

代码:
```Java
class Solution {
    public boolean stoneGameIX(int[] stones) {
        int[] cnts = new int[3];
        for (int i : stones) cnts[i % 3]++;
        return cnts[0] % 2 == 0 ? !(cnts[1] == 0 || cnts[2] == 0) : Math.abs(cnts[1] - cnts[2]) > 2;
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


LeetCode/2031-2040/2031. 1 比 0 多的子数组个数（中等）.md

2031. 1 比 0 多的子数组个数

Question:
给你一个只包含 $0$ 和 $1$ 的数组 $nums$,请返回 $1$ 的数量 大于 $04 的数量的子数组的个数。

由于答案可能很大,请返回答案对 $10^9 + 7$ 取余 的结果。

一个 子数组 指的是原数组中连续的一个子序列。

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

输出: 9

解释:
长度为 1 的、1 的数量大于 0 的数量的子数组有: [1], [1], [1]
长度为 2 的、1 的数量大于 0 的数量的子数组有: [1,1]
长度为 3 的、1 的数量大于 0 的数量的子数组有: [0,1,1], [1,1,0], [1,0,1]
长度为 4 的、1 的数量大于 0 的数量的子数组有: [1,1,0,1]
长度为 5 的、1 的数量大于 0 的数量的子数组有: [0,1,1,0,1]
```
示例 2:
```
输入: nums = [0]

输出: 0

解释:
没有子数组的 1 的数量大于 0 的数量。
```
示例 3:
```
输入: nums = [1]

输出: 1

解释:
长度为 1 的、1 的数量大于 0 的数量的子数组有: [1]
```

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

Solution:
### 树状数组

为了方便,我们调整数组 $nums$ 下标从 $1$ 开始。同时将 $nums[i] = 0$ 的值看作 $nums[i] = -1$,并预处理出前缀和数组 $sum$。对于任意 $sum[i]$ 而言,其值域范围为 $[-n, n]$,我们可以通过对 $sum[i]$ 做整体 $n + 1$ 的偏移,将值域映射到 $[1, 2 * n + 1]$。

对于任意一个子数组 $i...j$ 而言,如果满足 $1$ 的数量大于 $0$,则必然有 $sum[j] - sum[i - 1] > 0$。

因此在求解以 $nums[j]$ 为右端点的「满足 $1$ 数量大于 $0$ 数量」的子数组个数时,等价于在问 $[0, j - 1]$ 范围内有多少个下标 $i$ 满足 $sum[i] < sum[j]$(即有多少个下标可以作为左端点)。

求解比 $x$ 小的数有多少个,可以使用「树状数组」来做。

具体的,我们可以遍历每个位置,假设当前处理到的位置是 $i$,其前缀和值 $t = sum[i]$,我们可以通过查询小于等于 $t - 1$ 的值数量来得知以 $i$ 为右端点的合法子数组数量,统计后,我们需要对数值为 $t$ 出现次数进行加一。

代码:
```Java
class Solution {
    int N = 200010, MOD = (int)1e9+7;
    int[] tr = new int[N];
    int n;
    int lowbit(int x) {
        return x & -x;
    }
    void add(int x, int v) {
        for (int i = x; i <= 2 * n + 1; i += lowbit(i)) tr[i] += v;
    }
    int query(int x) {
        int ans = 0;
        for (int i = x; i > 0; i -= lowbit(i)) ans += tr[i];
        return ans;
    }
    public int subarraysWithMoreZerosThanOnes(int[] nums) {
        n = nums.length;
        int ans = 0;
        int[] sum = new int[n + 10];
        for (int i = 1; i <= n; i++) sum[i] = sum[i - 1] + (nums[i - 1] == 0 ? -1 : 1);
        for (int i = 0; i <= n; i++) {
            int t = sum[i] + n + 1;
            ans = (ans + query(t - 1)) % MOD;
            add(t, 1);
        }
        return ans;
    }
}
```
* 时间复杂度:预处理前缀和数组的复杂度为 $O(n)$;查询和插入的复杂度均为 $O(\log{n})$,整体复杂度为 $O(n\log{n})$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


2034. 股票价格波动

Question:
给你一支股票价格的数据流。数据流中每一条记录包含一个 时间戳 和该时间点股票对应的 价格 。

不巧的是,由于股票市场内在的波动性,股票价格记录可能不是按时间顺序到来的。某些情况下,有的记录可能是错的。如果两个有相同时间戳的记录出现在数据流中,前一条记录视为错误记录,后出现的记录 更正 前一条错误的记录。

请你设计一个算法,实现:
* 更新 股票在某一时间戳的股票价格,如果有之前同一时间戳的价格,这一操作将 更正 之前的错误价格。
* 找到当前记录里 最新股票价格 。最新股票价格 定义为时间戳最晚的股票价格。
* 找到当前记录里股票的 最高价格 。
* 找到当前记录里股票的 最低价格 。

请你实现 `StockPrice` 类:
* `StockPrice()` 初始化对象,当前无股票价格记录。
* `void update(int timestamp, int price)` 在时间点 `timestamp` 更新股票价格为 `price`。
* `int current()` 返回股票 最新价格 。
* `int maximum()` 返回股票 最高价格 。
* `int minimum()` 返回股票 最低价格 。

示例 1:
```
输入:
["StockPrice", "update", "update", "current", "maximum", "update", "maximum", "update", "minimum"]
[[], [1, 10], [2, 5], [], [], [1, 3], [], [4, 2], []]
输出:
[null, null, null, 5, 10, null, 5, null, 2]

解释:
StockPrice stockPrice = new StockPrice();
stockPrice.update(1, 10); // 时间戳为 [1] ,对应的股票价格为 [10] 。
stockPrice.update(2, 5);  // 时间戳为 [1,2] ,对应的股票价格为 [10,5] 。
stockPrice.current();     // 返回 5 ,最新时间戳为 2 ,对应价格为 5 。
stockPrice.maximum();     // 返回 10 ,最高价格的时间戳为 1 ,价格为 10 。
stockPrice.update(1, 3);  // 之前时间戳为 1 的价格错误,价格更新为 3 。
                          // 时间戳为 [1,2] ,对应股票价格为 [3,5] 。
stockPrice.maximum();     // 返回 5 ,更正后最高价格为 5 。
stockPrice.update(4, 2);  // 时间戳为 [1,2,4] ,对应价格为 [3,5,2] 。
stockPrice.minimum();     // 返回 2 ,最低价格时间戳为 4 ,价格为 2 。
```

提示:
* $1 <= timestamp, price <= 10^9$
* `update`,`current`,`maximum` 和 `minimum` **总** 调用次数不超过 $10^5$
* `current`,`maximum` 和 `minimum` 被调用时,`update` 操作 **至少** 已经被调用过 一次 。

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

容易想到我们需要使用「哈希表」来记录 `{时间:价格}` 的映射关系。

**关于 `current` 操作,我们可以维护一个最大的时间戳 `cur`,在调用 `current` 的时候直接 $O(1)$ 查得结果。**

然后考虑解决 `update` 操作中对相同时间点的更新问题,我们可以使用 `TreeMap`(红黑树)来解决该问题。以 `{价格:该价格对应的时间点数量}` 的 `KV` 形式进行存储,`key` 按照「升序」进行排序。

然后对传入的 `timestamp` 是否已经被记录(是否已经存在哈希表中)进行分情况讨论:

* 传入的 `timestamp` 未被记录,直接更新哈希表和 `TreeMap`;
* 传入的 `timestamp` 已被记录,此时需要先从哈希表取出旧价格 `old`,然后用旧价格对 `TreeMap` 进行修改(如果该价格只有一个时间点,将该价格直接从 `TreeMap` 中移除;若有多个时间点,则对该价格对应的时间点数量进行减一操作),然后再使用传入的新价格 `price` 更新哈希表和 `TreeMap`。

**`minimum` 和 `maximum` 操作则只需要取得 `TreeMap` 的首尾 `Key` 即可。**

代码:
```Java
class StockPrice {
    int cur;
    Map<Integer, Integer> map = new HashMap<>();
    TreeMap<Integer, Integer> ts = new TreeMap<>();
    public void update(int timestamp, int price) {
        cur = Math.max(cur, timestamp);
        if (map.containsKey(timestamp)) {
            int old = map.get(timestamp);
            int cnt = ts.get(old);
            if (cnt == 1) ts.remove(old);
            else ts.put(old, cnt - 1);
        }
        map.put(timestamp, price);
        ts.put(price, ts.getOrDefault(price, 0) + 1);
    }

    public int current() {
        return map.get(cur);
    }

    public int maximum() {
        return ts.lastKey();
    }

    public int minimum() {
        return ts.firstKey();
    }
}
```
* 时间复杂度:令 $n$ 为最大调用次数,`update` 复杂度为 $O(\log{n})$;`current`、`maximum` 和 `minimum` 操作复杂度为 $O(1)$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


LeetCode/2031-2040/2038. 如果相邻两个颜色均相同则删除当前颜色（中等）.md

2038. 如果相邻两个颜色均相同则删除当前颜色

Question:
总共有 $n$ 个颜色片段排成一列,每个颜色片段要么是 `'A'` 要么是 `'B'` 。

给你一个长度为 $n$ 的字符串 `colors` ,其中 $colors[i]$ 表示第 $i$ 个颜色片段的颜色。

Alice 和 Bob 在玩一个游戏,他们轮流从这个字符串中删除颜色。Alice 先手 。

* 如果一个颜色片段为 `'A'` 且相邻两个颜色都是颜色 `'A'` ,那么 Alice 可以删除该颜色片段。Alice 不可以删除任何颜色 `'B'` 片段。
* 如果一个颜色片段为 `'B'` 且相邻两个颜色都是颜色 `'B'` ,那么 Bob 可以删除该颜色片段。Bob 不可以删除任何颜色 `'A'` 片段。
* Alice 和 Bob 不能从字符串两端删除颜色片段。
* 如果其中一人无法继续操作,则该玩家输掉游戏且另一玩家获胜。

假设 Alice 和 Bob 都采用最优策略,如果 Alice 获胜,请返回 true,否则 Bob 获胜,返回 false。

示例 1:
```
输入:colors = "AAABABB"

输出:true

解释:
AAABABB -> AABABB
Alice 先操作。
她删除从左数第二个 'A' ,这也是唯一一个相邻颜色片段都是 'A' 的 'A' 。

现在轮到 Bob 操作。
Bob 无法执行任何操作,因为没有相邻位置都是 'B' 的颜色片段 'B' 。
因此,Alice 获胜,返回 true 。
```
示例 2:
```
输入:colors = "AA"

输出:false

解释:
Alice 先操作。
只有 2 个 'A' 且它们都在字符串的两端,所以她无法执行任何操作。
因此,Bob 获胜,返回 false 。
```
示例 3:
```
输入:colors = "ABBBBBBBAAA"

输出:false

解释:
ABBBBBBBAAA -> ABBBBBBBAA
Alice 先操作。
她唯一的选择是删除从右数起第二个 'A' 。

ABBBBBBBAA -> ABBBBBBAA
接下来轮到 Bob 操作。
他有许多选择,他可以选择任何一个 'B' 删除。

然后轮到 Alice 操作,她无法删除任何片段。
所以 Bob 获胜,返回 false 。
```

提示:
* $1 <= colors.length <= 10^5$
* `colors` 只包含字母 `'A'` 和 `'B'`

Solution:
### 脑筋急转弯

根据删除规则,删除任意一个 `A` 不会影响可被删删除的 `B` 的数量,反之亦然。

因此直接统计「可删除的 `A` 的数量」和「可删除的 `B` 的数量」,分别记为 $a$ 和 $b$,比较 $a$ 和 $b$ 的大小即可得到答案(只有 $a > b$ 时,先手获胜)。

代码:
```Java
class Solution {
    public boolean winnerOfGame(String colors) {
        char[] cs = colors.toCharArray();
        int n = cs.length;
        int a = 0, b = 0;
        for (int i = 1; i < n - 1; i++) {
            if (cs[i] == 'A' && cs[i - 1] == 'A' && cs[i + 1] == 'A') a++;
            if (cs[i] == 'B' && cs[i - 1] == 'B' && cs[i + 1] == 'B') b++;
        }
        return a > b;
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:使用 `toCharArray` 操作会产生新数组,复杂度为 $O(n)$,而使用 `charAt` 代替的话复杂度为 $O(1)$

### 最后

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

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

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

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


2039. 网络空闲的时刻

Question:
给你一个有 $n$ 个服务器的计算机网络,服务器编号为 $0$ 到 $n - 1$ 。同时给你一个二维整数数组 `edges` ,其中 $edges[i] = [u_i, v_i]$ 表示服务器 $u_i$ 和 $v_i$ 之间有一条信息线路,在一秒内它们之间可以传输任意数目的信息。再给你一个长度为 $n$ 且下标从 $0$ 开始的整数数组 `patience` 。

题目保证所有服务器都是相通的,也就是说一个信息从任意服务器出发,都可以通过这些信息线路直接或间接地到达任何其他服务器。

编号为 $0$ 的服务器是主服务器,其他服务器为数据服务器。每个数据服务器都要向主服务器发送信息,并等待回复。信息在服务器之间按最优线路传输,也就是说每个信息都会以最少时间到达主服务器。主服务器会处理所有新到达的信息并立即按照每条信息来时的路线反方向发送回复信息。

在 $0$ 秒的开始,所有数据服务器都会发送各自需要处理的信息。从第 $1$ 秒开始,每一秒最开始时,每个数据服务器都会检查它是否收到了主服务器的回复信息(包括新发出信息的回复信息):

* 如果还没收到任何回复信息,那么该服务器会周期性重发信息。数据服务器 $i$ 每 $patience[i]$ 秒都会重发一条信息,也就是说,数据服务器 $i$ 在上一次发送信息给主服务器后的 $patience[i]$ 秒 后 会重发一条信息给主服务器。
* 否则,该数据服务器不会重发信息。

当没有任何信息在线路上传输或者到达某服务器时,该计算机网络变为空闲状态。

请返回计算机网络变为空闲状态的最早秒数。

示例 1:

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

输出:8

解释:
0 秒最开始时,
- 数据服务器 1 给主服务器发出信息(用 1A 表示)。
- 数据服务器 2 给主服务器发出信息(用 2A 表示)。

1 秒时,
- 信息 1A 到达主服务器,主服务器立刻处理信息 1A 并发出 1A 的回复信息。
- 数据服务器 1 还没收到任何回复。距离上次发出信息过去了 1 秒(1 < patience[1] = 2),所以不会重发信息。
- 数据服务器 2 还没收到任何回复。距离上次发出信息过去了 1 秒(1 == patience[2] = 1),所以它重发一条信息(用 2B 表示)。

2 秒时,
- 回复信息 1A 到达服务器 1 ,服务器 1 不会再重发信息。
- 信息 2A 到达主服务器,主服务器立刻处理信息 2A 并发出 2A 的回复信息。
- 服务器 2 重发一条信息(用 2C 表示)。
...
4 秒时,
- 回复信息 2A 到达服务器 2 ,服务器 2 不会再重发信息。
...
7 秒时,回复信息 2D 到达服务器 2 。

从第 8 秒开始,不再有任何信息在服务器之间传输,也不再有信息到达服务器。
所以第 8 秒是网络变空闲的最早时刻。
```
示例 2:

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

输出:3

解释:数据服务器 1 和 2 第 2 秒初收到回复信息。
从第 3 秒开始,网络变空闲。
```

提示:
* $n == patience.length$
* $2 <= n <= 10^5$
* $patience[0] == 0$
* 对于 $1 <= i < n$ ,满足 $1 <= patience[i] <= 10^5$
* $1 <= edges.length <= \min(105, n * (n - 1) / 2)$
* $edges[i].length == 2$
* $0 <= ui, vi < n$
* $ui != vi$
* 不会有重边。
* 每个服务器都直接或间接与别的服务器相连。

Solution:
### 建图 + BFS

根据题目可知这是一个边权为 $1$ 的无向连通图,我们可以采用「邻接表建图 + BFS」的方式预处理出 $dist$ 数组,$dist[i]$ 含义为节点 $i$ 到 $0$ 号点的最短距离。

一个数据服务器 $i$ 往主服务器发送消息所消耗的时间为两节点之间的最短路径 $dist[i]$,而从发送消息到收到回复所需的时间为 $di = 2 * dist[i]$。

同时每个数据服务器还存在时间间隔为 $t = patience[i]$ 的重发动作,并且动作只有在第一次收到主服务的回复后才会停止。

因此如果 $di <= t$,那么数据服务器不会发生重发动作,该节点活动停止时间点为 $di$;当 $di > t$,数据服务器将会发生重发动作,且最后一个数据包的发送时间为 $(di - 1) / t * t$,只有当最后一个数据包收到回复,该节点的所有活动才算停止,停止时间点为 $(di - 1) / t * t + di$。所有节点的活动停止时间点的最大值即是答案。

> 一些细节:老规矩,为了避免每个样例都 `new` 大数组,我们使用 `static` 优化。使用 `static` 优化后,你甚至会收获一个稳定 $100$% 的结果 🤣

代码:
```Java
class Solution {
    static int N = 100010, M = N * 2, INF = 0x3f3f3f3f;
    static int[] he = new int[N], e = new int[M], ne = new int[M];
    static int[] dist = new int[N];
    int idx;
    void add(int a, int b) {
        e[idx] = b;
        ne[idx] = he[a];
        he[a] = idx++;
    }
    public int networkBecomesIdle(int[][] edges, int[] patience) {
        Arrays.fill(he, -1);
        Arrays.fill(dist, INF);
        int n = patience.length;
        for (int[] e : edges) {
            add(e[0], e[1]);
            add(e[1], e[0]);
        }
        Deque<Integer> d = new ArrayDeque<>();
        d.addLast(0);
        dist[0] = 0;
        while (!d.isEmpty()) {
            int t = d.pollFirst();
            for (int i = he[t]; i != -1; i = ne[i]) {
                int j = e[i];
                if (dist[j] != INF) continue;
                dist[j] = dist[t] + 1;
                d.addLast(j);
            }
        }
        int ans = 0;
        for (int i = 1; i < n; i++) {
            int di = dist[i] * 2, t = patience[i];
            int cur = di <= t ? di : (di - 1) / t * t + di;
            if (cur > ans) ans = cur;
        }
        return ans + 1;
    }
}
```
* 时间复杂度:$O(n + m)$
* 空间复杂度:$O(n + m)$

### 最后

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

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

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

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


2043. 简易银行系统

Question:
你的任务是为一个很受欢迎的银行设计一款程序,以自动化执行所有传入的交易(转账,存款和取款)。

银行共有 $n$ 个账户,编号从 $1$ 到 $n$ 。每个账号的初始余额存储在一个下标从 $0$ 开始的整数数组 `balance` 中,其中第 $(i + 1)$ 个账户的初始余额是 $balance[i]$ 。

请你执行所有 **有效的** 交易。如果满足下面全部条件,则交易有效 :
* 指定的账户数量在 $1$ 和 $n$ 之间,且
* 取款或者转账需要的钱的总数 小于或者等于 账户余额。

实现 `Bank` 类:
* `Bank(long[] balance)` 使用下标从 $0$ 开始的整数数组 `balance` 初始化该对象。
* `boolean transfer(int account1, int account2, long money)` 从编号为 `account1` 的账户向编号为 `account2` 的账户转帐 `money` 美元。如果交易成功,返回 `true` ,否则,返回 `false` 。
* `boolean deposit(int account, long money)` 向编号为 `account` 的账户存款 `money` 美元。如果交易成功,返回 `true` ;否则,返回 `false` 。
* `boolean withdraw(int account, long money)` 从编号为 `account` 的账户取款 `money` 美元。如果交易成功,返回 `true` ;否则,返回 `false` 。

示例:
```
输入:
["Bank", "withdraw", "transfer", "deposit", "transfer", "withdraw"]
[[[10, 100, 20, 50, 30]], [3, 10], [5, 1, 20], [5, 20], [3, 4, 15], [10, 50]]

输出:
[null, true, true, true, false, false]

解释:
Bank bank = new Bank([10, 100, 20, 50, 30]);
bank.withdraw(3, 10);    // 返回 true ,账户 3 的余额是 $20 ,所以可以取款 $10 。
                         // 账户 3 余额为 $20 - $10 = $10 。
bank.transfer(5, 1, 20); // 返回 true ,账户 5 的余额是 $30 ,所以可以转账 $20 。
                         // 账户 5 的余额为 $30 - $20 = $10 ,账户 1 的余额为 $10 + $20 = $30 。
bank.deposit(5, 20);     // 返回 true ,可以向账户 5 存款 $20 。
                         // 账户 5 的余额为 $10 + $20 = $30 。
bank.transfer(3, 4, 15); // 返回 false ,账户 3 的当前余额是 $10 。
                         // 所以无法转账 $15 。
bank.withdraw(10, 50);   // 返回 false ,交易无效,因为账户 10 并不存在。
```

提示:
* $n == balance.length$
* $1 <= n, account, account_1, account_2 <= 10^5$
* $0 <= balance[i], money <= 10^{12}$
* `transfer`, `deposit`, `withdraw` 三个函数,每个 最多调用 $10^4$ 次

Solution:
### 模拟

根据题意进行模拟即可。

代码:
```Java
class Bank {
    long[] val;
    public Bank(long[] balance) {
        val = balance;
    }

    boolean check(int account) {
        return 1 <= account && account <= val.length;
    }

    public boolean transfer(int a, int b, long c) {
        if (!check(a) || !check(b)) return false;
        if (val[a - 1] >= c) {
            val[a - 1] -= c; val[b - 1] += c;
            return true;
        }
        return false;
    }

    public boolean deposit(int a, long c) {
        if (!check(a)) return false;
        val[a - 1] += c;
        return true;
    }

    public boolean withdraw(int a, long c) {
        if (!check(a)) return false;
        if (val[a - 1] >= c) {
            val[a - 1] -= c;
            return true;
        }
        return false;
    }
}
```
* 时间复杂度:$O(1)$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


LeetCode/2041-2050/2044. 统计按位或能得到最大值的子集数目（中等）.md

2044. 统计按位或能得到最大值的子集数目

Question:
给你一个整数数组 $nums$ ,请你找出 $nums$ 子集 **按位或** 可能得到的 **最大值** ,并返回按位或能得到最大值的 **不同非空子集的数目** 。

如果数组 $a$ 可以由数组 $b$ 删除一些元素(或不删除)得到,则认为数组 $a$ 是数组 $b$ 的一个 子集 。如果选中的元素下标位置不一样,则认为两个子集 不同 。

对数组 $a$ 执行 **按位或** ,结果等于 $a[0]$ `OR` $a[1]$ `OR` `...` `OR` $a[a.length - 1]$(下标从 $0$ 开始)。

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

输出:2

解释:子集按位或能得到的最大值是 3 。有 2 个子集按位或可以得到 3 :
- [3]
- [3,1]
```
示例 2:
```
输入:nums = [2,2,2]

输出:7

解释:[2,2,2] 的所有非空子集的按位或都可以得到 2 。总共有 23 - 1 = 7 个子集。
```
示例 3:
```
输入:nums = [3,2,1,5]

输出:6

解释:子集按位或可能的最大值是 7 。有 6 个子集按位或可以得到 7 :
- [3,5]
- [3,1,5]
- [3,2,5]
- [3,2,1,5]
- [2,5]
- [2,1,5]
```

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

Solution:
### 二进制枚举

令 $n$ 为 $nums$ 的长度,利用 $n$ 不超过 $16$,我们可以使用一个 `int` 数值来代指 $nums$ 的使用情况(子集状态)。

假设当前子集状态为 $state$,$state$ 为一个仅考虑低 $n$ 位的二进制数,当第 $k$ 位为 $1$,代表 $nums[k]$ 参与到当前的按位或运算,当第 $k$ 位为 $0$,代表 $nums[i]$ 不参与到当前的按位或运算。

在枚举这 $2^n$ 个状态过程中,我们使用变量 `max` 记录最大的按位或得分,使用 `ans` 记录能够取得最大得分的状态数量。

代码:
```Java
class Solution {
    public int countMaxOrSubsets(int[] nums) {
        int n = nums.length, mask = 1 << n;
        int max = 0, ans = 0;
        for (int s = 0; s < mask; s++) {
            int cur = 0;
            for (int i = 0; i < n; i++) {
                if (((s >> i) & 1) == 1) cur |= nums[i];
            }
            if (cur > max) {
                max = cur; ans = 1;
            } else if (cur == max) {
                ans++;
            }
        }
        return ans;
    }
}
```
* 时间复杂度:令 $nums$ 长度为 $n$,共有 $2^n$ 个子集状态,计算每个状态的按位或得分的复杂度为 $O(n)$。整体复杂度为 $O(2^n * n)$
* 空间复杂度:$O(1)$

---

### 状压 DP

为了优化解法一中「每次都要计算某个子集的得分」这一操作,我们可以将所有状态的得分记下来,采用「动态规划」思想进行优化。

需要找到当前状态 $state$ 可由哪些状态转移而来:假设当前 $state$ 中处于最低位的 $1$ 位于第 $idx$ 位,首先我们可以使用 `lowbit` 操作得到「仅保留第 $idx$ 的 $1$ 所对应的数值」,记为 $lowbit$,那么显然对应的状态方程为:

$$
f[state] = f[state - lowbit] \wedge nums[idx]
$$

再配合我们从小到大枚举所有的 $state$ 即可确保计算 $f[state]$ 时所依赖的 $f[state - lowbit]$ 已被计算。

最后为了快速知道数值 $lowbit$ 最低位 $1$ 所处于第几位(也就是 $idx$ 为何值),我们可以利用 $nums$ 长度最多不超过 $16$ 来进行「打表」预处理。

代码:
```Java
class Solution {
    static Map<Integer, Integer> map = new HashMap<>();
    static {
        for (int i = 0; i < 20; i++) map.put((1 << i), i);
    }
    public int countMaxOrSubsets(int[] nums) {
        int n = nums.length, mask = 1 << n;
        int[] f = new int[mask];
        int max = 0, ans = 0;
        for (int s = 1; s < mask; s++) {
            int lowbit = (s & -s);
            int prev = s - lowbit, idx = map.get(lowbit);
            f[s] = f[prev] | nums[idx];
            if (f[s] > max) {
                max = f[s]; ans = 1;
            } else if (f[s] == max) {
                ans++;
            }
        }
        return ans;
    }
}
```
* 时间复杂度:$O(2^n)$
* 空间复杂度:$O(2^n)$

---

### DFS

解法一将「枚举子集/状态」&「计算状态对应的得分」两个过程分开进行,导致了复杂度上界为 $O(2^n * n)$。

事实上,我们可以在「枚举子集」的同时「计算相应得分」,设计 `void dfs(int u, int val)` 的 `DFS`  函数来实现「爆搜」,其中 $u$ 为当前的搜索到 $nums$ 的第几位,$val$ 为当前的得分情况。

对于任意一位 $x$ 而言,都有「选」和「不选」两种选择,分别对应了 `dfs(u + 1, val | nums[x])` 和 `dfs(u + 1, val)` 两条搜索路径,在搜索所有状态过程中,使用全局变量 `max` 和 `ans` 来记录「最大得分」以及「取得最大得分的状态数量」。

该做法将多条「具有相同前缀」的搜索路径的公共计算部分进行了复用,从而将算法复杂度下降为 $O(2^n)$。

代码:
```Java
class Solution {
    int[] nums;
    int max = 0, ans = 0;
    public int countMaxOrSubsets(int[] _nums) {
        nums = _nums;
        dfs(0, 0);
        return ans;
    }
    void dfs(int u, int val) {
        if (u == nums.length) {
            if (val > max) {
                max = val; ans = 1;
            } else if (val == max) {
                ans++;
            }
            return ;
        }
        dfs(u + 1, val);
        dfs(u + 1, val | nums[u]);
    }
}
```
* 时间复杂度:令 $nums$ 长度为 $n$,共有 $2^n$ 个子集状态。整体复杂度为 $O(2^n)$
* 空间复杂度:忽略递归带来的额外空间开销,复杂度为 $O(1)$

### 最后

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

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

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

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