Question:
给你一个正整数 $n$ ,生成一个包含 $1$ 到 $n^2$ 所有元素,且元素按顺时针顺序螺旋排列的 `n x n` 正方形矩阵 `matrix` 。

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

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

输出:[[1]]
```

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

Solution:
### 按照「形状」进行模拟

我们可以按「圈」进行构建。

使用「左上角」$(x1,y1)$ &「右下角」$(x2,y2)$ 来确定某个「圈」,进行构建。

完成后,令「左上角」&「右下角」往里收,分别得到 $(x1 + 1, y1 + 1)$ 和 $(x2 - 1, y2 - 1)$,执行相同的构建规则。

代码:
```Java
class Solution {
    public int[][] generateMatrix(int n) {
        int[][] ans = new int[n][n];
        circle(0, 0, n - 1, n - 1, 1, ans);
        return ans;
    }
    void circle(int x1, int y1, int x2, int y2, int start, int[][] ans) {
        if (x2 < x1 || y2 < y1) return ;
        if (x1 == x2) {
            ans[x1][y1] = start;
            return;
        }
        int val = start;
        for (int i = y1; i < y2; i++) ans[x1][i] = val++;
        for (int i = x1; i < x2; i++) ans[i][y2] = val++;
        for (int i = y2; i > y1; i--) ans[x2][i] = val++;
        for (int i = x2; i > x1; i--) ans[i][y1] = val++;
        circle(x1 + 1, y1 + 1, x2 - 1, y2 - 1, val, ans);
    }
}
```
* 时间复杂度:$O(n^2)$
* 空间复杂度:$O(n^2)$

---

### 按照「方向」进行模拟

事实上,我们还可以根据「方向」进行模拟。

因为每一圈的构建都是按照特定的「四个方向」进行的。

这种解法更为简洁。而触发方向转换的时机:

1. 下一步发生位置溢出
2. 回到了本圈的起点

代码:
```Java
class Solution {
    public int[][] generateMatrix(int n) {
        int[][] ans = new int[n][n];
        // 定义四个方向
        int[][] dirs = new int[][]{{0,1},{1,0},{0,-1},{-1,0}};
        for (int x = 0, y = 0, d = 0, i = 1; i <= n * n; i++) {
            ans[x][y] = i;
            // 下一步要到达的位置
            int nx = x + dirs[d][0], ny = y + dirs[d][1];
            // 如果下一步发生「溢出」或者已经访问过(说明四个方向已经走过一次)
            if (nx < 0 || nx >= n || ny < 0 || ny >= n || ans[nx][ny] != 0) {
                d = (d + 1) % 4;
                nx = x + dirs[d][0]; ny = y + dirs[d][1];
            }
            x = nx; y = ny;
        }
        return ans;
    }
}
```

-

```C++
class Solution {
public:
    const int dir[4][2] = { {0,1},{1,0},{0,-1},{-1,0}};
    vector<vector<int>> generateMatrix(int n) {
        vector<vector<int>> res(n,vector<int>(n,0));
        for(int i = 1, x = 0, y = 0, d = 0; i <= n * n; i++){
            res[x][y] = i;
            int t_x = x + dir[d][0],t_y = y + dir[d][1];
            if(t_x < 0 or t_x >= n or t_y < 0 or t_y >= n or res[t_x][t_y] != 0){
                d = (d + 1) % 4;
                t_x = x + dir[d][0],t_y = y + dir[d][1];
            }
            x = t_x, y = t_y;
        }
        return res;
    }
};
```
* 时间复杂度:$O(n^2)$
* 空间复杂度:$O(n^2)$

### 最后

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

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

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

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


513. 找树左下角的值

Question:
给定一个二叉树的 根节点 `root`,请找出该二叉树的 最底层 最左边 节点的值。

假设二叉树中至少有一个节点。

示例 1:

```
输入: root = [2,1,3]

输出: 1
```
示例 2:

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

输出: 7
```

提示:
* 二叉树的节点个数的范围是 $[1,10^4]$
* $-2^{31} <= Node.val <= 2^{31} - 1$

Solution:
### BFS

使用 `BFS` 进行「层序遍历」,每次用当前层的首个节点来更新 `ans`,当 `BFS` 结束后,`ans` 存储的是最后一层最靠左的节点。

代码:
```Java
class Solution {
    public int findBottomLeftValue(TreeNode root) {
        Deque<TreeNode> d = new ArrayDeque<>();
        d.addLast(root);
        int ans = 0;
        while (!d.isEmpty()) {
            int sz = d.size();
            ans = d.peek().val;
            while (sz-- > 0) {
                TreeNode poll = d.pollFirst();
                if (poll.left != null) d.addLast(poll.left);
                if (poll.right != null) d.addLast(poll.right);
            }
        }
        return ans;
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:最坏情况下所有节点都在同一层,复杂度为 $O(n)$

---

### DFS

同理,可以使用 `DFS`  进行树的遍历,每次优先 `DFS` 当前节点的左子树,每次第一次搜索到当前深度 `depth` 时,必然是当前深度的最左节点,此时用当前节点值来更新 `ans`。

代码:
```Java
class Solution {
    int max, ans;
    public int findBottomLeftValue(TreeNode root) {
        dfs(root, 1);
        return ans;
    }
    void dfs(TreeNode root, int depth) {
        if (root == null) return ;
        if (depth > max) {
            max = depth; ans = root.val;
        }
        dfs(root.left, depth + 1);
        dfs(root.right, depth + 1);
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:最坏情况下退化成链,递归深度为 $n$。复杂度为 $O(n)$

### 最后

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

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

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

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


515. 在每个树行中找最大值

Question:
给定一棵二叉树的根节点 `root`,请找出该二叉树中每一层的最大值。

示例1:

```
输入: root = [1,3,2,5,3,null,9]

输出: [1,3,9]
```
示例2:
```
输入: root = [1,2,3]

输出: [1,3]
```

提示:
* 二叉树的节点个数的范围是 $[0,10^4]$
* $-2^{31} <= Node.val <= 2^{31} - 1$

Solution:
### BFS

使用 `BFS` 进行层序遍历,单次 `BFS` 逻辑将整一层的元素进行出队,维护当前层的最大值,并将最大值加入答案。

代码:
```Java
class Solution {
    public List<Integer> largestValues(TreeNode root) {
        List<Integer> ans = new ArrayList<>();
        if (root == null) return ans;
        Deque<TreeNode> d = new ArrayDeque<>();
        d.addLast(root);
        while (!d.isEmpty()) {
            int sz = d.size(), max = d.peek().val;
            while (sz-- > 0) {
                TreeNode node = d.pollFirst();
                max = Math.max(max, node.val);
                if (node.left != null) d.addLast(node.left);
                if (node.right != null) d.addLast(node.right);
            }
            ans.add(max);
        }
        return ans;
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n)$

---

### DFS

同理,可以借助 `DFS` 进行求解,在 `DFS` 整棵树时,同时传递一个当前层深度 `depth`,使用「哈希表」维护所有深度的最大节点值,同时使用变量 `max` 记录最大深度。

结束 `DFS` 后,使用哈希表构造答案。

代码:
```Java
class Solution {
    int max = 0;
    Map<Integer, Integer> map = new HashMap<>();
    public List<Integer> largestValues(TreeNode root) {
        List<Integer> ans = new ArrayList<>();
        dfs(root, 1);
        for (int i = 1; i <= max; i++) ans.add(map.get(i));
        return ans;
    }
    void dfs(TreeNode node, int depth) {
        if (node == null) return ;
        max = Math.max(max, depth);
        map.put(depth, Math.max(map.getOrDefault(depth, Integer.MIN_VALUE), node.val));
        dfs(node.left, depth + 1);
        dfs(node.right, depth + 1);
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


516. 最长回文子序列

Question:
给你一个字符串 `s` ,找出其中最长的回文子序列,并返回该序列的长度。

子序列定义为:不改变剩余字符顺序的情况下,删除某些字符或者不删除任何字符形成的一个序列。

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

输出:4

解释:一个可能的最长回文子序列为 "bbbb" 。
```
示例 2:
```
输入:s = "cbbd"

输出:2

解释:一个可能的最长回文子序列为 "bb" 。
```

提示:
* $1 <= s.length <= 1000$
* `s` 仅由小写英文字母组成

Solution:
### 动态规划

这是一道经典的区间 DP 题。

**之所以可以使用区间 DP 进行求解,是因为在给定一个回文串的基础上,如果在回文串的边缘分别添加两个新的字符,可以通过判断两字符是否相等来得知新串是否回文。**

也就是说,使用小区间的回文状态可以推导出大区间的回文状态值。

**从图论意义出发就是,任何一个长度为 $len$ 的回文串,必然由「长度为 $len - 1$」或「长度为 $len - 2$」的回文串转移而来。**

**两个具有公共回文部分的回文串之间存在拓扑序(存在由「长度较小」回文串指向「长度较大」回文串的有向边)。**

通常区间 DP 问题都是,常见的基本流程为:

1. 从小到大枚举区间大小 $len$
2. 枚举区间左端点 $l$,同时根据区间大小 $len$ 和左端点计算出区间右端点 $r = l + len - 1$
3. 通过状态转移方程求 $f[l][r]$ 的值

因此,我们 **定义 $f[l][r]$ 为考虑区间 $[l, r]$ 的最长回文子序列长度为多少。**

不失一般性的考虑 $f[l][r]$ 该如何转移。

由于我们的状态定义 **没有限制** 回文串中必须要选 $s[l]$ 或者 $s[r]$。

我们对边界字符 $s[l]$ 和 $s[r]$ 分情况讨论,最终的 $f[l][r]$ 应该在如下几种方案中取 $max$ :

* 形成的回文串一定不包含 $s[l]$ 和 $s[r]$,即完全不考虑 $s[l]$ 和 $s[r]$:
$$
f[l][r] = f[l + 1][r - 1]
$$

* 形成的回文串可能包含 $s[l]$,但一定不包含 $s[r]$:

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

* 形成的回文串可能包含 $s[r]$,但一定不包含 $s[l]$:

$$
f[l][r] = f[l + 1][r]
$$

* 形成的回文串可能包含 $s[l]$,也可能包含 $s[r]$,根据 $s[l]$ 和 $s[r]$ 是否相等:

$$
f[l][r] = \begin{cases}
f[l + 1][r - 1] + 2 & s[l] = s[r] \\
f[l + 1][r - 1] & s[l] \neq s[r] \\
\end{cases}
$$

需要说明的是,上述几种情况可以确保我们做到「不漏」,但不能确保「不重」,对于求最值问题,我们只需要确保「不漏」即可,某些状态重复参与比较,不会影响结果的正确性。

> 一些细节:我们需要特判掉长度为 $1$ 和 $2$ 的两种基本情况。当长度为 $1$ 时,必然回文,当长度为 $2$ 时,当且仅当两字符相等时回文。

代码:
```Java
class Solution {
    public int longestPalindromeSubseq(String s) {
        int n = s.length();
        char[] cs = s.toCharArray();
        int[][] f = new int[n][n];
        for (int len = 1; len <= n; len++) {
            for (int l = 0; l + len - 1 < n; l++) {
                int r = l + len - 1;
                if (len == 1) {
                    f[l][r] = 1;
                } else if (len == 2) {
                    f[l][r] = cs[l] == cs[r] ? 2 : 1;
                } else {
                    f[l][r] = Math.max(f[l + 1][r], f[l][r - 1]);
                    f[l][r] = Math.max(f[l][r], f[l + 1][r - 1] + (cs[l] == cs[r] ? 2 : 0));
                }
            }
        }
        return f[0][n - 1];
    }
}
```
* 时间复杂度:$O(n^2)$
* 空间复杂度:$O(n^2)$

### 最后

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

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

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

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


Question:
假设有 `n` 台超级洗衣机放在同一排上。开始的时候,每台洗衣机内可能有一定量的衣服,也可能是空的。

在每一步操作中,你可以选择任意 `m` (`1 <= m <= n`) 台洗衣机,与此同时将每台洗衣机的一件衣服送到相邻的一台洗衣机。

给定一个整数数组 `machines` 代表从左至右每台洗衣机中的衣物数量,请给出能让所有洗衣机中剩下的衣物的数量相等的 最少的操作步数 。如果不能使每台洗衣机中衣物的数量相等,则返回 `-1` 。

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

输出:3

解释:
第一步:    1     0 <-- 5    =>    1     1     4
第二步:    1 <-- 1 <-- 4    =>    2     1     3
第三步:    2     1 <-- 3    =>    2     2     2
```
示例 2:
```
输入:machines = [0,3,0]

输出:2

解释:
第一步:    0 <-- 3     0    =>    1     2     0
第二步:    1     2 --> 0    =>    1     1     1
```
示例 3:
```
输入:machines = [0,2,0]

输出:-1

解释:
不可能让所有三个洗衣机同时剩下相同数量的衣物。
```

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

Solution:
### 基本分析

由于最终是要让所有洗衣机衣服相等,因此无解的情况很好分析,如果衣服数量 $sum$ 不能整除洗衣机数量 $n$ 的话,则返回 $-1$,否则必然有解(最坏情况下,每次只移动一件衣服,也可以使得衣服均分),要求最小移动次数。

由于每次操作都可以选任意台机器进行,不难猜想到最小移动次数为 **所有机器的「最小运输衣服数量」中的最大值**。

计算某台洗衣机的「最小运输衣服数量」为经过当前机器的衣服数量(每次只能运输一件衣服),其值等于「起始左边衣服总量 与 最终左边衣服总量 的差值」+「起始右边衣服总量 与 最终右边衣服总量 的差值」,这里的差值都需要与 $0$ 取 $\max$ 代指缺少衣服的数量(因为如果是多余数量的话,可以通过同时传输来满足增加缺少的一边,减少多余的一边)。

我们猜想取所有机器中的「最小操作次数」的最大值即是答案。

**但这显然是理论的最小操作次数,我们来证明最终答案等于该值。**

假设理论最下操作次数为 $cnt$,真实答案为 $ans$,那么天然有 $ans \geq cnt$,我们需要通过证明 $ans \leq cnt$ 恒成立,来得证 $ans = cnt$。

可以通过「反证法」来证明 $ans \leq cnt$ 恒成立,假设 $ans > cnt$,即在某个特定序列中,实际最小操作次数 $ans$ 大于 $cnt$,假定我们是在位置 $x$ 中取得这个实际最小操作次数。

那么我们需要思考:**在没有无效传输的前提,什么情况下需要在 $x$ 位置传输大于 $cnt$ 件衣服来达到最终平衡。**

> 注:无效的意思是,衣服从位置 $x$ 的一边传到另外一边,随后又传输回来。

(注 1)当且仅当位置 $x$ 本身衣服为 $0$ 时,会发生该种情况。

也就是说首次传输,并没有实现「从 $x$ 左边往右边传输衣服」或者「从 $x$ 右边往左边传输衣服」的目的,而是需要先往位置 $x$ 填送衣服。

那么是否可能由起始衣服为 $0$ 的位置来取得 $ans$ 呢?我们通过「反证法」来证明「$ans$ 不可能由衣服为 $0$ 的起始位置得出」。

由于位置 $x$ 的起始数量为 $0$,那么位置 $x$ 必然至少有一侧的起始数量小于最终数量的(缺少衣服的),可以继续利用「反证法」来证明:

* 如果是两边都多于最终数量,说明最终是两边衣服流向位置 $x$,而且我们得到的 $ans$ 是两边的缺少总和,这种情况下得到的 $ans$ 为 $0$,但是整体衣服本身不相等,必然要消耗步数,必然不为 $0$,因此该情况不存在。

既然位置 $x$ 至少有一侧的起始数量小于最终数量的(缺少衣服的),那么自然我们可以将位置 $x$ 归到那一边,使得那一侧缺少衣服的数量更多,从而使答案 $ans$ 更大。这与 $ans$ 为所有位置中的「最小操作次数」最大的位置矛盾。

**得证,取得 $ans$ 的位置 $x$ 起始衣服必然不为 $0$。**

如果位置 $x$ 起始衣服必然不为 $0$,那么(注 1)的条件不成立,则 $ans > cnt$ 恒不成立,得证 $ans \leq cnt$ 恒成立。

**至此,我们通过三次「反证法」来证明了结论成立。首先通过「反证法」证明取得 $ans$ 的位置 $x$ 衣服不可能为 $0$;然后根据该位置起始衣服不为 $0$ 的前提条件,来证明 $ans > cnt$ 恒不成立,得证 $ans \leq cnt$ 恒成立,最终结合 $ans \geq cnt$ 来得证 $ans = cnt$。**

---

### 贪心

实现上,首先我们可以求得衣服总和 $sum$ 以及洗衣机数量 $n$,从而判断无解情况(`sum % n != 0`),或者计算最终每台洗衣机的衣服数量 $t = sum / n$。

然后使用两个变量 $ls$ 和 $rs$ 分别表示当前位置「左边的衣服总数」和「右边的衣服总数」,并在从左往右的遍历过程中实时维护。

对于某个位置 $x$ 而言,达到最终平衡需要从 $x$ 右边往左边运送的衣服数量为 $a =  \max(i * t - ls, 0)$,即左边的当前的衣服数量与最终状态的衣服数量的差值,与 $0$ 取 $\max$ 含义代表为如果当前左边衣服多于最终衣服数量时,此时不需要消耗从右到左的移动次数(只需要消耗从 $x$ 左边到 $x$ 右边的移动次数);右边分析同理,我们可以得到达到最终平衡需要从 $x$ 左边到右运送的衣服数量为 $b =  \max((n - i - 1) * t - rs, 0)$。

在所有位置的 $a + b$ 之间取最大值即是答案。

代码:
```Java
class Solution {
    public int findMinMoves(int[] ms) {
        int n = ms.length;
        int sum = 0;
        for (int i : ms) sum += i;
        if (sum % n != 0) return -1;
        int t = sum / n;
        int ls = 0, rs = sum;
        int ans = 0;
        for (int i = 0; i < n; i++) {
            rs -= ms[i];
            int a = Math.max(t * i - ls, 0);
            int b = Math.max((n - i - 1) * t - rs, 0);
            ans = Math.max(ans, a + b);
            ls += ms[i];
        }
        return ans;
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


Question:
给定不同面额的硬币和一个总金额。写出函数来计算可以凑成总金额的硬币组合数。假设每一种面额的硬币有无限个。

示例 1:
```
输入: amount = 5, coins = [1, 2, 5]
输出: 4
解释: 有四种方式可以凑成总金额:
5=5
5=2+2+1
5=2+1+1+1
5=1+1+1+1+1
```
示例 2:
```
输入: amount = 3, coins = [2]
输出: 0
解释: 只用面额2的硬币不能凑成总金额3。
```
示例 3:
```
输入: amount = 10, coins = [10]
输出: 1
```

注意:

你可以假设:
* 0 <= amount (总金额) <= 5000
* 1 <= coin (硬币面额) <= 5000
* 硬币种类不超过 500 种
* 结果符合 32 位符号整数

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

在 [322. 零钱兑换](https://leetcode-cn.com/problems/coin-change/solution/dong-tai-gui-hua-bei-bao-wen-ti-zhan-zai-3265/) 中,我们求的是「取得特定价值所需要的最小物品个数」。

对于本题,我们求的是「取得特定价值的方案数量」。

**求的东西不一样,但问题的本质没有发生改变,同样属于「组合优化」问题。**

你可以这样来理解什么是组合优化问题:

**被选物品之间不需要满足特定关系,只需要选择物品,以达到「全局最优」或者「特定状态」即可。**

同时硬币相当于我们的物品,每种硬币可以选择「无限次」,很自然的想到「完全背包」。

这时候可以将「完全背包」的状态定义搬过来进行“微调”:

**定义 $f[i][j]$ 为考虑前 $i$ 件物品,凑成总和为 $j$ 的方案数量。**

为了方便初始化,我们一般让 $f[0][x]$ 代表不考虑任何物品的情况。

因此我们有显而易见的初始化条件:$f[0][0] = 1$,其余 $f[0][x] = 0$。

代表当没有任何硬币的时候,存在凑成总和为 0 的方案数量为 1;凑成其他总和的方案不存在。

当「状态定义」与「基本初始化」有了之后,我们不失一般性的考虑 $f[i][j]$ 该如何转移。

对于第 $i$ 个硬币我们有两种决策方案:

* 不使用该硬币:

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

* 使用该硬币:由于每个硬币可以被选择多次(容量允许的情况下),因此方案数量应当是选择「任意个」该硬币的方案总和:

$$
\sum_{k = 1}^{\left \lfloor {j / val} \right \rfloor}f[i - 1][j - k * val], val = nums[i - 1]
$$

代码:
```Java
class Solution {
    public int change(int cnt, int[] cs) {
        int n = cs.length;
        int[][] f = new int[n + 1][cnt + 1];
        f[0][0] = 1;
        for (int i = 1; i <= n; i++) {
            int val = cs[i - 1];
            for (int j = 0; j <= cnt; j++) {
                f[i][j] = f[i - 1][j];
                for (int k = 1; k * val <= j; k++) {
                    f[i][j] += f[i - 1][j - k * val];
                }
            }
        }
        return f[n][cnt];
    }
}
```
* 时间复杂度:共有 $n * cnt$ 个状态需要转移,每个状态转移最多遍历 $cnt$ 次。整体复杂度为 $O(n * cnt^2)$。
* 空间复杂度:$O(n * cnt)$。

---

### 完全背包(一维优化)

显然二维完全背包求解方案复杂度有点高。

$n$ 的数据范围为 $10^2$,$cnt$ 的数据范围为 $10^3$,总的计算量为 $10^8$ 以上,处于超时边缘(实际测试可通过)。

我们需要对其进行「降维优化」,可以使用最开始讲的 [数学分析方式](https://leetcode-cn.com/problems/perfect-squares/solution/dong-tai-gui-hua-bei-bao-wen-ti-qiang-hu-hcmi/),或者上一讲讲的 [换元优化方式](https://leetcode-cn.com/problems/coin-change/solution/dong-tai-gui-hua-bei-bao-wen-ti-zhan-zai-3265/) 进行降维优化。

由于 [数学分析方式](https://leetcode-cn.com/problems/perfect-squares/solution/dong-tai-gui-hua-bei-bao-wen-ti-qiang-hu-hcmi/) 十分耗时,我们用得更多的 [换元优化方式](https://leetcode-cn.com/problems/coin-change/solution/dong-tai-gui-hua-bei-bao-wen-ti-zhan-zai-3265/)。两者同样具有「可推广」特性。

因为后者更为常用,所以我们再来回顾一下如何进行「直接上手写一维空间优化的版本」 :

1. 在二维解法的基础上,直接取消「物品维度」
2. 确保「容量维度」的遍历顺序为「从小到大」(适用于「完全背包」)
3. 将形如 $f[i - 1][j - k * val]$ 的式子更替为 $f[j - val]$,同时解决「数组越界」问题(将物品维度的遍历修改为从 $val$ 开始)

代码:
```java []
class Solution {
    public int change(int cnt, int[] cs) {
        int n = cs.length;
        int[] f = new int[cnt + 1];
        f[0] = 1;
        for (int i = 1; i <= n; i++) {
            int val = cs[i - 1];
            for (int j = val; j <= cnt; j++) {
                f[j] += f[j - val];
            }
        }
        return f[cnt];
    }
}
```
* 时间复杂度:共有 $n * cnt$ 个状态需要转移,整体复杂度为 $O(n * cnt)$。
* 空间复杂度:$O(cnt)$。

### 最后

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

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

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

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


Question:
给你一个 $m x n$ 的二元矩阵 $matrix$,且所有值被初始化为 $0$。

请你设计一个算法,随机选取一个满足 `matrix[i][j] == 0` 的下标 $(i, j)$ ,并将它的值变为 $1$ 。

所有满足 `matrix[i][j] == 0` 的下标 $(i, j)$ 被选取的概率应当均等。

尽量最少调用内置的随机函数,并且优化时间和空间复杂度。

实现 `Solution` 类:

* `Solution(int m, int n)` 使用二元矩阵的大小 $m$ 和 $n$ 初始化该对象
* `int[] flip()` 返回一个满足 `matrix[i][j] == 0` 的随机下标 `[i, j]` ,并将其对应格子中的值变为 $1$
* `void reset()` 将矩阵中所有的值重置为 $0$

示例:
```
输入
["Solution", "flip", "flip", "flip", "reset", "flip"]
[[3, 1], [], [], [], [], []]

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

解释
Solution solution = new Solution(3, 1);
solution.flip();  // 返回 [1, 0],此时返回 [0,0]、[1,0] 和 [2,0] 的概率应当相同
solution.flip();  // 返回 [2, 0],因为 [1,0] 已经返回过了,此时返回 [2,0] 和 [0,0] 的概率应当相同
solution.flip();  // 返回 [0, 0],根据前面已经返回过的下标,此时只能返回 [0,0]
solution.reset(); // 所有值都重置为 0 ,并可以再次选择下标返回
solution.flip();  // 返回 [2, 0],此时返回 [0,0]、[1,0] 和 [2,0] 的概率应当相同
```

提示:
* $1 <= m, n <= 10^4$
* 每次调用 `flip` 时,矩阵中至少存在一个值为 $0$ 的格子。
* 最多调用 1000 次 `flip` 和 `reset` 方法。

Solution:
### 双指针

矩阵大小的数据范围为 $10^4$,因此我们不能使用真实构建矩阵的做法来做,同时利用二维的坐标能够唯一对应出编号($idx = row * n + col$),可以将问题转换为一维问题。

一个最为朴素的做法是利用调用次数只有 $10^3$,我们可以在 $[0, m * n)$ 范围内随机出一个下标 $idx$(对应矩阵的某个具体位置),然后从 $idx$ 往两边进行扫描,找到最近一个未被使用的位置,将其进行标记翻转并返回。

该做法相比于一直随机的「拒绝采样」做法,能够确保单次 `flip` 操作中只会调用一次随机方法,同时利用矩阵只有 $10^3$ 个位置被翻转,因而复杂度上具有保证。

代码:
```Java
class Solution {
    int m, n;
    Set<Integer> set = new HashSet<>();
    Random random = new Random(300);
    public Solution(int _m, int _n) {
        m = _m; n = _n;
    }
    public int[] flip() {
        int a = random.nextInt(m * n), b = a;
        while (a >= 0 && set.contains(a)) a--;
        while (b < m * n && set.contains(b)) b++;
        int c = a >= 0 && !set.contains(a) ? a : b;
        set.add(c);
        return new int[]{c / n, c % n};
    }
    public void reset() {
        set.clear();
    }
}
```
* 时间复杂度:令最大调用次数为 $C = 1000$,即矩阵中最多有 $C$ 个位置被翻转。`flip` 操作的复杂度为 $O(C)$;`reset` 复杂度为 $O(C)$
* 空间复杂度:$O(C)$

---

### 哈希表 + swap

在解法一中,我们将二维问题转化为了一维问题。

起始时,我们只需要在 $[0, m * n)$ 这连续一段的区间内进行随机,但当我们经过了多次翻转后,该区间内的某些位置会被断开,使得数组不再连续。

如果我们希望在每次随机时都采用起始的方式(在连续一段内进行随机),需要确保某些位置被翻转后,剩余位置仍是连续。

具体的,我们可以使用「哈希表」多记录一层映射关系:起始时所有位置未被翻转,我们规定未被翻转的位置其映射值为编号本身($idx = row * n + col$),由于未被翻转的部分具有等值映射关系,因此无须在哈希表中真实存储。当随机到某个位置 $idx$ 时,进行分情况讨论:

* 该位置未被哈希表真实记录(未被翻转):说明 $idx$ 可被直接使用,使用 $idx$ 作为本次随机点。同时将右端点(未被使用的)位置的映射值放到该位置,将右端点左移。确保下次再随机到 $idx$,仍能直接使用 $idx$ 的映射值,同时维护了随机区间的连续性;
* 该位置已被哈希表真实记录(已被翻转):此时直接使用 $idx$ 存储的映射值(上一次交换时的右端点映射值)即可,然后用新的右端点映射值将其进行覆盖,更新右端点。确保下次再随机到 $idx$,仍能直接使用 $idx$ 的映射值,同时维护了随机区间的连续性。

代码:
```Java
class Solution {
    int m, n, cnt; // cnt 为剩余数个数,同时 cnt - 1 为区间右端点位置
    Map<Integer, Integer> map = new HashMap<>();
    Random random = new Random(300);
    public Solution(int _m, int _n) {
        m = _m; n = _n; cnt = m * n;
    }
    public int[] flip() {
        int x = random.nextInt(cnt--);
        int idx = map.getOrDefault(x, x);
        map.put(x, map.getOrDefault(cnt, cnt));
        return new int[]{idx / n, idx % n};
    }
    public void reset() {
        cnt = m * n;
        map.clear();
    }
}
```
* 时间复杂度:令最大调用次数为 $C = 1000$,即矩阵中最多有 $C$ 个位置被翻转。`flip` 操作的复杂度为 $O(1)$;`reset` 复杂度为 $O(C)$
* 空间复杂度:$O(C)$

### 最后

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

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

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

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


Question:
我们定义,在以下情况时,单词的大写用法是正确的:

* 全部字母都是大写,比如 "USA" 。
* 单词中所有字母都不是大写,比如 "leetcode" 。
* 如果单词不只含有一个字母,只有首字母大写, 比如 "Google" 。

给你一个字符串 word 。如果大写用法正确,返回 true ;否则,返回 false 。

示例 1:
```
输入:word = "USA"

输出:true
```
示例 2:
```
输入:word = "FlaG"

输出:false
```
提示:
* 1 <= word.length <= 100
* word 由小写和大写英文字母组成

Solution:
### 模拟

根据题意,分别进行三种规则的判断即可。

代码:
```Java
class Solution {
    public boolean detectCapitalUse(String word) {
        if (word.toUpperCase().equals(word)) return true;
        if (word.toLowerCase().equals(word)) return true;
        int n = word.length(), idx = 1;
        if (Character.isUpperCase(word.charAt(0))) {
            while (idx < n && Character.isLowerCase(word.charAt(idx))) idx++;
        }
        return idx == n;
    }
}
```
* 时间复杂度:$O(n)$,常数为 $5$。
* 空间复杂度:算法执行过程会产生新的字符串,复杂度为 $O(n)$

### 最后

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

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

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

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


521. 最长特殊序列 I

Question:
给你两个字符串 `a` 和 `b`,请返回 这两个字符串中 最长的特殊序列  。如果不存在,则返回 $-1$ 。

「最长特殊序列」 定义如下:该序列为 某字符串独有的最长子序列(即不能是其他字符串的子序列) 。

字符串 `s` 的子序列是在从 `s` 中删除任意数量的字符后可以获得的字符串。

例如,`“abc”` 是 `“aebdc”` 的子序列,因为您可以删除 `“aebdc”` 中的下划线字符来得到 `“abc”` 。 `“aebdc”` 的子序列还包括 `“aebdc”` 、 `“aeb”` 和 `“”` (空字符串)。

示例 1:
```
输入: a = "aba", b = "cdc"

输出: 3

解释: 最长特殊序列可为 "aba" (或 "cdc"),两者均为自身的子序列且不是对方的子序列。
```
示例 2:
```
输入:a = "aaa", b = "bbb"

输出:3

解释: 最长特殊序列是“aaa”和“bbb”。
```
示例 3:
```
输入:a = "aaa", b = "aaa"

输出:-1

解释: 字符串a的每个子序列也是字符串b的每个子序列。同样,字符串b的每个子序列也是字符串a的子序列。
```

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

Solution:
### 脑筋急转弯

当两字符串不同时,我们总能选择长度不是最小的字符串作为答案,而当两字符串相同时,我们无法找到特殊序列。

代码:
```Java
class Solution {
    public int findLUSlength(String a, String b) {
        return a.equals(b) ? -1 : Math.max(a.length(), b.length());
    }
}
```
* 时间复杂度:字符串比较复杂度与长度成正比,复杂度为 $O(\max(n, m))$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


522. 最长特殊序列 II

Question:
给定字符串列表 `strs`,返回其中 最长的特殊序列 。如果最长特殊序列不存在,返回 $-1$ 。

特殊序列 定义如下:该序列为某字符串 独有的子序列(即不能是其他字符串的子序列)。

`s` 的 子序列可以通过删去字符串 `s` 中的某些字符实现。

* 例如,`"abc"` 是 `"aebdc"` 的子序列,因为您可以删除 `"aebdc"` 中的下划线字符来得到 `"abc"` 。`"aebdc"` 的子序列还包括 `"aebdc"`、 `"aeb"` 和 `""` (空字符串)。

示例 1:
```
输入: strs = ["aba","cdc","eae"]

输出: 3
```
示例 2:
```
输入: strs = ["aaa","aaa","aa"]

输出: -1
```

提示:
* $2 <= strs.length <= 50$
* $1 <= strs[i].length <= 10$
* `strs[i]` 只包含小写英文字母

Solution:
### LCS

记 `strs` 数组长度为 $n$,单个 $strs[i]$ 的最大长度 $m$。其中 $n$ 数据范围为 $50$,$m$ 数据范围为 $10$。

根据题意,我们可以枚举每个 $s1 = str[i]$,检查其是否为其他 $s2 = strs[j]$ 的子序列,这需要枚举所有点对,复杂度为 $O(n^2)$。同时记录遍历过程中的最大长度 `ans`,用于剪枝(对于字符串长度本身小于等于 `ans` 的 $strs[i]$ 可直接跳过)。

我们可以实现一个 `check` 函数来检查 `s1` 是否为 `s2` 的子序列,该问题可转化为求 `s1` 和 `s2` 的最长公共子序列长度。若最长公共子序列长度为 `s1` 长度,说明 `s1` 为 `s2` 的子序列,此时 $strs[i]$ 不满足条件,否则我们使用 $strs[i]$ 的长度来更新 `ans`。`check` 函数的复杂度为 $O(m^2)$。

> **不了解 LCS 的同学可以看前置 🧀 : [LCS 模板题](https://mp.weixin.qq.com/s?__biz=MzU4NDE3MTEyMA==&mid=2247492097&idx=1&sn=f51f29d86df809d8ac43a40a1369b3d6)**

因此总的计算量为 $n^2 \times m^2 = 2.5 * 10^5$,可以过。

代码:
```Java
class Solution {
    public int findLUSlength(String[] strs) {
        int n = strs.length, ans = -1;
        for (int i = 0; i < n; i++) {
            if (strs[i].length() <= ans) continue;
            boolean ok = true;
            for (int j = 0; j < n && ok; j++) {
                if (i == j) continue;
                if (check(strs[i], strs[j])) ok = false;
            }
            if (ok) ans = strs[i].length();
        }
        return ans;
    }
    boolean check(String s1, String s2) {
        int n = s1.length(), m = s2.length();
        if (m < n) return false;
        int[][] f = new int[n + 1][m + 1];
        for (int i = 1; i <= n; i++) {
            for (int j = 1; j <= m; j++) {
                f[i][j] = s1.charAt(i - 1) == s2.charAt(j - 1) ? f[i - 1][j - 1] + 1 : f[i - 1][j - 1];
                f[i][j] = Math.max(f[i][j], f[i - 1][j]);
                f[i][j] = Math.max(f[i][j], f[i][j - 1]);
                if (f[i][j] == n) return true;
            }
        }
        return false;
    }
}
```
* 时间复杂度:检查每个 $strs[i]$ 是否符合要求,需要枚举所有的点对,复杂度为 $O(n^2)$;求解 LCS 问题的复杂度为 $O(m^2)$。整体复杂度为 $O(n^2 \times m^2)$
* 空间复杂度:$O(m^2)$

### 最后

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

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

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

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


523. 连续的子数组和

Question:
给你一个整数数组 `nums` 和一个整数 `k` ,编写一个函数来判断该数组是否含有同时满足下述条件的连续子数组:

* 子数组大小至少为 `2` ,且
* 子数组元素总和为 `k` 的倍数。

如果存在,返回 `true`;否则,返回 `false`。

如果存在一个整数 `n` ,令整数 `x` 符合 `x = n * k` ,则称 `x` 是 `k` 的一个倍数。

示例 1:
```
输入:nums = [23,2,4,6,7], k = 6

输出:true

解释:[2,4] 是一个大小为 2 的子数组,并且和为 6 。
```
示例 2:
```
输入:nums = [23,2,6,4,7], k = 6

输出:true

解释:[23, 2, 6, 4, 7] 是大小为 5 的子数组,并且和为 42 。
42 是 6 的倍数,因为 42 = 7 * 6 且 7 是一个整数。
```
示例 3:
```
输入:nums = [23,2,6,4,7], k = 13

输出:false
```

提示:
* $1 <= nums.length <= 10^5$
* $0 <= nums[i] <= 10^9$
* $0 <= sum(nums[i]) <= 2^{31} - 1$
* $1 <= k <= 2^{31} - 1$

Solution:
### 基本分析

这是一道很经典的前缀和题目,类似的原题也在蓝桥杯出现过,坐标在 [K 倍区间](http://lx.lanqiao.cn/problem.page?gpid=T444) http://lx.lanqiao.cn/problem.page?gpid=T444。

本题与那道题不同在于:

* [K 倍区间] 需要求得所有符合条件的区间数量;本题需要判断是否存在。
* [K 倍区间] 序列全是正整数,不需要考虑 $0$ 值问题;本题需要考虑 $0$ 值问题。

数据范围为 $10^4$,因此无论是纯朴素的做法 ($O(n^3)$)还是简单使用前缀和优化的做法 ($O(n^2)$) 都不能满足要求。

我们需要从 $k$ 的倍数作为切入点来做。

预处理前缀和数组 $sum$,方便快速求得某一段区间的和。然后假定 $[i, j]$ 是我们的目标区间,那么有:

$$
sum[j] - sum[i - 1] = n * k
$$

经过简单的变形可得:

$$
\frac{sum[j]}{k} - \frac{sum[i - 1]}{k} = n
$$

**要使得两者除 $k$ 相减为整数,需要满足 $sum[j]$ 和 $sum[i - 1]$ 对 $k$ 取余相同。**

也就是说,我们只需要枚举右端点 $j$,然后在枚举右端点 $j$ 的时候检查之前是否出现过左端点 $i$,使得 $sum[j]$ 和 $sum[i - 1]$ 对 $k$ 取余相同。

---

### 前缀和 + HashSet

具体的,使用 `HashSet` 来保存出现过的值。

让循环从 $2$ 开始枚举右端点(根据题目要求,子数组长度至少为 $2$),每次将符合长度要求的位置的取余结果存入 `HashSet`。

如果枚举某个右端点 $j$ 时发现存在某个左端点 $i$ 符合要求,则返回 `True`。

代码:
```Java
class Solution {
    public boolean checkSubarraySum(int[] nums, int k) {
        int n = nums.length;
        int[] sum = new int[n + 1];
        for (int i = 1; i <= n; i++) sum[i] = sum[i - 1] + nums[i - 1];
        Set<Integer> set = new HashSet<>();
        for (int i = 2; i <= n; i++) {
            set.add(sum[i - 2] % k);
            if (set.contains(sum[i] % k)) return true;
        }
        return false;
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n)$

---

### 拓展(求方案数)

蓝桥杯官网登录经常性罢工,我登录十几次都没登录上去,这里直接截图了 [K 倍区间] 的题目给大家。

写了代码,但很可惜没 OJ 可以测试 🤣

比较简单,应该没啥问题,可以直接参考 🤣

代码:
```Java
import java.util.*;
class Main {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        int n = sc.nextInt(), k = sc.nextInt();
        long[] s  = new long[n + 1];
        for (int i = 1; i <= n; i++) s[i] = s[i - 1] + sc.nextLong();
        long ans = 0;
        Map<Long, Integer> map = new HashMap<>();
        map.put(0L, 1);
        for (int i = 1 ; i <= n; i++) {
            long u = s[i] % k;
            if (map.containsKey(u)) ans += map.get(u);
            map.put(u, map.getOrDefault(u, 0) + 1);
        }
        System.out.println(ans);
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


LeetCode/521-530/524. 通过删除字母匹配到字典里最长单词（中等）.md

524. 通过删除字母匹配到字典里最长单词

Question:
给你一个字符串 `s` 和一个字符串数组 `dictionary` 作为字典,找出并返回字典中最长的字符串,该字符串可以通过删除 `s` 中的某些字符得到。

如果答案不止一个,返回长度最长且字典序最小的字符串。

如果答案不存在,则返回空字符串。

示例 1:
```
输入:s = "abpcplea", dictionary = ["ale","apple","monkey","plea"]

输出:"apple"
```
示例 2:
```
输入:s = "abpcplea", dictionary = ["a","b","c"]

输出:"a"
```

提示:
* $1 <= s.length <= 1000$
* $1 <= dictionary.length <= 1000$
* $1 <= dictionary[i].length <= 1000$
* `s` 和 `dictionary[i]` 仅由小写英文字母组成

Solution:
### 排序 + 双指针 + 贪心

根据题意,我们需要找到 `dictionary` 中为 `s` 的子序列,且「长度最长(优先级 $1$)」及「字典序最小(优先级 $2$)」的字符串。

数据范围全是 $1000$。

我们可以先对 `dictionary` 根据题意进行自定义排序:

1. 长度不同的字符串,按照字符串长度排倒序;
2. 长度相同的,则按照字典序排升序。

然后我们只需要对 `dictionary` 进行顺序查找,找到的第一个符合条件的字符串即是答案。

具体的,我们可以使用「贪心」思想的「双指针」实现来进行检查:

1. 使用两个指针 `i` 和 `j` 分别代表检查到 `s` 和 `dictionary[x]` 中的哪位字符;
2. 当 `s[i] != dictionary[x][j]`,我们使 `i` 指针右移,直到找到 `s` 中第一位与 `dictionary[x][j]` 对得上的位置,然后当 `i` 和 `j` 同时右移,匹配下一个字符;
3. 重复步骤 $2$,直到整个 `dictionary[x]` 被匹配完。

> 证明:对于某个字符 `dictionary[x][j]` 而言,选择 `s` 中 **当前** 所能选择的下标最小的位置进行匹配,对于后续所能进行选择方案,会严格覆盖不是选择下标最小的位置,因此结果不会变差。

Java 代码:
```Java
class Solution {
    public String findLongestWord(String s, List<String> dictionary) {
        Collections.sort(dictionary, (a,b)->{
            if (a.length() != b.length()) return b.length() - a.length();
            return a.compareTo(b);
        });
        int n = s.length();
        for (String ss : dictionary) {
            int m = ss.length();
            int i = 0, j = 0;
            while (i < n && j < m) {
                if (s.charAt(i) == ss.charAt(j)) j++;
                i++;
            }
            if (j == m) return ss;
        }
        return "";
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    string findLongestWord(string s, vector<string>& dictionary) {
        sort(dictionary.begin(), dictionary.end(), [](const string& a, const string& b) {
            if (a.length() != b.length()) return b.length() < a.length();
            return a < b;
        });
        int n = s.length();
        for (const string& word : dictionary) {
            int m = word.length();
            int i = 0, j = 0;
            while (i < n && j < m) {
                if (s[i] == word[j]) j++;
                i++;
            }
            if (j == m) return word;
        }
        return "";
    }
};
```
Python 代码:
```Python
class Solution:
    def findLongestWord(self, s: str, dictionary: List[str]) -> str:
        dictionary.sort(key=lambda x: (-len(x), x))
        n = len(s)
        for word in dictionary:
            m = len(word)
            i, j = 0, 0
            while i < n and j < m:
                if s[i] == word[j]: j += 1
                i += 1
            if j == m: return word
        return ""
```
TypeScript 代码:
```TypeScript
function findLongestWord(s: string, dictionary: string[]): string {
    dictionary.sort((a, b) => {
        if (a.length !== b.length) return b.length - a.length;
        return a.localeCompare(b);
    });
    const n = s.length;
    for (const word of dictionary) {
        const m = word.length;
        let i = 0, j = 0;
        while (i < n && j < m) {
            if (s[i] === word[j]) j++;
            i++;
        }
        if (j === m) return word;
    }
    return "";
};
```
* 时间复杂度:令 `n` 为 `s` 的长度,`m` 为 `dictionary` 的长度。排序复杂度为 $O(m\log{m})$;对 `dictionary` 中的每个字符串进行检查,单个字符串的检查复杂度为 $O(\min(n, dictionary[i]))\approx O(n)$。整体复杂度为 $O(m\log{m} + m \times n)$
* 空间复杂度:$O(\log{m})$

### 最后

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

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

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

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


Question:
给定一个二进制数组 `nums` , 找到含有相同数量的 $0$ 和 $1$ 的最长连续子数组,并返回该子数组的长度。

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

输出: 2

说明: [0, 1] 是具有相同数量0和1的最长连续子数组。
```
示例 2:
```
输入: nums = [0,1,0]

输出: 2

说明: [0, 1] (或 [1, 0]) 是具有相同数量0和1的最长连续子数组。
```

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

Solution:
### 前缀和 + 哈希表

根据题意,我们可以轻易发现如下性质:如果答案非 $0$,那么子数组长度必然为偶数,且长度至少为 $2$。

具体的,我们在预处理前缀和时,将 $nums[i]$ 为 $0$ 的值当做 $-1$ 处理。

从而将问题转化为:**如何求得最长一段区间和为 $0$ 的子数组。** 同时使用「哈希表」来记录「某个前缀和出现的最小下标」是多少。

Java 代码:
```Java
class Solution {
    public int findMaxLength(int[] nums) {
        int n = nums.length, ans = 0;
        int[] sum = new int[n + 10];
        for (int i = 1; i <= n; i++) sum[i] = sum[i - 1] + (nums[i - 1] == 0 ? -1 : 1);
        Map<Integer, Integer> map = new HashMap<>();
        map.put(0, 0);
        for (int i = 1; i <= n; i++) {
            int t = sum[i];
            if (map.containsKey(t)) ans = Math.max(ans, i - map.get(t));
            if (!map.containsKey(t)) map.put(t, i);
        }
        return ans;
    }
}
```
TypeScript 代码:
```TypeScript
function findMaxLength(nums: number[]): number {
    let n = nums.length, ans = 0
    const sum = new Array<number>(n + 10).fill(0)
    for (let i = 1; i <= n; i++) sum[i] = sum[i - 1] + (nums[i - 1] == 0 ? -1 : 1)
    const map = new Map<number, number>()
    map.set(0, 0)
    for (let i = 1; i <= n; i++) {
        const t = sum[i]
        if (map.has(t)) ans = Math.max(ans, i - map.get(t))
        if (!map.has(t)) map.set(t, i)
    }
    return ans
};
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


Question:
假设有从 $1$ 到 $N$ 的 $N$ 个整数,如果从这 $N$ 个数字中成功构造出一个数组,使得数组的第 $i$ 位 ($1 <= i <= N$) 满足如下两个条件中的一个,我们就称这个数组为一个优美的排列。

条件:
* 第 $i$ 位的数字能被 $i$ 整除
* $i$ 能被第 $i$ 位上的数字整除

现在给定一个整数 $N$,请问可以构造多少个优美的排列?

示例1:
```
输入: 2

输出: 2

解释:
第 1 个优美的排列是 [1, 2]:
  第 1 个位置(i=1)上的数字是1,1能被 i(i=1)整除
  第 2 个位置(i=2)上的数字是2,2能被 i(i=2)整除

第 2 个优美的排列是 [2, 1]:
  第 1 个位置(i=1)上的数字是2,2能被 i(i=1)整除
  第 2 个位置(i=2)上的数字是1,i(i=2)能被 1 整除
```

说明:
* $N$ 是一个正整数,并且不会超过 $15$。

Solution:
### 状态压缩 DP

利用数据范围不超过 $15$,我们可以使用「状态压缩 DP」进行求解。

**使用一个二进制数表示当前哪些数已被选,哪些数未被选,目的是为了可以使用位运算进行加速。**

我们可以通过一个具体的样例,来感受下「状态压缩」是什么意思:

例如 $(000...0101)_2$ 代表值为 $1$ 和值为 $3$ 的数字已经被使用了,而值为 $2$ 的节点尚未被使用。

然后再来看看使用「状态压缩」的话,一些基本的操作该如何进行:

假设变量 $state$ 存放了「当前数的使用情况」,当我们需要检查值为 $k$ 的数是否被使用时,可以使用位运算 `a = (state >> k) & 1`,来获取 $state$ 中第 $k$ 位的二进制表示,如果 `a` 为 $1$ 代表值为 $k$ 的数字已被使用,如果为 $0$ 则未被访问。

**定义 $f[i][state]$ 为考虑前 $i$ 个数,且当前选择方案为 $state$ 的所有方案数量。**

一个显然的初始化条件为 $f[0][0] = 1$,代表当我们不考虑任何数($i = 0$)的情况下,一个数都不被选择($state = 0$)为一种合法方案。

不失一般性的考虑 $f[i][state]$ 该如何转移,由于本题是求方案数,我们的转移方程必须做到「不重不漏」。

我们可以通过枚举当前位置 $i$ 是选哪个数,假设位置 $i$ 所选数值为 $k$,首先 $k$ 值需要同时满足如下两个条件:

* $state$ 中的第 $k$ 位为 $1$;
* 要么 $k$ 能被 $i$ 整除,要么 $i$ 能被 $k$ 整除。

那么根据状态定义,位置 $i$ 选了数值 $k$,通过位运算我们可以直接得出决策位置 $i$ 之前的状态是什么:$state \& (\lnot (1 << (k - 1)))$,代表将 $state$ 的二进制表示中的第 $k$ 位置 $0$。

最终的 $f[i][state]$ 为当前位置 $i$ 选择的是所有合法的 $k$ 值的方案数之和:

$$
f[i][state] = \sum_{k = 1}^{n} f[i - 1][state \& (\lnot (1 << (k - 1)))]
$$

> 一些细节:由于给定的数值范围为 $[1,n]$,但实现上为了方便,我们使用 $state$ 从右往左的第 $0$ 位表示数值 $1$ 选择情况,第 $1$ 位表示数值 $2$ 的选择情况 ... 即对选择数值 $k$ 做一个 $-1$ 的偏移。

代码:
```Java
class Solution {
    public int countArrangement(int n) {
        int mask = 1 << n;
        int[][] f = new int[n + 1][mask];
        f[0][0] = 1;
        for (int i = 1; i <= n; i++) {
            // 枚举所有的状态
            for (int state = 0; state < mask; state++) {
                // 枚举位置 i(最后一位)选的数值是 k
                for (int k = 1; k <= n; k++) {
                    // 首先 k 在 state 中必须是 1
                    if (((state >> (k - 1)) & 1) == 0) continue;
                    // 数值 k 和位置 i 之间满足任一整除关系
                    if (k % i != 0 && i % k != 0) continue;
                    // state & (~(1 << (k - 1))) 代表将 state 中数值 k 的位置置零
                    f[i][state] += f[i - 1][state & (~(1 << (k - 1)))];
                }
            }
        }
        return f[n][mask - 1];
    }
}
```
* 时间复杂度:共有 $n * 2^n$ 的状态需要被转移,每次转移复杂度为 $O(n)$,整体复杂度为 $O(n^2 * 2^n)$
* 空间复杂度:$O(n * 2^n)$

---

### 状态压缩 DP(优化)

通过对朴素的状压 DP 的分析,我们发现,在决策第 $i$ 位的时候,理论上我们应该使用的数字数量也应该为 $i$ 个。

但这一点在朴素状压 DP 中并没有体现,这就导致了我们在决策第 $i$ 位的时候,仍然需要对所有的 $state$ 进行计算检查(即使是那些二进制表示中 $1$ 的出现次数不为 $i$ 个的状态)。

因此我们可以换个思路进行枚举(使用新的状态定义并优化转移方程)。

**定义 $f[state]$ 为当前选择数值情况为 $state$ 时的所有方案的数量。**

这样仍然有 $f[0] = 1$ 的初始化条件,最终答案为 $f[(1 << n) - 1]$。

不失一般性考虑 $f[state]$ 如何计算:

**从当前状态 $state$ 进行出发,检查 $state$ 中的每一位 $1$ 作为最后一个被选择的数值,这样仍然可以确保方案数「不重不漏」的被统计,同时由于我们「从小到大」对 $state$ 进行枚举,因此计算 $f[state]$ 所依赖的其他状态值必然都已经被计算完成。**

同样的,我们仍然需要确保 $state$ 中的那一位作为最后一个的 $1$ 需要与所放的位置成整除关系。

因此我们需要一个计算 $state$ 的 $1$ 的个数的方法,这里使用 $lowbit$ 实现即可。

最终的 $f[state]$ 为当前位置选择的是所有合法值的方案数之和:

$$
f[state] = \sum_{i = 0}^{n}f[state \& ( \lnot (1 << i))]
$$

代码:
```Java
class Solution {
    int getCnt(int x) {
        int ans = 0;
        while (x != 0) {
            x -= (x & -x); // lowbit
            ans++;
        }
        return ans;
    }
    public int countArrangement(int n) {
        int mask = 1 << n;
        int[] f = new int[mask];
        f[0] = 1;
        // 枚举所有的状态
        for (int state = 1; state < mask; state++) {
            // 计算 state 有多少个 1(也就是当前排序长度为多少)
            int cnt = getCnt(state);
            // 枚举最后一位数值为多少
            for (int i = 0; i < n; i++) {
                // 数值在 state 中必须是 1
                if (((state >> i) & 1) == 0) continue;
                // 数值(i + 1)和位置(cnt)之间满足任一整除关系
                if ((i + 1) % cnt != 0 && cnt % (i + 1) != 0) continue;
                // state & (~(1 << i)) 代表将 state 中所选数值的位置置零
                f[state] += f[state & (~(1 << i))];
            }
        }
        return f[mask - 1];
    }
}
```
* 时间复杂度:共有 $2^n$ 的状态需要被转移,每次转移复杂度为 $O(n)$,整体复杂度为 $O(n * 2^n)$
* 空间复杂度:$O(2^n)$

---

### 总结

不难发现,其实两种状态压缩 DP 的思路其实是完全一样的。

只不过在朴素状压 DP 中我们是显式的枚举了考虑每一种长度的情况(存在维度 $i$),而在状压 DP(优化)中利用则 $state$ 中的 $1$ 的个数中蕴含的长度信息。

### 最后

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

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

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

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


528. 按权重随机选择

Question:
给定一个正整数数组 $w$ ,其中 $w[i]$ 代表下标 $i$ 的权重(下标从 $0$ 开始),请写一个函数 `pickIndex` ,它可以随机地获取下标 $i$,选取下标 $i$ 的概率与 $w[i]$ 成正比。

例如,对于 $w = [1, 3]$,挑选下标 $0$ 的概率为 $1 / (1 + 3) = 0.25$ (即,$25$%),而选取下标 $1$ 的概率为 $3 / (1 + 3) = 0.75$(即,$75$%)。

也就是说,选取下标 $i$ 的概率为 $w[i] / sum(w)$ 。

示例 1:
```
输入:
["Solution","pickIndex"]
[[[1]],[]]

输出:
[null,0]

解释:
Solution solution = new Solution([1]);
solution.pickIndex(); // 返回 0,因为数组中只有一个元素,所以唯一的选择是返回下标 0。
```
示例 2:
```
输入:
["Solution","pickIndex","pickIndex","pickIndex","pickIndex","pickIndex"]
[[[1,3]],[],[],[],[],[]]

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

解释:
Solution solution = new Solution([1, 3]);
solution.pickIndex(); // 返回 1,返回下标 1,返回该下标概率为 3/4 。
solution.pickIndex(); // 返回 1
solution.pickIndex(); // 返回 1
solution.pickIndex(); // 返回 1
solution.pickIndex(); // 返回 0,返回下标 0,返回该下标概率为 1/4 。

由于这是一个随机问题,允许多个答案,因此下列输出都可以被认为是正确的:
[null,1,1,1,1,0]
[null,1,1,1,1,1]
[null,1,1,1,0,0]
[null,1,1,1,0,1]
[null,1,0,1,0,0]
......
诸若此类。
```

提示:
* $1 <= w.length <= 10000$
* $1 <= w[i] <= 10^5$
* `pickIndex` 将被调用不超过 $10000$ 次

Solution:
### 前缀和 + 二分

根据题意,权重值 $w[i]$ 可以作为 `pickIndex` 调用总次数为 $\sum_{i = 0}^{w.length - 1} w[i]$ 时,下标 $i$ 的返回次数。

随机数的产生可以直接使用语言自带的 API,剩下的我们需要构造一个分布符合权重的序列。

由于 $1 <= w[i] <= 10^5$,且 $w$ 长度为 $10^4$,因此直接使用构造一个有 $w[i]$ 个的 $i$ 的数字会 MLE。

**我们可以使用「前缀和」数组来作为权重分布序列,权重序列的基本单位为 $1$。**

一个长度为 $n$ 的构造好的「前缀和」数组可以看是一个基本单位为 $1$ 的 $[1, sum[n - 1]]$ 数轴。

使用随机函数参数产生 $[1, sum[n - 1]]$ 范围内的随机数,通过「二分」前缀和数组即可找到分布位置对应的原始下标值。

代码:
```Java
class Solution {
    int[] sum;
    public Solution(int[] w) {
        int n = w.length;
        sum = new int[n + 1];
        for (int i = 1; i <= n; i++) sum[i] = sum[i - 1] + w[i - 1];
    }

    public int pickIndex() {
        int n = sum.length;
        int t = (int) (Math.random() * sum[n - 1]) + 1;
        int l = 1, r = n - 1;
        while (l < r) {
            int mid = l + r >> 1;
            if (sum[mid] >= t) r = mid;
            else l = mid + 1;
        }
        return r - 1;
    }
}
```
* 时间复杂度:`Solution` 类的构造方法整体复杂度为 $O(n)$;`pickIndex` 的复杂度为 $O(\log{n})$
* 空间复杂度:$O(n)$

---

### 模拟(桶轮询)

利用 OJ 不太聪明(对权重分布做近似检查),我们可以构造一个最小轮询序列(权重精度保留到小数点一位),并使用 $(i, cnt)$ 的形式进行存储,代表下标 $i$ 在最小轮询序列中出现次数为 $cnt$。

然后使用两个编号 $bid$ 和 $iid$ 来对桶进行轮询返回(循环重置 & 跳到下一个桶)。

**该解法的最大好处是不需要使用 random 函数,同时返回的连续序列满足每一段(长度不短于最小段)都符合近似权重分布。**

代码:
```Java
class Solution {
    // 桶编号 / 桶内编号 / 总数
    int bid, iid, tot;
    List<int[]> list = new ArrayList<>();
    public Solution(int[] w) {
        int n = w.length;
        double sum = 0, min = 1e9;
        for (int i : w) {
            sum += i;
            min = Math.min(min, i);
        }
        double minv = min / sum;
        int k = 1;
        while (minv * k < 1) k *= 10;
        for (int i = 0; i < n; i++) {
            int cnt = (int)(w[i] / sum * k);
            list.add(new int[]{i, cnt});
            tot += cnt;
        }
    }

    public int pickIndex() {
        if (bid >= list.size()) {
            bid = 0; iid = 0;
        }
        int[] info = list.get(bid);
        int id = info[0], cnt = info[1];
        if (iid >= cnt) {
            bid++; iid = 0;
            return pickIndex();
        }
        iid++;
        return id;
    }
}
```
* 时间复杂度:计算 $k$ 的操作只会发生一次,可以看作是一个均摊到每个下标的常数计算,`Solution` 类的构造方法的整体复杂度可看作 $O(n)$;`pickIndex` 的复杂度为 $O(1)$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


LeetCode/531-540/532. 数组中的 k-diff 数对（中等）.md

Question:
给定一个整数数组和一个整数 $k$,你需要在数组里找到 不同的 `k-diff` 数对,并返回不同的 `k-diff` 数对 的数目。

这里将 `k-diff` 数对定义为一个整数对 $(nums[i], nums[j])$,并满足下述全部条件:

* `0 <= i < j < nums.length`
* `|nums[i] - nums[j]| == k`

注意,`|val|` 表示 `val` 的绝对值。

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

输出:2

解释:数组中有两个 2-diff 数对, (1, 3) 和 (3, 5)。
尽管数组中有两个1,但我们只应返回不同的数对的数量。
```
示例 2:
```
输入:nums = [1, 2, 3, 4, 5], k = 1

输出:4

解释:数组中有四个 1-diff 数对, (1, 2), (2, 3), (3, 4) 和 (4, 5)。
```
示例 3:
```
输入:nums = [1, 3, 1, 5, 4], k = 0

输出:1

解释:数组中只有一个 0-diff 数对,(1, 1)。
```

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

Solution:
### 哈希表

一个简单的想法是先使用「哈希表」进行计数。

对于每个 $x = nums[i]$ 而言,根据 $k$ 是否为 $0$ 进行分情况讨论:

* $k$ 为 $0$:此时只能 $(x, x)$ 组成数对,此时判断 $nums[i]$ 出现次数是否大于 $1$ 次,若满足则进行计数加一;
* $k$ 不为 $0$:此时 $x$ 能够与 $a = x - k$ 或 $b = x + k$ 组成数对,分别查询 $a$ 和 $b$ 是否出现过,若出现过则进行计数加一。

为了防止相同的 $x$ 进行重复计数,我们需要统计完 $x$ 后,清空其出现次数。

代码:
```Java
class Solution {
    public int findPairs(int[] nums, int k) {
        Map<Integer, Integer> map = new HashMap<>();
        for (int i : nums) map.put(i, map.getOrDefault(i, 0) + 1);
        int ans = 0;
        for (int i : nums) {
            if (map.get(i) == 0) continue;
            if (k == 0) {
                if (map.get(i) > 1) ans++;
            } else {
                int a = i - k, b = i + k;
                if (map.getOrDefault(a, 0) > 0) ans++;
                if (map.getOrDefault(b, 0) > 0) ans++;
            }
            map.put(i, 0);
        }
        return ans;
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n)$

---

### 离散化 + 二分

我们知道可以使用「数组」充当「哈希表」来减少常数,但由于 $nums[i]$ 的值域大小为 $2 \times 10^7$,可能会有 `MLE` 风险。

我们需要对 $nums$ 进行离散化,预处理出一个去重且有序的数组 `list`。

在统计答案时,我们不再需要在 `nums` 基础上统计,而可以直接在去重数组 `list` 上进行统计,同时将对「数值」的统计操作转换为对「下标」的统计操作。

代码:
```Java
class Solution {
    static int[] cnt = new int[10010];
    List<Integer> list;
    int find(int x) {
        int n = list.size(), l = 0, r = n - 1;
        while (l < r) {
            int mid = l + r >> 1;
            if (list.get(mid) >= x) r = mid;
            else l = mid + 1;
        }
        return list.get(r) == x ? r : -1;
    }
    public int findPairs(int[] nums, int k) {
        Arrays.sort(nums);
        list = new ArrayList<>();
        for (int i : nums) {
            if (list.isEmpty() || i != list.get(list.size() - 1)) list.add(i);
        }
        Arrays.fill(cnt, 0);
        for (int i : nums) cnt[find(i)]++;
        int ans = 0, idx = 0;
        for (int i : list) {
            if (k == 0) {
                if (cnt[idx] > 1) ans++;
            } else {
                int a = find(i - k), b = find(i + k);
                if (a != -1 && cnt[a] > 0) ans++;
                if (b != -1 && cnt[b] > 0) ans++;
            }
            cnt[idx++] = 0;
        }
        return ans;
    }
}
```
* 时间复杂度:排序离散化复杂度为 $O(n\log{n})$,统计答案复杂度为 $O(n\log{n})$。整体复杂度为 $O(n\log{n})$
* 空间复杂度:$O(n)$

---

### 离散化 + 双指针

跟进一步的,对于 $x = nums[i]$ 而言,我们每次都在整段的 `list` 中二分找 $a = nums[i] - k$ 和 $b = nums[i] + k$ 的下标,导致我们统计 $nums[i]$ 对答案的贡献时复杂度为 $O(\log{n})$,统计所有 $nums[i]$ 对答案的贡献为 $O(n\log{n})$。

实际上,利用 `list` 本身的有序性,我们在从前往后处理每个 $nums[idx]$ 时,对应的 $a$ 和 $b$ 也必然是逐步增大,因此我们可以使用「双指针」来避免「二分」操作,使得统计所有 $nums[i]$ 对答案贡献的复杂度从 $O(n\log{n})$ 下降到 $O(n)$。

代码:
```Java
class Solution {
    static int[] cnt = new int[10010];
    public int findPairs(int[] nums, int k) {
        Arrays.sort(nums);
        List<Integer> list = new ArrayList<>();
        for (int i : nums) {
            if (list.isEmpty() || i != list.get(list.size() - 1)) list.add(i);
        }
        Arrays.fill(cnt, 0);
        for (int i = 0, j = 0; i < nums.length; i++) {
            if (nums[i] != list.get(j)) j++;
            cnt[j]++;
        }
        int n = list.size(), idx = 0, ans = 0, l = 0, r = 0;
        for (int i : list) {
            if (k == 0) {
                if (cnt[idx] > 1) ans++;
            } else {
                int a = i - k, b = i + k;
                while (l < n && list.get(l) < a) l++;
                while (r < n && list.get(r) < b) r++;
                if (l < n && list.get(l) == a && cnt[l] > 0) ans++;
                if (r < n && list.get(r) == b && cnt[r] > 0) ans++;
            }
            cnt[idx++] = 0;
        }
        return ans;
    }
}
```
* 时间复杂度:排序离散化复杂度为 $O(n\log{n})$,统计答案复杂度为 $O(n)$,整体复杂度为 $O(n\log{n})$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


Question:
`TinyURL` 是一种 `URL` 简化服务, 比如:当你输入一个 URL `https://leetcode.com/problems/design-tinyurl` 时,它将返回一个简化的URL `http://tinyurl.com/4e9iAk` 。

请你设计一个类来加密与解密 `TinyURL` 。

加密和解密算法如何设计和运作是没有限制的,你只需要保证一个 `URL` 可以被加密成一个 `TinyURL` ,并且这个 `TinyURL` 可以用解密方法恢复成原本的 `URL` 。

实现 `Solution` 类:
* `Solution()` 初始化 `TinyURL` 系统对象。
* `String encode(String longUrl)` 返回 `longUrl` 对应的 `TinyURL` 。
* `String decode(String shortUrl)` 返回 `shortUrl` 原本的 `URL`。题目数据保证给定的 `shortUrl` 是由同一个系统对象加密的。

示例:
```
输入:url = "https://leetcode.com/problems/design-tinyurl"

输出:"https://leetcode.com/problems/design-tinyurl"

解释:
Solution obj = new Solution();
string tiny = obj.encode(url); // 返回加密后得到的 TinyURL 。
string ans = obj.decode(tiny); // 返回解密后得到的原本的 URL 。
```

提示:
* $1 <= url.length <= 10^4$
* 题目数据保证 `url` 是一个有效的 `URL`

Solution:
### 模拟 + 哈希表

对于每个 `longUrl` 我们都在「大写字母/小写字母/数字」中随机 $k = 6$ 位作为其映射标识,这需要使用一个哈希表 `tiny2Origin` 进行记录。

同时了防止相同的 `longUrl` 多次调用,确保 `encode` 服务的「幂等性」,我们再额外建立哈希表 `origin2Tiny` 来记录原串和映射标识的对应关系。

Java 代码:
```Java
public class Codec {
    Map<String, String> origin2Tiny = new HashMap<>(), tiny2Origin = new HashMap<>();
    String str = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890";
    String prefix = "https://acoier.com/tags/";
    int k = 6;
    Random random = new Random();
    public String encode(String longUrl) {
        while (!origin2Tiny.containsKey(longUrl)) {
            char[] cs = new char[k];
            for (int i = 0; i < k; i++) cs[i] = str.charAt(random.nextInt(str.length()));
            String cur = prefix + String.valueOf(cs);
            if (tiny2Origin.containsKey(cur)) continue;
            tiny2Origin.put(cur, longUrl);
            origin2Tiny.put(longUrl, cur);
        }
        return origin2Tiny.get(longUrl);
    }
    public String decode(String shortUrl) {
		return tiny2Origin.get(shortUrl);
    }
}
```

Python 代码:

```Python
class Codec:
    def __init__(self):
        self.origin_to_tiny = {}
        self.tiny_to_origin = {}
        self.k = 6
        self.prefix = 'https://acoier.com/tags/'
        self.ss = string.ascii_letters + string.digits

    def encode(self, longUrl: str) -> str:
        while longUrl not in self.origin_to_tiny.keys():
            cur = ''.join([self.ss[random.randint(0,len(self.ss) - 1)] for _ in range(self.k)])
            if cur in self.tiny_to_origin.keys():
                continue
            self.tiny_to_origin[cur] = longUrl
            self.origin_to_tiny[longUrl] = cur
        return self.origin_to_tiny[longUrl]

    def decode(self, shortUrl: str) -> str:
        return self.tiny_to_origin[shortUrl]
```
* 时间复杂度:`encode` 操作复杂度为 $O(C + L)$,其中 $C = 6$ 为短串长度,$L$ 为传入参数 `longUrl` 的长度(存入哈希表需要计算 `longUrl` 哈希值,该过程需要遍历 `longUrl`);`decode` 操作复杂度为 $O(C)$,其中 $L$ 为传入参数 `shortUrl` 的长度,该长度固定为 `prefix` 长度加 $6$
* 空间复杂度:$O(K \times L)$,其中 $K$ 为调用次数,$L$ 为平均 `longUrl` 长度

---

### 关于使用「自增 `id`」的答疑

群里有同学问到为啥要使用「哈希存储映射关系」的方式,而不是用「自增 `id`」的方式,感觉可能是一部分同学的共同疑惑,这里统一回答一下。

其中两点较为致命的弊端是:

1. 使用「自增 `id`」更容易被枚举;
    (再补充,提出原问题的好奇宝宝又问了另外一个问题)关于被枚举的坏处:粗略来说会导致安全性下降,被攻击的可能性大大增强。因为可枚举意味着,不能光靠 URL 特定发放来确保安全。
    结合具体场景来说,假设某个活动日期生成了短 URL 的活动链接,只有特定用户可以访问,可枚举意味着竞品只需要大概知道你自增 id 当前在什么范围即可拿到活动链接,即使活动链接做了鉴权,也能通过攻击手段导致服务请求数量突增,影响真实的活动用户使用。
    或许这个场景并不致命,竞品要拿到活动链接有很多方式,未必是通过枚举短 URL 来做到。
    但另外一个场景则是致命的,就是可枚举会导致使用短 URL 服务的「验证服务」失效。
    例如某个服务会通过「邮件/短信」发送短 URL 链接,让用户通过点击短 URL 来验证是否为某个「邮箱/手机号」的拥有者,短 URL 可枚举,意味着只需要知道当前自增 `id` 大概范围,就可通过枚举的方式访问到真实的验证地址,从而实现「不收到某个邮件/短信」也可以通过验证的目的。

2. 相比于使用「哈希存储映射关系」的方式,不好重用:

    举个🌰,例如映射关系都是 $7$ 天过期,每天会产生了 $10$ 个短 URL,当进行到第 $8$ 天,此时 `id` 已经自增到 $71$,同时第一天产生的短 URL 已过期,但是实现上,我们不好将 `id` 进行回拨(需要考虑当重用数字用完后要回到 $71$ 的位置)。也就是说,发现而且重用某些数字段(过期的 `id`)会为“自增”带来困扰,而引入「重用池」则违背了选择自增方案的初衷。

    但「哈希存储映射关系」要实现重用则是简单的,如果随机产生的短 URL 发生冲突,只需要直接拒绝进行重试即可,一旦产生短 URL 无冲突,则可以直接使用。同时由于随机通常服从某种分布,我们无须引入「过期时间戳」等信息,即可达到「某个短 URL 在使用后的一段时间后,都不会被随机到(不会在过期后就被迅速重用)」的作用,这一点可以通过调大 $k$ 值来进一步保证。

防杠声明:当然,如果你只是在单纯的做算法题,就当我没说 🤣

### 最后

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

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

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

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


Question:
复数 可以用字符串表示,遵循 "实部+虚部i" 的形式,并满足下述条件:

* 实部 是一个整数,取值范围是 $[-100, 100]$
* 虚部 也是一个整数,取值范围是 $[-100, 100]$
* $i^2 == -1$

给你两个字符串表示的复数 `num1` 和 `num2` ,请你遵循复数表示形式,返回表示它们乘积的字符串。

示例 1:
```
输入:num1 = "1+1i", num2 = "1+1i"

输出:"0+2i"

解释:(1 + i) * (1 + i) = 1 + i2 + 2 * i = 2i ,你需要将它转换为 0+2i 的形式。
```
示例 2:
```
输入:num1 = "1+-1i", num2 = "1+-1i"

输出:"0+-2i"

解释:(1 - i) * (1 - i) = 1 + i2 - 2 * i = -2i ,你需要将它转换为 0+-2i 的形式。
```

提示:
* `num1` 和 `num2` 都是有效的复数表示。

Solution:
### 模拟

本题考察的是「复数乘法法则」运用。

根据 $(a+bi)(c+di)=(ac-bd)+(bc+ad)i$,我们可以直接对 `nums1` 和 `nums2` 进行「实数」和「虚数」分割,按照法则运算后重新组成答案。

代码:
```Java
class Solution {
    public String complexNumberMultiply(String num1, String num2) {
        String[] ss1 = num1.split("\\+|i"), ss2 = num2.split("\\+|i");
        int a = parse(ss1[0]), b = parse(ss1[1]);
        int c = parse(ss2[0]), d = parse(ss2[1]);
        int A = a * c - b * d, B = b * c + a * d;
        return A + "+" + B + "i";
    }
    int parse(String s) {
        return Integer.parseInt(s);
    }
}
```
* 时间复杂度:令 $n$ 和 $m$ 分别为两字符串长度,复杂度上界取决于正则分割,正则复杂度与字符串长度呈正比,整体复杂度为 $O(n + m)$
* 空间复杂度:$O(n + m)$

### 最后

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

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

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

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


LeetCode/531-540/538. 把二叉搜索树转换为累加树（中等）.md

538. 把二叉搜索树转换为累加树

Question:
给出二叉搜索树的根节点,该树的节点值各不相同,请你将其转换为累加树(Greater Sum Tree),使每个节点 `node` 的新值等于原树中大于或等于 `node.val` 的值之和。

提醒一下, 二叉搜索树满足下列约束条件:

* 节点的左子树仅包含键小于节点键的节点。
* 节点的右子树仅包含键大于节点键的节点。
* 左右子树也必须是二叉搜索树。

示例 1:

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

输出:[30,36,21,36,35,26,15,null,null,null,33,null,null,null,8]
```
示例 2:
```
输入:root = [0,null,1]

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

提示:
* 树中的节点数在 $[1, 100]$ 范围内。
* $0 <= Node.val <= 100$
* 树中的所有值均不重复 。

Solution:
### 中序遍历

利用 **`BST` 的中序遍历是有序** 的特性,我们可以通过两次遍历 `BST` 来求解问题。

首先,通过一次遍历,计算出整棵树的节点总和 `tot`,然后在中序遍历过程中,不断对 `tot` 进行更新,将其作为当前未遍历到的节点的总和,用于给当前节点赋值。

假设当前遍历到的节点为 `x`(起始节点值为 `t`),那么将节点更新为当前节点 `tot` 后,更新 `tot = tot - t`。

这是常规的中序遍历做法,更进一步,如果将其中序遍历的顺序进行翻转(从「左中右」调整为「右中左」),则可实现一次遍历。

Java 代码:
```Java
class Solution {
    int tot = 0;
    public TreeNode convertBST(TreeNode root) {
        dfs(root);
        return root;
    }
    void dfs(TreeNode root) {
        if (root == null) return ;
        dfs(root.right);
        tot += root.val;
        root.val = tot;
        dfs(root.left);
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    int tot = 0;
    TreeNode* convertBST(TreeNode* root) {
        dfs(root);
        return root;
    }
    void dfs(TreeNode* root) {
        if (root == nullptr) return;
        dfs(root->right);
        tot += root->val;
        root->val = tot;
        dfs(root->left);
    }
};
```
Python 代码:
```Python
class Solution:
    def convertBST(self, root: TreeNode) -> TreeNode:
        tot = 0
        def dfs(root):
            nonlocal tot
            if not root: return
            dfs(root.right)
            tot += root.val
            root.val = tot
            dfs(root.left)
        dfs(root)
        return root
```
TypeScript 代码:
```TypeScript
function convertBST(root: TreeNode | null): TreeNode | null {
    let tot = 0;
    const dfs = function(root: TreeNode | null): void {
        if (!root) return ;
        dfs(root.right);
        tot += root.val;
        root.val = tot;
        dfs(root.left);
    }
    dfs(root);
    return root;
};
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


Question:
给定一个 $24$ 小时制(小时:分钟 `"HH:MM"`)的时间列表,找出列表中任意两个时间的最小时间差并以分钟数表示。

示例 1:
```
输入:timePoints = ["23:59","00:00"]

输出:1
```
示例 2:
```
输入:timePoints = ["00:00","23:59","00:00"]

输出:0
```

提示:
* $2 <= timePoints <= 2 * 10^4$
* `timePoints[i]` 格式为 `"HH:MM"`

Solution:
### 模拟(排序)

根据题意,我们需要找出「时钟盘」中的夹角最小的两个时间点,其中包括了分布在 `00:00` 左右两侧(横跨了一天)的时间点。

因此,一种简单的方式是对于每个 $timePoints[i]$,我们不仅记录「当天该时间点」对应的的偏移量,还记录「下一天该时间点」对应的偏移量。

处理所有的 $timePoints[i]$ 后,对偏移量进行排序,遍历找到所有相邻元素之间的最小值。

**代码(感谢 [@Benhao](/u/himymben/) 和 [@🍭可乐可乐吗](/u/littletime_cc/) 同学提供的其他语言版本):**
```Java
class Solution {
    public int findMinDifference(List<String> timePoints) {
        int n = timePoints.size() * 2;
        int[] nums = new int[n];
        for (int i = 0, idx = 0; i < n / 2; i++, idx += 2) {
            String[] ss = timePoints.get(i).split(":");
            int h = Integer.parseInt(ss[0]), m = Integer.parseInt(ss[1]);
            nums[idx] = h * 60 + m;
            nums[idx + 1] = nums[idx] + 1440;
        }
        Arrays.sort(nums);
        int ans = nums[1] - nums[0];
        for (int i = 0; i < n - 1; i++) ans = Math.min(ans, nums[i + 1] - nums[i]);
        return ans;
    }
}
```
-
```Python3
class Solution:
    def findMinDifference(self, timePoints: List[str]) -> int:
        n = len(timePoints) * 2
        nums, idx = [0] * n, 0
        for time in timePoints:
            h, m = int(time[:2]), int(time[-2:])
            nums[idx] = h * 60 + m
            nums[idx + 1] = nums[idx] + 1440
            idx += 2
        nums.sort()
        return min(nums[i + 1] - nums[i] for i in range(n - 1))
```
-
```Go
func findMinDifference(timePoints []string) int {
    n := len(timePoints) * 2
    nums := make([]int, n)
    for i, idx := 0, 0; i < n / 2; i++ {
        ss := strings.Split(timePoints[i], ":")
        h, _ := strconv.Atoi(ss[0])
        m, _ := strconv.Atoi(ss[1])
        nums[idx] = h * 60 + m
        nums[idx + 1] = nums[idx] + 1440
        idx += 2
    }
    sort.Ints(nums)
    ans := nums[1] - nums[0];
    for i := 1; i < n - 1; i++ {
        if v := nums[i + 1] - nums[i]; v < ans {
            ans = v
        }
    }
    return ans
}
```
-
```C++
class Solution {
public:
    int findMinDifference(vector<string>& timePoints) {
        int n = timePoints.size() * 2;
        vector<int> nums(n);
        for(int i = 0, idx = 0; i < n / 2; i++, idx += 2){
            string ss = timePoints[i];
            auto pos = ss.find(':');
            int h = stoi(ss.substr(0, pos)), m = stoi(ss.substr(pos + 1));
            nums[idx] = h * 60 + m;
            nums[idx + 1] = nums[idx] + 1440;
        }
        sort(nums.begin(), nums.end());
        int ans = nums[1] - nums[0];
        for(int i = 0; i < n - 1; i++){
            ans = min(ans, nums[i + 1] - nums[i]);
        }
        return ans;
    }
};
```
-
```C
int min(int a,int b){ return a > b ? b : a; }
int cmp(const void* a , const void* b){ return  (*(int*)a) - (*(int*)b); }

int findMinDifference(char ** timePoints, int timePointsSize){
    int n = timePointsSize * 2;
    int* nums = (int*) malloc(sizeof(int) * n);
    for(int i = 0, idx = 0; i < n / 2; i++, idx += 2){
        timePoints[i][2] = '\0';
        int h = atoi(timePoints[i]), m = atoi(timePoints[i] + 3);
        nums[idx] = h * 60 + m;
        nums[idx + 1] = nums[idx] + 1440;
    }
    qsort(nums, n, sizeof(nums[0]), cmp);
    int ans = nums[1] - nums[0];
    for(int i = 0; i < n - 1; i++){
        ans = min(ans, nums[i + 1] - nums[i]);
    }
    free(nums);
    nums = NULL;
    return ans;
}
```
* 时间复杂度:$O(n\log{n})$
* 空间复杂度:$O(n)$

---

### 模拟(哈希表计数)

利用当天最多只有 $60 * 24 = 1440$ 个不同的时间点(跨天的话则是双倍),我们可以使用数组充当哈希表进行计数,同时根据「抽屉原理」,若 $timePoints$ 数量大于 $1440$,必然有两个相同时间点,用作剪枝。

然后找到间隔最小两个时间点,这种利用「桶排序」的思路避免了对 $timePoints$ 所对应的偏移量进行排序,而 $O(C)$ 的复杂度使得所能处理的数据范围没有上限。

代码:
```Java
class Solution {
    public int findMinDifference(List<String> timePoints) {
        int n = timePoints.size();
        if (n > 1440) return 0;
        int[] cnts = new int[1440 * 2 + 10];
        for (String s : timePoints) {
            String[] ss = s.split(":");
            int h = Integer.parseInt(ss[0]), m = Integer.parseInt(ss[1]);
            cnts[h * 60 + m]++;
            cnts[h * 60 + m + 1440]++;
        }
        int ans = 1440, last = -1;
        for (int i = 0; i <= 1440 * 2 && ans != 0; i++) {
            if (cnts[i] == 0) continue;
            if (cnts[i] > 1) ans = 0;
            else if (last != -1) ans = Math.min(ans, i - last);
            last = i;
        }
        return ans;
    }
}
```
* 时间复杂度:$O(C)$
* 空间复杂度:$O(C)$

### 最后

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

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

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

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


540. 有序数组中的单一元素

Question:
给你一个仅由整数组成的有序数组,其中每个元素都会出现两次,唯有一个数只会出现一次。

请你找出并返回只出现一次的那个数。

你设计的解决方案必须满足 $O(\log n)$ 时间复杂度和 $O(1)$ 空间复杂度。

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

输出: 2
```
示例 2:
```
输入: nums =  [3,3,7,7,10,11,11]

输出: 10
```

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

Solution:
### 遍历

数据范围为 $10^5$,最简单的方法是以「步长为 $2$」的方式进行从前往后的遍历,找到第一个不符合「与后一个数相等」条件的值即是答案。

或是利用单个元素只有一个(其余成对出现),从头到尾异或一遍,最终结果为单一元素值。

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

---

### 二分

这道题的「二段性」分析需要一点点「脑筋急转弯」。

由于给定数组有序 且 常规元素总是两两出现,**因此如果不考虑“特殊”的单一元素的话,我们有结论:成对元素中的第一个所对应的下标必然是偶数,成对元素中的第二个所对应的下标必然是奇数。**

然后再考虑存在单一元素的情况,假如单一元素所在的下标为 $x$,那么下标 $x$ 之前(左边)的位置仍满足上述结论,而下标 $x$ 之后(右边)的位置由于 $x$ 的插入,导致结论翻转。

存在这样的二段性,指导我们根据当前二分点 $mid$ 的奇偶性进行分情况讨论:

* $mid$ 为偶数下标:根据上述结论,正常情况下偶数下标的值会与下一值相同,因此如果满足该条件,可以确保 $mid$ 之前并没有插入单一元素。正常情况下,此时应该更新 $l = mid$,否则应当让 $r = mid - 1$,但需要注意这样的更新逻辑,会因为更新 $r$ 时否决 $mid$ 而错过答案,我们可以将否决 $mid$ 的动作放到更新 $l$ 的一侧,即需要将更新逻辑修改为 $l = mid + 1$ 和 $r = mid$ ;

* $mid$ 为奇数下标:同理,根据上述结论,正常情况下奇数下标的值会与上一值相同,因此如果满足该条件,可以确保 $mid$ 之前并没有插入单一元素,相应的更新 $l$ 和 $r$。

代码:
```Java
class Solution {
    public int singleNonDuplicate(int[] nums) {
        int n = nums.length;
        int l = 0, r = n - 1;
        while (l < r) {
            int mid = l + r >> 1;
            if (mid % 2 == 0) {
                if (mid + 1 < n && nums[mid] == nums[mid + 1]) l = mid + 1;
                else r = mid;
            } else {
                if (mid - 1 >= 0 && nums[mid - 1] == nums[mid]) l = mid + 1;
                else r = mid;
            }
        }
        return nums[r];
    }
}
```
* 时间复杂度:$O(\log{n})$
* 空间复杂度:$O(1)$

---

### 二分(异或技巧)

不难发现,上述解法对奇偶下标的分情况讨论仅在于当前值 $nums[i]$ 是与 $nums[i + 1]$(当 $i$ 为偶数时)还是 $nums[i - 1]$(当 $i$ 为奇数时)进行比较。

而这样的「连续段 偶奇 两两成对」的组合,适合使用「异或」来找相应的成组对象。

实际上,该技巧广泛地应用在图论存图中:**使用邻接表(链式向前星)存无向图时,直接访问「当前边 $e$」所对应的「反向边 $e'$」。这也是为什么在「链式向前星」中我们只需要使用「单链表」并设定 $idx$ 从 $0$ 开始进行存图即可:能够满足遍历所有出边,同时如果有访问相应反向边的需求,只需要通过 `e[i^1]` 访问。**

对这种存图方式不熟悉的同学,可以看前置 🧀:[涵盖所有的「存图方式」与「最短路算法(详尽注释)」](https://mp.weixin.qq.com/s?__biz=MzU4NDE3MTEyMA==&mid=2247488007&idx=1&sn=9d0dcfdf475168d26a5a4bd6fcd3505d&chksm=fd9cb918caeb300e1c8844583db5c5318a89e60d8d552747ff8c2256910d32acd9013c93058f&token=381012359&lang=zh_CN#rd)。

代码:
```Java
class Solution {
    public int singleNonDuplicate(int[] nums) {
        int n = nums.length;
        int l = 0, r = n - 1;
        while (l < r) {
            int mid = l + r >> 1;
            if (nums[mid] == nums[mid ^ 1]) l = mid + 1;
            else r = mid;
        }
        return nums[r];
    }
}
```
* 时间复杂度:$O(\log{n})$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


Question:
给定一个字符串 s 和一个整数 k,从字符串开头算起,每 2k 个字符反转前 k 个字符。

* 如果剩余字符少于 k 个,则将剩余字符全部反转。
* 如果剩余字符小于 2k 但大于或等于 k 个,则反转前 k 个字符,其余字符保持原样。

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

输出:"bacdfeg"
```
示例 2:
```
输入:s = "abcd", k = 2

输出:"bacd"
```

提示:
* 1 <= s.length <= $10^4$
* s 仅由小写英文组成
* 1 <= k <= $10^4$

Solution:
### 模拟

使用 `l` 和 `r` 两个指针分别圈出每次需要翻转的“理论”范围,每次翻转完更新 `l` 和 `r`,同时注意范围 $[l, r]$ 内不足 $k$ 个的情况(将 `r` 与真实边界 `n - 1`取个 $min$)。

代码:
```Java
class Solution {
    public String reverseStr(String s, int k) {
        char[] cs = s.toCharArray();
        int n = s.length();
        for (int l = 0; l < n; l = l + 2 * k) {
            int r = l + k - 1;
            reverse(cs, l, Math.min(r, n - 1));
        }
        return String.valueOf(cs);
    }
    void reverse(char[] cs, int l, int r) {
        while (l < r) {
            char c = cs[l];
            cs[l] = cs[r];
            cs[r] = c;
            l++; r--;
        }
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:Java 中 `String` 属于不可变,复杂度为 $O(n)$

### 最后

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

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

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

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


Question:
给你一棵二叉树的根节点,返回该树的直径。

二叉树的直径是指树中任意两个节点之间最长路径的长度。

这条路径可能经过也可能不经过根节点 `root`。

两节点之间路径的长度由它们之间边数表示。

示例 1:

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

输出:3

解释:3 ,取路径 [4,2,1,3] 或 [5,2,1,3] 的长度。
```
示例 2:
```
输入:root = [1,2]

输出:1
```

提示:
* 树中节点数目在范围 $[1, 10^4]$ 内
* $-100 <= Node.val <= 100$

Solution:
### DFS

定义全局变量 `ans`(初始化为 `0`),用于记录二叉树的最大直径。

设计用于遍历二叉树的 `DFS` 函数:函数传入当前节点 `u`,返回以该节点为根时,方向“往下”的最大路径节点数量(注意这里是点数,而不是题目要求的边数)。

单次执行流程中,先递归处理当前节点 `u` 的左右节点,得到左右子树为根时的“往下”最大路径 `l` 和 `r`,两者中的较大值 `+1` 即是本次执行流程的返回值(`+1` 的含义是在子路径基础上增加当前节点)。

同时,`l + r` 则是以当前节点 `u` 为路径最高点时的路径长度,用此更新全局 `ans` 即可。

Java 代码:
```Java
class Solution {
    int ans = 0;
    public int diameterOfBinaryTree(TreeNode root) {
        dfs(root);
        return ans;
    }
    int dfs(TreeNode u) {
        if (u == null) return 0;
        int l = dfs(u.left), r = dfs(u.right);
        ans = Math.max(ans, l + r);
        return Math.max(l, r) + 1;
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    int ans = 0;
    int diameterOfBinaryTree(TreeNode* root) {
        dfs(root);
        return ans;
    }
    int dfs(TreeNode* u) {
        if (u == NULL) return 0;
        int l = dfs(u->left), r = dfs(u->right);
        ans = max(ans, l + r);
        return max(l, r) + 1;
    }
};
```
Python 代码:
```Python
class Solution:
    def diameterOfBinaryTree(self, root: TreeNode) -> int:
        ans = 0
        def dfs(u):
            nonlocal ans
            if not u: return 0
            left, right = dfs(u.left), dfs(u.right)
            ans = max(ans, left + right)
            return max(left, right) + 1
        dfs(root)
        return ans
```
TypeScript 代码:
```TypeScript
function diameterOfBinaryTree(root: TreeNode | null): number {
    let ans = 0;
    const dfs = function(u: TreeNode): number {
        if (!u) return 0;
        const l = dfs(u.left), r = dfs(u.right);
        ans = Math.max(ans, l + r);
        return Math.max(l, r) + 1;
    };
    dfs(root);
    return ans;
};
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


551. 学生出勤记录 I

Question:
给你一个字符串 s 表示一个学生的出勤记录,其中的每个字符用来标记当天的出勤情况(缺勤、迟到、到场)。记录中只含下面三种字符:
* 'A':Absent,缺勤
* 'L':Late,迟到
* 'P':Present,到场

如果学生能够 同时 满足下面两个条件,则可以获得出勤奖励:

按 总出勤 计,学生缺勤('A')严格 少于两天。
学生 不会 存在 连续 3 天或 3 天以上的迟到('L')记录。
如果学生可以获得出勤奖励,返回 true ;否则,返回 false 。

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

输出:true

解释:学生缺勤次数少于 2 次,且不存在 3 天或以上的连续迟到记录。
```
示例 2:
```
输入:s = "PPALLL"

输出:false

解释:学生最后三天连续迟到,所以不满足出勤奖励的条件。
```

提示:
* 1 <= s.length <= 1000
* s[i] 为 'A'、'L' 或 'P'

Solution:
### 模拟

放大假,根据题意进行模拟即可。

代码:
```Java
class Solution {
    public boolean checkRecord(String s) {
        int n = s.length();
        char[] cs = s.toCharArray();
        for (int i = 0, cnt = 0; i < n; ) {
            char c = cs[i];
            if (c == 'A') {
                cnt++;
                if (cnt >= 2) return false;
            } else if (c == 'L') {
                int j = i;
                while (j < n && cs[j] == 'L') j++;
                int len = j - i;
                if (len >= 3) return false;
                i = j;
                continue;
            }
            i++;
        }
        return true;
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:使用 `charAt` 代替 `toCharArray` 的话为 $O(1)$,否则为 $O(n)$

### 最后

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

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

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

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


552. 学生出勤记录 II

Question:
可以用字符串表示一个学生的出勤记录,其中的每个字符用来标记当天的出勤情况(缺勤、迟到、到场)。

记录中只含下面三种字符:

* 'A':Absent,缺勤
* 'L':Late,迟到
* 'P':Present,到场

如果学生能够**同时**满足下面两个条件,则可以获得出勤奖励:

* 按**总出勤**计,学生缺勤('A')严格 少于两天。
* 学生**不会**存在 连续 3 天或 连续 3 天以上的迟到('L')记录。

给你一个整数 n ,表示出勤记录的长度(次数)。请你返回记录长度为 n 时,可能获得出勤奖励的记录情况 数量 。

答案可能很大,所以返回对 $10^9 + 7$ **取余**的结果。

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

输出:8

解释:
有 8 种长度为 2 的记录将被视为可奖励:
"PP" , "AP", "PA", "LP", "PL", "AL", "LA", "LL"
只有"AA"不会被视为可奖励,因为缺勤次数为 2 次(需要少于 2 次)。
```
示例 2:
```
输入:n = 1

输出:3
```
示例 3:
```
输入:n = 10101

输出:183236316
```

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

Solution:
### 基本分析

根据题意,我们知道一个合法的方案中 `A` 的总出现次数最多为 $1$ 次,`L` 的连续出现次数最多为 $2$ 次。

**因此在枚举/统计合法方案的个数时,当我们决策到某一位应该选什么时,我们关心的是当前方案中已经出现了多少个 `A`(以决策当前能否填入 `A`)以及连续出现的 `L` 的次数是多少(以决策当前能否填入 `L`)。**

---

### 记忆化搜索

枚举所有方案的爆搜 `DFS` 代码不难写,大致的函数签名设计如下:

```Java
/**
 * @param u 当前还剩下多少位需要决策
 * @param acnt 当前方案中 A 的总出现次数
 * @param lcnt 当前方案中结尾 L 的连续出现次数
 * @param cur 当前方案
 * @param ans 结果集
 */
void dfs(int u, int acnt, int lcnt, String cur, List<String> ans);
```
实际上,我们不需要枚举所有的方案数,因此我们只需要保留函数签名中的前三个参数即可。

同时由于我们在计算某个 $(u, acnt, lcnt)$ 的方案数时,其依赖的状态可能会被重复使用,考虑加入记忆化,将结果缓存起来。

根据题意,$n$ 的取值范围为 $[0, n]$,$acnt$ 取值范围为 $[0,1]$,$lcnt$ 取值范围为 $[0, 2]$。

代码:
```Java
class Solution {
    int mod = (int)1e9+7;
    int[][][] cache;
    public int checkRecord(int n) {
        cache = new int[n + 1][2][3];
        for (int i = 0; i <= n; i++) {
            for (int j = 0; j < 2; j++) {
                for (int k = 0; k < 3; k++) {
                    cache[i][j][k] = -1;
                }
            }
        }
        return dfs(n, 0, 0);
    }
    int dfs(int u, int acnt, int lcnt) {
        if (acnt >= 2) return 0;
        if (lcnt >= 3) return 0;
        if (u == 0) return 1;
        if (cache[u][acnt][lcnt] != -1) return cache[u][acnt][lcnt];
        int ans = 0;
        ans = dfs(u - 1, acnt + 1, 0) % mod; // A
        ans = (ans + dfs(u - 1, acnt, lcnt + 1)) % mod; // L
        ans = (ans + dfs(u - 1, acnt, 0)) % mod; // P
        cache[u][acnt][lcnt] = ans;
        return ans;
    }
}
```
* 时间复杂度:有 $O(n * 2 * 3)$ 个状态需要被计算,复杂度为 $O(n)$
* 空间复杂度:$O(n)$

---

### 状态机 DP

通过记忆化搜索的分析我们发现,当我们在决策下一位是什么的时候,依赖于前面已经填入的 `A` 的个数以及当前结尾处的 `L` 的连续出现次数。

**也就说是,状态 $f[u][acnt][lcnt]$ 必然被某些特定状态所更新,或者说由 $f[u][[acnt][lcnt]$ 出发,所能更新的状态是固定的。**

因此这其实是一个状态机模型的 DP 问题。

**根据「更新 $f[u][acnt][lcnt]$ 需要哪些状态值」还是「从 $f[u][acnt][lcnt]$  出发,能够更新哪些状态」,我们能够写出两种方式(方向)的 DP 代码:**

**一类是从 $f[u][acnt][lcnt]$ 往回找所依赖的状态;一类是从 $f[u][acnt][lcnt]$  出发往前去更新所能更新的状态值。**

无论是何种方式(方向)的 DP 实现都只需搞清楚「当前位的选择对 $acnt$ 和 $lcnt$ 的影响」即可。

代码:
```Java
// 从 f[u][acnt][lcnt] 往回找所依赖的状态
class Solution {
    int mod = (int)1e9+7;
    public int checkRecord(int n) {
        int[][][] f = new int[n + 1][2][3];
        f[0][0][0] = 1;
        for (int i = 1; i <= n; i++) {
            for (int j = 0; j < 2; j++) {
                for (int k = 0; k < 3; k++) {
                    if (j == 1 && k == 0) { // A
                        f[i][j][k] = (f[i][j][k] + f[i - 1][j - 1][0]) % mod;
                        f[i][j][k] = (f[i][j][k] + f[i - 1][j - 1][1]) % mod;
                        f[i][j][k] = (f[i][j][k] + f[i - 1][j - 1][2]) % mod;
                    }
                    if (k != 0) { // L
                        f[i][j][k] = (f[i][j][k] + f[i - 1][j][k - 1]) % mod;
                    }
                    if (k == 0) { // P
                        f[i][j][k] = (f[i][j][k] + f[i - 1][j][0]) % mod;
                        f[i][j][k] = (f[i][j][k] + f[i - 1][j][1]) % mod;
                        f[i][j][k] = (f[i][j][k] + f[i - 1][j][2]) % mod;
                    }
                }
            }
        }
        int ans = 0;
        for (int j = 0; j < 2; j++) {
            for (int k = 0; k < 3; k++) {
                ans += f[n][j][k];
                ans %= mod;
            }
        }
        return ans;
    }
}
```

```Java
// 从 f[u][acnt][lcnt] 出发往前去更新所能更新的状态值
class Solution {
    int mod = (int)1e9+7;
    public int checkRecord(int n) {
        int[][][] f = new int[n + 1][2][3];
        f[0][0][0] = 1;
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < 2; j++) {
                for (int k = 0; k < 3; k++) {
                    if (j != 1) f[i + 1][j + 1][0] = (f[i + 1][j + 1][0] + f[i][j][k]) % mod; // A
                    if (k != 2) f[i + 1][j][k + 1] = (f[i + 1][j][k + 1] + f[i][j][k]) % mod; // L
                    f[i + 1][j][0] = (f[i + 1][j][0] + f[i][j][k]) % mod; // P
                }
            }
        }
        int ans = 0;
        for (int j = 0; j < 2; j++) {
            for (int k = 0; k < 3; k++) {
                ans += f[n][j][k];
                ans %= mod;
            }
        }
        return ans;
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n)$

---

### 矩阵快速幂

之所以在动态规划解法中强调更新状态的方式(方向)是「往回」还是「往前」,是因为对于存在线性关系(同时又具有结合律)的递推式,我们能够通过「矩阵快速幂」来进行加速。

矩阵快速幂的基本分析之前在 [(题解) 1137. 第 N 个泰波那契数](https://leetcode-cn.com/problems/n-th-tribonacci-number/solution/gong-shui-san-xie-yi-ti-si-jie-die-dai-d-m1ie/) 详细讲过。

由于 $acnt$ 和 $lcnt$ 的取值范围都很小,其组合的状态只有 $2 * 3  = 6$ 种,我们使用 $idx = acnt * 3 + lcnt$ 来代指组合(通用的二维转一维方式):

* $idx = 0$ :$acnt = 0、lcnt = 0$;
* $idx = 1$ :$acnt = 1、lcnt = 0$;
...
* $idx = 5$ :$acnt = 1、lcnt = 2$;

最终答案为 $ans = \sum_{idx = 0}^{5} f[n][idx]$,将答案依赖的状态整理成列向量:

$$
g[n] = \begin{bmatrix}
f[n][0]\\
f[n][1]\\
f[n][2]\\
f[n][3]\\
f[n][4]\\
f[n][5]
\end{bmatrix}
$$

根据状态机逻辑,可得:

$$
g[n] = \begin{bmatrix}
f[n][0]\\
f[n][1]\\
f[n][2]\\
f[n][3]\\
f[n][4]\\
f[n][5]
\end{bmatrix}
 = \begin{bmatrix}
f[n - 1][0] * 1 + f[n - 1][1] * 1 + f[n - 1][2] * 1 + f[n - 1][3] * 0 + f[n - 1][4] * 0 + f[n - 1][5] * 0\\
f[n - 1][0] * 1 + f[n - 1][1] * 0 + f[n - 1][2] * 0 + f[n - 1][3] * 0 + f[n - 1][4] * 0 + f[n - 1][5] * 0\\
f[n - 1][0] * 0 + f[n - 1][1] * 1 + f[n - 1][2] * 0 + f[n - 1][3] * 0 + f[n - 1][4] * 0 + f[n - 1][5] * 0\\
f[n - 1][0] * 1 + f[n - 1][1] * 1 + f[n - 1][2] * 1 + f[n - 1][3] * 1 + f[n - 1][4] * 1 + f[n - 1][5] * 1\\
f[n - 1][0] * 0 + f[n - 1][1] * 0 + f[n - 1][2] * 0 + f[n - 1][3] * 1 + f[n - 1][4] * 0 + f[n - 1][5] * 0\\
f[n - 1][0] * 0 + f[n - 1][1] * 0 + f[n - 1][2] * 0 + f[n - 1][3] * 0 + f[n - 1][4] * 1 + f[n - 1][5] * 0
\end{bmatrix}
$$

我们令:
$$
mat = \begin{bmatrix}
1 &1 &1 &0 &0 &0 \\
1 &0 &0 &0 &0 &0 \\
0 &1 &0 &0 &0 &0 \\
1 &1 &1 &1 &1 &1 \\
0 &0 &0 &1 &0 &0 \\
0 &0 &0 &0 &1 &0
\end{bmatrix}
$$

根据「矩阵乘法」即有:

$$
g[n] = mat * g[n - 1]
$$

起始时,我们只有 $g[0]$,根据递推式得:

$$
g[n] = mat * mat * ... * mat * g[0]
$$

再根据矩阵乘法具有「结合律」,最终可得:

$$
g[n] = mat^n * g[0]
$$

计算 $mat^n$ 可以套用「快速幂」进行求解。

代码:
```Java
class Solution {
    int N = 6;
    int mod = (int)1e9+7;
    long[][] mul(long[][] a, long[][] b) {
        int r = a.length, c = b[0].length, z = b.length;
        long[][] ans = new long[r][c];
        for (int i = 0; i < r; i++) {
            for (int j = 0; j < c; j++) {
                for (int k = 0; k < z; k++) {
                    ans[i][j] += a[i][k] * b[k][j];
                    ans[i][j] %= mod;
                }
            }
        }
        return ans;
    }
    public int checkRecord(int n) {
        long[][] ans = new long[][]{
            {1}, {0}, {0}, {0}, {0}, {0}
        };
        long[][] mat = new long[][]{
            {1, 1, 1, 0, 0, 0},
            {1, 0, 0, 0, 0, 0},
            {0, 1, 0, 0, 0, 0},
            {1, 1, 1, 1, 1, 1},
            {0, 0, 0, 1, 0, 0},
            {0, 0, 0, 0, 1, 0}
        };
        while (n != 0) {
            if ((n & 1) != 0) ans = mul(mat, ans);
            mat = mul(mat, mat);
            n >>= 1;
        }
        int res = 0;
        for (int i = 0; i < N; i++) {
            res += ans[i][0];
            res %= mod;
        }
        return res;
    }
}
```
* 时间复杂度:$O(\log{n})$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


Question:
给定一组正整数,相邻的整数之间将会进行浮点除法操作。例如, [2,3,4] -> 2 / 3 / 4 。

但是,你可以在任意位置添加任意数目的括号,来改变算数的优先级。你需要找出怎么添加括号,才能得到最大的结果,并且返回相应的字符串格式的表达式。你的表达式不应该含有冗余的括号。

示例:
```
输入: [1000,100,10,2]

输出: "1000/(100/10/2)"

解释:
1000/(100/10/2) = 1000/((100/10)/2) = 200
但是,以下加粗的括号 "1000/((100/10)/2)" 是冗余的,
因为他们并不影响操作的优先级,所以你需要返回 "1000/(100/10/2)"。

其他用例:
1000/(100/10)/2 = 50
1000/(100/(10/2)) = 50
1000/100/10/2 = 0.5
1000/100/(10/2) = 2
```
说明:
* 输入数组的长度在 $[1, 10]$ 之间。
* 数组中每个元素的大小都在 $[2, 1000]$ 之间。
* 每个测试用例只有一个最优除法解。

Solution:
### 数学 + 贪心

我们假定取得最优解的表示为 $\frac{a}{b}$,可以留意到任意的 $nums[i]$ 的范围为 $[2, 1000]$,因此我们应当让尽可能多的 $nums[i]$ 参与 $a$(分子)的构建中。

因此一种可以构成最优表示的方式为「将除第一位以外的所有数作为一组,进行连除(转乘法),从而将所有 **可以** 变成分子的数都参与到 $a$ 的构建中」。

即有:
$$
\frac{nums[0]}{nums[1] / nums[2] / ... / nums[n - 1]} = \frac{nums[0]}{nums[1] * \frac{1}{nums[2]} * ... * \frac{1}{nums[n - 1]}} = \frac{nums[0] * nums[2] * ... * nums[n - 1]}{nums[1]}
$$

综上,我们只需要从前往后进行构建出连除的答案,如果 $nums$ 的长度大于 $2$,再追加一对大括号即可。

$[a_0, a_1, ... , a_n]$ => $a_0/a_1/.../a_n$ => $a_0/(a_1/.../a_n)$

代码:
```Java
class Solution {
    public String optimalDivision(int[] nums) {
        int n = nums.length;
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < n; i++) {
            sb.append(nums[i]);
            if (i + 1 < n) sb.append("/");
        }
        if (n > 2) {
            sb.insert(sb.indexOf("/") + 1, "(");
            sb.append(")");
        }
        return sb.toString();
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n * C)$,其中 $C$ 为 $nums[i]$ 的最大长度,对于本题 $C = 4$

### 最后

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

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

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

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


Question:
你的面前有一堵矩形的、由 n 行砖块组成的砖墙。

这些砖块高度相同(也就是一个单位高)但是宽度不同。每一行砖块的宽度之和相等。

你现在要画一条 自顶向下 的、穿过 最少 砖块的垂线。

如果你画的线只是从砖块的边缘经过,就不算穿过这块砖。

你不能沿着墙的两个垂直边缘之一画线,这样显然是没有穿过一块砖的。

给你一个二维数组 wall ,该数组包含这堵墙的相关信息。

其中,wall[i] 是一个代表从左至右每块砖的宽度的数组。

你需要找出怎样画才能使这条线 穿过的砖块数量最少 ,并且返回 穿过的砖块数量 。

示例 1:

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

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

输出:3
```
提示:
* n == wall.length
* 1 <= n <= $10^4$
* 1 <= wall[i].length <= $10^4$
* 1 <= sum(wall[i].length) <= 2 * $10^4$
* 对于每一行 i ,sum(wall[i]) 是相同的
* 1 <= `wall[i][j]` <= $2^{31}$ - 1

Solution:
### 哈希表

题目要求穿过的砖块数量最少,等效于通过的间隙最多。

**我们可以使用「哈希表」记录每个间隙的出现次数,最终统计所有行中哪些间隙出现得最多,使用「总行数」减去「间隙出现的最多次数」即是答案。**

如何记录间隙呢?直接使用行前缀记录即可。

就用示例数据来举 🌰 :

* 第 1 行的间隙有 `[1,3,5]`
* 第 2 行的间隙有 `[3,4]`
* 第 3 行的间隙有 `[1,4]`
* 第 4 行的间隙有 `[2]`
* 第 5 行的间隙有 `[3,4]`
* 第 6 行的间隙有 `[1,4,5]`

对间隙计数完成后,遍历「哈希表」找出出现次数最多间隙 `4`,根据同一个间隙编号只会在单行内被统计一次,用总行数减去出现次数,即得到「最少穿过的砖块数」。

代码:
```Java
class Solution {
    public int leastBricks(List<List<Integer>> wall) {
        int n = wall.size();
        Map<Integer, Integer> map = new HashMap<>();
        for (int i = 0, sum = 0; i < n; i++, sum = 0) {
            for (int cur : wall.get(i)) {
                sum += cur;
                map.put(sum, map.getOrDefault(sum, 0) + 1);
            }
            map.remove(sum); // 不能从两边穿过,需要 remove 掉最后一个
        }
        int ans = n;
        for (int u : map.keySet()) {
            int cnt = map.get(u);
            ans = Math.min(ans, n - cnt);
        }
        return ans;
    }
}
```
* 时间复杂度:记所有砖块数量为 `n`,所有砖块都会被扫描。复杂度为 $O(n)$
* 空间复杂度:$O(n)$

---

### 关于是否需要考虑「溢出」的说明

类似的问题,在之前 [题解](https://leetcode-cn.com/problems/combination-sum-iv/solution/gong-shui-san-xie-yu-wan-quan-bei-bao-we-x0kn/) 也说过,这里再提一下 ~

**当 Java 发生溢出时,会直接转成负数来处理**。因此对于本题不会影响正确性(不重复溢出的话)。

可以通过以下例子来体会:

```Java
{
    System.out.println(Integer.MIN_VALUE); // -2147483648

    int a = Integer.MAX_VALUE;
    System.out.println(a); // 2147483647
    a += 1;
    System.out.println(a); // -2147483648
    a -= 1;
    System.out.println(a); //2147483647
}
```

这意味着,如果我们在运算过程中如果只涉及「纯加减运算」,而不涉及「乘除」、「取最大值/最小值」和「数值大小判断」的话,Java 是不需要使用 `Long` 来确保正确性的,因为最终溢出会被转化回来。

按道理,CPP 本身对于 `int` 溢出的转化处理也是一样的。

**但在 LC 上的 CPP 发生溢出时,不会直接转成负数来处理,而是直接抛出异常**。因此同样的代码在 LC 上是无法被正常执行的:

```C++ []
{
    cout << INT_MIN << endl; //-2147483648

    int a = INT_MAX;
    cout << a << endl; // 2147483647
    a += 1; // 溢出报错
    cout << a << endl;
    a -= 1;
    cout << a << endl;
}
```

**这是一般性的,对于 LC 上的同一道题,Java 不需要处理溢出,CPP 需要处理的原因。**

***
其实对应到本题,我这里不使用 long long,是因为猜想「题目」中的条件写错了:

```Java
1 <= sum(wall[i]) <= 2 * 10^4

错写成了

1 <= sum(wall[i].length) <= 2 * 10^4
```

因为在给定下面两个条件的情况下:

```
1 <= n <= 10^4
1 <= wall[i].length <= 10^4
```

$1 <= sum(wall[i].length) <= 2 * 10^4$ 十分多余。

针对反例:
```Java
[
    [2147483647,2147483647,2147483647,2147483647,1,2],
    [2,2147483647,2147483647,2147483647,2147483647,1]
]
```

我用官方提供的代码,跑的也是 0,所以如果是以官方的代码为评测标准的话,用 long long 反而是 WA 了 🤣

### 最后

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

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

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

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


556. 下一个更大元素 III

Question:
给你一个正整数 $n$,请你找出符合条件的最小整数,其由重新排列 $n$ 中存在的每位数字组成,并且其值大于 $n$。

如果不存在这样的正整数,则返回 $-1$ 。

注意 ,返回的整数应当是一个 $32$ 位整数 ,如果存在满足题意的答案,但不是 $32$ 位整数 ,同样返回 $-1$。

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

输出:21
```
示例 2:
```
输入:n = 21

输出:-1
```

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

Solution:
### 模拟

根据题意,只有给定数字 $x$ 本身从高位到低位是「非严格降序」时,我们无法找到一个合法值。

首先,我们可以先对 $x$ 诸位取出,存成 `nums` 数组(数组的首位元素为原数 $x$ 的最低位)。

然后尝试找到第一个能够交换的位置 `idx`(若无法找到,说明不存在合法值,返回 $-1$),即从 $nums[0]$(原始 $x$ 的最低位)开始找,找到第一个「降序」的位置 `idx`,然后从 $[0, idx)$ 范围内找一个比 $nums[idx]$ 要大的最小数与其进行互换,也是从 $nums[0]$ 开始找,找到第一个满足比 $nums[idx]$ 大的数。

当互换完成后,此时比 $x$ 要大这一目标已由第 `idx` 位所保证(后续的排序应该按照从小到大放置),同时互换结束后的 $[0, idx)$ 仍然满足「非严格升序」特性,因此我们可以对其运用「双指针」进行翻转。

Java 代码:
```Java
class Solution {
    public int nextGreaterElement(int x) {
        List<Integer> nums = new ArrayList<>();
        while (x != 0) {
            nums.add(x % 10);
            x /= 10;
        }
        int n = nums.size(), idx = -1;
        for (int i = 0; i < n - 1 && idx == -1; i++) {
            if (nums.get(i + 1) < nums.get(i)) idx = i + 1;
        }
        if (idx == -1) return -1;
        for (int i = 0; i < idx; i++) {
            if (nums.get(i) > nums.get(idx)) {
                swap(nums, i, idx);
                break;
            }
        }
        for (int l = 0, r = idx - 1; l < r; l++, r--) swap(nums, l, r);
        long ans = 0;
        for (int i = n - 1; i >= 0; i--) ans = ans * 10 + nums.get(i);
        return ans > Integer.MAX_VALUE ? -1 : (int)ans;
    }
    void swap(List<Integer> nums, int a, int b) {
        int c = nums.get(a);
        nums.set(a, nums.get(b));
        nums.set(b, c);
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    int nextGreaterElement(int x) {
        vector<int> nums;
        while (x != 0) {
            nums.push_back(x % 10);
            x /= 10;
        }
        int n = nums.size(), idx = -1;
        for (int i = 0; i < n - 1 && idx == -1; i++) {
            if (nums[i + 1] < nums[i]) idx = i + 1;
        }
        if (idx == -1) return -1;
        for (int i = 0; i < idx; i++) {
            if (nums[i] > nums[idx]) {
                swap(nums[i], nums[idx]);
                break;
            }
        }
        reverse(nums.begin(), nums.begin() + idx);
        long long ans = 0;
        for (int i = n - 1; i >= 0; i--) ans = ans * 10 + nums[i];
        return ans > INT_MAX ? -1 : static_cast<int>(ans);
    }
};
```
Python 代码:
```Python
class Solution:
    def nextGreaterElement(self, x: int) -> int:
        nums = []
        while x != 0:
            nums.append(x % 10)
            x //= 10
        n, idx = len(nums), -1
        for i in range(n - 1):
            if nums[i + 1] < nums[i]:
                idx = i + 1
                break
        if idx == -1: return -1
        for i in range(idx):
            if nums[i] > nums[idx]:
                nums[i], nums[idx] = nums[idx], nums[i]
                break
        l, r = 0, idx - 1
        while l < r:
            nums[l], nums[r] = nums[r], nums[l]
            l, r = l + 1, r - 1
        ans = 0
        for num in nums[::-1]: ans = ans * 10 + num
        return -1 if ans > 2**31 - 1 else ans
```
TypeScript 代码:
```TypeScript
function nextGreaterElement(x: number): number {
    const nums = [];
    while (x !== 0) {
        nums.push(x % 10);
        x = Math.floor(x / 10);
    }
    const n = nums.length;
    let idx = -1;
    for (let i = 0; i < n - 1 && idx == -1; i++) {
        if (nums[i + 1] < nums[i]) idx = i + 1;
    }
    if (idx == -1) return -1;
    for (let i = 0; i < idx; i++) {
        if (nums[i] > nums[idx]) {
            [nums[i], nums[idx]] = [nums[idx], nums[i]];
            break;
        }
    }
    for (let l = 0, r = idx - 1; l < r; l++, r--) [nums[l], nums[r]] = [nums[r], nums[l]];
    let ans = 0;
    for (let i = n - 1; i >= 0; i--) ans = ans * 10 + nums[i];
    return ans > 2**31 - 1 ? -1 : ans;
};
```
* 时间复杂度:$O(\log{n})$
* 空间复杂度:$O(\log{n})$

### 最后

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

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

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

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


Question:
二进制矩阵中的所有元素不是 $0$ 就是 $1$ 。

给你两个四叉树,`quadTree1` 和 `quadTree2`。

其中 `quadTree1` 表示一个 $n \times n$ 二进制矩阵,而 `quadTree2` 表示另一个 $n \times n$ 二进制矩阵。

请你返回一个表示 $n \times n$ 二进制矩阵的四叉树,它是 `quadTree1` 和 `quadTree2` 所表示的两个二进制矩阵进行 按位逻辑或运算 的结果。

注意,当 `isLeaf` 为 `False` 时,你可以把 `True` 或者 `False` 赋值给节点,两种值都会被判题机制 接受 。

四叉树数据结构中,每个内部节点只有四个子节点。此外,每个节点都有两个属性:

* `val`:储存叶子结点所代表的区域的值。$1$ 对应 `True`,$0$ 对应 `False`;
* `isLeaf`: 当这个节点是一个叶子结点时为 `True`,如果它有 $4$ 个子节点则为 `False` 。

```
class Node {
    public boolean val;
    public boolean isLeaf;
    public Node topLeft;
    public Node topRight;
    public Node bottomLeft;
    public Node bottomRight;
}
```
我们可以按以下步骤为二维区域构建四叉树:

1. 如果当前网格的值相同(即,全为 $0$ 或者全为 $1$),将 `isLeaf` 设为 `True` ,将 `val` 设为网格相应的值,并将四个子节点都设为 `Null` 然后停止。
2. 如果当前网格的值不同,将 `isLeaf` 设为 `False`, 将 `val` 设为任意值,然后如下图所示,将当前网格划分为四个子网格。
3. 使用适当的子网格递归每个子节点。

四叉树格式:

输出为使用层序遍历后四叉树的序列化形式,其中 null 表示路径终止符,其下面不存在节点。

它与二叉树的序列化非常相似。唯一的区别是节点以列表形式表示 `[isLeaf, val]` 。

如果 `isLeaf` 或者 `val` 的值为 `True` ,则表示它在列表 `[isLeaf, val]` 中的值为 $1$ ;如果 `isLeaf` 或者 `val` 的值为 `False` ,则表示值为 $0$ 。

示例 1:

```
输入:quadTree1 = [[0,1],[1,1],[1,1],[1,0],[1,0]]
, quadTree2 = [[0,1],[1,1],[0,1],[1,1],[1,0],null,null,null,null,[1,0],[1,0],[1,1],[1,1]]

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

解释:quadTree1 和 quadTree2 如上所示。由四叉树所表示的二进制矩阵也已经给出。
如果我们对这两个矩阵进行按位逻辑或运算,则可以得到下面的二进制矩阵,由一个作为结果的四叉树表示。
注意,我们展示的二进制矩阵仅仅是为了更好地说明题意,你无需构造二进制矩阵来获得结果四叉树。
```
示例 2:
```
输入:quadTree1 = [[1,0]]
, quadTree2 = [[1,0]]

输出:[[1,0]]

解释:两个数所表示的矩阵大小都为 1*1,值全为 0
结果矩阵大小为 1*1,值全为 0 。
```
示例 3:
```
输入:quadTree1 = [[0,0],[1,0],[1,0],[1,1],[1,1]]
, quadTree2 = [[0,0],[1,1],[1,1],[1,0],[1,1]]

输出:[[1,1]]
```
示例 4:
```
输入:quadTree1 = [[0,0],[1,1],[1,0],[1,1],[1,1]]
, quadTree2 = [[0,0],[1,1],[0,1],[1,1],[1,1],null,null,null,null,[1,1],[1,0],[1,0],[1,1]]

输出:[[0,0],[1,1],[0,1],[1,1],[1,1],null,null,null,null,[1,1],[1,0],[1,0],[1,1]]
```
示例 5:
```
输入:quadTree1 = [[0,1],[1,0],[0,1],[1,1],[1,0],null,null,null,null,[1,0],[1,0],[1,1],[1,1]]
, quadTree2 = [[0,1],[0,1],[1,0],[1,1],[1,0],[1,0],[1,0],[1,1],[1,1]]

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

提示:
* `quadTree1` 和 `quadTree2` 都是符合题目要求的四叉树,每个都代表一个 $n \times n$ 的矩阵。
* $n == 2^x$ ,其中 $0 <= x <= 9$

Solution:
### 递归

为了方便,我们令 `quadTree1` 为 `t1`,令 `quadTree2` 为 `t2`。

根据题意,并利用给定函数作为递归函数,当 `t1` 和 `t2` 均为叶子节点数时,执行「与逻辑」,即若 `t1` 和 `t2` 任一值为 $1$ 时,返回该节点,否则(两者均为 $0$),返回任一节点。

然后考虑其他情况下,如何使用 `t1` 和 `t2` 构造新节点 `ans`,分别使用对应位置的进行「递归」构造即可(即使用 `t1.topLeft` 和 `t2.topLeft` 来赋值给 `ans.topLeft`,其余位置同理),要注意可能存在 `t1` 或 `t2` 其中一节点为叶子节点的情况,此时应当使用当前叶子节点和另一节点的子节点进行构造。

最后考虑什么情况下,会产生新的叶子节点:若当前节点 `ans` 的四个子节点均为叶子节点,并且值相同时,`ans` 会成为叶子节点,`ans` 值为叶子节点的值,此时需要执行的操作为将 `isLeaf` 设定为 `True`,修改 `val` 为原子节点的值,将四个原子节点置空。

Java 代码:
```Java
class Solution {
    public Node intersect(Node t1, Node t2) {
        if (t1.isLeaf && t2.isLeaf) {
            if (t1.val) return t1;
            else if (t2.val) return t2;
            else return t1;
        }
        Node ans = new Node();
        ans.topLeft = intersect(t1.isLeaf ? t1 : t1.topLeft, t2.isLeaf ? t2 : t2.topLeft);
        ans.topRight = intersect(t1.isLeaf ? t1 : t1.topRight, t2.isLeaf ? t2 : t2.topRight);
        ans.bottomLeft = intersect(t1.isLeaf ? t1 : t1.bottomLeft, t2.isLeaf ? t2 : t2.bottomLeft);
        ans.bottomRight = intersect(t1.isLeaf ? t1 : t1.bottomRight, t2.isLeaf ? t2 : t2.bottomRight);
        boolean a = ans.topLeft.isLeaf && ans.topRight.isLeaf && ans.bottomLeft.isLeaf && ans.bottomRight.isLeaf;
        boolean b = ans.topLeft.val && ans.topRight.val && ans.bottomLeft.val && ans.bottomRight.val;
        boolean c = ans.topLeft.val || ans.topRight.val || ans.bottomLeft.val || ans.bottomRight.val;
        ans.isLeaf = a && (b || !c);
        ans.val = ans.topLeft.val;
        if (ans.isLeaf) ans.topLeft = ans.topRight = ans.bottomLeft = ans.bottomRight = null;
        return ans;
    }
}
```
TypeScript 代码:
```TypeScript
function intersect(t1: Node | null, t2: Node | null): Node | null {
    if (t1.isLeaf && t2.isLeaf) {
        if (t1.val) return t1
        else if (t2.val) return t2
        else return t1
    }
    const ans: Node = new Node()
    ans.topLeft = intersect(t1.isLeaf ? t1 : t1.topLeft, t2.isLeaf ? t2 : t2.topLeft)
    ans.topRight = intersect(t1.isLeaf ? t1 : t1.topRight, t2.isLeaf ? t2 : t2.topRight)
    ans.bottomLeft = intersect(t1.isLeaf ? t1 : t1.bottomLeft, t2.isLeaf ? t2 : t2.bottomLeft)
    ans.bottomRight = intersect(t1.isLeaf ? t1 : t1.bottomRight, t2.isLeaf ? t2 : t2.bottomRight)
    const a: boolean = ans.topLeft.isLeaf && ans.topRight.isLeaf && ans.bottomLeft.isLeaf && ans.bottomRight.isLeaf
    const b: boolean = ans.topLeft.val && ans.topRight.val && ans.bottomLeft.val && ans.bottomRight.val
    const c: boolean = ans.topLeft.val || ans.topRight.val || ans.bottomLeft.val || ans.bottomRight.val
    ans.isLeaf = a && (b || !c)
    ans.val = ans.topLeft.val
    if (ans.isLeaf) ans.topLeft = ans.topRight = ans.bottomLeft = ans.bottomRight = null
    return ans
};
```
* 时间复杂度:复杂度与最终矩阵大小相关,而最终矩阵大小不会超过原矩阵大小,复杂度为 $O(n^2)$
* 空间复杂度:忽略递归带来的额外空间开销,复杂度为 $O(1)$

---

### 加餐

**另外一道也是「四叉树」相关的递归运用题 : [【综合笔试题】难度 2/5,递归运用及前缀和优化](https://mp.weixin.qq.com/s?__biz=MzU4NDE3MTEyMA==&mid=2247492391&idx=1&sn=bf2b27842d461bba6766b1ca2b426eff) 🎉🎉**

### 最后

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

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

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

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


559. N 叉树的最大深度

Question:
给定一个 `N` 叉树,找到其最大深度。

最大深度是指从根节点到最远叶子节点的最长路径上的节点总数。

`N` 叉树输入按层序遍历序列化表示,每组子节点由空值分隔(请参见示例)。

示例 1:

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

输出:3
```
示例 2:

```
输入:root = [1,null,2,3,4,5,null,null,6,7,null,8,null,9,10,null,null,11,null,12,null,13,null,null,14]

输出:5
```

提示:
* 树的深度不会超过 1000 。
* 树的节点数目位于 $[0, 10^4]$ 之间。

Solution:
### DFS

根据题意,可以写出如下的 `DFS` 实现:从 $root$ 的所有子节点中的取最大深度,并在此基础上加一(统计 $root$ 节点)即是答案。

代码:
```Java
class Solution {
    public int maxDepth(Node root) {
        if (root == null) return 0;
        int ans = 0;
        for (Node node : root.children) {
            ans = Math.max(ans, maxDepth(node));
        }
        return ans + 1;
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:忽略递归带来的额外空间开销,复杂度为 $O(1)$

---

### BFS

同理,可以使用 `BFS`  进行求解:其本质是对多叉树进行层序处理,当 `BFS`  过程结束,意味着达到最大层数(深度),所记录的最大层数(深度)即是答案。

代码:
```Java
class Solution {
    public int maxDepth(Node root) {
        if (root == null) return 0;
        int ans = 0;
        Deque<Node> d = new ArrayDeque<>();
        d.addLast(root);
        while (!d.isEmpty()) {
            int size = d.size();
            while (size-- > 0) {
                Node t = d.pollFirst();
                for (Node node : t.children) {
                    d.addLast(node);
                }
            }
            ans++;
        }
        return ans;
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


560. 和为 K 的子数组

Question:
给你一个整数数组 `nums` 和一个整数 `k` ,请你统计并返回该数组中和为 `k` 的子数组的个数 。

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

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

输出:2
```

提示:
* $1 <= nums.length <= 2 \times 10^4$
* $-1000 <= nums[i] <= 1000$
* $-10^7 <= k <= 10^7$

Solution:
### 前缀和 + 哈希表

这是一道经典的前缀和运用题。

统计以每一个 $nums[i]$ 为结尾,和为 $k$ 的子数组数量即是答案。

我们可以预处理前缀和数组 `sum`(前缀和数组下标默认从 $1$ 开始),对于求解以某一个 $nums[i]$ 为结尾的,和为 $k$ 的子数组数量,本质上是求解在 $[0, i]$ 中,`sum` 数组中有多少个值为 $sum[i + 1] - k$ 的数,这可以在遍历过程中使用「哈希表」进行同步记录。

Java 代码:
```Java
class Solution {
    public int subarraySum(int[] nums, int k) {
        int n = nums.length, ans = 0;
        int[] sum = new int[n + 10];
        for (int i = 1; i <= n; i++) sum[i] = sum[i - 1] + nums[i - 1];
        Map<Integer, Integer> map = new HashMap<>();
        map.put(0, 1);
        for (int i = 1; i <= n; i++) {
            int t = sum[i], d = t - k;
            ans += map.getOrDefault(d, 0);
            map.put(t, map.getOrDefault(t, 0) + 1);
        }
        return ans;
    }
}
```
TypeScript 代码:
```TypeScript
function subarraySum(nums: number[], k: number): number {
    let n = nums.length, ans = 0
    const sum = new Array<number>(n + 10).fill(0)
    for (let i = 1; i <= n; i++) sum[i] = sum[i - 1] + nums[i - 1]
    const map = new Map<number, number>()
    map.set(0, 1)
    for (let i = 1; i <= n; i++) {
        const t = sum[i], d = t - k
        if (map.has(d)) ans += map.get(d)
        map.set(t, map.has(t) ? map.get(t) + 1 : 1)
    }
    return ans
};
```
* 时间复杂度:预处理前缀和的复杂度为 $O(n)$,统计答案的复杂度为 $O(n)$。整体复杂度为 $O(n)$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


Question:
给定长度为 $2 \times n$ 的整数数组 `nums `,你的任务是将这些数分成 `n` 对, 例如 $(a_1, b_1), (a_2, b_2), ..., (a_n, b_n)$ ,使得从 `1` 到 `n` 的 $min(a_i, b_i)$ 总和最大。

返回该 最大总和 。

示例 1:
```
输入:nums = [1,4,3,2]
输出:4
解释:所有可能的分法(忽略元素顺序)为:
1. (1, 4), (2, 3) -> min(1, 4) + min(2, 3) = 1 + 2 = 3
2. (1, 3), (2, 4) -> min(1, 3) + min(2, 4) = 1 + 2 = 3
3. (1, 2), (3, 4) -> min(1, 2) + min(3, 4) = 1 + 3 = 4
所以最大总和为 4
```
示例 2:
```
输入:nums = [6,2,6,5,1,2]
输出:9
解释:最优的分法为 (2, 1), (2, 5), (6, 6). min(2, 1) + min(2, 5) + min(6, 6) = 1 + 2 + 6 = 9
```

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

Solution:
### 贪心解法

我们先对数组进行排序。

由于每两个数,我们只能选择当前小的一个进行累加。

因此我们猜想应该从第一个位置进行选择,然后隔一步选择下一个数。这样形成的序列的求和值最大。

```java
class Solution {
    public int arrayPairSum(int[] nums) {
        int n = nums.length;
        Arrays.sort(nums);
        int ans = 0;
        for (int i = 0; i < n; i += 2) ans += nums[i];
        return ans;
    }
}
```
* 时间复杂度:$O(n\log{n})$
* 空间复杂度:$O(\log{n})$

***

### 证明

我们用反证法来证明下,为什么这样选择的序列的求和值一定是最大的:

猜想:对数组进行排序,从第一个位置进行选择,然后隔一步选择下一个数。这样形成的序列的求和值最大(下图黑标,代表当前被选择的数字)。

之所以我们能这么选择,是因为每一个被选择的数的「下一位位置」都对应着一个「大于等于」当前数的值(假设位置为 `k` ),使得当前数在 `min(a,b)` 关系中能被选择(下图红标,代表保证前面一个黑标能够被选择的辅助数)。

假如我们这样选择的序列求和不是最大值,那么说明至少我们有一个值选错了,应该选择更大的数才对。

那么意味着我们「某一位置」的黑标应该从当前位置指向更后的位置。

**PS. 因为要满足 min(a, b) 的数才会被累加,因此每一个红标右移(变大)必然导致原本所对应的黑标发生「同样程度 或 不同程度」的右移(变大)**

这会导致我们所有的红标黑标同时往后平移。

最终会导致我们最后一个黑标出现在最后一位,这时候最后一位黑标不得不与我们第 `k` 个位置的数形成一对。

我们看看这是求和序列的变化( `k` 位置前的求和项没有发生变化,我们从 `k` 位置开始分析):

1. 原答案 = `nums[k] + nums[k + 2] + ... + nums[n - 1]`
2. 调整后答案 = `nums[k + 1] + nums[k + 3] + ... + nums[n - 2] + min(nums[n], nums[k])`
由于 `min(nums[n], nums[k])` 中必然是 `nums[k]` 被选择。因此:
调整后答案 = `nums[k] + nums[k + 1] + nums[k + 3] + ... + nums[n - 2]`

显然从原答案的每一项都「大于等于」调整后答案的每一项,因此**不可能在「假想序列」中通过选择别的更大的数得到更优解,假想得证。**

***

### 为什么要「证明」或「理解证明」?

证明的意义在于,**你知道为什么这样做是对的**。

带来的好处是:

1. 一道「贪心」题目能搞清楚证明,那么同类的「贪心」题目你就都会做了。**否则就会停留在“我知道这道题可以这样贪心,别的题我不确定是否也能这样做”**。
2. 在「面试」阶段,**你可以很清晰讲解你的思路**。让面试官从你的「思维方式」上喜欢上你( emmm 当然从颜值上也可以 :)
...

***

### 更多与证明/分析相关的题解:

[765. 情侣牵手](https://leetcode-cn.com/problems/couples-holding-hands/) : [为什么交换任意一个都是对的?:两种 100% 的解法:并查集 & 贪心](https://leetcode-cn.com/problems/couples-holding-hands/solution/liang-chong-100-de-jie-fa-bing-cha-ji-ta-26a6/)
[1579. 保证图可完全遍历](https://leetcode-cn.com/problems/remove-max-number-of-edges-to-keep-graph-fully-traversable/) : [为什么先处理公共边是对的?含贪心证明 + 数组模板 ~](https://leetcode-cn.com/problems/remove-max-number-of-edges-to-keep-graph-fully-traversable/solution/tan-xin-bing-cha-ji-shu-zu-shi-xian-jian-w7ko/)

[1631. 最小体力消耗路径](https://leetcode-cn.com/problems/path-with-minimum-effort/) : [反证法证明思路的合法性](https://leetcode-cn.com/problems/path-with-minimum-effort/solution/fan-zheng-fa-zheng-ming-si-lu-de-he-fa-x-ohby/)

[11. 盛最多水的容器](https://leetcode-cn.com/problems/container-with-most-water/) : [双指针+贪心解法【含证明】](https://leetcode-cn.com/problems/container-with-most-water/solution/shua-chuan-lc-shuang-zhi-zhen-tan-xin-ji-52gf/)

### 最后

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

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

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

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


Question:
给定一个二叉树,计算**整个树**的坡度 。

一个树的 节点的坡度 定义即为,该节点左子树的节点之和和右子树节点之和的 差的绝对值 。

如果没有左子树的话,左子树的节点之和为 $0$ ;没有右子树的话也是一样。空结点的坡度是 $0$ 。

整个树 的坡度就是其所有节点的坡度之和。

示例 1:

```
输入:root = [1,2,3]

输出:1

解释:
节点 2 的坡度:|0-0| = 0(没有子节点)
节点 3 的坡度:|0-0| = 0(没有子节点)
节点 1 的坡度:|2-3| = 1(左子树就是左子节点,所以和是 2 ;右子树就是右子节点,所以和是 3 )
坡度总和:0 + 0 + 1 = 1
```
示例 2:

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

输出:15

解释:
节点 3 的坡度:|0-0| = 0(没有子节点)
节点 5 的坡度:|0-0| = 0(没有子节点)
节点 7 的坡度:|0-0| = 0(没有子节点)
节点 2 的坡度:|3-5| = 2(左子树就是左子节点,所以和是 3 ;右子树就是右子节点,所以和是 5 )
节点 9 的坡度:|0-7| = 7(没有左子树,所以和是 0 ;右子树正好是右子节点,所以和是 7 )
节点 4 的坡度:|(3+5+2)-(9+7)| = |10-16| = 6(左子树值为 3、5 和 2 ,和是 10 ;右子树值为 9 和 7 ,和是 16 )
坡度总和:0 + 0 + 0 + 2 + 7 + 6 = 15
```
示例 3:

```
输入:root = [21,7,14,1,1,2,2,3,3]

输出:9
```

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

Solution:
### 递归

根据题目对「坡度」的定义,我们可以直接写出对应的递归实现。

代码:
```Java
class Solution {
    public int findTilt(TreeNode root) {
        if (root == null) return 0;
        return findTilt(root.left) + findTilt(root.right) + Math.abs(getSum(root.left) - getSum(root.right));
    }
    int getSum(TreeNode root) {
        if (root == null) return 0;
        return getSum(root.left) + getSum(root.right) + root.val;
    }
}
```
* 时间复杂度:每个节点被访问的次数与其所在深度有关。复杂度为 $O(n^2)$
* 空间复杂度:忽略递归来带的额外空间消耗。复杂度为 $O(1)$

---

### 递归

上述解法之所以为 $O(n^2)$ 的时间复杂度,是因为我们将「计算子树坡度」和「计算子树权值和」两个操作分开进行。

事实上,我们可以在计算子树权值和的时候将坡度进行累加,从而将复杂度降为 $O(n)$。

代码:
```Java
class Solution {
    int ans;
    public int findTilt(TreeNode root) {
        dfs(root);
        return ans;
    }
    int dfs(TreeNode root) {
        if (root == null) return 0;
        int l = dfs(root.left), r = dfs(root.right);
        ans += Math.abs(l - r);
        return l + r + root.val;
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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