diff --git a/docs/00_preface/00_05_solutions_list.md b/docs/00_preface/00_05_solutions_list.md index aa3996bd..fe9755fc 100644 --- a/docs/00_preface/00_05_solutions_list.md +++ b/docs/00_preface/00_05_solutions_list.md @@ -1,4 +1,4 @@ -# LeetCode 题解(已完成 927 道) +# LeetCode 题解(已完成 940 道) ### 第 1 ~ 99 题 @@ -305,17 +305,22 @@ | [0300. 最长递增子序列](https://leetcode.cn/problems/longest-increasing-subsequence/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/longest-increasing-subsequence.md) | 数组、二分查找、动态规划 | 中等 | | [0303. 区域和检索 - 数组不可变](https://leetcode.cn/problems/range-sum-query-immutable/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/range-sum-query-immutable.md) | 设计、数组、前缀和 | 简单 | | [0304. 二维区域和检索 - 矩阵不可变](https://leetcode.cn/problems/range-sum-query-2d-immutable/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/range-sum-query-2d-immutable.md) | 设计、数组、矩阵、前缀和 | 中等 | +| [0306. 累加数](https://leetcode.cn/problems/additive-number/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/additive-number.md) | 字符串、回溯 | 中等 | | [0307. 区域和检索 - 数组可修改](https://leetcode.cn/problems/range-sum-query-mutable/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/range-sum-query-mutable.md) | 设计、树状数组、线段树、数组、分治 | 中等 | | [0309. 买卖股票的最佳时机含冷冻期](https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-with-cooldown/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/best-time-to-buy-and-sell-stock-with-cooldown.md) | 数组、动态规划 | 中等 | | [0310. 最小高度树](https://leetcode.cn/problems/minimum-height-trees/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/minimum-height-trees.md) | 深度优先搜索、广度优先搜索、图、拓扑排序 | 中等 | | [0312. 戳气球](https://leetcode.cn/problems/burst-balloons/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/burst-balloons.md) | 数组、动态规划 | 困难 | +| [0314. 二叉树的垂直遍历](https://leetcode.cn/problems/binary-tree-vertical-order-traversal/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/binary-tree-vertical-order-traversal.md) | 树、深度优先搜索、广度优先搜索、哈希表、二叉树、排序 | 中等 | | [0315. 计算右侧小于当前元素的个数](https://leetcode.cn/problems/count-of-smaller-numbers-after-self/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/count-of-smaller-numbers-after-self.md) | 树状数组、线段树、数组、二分查找、分治、有序集合、归并排序 | 困难 | | [0316. 去除重复字母](https://leetcode.cn/problems/remove-duplicate-letters/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/remove-duplicate-letters.md) | 栈、贪心、字符串、单调栈 | 中等 | | [0318. 最大单词长度乘积](https://leetcode.cn/problems/maximum-product-of-word-lengths/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/maximum-product-of-word-lengths.md) | 位运算、数组、字符串 | 中等 | +| [0319. 灯泡开关](https://leetcode.cn/problems/bulb-switcher/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/bulb-switcher.md) | 脑筋急转弯、数学 | 中等 | +| [0321. 拼接最大数](https://leetcode.cn/problems/create-maximum-number/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/create-maximum-number.md) | 栈、贪心、数组、双指针、单调栈 | 困难 | | [0322. 零钱兑换](https://leetcode.cn/problems/coin-change/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/coin-change.md) | 广度优先搜索、数组、动态规划 | 中等 | | [0323. 无向图中连通分量的数目](https://leetcode.cn/problems/number-of-connected-components-in-an-undirected-graph/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/number-of-connected-components-in-an-undirected-graph.md) | 深度优先搜索、广度优先搜索、并查集、图 | 中等 | | [0324. 摆动排序 II](https://leetcode.cn/problems/wiggle-sort-ii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/wiggle-sort-ii.md) | 贪心、数组、分治、快速选择、排序 | 中等 | | [0326. 3 的幂](https://leetcode.cn/problems/power-of-three/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/power-of-three.md) | 递归、数学 | 简单 | +| [0327. 区间和的个数](https://leetcode.cn/problems/count-of-range-sum/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/count-of-range-sum.md) | 树状数组、线段树、数组、二分查找、分治、有序集合、归并排序 | 困难 | | [0328. 奇偶链表](https://leetcode.cn/problems/odd-even-linked-list/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/odd-even-linked-list.md) | 链表 | 中等 | | [0329. 矩阵中的最长递增路径](https://leetcode.cn/problems/longest-increasing-path-in-a-matrix/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/longest-increasing-path-in-a-matrix.md) | 深度优先搜索、广度优先搜索、图、拓扑排序、记忆化搜索、数组、动态规划、矩阵 | 困难 | | [0334. 递增的三元子序列](https://leetcode.cn/problems/increasing-triplet-subsequence/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/increasing-triplet-subsequence.md) | 贪心、数组 | 中等 | @@ -330,13 +335,19 @@ | [0345. 反转字符串中的元音字母](https://leetcode.cn/problems/reverse-vowels-of-a-string/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/reverse-vowels-of-a-string.md) | 双指针、字符串 | 简单 | | [0346. 数据流中的移动平均值](https://leetcode.cn/problems/moving-average-from-data-stream/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/moving-average-from-data-stream.md) | 设计、队列、数组、数据流 | 简单 | | [0347. 前 K 个高频元素](https://leetcode.cn/problems/top-k-frequent-elements/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/top-k-frequent-elements.md) | 数组、哈希表、分治、桶排序、计数、快速选择、排序、堆(优先队列) | 中等 | +| [0348. 设计井字棋](https://leetcode.cn/problems/design-tic-tac-toe/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/design-tic-tac-toe.md) | 设计、数组、哈希表、矩阵、模拟 | 中等 | | [0349. 两个数组的交集](https://leetcode.cn/problems/intersection-of-two-arrays/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/intersection-of-two-arrays.md) | 数组、哈希表、双指针、二分查找、排序 | 简单 | | [0350. 两个数组的交集 II](https://leetcode.cn/problems/intersection-of-two-arrays-ii/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/intersection-of-two-arrays-ii.md) | 数组、哈希表、双指针、二分查找、排序 | 简单 | | [0351. 安卓系统手势解锁](https://leetcode.cn/problems/android-unlock-patterns/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/android-unlock-patterns.md) | 位运算、动态规划、回溯、状态压缩 | 中等 | +| [0352. 将数据流变为多个不相交区间](https://leetcode.cn/problems/data-stream-as-disjoint-intervals/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/data-stream-as-disjoint-intervals.md) | 设计、二分查找、有序集合 | 困难 | +| [0353. 贪吃蛇](https://leetcode.cn/problems/design-snake-game/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/design-snake-game.md) | 设计、队列、数组、哈希表、模拟 | 中等 | | [0354. 俄罗斯套娃信封问题](https://leetcode.cn/problems/russian-doll-envelopes/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/russian-doll-envelopes.md) | 数组、二分查找、动态规划、排序 | 困难 | +| [0355. 设计推特](https://leetcode.cn/problems/design-twitter/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/design-twitter.md) | 设计、哈希表、链表、堆(优先队列) | 中等 | | [0357. 统计各位数字都不同的数字个数](https://leetcode.cn/problems/count-numbers-with-unique-digits/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/count-numbers-with-unique-digits.md) | 数学、动态规划、回溯 | 中等 | | [0359. 日志速率限制器](https://leetcode.cn/problems/logger-rate-limiter/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/logger-rate-limiter.md) | 设计、哈希表、数据流 | 简单 | | [0360. 有序转化数组](https://leetcode.cn/problems/sort-transformed-array/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/sort-transformed-array.md) | 数组、数学、双指针、排序 | 中等 | +| [0361. 轰炸敌人](https://leetcode.cn/problems/bomb-enemy/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/bomb-enemy.md) | 数组、动态规划、矩阵 | 中等 | +| [0362. 敲击计数器](https://leetcode.cn/problems/design-hit-counter/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/design-hit-counter.md) | 设计、队列、数组、二分查找、数据流 | 中等 | | [0367. 有效的完全平方数](https://leetcode.cn/problems/valid-perfect-square/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/valid-perfect-square.md) | 数学、二分查找 | 简单 | | [0370. 区间加法](https://leetcode.cn/problems/range-addition/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/range-addition.md) | 数组、前缀和 | 中等 | | [0371. 两整数之和](https://leetcode.cn/problems/sum-of-two-integers/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/sum-of-two-integers.md) | 位运算、数学 | 中等 | @@ -345,12 +356,14 @@ | [0376. 摆动序列](https://leetcode.cn/problems/wiggle-subsequence/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/wiggle-subsequence.md) | 贪心、数组、动态规划 | 中等 | | [0377. 组合总和 Ⅳ](https://leetcode.cn/problems/combination-sum-iv/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/combination-sum-iv.md) | 数组、动态规划 | 中等 | | [0378. 有序矩阵中第 K 小的元素](https://leetcode.cn/problems/kth-smallest-element-in-a-sorted-matrix/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/kth-smallest-element-in-a-sorted-matrix.md) | 数组、二分查找、矩阵、排序、堆(优先队列) | 中等 | +| [0379. 电话目录管理系统](https://leetcode.cn/problems/design-phone-directory/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/design-phone-directory.md) | 设计、队列、数组、哈希表、链表 | 中等 | | [0380. O(1) 时间插入、删除和获取随机元素](https://leetcode.cn/problems/insert-delete-getrandom-o1/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/insert-delete-getrandom-o1.md) | 设计、数组、哈希表、数学、随机化 | 中等 | | [0383. 赎金信](https://leetcode.cn/problems/ransom-note/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/ransom-note.md) | 哈希表、字符串、计数 | 简单 | | [0384. 打乱数组](https://leetcode.cn/problems/shuffle-an-array/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/shuffle-an-array.md) | 设计、数组、数学、随机化 | 中等 | | [0386. 字典序排数](https://leetcode.cn/problems/lexicographical-numbers/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/lexicographical-numbers.md) | 深度优先搜索、字典树 | 中等 | | [0387. 字符串中的第一个唯一字符](https://leetcode.cn/problems/first-unique-character-in-a-string/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/first-unique-character-in-a-string.md) | 队列、哈希表、字符串、计数 | 简单 | | [0389. 找不同](https://leetcode.cn/problems/find-the-difference/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/find-the-difference.md) | 位运算、哈希表、字符串、排序 | 简单 | +| [0390. 消除游戏](https://leetcode.cn/problems/elimination-game/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/elimination-game.md) | 递归、数学 | 中等 | | [0391. 完美矩形](https://leetcode.cn/problems/perfect-rectangle/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/perfect-rectangle.md) | 几何、数组、哈希表、数学、扫描线 | 困难 | | [0392. 判断子序列](https://leetcode.cn/problems/is-subsequence/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/is-subsequence.md) | 双指针、字符串、动态规划 | 简单 | | [0394. 字符串解码](https://leetcode.cn/problems/decode-string/) | [题解](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/decode-string.md) | 栈、递归、字符串 | 中等 | diff --git a/docs/others/update_time.md b/docs/others/update_time.md index 8c77e6c9..faae8989 100644 --- a/docs/others/update_time.md +++ b/docs/others/update_time.md @@ -1,5 +1,6 @@ ## 2025-10 +- 2025-10-21 补充第 300 ~ 399 题的题目解析(增加 13 道题) - 2025-10-20 补全第 200 ~ 299 题的题目解析(增加 33 道题) - 2025-10-19 补充第 200 ~ 299 题的题目解析(增加 8 道题) - 2025-10-17 补全第 100 ~ 199 题的题目解析(增加 12 道题) diff --git a/docs/solutions/0200-0299/palindrome-permutation-ii.md b/docs/solutions/0200-0299/palindrome-permutation-ii.md index 918a3718..90ed8df1 100644 --- a/docs/solutions/0200-0299/palindrome-permutation-ii.md +++ b/docs/solutions/0200-0299/palindrome-permutation-ii.md @@ -102,5 +102,5 @@ class Solution: ### 思路 1:复杂度分析 -- **时间复杂度**:令 $m = \sum_i \lfloor c_i/2 \rfloor$。半串的唯一排列数为 $\dfrac{m!}{\prod_i (\lfloor c_i/2 \rfloor)!}$。对每个半串构造完整回文需要 $O(n)$ 拼接与拷贝,因此总复杂度为 $O\!\left(\dfrac{m!}{\prod_i (\lfloor c_i/2 \rfloor)!} \cdot n\right)$。在 $n \le 16$ 的约束下不会超时。 +- **时间复杂度**:令 $m = \sum_i \lfloor c_i/2 \rfloor$。半串的唯一排列数为 $\dfrac{m!}{\prod_i (\lfloor c_i/2 \rfloor)!}$。对每个半串构造完整回文需要 $O(n)$ 拼接与拷贝,因此总复杂度为 $O\!\left(\dfrac{m!}{\prod_i (\lfloor c_i/2 \rfloor)!} \times n\right)$。在 $n \le 16$ 的约束下不会超时。 - **空间复杂度**:回溯深度与路径为 $O(m)$,计数表为 $O(\Sigma)$($\Sigma$ 为字符集大小,这里为小写字母)。不计输出的额外空间为 $O(m + \Sigma)$。 diff --git a/docs/solutions/0300-0399/additive-number.md b/docs/solutions/0300-0399/additive-number.md new file mode 100644 index 00000000..37dc80ce --- /dev/null +++ b/docs/solutions/0300-0399/additive-number.md @@ -0,0 +1,130 @@ +# [0306. 累加数](https://leetcode.cn/problems/additive-number/) + +- 标签:字符串、回溯 +- 难度:中等 + +## 题目链接 + +- [0306. 累加数 - 力扣](https://leetcode.cn/problems/additive-number/) + +## 题目大意 + +**描述**: + +「累加数」是一个字符串,组成它的数字可以形成累加序列。 +一个有效的 累加序列 必须「至少」包含 $3$ 个数。除了最开始的两个数以外,序列中的每个后续数字必须是它之前两个数字之和。 + +给定一个只包含数字 $0 \sim 9$ 的字符串。 + +**要求**: + +编写一个算法来判断给定输入是否是「累加数 」。如果是,返回 $true$;否则,返回 $false$。 + +**说明**: + +- 累加序列里的数,除数字 $0$ 之外,不会以 $0$ 开头,所以不会出现 $1, 2, 03$ 或者 $1, 02, 3$ 的情况。 +- $1 \le num.length \le 35$。 +- $num$ 仅由数字 $0 sim 9$ 组成。 + +- 进阶:你计划如何处理由过大的整数输入导致的溢出? + +**示例**: + +- 示例 1: + +```python +输入:"112358" +输出:true +解释:累加序列为: 1, 1, 2, 3, 5, 8 。1 + 1 = 2, 1 + 2 = 3, 2 + 3 = 5, 3 + 5 = 8 +``` + +- 示例 2: + +```python +输入:"199100199" +输出:true +解释:累加序列为: 1, 99, 100, 199。1 + 99 = 100, 99 + 100 = 199 +``` + +## 解题思路 + +### 思路 1:回溯算法 + +累加数问题可以通过回溯算法来解决。我们需要找到字符串中所有可能的数字分割方式,使得分割后的数字序列满足累加数的定义。 + +**问题分析**: + +对于字符串 $num$,我们需要: + +- 确定前两个数字 $a$ 和 $b$。 +- 验证后续数字是否满足 $c = a + b$ 的关系。 +- 递归验证整个序列。 + +**算法步骤**: + +1. **枚举前两个数字**:使用双重循环枚举所有可能的前两个数字 $a$ 和 $b$ 的起始位置。 +2. **验证数字有效性**:确保数字不以 $0$ 开头(除非数字本身就是 $0$)。 +3. **递归验证序列**:从第三个数字开始,验证每个数字是否等于前两个数字的和。 +4. **回溯剪枝**:如果某个分支不满足条件,立即返回 $false$。 + +###### 3. 关键变量 + +- $i$:第一个数字的结束位置。 +- $j$:第二个数字的结束位置。 +- $a$:第一个数字的值。 +- $b$:第二个数字的值。 +- $sum$:前两个数字的和,用于验证第三个数字。 + +### 思路 1:代码 + +```python +class Solution: + def isAdditiveNumber(self, num: str) -> bool: + n = len(num) + + # 枚举前两个数字的所有可能位置 + for i in range(1, n): + for j in range(i + 1, n): + # 获取第一个数字 + first = num[:i] + # 获取第二个数字 + second = num[i:j] + + # 检查数字是否有效(不能以0开头,除非数字本身就是0) + if (len(first) > 1 and first[0] == '0') or \ + (len(second) > 1 and second[0] == '0'): + continue + + # 转换为整数 + a = int(first) + b = int(second) + + # 递归验证剩余部分 + if self._isValid(num, j, a, b): + return True + + return False + + def _isValid(self, num: str, start: int, a: int, b: int) -> bool: + # 递归验证从 start 位置开始的字符串是否满足累加数条件 + + # 如果已经到达字符串末尾,说明验证成功 + if start == len(num): + return True + + # 计算下一个数字的期望值 + expected_sum = a + b + expected_str = str(expected_sum) + + # 检查剩余字符串是否以期望的数字开头 + if num[start:].startswith(expected_str): + # 递归验证下一个数字 + return self._isValid(num, start + len(expected_str), b, expected_sum) + + return False +``` + +### 思路 1:复杂度分析 + +- **时间复杂度**:$O(n^3)$,其中 $n$ 是字符串长度。外层双重循环枚举前两个数字的位置需要 $O(n^2)$ 时间,内层递归验证需要 $O(n)$ 时间,因此总时间复杂度为 $O(n^3)$。 +- **空间复杂度**:$O(n)$,递归调用栈的深度最多为 $O(n)$。 diff --git a/docs/solutions/0300-0399/binary-tree-vertical-order-traversal.md b/docs/solutions/0300-0399/binary-tree-vertical-order-traversal.md new file mode 100644 index 00000000..1982c738 --- /dev/null +++ b/docs/solutions/0300-0399/binary-tree-vertical-order-traversal.md @@ -0,0 +1,133 @@ +# [0314. 二叉树的垂直遍历](https://leetcode.cn/problems/binary-tree-vertical-order-traversal/) + +- 标签:树、深度优先搜索、广度优先搜索、哈希表、二叉树、排序 +- 难度:中等 + +## 题目链接 + +- [0314. 二叉树的垂直遍历 - 力扣](https://leetcode.cn/problems/binary-tree-vertical-order-traversal/) + +## 题目大意 + +**描述**: + +给定一个二叉树的根结点 $root$。 + +**要求**: + +返回其结点按垂直方向(从上到下,逐列)遍历的结果。 + +**说明**: + +- 如果两个结点在同一行和列,那么顺序则为「从左到右」。 +- 树中结点的数目在范围 $[0, 10^{3}]$ 内。 +- $-10^{3} \le Node.val \le 10^{3}$。 + +**示例**: + +- 示例 1: + +![](https://pic.leetcode.cn/1727276130-UOKFsu-image.png) + +```python +输入:root = [3,9,20,null,null,15,7] +输出:[[9],[3,15],[20],[7]] +``` + +- 示例 2: + +![](https://pic.leetcode.cn/1727276212-bzuKab-image.png) + +```python +输入:root = [3,9,8,4,0,1,7] +输出:[[4],[9],[3,0,1],[8],[7]] +``` + +## 解题思路 + +### 思路 1:BFS + 哈希表 + +二叉树的垂直遍历问题可以通过 BFS(广度优先搜索)结合哈希表来解决。我们需要为每个节点分配一个列坐标,然后按照列坐标对节点进行分组。 + +**问题分析**: + +对于二叉树中的每个节点,我们需要: + +- 为根节点分配列坐标 $0$。 +- 左子节点的列坐标为父节点列坐标 $-1$。 +- 右子节点的列坐标为父节点列坐标 $+1$。 +- 使用 BFS 保证同一列中节点的从上到下顺序。 +- 使用哈希表按列坐标分组存储节点值。 + +**算法步骤**: + +1. **初始化**:如果根节点为空,返回空列表。创建队列存储 `(节点, 列坐标)` 元组,创建哈希表存储每列的节点值列表。 +2. **BFS 遍历**:从根节点开始,将根节点和列坐标 $0$ 加入队列。 +3. **处理节点**:对于队列中的每个节点,将其值加入对应列的列表中,然后处理其左右子节点。 +4. **更新坐标**:左子节点列坐标 $-1$,右子节点列坐标 $+1$。 +5. **排序输出**:按照列坐标从小到大排序,返回每列的节点值列表。 + +**关键变量**: + +- $col$:节点的列坐标,根节点为 $0$。 +- $queue$:BFS 队列,存储 `(节点, 列坐标)` 元组。 +- $column\_table$:哈希表,键为列坐标,值为该列的节点值列表。 +- $min\_col$ 和 $max\_col$:记录最小和最大列坐标,用于最终排序。 + +### 思路 1:代码 + +```python +# Definition for a binary tree node. +# class TreeNode: +# def __init__(self, val=0, left=None, right=None): +# self.val = val +# self.left = left +# self.right = right +class Solution: + def verticalOrder(self, root: Optional[TreeNode]) -> List[List[int]]: + # 如果根节点为空,返回空列表 + if not root: + return [] + + # 使用队列进行 BFS,存储 (节点, 列坐标) 元组 + queue = [(root, 0)] + # 哈希表存储每列的节点值列表 + column_table = {} + # 记录最小和最大列坐标 + min_col = 0 + max_col = 0 + + # BFS 遍历 + while queue: + node, col = queue.pop(0) + + # 将节点值加入对应列的列表 + if col not in column_table: + column_table[col] = [] + column_table[col].append(node.val) + + # 更新列坐标范围 + min_col = min(min_col, col) + max_col = max(max_col, col) + + # 处理左子节点,列坐标 -1 + if node.left: + queue.append((node.left, col - 1)) + + # 处理右子节点,列坐标 +1 + if node.right: + queue.append((node.right, col + 1)) + + # 按照列坐标从小到大排序,构建结果列表 + result = [] + for col in range(min_col, max_col + 1): + if col in column_table: + result.append(column_table[col]) + + return result +``` + +### 思路 1:复杂度分析 + +- **时间复杂度**:$O(n \log n)$,其中 $n$ 是树中节点的数量。BFS 遍历需要 $O(n)$ 时间,最后按列坐标排序需要 $O(k \log k)$ 时间,其中 $k$ 是列的数量,最坏情况下 $k = n$,因此总时间复杂度为 $O(n \log n)$。 +- **空间复杂度**:$O(n)$,队列和哈希表最多存储 $n$ 个节点,递归调用栈的深度最多为 $O(n)$。 diff --git a/docs/solutions/0300-0399/bomb-enemy.md b/docs/solutions/0300-0399/bomb-enemy.md new file mode 100644 index 00000000..33885e18 --- /dev/null +++ b/docs/solutions/0300-0399/bomb-enemy.md @@ -0,0 +1,154 @@ +# [0361. 轰炸敌人](https://leetcode.cn/problems/bomb-enemy/) + +- 标签:数组、动态规划、矩阵 +- 难度:中等 + +## 题目链接 + +- [0361. 轰炸敌人 - 力扣](https://leetcode.cn/problems/bomb-enemy/) + +## 题目大意 + +**描述**: + +给定一个大小为 $m \times n$ 的矩阵 $grid$,其中每个单元格都放置有一个字符: + +- `'W'` 表示一堵墙。 +- `'E'` 表示一个敌人。 +- `'0'`(数字 $0$)表示一个空位。 + +**要求**: + +返回你使用「一颗炸弹」可以击杀的最大敌人数目。 + +**说明**: + +- 你只能把炸弹放在一个空位里。 +- 由于炸弹的威力不足以穿透墙体,炸弹只能击杀同一行和同一列没被墙体挡住的敌人。 +- $m == grid.length$。 +- $n == grid[i].length$。 +- $1 \le m, n \le 500$。 +- $grid[i][j]$ 可以是 `'W'`、`'E'` 或 `'0'`。 + +**示例**: + +- 示例 1: + +![](https://assets.leetcode.com/uploads/2021/03/27/bomb1-grid.jpg) + +```python +输入:grid = [["0","E","0","0"],["E","0","W","E"],["0","E","0","0"]] +输出:3 +``` + +- 示例 2: + +![](https://assets.leetcode.com/uploads/2021/03/27/bomb2-grid.jpg) + +```python +输入:grid = [["W","W","W"],["0","0","0"],["E","E","E"]] +输出:1 +``` + +## 解题思路 + +### 思路 1:动态规划 + 预处理 + +轰炸敌人问题可以通过动态规划预处理来解决。我们需要预先计算每个位置在四个方向上能击杀的敌人数,然后找到最大值。 + +**问题分析**: + +对于矩阵 $grid$ 中的每个空位 $(i, j)$,炸弹可以击杀: + +- 同一行中左右方向的敌人(被墙阻挡前)。 +- 同一列中上下方向的敌人(被墙阻挡前)。 + +**算法步骤**: + +1. **预处理行方向**:对每一行,从左到右和从右到左分别计算每个位置能击杀的敌人数。 +2. **预处理列方向**:对每一列,从上到下和从下到上分别计算每个位置能击杀的敌人数。 +3. **计算最大值**:遍历所有空位,计算四个方向击杀敌人数之和的最大值。 + +**关键变量**: + +- $m$:矩阵的行数。 +- $n$:矩阵的列数。 +- $row\_kills[i][j]$:位置 $(i, j)$ 在行方向上能击杀的敌人数。 +- $col\_kills[i][j]$:位置 $(i, j)$ 在列方向上能击杀的敌人数。 +- $max\_kills$:能击杀的最大敌人数。 + +### 思路 1:代码 + +```python +class Solution: + def maxKilledEnemies(self, grid: List[List[str]]) -> int: + if not grid or not grid[0]: + return 0 + + m, n = len(grid), len(grid[0]) + max_kills = 0 + + # 预处理:计算每个位置在行方向上能击杀的敌人数 + row_kills = [[0] * n for _ in range(m)] + + # 从左到右计算行方向的击杀数 + for i in range(m): + count = 0 + for j in range(n): + if grid[i][j] == 'W': + count = 0 # 遇到墙,重置计数 + elif grid[i][j] == 'E': + count += 1 # 遇到敌人,增加计数 + else: # 空位 + row_kills[i][j] += count + + # 从右到左计算行方向的击杀数 + for i in range(m): + count = 0 + for j in range(n - 1, -1, -1): + if grid[i][j] == 'W': + count = 0 # 遇到墙,重置计数 + elif grid[i][j] == 'E': + count += 1 # 遇到敌人,增加计数 + else: # 空位 + row_kills[i][j] += count + + # 预处理:计算每个位置在列方向上能击杀的敌人数 + col_kills = [[0] * n for _ in range(m)] + + # 从上到下计算列方向的击杀数 + for j in range(n): + count = 0 + for i in range(m): + if grid[i][j] == 'W': + count = 0 # 遇到墙,重置计数 + elif grid[i][j] == 'E': + count += 1 # 遇到敌人,增加计数 + else: # 空位 + col_kills[i][j] += count + + # 从下到上计算列方向的击杀数 + for j in range(n): + count = 0 + for i in range(m - 1, -1, -1): + if grid[i][j] == 'W': + count = 0 # 遇到墙,重置计数 + elif grid[i][j] == 'E': + count += 1 # 遇到敌人,增加计数 + else: # 空位 + col_kills[i][j] += count + + # 计算每个空位能击杀的敌人数,并更新最大值 + for i in range(m): + for j in range(n): + if grid[i][j] == '0': # 空位 + total_kills = row_kills[i][j] + col_kills[i][j] + max_kills = max(max_kills, total_kills) + + return max_kills +``` + +### 思路 1:复杂度分析 + +- **时间复杂度**:$O(m \times n)$,其中 $m$ 是矩阵的行数,$n$ 是矩阵的列数。我们需要遍历矩阵四次(行方向两次,列方向两次),每次遍历的时间复杂度都是 $O(m \times n)$。 +- **空间复杂度**:$O(m \times n)$,需要两个 $m \times n$ 的二维数组来存储预处理结果。 diff --git a/docs/solutions/0300-0399/bulb-switcher.md b/docs/solutions/0300-0399/bulb-switcher.md new file mode 100644 index 00000000..6ac8b34b --- /dev/null +++ b/docs/solutions/0300-0399/bulb-switcher.md @@ -0,0 +1,77 @@ +# [0319. 灯泡开关](https://leetcode.cn/problems/bulb-switcher/) + +- 标签:脑筋急转弯、数学 +- 难度:中等 + +## 题目链接 + +- [0319. 灯泡开关 - 力扣](https://leetcode.cn/problems/bulb-switcher/) + +## 题目大意 + +**描述**: + +初始时有 $n$ 个灯泡处于关闭状态。第一轮,你将会打开所有灯泡。接下来的第二轮,你将会每两个灯泡关闭第二个。 + +第三轮,你每三个灯泡就切换第三个灯泡的开关(即,打开变关闭,关闭变打开)。第 $i$ 轮,你每 $i$ 个灯泡就切换第 $i$ 个灯泡的开关。直到第 $n$ 轮,你只需要切换最后一个灯泡的开关。 + +**要求**: + +找出并返回 $n$ 轮后有多少个亮着的灯泡。 + +**说明**: + +- $0 \le n \le 10^{9}$。 + +**示例**: + +- 示例 1: + +![](https://assets.leetcode.com/uploads/2020/11/05/bulb.jpg) + +```python +输入:n = 3 +输出:1 +解释: +初始时, 灯泡状态 [关闭, 关闭, 关闭]. +第一轮后, 灯泡状态 [开启, 开启, 开启]. +第二轮后, 灯泡状态 [开启, 关闭, 开启]. +第三轮后, 灯泡状态 [开启, 关闭, 关闭]. + +你应该返回 1,因为只有一个灯泡还亮着。 +``` + +- 示例 2: + +```python +输入:n = 0 +输出:0 +``` + +## 解题思路 + +### 思路 1:数学分析 + +通过分析灯泡的开关规律,我们可以发现一个重要的数学性质:第 $i$ 个灯泡最终的状态取决于它被切换了多少次。 + +具体分析: + +1. 第 $i$ 个灯泡在第 $j$ 轮会被切换,当且仅当 $j$ 是 $i$ 的因子。 +2. 因此,第 $i$ 个灯泡被切换的次数等于 $i$ 的因子个数。 +3. 如果一个数有奇数个因子,那么它最终会亮着;如果有偶数个因子,那么它最终会关闭。 +4. 只有完全平方数有奇数个因子,因为因子总是成对出现的,除了完全平方数的平方根。 + +因此,答案就是小于等于 $n$ 的完全平方数的个数,即 $\lfloor \sqrt{n} \rfloor$。 + +### 思路 1:代码 + +```python +class Solution: + def bulbSwitch(self, n: int) -> int: + return int(n ** 0.5) +``` + +### 思路 1:复杂度分析 + +- **时间复杂度**:$O(1)$,只需要计算一次平方根。 +- **空间复杂度**:$O(1)$,只使用了常数额外空间。 diff --git a/docs/solutions/0300-0399/count-of-range-sum.md b/docs/solutions/0300-0399/count-of-range-sum.md new file mode 100644 index 00000000..ee64a5be --- /dev/null +++ b/docs/solutions/0300-0399/count-of-range-sum.md @@ -0,0 +1,205 @@ +# [0327. 区间和的个数](https://leetcode.cn/problems/count-of-range-sum/) + +- 标签:树状数组、线段树、数组、二分查找、分治、有序集合、归并排序 +- 难度:困难 + +## 题目链接 + +- [0327. 区间和的个数 - 力扣](https://leetcode.cn/problems/count-of-range-sum/) + +## 题目大意 + +**描述**: + +给定一个整数数组 $nums$ 以及两个整数 $lower$ 和 $upper$。 + +**要求**: + +求数组中,值位于范围 $[lower, upper]$(包含 $lower$ 和 $upper$)之内的「区间和的个数」。 + +**说明**: + +- 区间和 $S(i, j)$:表示在 $nums$ 中,位置从 $i$ 到 $j$ 的元素之和,包含 $i$ 和 $j$ ($i \le j$)。 +- $1 \le nums.length \le 10^{5}$。 +- $-2^{31} \le nums[i] \le 2^{31} - 1$。 +- $-10^{5} \le lower \le upper \le 10^{5}$。 +- 题目数据保证答案是一个 $32$ 位的整数。 + +**示例**: + +- 示例 1: + +```python +输入:nums = [-2,5,-1], lower = -2, upper = 2 +输出:3 +解释:存在三个区间:[0,0]、[2,2] 和 [0,2] ,对应的区间和分别是:-2 、-1 、2 。 +``` + +- 示例 2: + +```python +输入:nums = [0], lower = 0, upper = 0 +输出:1 +``` + +## 解题思路 + +### 思路 1:前缀和 + 归并排序 + +这道题要求统计区间和 $S(i, j) = \sum_{k=i}^{j} nums[k]$ 在 $[lower, upper]$ 范围内的个数。 + +我们可以使用前缀和的思想:设 $prefix[i] = \sum_{k=0}^{i} nums[k]$,则区间和 $S(i, j) = prefix[j] - prefix[i-1]$。 + +对于每个位置 $j$,我们需要找到满足条件的 $i$,使得:$lower \leq prefix[j] - prefix[i-1] \leq upper$ + +即:$prefix[j] - upper \leq prefix[i-1] \leq prefix[j] - lower$ + +这可以转化为:对于每个 $prefix[j]$,统计在 $prefix[0]$ 到 $prefix[j-1]$ 中有多少个值在区间 $[prefix[j] - upper, prefix[j] - lower]$ 内。 + +使用归并排序的思想: + +1. 将数组分为左右两部分 +2. 递归处理左右两部分,得到左右两部分的答案 +3. 处理跨越中点的区间:对于右半部分的每个 $prefix[j]$,在左半部分中统计满足条件的 $prefix[i]$ +4. 合并左右两部分并排序 + +### 思路 1:代码 + +```python +class Solution: + def countRangeSum(self, nums: List[int], lower: int, upper: int) -> int: + # 计算前缀和数组 + prefix = [0] + for num in nums: + prefix.append(prefix[-1] + num) + + def merge_sort_count(left, right): + if left >= right: + return 0 + + mid = (left + right) // 2 + # 递归处理左右两部分 + count = merge_sort_count(left, mid) + merge_sort_count(mid + 1, right) + + # 处理跨越中点的区间 + i = j = mid + 1 + for k in range(left, mid + 1): + # 找到左边界:prefix[j] >= prefix[k] + lower + while i <= right and prefix[i] < prefix[k] + lower: + i += 1 + # 找到右边界:prefix[j] <= prefix[k] + upper + while j <= right and prefix[j] <= prefix[k] + upper: + j += 1 + # 统计满足条件的区间数量 + count += j - i + + # 合并排序 + temp = [] + i, j = left, mid + 1 + while i <= mid and j <= right: + if prefix[i] <= prefix[j]: + temp.append(prefix[i]) + i += 1 + else: + temp.append(prefix[j]) + j += 1 + + # 添加剩余元素 + while i <= mid: + temp.append(prefix[i]) + i += 1 + while j <= right: + temp.append(prefix[j]) + j += 1 + + # 更新原数组 + for i in range(len(temp)): + prefix[left + i] = temp[i] + + return count + + return merge_sort_count(0, len(prefix) - 1) +``` + +### 思路 1:复杂度分析 + +- **时间复杂度**:$O(n \log n)$,其中 $n$ 是数组长度。归并排序的时间复杂度为 $O(n \log n)$,每次合并时处理跨越中点的区间需要 $O(n)$ 时间。 +- **空间复杂度**:$O(n)$,需要额外的空间存储前缀和数组和归并排序的临时数组。 + +### 思路 2:树状数组 + +同样使用前缀和的思想,但这次我们使用树状数组来优化查询。 + +对于每个前缀和 $prefix[j]$,我们需要统计在 $prefix[0]$ 到 $prefix[j-1]$ 中有多少个值在区间 $[prefix[j] - upper, prefix[j] - lower]$ 内。 + +使用树状数组的步骤: + +1. 计算所有可能的前缀和值,包括 $prefix[j] - upper$ 和 $prefix[j] - lower$ +2. 对这些值进行离散化处理 +3. 从左到右遍历前缀和数组,对于每个 $prefix[j]$: + - 查询区间 $[prefix[j] - upper, prefix[j] - lower]$ 内的元素个数 + - 将 $prefix[j]$ 插入到树状数组中 + +### 思路 2:代码 + +```python +class Solution: + def countRangeSum(self, nums: List[int], lower: int, upper: int) -> int: + # 计算前缀和数组 + prefix = [0] + for num in nums: + prefix.append(prefix[-1] + num) + + # 收集所有需要离散化的值 + values = set() + for p in prefix: + values.add(p) + values.add(p - lower) + values.add(p - upper) + + # 离散化 + sorted_values = sorted(values) + value_to_idx = {v: i + 1 for i, v in enumerate(sorted_values)} + + # 树状数组类 + class BIT: + def __init__(self, n): + self.n = n + self.tree = [0] * (n + 1) + + def update(self, idx, delta): + while idx <= self.n: + self.tree[idx] += delta + idx += idx & (-idx) + + def query(self, idx): + res = 0 + while idx > 0: + res += self.tree[idx] + idx -= idx & (-idx) + return res + + def range_query(self, left, right): + return self.query(right) - self.query(left - 1) + + # 初始化树状数组 + bit = BIT(len(sorted_values)) + count = 0 + + # 从左到右处理前缀和 + for p in prefix: + # 查询区间 [p - upper, p - lower] 内的元素个数 + left_idx = value_to_idx[p - upper] + right_idx = value_to_idx[p - lower] + count += bit.range_query(left_idx, right_idx) + + # 将当前前缀和插入树状数组 + bit.update(value_to_idx[p], 1) + + return count +``` + +### 思路 2:复杂度分析 + +- **时间复杂度**:$O(n \log n)$,其中 $n$ 是数组长度。离散化需要 $O(n \log n)$ 时间,树状数组的每次更新和查询操作都是 $O(\log n)$,总共需要 $O(n \log n)$ 时间。 +- **空间复杂度**:$O(n)$,需要存储离散化后的值和树状数组。 diff --git a/docs/solutions/0300-0399/create-maximum-number.md b/docs/solutions/0300-0399/create-maximum-number.md new file mode 100644 index 00000000..735e3a63 --- /dev/null +++ b/docs/solutions/0300-0399/create-maximum-number.md @@ -0,0 +1,129 @@ +# [0321. 拼接最大数](https://leetcode.cn/problems/create-maximum-number/) + +- 标签:栈、贪心、数组、双指针、单调栈 +- 难度:困难 + +## 题目链接 + +- [0321. 拼接最大数 - 力扣](https://leetcode.cn/problems/create-maximum-number/) + +## 题目大意 + +**描述**: + +给定两个整数数组 $nums1$ 和 $nums2$,它们的长度分别为 $m$ 和 $n$。数组 $nums1$ 和 $nums2$ 分别代表两个数各位上的数字。同时你也会得到一个整数 $k$。 + +**要求**: + +请你利用这两个数组中的数字创建一个长度为 $k \le m + n$ 的最大数。同一数组中数字的相对顺序必须保持不变。 + +返回代表答案的长度为 $k$ 的数组。 + +**说明**: + +- $m == nums1.length$。 +- $n == nums2.length$。 +- $1 \le m, n \le 500$。 +- $0 \le nums1[i], nums2[i] \le 9$。 +- $1 \le k \le m + n$。 +- $nums1$ 和 $nums2$ 没有前导 $0$。 + +**示例**: + +- 示例 1: + +```python +输入:nums1 = [3,4,6,5], nums2 = [9,1,2,5,8,3], k = 5 +输出:[9,8,6,5,3] +``` + +- 示例 2: + +```python +输入:nums1 = [6,7], nums2 = [6,0,4], k = 5 +输出:[6,7,6,0,4] +``` + +## 解题思路 + +### 思路 1:贪心 + 单调栈 + +这道题的核心思想是:对于长度为 $k$ 的最大数,我们需要从两个数组中选择数字,使得最终结果最大。 + +解题步骤: + +1. **枚举所有可能的分割方式**:假设从 $nums1$ 中选择 $i$ 个数字,从 $nums2$ 中选择 $k-i$ 个数字,其中 $0 \le i \le \min(len(nums1), k)$ 且 $0 \le k-i \le len(nums2)$。 +2. **从单个数组中选择最大子序列**:使用单调栈的思想,从 $nums1$ 中选择 $i$ 个数字组成最大子序列,从 $nums2$ 中选择 $k-i$ 个数字组成最大子序列。 +3. **合并两个子序列**:将两个子序列合并成一个长度为 $k$ 的最大序列。 +4. **比较所有可能的结果**:返回所有可能结果中的最大值。 + +关键算法: + +- 使用单调栈从数组中选择 $k$ 个数字组成最大子序列。 +- 使用双指针合并两个子序列,每次选择较大的数字。 + +### 思路 1:代码 + +```python +class Solution: + def maxNumber(self, nums1: List[int], nums2: List[int], k: int) -> List[int]: + def getMaxSubsequence(nums, k): + """从数组中选择k个数字组成最大子序列""" + stack = [] + # 需要删除的数字个数 + drop = len(nums) - k + + for num in nums: + # 如果栈不为空,且当前数字大于栈顶数字,且还有数字可以删除 + while stack and num > stack[-1] and drop > 0: + stack.pop() + drop -= 1 + stack.append(num) + + # 如果还有数字需要删除,从末尾删除 + return stack[:k] + + def merge(nums1, nums2): + """合并两个数组,保持相对顺序,使结果最大""" + result = [] + i = j = 0 + + while i < len(nums1) and j < len(nums2): + # 比较从当前位置开始的子序列,选择字典序更大的 + if nums1[i:] > nums2[j:]: + result.append(nums1[i]) + i += 1 + else: + result.append(nums2[j]) + j += 1 + + # 添加剩余元素 + result.extend(nums1[i:]) + result.extend(nums2[j:]) + + return result + + max_result = [] + + # 枚举所有可能的分割方式 + for i in range(max(0, k - len(nums2)), min(len(nums1), k) + 1): + j = k - i + + # 从两个数组中选择最大子序列 + sub1 = getMaxSubsequence(nums1, i) + sub2 = getMaxSubsequence(nums2, j) + + # 合并两个子序列 + merged = merge(sub1, sub2) + + # 更新最大结果 + if merged > max_result: + max_result = merged + + return max_result +``` + +### 思路 1:复杂度分析 + +- **时间复杂度**:$O(k \times (m + n) \times k)$,其中 $m$ 和 $n$ 分别是两个数组的长度。需要枚举 $O(k)$ 种分割方式,每种方式需要 $O(m + n)$ 时间选择子序列,$O(k)$ 时间合并。 +- **空间复杂度**:$O(k)$,用于存储临时结果和栈。 diff --git a/docs/solutions/0300-0399/data-stream-as-disjoint-intervals.md b/docs/solutions/0300-0399/data-stream-as-disjoint-intervals.md new file mode 100644 index 00000000..b59c4387 --- /dev/null +++ b/docs/solutions/0300-0399/data-stream-as-disjoint-intervals.md @@ -0,0 +1,135 @@ +# [0352. 将数据流变为多个不相交区间](https://leetcode.cn/problems/data-stream-as-disjoint-intervals/) + +- 标签:设计、二分查找、有序集合 +- 难度:困难 + +## 题目链接 + +- [0352. 将数据流变为多个不相交区间 - 力扣](https://leetcode.cn/problems/data-stream-as-disjoint-intervals/) + +## 题目大意 + +**描述**: + +给定一个由非负整数 $a1, a2, ..., an$ 组成的数据流输入。 + +**要求**: + +请你将到目前为止看到的数字总结为不相交的区间列表。 + +实现 `SummaryRanges` 类: + +- `SummaryRanges()` 使用一个空数据流初始化对象。 +- `void addNum(int val)` 向数据流中加入整数 $val$ 。 +- `int[][] getIntervals()` 以不相交区间 $[start_i, end_i]$ 的列表形式返回对数据流中整数的总结。 + +**说明**: + +- $0 \le val \le 10^{4}$。 +- 最多调用 `addNum` 和 `getIntervals` 方法 $3 \times 10^{4}$ 次。 + +- 进阶:如果存在大量合并,并且与数据流的大小相比,不相交区间的数量很小,该怎么办? + +**示例**: + +- 示例 1: + +```python +输入: +["SummaryRanges", "addNum", "getIntervals", "addNum", "getIntervals", "addNum", "getIntervals", "addNum", "getIntervals", "addNum", "getIntervals"] +[[], [1], [], [3], [], [7], [], [2], [], [6], []] +输出: +[null, null, [[1, 1]], null, [[1, 1], [3, 3]], null, [[1, 1], [3, 3], [7, 7]], null, [[1, 3], [7, 7]], null, [[1, 3], [6, 7]]] + +解释: +SummaryRanges summaryRanges = new SummaryRanges(); +summaryRanges.addNum(1); // arr = [1] +summaryRanges.getIntervals(); // 返回 [[1, 1]] +summaryRanges.addNum(3); // arr = [1, 3] +summaryRanges.getIntervals(); // 返回 [[1, 1], [3, 3]] +summaryRanges.addNum(7); // arr = [1, 3, 7] +summaryRanges.getIntervals(); // 返回 [[1, 1], [3, 3], [7, 7]] +summaryRanges.addNum(2); // arr = [1, 2, 3, 7] +summaryRanges.getIntervals(); // 返回 [[1, 3], [7, 7]] +summaryRanges.addNum(6); // arr = [1, 2, 3, 6, 7] +summaryRanges.getIntervals(); // 返回 [[1, 3], [6, 7]] +``` + +## 解题思路 + +### 思路 1:集合 + 动态生成区间 + +这道题的核心是维护一个数字集合,然后在需要时动态生成不相交的区间列表。 + +**算法思路**: + +1. **数据结构选择**:使用集合(set)存储所有出现过的数字,利用集合的去重特性自动处理重复数字。 +2. **添加数字**:当添加数字 $val$ 时,直接将其加入集合中,时间复杂度为 $O(1)$。 +3. **获取区间**:当需要获取区间列表时: + - 将集合中的所有数字排序。 + - 遍历排序后的数字,连续的数字合并为一个区间。 + - 遇到不连续的数字时,开始新的区间。 + +**具体步骤**: + +1. 使用集合存储所有添加的数字。 +2. `addNum(val)`:将 $val$ 添加到集合中。 +3. `getIntervals()`: + - 对集合中的数字排序得到 $sorted\_nums$。 + - 初始化 $start = end = sorted\_nums[0]$。 + - 遍历剩余数字,如果 $sorted\_nums[i] = end + 1$,则扩展当前区间 $end = sorted\_nums[i]$。 + - 否则,保存当前区间 $[start, end]$,开始新区间 $start = end = sorted\_nums[i]$。 + - 最后添加最后一个区间。 + +### 思路 1:代码 + +```python +class SummaryRanges: + def __init__(self): + # 使用集合存储所有出现过的数字 + self.nums = set() + + def addNum(self, value: int) -> None: + # 将数字添加到集合中,集合自动去重 + self.nums.add(value) + + def getIntervals(self) -> List[List[int]]: + # 如果集合为空,返回空列表 + if not self.nums: + return [] + + # 将集合中的数字排序 + sorted_nums = sorted(self.nums) + intervals = [] + + # 初始化第一个区间 + start = end = sorted_nums[0] + + # 遍历剩余数字,构建区间 + for i in range(1, len(sorted_nums)): + if sorted_nums[i] == end + 1: + # 当前数字与前一个数字连续,扩展当前区间 + end = sorted_nums[i] + else: + # 当前数字与前一个数字不连续,保存当前区间并开始新区间 + intervals.append([start, end]) + start = end = sorted_nums[i] + + # 添加最后一个区间 + intervals.append([start, end]) + + return intervals + + +# Your SummaryRanges object will be instantiated and called as such: +# obj = SummaryRanges() +# obj.addNum(value) +# param_2 = obj.getIntervals() +``` + +### 思路 1:复杂度分析 + +- **时间复杂度**: + - `addNum(val)`:$O(1)$,集合的插入操作时间复杂度为常数。 + - `getIntervals()`:$O(n \log n)$,其中 $n$ 为集合中数字的个数,主要时间消耗在排序上。 +- **空间复杂度**:$O(n)$,其中 $n$ 为添加的不同数字的个数。 diff --git a/docs/solutions/0300-0399/design-hit-counter.md b/docs/solutions/0300-0399/design-hit-counter.md new file mode 100644 index 00000000..15a52df3 --- /dev/null +++ b/docs/solutions/0300-0399/design-hit-counter.md @@ -0,0 +1,109 @@ +# [0362. 敲击计数器](https://leetcode.cn/problems/design-hit-counter/) + +- 标签:设计、队列、数组、二分查找、数据流 +- 难度:中等 + +## 题目链接 + +- [0362. 敲击计数器 - 力扣](https://leetcode.cn/problems/design-hit-counter/) + +## 题目大意 + +**要求**: + +设计一个敲击计数器,使它可以统计在过去 $5$ 分钟内被敲击次数(即过去 $300$ 秒)。 + +您的系统应该接受一个时间戳参数 $timestamp$ (单位为秒),并且您可以假定对系统的调用是按时间顺序进行的(即 $timestamp$ 是单调递增的)。几次撞击可能同时发生。 + +实现 `HitCounter` 类: + +- `HitCounter()` 初始化命中计数器系统。 +- `void hit(int timestamp)` 记录在 $timestamp$ (单位为秒)发生的一次命中。在同一个 $timestamp$ 中可能会出现几个点击。 +- `int getHits(int timestamp)` 返回 $timestamp$ 在过去 $5$ 分钟内(即过去 $300$ 秒)的命中次数。 + +**说明**: + +- $1 \le timestamp \le 2 \times 10^{9}$。 +- 所有对系统的调用都是按时间顺序进行的(即 timestamp 是单调递增的)。 +- `hit` 和 `getHits` 最多被调用 $300$ 次。 + +- 进阶: 如果每秒的敲击次数是一个很大的数字,你的计数器可以应对吗? + +**示例**: + +- 示例 1: + +```python +输入: +["HitCounter", "hit", "hit", "hit", "getHits", "hit", "getHits", "getHits"] +[[], [1], [2], [3], [4], [300], [300], [301]] +输出: +[null, null, null, null, 3, null, 4, 3] + +解释: +HitCounter counter = new HitCounter(); +counter.hit(1);// 在时刻 1 敲击一次。 +counter.hit(2);// 在时刻 2 敲击一次。 +counter.hit(3);// 在时刻 3 敲击一次。 +counter.getHits(4);// 在时刻 4 统计过去 5 分钟内的敲击次数, 函数返回 3 。 +counter.hit(300);// 在时刻 300 敲击一次。 +counter.getHits(300); // 在时刻 300 统计过去 5 分钟内的敲击次数,函数返回 4 。 +counter.getHits(301); // 在时刻 301 统计过去 5 分钟内的敲击次数,函数返回 3 。 +``` + +## 解题思路 + +### 思路 1:队列 + +使用队列来存储所有的敲击时间戳。对于每次 `hit` 操作,我们将时间戳加入队列。对于 `getHits` 操作,我们需要移除所有超过 $300$ 秒的时间戳,然后返回队列中剩余元素的个数。 + +具体步骤: + +1. **初始化**:创建一个空队列 $queue$ 来存储时间戳。 +2. **hit 操作**:将当前时间戳 $timestamp$ 加入队列。 +3. **getHits 操作**: + - 计算时间窗口的起始时间:$start\_time = timestamp - 300$。 + - 移除队列中所有小于等于 $start\_time$ 的时间戳。 + - 返回队列中剩余元素的个数。 + +这种方法简单直观,但每次 `getHits` 操作都需要遍历队列来移除过期的时间戳。 + +### 思路 1:代码 + +```python +from collections import deque + +class HitCounter: + + def __init__(self): + # 使用双端队列存储时间戳 + self.queue = deque() + + def hit(self, timestamp: int) -> None: + # 将当前时间戳加入队列 + self.queue.append(timestamp) + + def getHits(self, timestamp: int) -> int: + # 计算时间窗口的起始时间(过去 300 秒) + start_time = timestamp - 300 + + # 移除所有过期的时间戳 + while self.queue and self.queue[0] <= start_time: + self.queue.popleft() + + # 返回队列中剩余元素的个数 + return len(self.queue) + + +# Your HitCounter object will be instantiated and called as such: +# obj = HitCounter() +# obj.hit(timestamp) +# param_2 = obj.getHits(timestamp) +``` + +### 思路 1:复杂度分析 + +- **时间复杂度**: + - `hit` 操作:$O(1)$,只需要将元素加入队列。 + - `getHits` 操作:$O(k)$,其中 $k$ 是需要移除的过期时间戳数量。 +- **空间复杂度**:$O(n)$,其中 $n$ 是在过去 $300$ 秒内的敲击次数。 diff --git a/docs/solutions/0300-0399/design-phone-directory.md b/docs/solutions/0300-0399/design-phone-directory.md new file mode 100644 index 00000000..d859068a --- /dev/null +++ b/docs/solutions/0300-0399/design-phone-directory.md @@ -0,0 +1,123 @@ +# [0379. 电话目录管理系统](https://leetcode.cn/problems/design-phone-directory/) + +- 标签:设计、队列、数组、哈希表、链表 +- 难度:中等 + +## 题目链接 + +- [0379. 电话目录管理系统 - 力扣](https://leetcode.cn/problems/design-phone-directory/) + +## 题目大意 + +**要求**: + +设计一个电话目录管理系统,一开始有 $maxNumbers$ 个位置能够储存号码。系统应该存储号码,检查某个位置是否为空,并清空给定的位置。 + +实现 `PhoneDirectory` 类: + +- `PhoneDirectory(int maxNumbers)` 电话目录初始有 $maxNumbers$ 个可用位置。 +- `int get()` 提供一个未分配给任何人的号码。如果没有可用号码则返回 $-1$。 +- `bool check(int number)` 如果位置 $number$ 可用返回 $true$ 否则返回 $false$。 +- `void release(int number)` 回收或释放位置 $number$。 + +**说明**: + +- $1 \le maxNumbers \le 10^{4}$。 +- $0 \le number \lt maxNumbers$。 +- `get`,`check` 和 `release` 最多被调用 $2 \times 10^{4}$ 次。 + +**示例**: + +- 示例 1: + +```python +输入: +["PhoneDirectory", "get", "get", "check", "get", "check", "release", "check"] +[[3], [], [], [2], [], [2], [2], [2]] +输出: +[null, 0, 1, true, 2, false, null, true] + +解释: +PhoneDirectory phoneDirectory = new PhoneDirectory(3); +phoneDirectory.get(); // 它可以返回任意可用的数字。这里我们假设它返回 0。 +phoneDirectory.get(); // 假设它返回 1。 +phoneDirectory.check(2); // 数字 2 可用,所以返回 true。 +phoneDirectory.get(); // 返回剩下的唯一一个数字 2。 +phoneDirectory.check(2); // 数字 2 不再可用,所以返回 false。 +phoneDirectory.release(2); // 将数字 2 释放回号码池。 +phoneDirectory.check(2); // 数字 2 重新可用,返回 true。 +``` + +## 解题思路 + +### 思路 1:集合 + 队列 + +使用集合来快速检查号码是否可用,使用队列来高效分配号码。这种方法结合了集合的 $O(1)$ 查找性能和队列的先进先出特性。 + +具体步骤: + +1. **初始化**:创建集合 $available$ 存储所有可用号码,创建队列 $queue$ 用于快速分配号码。 +2. **get 操作**:从队列头部取出一个号码,并从集合中移除该号码。 +3. **check 操作**:检查号码是否在可用集合中。 +4. **release 操作**:将号码重新加入集合和队列。 + +这种方法的关键是维护两个数据结构的一致性,确保号码的状态在两个数据结构中保持同步。 + +### 思路 1:代码 + +```python +from collections import deque + +class PhoneDirectory: + + def __init__(self, maxNumbers: int): + # 使用集合存储所有可用的号码,支持 O(1) 查找 + self.available = set() + # 使用队列存储可用号码,支持 O(1) 分配 + self.queue = deque() + + # 初始化所有号码为可用状态 + for i in range(maxNumbers): + self.available.add(i) + self.queue.append(i) + + def get(self) -> int: + # 如果没有可用号码,返回 -1 + if not self.available: + return -1 + + # 从队列头部取出一个号码 + number = self.queue.popleft() + # 从可用集合中移除该号码 + self.available.remove(number) + + return number + + def check(self, number: int) -> bool: + # 检查号码是否在可用集合中 + return number in self.available + + def release(self, number: int) -> None: + # 如果号码已经可用,不需要重复释放 + if number in self.available: + return + + # 将号码重新加入可用集合和队列 + self.available.add(number) + self.queue.append(number) + + +# Your PhoneDirectory object will be instantiated and called as such: +# obj = PhoneDirectory(maxNumbers) +# param_1 = obj.get() +# param_2 = obj.check(number) +# obj.release(number) +``` + +### 思路 1:复杂度分析 + +- **时间复杂度**: + - `get` 操作:$O(1)$,从队列头部取出元素,从集合中移除元素。 + - `check` 操作:$O(1)$,集合查找操作。 + - `release` 操作:$O(1)$,向集合和队列添加元素。 +- **空间复杂度**:$O(n)$,其中 $n$ 是 $maxNumbers$,需要存储所有号码的状态。 diff --git a/docs/solutions/0300-0399/design-snake-game.md b/docs/solutions/0300-0399/design-snake-game.md new file mode 100644 index 00000000..3e24e977 --- /dev/null +++ b/docs/solutions/0300-0399/design-snake-game.md @@ -0,0 +1,167 @@ +# [0353. 贪吃蛇](https://leetcode.cn/problems/design-snake-game/) + +- 标签:设计、队列、数组、哈希表、模拟 +- 难度:中等 + +## 题目链接 + +- [0353. 贪吃蛇 - 力扣](https://leetcode.cn/problems/design-snake-game/) + +## 题目大意 + +**要求**: + +设计一个「贪吃蛇游戏」,该游戏将会在一个「屏幕尺寸 = 宽度 x 高度」的屏幕上运行。如果你不熟悉这个游戏,可以 [点击这里](http://patorjk.com/games/snake/) 在线试玩。 + +起初时,蛇在左上角的 $(0, 0)$ 位置,身体长度为 $1$ 个单位。 + +你将会被给出一个数组形式的食物位置序列 $food$ ,其中 $food[i] = (ri, ci)$。当蛇吃到食物时,身子的长度会增加 $1$ 个单位,得分也会 $+1$。 + +食物不会同时出现,会按列表的顺序逐一显示在屏幕上。比方讲,第一个食物被蛇吃掉后,第二个食物才会出现。 + +当一个食物在屏幕上出现时,保证不会出现在被蛇身体占据的格子里。 + +如果蛇越界(与边界相撞)或者头与「移动后」的身体相撞(即,身长为 $4$ 的蛇无法与自己相撞),游戏结束。 + +实现 `SnakeGame` 类: + +- `SnakeGame(int width, int height, int[][] food)` 初始化对象,屏幕大小为 $height \times width$,食物位置序列为 $food$。 +- `int move(String direction)` 返回蛇在方向 $direction$ 上移动后的得分。如果游戏结束,返回 $-1$。 + +**说明**: + +- $1 \le width, height \le 10^{4}$。 +- $1 \le food.length \le 50$。 +- $food[i].length == 2$。 +- $0 \le ri \lt height$。 +- $0 \le ci \lt width$。 +- $direction.length == 1$。 +- $direction$ 为 `'U'`, `'D'`, `'L'`, or `'R'`。 +- 最多调用 $10^{4}$ 次 `move` 方法。 + +**示例**: + +- 示例 1: + +![](https://assets.leetcode.com/uploads/2021/01/13/snake.jpg) + +```python +输入: +["SnakeGame", "move", "move", "move", "move", "move", "move"] +[[3, 2, [[1, 2], [0, 1]]], ["R"], ["D"], ["R"], ["U"], ["L"], ["U"]] +输出: +[null, 0, 0, 1, 1, 2, -1] + +解释: +SnakeGame snakeGame = new SnakeGame(3, 2, [[1, 2], [0, 1]]); +snakeGame.move("R"); // 返回 0 +snakeGame.move("D"); // 返回 0 +snakeGame.move("R"); // 返回 1 ,蛇吃掉了第一个食物,同时第二个食物出现在 (0, 1) +snakeGame.move("U"); // 返回 1 +snakeGame.move("L"); // 返回 2 ,蛇吃掉了第二个食物,没有出现更多食物 +snakeGame.move("U"); // 返回 -1 ,蛇与边界相撞,游戏结束 +``` + +## 解题思路 + +### 思路 1:队列 + 哈希表 + +使用双端队列 `deque` 来存储蛇的身体位置,使用集合 `set` 来快速检查碰撞。蛇的头部在队列的末尾,尾部在队列的开头。 + +具体步骤: + +1. **初始化**: + - 使用双端队列 $snake$ 存储蛇的身体位置,初始位置为 $(0, 0)$。 + - 使用集合 $occupied$ 存储蛇身体占据的位置,用于快速碰撞检测。 + - 记录屏幕尺寸 $width$ 和 $height$。 + - 记录食物列表 $food$ 和当前食物索引 $food\_index$。 + - 记录当前得分 $score$。 + +2. **move 操作**: + - 根据方向 $direction$ 计算新的头部位置 $new\_head$。 + - 检查新位置是否越界:$new\_head[0] < 0$ 或 $new\_head[0] \geq height$ 或 $new\_head[1] < 0$ 或 $new\_head[1] \geq width$。 + - 检查新位置是否与蛇身碰撞:$new\_head$ 在 $occupied$ 集合中。 + - 如果碰撞,返回 $-1$。 + - 将新头部加入队列和集合。 + - 检查是否吃到食物:$new\_head$ 等于当前食物位置。 + - 如果吃到食物,得分 $+1$,食物索引 $+1$。 + - 如果没吃到食物,移除尾部位置。 + - 返回当前得分。 + +### 思路 1:代码 + +```python +class SnakeGame: + + def __init__(self, width: int, height: int, food: List[List[int]]): + # 初始化屏幕尺寸 + self.width = width + self.height = height + + # 初始化食物列表和索引 + self.food = food + self.food_index = 0 + + # 初始化蛇的身体(使用双端队列,头部在末尾) + self.snake = deque([(0, 0)]) + + # 使用集合存储蛇身体占据的位置,用于快速碰撞检测 + self.occupied = {(0, 0)} + + # 初始化得分 + self.score = 0 + + def move(self, direction: str) -> int: + # 获取当前头部位置 + head_row, head_col = self.snake[-1] + + # 根据方向计算新的头部位置 + if direction == 'U': + new_head = (head_row - 1, head_col) + elif direction == 'D': + new_head = (head_row + 1, head_col) + elif direction == 'L': + new_head = (head_row, head_col - 1) + else: # direction == 'R' + new_head = (head_row, head_col + 1) + + # 检查是否越界 + if (new_head[0] < 0 or new_head[0] >= self.height or + new_head[1] < 0 or new_head[1] >= self.width): + return -1 + + # 检查是否吃到食物 + eating_food = (self.food_index < len(self.food) and + new_head == tuple(self.food[self.food_index])) + + if not eating_food: + # 没吃到食物,先移除尾部 + tail = self.snake.popleft() + self.occupied.remove(tail) + + # 检查是否与蛇身碰撞(在移除尾部后检查) + if new_head in self.occupied: + return -1 + + # 将新头部加入蛇身 + self.snake.append(new_head) + self.occupied.add(new_head) + + if eating_food: + # 吃到食物,得分 +1,食物索引 +1 + self.score += 1 + self.food_index += 1 + + return self.score + + +# Your SnakeGame object will be instantiated and called as such: +# obj = SnakeGame(width, height, food) +# param_1 = obj.move(direction) +``` + +### 思路 1:复杂度分析 + +- **时间复杂度**: + - `move` 操作:$O(1)$,所有操作都是常数时间。 +- **空间复杂度**:$O(n)$,其中 $n$ 是蛇的最大长度。 diff --git a/docs/solutions/0300-0399/design-tic-tac-toe.md b/docs/solutions/0300-0399/design-tic-tac-toe.md new file mode 100644 index 00000000..d24e326b --- /dev/null +++ b/docs/solutions/0300-0399/design-tic-tac-toe.md @@ -0,0 +1,173 @@ +# [0348. 设计井字棋](https://leetcode.cn/problems/design-tic-tac-toe/) + +- 标签:设计、数组、哈希表、矩阵、模拟 +- 难度:中等 + +## 题目链接 + +- [0348. 设计井字棋 - 力扣](https://leetcode.cn/problems/design-tic-tac-toe/) + +## 题目大意 + +**要求**: + +在 $n \times n$ 的棋盘上,实现一个判定井字棋(Tic-Tac-Toe)胜负的神器,判断每一次玩家落子后,是否有胜出的玩家。 + +在这个井字棋游戏中,会有 $2$ 名玩家,他们将轮流在棋盘上放置自己的棋子。 + +在实现这个判定器的过程中,你可以假设以下这些规则一定成立: + +1. 每一步棋都是在棋盘内的,并且只能被放置在一个空的格子里; +2. 一旦游戏中有一名玩家胜出的话,游戏将不能再继续; +3. 一个玩家如果在同一行、同一列或者同一斜对角线上都放置了自己的棋子,那么他便获得胜利。 + +**说明**: + +- $2 \le n \le 10^{3}$。 +- 玩家是 $1$ 或 $2$。 +- $0 \le row, col \lt n$。 +- 每次调用 `move` 时 $(row, col)$ 都是不同的。 +- 最多调用 `move` $n2$ 次。 + +- 进阶:有没有可能将每一步的 `move()` 操作优化到比 $O(n2)$ 更快吗? + +**示例**: + +- 示例 1: + +```python +给定棋盘边长 n = 3, 玩家 1 的棋子符号是 "X",玩家 2 的棋子符号是 "O"。 + +TicTacToe toe = new TicTacToe(3); + +toe.move(0, 0, 1); -> 函数返回 0 (此时,暂时没有玩家赢得这场对决) +|X| | | +| | | | // 玩家 1 在 (0, 0) 落子。 +| | | | + +toe.move(0, 2, 2); -> 函数返回 0 (暂时没有玩家赢得本场比赛) +|X| |O| +| | | | // 玩家 2 在 (0, 2) 落子。 +| | | | + +toe.move(2, 2, 1); -> 函数返回 0 (暂时没有玩家赢得比赛) +|X| |O| +| | | | // 玩家 1 在 (2, 2) 落子。 +| | |X| + +toe.move(1, 1, 2); -> 函数返回 0 (暂没有玩家赢得比赛) +|X| |O| +| |O| | // 玩家 2 在 (1, 1) 落子。 +| | |X| + +toe.move(2, 0, 1); -> 函数返回 0 (暂无玩家赢得比赛) +|X| |O| +| |O| | // 玩家 1 在 (2, 0) 落子。 +|X| |X| + +toe.move(1, 0, 2); -> 函数返回 0 (没有玩家赢得比赛) +|X| |O| +|O|O| | // 玩家 2 在 (1, 0) 落子. +|X| |X| + +toe.move(2, 1, 1); -> 函数返回 1 (此时,玩家 1 赢得了该场比赛) +|X| |O| +|O|O| | // 玩家 1 在 (2, 1) 落子。 +|X|X|X| +``` + +## 解题思路 + +### 思路 1:计数法 + +**核心思想**:使用计数数组来跟踪每个玩家在每行、每列和两条对角线上的棋子数量,避免每次检查整个棋盘。 + +**算法步骤**: + +1. **初始化**:创建 $n \times n$ 的棋盘,以及用于计数的数组: + - `rows[i]`:第 $i$ 行上每个玩家的棋子数量。 + - `cols[j]`:第 $j$ 列上每个玩家的棋子数量。 + - `diagonal`:主对角线上的棋子数量。 + - `anti_diagonal`:副对角线上的棋子数量。 + +2. **落子操作**: + - 在位置 $(row, col)$ 放置玩家 $player$ 的棋子。 + - 更新对应的行、列计数。 + - 如果 $(row, col)$ 在主对角线上($row = col$),更新 `diagonal`。 + - 如果 $(row, col)$ 在副对角线上($row + col = n - 1$),更新 `anti_diagonal`。 + +3. **胜负判断**: + - 检查当前行、列或对角线是否被当前玩家完全占据。 + - 如果任一计数达到 $n$,则该玩家获胜。 + +### 思路 1:代码 + +```python +class TicTacToe: + + def __init__(self, n: int): + """ + 初始化井字棋游戏 + :param n: 棋盘大小 n x n + """ + self.n = n + # 初始化棋盘 + self.board = [[0 for _ in range(n)] for _ in range(n)] + + # 计数数组:跟踪每个玩家在每行、每列的棋子数量 + # rows[i][player] 表示第 i 行上玩家 player 的棋子数量 + self.rows = [[0, 0] for _ in range(n)] # 索引 0 和 1 分别对应玩家 1 和 2 + self.cols = [[0, 0] for _ in range(n)] # 索引 0 和 1 分别对应玩家 1 和 2 + + # 对角线计数 + self.diagonal = [0, 0] # 主对角线 (i == j) + self.anti_diagonal = [0, 0] # 副对角线 (i + j == n - 1) + + def move(self, row: int, col: int, player: int) -> int: + """ + 玩家在指定位置落子 + :param row: 行索引 + :param col: 列索引 + :param player: 玩家编号 (1 或 2) + :return: 获胜玩家编号,如果无人获胜返回 0 + """ + # 将玩家编号转换为数组索引 (1 -> 0, 2 -> 1) + player_idx = player - 1 + + # 在棋盘上放置棋子 + self.board[row][col] = player + + # 更新行计数 + self.rows[row][player_idx] += 1 + + # 更新列计数 + self.cols[col][player_idx] += 1 + + # 更新主对角线计数 (row == col) + if row == col: + self.diagonal[player_idx] += 1 + + # 更新副对角线计数 (row + col == n - 1) + if row + col == self.n - 1: + self.anti_diagonal[player_idx] += 1 + + # 检查是否获胜 + # 如果当前行、列或任一对角线被当前玩家完全占据,则获胜 + if (self.rows[row][player_idx] == self.n or + self.cols[col][player_idx] == self.n or + self.diagonal[player_idx] == self.n or + self.anti_diagonal[player_idx] == self.n): + return player + + return 0 # 无人获胜 + + +# Your TicTacToe object will be instantiated and called as such: +# obj = TicTacToe(n) +# param_1 = obj.move(row,col,player) +``` + +### 思路 1:复杂度分析 + +- **时间复杂度**:$O(1)$。每次 `move` 操作只需要更新计数数组和检查胜负,时间复杂度为常数。 +- **空间复杂度**:$O(n)$。需要 $O(n^2)$ 空间存储棋盘,$O(n)$ 空间存储计数数组,总体为 $O(n^2)$。 diff --git a/docs/solutions/0300-0399/design-twitter.md b/docs/solutions/0300-0399/design-twitter.md new file mode 100644 index 00000000..b3daf4a6 --- /dev/null +++ b/docs/solutions/0300-0399/design-twitter.md @@ -0,0 +1,151 @@ +# [0355. 设计推特](https://leetcode.cn/problems/design-twitter/) + +- 标签:设计、哈希表、链表、堆(优先队列) +- 难度:中等 + +## 题目链接 + +- [0355. 设计推特 - 力扣](https://leetcode.cn/problems/design-twitter/) + +## 题目大意 + +**要求**: + +设计一个简化版的推特(Twitter),可以让用户实现发送推文,关注/取消关注其他用户,能够看见关注人(包括自己)的最近 $10$ 条推文。 + +实现 `Twitter` 类: + +- `Twitter() ` 初始化简易版推特对象 +- `void postTweet(int userId, int tweetId)` 根据给定的 $tweetId$ 和 $userId$ 创建一条新推文。每次调用此函数都会使用一个不同的 $tweetId$ 。 +- `List getNewsFeed(int userId)` 检索当前用户新闻推送中最近 $10$ 条推文的 ID。新闻推送中的每一项都必须是由用户关注的人或者是用户自己发布的推文。推文必须 按照时间顺序由最近到最远排序 。 +- `void follow(int followerId, int followeeId)` ID 为 $followerId$ 的用户开始关注 ID 为 $followeeId$ 的用户。 +- `void unfollow(int followerId, int followeeId)` ID 为 $followerId$ 的用户不再关注 ID 为 $followeeId$ 的用户。 + +**说明**: + +- $1 \le userId, followerId, followeeId \le 500$。 +- $0 \le tweetId \le 10^{4}$。 +- 所有推特的 ID 都互不相同。 +- `postTweet`、`getNewsFeed`、`follow` 和 `unfollow` 方法最多调用 $3 times 10^{4}$ 次。 +- 用户不能关注自己。 + +**示例**: + +- 示例 1: + +```python +输入 +["Twitter", "postTweet", "getNewsFeed", "follow", "postTweet", "getNewsFeed", "unfollow", "getNewsFeed"] +[[], [1, 5], [1], [1, 2], [2, 6], [1], [1, 2], [1]] +输出 +[null, null, [5], null, null, [6, 5], null, [5]] + +解释 +Twitter twitter = new Twitter(); +twitter.postTweet(1, 5); // 用户 1 发送了一条新推文 (用户 id = 1, 推文 id = 5) +twitter.getNewsFeed(1); // 用户 1 的获取推文应当返回一个列表,其中包含一个 id 为 5 的推文 +twitter.follow(1, 2); // 用户 1 关注了用户 2 +twitter.postTweet(2, 6); // 用户 2 发送了一个新推文 (推文 id = 6) +twitter.getNewsFeed(1); // 用户 1 的获取推文应当返回一个列表,其中包含两个推文,id 分别为 -> [6, 5] 。推文 id 6 应当在推文 id 5 之前,因为它是在 5 之后发送的 +twitter.unfollow(1, 2); // 用户 1 取消关注了用户 2 +twitter.getNewsFeed(1); // 用户 1 获取推文应当返回一个列表,其中包含一个 id 为 5 的推文。因为用户 1 已经不再关注用户 2 +``` + +## 解题思路 + +### 思路 1:哈希表 + 全局时间戳 + +使用哈希表来存储用户信息和推文信息,结合全局时间戳来维护推文的时间顺序。 + +具体步骤: + +1. **数据结构设计**: + - 使用全局时间戳 $timestamp$ 来记录每条推文的发布时间。 + - 使用哈希表 $tweets$ 存储每个用户的推文列表,每个推文包含 $tweetId$ 和 $timestamp$。 + - 使用哈希表 $follows$ 存储每个用户关注的用户集合。 + - 使用哈希表 $followers$ 存储每个用户的粉丝集合。 + +2. **postTweet 操作**: + - 将新推文 $[tweetId, timestamp]$ 添加到对应用户的推文列表中。 + - 递增全局时间戳 $timestamp$。 + +3. **getNewsFeed 操作**: + - 获取用户自己及其关注的所有用户的推文。 + - 合并所有推文并按时间戳降序排序。 + - 返回最近的 $10$ 条推文。 + +4. **follow 操作**: + - 在 $follows[followerId]$ 中添加 $followeeId$。 + - 在 $followers[followeeId]$ 中添加 $followerId$。 + +5. **unfollow 操作**: + - 从 $follows[followerId]$ 中移除 $followeeId$。 + - 从 $followers[followeeId]$ 中移除 $followerId$。 + +### 思路 1:代码 + +```python +class Twitter: + + def __init__(self): + # 全局时间戳,用于记录推文发布顺序 + self.timestamp = 0 + # 存储每个用户的推文列表,每个推文格式为 [tweetId, timestamp] + self.tweets = defaultdict(list) + # 存储每个用户关注的用户集合 + self.follows = defaultdict(set) + # 存储每个用户的粉丝集合 + self.followers = defaultdict(set) + + def postTweet(self, userId: int, tweetId: int) -> None: + # 将新推文添加到用户的推文列表中 + self.tweets[userId].append([tweetId, self.timestamp]) + # 递增全局时间戳 + self.timestamp += 1 + + def getNewsFeed(self, userId: int) -> List[int]: + # 获取用户自己及其关注的所有用户 + all_users = {userId} | self.follows[userId] + + # 收集所有相关用户的推文 + all_tweets = [] + for user in all_users: + all_tweets.extend(self.tweets[user]) + + # 按时间戳降序排序(最新的在前) + all_tweets.sort(key=lambda x: x[1], reverse=True) + + # 返回最近的 10 条推文的 tweetId + return [tweet[0] for tweet in all_tweets[:10]] + + def follow(self, followerId: int, followeeId: int) -> None: + # 用户不能关注自己 + if followerId == followeeId: + return + + # 添加关注关系 + self.follows[followerId].add(followeeId) + self.followers[followeeId].add(followerId) + + def unfollow(self, followerId: int, followeeId: int) -> None: + # 移除关注关系 + self.follows[followerId].discard(followeeId) + self.followers[followeeId].discard(followerId) + + +# Your Twitter object will be instantiated and called as such: +# obj = Twitter() +# obj.postTweet(userId,tweetId) +# param_2 = obj.getNewsFeed(userId) +# obj.follow(followerId,followeeId) +# obj.unfollow(followerId,followeeId) +``` + +### 思路 1:复杂度分析 + +- **时间复杂度**: + - `postTweet` 操作:$O(1)$,只需要添加推文到列表末尾。 + - `getNewsFeed` 操作:$O(n \log n)$,其中 $n$ 是所有相关用户的推文总数,需要排序。 + - `follow` 操作:$O(1)$,只需要在集合中添加元素。 + - `unfollow` 操作:$O(1)$,只需要从集合中移除元素。 +- **空间复杂度**:$O(m + n)$,其中 $m$ 是推文总数,$n$ 是用户总数。 diff --git a/docs/solutions/0300-0399/elimination-game.md b/docs/solutions/0300-0399/elimination-game.md new file mode 100644 index 00000000..f9ab4b07 --- /dev/null +++ b/docs/solutions/0300-0399/elimination-game.md @@ -0,0 +1,106 @@ +# [0390. 消除游戏](https://leetcode.cn/problems/elimination-game/) + +- 标签:递归、数学 +- 难度:中等 + +## 题目链接 + +- [0390. 消除游戏 - 力扣](https://leetcode.cn/problems/elimination-game/) + +## 题目大意 + +**描述**: + +给定一个整数 $n$。列表 $arr$ 由在范围 $[1, n]$ 中的所有整数组成,并按严格递增排序。 + +**要求**: + +请你对 $arr$ 应用下述算法: + +- 从左到右,删除第一个数字,然后每隔一个数字删除一个,直到到达列表末尾。 +- 重复上面的步骤,但这次是从右到左。也就是,删除最右侧的数字,然后剩下的数字每隔一个删除一个。 +- 不断重复这两步,从左到右和从右到左交替进行,直到只剩下一个数字。 + +返回 $arr$ 最后剩下的数字。 + +**说明**: + +- $1 \le n \le 10^{9}$。 + +**示例**: + +- 示例 1: + +```python +输入:n = 9 +输出:6 +解释: +arr = [1, 2, 3, 4, 5, 6, 7, 8, 9] +arr = [2, 4, 6, 8] +arr = [2, 6] +arr = [6] +``` + +- 示例 2: + +```python +输入:n = 1 +输出:1 +``` + +## 解题思路 + +### 思路 1:递归 + 数学规律 + +观察消除过程,我们可以发现以下规律: + +1. **从左到右消除**:每次消除后,剩余数字的间隔变为原来的 $2$ 倍,起始位置变为原来的第 $2$ 个位置。 +2. **从右到左消除**:每次消除后,剩余数字的间隔变为原来的 $2$ 倍,但起始位置需要重新计算。 + +设 $f(n)$ 表示从 $1$ 到 $n$ 的数字经过消除游戏后剩余的数字。 + +**递推关系**: + +- 当 $n = 1$ 时:$f(1) = 1$。 +- 当 $n > 1$ 时: + - 从左到右消除后,剩余 $\lfloor \frac{n}{2} \rfloor$ 个数字,间隔为 $2$。 + - 然后从右到左消除,相当于对剩余数字进行镜像操作。 + - $f(n) = 2 \times (1 + \lfloor \frac{n}{2} \rfloor - f(\lfloor \frac{n}{2} \rfloor))$。 + +**关键观察**: + +- 从左到右消除后,剩余数字为 $[2, 4, 6, 8, ...]$,共 $\lfloor \frac{n}{2} \rfloor$ 个。 +- 这些数字可以重新编号为 $[1, 2, 3, 4, ...]$,共 $\lfloor \frac{n}{2} \rfloor$ 个。 +- 对重新编号的数字进行消除游戏,得到 $f(\lfloor \frac{n}{2} \rfloor)$。 +- 由于下一步是从右到左开始,需要计算镜像位置:$1 + \lfloor \frac{n}{2} \rfloor - f(\lfloor \frac{n}{2} \rfloor)$。 +- 最后乘以 $2$ 得到在原序列中的位置。 + +**示例验证**($n = 9$): +1. 初始:$[1, 2, 3, 4, 5, 6, 7, 8, 9]$。 +2. 从左到右消除:$[2, 4, 6, 8]$(剩余 $4$ 个数字)。 +3. 重新编号:$[1, 2, 3, 4]$,对 $4$ 个数字进行消除游戏。 +4. 计算 $f(4)$:$f(4) = 2 \times (1 + 2 - f(2)) = 2 \times (1 + 2 - 2) = 2$。 +5. 镜像位置:$1 + 4 - 2 = 3$,对应原序列中的 $2 \times 3 = 6$。 + +### 思路 1:代码 + +```python +class Solution: + def lastRemaining(self, n: int) -> int: + def f(n): + """计算从1到n的数字经过消除游戏后剩余的数字""" + if n == 1: + return 1 + # 从左到右消除后,剩余数字为[2,4,6,8,...],共n//2个 + # 这些数字可以重新编号为[1,2,3,4,...] + # 对重新编号的数字进行消除游戏,得到f(n//2) + # 由于下一步是从右到左,需要计算镜像位置 + return 2 * (1 + n // 2 - f(n // 2)) + + return f(n) +``` + +### 思路 1:复杂度分析 + +- **时间复杂度**:$O(\log n)$,每次递归调用都将问题规模减半。 +- **空间复杂度**:$O(\log n)$,递归调用栈的深度为 $O(\log n)$。 diff --git a/docs/solutions/0300-0399/index.md b/docs/solutions/0300-0399/index.md index 6bd837d2..f5c806b8 100644 --- a/docs/solutions/0300-0399/index.md +++ b/docs/solutions/0300-0399/index.md @@ -3,17 +3,22 @@ - [0300. 最长递增子序列](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/longest-increasing-subsequence.md) - [0303. 区域和检索 - 数组不可变](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/range-sum-query-immutable.md) - [0304. 二维区域和检索 - 矩阵不可变](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/range-sum-query-2d-immutable.md) +- [0306. 累加数](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/additive-number.md) - [0307. 区域和检索 - 数组可修改](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/range-sum-query-mutable.md) - [0309. 买卖股票的最佳时机含冷冻期](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/best-time-to-buy-and-sell-stock-with-cooldown.md) - [0310. 最小高度树](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/minimum-height-trees.md) - [0312. 戳气球](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/burst-balloons.md) +- [0314. 二叉树的垂直遍历](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/binary-tree-vertical-order-traversal.md) - [0315. 计算右侧小于当前元素的个数](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/count-of-smaller-numbers-after-self.md) - [0316. 去除重复字母](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/remove-duplicate-letters.md) - [0318. 最大单词长度乘积](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/maximum-product-of-word-lengths.md) +- [0319. 灯泡开关](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/bulb-switcher.md) +- [0321. 拼接最大数](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/create-maximum-number.md) - [0322. 零钱兑换](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/coin-change.md) - [0323. 无向图中连通分量的数目](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/number-of-connected-components-in-an-undirected-graph.md) - [0324. 摆动排序 II](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/wiggle-sort-ii.md) - [0326. 3 的幂](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/power-of-three.md) +- [0327. 区间和的个数](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/count-of-range-sum.md) - [0328. 奇偶链表](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/odd-even-linked-list.md) - [0329. 矩阵中的最长递增路径](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/longest-increasing-path-in-a-matrix.md) - [0334. 递增的三元子序列](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/increasing-triplet-subsequence.md) @@ -28,13 +33,19 @@ - [0345. 反转字符串中的元音字母](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/reverse-vowels-of-a-string.md) - [0346. 数据流中的移动平均值](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/moving-average-from-data-stream.md) - [0347. 前 K 个高频元素](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/top-k-frequent-elements.md) +- [0348. 设计井字棋](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/design-tic-tac-toe.md) - [0349. 两个数组的交集](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/intersection-of-two-arrays.md) - [0350. 两个数组的交集 II](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/intersection-of-two-arrays-ii.md) - [0351. 安卓系统手势解锁](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/android-unlock-patterns.md) +- [0352. 将数据流变为多个不相交区间](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/data-stream-as-disjoint-intervals.md) +- [0353. 贪吃蛇](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/design-snake-game.md) - [0354. 俄罗斯套娃信封问题](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/russian-doll-envelopes.md) +- [0355. 设计推特](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/design-twitter.md) - [0357. 统计各位数字都不同的数字个数](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/count-numbers-with-unique-digits.md) - [0359. 日志速率限制器](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/logger-rate-limiter.md) - [0360. 有序转化数组](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/sort-transformed-array.md) +- [0361. 轰炸敌人](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/bomb-enemy.md) +- [0362. 敲击计数器](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/design-hit-counter.md) - [0367. 有效的完全平方数](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/valid-perfect-square.md) - [0370. 区间加法](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/range-addition.md) - [0371. 两整数之和](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/sum-of-two-integers.md) @@ -43,12 +54,14 @@ - [0376. 摆动序列](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/wiggle-subsequence.md) - [0377. 组合总和 Ⅳ](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/combination-sum-iv.md) - [0378. 有序矩阵中第 K 小的元素](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/kth-smallest-element-in-a-sorted-matrix.md) +- [0379. 电话目录管理系统](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/design-phone-directory.md) - [0380. O(1) 时间插入、删除和获取随机元素](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/insert-delete-getrandom-o1.md) - [0383. 赎金信](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/ransom-note.md) - [0384. 打乱数组](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/shuffle-an-array.md) - [0386. 字典序排数](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/lexicographical-numbers.md) - [0387. 字符串中的第一个唯一字符](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/first-unique-character-in-a-string.md) - [0389. 找不同](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/find-the-difference.md) +- [0390. 消除游戏](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/elimination-game.md) - [0391. 完美矩形](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/perfect-rectangle.md) - [0392. 判断子序列](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/is-subsequence.md) - [0394. 字符串解码](https://github.com/ITCharge/AlgoNote/tree/main/docs/solutions/0300-0399/decode-string.md)