LeetCode/2301-2310/2304. 网格中的最小路径代价（中等）.md

2304. 网格中的最小路径代价

Question:
给你一个下标从 `0` 开始的整数矩阵 `grid`,矩阵大小为 `m x n`,由从 `0` 到 $m \times n - 1$ 的不同整数组成。

你可以在此矩阵中,从一个单元格移动到下一行的任何其他单元格。

如果你位于单元格 $(x, y)$ ,且满足 $x < m - 1$,你可以移动到 $(x + 1, 0)$, $(x + 1, 1)$, ..., $(x + 1, n - 1)$ 中的任何一个单元格。注意: 在最后一行中的单元格不能触发移动。

每次可能的移动都需要付出对应的代价,代价用一个下标从 `0` 开始的二维数组 `moveCost` 表示,该数组大小为 $(m \times n) \times n$ ,其中 `moveCost[i][j]` 是从值为 `i` 的单元格移动到下一行第 `j` 列单元格的代价。从 `grid` 最后一行的单元格移动的代价可以忽略。

`grid` 一条路径的代价是:所有路径经过的单元格的值之和加上所有移动的代价之和 。从第一行任意单元格出发,返回到达最后一行任意单元格的最小路径代价。

示例 1:

```
输入:grid = [[5,3],[4,0],[2,1]], moveCost = [[9,8],[1,5],[10,12],[18,6],[2,4],[14,3]]

输出:17

解释:最小代价的路径是 5 -> 0 -> 1 。
- 路径途经单元格值之和 5 + 0 + 1 = 6 。
- 从 5 移动到 0 的代价为 3 。
- 从 0 移动到 1 的代价为 8 。
路径总代价为 6 + 3 + 8 = 17 。
```
示例 2:
```
输入:grid = [[5,1,2],[4,0,3]], moveCost = [[12,10,15],[20,23,8],[21,7,1],[8,1,13],[9,10,25],[5,3,2]]

输出:6

解释:
最小代价的路径是 2 -> 3 。
- 路径途经单元格值之和 2 + 3 = 5 。
- 从 2 移动到 3 的代价为 1 。
路径总代价为 5 + 1 = 6 。
```

提示:
* $m = grid.length$
* $n = grid[i].length$
* $2 <= m, n <= 50$
* `grid` 由从 `0` 到 `m * n - 1` 的不同整数组成
* $moveCost.length = m \times n$
* $moveCost[i].length = n$
* $1 <= moveCost[i][j] <= 100$

Solution:
### 建新图 + 建虚拟点 + 堆优化 Dijkstra

> 注意:可以直接使用解法二的方法,但先认真看完本做法,再去看解法二,会有相当丝滑的体验。

每次移动,**实际路径权值 = 经过边的权值 + 目的地的权值**。

利用原图,构建新图:**每个单元格视为一个点,除最后一行外,每个点对下一行的所有点连一条有向边,边权 = 原图中该边的权值 + 原图中该目的地的权值**。

分析新图中的点边数量:

* 点:共 $m \times n$ 个点,数量为 $2.5 \times 10^3$
* 边:不算最后一行,共 $(m - 1) \times n$ 个点,这些点与下一行的每个点均有一条有向边,合计 $(m - 1) \times n^2$ 条边,数量为 $1.25 \times 10^5$

原问题转换为:求点 $i$ 到 $j$ 的最短路,其中点 $i$ 所在位置为第 $0$ 行,点 $j$ 所在位置为第 $m - 1$ 行。

这似乎是一个「多源汇最短路」问题?但求解多源汇最短路的 `Floyd` 算法是 $O(n^3)$ 的,会超时。

实际上,我们也并不真的关心图中任意点之间的最短路,仅仅关心第一行到最后一行的最短路。

因此,**我们可通过建立“虚拟源点”和“虚拟汇点”的方式,来将“多源汇最短路”问题转换为“单源最短路”问题。**

具体的,我们创建一个“虚拟源点”,该点向所有第一行的点连权值为 $grid[0][i]$ 的有向边;同时创建一个“虚拟汇点”,最后一行的所有点向该点连权值为 $0$ 的有向边。

问题进一步转化为:求“虚拟源点”到“虚拟汇点”的最短路。

至此,我们通过 **建新图 -> 创建虚拟源汇点(转换为单源最短路)-> 套用单源最短路算法** 解决本题。

将新图中点的数量记为 $n$,边数记为 $m$,朴素 `Dijkstra` 复杂度为 $O(n^2)$,堆优化的 `Dijkstra` 的复杂度为 $O(m\log{n})$,当 $m < n^2$ (相对稀疏)时,优先使用堆优化 `Dijkstra`。

Java 代码:
```Java
class Solution {
    int N = 50 * 50 + 2, M = 50 * 50 * 50, idx = 0, n;
    int[] he = new int[N], e = new int[M], ne = new int[M], w = new int[M];
    void add(int a, int b, int c) {
        e[idx] = b;
        ne[idx] = he[a];
        w[idx] = c;
        he[a] = idx++;
    }
    public int minPathCost(int[][] grid, int[][] moveCost) {
        int N = grid.length, M = grid[0].length;
        int S = N * M, T = S + 1;
        n = N * M + 2;
        Arrays.fill(he, -1);
        //「虚拟源点」向「第一行」进行连边
        for (int i = 0; i < M; i++) add(S, grid[0][i], grid[0][i]);
        // 转换原图
        for (int i = 0; i < N - 1; i++) {
            for (int j = 0; j < M; j++) {
                int a = grid[i][j];
                for (int k = 0; k < M; k++) {
                    int b = grid[i + 1][k];
                    add(a, b, moveCost[a][k] + b);
                }
            }
        }
        //「最后一行」向「虚拟汇点」进行连边
        for (int i = 0; i < M; i++) add(grid[N - 1][i], T, 0);
        // 最短路
        int[] dist = dijkstra(S);
        return dist[T];
    }
    int[] dijkstra(int x) {
        // 起始先将所有的点标记为「未更新」和「距离为正无穷」
        int[] dist = new int[n];
        Arrays.fill(dist, 0x3f3f3f3f);
        boolean[] vis = new boolean[n];
        dist[x] = 0;
        // 使用「优先队列」存储所有可用于更新的点
        // 以 (点编号, 到起点的距离) 进行存储,优先弹出「最短距离」较小的点
        PriorityQueue<int[]> q = new PriorityQueue<>((a,b)->a[1]-b[1]);
        q.add(new int[]{x, 0});
        while (!q.isEmpty()) {
            // 每次从「优先队列」中弹出
            int[] poll = q.poll();
            int u = poll[0], step = poll[1];
            // 如果弹出的点被标记「已更新」,则跳过
            if (vis[u]) continue;
            // 标记该点「已更新」,并使用该点更新其他点的「最短距离」
            vis[u] = true;
            for (int i = he[u]; i != -1; i = ne[i]) {
                int j = e[i];
                if (dist[j] <= dist[u] + w[i]) continue;
                dist[j] = dist[u] + w[i];
                q.add(new int[]{j, dist[j]});
            }
        }
        return dist;
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    static const int N = 50 * 50 + 2, M = 50 * 50 * 50;
    int he[N], e[M], ne[M], w[M], idx, n, INF = 0x3f3f3f3f;
    void add(int a, int b, int c) {
        e[idx] = b;
        ne[idx] = he[a];
        w[idx] = c;
        he[a] = idx++;
    }
    int minPathCost(vector<vector<int>>& grid, vector<vector<int>>& moveCost) {
        int N = grid.size(), M = grid[0].size();
        int S = N * M, T = S + 1;
        n = N * M + 2;
        fill(he, he + n, -1);
        //「虚拟源点」向「第一行」进行连边
        for (int i = 0; i < M; i++) add(S, grid[0][i], grid[0][i]);
        // 转换原图
        for (int i = 0; i < N - 1; i++) {
            for (int j = 0; j < M; j++) {
                int a = grid[i][j];
                for (int k = 0; k < M; k++) {
                    int b = grid[i + 1][k];
                    add(a, b, moveCost[a][k] + b);
                }
            }
        }
        //「最后一行」向「虚拟汇点」进行连边
        for (int i = 0; i < M; i++) add(grid[N - 1][i], T, 0);
        // 最短路
        vector<int> dist = dijkstra(S);
        return dist[T];
    }
    vector<int> dijkstra(int x) {
        vector<int> dist(n, 0x3f3f3f3f);
        vector<bool> vis(n, false);
        dist[x] = 0;
        // 使用「优先队列」存储所有可用于更新的点
        // 以 (到起点的距离, 点编号) 进行存储,优先弹出「最短距离」较小的点
        priority_queue<pair<int, int>, vector<pair<int, int>>, greater<pair<int, int>>> q;
        q.push({0, x});
        while (!q.empty()) {
            // 每次从「优先队列」中弹出
            auto [step, u] = q.top();
            q.pop();
            // 如果弹出的点被标记「已更新」,则跳过
            if (vis[u]) continue;
            // 标记该点「已更新」,并使用该点更新其他点的「最短距离」
            vis[u] = true;
            for (int i = he[u]; i != -1; i = ne[i]) {
                int j = e[i];
                if (dist[j] <= dist[u] + w[i]) continue;
                dist[j] = dist[u] + w[i];
                q.push({dist[j], j});
            }
        }
        return dist;
    }
};
```
Python 代码:
```Python
import heapq

class Solution:
    def minPathCost(self, grid, moveCost):
        N, M = len(grid), len(grid[0])
        S, T = N * M, N * M + 1
        n = N * M + 2
        he = [-1] * n
        e, ne, w = [-1] * (50 * 50 * 50), [-1] * (50 * 50 * 50), [-1] * (50 * 50 * 50)
        idx = 0

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

        def dijkstra(x):
            dist = [float('inf')] * n
            vis = [False] * n
            dist[x] = 0
            # 使用「优先队列」存储所有可用于更新的点
            # 以 (到起点的距离, 点编号) 进行存储,优先弹出「最短距离」较小的点
            q = [(0, x)]
            heapq.heapify(q)
            while q:
                # 每次从「优先队列」中弹出
                step, u = heapq.heappop(q)
                # 如果弹出的点被标记「已更新」,则跳过
                if vis[u]: continue
                # 标记该点「已更新」,并使用该点更新其他点的「最短距离」
                vis[u] = True
                i = he[u]
                while i != -1:
                    j, c = e[i], w[i]
                    i = ne[i]
                    if dist[j] <= dist[u] + c: continue
                    dist[j] = dist[u] + c
                    heapq.heappush(q, (dist[j], j))
            return dist

        #「虚拟源点」向「第一行」进行连边
        for i in range(M):
            add(S, grid[0][i], grid[0][i])
        # 转换原图
        for i in range(N - 1):
            for j in range(M):
                a = grid[i][j]
                for k in range(M):
                    b = grid[i + 1][k]
                    add(a, b, moveCost[a][k] + b)
        #「最后一行」向「虚拟汇点」进行连边
        for i in range(M):
            add(grid[N - 1][i], T, 0)
        # 最短路
        dist = dijkstra(S)
        return dist[T]
```
* 时间复杂度:$O(m\log{n})$,其中 $n$ 为新图中的点数 $N \times M = 2.5 \times 10^3$,$m$ 为新图中的边数 $(M - 1) \times N^2 = 1.25 \times 10^5$
* 空间复杂度:$O(n + m)$

---

### 堆优化 Dijkstra

什么?你说你实在不想建新图,也不想搞什么虚拟点,就想用你心爱的 `BFS` 来做?!

我懂你意思,但那不叫 `BFS`。

只是将「建新图」和「建虚拟点」的过程省掉,仍需要使用优先队列(堆)来每次取出当前“路径代价最小”的点来进行扩充,执行过程仍为堆优化 `Dijkstra` 的核心操作。

尤其所谓“省掉” 建新图 和 建虚拟点,真就字面上的“省掉”,并非不存在,因为两种做法思路是完全一致的。可简单列举「本解法」与「解法一」的对应关系:

* 起始往队列放入首行元素,对应了解法一的“建立虚拟源点”过程;
* 从队列中取元素出来扩充时,若当前元素所在行是最后一行时,用当前路径代价来更新答案,对应了解法一的“建立虚拟汇点”过程;
* 扩充时直接遍历列(即下一行的所有点),对应的解法一的“用原图边建新图”的过程。

Java 代码:

```Java
class Solution {
    public int minPathCost(int[][] grid, int[][] moveCost) {
        int m = grid.length, n = grid[0].length, INF = 0x3f3f3f3f, ans = INF;
        int[][] dist = new int[m][n];
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) dist[i][j] = INF;
        }
        PriorityQueue<int[]> d = new PriorityQueue<>((a,b)->a[2]-b[2]);
        for (int i = 0; i < n; i++) {
            d.add(new int[]{0, i, grid[0][i]});
            dist[0][i] = grid[0][i];
        }
        while (!d.isEmpty()) {
            int[] info = d.poll();
            int x = info[0], y = info[1], cur = info[2];
            if (x == m - 1) {
                ans = Math.min(ans, cur);
                continue;
            }
            for (int i = 0; i < n; i++) {
                int step = moveCost[grid[x][y]][i], ne = grid[x + 1][i];
                int tot = cur + step + ne;
                if (tot >= ans || dist[x + 1][i] <= tot) continue;
                dist[x + 1][i] = tot;
                d.add(new int[]{x + 1, i, tot});
            }
        }
        return ans;
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    int minPathCost(vector<vector<int>>& grid, vector<vector<int>>& moveCost) {
        int m = grid.size(), n = grid[0].size(), INF = 0x3f3f3f3f, ans = INF;
        vector<vector<int>> dist(m, vector<int>(n, INF));
        priority_queue<vector<int>, vector<vector<int>>, greater<vector<int>>> pq;
        for (int i = 0; i < n; i++) {
            pq.push({0, i, grid[0][i]});
            dist[0][i] = grid[0][i];
        }
        while (!pq.empty()) {
            vector<int> info = pq.top();
            pq.pop();
            int x = info[0], y = info[1], cur = info[2];
            if (x == m - 1) {
                ans = min(ans, cur);
                continue;
            }
            for (int i = 0; i < n; i++) {
                int step = moveCost[grid[x][y]][i], ne = grid[x + 1][i];
                int tot = cur + step + ne;
                if (tot >= ans || dist[x + 1][i] <= tot) continue;
                dist[x + 1][i] = tot;
                pq.push({x + 1, i, tot});
            }
        }
        return ans;
    }
};
```
Python 代码:
```Python
class Solution:
    def minPathCost(self, grid, moveCost):
        m, n, INF = len(grid), len(grid[0]), float('inf')
        ans = INF
        dist = [[INF] * n for _ in range(m)]
        for i in range(n):
            dist[0][i] = grid[0][i]
        pq = [(0, i, grid[0][i]) for i in range(n)]
        while pq:
            x, y, cur = heapq.heappop(pq)
            if x == m - 1:
                ans = min(ans, cur)
                continue
            for i in range(n):
                step, ne = moveCost[grid[x][y]][i], grid[x + 1][i]
                tot = cur + step + ne
                if tot >= ans or dist[x + 1][i] <= tot: continue
                dist[x + 1][i] = tot
                heapq.heappush(pq, (x + 1, i, tot))
        return ans
```
* 时间复杂度:$O(m\log{n})$,其中 $n$ 为新图中的点数 $N \times M = 2.5 \times 10^3$,$m$ 为新图中的边数 $(M - 1) \times N^2 = 1.25 \times 10^5$
* 空间复杂度:$O(n + m)$

---

### 原地模拟

什么?你说你连图论的方法都不想用,想就着题意做一遍?

可以。甚至当你调整更新方向,还能利用已有的 `grid`,实现原地模拟。

具体的,我们将“从上往下走”调整为“从下往上走”,这样可以确保当我们使用底下一行 $grid[i + 1][j]$ 来更新当前行 $grid[i][j]$ 时,所用到的 $grid[i + 1][j]$ 不会被覆盖。

Java 代码:
```Java
class Solution {
    public int minPathCost(int[][] grid, int[][] moveCost) {
        int m = grid.length, n = grid[0].length, INF = 0x3f3f3f3f, ans = INF;
        for (int i = m - 2; i >= 0; i--) {
            for (int j = 0; j < n; j++) {
                int cur = INF;
                for (int k = 0; k < n; k++) cur = Math.min(cur, grid[i + 1][k] + moveCost[grid[i][j]][k]);
                grid[i][j] += cur;
            }
        }
        for (int i = 0; i < n; i++) ans = Math.min(ans, grid[0][i]);
        return ans;
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    int minPathCost(vector<vector<int>>& grid, vector<vector<int>>& moveCost) {
        int m = grid.size(), n = grid[0].size(), INF = INT_MAX, ans = INF;
        for (int i = m - 2; i >= 0; i--) {
            for (int j = 0; j < n; j++) {
                int cur = INF;
                for (int k = 0; k < n; k++) cur = min(cur, grid[i + 1][k] + moveCost[grid[i][j]][k]);
                grid[i][j] += cur;
            }
        }
        for (int i = 0; i < n; i++) ans = min(ans, grid[0][i]);
        return ans;
    }
};
```
Python 代码:
```Python
class Solution:
    def minPathCost(self, grid, moveCost):
        m, n = len(grid), len(grid[0])
        for i in range(m - 2, -1, -1):
            for j in range(n):
                grid[i][j] += min([grid[i + 1][k] + moveCost[grid[i][j]][k] for k in range(n)])
        return min([grid[0][i] for i in range(n)])
```
TypeScript 代码:
```TypeScript
function minPathCost(grid: number[][], moveCost: number[][]): number {
    let m = grid.length, n = grid[0].length, INF = 0x3f3f3f3f, ans = INF;
    for (let i = m - 2; i >= 0; i--) {
        for (let j = 0; j < n; j++) {
            let cur = INF;
            for (let k = 0; k < n; k++) cur = Math.min(cur, grid[i + 1][k] + moveCost[grid[i][j]][k]);
            grid[i][j] += cur;
        }
    }
    for (let i = 0; i < n; i++) ans = Math.min(ans, grid[0][i]);
    return ans;
};
```
* 时间复杂度:$O(m \times n^2)$,其中 $m$ 和 $n$ 分别代表给定 `grid` 的长宽
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


2305. 公平分发饼干

Question:
给你一个整数数组 `cookies`,其中 `cookies[i]` 表示在第 `i` 个零食包中的饼干数量。另给你一个整数 `k` 表示等待分发零食包的孩子数量,所有零食包都需要分发。

在同一个零食包中的所有饼干都必须分发给同一个孩子,不能分开。

分发的不公平程度定义为单个孩子在分发过程中能够获得饼干的最大总数。

返回所有分发的最小不公平程度。

示例 1:
```
输入:cookies = [8,15,10,20,8], k = 2

输出:31

解释:一种最优方案是 [8,15,8] 和 [10,20] 。
- 第 1 个孩子分到 [8,15,8] ,总计 8 + 15 + 8 = 31 块饼干。
- 第 2 个孩子分到 [10,20] ,总计 10 + 20 = 30 块饼干。
分发的不公平程度为 max(31,30) = 31 。
可以证明不存在不公平程度小于 31 的分发方案。
```
示例 2:
```
输入:cookies = [6,1,3,2,2,4,1,2], k = 3

输出:7

解释:一种最优方案是 [6,1]、[3,2,2] 和 [4,1,2] 。
- 第 1 个孩子分到 [6,1] ,总计 6 + 1 = 7 块饼干。
- 第 2 个孩子分到 [3,2,2] ,总计 3 + 2 + 2 = 7 块饼干。
- 第 3 个孩子分到 [4,1,2] ,总计 4 + 1 + 2 = 7 块饼干。
分发的不公平程度为 max(7,7,7) = 7 。
可以证明不存在不公平程度小于 7 的分发方案。
```

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

Solution:
### 状压 DP

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

**定义 $f[i][s]$ 为考虑前 $i$ 个人,对 `cs` 的分配情况为 `s` 时的最小不公平程度**。

其中 `s` 为一个二进制数,若 `s` 的第 `i` 位为 `1` 代表 `cs[i]` 已被分配,反之代表未分配。同时我们可以预处理 `g` 数组,`g[s] = t` 含义为选择 `cs` 状态为 `s` 时得到的饼干总和为 `t`。

初始化只有 $f[0][0] = 0$,其余均为正无穷 `0x3f3f3f3f`。

不失一般性考虑 $f[i][s]$ 该如何计算,**通过枚举第 $i$ 个小朋友所分配到的饼干情况 `p`(`s` 的子集)进行转移**:若第 $i$ 个小朋友分配到的饼干情况为 `p`,则此前 $i - 1$ 个小朋友分配到饼干情况为 $s - p$,前 $i - 1$ 个小朋友的最小不公平程度为 $f[i - 1][s - p]$,当前第 $i$ 个小朋友的不公平程度为 $g[p]$,两者中最大值可用于更新 $f[i][s]$。

$$
f[i][s] = \min(f[i][s], \max(f[i - 1][s - p], g[p])), p \subseteq s
$$

最终 $f[k][2^n - 1]$ 即是答案。

Java 代码:
```Java
class Solution {
    public int distributeCookies(int[] cs, int k) {
        int n = cs.length, mask = 1 << n, INF = 0x3f3f3f3f;
        int[] g = new int[mask];
        for (int s = 0; s < mask; s++) {
            int t = 0;
            for (int i = 0; i < n; i++) t += ((s >> i) & 1) == 1 ? cs[i] : 0;
            g[s] = t;
        }
        int[][] f = new int[k + 10][mask];
        for (int i = 0; i <= k; i++) Arrays.fill(f[i], INF);
        f[0][0] = 0;
        for (int i = 1; i <= k; i++) {
            for (int s = 0; s < mask; s++) {
                for (int p = s; p != 0; p = (p - 1) & s) {
                    f[i][s] = Math.min(f[i][s], Math.max(f[i - 1][s - p], g[p]));
                }
            }
        }
        return f[k][mask - 1];
    }
}
```
Python 代码:
```Python
class Solution:
    def distributeCookies(self, cs: List[int], k: int) -> int:
        n, mask, INF = len(cs), 1 << len(cs), 0x3f3f3f3f
        g = [0] * mask
        for s in range(mask):
            t = 0
            for i in range(n):
                t += cs[i] if (s >> i) & 1 == 1 else 0
            g[s] = t
        f = [[INF] * mask for _ in range(k + 10)]
        f[0][0] = 0
        for i in range(1, k + 1):
            for s in range(mask):
                p = s
                while p != 0:
                    f[i][s] = min(f[i][s], max(f[i - 1][s - p], g[p]))
                    p = (p - 1) & s
        return f[k][mask - 1]
```
C++ 代码:
```C++
class Solution {
public:
    int distributeCookies(vector<int>& cs, int k) {
        int n = cs.size(), mask = 1 << n, INF = 0x3f3f3f3f;
        vector<int> g(mask, 0);
        for (int s = 0; s < mask; s++) {
            int t = 0;
            for (int i = 0; i < n; i++) t += ((s >> i) & 1) == 1 ? cs[i] : 0;
            g[s] = t;
        }
        vector<vector<int>> f(k + 10, vector<int>(mask, INF));
        for (int i = 0; i <= k; i++) fill(f[i].begin(), f[i].end(), INF);
        f[0][0] = 0;
        for (int i = 1; i <= k; i++) {
            for (int s = 0; s < mask; s++) {
                for (int p = s; p != 0; p = (p - 1) & s) {
                    f[i][s] = min(f[i][s], max(f[i - 1][s - p], g[p]));
                }
            }
        }
        return f[k][mask - 1];
    }
};
```
TypeScirpt 代码:
```TypeScript
function distributeCookies(cs: number[], k: number): number {
    const n = cs.length, mask = 1 << n, INF = 0x3f3f3f3f;
    const g = new Array(mask).fill(0);
    for (let s = 0; s < mask; s++) {
        let t = 0;
        for (let i = 0; i < n; i++) t += ((s >> i) & 1) === 1 ? cs[i] : 0;
        g[s] = t;
    }
    const f = new Array(k + 10).fill(0).map(() => new Array(mask).fill(INF));
    f[0][0] = 0;
    for (let i = 1; i <= k; i++) {
        for (let s = 0; s < mask; s++) {
            for (let p = s; p != 0; p = (p - 1) & s) {
                f[i][s] = Math.min(f[i][s], Math.max(f[i - 1][s - p], g[p]));
            }
        }
    }
    return f[k][mask - 1];
};
```
* 时间复杂度:将 `cs` 长度记为 $n$,状态数量记为 $m = 2^n$,预处理复杂度为 $O(n \times m)$;`DP` 过程需要枚举二进制长度为 $n$ 的所有子集的子集,复杂度为 $O(3^n)$,`DP` 过程复杂度为 $O(k \times 3^n)$。整体复杂度为 $O(k \times 3^n)$
* 空间复杂度:$O(k \times m)$

### 最后

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

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

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

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


Question:
给你一个整数 n,请你判断该整数是否是 2 的幂次方。如果是,返回 true ;否则,返回 false 。

如果存在一个整数 x 使得 n == $2^x$ ,则认为 n 是 2 的幂次方。

示例 1:
```
输入:n = 1
输出:true
解释:20 = 1
```
示例 2:
```
输入:n = 16
输出:true
解释:24 = 16
```
示例 3:
```
输入:n = 3
输出:false
```
示例 4:
```
输入:n = 4
输出:true
```
示例 5:
```
输入:n = 5
输出:false
```

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

进阶:你能够不使用循环/递归解决此问题吗?

Solution:
### 朴素做法

首先小于等于 $0$ 的数必然不是,$1$ 必然是。

在处理完这些边界之后,尝试将 $n$ 除干净,如果最后剩余数值为 $1$ 则说明开始是 $2$ 的幂。

代码:
```Java
class Solution {
    public boolean isPowerOfTwo(int n) {
        if (n <= 0) return false;
        while (n % 2 == 0) n /= 2;
        return n == 1;
    }
}
```
* 时间复杂度:$O(\log{n})$
* 空间复杂度:$O(1)$

---

### lowbit

熟悉树状数组的同学都知道,`lowbit` 可以快速求得 `x` 二进制表示中最低位  `1` 表示的值。

如果一个数 $n$ 是 $2$ 的幂,那么有 `lowbit(n) = n` 的性质($2$ 的幂的二进制表示中必然是最高位为 $1$,低位为 $0$)。

代码;
```Java
class Solution {
    public boolean isPowerOfTwo(int n) {
        return n > 0 && (n & -n) == n;
    }
}
```
* 时间复杂度:$O(1)$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


Question:
请你仅使用两个栈实现先入先出队列。队列应当支持一般队列支持的所有操作(push、pop、peek、empty):

实现 MyQueue 类:

* void push(int x) 将元素 x 推到队列的末尾
* int pop() 从队列的开头移除并返回元素
* int peek() 返回队列开头的元素
* boolean empty() 如果队列为空,返回 true ;否则,返回 false

说明:
* 你只能使用标准的栈操作 —— 也就是只有 push to top, peek/pop from top, size, 和 is empty 操作是合法的。
* 你所使用的语言也许不支持栈。你可以使用 list 或者 deque(双端队列)来模拟一个栈,只要是标准的栈操作即可。

进阶:
* 你能否实现每个操作均摊时间复杂度为 O(1) 的队列?换句话说,执行 n 个操作的总时间复杂度为 O(n) ,即使其中一个操作可能花费较长时间。

示例:
```
输入:
["MyQueue", "push", "push", "peek", "pop", "empty"]
[[], [1], [2], [], [], []]
输出:
[null, null, null, 1, 1, false]

解释:
MyQueue myQueue = new MyQueue();
myQueue.push(1); // queue is: [1]
myQueue.push(2); // queue is: [1, 2] (leftmost is front of the queue)
myQueue.peek(); // return 1
myQueue.pop(); // return 1, queue is [2]
myQueue.empty(); // return false
```

提示:
* 1 <= x <= 9
* 最多调用 100 次 push、pop、peek 和 empty
* 假设所有操作都是有效的 (例如,一个空的队列不会调用 pop 或者 peek 操作)

Solution:
### 基本思路

无论「用栈实现队列」还是「用队列实现栈」,思路都是类似的。

都可以通过使用两个栈/队列来解决。

我们创建两个栈,分别为 `out` 和 `in`,用作处理「输出」和「输入」操作。

其实就是两个栈来回「倒腾」。

而对于「何时倒腾」决定了是 **O(n) 解法** 还是 **均摊 O(1) 解法**。

***

### O(n) 解法

我们创建两个栈,分别为 `out` 和 `in`:

* **`in` 用作处理输入操作 `push()`,使用 `in` 时需确保 `out` 为空**
* **`out` 用作处理输出操作 `pop()` 和 `peek()`,使用 `out` 时需确保 `in` 为空**

```java
class MyQueue {
    Deque<Integer> out, in;
    public MyQueue() {
        in = new ArrayDeque<>();
        out = new ArrayDeque<>();
    }

    public void push(int x) {
        while (!out.isEmpty()) in.addLast(out.pollLast());
        in.addLast(x);
    }

    public int pop() {
        while (!in.isEmpty()) out.addLast(in.pollLast());
        return out.pollLast();
    }

    public int peek() {
        while (!in.isEmpty()) out.addLast(in.pollLast());
        return out.peekLast();
    }

    public boolean empty() {
        return out.isEmpty() && in.isEmpty();
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n)$

***

### 均摊 O(1) 解法

事实上,我们不需要在每次的「入栈」和「出栈」操作中都进行「倒腾」。

**我们只需要保证,输入的元素总是跟在前面的输入元素的后面,而输出元素总是最早输入的那个元素即可。**

可以通过调整「倒腾」的时机来确保满足上述要求,但又不需要发生在每一次操作中:

* **只有在「输出栈」为空的时候,才发生一次性的「倒腾」**

```java
class MyQueue {
    Deque<Integer> out, in;
    public MyQueue() {
        in = new ArrayDeque<>();
        out = new ArrayDeque<>();
    }

    public void push(int x) {
        in.addLast(x);
    }

    public int pop() {
        if (out.isEmpty()) {
            while (!in.isEmpty()) out.addLast(in.pollLast());
        }
        return out.pollLast();
    }

    public int peek() {
        if (out.isEmpty()) {
            while (!in.isEmpty()) out.addLast(in.pollLast());
        }
        return out.peekLast();
    }

    public boolean empty() {
        return out.isEmpty() && in.isEmpty();
    }
}
```
* 时间复杂度:`pop()` 和 `peek()` 操作都是均摊 $O(1)$
* 空间复杂度:$O(n)$

***

### 关于「均摊复杂度」的说明

我们先用另外一个例子来理解「均摊复杂度」,大家都知道「哈希表」底层是通过数组实现的。

正常情况下,计算元素在哈希桶的位置,然后放入哈希桶,复杂度为 $O(1)$,假定是通过简单的“拉链法”搭配「头插法」方式来解决哈希冲突。

但当某次元素插入后,「哈希表」达到扩容阈值,则需要对底层所使用的数组进行扩容,这个复杂度是 $O(n)$

显然「扩容」操作不会发生在每一次的元素插入中,因此扩容的 $O(n)$ 都会伴随着 `n` 次的 $O(1)$,也就是 $O(n)$ 的复杂度会被均摊到每一次插入当中,因此哈希表插入仍然是 $O(1)$ 的。

**同理,我们的「倒腾」不是发生在每一次的「输出操作」中,而是集中发生在一次「输出栈为空」的时候,因此 `pop` 和 `peek` 都是均摊复杂度为 $O(1)$ 的操作。**

由于本题的调用次数只有 100 次,所以铁定是一个人均 100% 的算法(0 ms)🐶 🐶

我们需要对操作进行复杂度分析进行判断,而不是看时间来判断自己是不是均摊 O(1) 哦 ~

### 最后

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

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

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

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


233. 数字 1 的个数

Question:
给定一个整数 $n$,计算所有小于等于 $n$ 的非负整数中数字 $1$ 出现的个数。

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

输出:6
```
示例 2:
```
输入:n = 0

输出:0
```

提示:
* 0 <= n <= $2 * 10^9$

Solution:
### 基本分析

这是一道经典的「数位 DP」模板题的简化版,原题在 [这里](https://www.luogu.com.cn/problem/P1980) 。

这几天每日一题出得挺好,「序列 DP」、「区间 DP」、「数位 DP」轮着来 🤣

但由于本题只需求 $1$ 出现的次数,而不需要求解 $0$ 到 $9$ 的出现次数,同时意味着不需要考虑统计 $0$ 次数时的前导零边界问题。

因此,也可以不当作数位 DP 题来做,只当作一道计数类模拟题来求解。

---

### 计数类模拟

回到本题,我们需要计算 $[1, n]$ 范围内所有数中 $1$ 出现的次数。

我们可以统计 $1$ 在每一位出现的次数,将其累加起来即是答案。

举个 🌰,对于一个长度为 $m$ 的数字 $n$,我们可以计算其在「个位(从右起第 $1$ 位)」、「十位(第 $2$ 位)」、「百位(第 $3$ 位)」和「第 $m$ 位」中 $1$ 出现的次数。

假设有 $n = abcde$,即 $m = 5$,假设我们需要统计第 $3$ 位中 $1$ 出现的次数,即可统计满足 $--1--$ 形式,同时满足 $1 <= --1-- <= abcde$ 要求的数有多少个,我们称 $1 <= --1-- <= abcde$ 关系为「大小要求」。

我们只需对 $c$ 前后出现的值进行分情况讨论:

* 当 $c$ 前面的部分 $< ab$,即范围为 $[0, ab)$,此时必然满足「大小要求」,因此后面的部分可以任意取,即范围为 $[0, 99]$。根据「乘法原理」,可得知此时数量为 $ab * 100$;
* 当 $c$ 前面的部分 $= ab$,这时候「大小关系」主要取决于 $c$:
    * 当 $c = 0$,必然不满足「大小要求」,数量为 $0$;
    * 当 $c = 1$,此时「大小关系」取决于后部分,后面的取值范围为 $[0, de]$,数量为 $1 * (de + 1)$;
    * 当 $c > 1$,必然满足「大小关系」,后面的部分可以任意取,即范围为 $[0, 99]$,数量为 $1 * 100$;
* 当 $c$ 前面的部分 $> ab$,必然不满足「大小要求」,数量为 $0$。

其他数位的分析同理。

代码:
```Java
class Solution {
    public int countDigitOne(int n) {
        String s = String.valueOf(n);
        int m = s.length();
        if (m == 1) return n > 0 ? 1 : 0;
        // 计算第 i 位前缀代表的数值,和后缀代表的数值
        // 例如 abcde 则有 ps[2] = ab; ss[2] = de
        int[] ps = new int[m], ss = new int[m];
        ss[0] = Integer.parseInt(s.substring(1));
        for (int i = 1; i < m - 1; i++) {
            ps[i] = Integer.parseInt(s.substring(0, i));
            ss[i] = Integer.parseInt(s.substring(i + 1));
        }
        ps[m - 1] = Integer.parseInt(s.substring(0, m - 1));
        // 分情况讨论
        int ans = 0;
        for (int i = 0; i < m; i++) {
            // x 为当前位数值,len 为当前位后面长度为多少
            int x = s.charAt(i) - '0', len = m - i - 1;
            int prefix = ps[i], suffix = ss[i];
            int tot = 0;
            tot += prefix * Math.pow(10, len);
            if (x == 0) {
            } else if (x == 1) {
                tot += suffix + 1;
            } else {
                tot += Math.pow(10, len);
            }
            ans += tot;
        }
        return ans;
    }
}
```
* 时间复杂度:$O(m)$
* 空间复杂度:$O(m)$

### 最后

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

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

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

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


LeetCode/231-240/235. 二叉搜索树的最近公共祖先（中等）.md

235. 二叉搜索树的最近公共祖先

Question:
给定一个二叉搜索树, 找到该树中两个指定节点的最近公共祖先。

百度百科中最近公共祖先的定义为:“对于有根树 `T` 的两个结点 `p`、`q`,最近公共祖先表示为一个结点 `x`,满足 `x` 是 `p`、`q` 的祖先且 `x` 的深度尽可能大(一个节点也可以是它自己的祖先)。”

例如,给定如下二叉搜索树:  `root = [6,2,8,0,4,7,9,null,null,3,5]`

示例 1:

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

输出: 6

解释: 节点 2 和节点 8 的最近公共祖先是 6。
```
示例 2:
```
输入: root = [6,2,8,0,4,7,9,null,null,3,5], p = 2, q = 4

输出: 2

解释: 节点 2 和节点 4 的最近公共祖先是 2, 因为根据定义最近公共祖先节点可以为节点本身。
```

说明:
* 所有节点的值都是唯一的。
* `p`、`q` 为不同节点且均存在于给定的二叉搜索树中。

Solution:
### DFS

也是常见的 `LCA` 问题,但相比 [236. 二叉树的最近公共祖先](https://mp.weixin.qq.com/s?__biz=MzU4NDE3MTEyMA==&mid=2247500218&idx=1&sn=a6d790dcecfee353be5f3b53c312c3d4&chksm=fd9f6aa5cae8e3b36d85448674b189ed2d35fdce18381ac7676ec90a33d62da05718f85e3103#rd),本题搜索对象为二叉搜索树,利用此特性,我们可以将搜索复杂度从 $O(n)$ 优化至 $O(h)$。

利用原函数作为递归函数,复用 `root` 作为搜索过程中的当前节点,根据 `root` 和两节点关系进行分情况讨论:

* 若 `root` 为 `p` 和 `q` 中的任一节点:由于搜索过程是从上往下,因此 `root` 必然是距离两者垂直距离最远的最近公共祖先,返回 `root`;
* 否则根据 `root` 和 `p`、`q` 的节点值大小进一步讨论,将 `root` 节点值记为 `a`,两节点值中的较小值记为 `b`,两节点中的较大值记为 `c`:
  * 若有 $b < a < c$,说明 `p` 和 `q` 中值较小的节点在当前节点 `root` 的左子树内,`p` 和 `q` 中值较大的节点在当前节点 `root` 的右子树内。由于搜索过程是从上往下,因此 `root` 作为首个满足该条件的节点,必然是距离两者垂直距离最远的最近公共祖先,返回 `root`;
  * 若有 $a < b$,说明两节点均在当前节点 `root` 的右子树内,递归处理 `root.right`;
  * 若有 $a > c$,说明两节点均在当前节点 `root` 的左子树内,递归处理 `root.left`。

Java 代码:
```Java
class Solution {
    public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
        if (root == p || root == q) return root;
        int a = root.val, b = Math.min(p.val, q.val), c = Math.max(p.val, q.val);
        if (a > b && a < c) return root;
        else if (a < b) return lowestCommonAncestor(root.right, p, q);
        else return lowestCommonAncestor(root.left, p, q);
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
        if (root == p || root == q) return root;
        int a = root->val, b = min(p->val, q->val), c = max(p->val, q->val);
        if (a > b && a < c) return root;
        else if (a < b) return lowestCommonAncestor(root->right, p, q);
        else return lowestCommonAncestor(root->left, p, q);
    }
};
```
Python 代码:
```Python
class Solution:
    def lowestCommonAncestor(self, root: 'TreeNode', p: 'TreeNode', q: 'TreeNode') -> 'TreeNode':
        if root == p or root == q:
            return root
        a, b = root.val, min(p.val, q.val)
        c = max(p.val, q.val)
        if a > b and a < c:
            return root
        elif a < b:
            return self.lowestCommonAncestor(root.right, p, q)
        else:
            return self.lowestCommonAncestor(root.left, p, q)
```
TypeScript 代码:
```TypeScript
function lowestCommonAncestor(root: TreeNode | null, p: TreeNode | null, q: TreeNode | null): TreeNode | null {
    if (root === p || root === q) return root;
    const a = root.val, b = Math.min(p.val, q.val), c = Math.max(p.val, q.val);
    if (a > b && a < c) return root;
    else if (a < b) return lowestCommonAncestor(root.right, p, q);
    else return lowestCommonAncestor(root.left, p, q);
};
```
* 时间复杂度:$O(h)$,其中 $h$ 为二叉搜索树的深度
* 空间复杂度:$O(h)$

### 最后

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

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

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

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


236. 二叉树的最近公共祖先

Question:
给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。

百度百科中最近公共祖先的定义为:“对于有根树 T 的两个节点 `p`、`q`,最近公共祖先表示为一个节点 `x`,满足 `x` 是 `p`、`q` 的祖先且 `x` 的深度尽可能大(一个节点也可以是它自己的祖先)。”

示例 1:

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

输出:3

解释:节点 5 和节点 1 的最近公共祖先是节点 3 。
```
示例 2:

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

输出:5

解释:节点 5 和节点 4 的最近公共祖先是节点 5 。因为根据定义最近公共祖先节点可以为节点本身。
```
示例 3:
```
输入:root = [1,2], p = 1, q = 2

输出:1
```

提示:
* 树中节点数目在范围 $[2, 10^5]$ 内。
* $-10^9 <= Node.val <= 10^9$
* 所有 `Node.val` 互不相同 。
* $p != q$
* `p` 和 `q` 均存在于给定的二叉树中。

Solution:
### DFS

常见的 `LCA` 问题。

设计 `DFS` 函数 `boolean dfs(TreeNode cur, TreeNode t, List<TreeNode> path)`:其中 **`cur` 为当前处理到的节点,`t` 为需要找到的目的节点,`path` 为从根节点到当前节点 `cur` 所经过的路径**。若能够以当前节点 `cur` 为根的子树包含目标节点 `t`,函数返回 `true`,否则返回 `false`。z

调用函数分别传入 `p` 和 `q` 作为目标节点,从而得到从根节点到 `p` 和 `q` 的路径列表,遍历路径找到最后一个相同的节点即是答案。

Java 代码:
```Java
class Solution {
    public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
        List<TreeNode> a = new ArrayList<>(), b = new ArrayList<>();
        dfs(root, p, a); dfs(root, q, b);
        TreeNode ans = null;
        for (int i = 0; i < Math.min(a.size(), b.size()) && a.get(i) == b.get(i); i++) ans = a.get(i);
        return ans;
    }
    boolean dfs(TreeNode cur, TreeNode t, List<TreeNode> path) {
        if (cur == null) return false;
        path.add(cur);
        if (cur == t || dfs(cur.left, t, path) || dfs(cur.right, t, path)) {
            return true;
        } else {
            path.remove(path.size() - 1);
            return false;
        }
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
        vector<TreeNode*> a, b;
        dfs(root, p, a); dfs(root, q, b);
        TreeNode* ans = nullptr;
        for (int i = 0; i < min(a.size(), b.size()) && a[i] == b[i]; i++) ans = a[i];
        return ans;
    }
    bool dfs(TreeNode* cur, TreeNode* t, vector<TreeNode*>& path) {
        if (!cur) return false;
        path.push_back(cur);
        if (cur == t || dfs(cur->left, t, path) || dfs(cur->right, t, path)) {
            return true;
        } else {
            path.pop_back();
            return false;
        }
    }
};
```
Python 代码:
```Python
class Solution:
    def lowestCommonAncestor(self, root: 'TreeNode', p: 'TreeNode', q: 'TreeNode') -> 'TreeNode':
        def dfs(cur, t, path):
            if not cur:
                return False
            path.append(cur)
            if cur == t or dfs(cur.left, t, path) or dfs(cur.right, t, path):
                return True
            else:
                path.pop()
                return False
        a, b = [], []
        dfs(root, p, a)
        dfs(root, q, b)
        ans = None
        for i in range(min(len(a), len(b))):
            if a[i] == b[i]:
                ans = a[i]
        return ans
```
TypeScript 代码:
```TypeScript
function lowestCommonAncestor(root: TreeNode | null, p: TreeNode | null, q: TreeNode | null): TreeNode | null {
    const a = [], b = [];
    dfs(root, p, a);
    dfs(root, q, b);
    let ans = null;
    for (let i = 0; i < Math.min(a.length, b.length) && a[i] === b[i]; i++) ans = a[i];
    return ans;
}
function dfs(cur: TreeNode | null, t: TreeNode | null, path: TreeNode[]): boolean {
    if (!cur) return false;
    path.push(cur);
    if (cur === t || dfs(cur.left, t, path) || dfs(cur.right, t, path)) {
        return true;
    } else {
        path.pop();
        return false;
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


237. 删除链表中的节点

Question:
请编写一个函数,用于**删除单链表中某个特定节点 **。

在设计函数时需要注意,你无法访问链表的头节点 head ,只能直接访问**要被删除的节点**。

题目数据保证需要删除的节点 不是末尾节点 。

示例 1:

```
输入:head = [4,5,1,9], node = 5

输出:[4,1,9]

解释:指定链表中值为 5 的第二个节点,那么在调用了你的函数之后,该链表应变为 4 -> 1 -> 9
```
示例 2:

```
输入:head = [4,5,1,9], node = 1

输出:[4,5,9]

解释:指定链表中值为 1 的第三个节点,那么在调用了你的函数之后,该链表应变为 4 -> 5 -> 9
```
示例 3:
```
输入:head = [1,2,3,4], node = 3

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

输出:[1]
```
示例 5:
```
输入:head = [-3,5,-99], node = -3

输出:[5,-99]
```

提示:
* 链表中节点的数目范围是 [2, 1000]
* -1000 <= Node.val <= 1000
* 链表中每个节点的值都是唯一的
* 需要删除的节点 node 是 链表中的一个有效节点 ,且 不是末尾节点

Solution:
### 模拟

对于一般性的链表删除操作而言,我们需要知道待删除节点的前一节点与后一节点,并对两者建立联系。

对于本题,由于我们只知道待删除节点本身,同时该链表为单链表(无法访问前一节点),因此我们只能先将后一节点的值复制到当前节点,然后将后一节点当作「待删除节点」来进行常规删除。

代码:
```Java
class Solution {
    public void deleteNode(ListNode node) {
        node.val = node.next.val;
        node.next = node.next.next;
    }
}
```
* 时间复杂度:$O(1)$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


238. 除自身以外数组的乘积

Question:
给你一个整数数组 `nums`,返回 数组 `answer`,其中 $answer[i]$ 等于 `nums` 中除 $nums[i]$ 之外其余各元素的乘积 。

题目数据 保证 数组 `nums` 之中任意元素的全部前缀元素和后缀的乘积都在  $32$ 位 整数范围内。

请不要使用除法,且在 $O(n)$ 时间复杂度内完成此题。

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

输出: [24,12,8,6]
```
示例 2:
```
输入: nums = [-1,1,0,-3,3]

输出: [0,0,9,0,0]
```

提示:
* $2 <= nums.length <= 10^5$
* $-30 <= nums[i] <= 30$
* 保证 数组 `nums` 之中任意元素的全部前缀元素和后缀的乘积都在  $32$ 位 整数范围内

进阶:你可以在 $O(1)$ 的额外空间复杂度内完成这个题目吗?( 出于对空间复杂度分析的目的,输出数组不被视为额外空间。)

Solution:
### 前缀和

根据题意,对于每个 $ans[i]$ 均由两部分组成:

$$
(nums[0] \times nums[1] \times ... \times nums[i - 1]) \times (nums[i + 1] \times nums[i + 2] \times ... \times nums[n - 1])
$$

因此我们可以运用「前缀和」思想,使用数组 `s1` 和 `s2` 分别计算范围 $[1, x]$ 的前缀乘 和 范围 $[x, n]$ 的后缀乘法(前缀和数组下标默认从 $1$ 开始),即 $s1[i]$ 代表范围 $[1,i]$ 的前缀乘值,$s2[i]$ 代表范围 $[i, n]$ 的后缀乘值。

预处理完 `s1` 和 `s2` 之后,即可计算每个 $ans[i - 1] = s1[i - 1] \times s2[i + 1]$(`ans` 数组下标从 $0$ 开始)。

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

---

### 前缀和(空间优化)

题目的进阶部分要求我们使用 $O(1)$ 空间来做(不算 `ans` 数组)。

这样很好处理,按照我们解法一的思路,将两部分分开算即可:建立 `ans` 数组,先从前往后遍历 `nums`,计算每个 $ans[i]$ 前缀乘值部分,再从后往前遍历 `nums`,计算每个 $ans[i]$ 后缀乘值的部分,两部分相乘即是最终的 $ans[i]$。

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

### 最后

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

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

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

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


239. 滑动窗口最大值

Question:
给你一个整数数组 `nums`,有一个大小为 `k` 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 `k` 个数字。滑动窗口每次只向右移动一位。

返回滑动窗口中的最大值 。

示例 1:
```
输入:nums = [1,3,-1,-3,5,3,6,7], k = 3

输出:[3,3,5,5,6,7]

解释:
滑动窗口的位置                最大值
---------------               -----
[1  3  -1] -3  5  3  6  7       3
 1 [3  -1  -3] 5  3  6  7       3
 1  3 [-1  -3  5] 3  6  7       5
 1  3  -1 [-3  5  3] 6  7       5
 1  3  -1  -3 [5  3  6] 7       6
 1  3  -1  -3  5 [3  6  7]      7
```
示例 2:
```
输入:nums = [1], k = 1

输出:[1]
```

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

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

根据题意,容易想到优先队列(大根堆),同时为了确保滑动窗口的大小合法性,我们以二元组 $(idx, nums[idx])$ 的形式进行入队。

当下标达到首个滑动窗口的右端点后,每次尝试从优先队列(大根堆)中取出最大值(若堆顶元素的下标小于当前滑动窗口左端点时,则丢弃该元素)。

代码:
```Java
class Solution {
    public int[] maxSlidingWindow(int[] nums, int k) {
        PriorityQueue<int[]> q = new PriorityQueue<>((a,b)->b[1]-a[1]);
        int n = nums.length, m = n - k + 1, idx = 0;
        int[] ans = new int[m];
        for (int i = 0; i < n; i++) {
            q.add(new int[]{i, nums[i]});
            if (i >= k - 1) {
                while (q.peek()[0] <= i - k) q.poll();
                ans[idx++] = q.peek()[1];
            }
        }
        return ans;
    }
}
```
* 时间复杂度:$O(n\log{n})$
* 空间复杂度:$O(n)$

---

### 线段树

容易将问题转换为「区间求和」问题:使用原始的 `nums` 构建线段树等价于在位置 $i$ 插入 $nums[i]$,即单点操作,而查询每个滑动窗口最大值,则对应的区间查询。

由于只涉及单点修改,无须实现懒标记 `pushdown` 操作,再结合 $n$ 的数据范围为 $10^5$,无须进行动态开点。

直接写 `build` 四倍空间的线段树数组实现即可。

代码:
```Java
class Solution {
    class Node {
        int l, r, val;
        Node (int _l, int _r) {
            l = _l; r = _r; val = Integer.MIN_VALUE;
        }
    }
    Node[] tr = new Node[100010 * 4];
    void build(int u, int l, int r) {
        tr[u] = new Node(l, r);
        if (l == r) return ;
        int mid = l + r >> 1;
        build(u << 1, l, mid);
        build(u << 1 | 1, mid + 1, r);
    }
    void update(int u, int x, int v) {
        if (tr[u].l == x && tr[u].r == x) {
            tr[u].val = Math.max(tr[u].val, v);
            return ;
        }
        int mid = tr[u].l + tr[u].r >> 1;
        if (x <= mid) update(u << 1, x, v);
        else update(u << 1 | 1, x, v);
        pushup(u);
    }
    int query(int u, int l, int r) {
        if (l <= tr[u].l && tr[u].r <= r) return tr[u].val;
        int mid = tr[u].l + tr[u].r >> 1, ans = Integer.MIN_VALUE;
        if (l <= mid) ans = query(u << 1, l, r);
        if (r > mid) ans = Math.max(ans, query(u << 1 | 1, l, r));
        return ans;
    }
    void pushup(int u) {
        tr[u].val = Math.max(tr[u << 1].val, tr[u << 1 | 1].val);
    }
    public int[] maxSlidingWindow(int[] nums, int k) {
        int n = nums.length, m = n - k + 1;
        int[] ans = new int[m];
        build(1, 1, n);
        for (int i = 0; i < n; i++) update(1, i + 1, nums[i]);
        for (int i = k; i <= n; i++) ans[i - k] = query(1, i - k + 1, i);
        return ans;
    }
}
```
* 时间复杂度:建立线段树复杂度为 $O(n\log{n})$;构建答案复杂度为 $O(n\log{n})$。整体复杂度为 $O(n\log{n})$
* 空间复杂度:$O(n)$

---

### 分块

另外一个做法是分块,又名「优雅的暴力」,也是莫队算法的基础。

具体的,除了给定的 `nums` 以外,我们构建一个分块数组 `region`,其中 `region[idx] = x` 含义为块编号为 `idx` 的最大值为 `x`,其中一个块对应一个原始区间 $[l, r]$。

如何定义块大小是实现分块算法的关键。

对于本题,本质是求若干个大小为 $k$ 的区间最大值。

我们可以设定块大小为 $\sqrt{k}$,这样所需创建的分块数组大小为 $\frac{n}{\sqrt{k}}$。分块数组的更新操作为 $O(1)$,而查询则为 $\sqrt{k}$。

容易证明查询操作的复杂度:对于每个长度为 $k$ 的 $[l, r]$ 查询操作而言,最多遍历两个(左右端点对应的块)的块内元素,复杂度为 $O(\sqrt{k})$,同时最多遍历 $\sqrt{k}$ 个块,复杂度同为 $O(\sqrt{k})$。

因此最多两步复杂度为 $O(\sqrt{k})$ 的块内操作,最多 $\sqrt{k}$ 步复杂度为 $O(1)$ 的块间操作,整体复杂度为 $O(\sqrt{k})$。

因此使用分块算法总的计算量为 $n\times\sqrt{k} = 10^6$,可以过。

分块算法的几个操作函数:

* `int getIdx(int x)` :计算原始下标对应的块编号;
* `void add(int x, int v)` :计算原始下标 `x` 对应的下标 `idx`,并将 `region[idx]` 和 `v` 取 `max` 来更新 `region[idx]`;
* `int query(int l, int r)` :查询 $[l, r]$ 中的最大值,如果 $l$ 和 $r$ 所在块相同,直接遍历 $[l, r]$ 进行取值;若 $l$ 和 $r$ 不同块,则处理 $l$ 和 $r$ 对应的块内元素后,对块编号在 $(getIdx(l), getIdx(r))$ 之间的块进行遍历。

代码:
```Java
class Solution {
    int n, m, len;
    int[] nums, region;
    int getIdx(int x) {
        return x / len;
    }
    void add(int x, int v) {
        region[getIdx(x)] = Math.max(region[getIdx(x)], v);
    }
    int query(int l, int r) {
        int ans = Integer.MIN_VALUE;
        if (getIdx(l) == getIdx(r)) {
            for (int i = l; i <= r; i++) ans = Math.max(ans, nums[i]);
        } else {
            int i = l, j = r;
            while (getIdx(i) == getIdx(l)) ans = Math.max(ans, nums[i++]);
            while (getIdx(j) == getIdx(r)) ans = Math.max(ans, nums[j--]);
            for (int k = getIdx(i); k <= getIdx(j); k++) ans = Math.max(ans, region[k]);
        }
        return ans;
    }
    public int[] maxSlidingWindow(int[] _nums, int k) {
        nums = _nums;
        n = nums.length; len = (int) Math.sqrt(k); m = n / len + 10;
        region = new int[m];
        Arrays.fill(region, Integer.MIN_VALUE);
        for (int i = 0; i < n; i++) add(i, nums[i]);
        int[] ans = new int[n - k + 1];
        for (int i = 0; i < n - k + 1; i++) ans[i] = query(i, i + k - 1);
        return ans;
    }
}
```
* 时间复杂度:数组大小为 $n$,块大小为 $\sqrt{k}$,分块数组大小为 $\frac{n}{\sqrt{k}}$。预处理分块数组复杂度为 $O(n)$(即 `add` 操作复杂度为 $O(1)$ );构造答案复杂度为 $O(n\times\sqrt{k})$(即 `query` 操作复杂度为 $O(\sqrt{k})$,最多有 $n$ 次查询)
* 空间复杂度:$\frac{n}{\sqrt{k}}$

---

### 单调队列

关于 `RMQ` 的另外一个优秀做法通常是使用「单调队列/单调栈」。

当然,我们也能不依靠经验,而从问题的本身出发,逐步分析出该做法。

假设我们当前处理到某个长度为 $k$ 的窗口,此时窗口往后滑动一格,会导致后一个数(新窗口的右端点)添加进来,同时会导致前一个数(旧窗口的左端点)移出窗口。

随着窗口的不断平移,该过程会一直发生。**若同一时刻存在两个数 $nums[j]$ 和 $nums[i]$($j < i$)所在一个窗口内,下标更大的数会被更晚移出窗口,此时如果有 $nums[j] <= nums[i]$ 的话,可以完全确定 $nums[j]$ 将不会成为后续任何一个窗口的最大值,此时可以将必然不会是答案的 $nums[j]$ 从候选中进行移除**。

不难发现,当我们将所有必然不可能作为答案的元素(即所有满足的小于等于 $nums[i]$ )移除后,候选集合满足「单调递减」特性,即集合首位元素为当前窗口中的最大值(为了满足窗口长度为 $k$ 的要求,在从集合头部取答案时需要先将下标小于的等于的 $i - k$ 的元素移除)。

为方便从尾部添加元素,从头部获取答案,我们可使用「双端队列」存储所有候选元素。

代码:
```Java
class Solution {
    public int[] maxSlidingWindow(int[] nums, int k) {
        Deque<Integer> d = new ArrayDeque<>();
        int n = nums.length, m = n - k + 1;
        int[] ans = new int[m];
        for (int i = 0; i < n; i++) {
            while (!d.isEmpty() && nums[d.peekLast()] <= nums[i]) d.pollLast();
            d.addLast(i);
            if (i >= k - 1) {
                while (!d.isEmpty() && d.peekFirst() <= i - k) d.pollFirst();
                ans[i - k + 1] = nums[d.peekFirst()];
            }
        }
        return ans;
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


240. 搜索二维矩阵 II

Question:
编写一个高效的算法来搜索 `m x n` 矩阵 `matrix` 中的一个目标值 `target` 。

该矩阵具有以下特性:

* 每行的元素从左到右升序排列。
* 每列的元素从上到下升序排列。

示例 1:

```
输入:matrix = [[1,4,7,11,15],[2,5,8,12,19],[3,6,9,16,22],[10,13,14,17,24],[18,21,23,26,30]], target = 5

输出:true
```
示例 2:

```
输入:matrix = [[1,4,7,11,15],[2,5,8,12,19],[3,6,9,16,22],[10,13,14,17,24],[18,21,23,26,30]], target = 20

输出:false
```

提示:
* m == matrix.length
* n == matrix[i].length
* 1 <= n, m <= 300
* $-10^9 <= matrix[i][j] <= 10^9$
* 每行的所有元素从左到右升序排列
* 每列的所有元素从上到下升序排列
* $-10^9 <= target <= 10^9$

Solution:
### 前言

这道题和之前的[(题解)74. 搜索二维矩阵](https://leetcode-cn.com/problems/search-a-2d-matrix/solution/gong-shui-san-xie-yi-ti-shuang-jie-er-fe-l0pq/) 类似,建议你将两题放到一起去做。

---

### 二分

与 [(题解)74. 搜索二维矩阵](https://leetcode-cn.com/problems/search-a-2d-matrix/solution/gong-shui-san-xie-yi-ti-shuang-jie-er-fe-l0pq/) 不同,本题没有确保「每行的第一个整数大于前一行的最后一个整数」,因此我们无法采取「两次二分」的做法。

只能退而求之,遍历行/列,然后再对列/行进行二分。

代码:
```Java
class Solution {
    public boolean searchMatrix(int[][] matrix, int target) {
        int m = matrix.length, n = matrix[0].length;
        for (int i = 0; i < m; i++) {
            int l = 0, r = n - 1;
            while (l < r) {
                int mid = l + r + 1 >> 1;
                if (matrix[i][mid] <= target) l = mid;
                else r = mid - 1;
            }
            if (matrix[i][r] == target) return true;
        }
        return false;
    }
}
```

```Java
class Solution {
    public boolean searchMatrix(int[][] matrix, int target) {
        int m = matrix.length, n = matrix[0].length;
        for (int i = 0; i < n; i++) {
            int l = 0, r = m - 1;
            while (l < r) {
                int mid = l + r + 1 >> 1;
                if (matrix[mid][i] <= target) l = mid;
                else r = mid - 1;
            }
            if (matrix[r][i] == target) return true;
        }
        return false;
    }
}
```
* 时间复杂度:$O(m\log{n})$ 或 $O(n\log{m})$
* 空间复杂度:$O(1)$

---

### 抽象 BST

该做法则与 [(题解)74. 搜索二维矩阵](https://leetcode-cn.com/problems/search-a-2d-matrix/solution/gong-shui-san-xie-yi-ti-shuang-jie-er-fe-l0pq/) 的「解法二」完全一致。

我们可以将二维矩阵抽象成「以右上角为根的 BST」:

那么我们可以从根(右上角)开始搜索,如果当前的节点不等于目标值,可以按照树的搜索顺序进行:

1. 当前节点「大于」目标值,搜索当前节点的「左子树」,也就是当前矩阵位置的「左方格子」,即 $c$--
2. 当前节点「小于」目标值,搜索当前节点的「右子树」,也就是当前矩阵位置的「下方格子」,即 $r$++

代码:
```Java
class Solution {
    public boolean searchMatrix(int[][] matrix, int target) {
        int m = matrix.length, n = matrix[0].length;
        int r = 0, c = n - 1;
        while (r < m && c >= 0) {
            if (matrix[r][c] < target) r++;
            else if (matrix[r][c] > target) c--;
            else return true;
        }
        return false;
    }
}
```
* 时间复杂度:$O(m + n)$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


LeetCode/2331-2340/2335. 装满杯子需要的最短总时长（简单）.md

2335. 装满杯子需要的最短总时长

Question:
现有一台饮水机,可以制备冷水、温水和热水。每秒钟,可以装满 `2` 杯 不同 类型的水或者 `1` 杯任意类型的水。

给你一个下标从 `0` 开始、长度为 `3` 的整数数组 `amount`,其中 `amount[0]`、`amount[1]` 和 `amount[2]` 分别表示需要装满冷水、温水和热水的杯子数量。

返回装满所有杯子所需的 **最少** 秒数。

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

输出:4

解释:下面给出一种方案:
第 1 秒:装满一杯冷水和一杯温水。
第 2 秒:装满一杯温水和一杯热水。
第 3 秒:装满一杯温水和一杯热水。
第 4 秒:装满一杯温水。
可以证明最少需要 4 秒才能装满所有杯子。
```
示例 2:
```
输入:amount = [5,4,4]

输出:7

解释:下面给出一种方案:
第 1 秒:装满一杯冷水和一杯热水。
第 2 秒:装满一杯冷水和一杯温水。
第 3 秒:装满一杯冷水和一杯温水。
第 4 秒:装满一杯温水和一杯热水。
第 5 秒:装满一杯冷水和一杯热水。
第 6 秒:装满一杯冷水和一杯温水。
第 7 秒:装满一杯热水。
```
示例 3:
```
输入:amount = [5,0,0]

输出:5

解释:每秒装满一杯冷水。
```

提示:
* $amount.length = 3$
* $0 <= amount[i] <= 100$

Solution:
### 排序 + 递归

水的种类固定为 `3`,且每种水的数据范围只有 $100$,可直接使用递归进行求解。

为了尽可能的凑成多的对数,我们可以每次取剩余数量最多且不为 `0` 的两类水进行成组(因此每次处理前需要先对当前 `amount` 进行排序),直到没有水剩余,或只有一类水的剩余数据量不为 `0`(剩下的水只能独自生成)。

Java 代码:
```Java
class Solution {
    public int fillCups(int[] amount) {
        Arrays.sort(amount);
        if (amount[1] == 0) return amount[2];
        amount[1]--; amount[2]--;
        return 1 + fillCups(amount);
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    int fillCups(vector<int>& amount) {
        sort(amount.begin(), amount.end());
        if (amount[1] == 0) return amount[2];
        amount[1]--; amount[2]--;
        return 1 + fillCups(amount);
    }
};
```
Python 代码:
```Python
class Solution:
    def fillCups(self, amount: List[int]) -> int:
        amount.sort()
        if amount[1] == 0:
            return amount[2]
        amount[1] -= 1
        amount[2] -= 1
        return 1 + self.fillCups(amount)
```
TypeScript 代码:
```TypeScript
function fillCups(amount: number[]): number {
    amount.sort((a, b) => a - b);
    if (amount[1] === 0) return amount[2];
    amount[1] -= 1;
    amount[2] -= 1;
    return 1 + fillCups(amount);
};
```
* 时间复杂度:由于 `amount` 的长度固定为 `3`,因此排序消耗可视为常量;每次至少会消耗两杯水,直到递归结束或只剩下一种水。复杂度为 $O(\sum_{i = 0}^{2} amount[i])$
* 空间复杂度:忽略递归和排序带来的额外空间开销,复杂度为 $O(1)$

---

### 贪心 + 数学

我们已经知道可以通过「每次取剩余数量最多且不为 `0` 的水成对」来实现最少生成次数。

那么是否存在直接计算最少生成次数的方法,而不是采用逐回合模拟。

考虑对 `amount` 进行排序,分别使用 `a`、`b` 和 `c` 代表剩余次数递增的三种种类。

根据 $a + b$ 和 $c$ 的大小关系进行分情况讨论:

* $a + b \leq c$,此时每消耗一个 $c$ 都可以顺带消除一个 $a$ 或 $b$,因此最少生成次数为 $c$;

* $a + b > c$,此时考虑其之间的差值 $t = a + b - c$,若能顺利通过 $\frac{t}{2}$ 次对 $a$ 和 $b$ 的合并,可以将局面转成情况一,最少生成次数为 $\frac{t}{2} + c$;

	考虑起始是否能先对 $a$ 和 $b$ 进行 $\frac{t}{2}$ 个回合,由于我们有 $a \leq b$,我们只需考虑是否必然有 $a \geq \frac{t}{2}$ 即可,可通过反证法证明 $a < \frac{t}{2}$ 必然不成立,若有 $a < \frac{t}{2}$,则有 $2a < a + b - c$ => $a < b - c$,由于 $b \leq c$,则有 $a < 0$,与原条件冲突。

	最后,为了处理 $t$ 的奇偶性问题,先用 $a$ 和 $b$ 进行 $\left \lceil \frac{a + b - c}{2} \right \rceil$ 抵消操作,再与 $c$ 进行抵消

Java 代码:
```Java
class Solution {
    public int fillCups(int[] amount) {
        Arrays.sort(amount);
        int a = amount[0], b = amount[1], c = amount[2];
        if (a + b <= c) return c;
        else return (a + b - c + 1) / 2 + c;
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    int fillCups(vector<int>& amount) {
        sort(amount.begin(), amount.end());
        int a = amount[0], b = amount[1], c = amount[2];
        if (a + b <= c) return c;
        else return (a + b - c + 1) / 2 + c;
    }
};
```
Python 代码:
```Python
class Solution:
    def fillCups(self, amount: List[int]) -> int:
        amount.sort()
        a, b, c = amount
        if a + b <= c:
            return c
        else:
            return (a + b - c + 1) // 2 + c
```
TypeScript 代码:
```TypeScript
function fillCups(amount: number[]): number {
    amount.sort((a, b) => a - b);
    const [a, b, c] = amount;
    if (a + b <= c) return c;
    else return Math.floor((a + b - c + 1) / 2) + c;
};
```
* 时间复杂度:由于 `amount` 的长度固定为 `3`,因此排序消耗可视为常量,整体复杂度为 $O(1)$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


LeetCode/2331-2340/2336. 无限集中的最小数字（中等）.md

2336. 无限集中的最小数字

Question:
现有一个包含所有正整数的集合 $[1, 2, 3, 4, 5, ...]$ 。

实现 `SmallestInfiniteSet` 类:

* `SmallestInfiniteSet()` 初始化 `SmallestInfiniteSet` 对象以包含所有正整数。
* `int popSmallest()` 移除并返回该无限集中的最小整数。
* `void addBack(int num)` 如果正整数 `num` 不存在于无限集中,则将一个 `num` 添加到该无限集中。

示例:
```
输入
["SmallestInfiniteSet", "addBack", "popSmallest", "popSmallest", "popSmallest", "addBack", "popSmallest", "popSmallest", "popSmallest"]
[[], [2], [], [], [], [1], [], [], []]

输出
[null, null, 1, 2, 3, null, 1, 4, 5]

解释
SmallestInfiniteSet smallestInfiniteSet = new SmallestInfiniteSet();
smallestInfiniteSet.addBack(2);    // 2 已经在集合中,所以不做任何变更。
smallestInfiniteSet.popSmallest(); // 返回 1 ,因为 1 是最小的整数,并将其从集合中移除。
smallestInfiniteSet.popSmallest(); // 返回 2 ,并将其从集合中移除。
smallestInfiniteSet.popSmallest(); // 返回 3 ,并将其从集合中移除。
smallestInfiniteSet.addBack(1);    // 将 1 添加到该集合中。
smallestInfiniteSet.popSmallest(); // 返回 1 ,因为 1 在上一步中被添加到集合中,
                                   // 且 1 是最小的整数,并将其从集合中移除。
smallestInfiniteSet.popSmallest(); // 返回 4 ,并将其从集合中移除。
smallestInfiniteSet.popSmallest(); // 返回 5 ,并将其从集合中移除。
```

提示:
* $1 <= num <= 1000$
* 最多调用 `popSmallest` 和 `addBack` 方法 共计 $1000$ 次

Solution:
### 优先队列(小根堆)+ 哈希表

使用 `idx` 代表顺序弹出的集合左边界,$[idx, +\infty]$ 范围内的数均为待弹出,起始有 $idx = 1$。

考虑当调用 `addBack` 往集合添加数值 `x` 时,该如何处理:

* $x \geq idx$:数值本身就存在于集合中,忽略该添加操作;
* $x = idx - 1$:数值刚好位于边界左侧,更新 $idx = idx - 1$;
* $x < idx - 1$:考虑将数值添加到某个容器中,该容器支持返回最小值,容易联想到“小根堆”;但小根堆并没有“去重”功能,为防止重复弹出,还需额外使用“哈希表”来记录哪些元素在堆中。

该做法本质上将集合分成两类:一类是从 `idx` 到正无穷的连续段,对此类操作的复杂度为 $O(1)$;一类是比 `idx` 要小的离散类数集,对该类操作复杂度为 $O(\log{n})$,其中 $n$ 为调用 `addBack` 的最大次数。

Java 代码:
```Java
class SmallestInfiniteSet {
    boolean[] vis = new boolean[1010];
    PriorityQueue<Integer> q = new PriorityQueue<>((a,b)->a-b);
    int idx = 1;
    public int popSmallest() {
        int ans = -1;
        if (!q.isEmpty()) {
            ans = q.poll();
            vis[ans] = false;
        } else {
            ans = idx++;
        }
        return ans;
    }
    public void addBack(int x) {
        if (x >= idx || vis[x]) return ;
        if (x == idx - 1) {
            idx--;
        } else {
            q.add(x);
            vis[x] = true;
        }
    }
}
```
C++ 代码:
```C++
class SmallestInfiniteSet {
public:
    vector<bool> vis;
    priority_queue<int, vector<int>, greater<int>> q;
    int idx;
    SmallestInfiniteSet() : idx(1) {
        vis.resize(1010, false);
    }
    int popSmallest() {
        int ans = -1;
        if (!q.empty()) {
            ans = q.top();
            q.pop();
            vis[ans] = false;
        } else {
            ans = idx++;
        }
        return ans;
    }
    void addBack(int x) {
        if (x >= idx || vis[x]) return;
        if (x == idx - 1) {
            idx--;
        } else {
            q.push(x);
            vis[x] = true;
        }
    }
};

```
Python 代码:
```Python
class SmallestInfiniteSet:
    def __init__(self):
        self.vis = [False] * 1010
        self.q = []
        self.idx = 1

    def popSmallest(self):
        ans = -1
        if self.q:
            ans = heapq.heappop(self.q)
            self.vis[ans] = False
        else:
            ans = self.idx
            self.idx += 1
        return ans

    def addBack(self, x):
        if x >= self.idx or self.vis[x]:
            return
        if x == self.idx - 1:
            self.idx -= 1
        else:
            heapq.heappush(self.q, x)
            self.vis[x] = True
```
TypeScript 代码:
```TypeScript
class SmallestInfiniteSet {
    vis: boolean[] = Array(1010).fill(false);
    q: number[] = [];
    idx: number = 1;
    popSmallest(): number {
        let ans: number = -1;
        if (this.q.length !== 0) {
            ans = this.q.sort((a, b) => a - b).shift() as number;
            this.vis[ans] = false;
        } else {
            ans = this.idx++;
        }
        return ans;
    }
    addBack(x: number): void {
        if (x >= this.idx || this.vis[x]) return;
        if (x == this.idx - 1) {
            this.idx--;
        } else {
            this.q.push(x);
            this.vis[x] = true;
        }
    }
}
```
* 时间复杂度:插入和取出的最坏复杂度为 $O(\log{n})$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


LeetCode/2341-2350/2342. 数位和相等数对的最大和（中等）.md

2342. 数位和相等数对的最大和

Question:
给你一个下标从 `0` 开始的数组 `nums`,数组中的元素都是正整数。

请你选出两个下标 `i` 和 `j`(`i != j`),且 `nums[i]` 的数位和与  `nums[j]` 的数位和相等。

请你找出所有满足条件的下标 `i` 和 `j`,找出并返回 `nums[i] + nums[j]` 可以得到的最大值。

示例 1:
```
输入:nums = [18,43,36,13,7]

输出:54

解释:满足条件的数对 (i, j) 为:
- (0, 2) ,两个数字的数位和都是 9 ,相加得到 18 + 36 = 54 。
- (1, 4) ,两个数字的数位和都是 7 ,相加得到 43 + 7 = 50 。
所以可以获得的最大和是 54 。
```
示例 2:
```
输入:nums = [10,12,19,14]

输出:-1

解释:不存在满足条件的数对,返回 -1 。
```

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

Solution:
### 模拟

既然每个 $nums[i]$ 都对应一个具体的数位和,统计每个数位和的最大值和次大值,然后在所有数位和的最大值和次大值求和中取 `max` 即是答案。

利用 $1 <= nums[i] <= 10^9$,我们知道数位和不会超过 $9 \times 9 = 81$,可直接起一个大小为 $100 \times 2$ 的二维数组进行统计,$val[x][0]$ 代表数位和为 $x$ 的次大值,$val[x][1]$ 代表数位和为 $x$ 的最大值。

Java 代码:
```Java
class Solution {
    public int maximumSum(int[] nums) {
        int[][] val = new int[100][2];
        for (int x : nums) {
            int t = x, cur = 0;
            while (t != 0) {
                cur += t % 10;
                t /= 10;
            }
            if (x >= val[cur][1]) { // 最大沦为次大, 更新最大
                val[cur][0] = val[cur][1];
                val[cur][1] = x;
            } else if (x > val[cur][0]) { // 更新次大
                val[cur][0] = x;
            }
        }
        int ans = -1;
        for (int i = 0; i < 100; i++) {
            if (val[i][0] != 0 && val[i][1] != 0) ans = Math.max(ans, val[i][0] + val[i][1]);
        }
        return ans;
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    int maximumSum(vector<int>& nums) {
        vector<vector<int>> val(100, vector<int>(2, 0));
        for (int x : nums) {
            int t = x, cur = 0;
            while (t != 0) {
                cur += t % 10;
                t /= 10;
            }
            if (x >= val[cur][1]) {
                val[cur][0] = val[cur][1];
                val[cur][1] = x;
            } else if (x > val[cur][0]) {
                val[cur][0] = x;
            }
        }
        int ans = -1;
        for (int i = 0; i < 100; i++) {
            if (val[i][0] != 0 && val[i][1] != 0) ans = max(ans, val[i][0] + val[i][1]);
        }
        return ans;
    }
};
```
Python 代码:
```Python
class Solution:
    def maximumSum(self, nums: List[int]) -> int:
        val = [[0, 0] for _ in range(100)]
        for x in nums:
            t, cur = x, 0
            while t != 0:
                cur += t % 10
                t //= 10
            if x >= val[cur][1]:
                val[cur][0], val[cur][1] = val[cur][1], x
            elif x > val[cur][0]:
                val[cur][0] = x
        ans = -1
        for i in range(100):
            if val[i][0] != 0 and val[i][1] != 0:
                ans = max(ans, val[i][0] + val[i][1])
        return ans
```
TypeScript 代码:
```TypeScript
function maximumSum(nums: number[]): number {
    const val = Array.from({ length: 100 }, () => [0, 0]);
    for (const x of nums) {
        let t = x, cur = 0;
        while (t !== 0) {
            cur += t % 10;
            t = Math.floor(t / 10);
        }
        if (x >= val[cur][1]) {
            val[cur][0] = val[cur][1];
            val[cur][1] = x;
        } else if (x > val[cur][0]) {
            val[cur][0] = x;
        }
    }
    let ans = -1;
    for (let i = 0; i < 100; i++) {
        if (val[i][0] !== 0 && val[i][1] !== 0) ans = Math.max(ans, val[i][0] + val[i][1]);
    }
    return ans;
};
```
* 时间复杂度:$O(n\log{m})$,其中 $n=1e5$ 为 `nums` 长度,$m=1e9$ 为 $nums[i]$ 值域上界
* 空间复杂度:$O(C)$,其中 $C = 2 \times 9 \times \log{m}$

---

### 模拟

更进一步,我们不需要记录次大值,仅记录某个“数对和”当前的最大值即可。

每次计算出当前 $nums[i]$ 对应的数对 `cur` 后,检查 `cur` 是否已出现过,若出现过用两者之和更新答案,并用 $nums[i]$ 来更新 `cur` 下的最大值。

该做法本质是用「遍历过程」代替「次大维护」。

Java 代码:
```Java
class Solution {
    public int maximumSum(int[] nums) {
        int[] val = new int[100];
        int ans = -1;
        for (int x : nums) {
            int t = x, cur = 0;
            while (t != 0) {
                cur += t % 10;
                t /= 10;
            }
            if (val[cur] != 0) ans = Math.max(ans, val[cur] + x);
            val[cur] = Math.max(val[cur], x);
        }
        return ans;
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    int maximumSum(vector<int>& nums) {
        vector<int> val(100, 0);
        int ans = -1;
        for (int x : nums) {
            int t = x, cur = 0;
            while (t != 0) {
                cur += t % 10;
                t /= 10;
            }
            if (val[cur] != 0) ans = max(ans, val[cur] + x);
            val[cur] = max(val[cur], x);
        }
        return ans;
    }
};
```
Python 代码:
```Python
class Solution:
    def maximumSum(self, nums: List[int]) -> int:
        val = [0] * 100
        ans = -1
        for x in nums:
            t, cur = x, 0
            while t != 0:
                cur += t % 10
                t //= 10
            if val[cur] != 0:
                ans = max(ans, val[cur] + x)
            val[cur] = max(val[cur], x)
        return ans
```
TypeScript 代码:
```TypeScript
function maximumSum(nums: number[]): number {
    const val = Array(100).fill(0);
    let ans = -1;
    for (const x of nums) {
        let t = x, cur = 0;
        while (t !== 0) {
            cur += t % 10;
            t = Math.floor(t / 10);
        }
        if (val[cur] !== 0) ans = Math.max(ans, val[cur] + x);
        val[cur] = Math.max(val[cur], x);
    }
    return ans;
};
```
* 时间复杂度:$O(n\log{m})$,其中 $n=1e5$ 为 `nums` 长度,$m=1e9$ 为 $nums[i]$ 值域上界
* 空间复杂度:$O(C)$,其中 $C = 9 \times \log{m}$

### 最后

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

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

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

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


LeetCode/241-250/241. 为运算表达式设计优先级（中等）.md

241. 为运算表达式设计优先级

Question:
给你一个由数字和运算符组成的字符串 `expression`,按不同优先级组合数字和运算符,计算并返回所有可能组合的结果。你可以 按任意顺序 返回答案。

生成的测试用例满足其对应输出值符合 $32$ 位整数范围,不同结果的数量不超过 $10^4$ 。

示例 1:
```
输入:expression = "2-1-1"

输出:[0,2]

解释:
((2-1)-1) = 0
(2-(1-1)) = 2
```
示例 2:
```
输入:expression = "2*3-4*5"

输出:[-34,-14,-10,-10,10]

解释:
(2*(3-(4*5))) = -34
((2*3)-(4*5)) = -14
((2*(3-4))*5) = -10
(2*((3-4)*5)) = -10
(((2*3)-4)*5) = 10
```

提示:
* $1 <= expression.length <= 20$
* `expression` 由数字和算符 `'+'`、`'-'` 和 `'*'` 组成。
* 输入表达式中的所有整数值在范围 $[0, 99]$

Solution:
### DFS

为了方便,我们令 `expression` 为 `s`。

数据范围为 $20$,且要统计所有的计算结果,我们可以运用 `DFS` 爆搜所有方案。

给定的 `s` 只有数字和运算符,我们可以根据运算符将式子分为左右两部分,设计递归函数 `List<Integer> dfs(int l, int r)`,含义为搜索子串 $s[l...r]$ 的所有运算结果。

最终答案为 `dfs(0,n-1)`,其中 $n$ 为入参字符串的长度,同时我们有显而易见的递归出口:当给定的 $s[l...r]$ 不包含任何运算符时,搜索结果为 $s[l...r]$ 所代表的数字本身。

考虑如何对任意 $s[l...r]$ 进行计算:我们可以通过枚举 $s[l...r]$ 范围内的所有的运算符位置来进行爆搜,假设当前枚举到的 $s[i]$ 为运算符,我们可以递归运算符的左边 `dfs(l,i-1)` 拿到左边所有的结果,递归运算符右边 `dfs(i+1,r)` 拿到右边的所有结果,结合「乘法原理」即可知道以当前运算符 $s[i]$ 为分割点的表达式的所有方案。

不难发现,上述过程都是由「小表达式」的结果推导出「大表达式」的结果,因此也可以运用「区间 DP」方式进行求解,复杂度与 `DFS` 一致。

Java 代码:
```Java
class Solution {
    char[] cs;
    public List<Integer> diffWaysToCompute(String s) {
        cs = s.toCharArray();
        return dfs(0, cs.length - 1);
    }
    List<Integer> dfs(int l, int r) {
        List<Integer> ans = new ArrayList<>();
        for (int i = l; i <= r; i++) {
            if (cs[i] >= '0' && cs[i] <= '9') continue;
            List<Integer> l1 = dfs(l, i - 1), l2 = dfs(i + 1, r);
            for (int a : l1) {
                for (int b : l2) {
                    int cur = 0;
                    if (cs[i] == '+') cur = a + b;
                    else if (cs[i] == '-') cur = a - b;
                    else cur = a * b;
                    ans.add(cur);
                }
            }
        }
        if (ans.isEmpty()) {
            int cur = 0;
            for (int i = l; i <= r; i++) cur = cur * 10 + (cs[i] - '0');
            ans.add(cur);
        }
        return ans;
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    string cs;
    vector<int> diffWaysToCompute(string s) {
        cs = s;
        return dfs(0, cs.size() - 1);
    }
    vector<int> dfs(int l, int r) {
        vector<int> ans;
        for (int i = l; i <= r; i++) {
            if (cs[i] >= '0' && cs[i] <= '9') continue;
            auto l1 = dfs(l, i - 1), l2 = dfs(i + 1, r);
            for (int a : l1) {
                for (int b : l2) {
                    int cur = 0;
                    if (cs[i] == '+') cur = a + b;
                    else if (cs[i] == '-') cur = a - b;
                    else cur = a * b;
                    ans.push_back(cur);
                }
            }
        }
        if (ans.empty()) {
            int cur = 0;
            for (int i = l; i <= r; i++) cur = cur * 10 + (cs[i] - '0');
            ans.push_back(cur);
        }
        return ans;
    }
};
```
Python 代码:
```Python
class Solution:
    def diffWaysToCompute(self, cs: str) -> List[int]:
        def dfs(l: int, r: int) -> List[int]:
            ans = []
            for i in range(l, r + 1):
                if '0' <= cs[i] <= '9': continue
                l1, l2 = dfs(l, i - 1), dfs(i + 1, r)
                for a, b in product(l1, l2):
                    cur = 0
                    if cs[i] == '+':
                        cur = a + b
                    elif cs[i] == '-':
                        cur = a - b
                    else:
                        cur = a * b
                    ans.append(cur)
            if not ans:
                cur = 0
                for i in range(l, r + 1):
                    cur = cur * 10 + (ord(cs[i]) - ord('0'))
                ans.append(cur)
            return ans
        return dfs(0, len(cs) - 1)
```
TypeScript 代码:
```TypeScript
function diffWaysToCompute(cs: string): number[] {
    const dfs = function(l: number, r: number): number[] {
        const ans = [];
        for (let i = l; i <= r; i++) {
            if ("0" <= cs[i] && cs[i] <= "9") continue;
            const l1 = dfs(l, i - 1), l2 = dfs(i + 1, r);
            for (const a of l1) {
                for (const b of l2) {
                    let cur = 0;
                    if (cs[i] === "+") cur = a + b;
                    else if (cs[i] === "-") cur = a - b;
                    else cur = a * b;
                    ans.push(cur);
                }
            }
        }
        if (ans.length == 0) {
            let cur = 0;
            for (let i = l; i <= r; i++) cur = cur * 10 + (parseInt(cs[i]) - 0);
            ans.push(cur);
        }
        return ans;
    }
    return dfs(0, cs.length - 1);
};
```
* 时间复杂度:复杂度与最终结果数相关,最终结果数为「卡特兰数」,复杂度为 $O(C_{n})$
* 空间复杂度:复杂度与最终结果数相关,最终结果数为「卡特兰数」,复杂度为 $O(C_{n})$

### 最后

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

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

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

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


LeetCode/2471-2480/2477. 到达首都的最少油耗（中等）.md

2477. 到达首都的最少油耗

Question:
给你一棵 `n` 个节点的树(一个无向、连通、无环图),每个节点表示一个城市,编号从 `0` 到 `n - 1`,且恰好有 `n - 1` 条路。

`0` 是首都。给你一个二维整数数组 `roads`,其中 $roads[i] = [a_{i}, b_{i}]$ ,表示城市 $a_{i}$ 和 $b_{i}$ 之间有一条 双向路 。

每个城市里有一个代表,他们都要去首都参加一个会议。

每座城市里有一辆车。给你一个整数 `seats` 表示每辆车里面座位的数目。

城市里的代表可以选择乘坐所在城市的车,或者乘坐其他城市的车。相邻城市之间一辆车的油耗是一升汽油。

请你返回到达首都最少需要多少升汽油。

示例 1:

```
输入:roads = [[0,1],[0,2],[0,3]], seats = 5

输出:3

解释:
- 代表 1 直接到达首都,消耗 1 升汽油。
- 代表 2 直接到达首都,消耗 1 升汽油。
- 代表 3 直接到达首都,消耗 1 升汽油。
最少消耗 3 升汽油。
```
示例 2:

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

输出:7

解释:
- 代表 2 到达城市 3 ,消耗 1 升汽油。
- 代表 2 和代表 3 一起到达城市 1 ,消耗 1 升汽油。
- 代表 2 和代表 3 一起到达首都,消耗 1 升汽油。
- 代表 1 直接到达首都,消耗 1 升汽油。
- 代表 5 直接到达首都,消耗 1 升汽油。
- 代表 6 到达城市 4 ,消耗 1 升汽油。
- 代表 4 和代表 6 一起到达首都,消耗 1 升汽油。
最少消耗 7 升汽油。
```
示例 3:

```
输入:roads = [], seats = 1

输出:0

解释:没有代表需要从别的城市到达首都。
```

提示:
* $1 <= n <= 105$
* $roads.length = n - 1$
* $roads[i].length = 2$
* $0 <= a_{i}, b_{i} < n$
* $a_{i} != b_{i}$
* `roads` 表示一棵合法的树。
* $1 <= seats <= 10^5$

Solution:
### DFS

将双向图看作是以节点 `0` 为根的有向树,从每个节点出发往 `0` 前行,可看作是自底向上的移动过程。

当 `seats = 1` 时,每个节点前往 `0` 的过程相互独立,总油耗为每节点到 `0` 的最短距离之和。

当 `seats` 不为 `1` 时,考虑组成顺风车,此时总的油耗不该超过 `seats = 1` 的情况。

不难发现,**只有「深度大的节点,在前往 `0` 过程中,搭乘深度小顺风车」可减少油耗**(例如在上图节点 `3` 在经过节点 `1` 时搭乘顺风车,可与节点 `1` 合计使用一份油耗前往到 `0`),否则如果是深度小的节点先往深度大的节点走,再一同前往 `0`,会额外多经过某些边,产生不必要的油耗。

考虑组成顺风车时,总油耗该如何计算。

基于上述分析,无论 `seats` 是否为 $1$(是否组成顺风车),每个节点前往 `0` 的路径总是不变,即经过的边固定不变,必然是自底向上。

因此我们可统计每条边会被多少个节点经过,通过 `DFS` 统计「以每个节点为根时,子树的节点数量」即是经过该节点往上的边。

知道经过某条边的节点数量 `cnt` 后,$\left \lceil \frac{cnt}{seats} \right \rceil$ 即是该边对答案的贡献。

Java 代码:

```Java
class Solution {
    int N = 100010, M = 2 * N, idx = 0;
    int[] he = new int[N], e = new int[M], ne = new int[M];
    void add(int a, int b) {
        e[idx] = b;
        ne[idx] = he[a];
        he[a] = idx++;
    }
    long ans = 0;
    public long minimumFuelCost(int[][] roads, int seats) {
        int n = roads.length + 1;
        Arrays.fill(he, -1);
        for (int[] r : roads) {
            int a = r[0], b = r[1];
            add(a, b); add(b, a);
        }
        dfs(0, -1, seats);
        return ans;
    }
    int dfs(int u, int fa, int t) {
        int cnt = 1;
        for (int i = he[u]; i != -1; i = ne[i]) {
            int j = e[i];
            if (j == fa) continue;
            cnt += dfs(j, u, t);
        }
        if (u != 0) ans += Math.ceil(cnt * 1.0 / t);
        return cnt;
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    int N = 100010, M = 2 * N, idx = 0;
    int he[100010], e[200020], ne[200020];
    long long ans = 0;
    void add(int a, int b) {
        e[idx] = b;
        ne[idx] = he[a];
        he[a] = idx++;
    }
    long long minimumFuelCost(vector<vector<int>>& roads, int seats) {
        int n = roads.size() + 1;
        memset(he, -1, sizeof(he));
        for (auto& r : roads) {
            int a = r[0], b = r[1];
            add(a, b); add(b, a);
        }
        dfs(0, -1, seats);
        return ans;
    }
    int dfs(int u, int fa, int t) {
        int cnt = 1;
        for (int i = he[u]; i != -1; i = ne[i]) {
            int j = e[i];
            if (j == fa) continue;
            cnt += dfs(j, u, t);
        }
        if (u != 0) ans += ceil(cnt * 1.0 / t);
        return cnt;
    }
};
```
Python 代码:
```Python
class Solution:
    def minimumFuelCost(self, roads: List[List[int]], seats: int) -> int:
        n, m, ans = len(roads) + 1, len(roads) * 2 + 1, 0
        he, e, ne, idx = [-1] * n, [0] * m, [0] * m, 0

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

        def dfs(u, fa, t):
            nonlocal ans
            cnt = 1
            i = he[u]
            while i != -1:
                j, i = e[i], ne[i]
                if j == fa: continue
                cnt += dfs(j, u, t)
            if u != 0:
                ans += math.ceil(cnt * 1.0 / t)
            return cnt

        for a, b in roads:
            add(a, b)
            add(b, a)
        dfs(0, -1, seats)
        return ans
```
TypeScript 代码:
```TypeScript
function minimumFuelCost(roads: number[][], seats: number): number {
    const n = roads.length + 1, m = n * 2;
    const he = Array(n).fill(-1), e = Array(m).fill(0), ne = Array(m).fill(0);
    let ans = 0, idx = 0;
    const add = function(a: number, b: number) {
        e[idx] = b;
        ne[idx] = he[a];
        he[a] = idx++;
    }
    const dfs = function(u: number, fa: number, t: number): number {
        let cnt = 1;
        for (let i = he[u]; i != -1; i = ne[i]) {
            const j = e[i];
            if (j == fa) continue;
            cnt += dfs(j, u, t);
        }
        if (u != 0) ans += Math.ceil(cnt * 1.0 / t);
        return cnt;
    }
    for (const [a, b] of roads) {
        add(a, b); add(b, a);
    }
    dfs(0, -1, seats);
    return ans;
};
```
* 时间复杂度:建图复杂度为 $O(n)$;递归统计每个子树节点数量并计算油耗复杂度 $O(n)$;整体复杂度为 $O(n)$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


Question:
给定一个非负整数 `num`,反复将各个位上的数字相加,直到结果为一位数。返回这个结果。

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

输出: 2

解释: 各位相加的过程为:
38 --> 3 + 8 --> 11
11 --> 1 + 1 --> 2
由于 2 是一位数,所以返回 2。
```
示例 1:
```
输入: num = 0

输出: 0
```

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

进阶:你可以不使用循环或者递归,在 $O(1)$ 时间复杂度内解决这个问题吗?

Solution:
### 模拟

一个直观的做法是根据题意进行模拟,只要 `num` 位数多于 $1$ 位就可以继续做。

代码:
```Java
class Solution {
    public int addDigits(int num) {
        while (num > 9) {
            int t = 0;
            while (num != 0) {
                t += num % 10;
                num /= 10;
            }
            num = t;
        }
        return num;
    }
}
```
* 时间复杂度:$O(\log{num})$
* 空间复杂度:$O(1)$

---

### 数学

这题还是道对 [同余定理](https://baike.baidu.com/item/%E5%90%8C%E4%BD%99%E5%AE%9A%E7%90%86/1212360) 应用的题目。

对于任意一个正整数而言,我们最终是要求得 $a_0 + a_1 + a_2 + ... + a_{n - 1}$ 对 $9$ 取模的数。

而利用「同余式相加」性质,等价于每个数分别对 $9$ 取模之和,再集合任意的 $10^n$ 对 $9$ 取模恒为 $1$,可得最终答案为原数对 $9$ 取模,剩下只需要对相加结果为 $9$ 的边界情况进行处理即可。

代码:
```Java
class Solution {
    public int addDigits(int num) {
        return (num - 1) % 9 + 1;
    }
}
```
* 时间复杂度:$O(1)$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


260. 只出现一次的数字 III

Question:
给定一个整数数组 `nums`,其中恰好有两个元素只出现一次,其余所有元素均出现两次。

找出只出现一次的那两个元素,你可以按任意顺序返回答案。

示例 1:

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

输出:[3,5]

解释:[5, 3] 也是有效的答案。
```
示例 2:
```
输入:nums = [-1,0]

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

输出:[1,0]
```
提示:
* $2 <= nums.length <= 3 \times 10^4$

* $-2^{31} <= nums[i] <= 2^{31} - 1$

* 除两个只出现一次的整数外,`nums` 中的其他数字都出现两次

进阶:你的算法应该具有线性时间复杂度,你能否仅使用常数空间复杂度来实现?

Solution:
### 哈希表

朴素的做法是利用哈希表进行统计,最后将统计次数为 $1$ 的元素加入答案。

Java 代码:
```Java
class Solution {
    public int[] singleNumber(int[] nums) {
        Map<Integer, Integer> map = new HashMap<>();
        for (int i : nums) map.put(i, map.getOrDefault(i, 0) + 1);
        int[] ans = new int[2];
        int idx = 0;
        for (int i : nums) {
            if (map.get(i) == 1) ans[idx++] = i;
        }
        return ans;
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    vector<int> singleNumber(vector<int>& nums) {
        unordered_map<int, int> cmap;
        for (int num : nums) cmap[num]++;
        vector<int> ans;
        for (int num : nums) {
            if (cmap[num] == 1) ans.push_back(num);
        }
        return ans;
    }
};
```
Python 代码:
```Python
class Solution:
    def singleNumber(self, nums: List[int]) -> List[int]:
        count_map = Counter(nums)
        return [num for num in nums if count_map[num] == 1]
```
TypeScript 代码:
```TypeScript
function singleNumber(nums: number[]): number[] {
    const countMap: Map<number, number> = new Map();
    for (const num of nums) countMap.set(num, (countMap.get(num) || 0) + 1);
    const ans: number[] = [];
    for (const num of nums) {
        if (countMap.get(num) === 1) ans.push(num);
    }
    return ans;
};
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n)$

---

### 异或

利用除答案以外的元素均出现两次,我们可以先对 $nums$ 中的所有元素执行异或操作,得到 $sum$,$sum$ 为两答案的异或值($sum$ 必然不为 $0$)。

然后取 $sum$ 二进制表示中为 $1$ 的任意一位 $k$,$sum$ 中的第 $k$ 位为 $1$ 意味着两答案的第 $k$ 位二进制表示不同。

对 $nums$ 进行遍历,对第 $k$ 位分别为 $0$ 和 $1$ 的元素分别求异或和(两答案必然会被分到不同的组),即为答案。

Java 代码:
```Java
class Solution {
    public int[] singleNumber(int[] nums) {
        int sum = 0, k = -1;
        for (int i : nums) sum ^= i;
        for (int i = 31; i >= 0 && k == -1; i--) {
            if (((sum >> i) & 1) == 1) k = i;
        }
        int[] ans = new int[2];
        for (int i : nums) {
            if (((i >> k) & 1) == 1) ans[1] ^= i;
            else ans[0] ^= i;
        }
        return ans;
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    vector<int> singleNumber(vector<int>& nums) {
        int sum = 0, k = -1;
        for (int num : nums) sum ^= num;
        for (int i = 31; i >= 0 && k == -1; i--) {
            if ((sum >> i) & 1) k = i;
        }
        vector<int> ans(2, 0);
        for (int num : nums) {
            if ((num >> k) & 1) ans[1] ^= num;
            else ans[0] ^= num;
        }
        return ans;
    }
};
```
Python 代码:
```Python
class Solution:
    def singleNumber(self, nums: List[int]) -> List[int]:
        sum_val, k = 0, -1
        for num in nums: sum_val ^= num
        for i in range(31, -1, -1):
            if (sum_val >> i) & 1:
                k = i
                break
        ans = [0, 0]
        for num in nums:
            if (num >> k) & 1: ans[1] ^= num
            else: ans[0] ^= num
        return ans
```
TypeScript 代码:
```TypeScript
function singleNumber(nums: number[]): number[] {
    let sum = 0, k = -1;
    for (const num of nums) sum ^= num;
    for (let i = 31; i >= 0 && k === -1; i--) {
        if (((sum >> i) & 1) === 1) k = i;
    }
    const ans: number[] = [0, 0];
    for (const num of nums) {
        if (((num >> k) & 1) === 1) ans[1] ^= num;
        else ans[0] ^= num;
    }
    return ans;
};
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


LeetCode/2511-2520/2520. 统计能整除数字的位数（简单）.md

2520. 统计能整除数字的位数

Question:
给你一个整数 `num`,返回 `num` 中能整除 `num` 的数位的数目。

如果满足 `nums % val == 0`,则认为整数 `val` 可以整除 `nums`。

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

输出:1

解释:7 被自己整除,因此答案是 1 。
```
示例 2:
```
输入:num = 121

输出:2

解释:121 可以被 1 整除,但无法被 2 整除。由于 1 出现两次,所以返回 2 。
```
示例 3:
```
输入:num = 1248

输出:4

解释:1248 可以被它每一位上的数字整除,因此答案是 4 。
```

提示:
* $1 <= num <= 10^9$
* `num` 的数位中不含 `0`

Solution:
### 模拟

根据题意进行模拟即可。

假设起始值为 `num`,符合要求的数位个数为 `ans`,按照「从低到高」的方式循环处理 `num` 中的每个数位:

1. 每次仅对当前数值 `num` 的最低位进行判断(当前数值 `num` 会被不断更新),**通过「模 $10$」的方式来「截取当前数值的最低位」**,并根据要求进行判断,若当前数位符合要求,对 `ans` 进行加一;

2. 判断完当前数组 `num` 的最低位后,**通过「与 $10$ 地板除」的方式来「摒弃当前数值的最低位」**,并以此更新 `num`;

3. 循环步骤 `1` 和步骤 `2`,直到 `num` 所有数位均被处理完,即 `num = 0`。

用题面的示例 3 来举个 🌰,给定 `num = 1248`,完整处理过程如下:

1. 当前数 `num = 1248`,通过 `mod 10` 取最低位 `8`,该数值满足要求,`ans = 1`;

   通过 `// 10` 摒弃已处理的最低位,得到 `num = 124`

2. 当前数 `num = 124`,通过 `mod 10` 取最低位 `4`,该数值满足要求,`ans = 2`;

   通过 `// 10` 摒弃已处理的最低位,得到 `num = 12`

3. 当前数 `num = 12`,通过 `mod 10` 取最低位 `2`,该数值满足要求,`ans = 3`;

   通过 `// 10` 摒弃已处理的最低位,得到 `num = 1`

4. 当前数 `num = 1`,通过 `mod 10` 取最低位 `1`,该数值满足要求,`ans = 4`;

   通过 `// 10` 摒弃已处理的最低位,得到 `num = 0`,循环结束

Java 代码:
```Java
class Solution {
    public int countDigits(int num) {
        int n = num, ans = 0;
        while (num != 0) {
            ans += n % (num % 10) == 0 ? 1 : 0;
            num /= 10;
        }
        return ans;
    }
}
```
Python 代码:
```Python
class Solution:
    def countDigits(self, num: int) -> int:
        return sum(num % int(i) == 0 for i in str(num))

        # n, ans = num, 0
        # while num != 0:
        #     ans += n % (num % 10) == 0
        #     num //= 10
        # return ans
```
C++ 代码:
```C++
class Solution {
public:
    int countDigits(int num) {
        int n = num, ans = 0;
        while (num != 0) {
            ans += n % (num % 10) == 0;
            num /= 10;
        }
        return ans;
    }
};
```
TypeScript 代码:
```TypeScript
function countDigits(num: number): number {
    let n = num, ans = 0;
    while (num !== 0) {
        ans += n % (num % 10) == 0 ? 1 : 0;
        num = Math.floor(num / 10);
    }
    return ans;
};
```
Kotlin 代码:
```Kotlin
class Solution {
    fun countDigits(num: Int): Int {
        return num.toString().count { x -> num % (x - '0') == 0 }
    }
}
```
* 时间复杂度:$O(\log{n})$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


LeetCode/2551-2560/2558. 从数量最多的堆取走礼物（简单）.md

2558. 从数量最多的堆取走礼物

Question:
给你一个整数数组 `gifts`,表示各堆礼物的数量。

每一秒,你需要执行以下操作:

* 选择礼物数量最多的那一堆。

* 如果不止一堆都符合礼物数量最多,从中选择任一堆即可。

* 选中的那一堆留下平方根数量的礼物(向下取整),取走其他的礼物。

返回在 `k` 秒后剩下的礼物数量。

示例 1:
```
输入:gifts = [25,64,9,4,100], k = 4

输出:29

解释:
按下述方式取走礼物:
- 在第一秒,选中最后一堆,剩下 10 个礼物。
- 接着第二秒选中第二堆礼物,剩下 8 个礼物。
- 然后选中第一堆礼物,剩下 5 个礼物。
- 最后,再次选中最后一堆礼物,剩下 3 个礼物。
最后剩下的礼物数量分别是 [5,8,9,4,3] ,所以,剩下礼物的总数量是 29 。
```
示例 2:
```
输入:gifts = [1,1,1,1], k = 4

输出:4

解释:
在本例中,不管选中哪一堆礼物,都必须剩下 1 个礼物。
也就是说,你无法获取任一堆中的礼物。
所以,剩下礼物的总数量是 4 。
```

提示:
* $1 <= gifts.length <= 10^3$
* $1 <= gifts[i] <= 10^9$
* $1 <= k <= 10^3$

Solution:
### 基本思想

为了方便,将 `gifts` 记为 `gs`。

从题面看,数组大小和执行次数范围均为 $1e3$,那么暴力做法的复杂度为 $O(n^2)$,计算量为 $1e6$,是可以通过的。

稍有数据结构基础的同学,容易进一步联想:存在一种数据结构,能够快速查得集合最值,使复杂度降低。

该数据结构是「堆」,在某些 `STL`  里面又称为「优先队列」。

---

### 手写堆入门

所有高级数据结构(例如之前讲过的 [字典树 Trie] 或 [并查集],都可以使用数组进行模拟,堆自然也不例外。

**堆本质是数据集合,只不过区别于简单数组而言,其具有 $O(1)$ 查找最值特性。**

既然我们使用数组来实现堆,同时又需要实现 $O(1)$ 查找最值,一个自然而然的想法,是把集合最值放到数组的特定位置,例如数组起始位置。

开始之前,先来了解一下,堆的基本操作:

* `add`:往堆中增加元素
* `peek`:快速查得最值
* `pop`:将最值从堆中移除

我们以小根堆为例,进行学习。

> PS. 大根堆亦是同理,如果已经有写好的小根堆模板,那么将数值进行符号翻转,即可实现大根堆;或是翻转模板中的数值比较逻辑,也可将小根堆轻松切换成大根堆。

#### 1. 堆长啥样?

堆是数组实现的,但其形态为「完全二叉树」(最多只有底下一层节点不满),目的是尽量让树平衡,降低树的高度,从而更高效的维护其性质。

虽是「完全二叉树」,但其又和另外一种特殊二叉树,二叉搜索树(`BST`)不同。

在 `BST`  的定义中:任意节点的左子树上的节点值均不大于该节点,任意节点的右子树上的节点值均不小于该节点。

在小根堆对应的「完全二叉树」定义中:任意节点的节点值均不大于其左右子树中的节点值。

因此将小根堆可视化后,也十分形象:小根堆的堆顶元素最小,然后从上往下,元素“依次”增大。

{:width=400}

即对于小根堆而言,每个子树的根节点,都是该子树的最小值。

#### 2. 如何用数组进行「完全二叉树」的存储?

我们只需要将「二叉树的左右子节点关系」和「数组下标」实现某种关联即可。

我们约定,对于某个节点,若其所在数组下标为 $idx$,那么该节点的左子节点的数组下标为 $2 \times idx$,该节点的右子节点的数组下标为 $2 \times idx + 1$。

注意:基于此规则,我们需要调整数组下标从 $1$ 开始进行存储。

{:width=400}

#### 3.基本操作如何实现?

所有操作的核心最终都归结到:如何通过有限的调整,重新恢复堆所对应的完全二叉树中的节点定义。

因此可抽象出 `up` 操作和 `down` 操作,俩操作均通过递归实现:

* `up` 操作是将当前节点 `u` 与父节点 `fa = u / 2` 进行比较,若发现父节点更大,则将两者进行互换,并递归处理父节点
* `down` 操作是将当前节点 `cur` 和左右节点 `l = cur * 2` 和 `r = cur * 2 + 1` 进行比较,若当前节点并非三者最小,则进行互换,并递归处理子节点

通过上述核心 `up` 和 `down` 操作,以及「完全二叉树」在数组的放置规则,我们可以容易拼凑出堆的基本操作。

假定当前使用一维数组 `heap` 实现堆,使用变量 `sz` 记录当前堆的元素个数,同时该变量充当 `heap` 的结尾游标:

* `add`:往堆中增加元素
  该操作可转换为:往数组尾部增加元素(`heap[sz++] = x`),并从尾部开始重整堆(`up(sz)`)
* `peek`:快速查得最小值
  直接范围数组首位元素,注意下标从 $1$ 开始,`heap[1]`
* `pop`:将最值从堆中移除
  先通过 `peek` 操作记录下待移除的最小值,然后将数组的结尾元素和首位元素互换,并更新数组长度减一(`heap[1] = heap[sz--]`),随后从头部开始重整堆(`down(1)`)

Java 完整模板:
```Java
int[] heap = new int[10010];
int sz = 0;
void swap(int a, int b) {
    int c = heap[a];
    heap[a] = heap[b];
    heap[b] = c;
}
void up(int u) {
    // 将「当前节点 i」与「父节点 i / 2」进行比较, 若父节点值更大, 则进行交换
    int fa = u / 2;
    if (fa != 0 && heap[fa] > heap[u]) {
        swap(fa, u);
        up(fa);
    }
}
void down(int u) {
    // 将当「前节点 cur」与「左节点 l」及「右节点 r」进行比较, 找出最小值, 若当前节点不是最小值, 则进行交换
    int cur = u;
    int l = u * 2, r = u * 2 + 1;
    if (l <= sz && heap[l] < heap[cur]) cur = l;
    if (r <= sz && heap[r] < heap[cur]) cur = r;
    if (cur != u) {
        swap(cur, u);
        down(cur);
    }
}
void add(int x) {
    heap[++sz] = x;
    up(sz);
}
int peek() {
    return heap[1];
}
int pop() {
    int ans = peek();
    heap[1] = heap[sz--];
    down(1);
    return ans;
}
```
C++ 完整模板:
```C++
int heap[1010];
int sz = 0;
void up(int u) {
    int fa = u / 2;
    if (fa && heap[fa] >= heap[u]) {
        swap(heap[fa], heap[u]);
        up(fa);
    }
}
void down(int u) {
    int cur = u;
    int l = cur * 2, r = cur * 2 + 1;
    if (l <= sz && heap[l] < heap[cur]) cur = l;
    if (r <= sz && heap[r] < heap[cur]) cur = r;
    if (cur != u) {
        swap(heap[cur], heap[u]);
        down(cur);
    }
}
void add(int x) {
    heap[++sz] = x;
    up(sz);
}
int peek() {
    return heap[1];
}
int poll() {
    int ans = peek();
    heap[1] = heap[sz--];
    down(1);
    return ans;
}
```

---

### 模板运用

利用「上述模板」以及「小根堆如何当大根堆」使用,我们可以轻松解决本题。

1. 起始先将逐元素进行符号翻转并入堆
2. 执行 `k` 次取出并重放操作(注意符号转换)
3. 统计所有元素之和,由于此时不再需要查询最值,可通过直接扫描数组的方式(注意符号转换)

Java 代码:
```Java
class Solution {
    int[] heap = new int[10010];
    int sz = 0;
    public long pickGifts(int[] gs, int k) {
        for (int x : gs) add(-x);
        while (k-- > 0) add(-(int)Math.sqrt(-pop()));
        long ans = 0;
        while (sz != 0) ans += -heap[sz--];  // 没必要再维持堆的有序, 直接读取累加
        return ans;
    }
    void swap(int a, int b) {
        int c = heap[a];
        heap[a] = heap[b];
        heap[b] = c;
    }
    void up(int u) {
        // 将「当前节点 i」与「父节点 i / 2」进行比较, 若父节点值更大, 则进行交换
        int fa = u / 2;
        if (fa != 0 && heap[fa] > heap[u]) {
            swap(fa, u);
            up(fa);
        }
    }
    void down(int u) {
        // 将当「前节点 cur」与「左节点 l」及「右节点 r」进行比较, 找出最小值, 若当前节点不是最小值, 则进行交换
        int cur = u;
        int l = u * 2, r = u * 2 + 1;
        if (l <= sz && heap[l] < heap[cur]) cur = l;
        if (r <= sz && heap[r] < heap[cur]) cur = r;
        if (cur != u) {
            swap(cur, u);
            down(cur);
        }
    }
    void add(int x) {
        heap[++sz] = x;
        up(sz);
    }
    int peek() {
        return heap[1];
    }
    int pop() {
        int ans = peek();
        heap[1] = heap[sz--];
        down(1);
        return ans;
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    int heap[1010];
    int sz = 0;
    long long pickGifts(vector<int>& gs, int k) {
        for (int x : gs) add(-x);
        while (k--) add(-sqrt(-poll()));
        long ans = 0;
        while (sz != 0) ans += -heap[sz--];
        return ans;
    }
    void up(int u) {
        int fa = u / 2;
        if (fa && heap[fa] >= heap[u]) {
            swap(heap[fa], heap[u]);
            up(fa);
        }
    }
    void down(int u) {
        int cur = u;
        int l = cur * 2, r = cur * 2 + 1;
        if (l <= sz && heap[l] < heap[cur]) cur = l;
        if (r <= sz && heap[r] < heap[cur]) cur = r;
        if (cur != u) {
            swap(heap[cur], heap[u]);
            down(cur);
        }
    }
    void add(int x) {
        heap[++sz] = x;
        up(sz);
    }
    int peek() {
        return heap[1];
    }
    int poll() {
        int ans = peek();
        heap[1] = heap[sz--];
        down(1);
        return ans;
    }
};
```
* 时间复杂度:建堆复杂度为 $O(n\log{n})$;执行 `k` 次操作复杂度为 $O(k\log{n})$;构建答案复杂度为 $O(n)$。整体复杂度为 $O((n + k) \log{n})$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


LeetCode/2581-2590/2586. 统计范围内的元音字符串数（简单）.md

2586. 统计范围内的元音字符串数

Question:
给你一个下标从 $0$ 开始的字符串数组 `words` 和两个整数:`left` 和 `right`。

如果字符串以元音字母开头并以元音字母结尾,那么该字符串就是一个 元音字符串 ,其中元音字母是 `'a'`、`'e'`、`'i'`、`'o'`、`'u'`。

返回 $words[i]$ 是元音字符串的数目,其中 `i` 在闭区间 $[left, right]$ 内。

示例 1:
```
输入:words = ["are","amy","u"], left = 0, right = 2

输出:2

解释:
- "are" 是一个元音字符串,因为它以 'a' 开头并以 'e' 结尾。
- "amy" 不是元音字符串,因为它没有以元音字母结尾。
- "u" 是一个元音字符串,因为它以 'u' 开头并以 'u' 结尾。
在上述范围中的元音字符串数目为 2 。
```
示例 2:
```
输入:words = ["hey","aeo","mu","ooo","artro"], left = 1, right = 4

输出:3

解释:
- "aeo" 是一个元音字符串,因为它以 'a' 开头并以 'o' 结尾。
- "mu" 不是元音字符串,因为它没有以元音字母开头。
- "ooo" 是一个元音字符串,因为它以 'o' 开头并以 'o' 结尾。
- "artro" 是一个元音字符串,因为它以 'a' 开头并以 'o' 结尾。
在上述范围中的元音字符串数目为 3 。
```

提示:
* $1 <= words.length <= 1000$
* $1 <= words[i].length <= 10$
* $words[i]$ 仅由小写英文字母组成
* $0 <= left <= right < words.length$

Solution:
### 模拟

检查下标范围在 $[left, right]$ 内的字符串 $words[i]$,若当前 $words[i]$ 首尾字母为 `aeiou` 之一,进行计数。

Java 代码:

```Java
class Solution {
    public int vowelStrings(String[] words, int left, int right) {
        int ans = 0;
        for (int i = left; i <= right; i++) {
            String s = words[i];
            char a = s.charAt(0), b = s.charAt(s.length() - 1);
            if ("aeiou".indexOf(a) != -1 && "eaiou".indexOf(b) != -1) ans++;
        }
        return ans;
    }
}
```
Python 代码:
```Python
class Solution:
    def vowelStrings(self, words: List[str], left: int, right: int) -> int:
        return sum(1 for i in range(left, right + 1) if words[i][0] in "aeiou" and words[i][-1] in "aeiou")
```
C++ 代码:
```C++
class Solution {
public:
    int vowelStrings(vector<string>& words, int left, int right) {
        int ans = 0;
        for (int i = left; i <= right; i++) {
            string s = words[i];
            if (s.find_first_of("aeiou") == 0 && s.find_last_of("aeiou") == s.length() - 1) ans++;
        }
        return ans;
    }
};
```
TypeScript 代码:
```TypeScript
function vowelStrings(words: string[], left: number, right: number): number {
    let ans = 0;
    for (let i = left; i <= right; i++) {
        const s = words[i];
        if ("aeiou".includes(s[0]) && "aeiou".includes(s.charAt(s.length - 1))) ans++;
    }
    return ans;
};
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


LeetCode/2601-2610/2609. 最长平衡子字符串（简单）.md

2609. 最长平衡子字符串

Question:
给你一个仅由 `0` 和 `1` 组成的二进制字符串 `s` 。

如果子字符串中 所有的 `0` 都在 `1` 之前 且其中 `0` 的数量等于 `1` 的数量,则认为 `s` 的这个子字符串是平衡子字符串。

请注意,空子字符串也视作平衡子字符串。

返回  `s` 中最长的平衡子字符串长度。

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

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

输出:6

解释:最长的平衡子字符串是 "000111" ,长度为 6 。
```
示例 2:
```
输入:s = "00111"

输出:4

解释:最长的平衡子字符串是 "0011" ,长度为  4 。
```
示例 3:
```
输入:s = "111"

输出:0

解释:除了空子字符串之外不存在其他平衡子字符串,所以答案为 0 。
```

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

Solution:
### 模拟

根据题意,平衡子字符串必然满足 `0...01...1` 格式(前半段全是 `0`,后半段全是 `1`,前后两段长度相同)。

使用变量 `idx` 对 `s` 进行遍历。在每轮处理过程中,按照如下流程进行:

1. 先统计连续段 `0` 的长度,记为 `a`;再统计连续段 `1` 的长度,记为 `b`(此操作满足:子串中 `0` 均在 `1` 前面)
2. 在 `a` 和 `b` 中取较小值,进行乘 $2$ 操作,作为当前平衡子字符串的长度,用于更新答案(此操作满足:子串中 `0` 和 `1` 数量相同)
3. 从当前轮的结束位置 `idx`,再进行下轮处理(重复步骤 $1$ 和步骤 $2$),直到 `s` 处理完成

Java 代码:

```Java
class Solution {
    public int findTheLongestBalancedSubstring(String s) {
        int n = s.length(), idx = 0, ans = 0;
        while (idx < n) {
            int a = 0, b = 0;
            while (idx < n && s.charAt(idx) == '0' && ++a >= 0) idx++;
            while (idx < n && s.charAt(idx) == '1' && ++b >= 0) idx++;
            ans = Math.max(ans, Math.min(a, b) * 2);
        }
        return ans;
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    int findTheLongestBalancedSubstring(string s) {
        int n = s.size(), idx = 0, ans = 0;
        while (idx < n) {
            int a = 0, b = 0;
            while (idx < n && s[idx] == '0' && ++a >= 0) idx++;
            while (idx < n && s[idx] == '1' && ++b >= 0) idx++;
            ans = max(ans, min(a, b) * 2);
        }
        return ans;
    }
};
```
Python 代码:
```Python
class Solution:
    def findTheLongestBalancedSubstring(self, s: str) -> int:
        n, idx, ans = len(s), 0, 0
        while idx < n:
            a, b = 0, 0
            while idx < n and s[idx] == '0':
                a, idx = a + 1, idx + 1
            while idx < n and s[idx] == '1':
                b, idx = b + 1, idx + 1
            ans = max(ans, min(a, b) * 2)
        return ans
```
TypeScript 代码:
```TypeScript
function findTheLongestBalancedSubstring(s: string): number {
    let n = s.length, idx = 0, ans = 0;
    while (idx < n) {
        let a = 0, b = 0;
        while (idx < n && s[idx] == '0' && ++a >= 0) idx++;
        while (idx < n && s[idx] == '1' && ++b >= 0) idx++;
        ans = Math.max(ans, Math.min(a, b) * 2);
    }
    return ans;
};
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


Question:
给你一个整数 `n` ,请你判断 n 是否为 丑数 。如果是,返回 `true` ;否则,返回 `false` 。

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

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

输出:true

解释:6 = 2 × 3
```
示例 2:
```
输入:n = 8

输出:true

解释:8 = 2 × 2 × 2
```
示例 3:
```
输入:n = 14

输出:false

解释:14 不是丑数,因为它包含了另外一个质因数 7 。
```
示例 4:
```
输入:n = 1

输出:true

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

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

Solution:
### 朴素解法

输入范围是 $-2^{31} <= n <= 2^{31} - 1$,我们只需要对输入进行分情况讨论即可:

* 如果 $n$ 不是正整数(即小于等于 0):必然不是丑数,直接返回 `false`。
* 如果 $n$ 是正整数:我们对 $n$ 执行 `2`、 `3`、 `5` 的整除操作即可,直到 $n$ 被除干净,如果 $n$ 最终为 1 说明是丑数,否则不是丑数。

注意,`2` `3` `5` 先除哪一个都是可以的,因为乘法本身具有交换律。

代码:
```java
class Solution {
    public boolean isUgly(int n) {
        if (n <= 0) return false;
        while (n % 2 == 0) n /= 2;
        while (n % 3 == 0) n /= 3;
        while (n % 5 == 0) n /= 5;
        return n == 1;
    }
}
```
* 时间复杂度:当 $n$ 是以 $2$ 为底的对数时,需要除以 $\log{n}$ 次。复杂度为 $O(\log{n})$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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