From 6db9753fab6faf3a9469a95fc320624abb3f27b9 Mon Sep 17 00:00:00 2001 From: lucifer Date: Thu, 15 Oct 2020 18:24:28 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E7=B2=BE=E9=80=89=E9=A2=98=E8=A7=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- 91/season2.md | 103 +++++ 91/two-pointers.md | 183 ++++++++ SUMMARY.md | 412 +++++++++--------- selected/LCS.md | 260 ++++++++++++ selected/LIS.md | 305 ++++++++++++++ selected/LSS.md | 387 +++++++++++++++++ selected/a-deleted.md | 385 +++++++++++++++++ selected/atMostK.md | 622 ++++++++++++++++++++++++++++ selected/byte-dance-algo-ex-2017.md | 530 ++++++++++++++++++++++++ selected/byte-dance-algo-ex.md | 269 ++++++++++++ selected/construct-binary-tree.md | 259 ++++++++++++ selected/mother-01.md | 410 ++++++++++++++++++ selected/schedule-topic.md | 518 +++++++++++++++++++++++ selected/serialize.md | 303 ++++++++++++++ selected/zuma-game.md | 155 +++++++ 15 files changed, 4905 insertions(+), 196 deletions(-) create mode 100644 91/season2.md create mode 100644 91/two-pointers.md create mode 100644 selected/LCS.md create mode 100644 selected/LIS.md create mode 100644 selected/LSS.md create mode 100644 selected/a-deleted.md create mode 100644 selected/atMostK.md create mode 100644 selected/byte-dance-algo-ex-2017.md create mode 100644 selected/byte-dance-algo-ex.md create mode 100644 selected/construct-binary-tree.md create mode 100644 selected/mother-01.md create mode 100644 selected/schedule-topic.md create mode 100644 selected/serialize.md create mode 100644 selected/zuma-game.md diff --git a/91/season2.md b/91/season2.md new file mode 100644 index 0000000..d817017 --- /dev/null +++ b/91/season2.md @@ -0,0 +1,103 @@ +# 回炉重铸, 91 天见证不一样的自己(第二期) + +力扣加加,一个努力做西湖区最好的算法题解的团队。就在今天它给大家带来了《91 天学算法》,帮助大家摆脱困境,征服算法。 + + + +## 初衷 + +为了让想学习的人能够真正学习到东西, 我打算新开一个栏目《91 天学算法》,在 91 天内来帮助那些想要学习算法,提升自己算法能力的同学,帮助大家建立完整的算法知识体系。 + +群里每天都会有题目,推荐大家讨论当天的题目。我们会帮助大家规划学习路线,91 天见证不一样的自己。群里会有专门的资深算法竞赛大佬坐阵解答大家的问题和疑问,并且会对前一天的题目进行讲解。 + +![](https://tva1.sinaimg.cn/large/007S8ZIlly1gf2b2zkclnj30xm0b6aat.jpg) + +## 活动时间 + +`2020-10-01` 至 `2020-12-30` + +> lucifer 我刚好 12.31 生日, 好巧! + +## 你能够得到什么? + +1. 显著提高你的刷题效率,让你少走弯路 +2. 掌握常见面试题的思路和解法 +3. 掌握常见套路,了解常见算法的本质,横向对比各种题目 +4. 纵向剖析一道题,多种方法不同角度解决同一题目 + +## 要求 + +- 🈲️ 不允许经常闲聊 +- 🈲️ 不允许发广告,软文(只能发算法相关的技术文章) +- ✅ 一周至少参与一次打卡 + +> 违反上述条件的人员会被强制清退 + +## 课程大纲 + +![](https://tva1.sinaimg.cn/large/007S8ZIlly1giq98aux20j30ju0qt781.jpg) + +部分公开的讲义: + +- [【91 算法-基础篇】05.双指针](https://lucifer.ren/blog/2020/05/26/91algo-basic-05.two-pointer/) +- [动态规划问题为什么要画表格?](https://lucifer.ren/blog/2020/08/27/91algo-dp-lecture/) + +### 基础篇(30 天) + +1. 数组,队列,栈 +2. 链表 +3. 树与递归 +4. 哈希表 +5. 双指针 + +### 进阶篇(30 天) + +1. 堆 +2. 前缀树 +3. 并查集 +4. 跳表 +5. 剪枝技巧 +6. RK 和 KMP +7. 高频面试题 + +... + +### 专题篇(31 天) + +1. 二分法 +2. 滑动窗口 +3. 位运算 +4. 背包问题 +5. 搜索(BFS,DFS,回溯) +6. 动态规划 +7. 分治 +8. 贪心 + +... + +## 游戏规则 + +- 每天会根据课程大纲的规划,出一道相关题目。 +- 大家可以在指定 https://91.leetcode-solution.cn 中打卡(不可以抄作业哦),对于不会做的题目可以在群里提问。 +- 第二天会对前一天的题目进行讲解。 + +## 奖励 + +- 对于坚持打卡满一个月的同学,可以参加抽奖,奖品包括`算法模拟面试`,算法相关的图书,科学上网兑换码等 +- 连续打卡七天可以获得补签卡一张哦 + +## 冲鸭 + +报名开始时间待定。 + +采用 微信群的方式进行,`前 50 个进群的小伙伴免费哦 ~`,50 名之后的小伙伴采取阶梯收费的形式。 + +收费标准: + +- 前 50 人免费 +- 51 - 100 收费 5 元 +- 101 - 500 收费 10 元 + +想要参与的小伙伴加我,发红包拉你进群。 + +- 微信号:DevelopeEngineer diff --git a/91/two-pointers.md b/91/two-pointers.md new file mode 100644 index 0000000..40029ef --- /dev/null +++ b/91/two-pointers.md @@ -0,0 +1,183 @@ +# 【91算法-基础篇】05.双指针 + +力扣加加,一个努力做西湖区最好的算法题解的团队。就在今天它给大家带来了《91 天学算法》,帮助大家摆脱困境,征服算法。 + + + +## 什么是双指针 + +顾名思议,双指针就是**两个指针**,但是不同于 C,C++中的指针, 其是一种**算法思想**。 + +如果说,我们迭代一个数组,并输出数组每一项,我们需要一个指针来记录当前遍历的项,这个过程我们叫单指针(index)的话。 + +```java +for(int i = 0;i < nums.size(); i++) { + 输出(nums[i]); +} +``` + +![](https://tva1.sinaimg.cn/large/007S8ZIlly1gf5w79tciyj30aa0hl77b.jpg) + +(图 1) + +那么双指针实际上就是有两个这样的指针,最为经典的就是二分法中的左右双指针啦。 + +```java +int l = 0; +int r = nums.size() - 1; + +while (l < r) { + if(一定条件) return 合适的值,一般是 l 和 r 的中点 + if(一定条件) l++ + if(一定条件) r-- +} +// 因为 l == r,因此返回 l 和 r 都是一样的 +return l +``` + +![](https://tva1.sinaimg.cn/large/007S8ZIlly1gf5yfe9da7j307504ut8r.jpg) + +(图 2) + +读到这里,你发现双指针是一个很宽泛的概念,就好像数组,链表一样,其类型会有很多很多, 比如二分法经常用到`左右端点双指针`。滑动窗口会用到`快慢指针和固定间距指针`。 因此双指针其实是一种综合性很强的类型,类似于数组,栈等。 但是我们这里所讲述的双指针,往往指的是某几种类型的双指针,而不是“只要有两个指针就是双指针了”。 + +> 有了这样一个**算法框架**,或者算法思维,有很大的好处。它能帮助你理清思路,当你碰到新的问题,在脑海里进行搜索的时候,**双指针**这个词就会在你脑海里闪过,闪过的同时你可以根据**双指针**的所有套路和这道题进行**穷举匹配**,这个思考解题过程本来就像是算法,我会在进阶篇《搜索算法》中详细阐述。 + +那么究竟我们算法中提到的双指针指的是什么呢?我们一起来看下算法中双指针的常见题型吧。 + +## 常见题型有哪些? + +这里我将其分为三种类类型,分别是: + +1. 快慢指针(两个指针步长不同) +2. 左右端点指针(两个指针分别指向头尾,并往中间移动,步长不确定) +3. 固定间距指针(两个指针间距相同,步长相同) + +> 上面是我自己的分类,没有参考别人。可以发现我的分类标准已经覆盖了几乎所有常见的情况。 大家在平时做题的时候一定要养成这样的习惯,将题目类型进行总结,当然这个总结可以是别人总结好的,也可以是自己独立总结的。不管是哪一种,都要进行一定的消化吸收,把它们变成真正属于自己的知识。 + +不管是哪一种双指针,只考虑双指针部分的话 ,由于最多还是会遍历整个数组一次,因此时间复杂度取决于步长,如果步长是 1,2 这种常数的话,那么时间复杂度就是 O(N),如果步长是和数据规模有关(比如二分法),其时间复杂度就是 O(logN)。并且由于不管规模多大,我们都只需要最多两个指针,因此空间复杂度是 O(1)。下面我们就来看看双指针的常见套路有哪些。 + +## 常见套路 + +### 快慢指针 + +1. 判断链表是否有环 + +这里给大家推荐两个非常经典的题目,一个是力扣 287 题,一个是 142 题。其中 142 题我在我的 LeetCode 题解仓库中的每日一题板块出过,并且给了很详细的证明和解答。而 287 题相对不直观,比较难以想到,这道题曾被官方选定为每日一题,也是相当经典的。 + +- [287. 寻找重复数](https://leetcode-cn.com/problems/find-the-duplicate-number/) + +- [【每日一题】- 2020-01-14 - 142. 环形链表 II · Issue #274 · azl397985856/leetcode](https://github.com/azl397985856/leetcode/issues/274) + +2. 读写指针。典型的是`删除重复元素` + +这里推荐我仓库中的一道题, 我这里写了一个题解,横向对比了几个相似题目,并剖析了这种题目的本质是什么,让你看透题目本质,推荐阅读。 + +- [80.删除排序数组中的重复项 II](https://github.com/azl397985856/leetcode/blob/master/problems/80.remove-duplicates-from-sorted-array-ii.md) + +## 左右端点指针 + +1. 二分查找。 + +二分查找会在专题篇展开,这里不多说,大家先知道就行了。 + +2. 暴力枚举中“从大到小枚举”(剪枝) + +一个典型的题目是我之前参加官方每日一题的时候给的一个解法,大家可以看下。这种解法是可以 AC 的。同样地,这道题我也给出了三种方法,帮助大家从多个纬度看清这个题目。强烈推荐大家做到一题多解。这对于你做题很多帮助。除了一题多解,还有一个大招是多题同解,这部分我们放在专题篇介绍。 + +[find-the-longest-substring-containing-vowels-in-even](https://leetcode-cn.com/problems/find-the-longest-substring-containing-vowels-in-even-counts/solution/qian-zhui-he-zhuang-tai-ya-suo-pythonjava-by-fe-lu/) + +3. 有序数组。 + +区别于上面的二分查找,这种算法指针移动是连续的,而不是跳跃性的,典型的是 LeetCode 的`两数和`,以及`N数和`系列问题。 + +## 固定间距指针 + +1. 一次遍历(One Pass)求链表的中点 + +2. 一次遍历(One Pass)求链表的倒数第 k 个元素 + +3. 固定窗口大小的滑动窗口 + +## 模板(伪代码) + +我们来看下上面三种题目的算法框架是什么样的。这个时候我们没必要纠结具体的语言,这里我直接使用了伪代码,就是防止你掉进细节。 + +当你掌握了这种算法的细节,就应该找几个题目试试。一方面是检测自己是否真的掌握了,另一方面是“细节”,”细节“是人类,尤其是软件工程师最大的敌人,毕竟我们都是`差不多先生`。 + +1. 快慢指针 + +```jsx +l = 0 +r = 0 +while 没有遍历完 + if 一定条件 + l += 1 + r += 1 +return 合适的值 +``` + +2. 左右端点指针 + +```jsx +l = 0 +r = n - 1 +while l < r + if 找到了 + return 找到的值 + if 一定条件1 + l += 1 + else if 一定条件2 + r -= 1 +return 没找到 + +``` + +3. 固定间距指针 + +```jsx +l = 0 +r = k +while 没有遍历完 + 自定义逻辑 + l += 1 + r += 1 +return 合适的值 +``` + +## 题目推荐 + +如果你`差不多`理解了上面的东西,那么可以拿下面的题练练手。Let's Go! + +### 左右端点指针 + +- 16.3Sum Closest (Medium) +- 713.Subarray Product Less Than K (Medium) +- 977.Squares of a Sorted Array (Easy) +- Dutch National Flag Problem + +> 下面是二分类型 + +- 33.Search in Rotated Sorted Array (Medium) +- 875.Koko Eating Bananas(Medium) +- 881.Boats to Save People(Medium) + +### 快慢指针 + +- 26.Remove Duplicates from Sorted Array(Easy) +- 141.Linked List Cycle (Easy) +- 142.Linked List Cycle II(Medium) +- 287.Find the Duplicate Number(Medium) +- 202.Happy Number (Easy) + +### 固定间距指针 + +- 1456.Maximum Number of Vowels in a Substring of Given Length(Medium) + +> 固定窗口大小的滑动窗口见专题篇的滑动窗口专题(暂未发布) + +## 其他 + +有时候也不能太思维定式,比如 https://leetcode-cn.com/problems/consecutive-characters/ 这道题根本就没必要双指针什么的。 + +再比如:https://lucifer.ren/blog/2020/05/31/101.symmetric-tree/ diff --git a/SUMMARY.md b/SUMMARY.md index 34df722..17569fc 100644 --- a/SUMMARY.md +++ b/SUMMARY.md @@ -1,208 +1,228 @@ ‌ + # Summary​ -* [高频考题(简单)](collections/easy.md) - * [面试题 17.12. BiNode](problems/binode-lcci.md) - * [0001.two-sum](https://github.com/azl397985856/leetcode/blob/mastproblems/1.two-sum.md) - * [0020.Valid Parentheses](problems/20.valid-parentheses.md) - * [0021.MergeTwoSortedLists](problems/21.merge-two-sorted-lists.md) - * [0026.remove-duplicates-from-sorted-array](problems/26.remove-duplicates-from-sorted-array.md) - * [0053.maximum-sum-subarray](problems/53.maximum-sum-subarray-cn.md) - * [0088.merge-sorted-array](problems/88.merge-sorted-array.md) - * [0101.symmetric-tree](problems/101.symmetric-tree.md) - * [0104.maximum-depth-of-binary-tree](problems/104.maximum-depth-of-binary-tree.md) - * [0108.convert-sorted-array-to-binary-search-tree](problems/108.convert-sorted-array-to-binary-search-tree.md) - * [0121.best-time-to-buy-and-sell-stock](problems/121.best-time-to-buy-and-sell-stock.md) - * [0122.best-time-to-buy-and-sell-stock-ii](problems/122.best-time-to-buy-and-sell-stock-ii.md) - * [0125.valid-palindrome](problems/125.valid-palindrome.md) - * [0136.single-number](problems/136.single-number.md) - * [0155.min-stack](problems/155.min-stack.md) - * [0167.two-sum-ii-input-array-is-sorted](problems/167.two-sum-ii-input-array-is-sorted.md) - * [0169.majority-element](problems/169.majority-element.md) - * [0172.factorial-trailing-zeroes](problems/172.factorial-trailing-zeroes.md) - * [0190.reverse-bits](problems/190.reverse-bits.md) - * [0191.number-of-1-bits](problems/191.number-of-1-bits.md) - * [0198.house-robber](problems/198.house-robber.md) - * [0203.remove-linked-list-elements](problems/203.remove-linked-list-elements.md) - * [0206.reverse-linked-list](problems/206.reverse-linked-list.md) - * [0219.contains-duplicate-ii](problems/219.contains-duplicate-ii.md) - * [0226.invert-binary-tree](problems/226.invert-binary-tree.md) - * [0232.implement-queue-using-stacks](problems/232.implement-queue-using-stacks.md) - * [0263.ugly-number](problems/263.ugly-number.md) - * [0283.move-zeroes](problems/283.move-zeroes.md) - * [0342.power-of-four](problems/342.power-of-four.md) - * [0349.intersection-of-two-arrays](problems/349.intersection-of-two-arrays.md) - * [0371.sum-of-two-integers](problems/371.sum-of-two-integers.md) - * [0437.path-sum-iii](problems/437.path-sum-iii.md) - * [0455.AssignCookies](problems/455.AssignCookies.md) - * [0575.distribute-candies](problems/575.distribute-candies.md) - * [0874.walking-robot-simulation](problems/874.walking-robot-simulation.md) - * [1260.shift-2d-grid](problems/1260.shift-2d-grid.md) - * [1332.remove-palindromic-subsequences](problems/1332.remove-palindromic-subsequences.md) +- [高频考题(简单)](collections/easy.md) + + - [面试题 17.12. BiNode](problems/binode-lcci.md) + - [0001.two-sum](https://github.com/azl397985856/leetcode/blob/mastproblems/1.two-sum.md) + - [0020.Valid Parentheses](problems/20.valid-parentheses.md) + - [0021.MergeTwoSortedLists](problems/21.merge-two-sorted-lists.md) + - [0026.remove-duplicates-from-sorted-array](problems/26.remove-duplicates-from-sorted-array.md) + - [0053.maximum-sum-subarray](problems/53.maximum-sum-subarray-cn.md) + - [0088.merge-sorted-array](problems/88.merge-sorted-array.md) + - [0101.symmetric-tree](problems/101.symmetric-tree.md) + - [0104.maximum-depth-of-binary-tree](problems/104.maximum-depth-of-binary-tree.md) + - [0108.convert-sorted-array-to-binary-search-tree](problems/108.convert-sorted-array-to-binary-search-tree.md) + - [0121.best-time-to-buy-and-sell-stock](problems/121.best-time-to-buy-and-sell-stock.md) + - [0122.best-time-to-buy-and-sell-stock-ii](problems/122.best-time-to-buy-and-sell-stock-ii.md) + - [0125.valid-palindrome](problems/125.valid-palindrome.md) + - [0136.single-number](problems/136.single-number.md) + - [0155.min-stack](problems/155.min-stack.md) + - [0167.two-sum-ii-input-array-is-sorted](problems/167.two-sum-ii-input-array-is-sorted.md) + - [0169.majority-element](problems/169.majority-element.md) + - [0172.factorial-trailing-zeroes](problems/172.factorial-trailing-zeroes.md) + - [0190.reverse-bits](problems/190.reverse-bits.md) + - [0191.number-of-1-bits](problems/191.number-of-1-bits.md) + - [0198.house-robber](problems/198.house-robber.md) + - [0203.remove-linked-list-elements](problems/203.remove-linked-list-elements.md) + - [0206.reverse-linked-list](problems/206.reverse-linked-list.md) + - [0219.contains-duplicate-ii](problems/219.contains-duplicate-ii.md) + - [0226.invert-binary-tree](problems/226.invert-binary-tree.md) + - [0232.implement-queue-using-stacks](problems/232.implement-queue-using-stacks.md) + - [0263.ugly-number](problems/263.ugly-number.md) + - [0283.move-zeroes](problems/283.move-zeroes.md) + - [0342.power-of-four](problems/342.power-of-four.md) + - [0349.intersection-of-two-arrays](problems/349.intersection-of-two-arrays.md) + - [0371.sum-of-two-integers](problems/371.sum-of-two-integers.md) + - [0437.path-sum-iii](problems/437.path-sum-iii.md) + - [0455.AssignCookies](problems/455.AssignCookies.md) + - [0575.distribute-candies](problems/575.distribute-candies.md) + - [0874.walking-robot-simulation](problems/874.walking-robot-simulation.md) + - [1260.shift-2d-grid](problems/1260.shift-2d-grid.md) + - [1332.remove-palindromic-subsequences](problems/1332.remove-palindromic-subsequences.md) + +- [高频考题(中等)](collections/medium.md) + + - [面试题 17.09. 第 k 个数](problems/get-kth-magic-number-lcci.md) -* 高频考题(中等) + - [0002.add-two-numbers](problems/2.add-two-numbers.md) + - [0003.longest-substring-without-repeating-characters](problems/3.longest-substring-without-repeating-characters.md) + - [0005.longest-palindromic-substring](problems/5.longest-palindromic-substring.md) + - [0011.container-with-most-water](problems/11.container-with-most-water.md) + - [0015.3-sum](problems/15.3sum.md) + - [0017.Letter-Combinations-of-a-Phone-Number](problems/17.Letter-Combinations-of-a-Phone-Number.md) + - [0019. Remove Nth Node From End of List](problems/19.removeNthNodeFromEndofList.md) + - [0022.generate-parentheses.md](problems/22.generate-parentheses.md) + - [0024. Swap Nodes In Pairs](problems/24.swapNodesInPairs.md) + - [0029.divide-two-integers](problems/29.divide-two-integers.md) + - [0031.next-permutation](problems/31.next-permutation.md) + - [0033.search-in-rotated-sorted-array](problems/33.search-in-rotated-sorted-array.md) + - [0039.combination-sum](problems/39.combination-sum.md) + - [0040.combination-sum-ii](problems/40.combination-sum-ii.md) + - [0046.permutations](problems/46.permutations.md) + - [0047.permutations-ii](problems/47.permutations-ii.md) + - [0048.rotate-image](problems/48.rotate-image.md) + - [0049.group-anagrams](problems/49.group-anagrams.md) + - [0050.pow-x-n](problems/50.pow-x-n.md) + - [0055.jump-game](problems/55.jump-game.md) + - [0056.merge-intervals](problems/56.merge-intervals.md) + - [0060.permutation-sequence](problems/60.permutation-sequence.md) + - [0062.unique-paths](problems/62.unique-paths.md) + - [0073.set-matrix-zeroes](problems/73.set-matrix-zeroes.md) + - [0075.sort-colors](problems/75.sort-colors.md) + - [0078.subsets](problems/78.subsets.md) + - [0079.word-search](problems/79.word-search-en.md) + - [0080.remove-duplicates-from-sorted-array-ii](problems/80.remove-duplicates-from-sorted-array-ii.md) + - [0086.partition-list](problems/86.partition-list.md) + - [0090.subsets-ii](problems/90.subsets-ii.md) + - [0091.decode-ways](problems/91.decode-ways.md) + - [0092.reverse-linked-list-ii](problems/92.reverse-linked-list-ii.md) + - [0094.binary-tree-inorder-traversal](problems/94.binary-tree-inorder-traversal.md) + - [0095.unique-binary-search-trees-ii](problems/95.unique-binary-search-trees-ii.md) + - [0096.unique-binary-search-trees](problems/96.unique-binary-search-trees.md) + - [0098.validate-binary-search-tree](problems/98.validate-binary-search-tree.md) + - [0102.binary-tree-level-order-traversal](problems/102.binary-tree-level-order-traversal.md) + - [0103.binary-tree-zigzag-level-order-traversal](problems/103.binary-tree-zigzag-level-order-traversal.md) + - [105.Construct-Binary-Tree-from-Preorder-and-Inorder-Traversal.md](problems/105.Construct-Binary-Tree-from-Preorder-and-Inorder-Traversal.md) + - [0113.path-sum-ii](problems/113.path-sum-ii.md) + - [0129.sum-root-to-leaf-numbers](problems/129.sum-root-to-leaf-numbers.md) + - [0130.surrounded-regions](problems/130.surrounded-regions.md) + - [0131.palindrome-partitioning](problems/131.palindrome-partitioning.md) + - [0139.word-break](problems/139.word-break.md) + - [0144.binary-tree-preorder-traversal](problems/144.binary-tree-preorder-traversal.md) + - [0150.evaluate-reverse-polish-notation](problems/150.evaluate-reverse-polish-notation.md) + - [0152.maximum-product-subarray](problems/152.maximum-product-subarray.md) + - [0199.binary-tree-right-side-view](problems/199.binary-tree-right-side-view.md) + - [0200.number-of-islands](problems/200.number-of-islands.md) + - [0201.bitwise-and-of-numbers-range](problems/201.bitwise-and-of-numbers-range.md) + - [0208.implement-trie-prefix-tree](problems/208.implement-trie-prefix-tree.md) + - [0209.minimum-size-subarray-sum](problems/209.minimum-size-subarray-sum.md) + - [0211.add-and-search-word-data-structure-design](problems/211.add-and-search-word-data-structure-design.md) + - [0215.kth-largest-element-in-an-array](problems/215.kth-largest-element-in-an-array.md) + - [0221.maximal-square](problems/221.maximal-square.md) + - [0229.majority-element-ii](problems/229.majority-element-ii.md) + - [0230.kth-smallest-element-in-a-bst](problems/230.kth-smallest-element-in-a-bst.md) + - [0236.lowest-common-ancestor-of-a-binary-tree](problems/236.lowest-common-ancestor-of-a-binary-tree.md) + - [0238product-of-array-except-self](problems/238product-of-array-except-self.md) + - [0240.search-a-2-d-matrix-ii](problems/240.search-a-2-d-matrix-ii.md) + - [0279.perfect-squares](problems/279.perfect-squares.md) + - [0309.best-time-to-buy-and-sell-stock-with-cooldown](problems/309.best-time-to-buy-and-sell-stock-with-cooldown.md) + - [0322.coin-change](problems/322.coin-change.md) + - [0328.odd-even-linked-list](problems/328.odd-even-linked-list.md) + - [0334.increasing-triplet-subsequence](problems/334.increasing-triplet-subsequence.md) + - [0337.house-robber-iii.md](problems/337.house-robber-iii.md) + - [0343.integer-break](problems/343.integer-break.md) + - [0365.water-and-jug-problem](problems/365.water-and-jug-problem.md) + - [0378.kth-smallest-element-in-a-sorted-matrix](problems/378.kth-smallest-element-in-a-sorted-matrix.md) + - [0380.insert-delete-getrandom-o1](problems/380.insert-delete-getrandom-o1.md) + - [0416.partition-equal-subset-sum](problems/416.partition-equal-subset-sum.md) + - [0445.add-two-numbers-ii](problems/445.add-two-numbers-ii.md) + - [0454.4-sum-ii](problems/454.4-sum-ii.md) + - [0474.ones-and-zeros](problems/474.ones-and-zeros-en.md) + - [0494.target-sum](problems/494.target-sum.md) + - [0516.longest-palindromic-subsequence](problems/516.longest-palindromic-subsequence.md) + - [0518.coin-change-2](problems/518.coin-change-2.md) + - [0547.friend-circles](problems/547.friend-circles-en.md) + - [0560.subarray-sum-equals-k](problems/560.subarray-sum-equals-k.md) + - [0609.find-duplicate-file-in-system](problems/609.find-duplicate-file-in-system.md) + - [0611.valid-triangle-number](problems/611.valid-triangle-number.md) + - [0718.maximum-length-of-repeated-subarray](problems/718.maximum-length-of-repeated-subarray.md) + - [0754.reach-a-number.md](problems/754.reach-a-number.md) + - [0785.is-graph-bipartite](problems/785.is-graph-bipartite.md) + - [0820.short-encoding-of-words](problems/820.short-encoding-of-words.md) + - [0875.koko-eating-bananas](problems/875.koko-eating-bananas.md) + - [0877.stone-game](problems/877.stone-game.md) + - [0886.possible-bipartition](problems/886.possible-bipartition.md) + - [0900.rle-iterator](problems/900.rle-iterator.md) + - [0912.sort-an-array](problems/912.sort-an-array.md) + - [0935.knight-dialer](problems/935.knight-dialer.md) + - [1011.capacity-to-ship-packages-within-d-days](problems/1011.capacity-to-ship-packages-within-d-days.md) + - [1014.best-sightseeing-pair](problems/1014.best-sightseeing-pair.md) + - [1015.smallest-integer-divisible-by-k](problems/1015.smallest-integer-divisible-by-k.md) + - [1019.next-greater-node-in-linked-list](problems/1019.next-greater-node-in-linked-list.md) + - [1020.number-of-enclaves](problems/1020.number-of-enclaves.md) + - [1023.camelcase-matching](problems/1023.camelcase-matching.md) + - [1031.maximum-sum-of-two-non-overlapping-subarrays](problems/1031.maximum-sum-of-two-non-overlapping-subarrays.md) + - [1104.path-in-zigzag-labelled-binary-tree](problems/1104.path-in-zigzag-labelled-binary-tree.md) + - [1131.maximum-of-absolute-value-expression](problems/1131.maximum-of-absolute-value-expression.md) + - [1186.maximum-subarray-sum-with-one-deletion](problems/1186.maximum-subarray-sum-with-one-deletion.md) + - [1218.longest-arithmetic-subsequence-of-given-difference](problems/1218.longest-arithmetic-subsequence-of-given-difference.md) + - [1227.airplane-seat-assignment-probability](problems/1227.airplane-seat-assignment-probability.md) + - [1261.find-elements-in-a-contaminated-binary-tree](problems/1261.find-elements-in-a-contaminated-binary-tree.md) + - [1262.greatest-sum-divisible-by-three](problems/1262.greatest-sum-divisible-by-three.md) + - [1297.maximum-number-of-occurrences-of-a-substring](problems/1297.maximum-number-of-occurrences-of-a-substring.md) + - [1310.xor-queries-of-a-subarray](problems/1310.xor-queries-of-a-subarray.md) + - [1334.find-the-city-with-the-smallest-number-of-neighbors-at-a-threshold-distance](problems/1334.find-the-city-with-the-smallest-number-of-neighbors-at-a-threshold-distance.md) + - [1371.find-the-longest-substring-containing-vowels-in-even-counts](problems/1371.find-the-longest-substring-containing-vowels-in-even-counts.md) - * [面试题 17.09. 第 k 个数](problems/get-kth-magic-number-lcci.md) +- [高频考题(困难)](collections/hard.md) - * [0002.add-two-numbers](problems/2.add-two-numbers.md) - * [0003.longest-substring-without-repeating-characters](problems/3.longest-substring-without-repeating-characters.md) - * [0005.longest-palindromic-substring](problems/5.longest-palindromic-substring.md) - * [0011.container-with-most-water](problems/11.container-with-most-water.md) - * [0015.3-sum](problems/15.3sum.md) - * [0017.Letter-Combinations-of-a-Phone-Number](problems/17.Letter-Combinations-of-a-Phone-Number.md) - * [0019. Remove Nth Node From End of List](problems/19.removeNthNodeFromEndofList.md) - * [0022.generate-parentheses.md](problems/22.generate-parentheses.md) - * [0024. Swap Nodes In Pairs](problems/24.swapNodesInPairs.md) - * [0029.divide-two-integers](problems/29.divide-two-integers.md) - * [0031.next-permutation](problems/31.next-permutation.md) - * [0033.search-in-rotated-sorted-array](problems/33.search-in-rotated-sorted-array.md) - * [0039.combination-sum](problems/39.combination-sum.md) - * [0040.combination-sum-ii](problems/40.combination-sum-ii.md) - * [0046.permutations](problems/46.permutations.md) - * [0047.permutations-ii](problems/47.permutations-ii.md) - * [0048.rotate-image](problems/48.rotate-image.md) - * [0049.group-anagrams](problems/49.group-anagrams.md) - * [0050.pow-x-n](problems/50.pow-x-n.md) - * [0055.jump-game](problems/55.jump-game.md) - * [0056.merge-intervals](problems/56.merge-intervals.md) - * [0060.permutation-sequence](problems/60.permutation-sequence.md) - * [0062.unique-paths](problems/62.unique-paths.md) - * [0073.set-matrix-zeroes](problems/73.set-matrix-zeroes.md) - * [0075.sort-colors](problems/75.sort-colors.md) - * [0078.subsets](problems/78.subsets.md) - * [0079.word-search](problems/79.word-search-en.md) - * [0080.remove-duplicates-from-sorted-array-ii](problems/80.remove-duplicates-from-sorted-array-ii.md) - * [0086.partition-list](problems/86.partition-list.md) - * [0090.subsets-ii](problems/90.subsets-ii.md) - * [0091.decode-ways](problems/91.decode-ways.md) - * [0092.reverse-linked-list-ii](problems/92.reverse-linked-list-ii.md) - * [0094.binary-tree-inorder-traversal](problems/94.binary-tree-inorder-traversal.md) - * [0095.unique-binary-search-trees-ii](problems/95.unique-binary-search-trees-ii.md) - * [0096.unique-binary-search-trees](problems/96.unique-binary-search-trees.md) - * [0098.validate-binary-search-tree](problems/98.validate-binary-search-tree.md) - * [0102.binary-tree-level-order-traversal](problems/102.binary-tree-level-order-traversal.md) - * [0103.binary-tree-zigzag-level-order-traversal](problems/103.binary-tree-zigzag-level-order-traversal.md) - * [105.Construct-Binary-Tree-from-Preorder-and-Inorder-Traversal.md](problems/105.Construct-Binary-Tree-from-Preorder-and-Inorder-Traversal.md) - * [0113.path-sum-ii](problems/113.path-sum-ii.md) - * [0129.sum-root-to-leaf-numbers](problems/129.sum-root-to-leaf-numbers.md) - * [0130.surrounded-regions](problems/130.surrounded-regions.md) - * [0131.palindrome-partitioning](problems/131.palindrome-partitioning.md) - * [0139.word-break](problems/139.word-break.md) - * [0144.binary-tree-preorder-traversal](problems/144.binary-tree-preorder-traversal.md) - * [0150.evaluate-reverse-polish-notation](problems/150.evaluate-reverse-polish-notation.md) - * [0152.maximum-product-subarray](problems/152.maximum-product-subarray.md) - * [0199.binary-tree-right-side-view](problems/199.binary-tree-right-side-view.md) - * [0200.number-of-islands](problems/200.number-of-islands.md) - * [0201.bitwise-and-of-numbers-range](problems/201.bitwise-and-of-numbers-range.md) - * [0208.implement-trie-prefix-tree](problems/208.implement-trie-prefix-tree.md) - * [0209.minimum-size-subarray-sum](problems/209.minimum-size-subarray-sum.md) - * [0211.add-and-search-word-data-structure-design](problems/211.add-and-search-word-data-structure-design.md) - * [0215.kth-largest-element-in-an-array](problems/215.kth-largest-element-in-an-array.md) - * [0221.maximal-square](problems/221.maximal-square.md) - * [0229.majority-element-ii](problems/229.majority-element-ii.md) - * [0230.kth-smallest-element-in-a-bst](problems/230.kth-smallest-element-in-a-bst.md) - * [0236.lowest-common-ancestor-of-a-binary-tree](problems/236.lowest-common-ancestor-of-a-binary-tree.md) - * [0238product-of-array-except-self](problems/238product-of-array-except-self.md) - * [0240.search-a-2-d-matrix-ii](problems/240.search-a-2-d-matrix-ii.md) - * [0279.perfect-squares](problems/279.perfect-squares.md) - * [0309.best-time-to-buy-and-sell-stock-with-cooldown](problems/309.best-time-to-buy-and-sell-stock-with-cooldown.md) - * [0322.coin-change](problems/322.coin-change.md) - * [0328.odd-even-linked-list](problems/328.odd-even-linked-list.md) - * [0334.increasing-triplet-subsequence](problems/334.increasing-triplet-subsequence.md) - * [0337.house-robber-iii.md](problems/337.house-robber-iii.md) - * [0343.integer-break](problems/343.integer-break.md) - * [0365.water-and-jug-problem](problems/365.water-and-jug-problem.md) - * [0378.kth-smallest-element-in-a-sorted-matrix](problems/378.kth-smallest-element-in-a-sorted-matrix.md) - * [0380.insert-delete-getrandom-o1](problems/380.insert-delete-getrandom-o1.md) - * [0416.partition-equal-subset-sum](problems/416.partition-equal-subset-sum.md) - * [0445.add-two-numbers-ii](problems/445.add-two-numbers-ii.md) - * [0454.4-sum-ii](problems/454.4-sum-ii.md) - * [0474.ones-and-zeros](problems/474.ones-and-zeros-en.md) - * [0494.target-sum](problems/494.target-sum.md) - * [0516.longest-palindromic-subsequence](problems/516.longest-palindromic-subsequence.md) - * [0518.coin-change-2](problems/518.coin-change-2.md) - * [0547.friend-circles](problems/547.friend-circles-en.md) - * [0560.subarray-sum-equals-k](problems/560.subarray-sum-equals-k.md) - * [0609.find-duplicate-file-in-system](problems/609.find-duplicate-file-in-system.md) - * [0611.valid-triangle-number](problems/611.valid-triangle-number.md) - * [0718.maximum-length-of-repeated-subarray](problems/718.maximum-length-of-repeated-subarray.md) - * [0754.reach-a-number.md](problems/754.reach-a-number.md) - * [0785.is-graph-bipartite](problems/785.is-graph-bipartite.md) - * [0820.short-encoding-of-words](problems/820.short-encoding-of-words.md) - * [0875.koko-eating-bananas](problems/875.koko-eating-bananas.md) - * [0877.stone-game](problems/877.stone-game.md) - * [0886.possible-bipartition](problems/886.possible-bipartition.md) - * [0900.rle-iterator](problems/900.rle-iterator.md) - * [0912.sort-an-array](problems/912.sort-an-array.md) - * [0935.knight-dialer](problems/935.knight-dialer.md) - * [1011.capacity-to-ship-packages-within-d-days](problems/1011.capacity-to-ship-packages-within-d-days.md) - * [1014.best-sightseeing-pair](problems/1014.best-sightseeing-pair.md) - * [1015.smallest-integer-divisible-by-k](problems/1015.smallest-integer-divisible-by-k.md) - * [1019.next-greater-node-in-linked-list](problems/1019.next-greater-node-in-linked-list.md) - * [1020.number-of-enclaves](problems/1020.number-of-enclaves.md) - * [1023.camelcase-matching](problems/1023.camelcase-matching.md) - * [1031.maximum-sum-of-two-non-overlapping-subarrays](problems/1031.maximum-sum-of-two-non-overlapping-subarrays.md) - * [1104.path-in-zigzag-labelled-binary-tree](problems/1104.path-in-zigzag-labelled-binary-tree.md) - * [1131.maximum-of-absolute-value-expression](problems/1131.maximum-of-absolute-value-expression.md) - * [1186.maximum-subarray-sum-with-one-deletion](problems/1186.maximum-subarray-sum-with-one-deletion.md) - * [1218.longest-arithmetic-subsequence-of-given-difference](problems/1218.longest-arithmetic-subsequence-of-given-difference.md) - * [1227.airplane-seat-assignment-probability](problems/1227.airplane-seat-assignment-probability.md) - * [1261.find-elements-in-a-contaminated-binary-tree](problems/1261.find-elements-in-a-contaminated-binary-tree.md) - * [1262.greatest-sum-divisible-by-three](problems/1262.greatest-sum-divisible-by-three.md) - * [1297.maximum-number-of-occurrences-of-a-substring](problems/1297.maximum-number-of-occurrences-of-a-substring.md) - * [1310.xor-queries-of-a-subarray](problems/1310.xor-queries-of-a-subarray.md) - * [1334.find-the-city-with-the-smallest-number-of-neighbors-at-a-threshold-distance](problems/1334.find-the-city-with-the-smallest-number-of-neighbors-at-a-threshold-distance.md) - * [1371.find-the-longest-substring-containing-vowels-in-even-counts](problems/1371.find-the-longest-substring-containing-vowels-in-even-counts.md) + - [0004.median-of-two-sorted-array](problems/4.median-of-two-sorted-arrays.md) + - [0023.merge-k-sorted-lists](problems/23.merge-k-sorted-lists.md) + - [0025.reverse-nodes-in-k-group](problems/25.reverse-nodes-in-k-groups-cn.md) + - [0030.substring-with-concatenation-of-all-words](problems/30.substring-with-concatenation-of-all-words.md) + - [0032.longest-valid-parentheses](problems/32.longest-valid-parentheses.md) + - [0042.trapping-rain-water](problems/42.trapping-rain-water.md) + - [0052.N-Queens-II](problems/52.N-Queens-II.md) + - [0084.largest-rectangle-in-histogram](problems/84.largest-rectangle-in-histogram.md) + - [0085.maximal-rectangle](problems/85.maximal-rectangle.md) + - [0124.binary-tree-maximum-path-sum](problems/124.binary-tree-maximum-path-sum.md) + - [0128.longest-consecutive-sequence](problems/128.longest-consecutive-sequence.md) + - [0145.binary-tree-postorder-traversal](problems/145.binary-tree-postorder-traversal.md) + - [0212.word-search-ii](problems/212.word-search-ii.md) + - [0239.sliding-window-maximum](problems/239.sliding-window-maximum.md) + - [0295.find-median-from-data-stream](problems/295.find-median-from-data-stream.md) + - [0301.remove-invalid-parentheses](problems/301.remove-invalid-parentheses.md) + - [0312.burst-balloons](problems/312.burst-balloons.md) + - [0335.self-crossPing](problems/335.self-crossing.md) + - [0460.lfu-cache](problems/460.lfu-cache.md) + - [0472.concatenated-words](problems/472.concatenated-words.md) + - [0488.zuma-game.md](problems/488.zuma-game.md) + - [0493.reverse-pairs](problems/493.reverse-pairs.md) + - [0887.super-egg-drop](problems/887.super-egg-drop.md) + - [0895.maximum-frequency-stack](problems/895.maximum-frequency-stack.md) + - [1032.stream-of-characters](problems/1032.stream-of-characters.md) + - [1168.optimize-water-distribution-in-a-village](problems/1168.optimize-water-distribution-in-a-village-cn.md) + - [1449.form-largest-integer-with-digits-that-add-up-to-target](problems/1449.form-largest-integer-with-digits-that-add-up-to-target.md) -* 高频考题(困难) +- 算法专题 - * [0004.median-of-two-sorted-array](problems/4.median-of-two-sorted-arrays.md) - * [0023.merge-k-sorted-lists](problems/23.merge-k-sorted-lists.md) - * [0025.reverse-nodes-in-k-group](problems/25.reverse-nodes-in-k-groups-cn.md) - * [0030.substring-with-concatenation-of-all-words](problems/30.substring-with-concatenation-of-all-words.md) - * [0032.longest-valid-parentheses](problems/32.longest-valid-parentheses.md) - * [0042.trapping-rain-water](problems/42.trapping-rain-water.md) - * [0052.N-Queens-II](problems/52.N-Queens-II.md) - * [0084.largest-rectangle-in-histogram](problems/84.largest-rectangle-in-histogram.md) - * [0085.maximal-rectangle](problems/85.maximal-rectangle.md) - * [0124.binary-tree-maximum-path-sum](problems/124.binary-tree-maximum-path-sum.md) - * [0128.longest-consecutive-sequence](problems/128.longest-consecutive-sequence.md) - * [0145.binary-tree-postorder-traversal](problems/145.binary-tree-postorder-traversal.md) - * [0212.word-search-ii](problems/212.word-search-ii.md) - * [0239.sliding-window-maximum](problems/239.sliding-window-maximum.md) - * [0295.find-median-from-data-stream](problems/295.find-median-from-data-stream.md) - * [0301.remove-invalid-parentheses](problems/301.remove-invalid-parentheses.md) - * [0312.burst-balloons](problems/312.burst-balloons.md) - * [0335.self-crossPing](problems/335.self-crossing.md) - * [0460.lfu-cache](problems/460.lfu-cache.md) - * [0472.concatenated-words](problems/472.concatenated-words.md) - * [0488.zuma-game.md](problems/488.zuma-game.md) - * [0493.reverse-pairs](problems/493.reverse-pairs.md) - * [0887.super-egg-drop](problems/887.super-egg-drop.md) - * [0895.maximum-frequency-stack](problems/895.maximum-frequency-stack.md) - * [1032.stream-of-characters](problems/1032.stream-of-characters.md) - * [1168.optimize-water-distribution-in-a-village](problems/1168.optimize-water-distribution-in-a-village-cn.md) - * [1449.form-largest-integer-with-digits-that-add-up-to-target](problems/1449.form-largest-integer-with-digits-that-add-up-to-target.md) + - [数据结构](thinkings/basic-data-structure.md) + - [基础算法](thinkings/basic-algorithm.md) + - [二叉树的遍历](thinkings/binary-tree-traversal.md) + - [动态规划](thinkings/dynamic-programming.md) + - [哈夫曼编码和游程编码](thinkings/run-length-encode-and-huffman-encode.md) + - [布隆过滤器](thinkings/bloom-filter.md) + - [字符串问题](thinkings/string-problems.md) + - [前缀树专题](thinkings/trie.md) + - [《贪婪策略》专题](thinkings/greedy.md) + - [《深度优先遍历》专题](thinkings/DFS.md) + - [滑动窗口(思路 + 模板)](thinkings/slide-window.md) + - [位运算](thinkings/bit.md) + - [设计题](thinkings/design.md) + - [小岛问题](thinkings/island.md) + - [最大公约数](thinkings/GCD.md) + - [并查集](thinkings/union-find.md) + - [前缀和](thinkings/prefix.md) + - [平衡二叉树专题](thinkings/balanced-tree.md) -* 算法专题 +- 91 算法 - * [数据结构](thinkings/basic-data-structure.md) - * [基础算法](thinkings/basic-algorithm.md) - * [二叉树的遍历](thinkings/binary-tree-traversal.md) - * [动态规划](thinkings/dynamic-programming.md) - * [哈夫曼编码和游程编码](thinkings/run-length-encode-and-huffman-encode.md) - * [布隆过滤器](thinkings/bloom-filter.md) - * [字符串问题](thinkings/string-problems.md) - * [前缀树专题](thinkings/trie.md) - * [《贪婪策略》专题](thinkings/greedy.md) - * [《深度优先遍历》专题](thinkings/DFS.md) - * [滑动窗口(思路 + 模板)](thinkings/slide-window.md) - * [位运算](thinkings/bit.md) - * [设计题](thinkings/design.md) - * [小岛问题](thinkings/island.md) - * [最大公约数](thinkings/GCD.md) - * [并查集](thinkings/union-find.md) - * [前缀和](thinkings/prefix.md) - * [平衡二叉树专题](thinkings/balanced-tree.md) + - [双指针](./91/two-pointers.md) + - [第二期](./91/season2.md) +- 精选题解 + - [《日程安排》专题](./selected/schedule-topic.md) + - [《构造二叉树》专题](./selected/construct-binary-tree.md) + - [字典序列删除](./selected/a-deleted.md) + - [百度的算法面试题 - 祖玛游戏](./selected/zuma-game.md) + - [西法带你学算法】一次搞定前缀和](./selected/atMostK.md) + - [字节跳动的算法面试题是什么难度?](./selected/byte-dance-algo-ex.md) + - [字节跳动的算法面试题是什么难度?(第二弹)](./selected/byte-dance-algo-ex-2017.md) + - [《我是你的妈妈呀》 - 第一期](./selected/mother-01.md) + - [一文带你看懂二叉树的序列化](./selected/serialize.md) + - [穿上衣服我就不认识你了?来聊聊最长上升子序列](./selected/LIS.md) + - [你的衣服我扒了 - 《最长公共子序列》](./selected/LCS.md) + - [一文看懂《最大子序列和问题》](./selected/LSS.md) diff --git a/selected/LCS.md b/selected/LCS.md new file mode 100644 index 0000000..4214cad --- /dev/null +++ b/selected/LCS.md @@ -0,0 +1,260 @@ +# 你的衣服我扒了 - 《最长公共子序列》 + +之前出了一篇[穿上衣服我就不认识你了?来聊聊最长上升子序列](https://lucifer.ren/blog/2020/06/20/LIS/),收到了大家的一致好评。今天给大家带来的依然是换皮题 - 最长公共子序列系列。 + +最长公共子序列是一个很经典的算法题。有的会直接让你求最长上升子序列,有的则会换个说法,但最终考察的还是最长公共子序列。那么问题来了,它穿上衣服你还看得出来是么? + +如果你完全看不出来了,说明抽象思维还不到火候。经常看我的题解的同学应该会知道,我经常强调`抽象思维`。没有抽象思维,所有的题目对你来说都是新题。你无法将之前做题的经验迁移到这道题,那你做的题意义何在? + +虽然抽象思维很难练成,但是幸好算法套路是有限的,经常考察的题型更是有限的。从这些入手,或许可以让你轻松一些。本文就从一个经典到不行的题型《最长公共子序列》,来帮你进一步理解`抽象思维`。 + +> 注意。 本文是帮助你识别套路,从横向上理清解题的思维框架,并没有采用最优解,所有的题目给的解法可能不是最优的,但是都可以通过所有的测试用例。如果你想看最优解,可以直接去讨论区看。或者期待我的`深入剖析系列`。 + +## 718. 最长重复子数组 + +### 题目地址 + +https://leetcode-cn.com/problems/maximum-length-of-repeated-subarray/ + +### 题目描述 + +``` +给两个整数数组  A  和  B ,返回两个数组中公共的、长度最长的子数组的长度。 + +示例 1: + +输入: +A: [1,2,3,2,1] +B: [3,2,1,4,7] +输出: 3 +解释: +长度最长的公共子数组是 [3, 2, 1]。 +说明: + +1 <= len(A), len(B) <= 1000 +0 <= A[i], B[i] < 100 +``` + +### 前置知识 + +- 哈希表 +- 数组 +- 二分查找 +- 动态规划 + +### 思路 + +这就是最经典的最长公共子序列问题。一般这种求解**两个数组或者字符串求最大或者最小**的题目都可以考虑动态规划,并且通常都定义 dp[i][j] 为 `以 A[i], B[j] 结尾的 xxx`。这道题就是:`以 A[i], B[j] 结尾的两个数组中公共的、长度最长的子数组的长度`。 + +> 关于状态转移方程的选择可以参考: [穿上衣服我就不认识你了?来聊聊最长上升子序列](https://lucifer.ren/blog/2020/06/20/LIS/) + +算法很简单: + +- 双层循环找出所有的 i, j 组合,时间复杂度 $O(m * n)$,其中 m 和 n 分别为 A 和 B 的 长度。 + - 如果 A[i] == B[j],dp[i][j] = dp[i - 1][j - 1] + 1 + - 否则,dp[i][j] = 0 +- 循环过程记录最大值即可。 + +**记住这个状态转移方程,后面我们还会频繁用到。** + +### 关键点解析 + +- dp 建模套路 + +### 代码 + +代码支持:Python + +Python Code: + +```py +class Solution: + def findLength(self, A, B): + m, n = len(A), len(B) + ans = 0 + dp = [[0 for _ in range(n + 1)] for _ in range(m + 1)] + for i in range(1, m + 1): + for j in range(1, n + 1): + if A[i - 1] == B[j - 1]: + dp[i][j] = dp[i - 1][j - 1] + 1 + ans = max(ans, dp[i][j]) + return ans +``` + +**复杂度分析** + +- 时间复杂度:$O(m * n)$,其中 m 和 n 分别为 A 和 B 的 长度。 +- 空间复杂度:$O(m * n)$,其中 m 和 n 分别为 A 和 B 的 长度。 + +> 二分查找也是可以的,不过并不容易想到,大家可以试试。 + +## 1143.最长公共子序列 + +### 题目地址 + +https://leetcode-cn.com/problems/longest-common-subsequence + +### 题目描述 + +给定两个字符串  text1 和  text2,返回这两个字符串的最长公共子序列的长度。 + +一个字符串的   子序列   是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。 +例如,"ace" 是 "abcde" 的子序列,但 "aec" 不是 "abcde" 的子序列。两个字符串的「公共子序列」是这两个字符串所共同拥有的子序列。 + +若这两个字符串没有公共子序列,则返回 0。 + +示例 1: + +输入:text1 = "abcde", text2 = "ace" +输出:3 +解释:最长公共子序列是 "ace",它的长度为 3。 +示例 2: + +输入:text1 = "abc", text2 = "abc" +输出:3 +解释:最长公共子序列是 "abc",它的长度为 3。 +示例 3: + +输入:text1 = "abc", text2 = "def" +输出:0 +解释:两个字符串没有公共子序列,返回 0。 + +提示: + +1 <= text1.length <= 1000 +1 <= text2.length <= 1000 +输入的字符串只含有小写英文字符。 + +### 前置知识 + +- 数组 +- 动态规划 + +### 思路 + +和上面的题目类似,只不过数组变成了字符串(这个无所谓),子数组(连续)变成了子序列 (非连续)。 + +算法只需要一点小的微调: `如果 A[i] != B[j],那么 dp[i][j] = max(dp[i - 1][j], dp[i][j - 1])` + +### 关键点解析 + +- dp 建模套路 + +### 代码 + +> 你看代码多像 + +代码支持:Python + +Python Code: + +```py +class Solution: + def longestCommonSubsequence(self, A: str, B: str) -> int: + m, n = len(A), len(B) + ans = 0 + dp = [[0 for _ in range(n + 1)] for _ in range(m + 1)] + for i in range(1, m + 1): + for j in range(1, n + 1): + if A[i - 1] == B[j - 1]: + dp[i][j] = dp[i - 1][j - 1] + 1 + ans = max(ans, dp[i][j]) + else: + dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]) + return ans +``` + +**复杂度分析** + +- 时间复杂度:$O(m * n)$,其中 m 和 n 分别为 A 和 B 的 长度。 +- 空间复杂度:$O(m * n)$,其中 m 和 n 分别为 A 和 B 的 长度。 + +## 1035. 不相交的线 + +### 题目地址 + +https://leetcode-cn.com/problems/uncrossed-lines/description/ + +### 题目描述 + +我们在两条独立的水平线上按给定的顺序写下  A  和  B  中的整数。 + +现在,我们可以绘制一些连接两个数字  A[i]  和  B[j]  的直线,只要  A[i] == B[j],且我们绘制的直线不与任何其他连线(非水平线)相交。 + +以这种方法绘制线条,并返回我们可以绘制的最大连线数。 + +示例 1: + +![](https://tva1.sinaimg.cn/large/007S8ZIlly1ggbkku13xuj315x0u0abp.jpg) + +输入:A = [1,4,2], B = [1,2,4] +输出:2 +解释: +我们可以画出两条不交叉的线,如上图所示。 +我们无法画出第三条不相交的直线,因为从 A[1]=4 到 B[2]=4 的直线将与从 A[2]=2 到 B[1]=2 的直线相交。 +示例 2: + +输入:A = [2,5,1,2,5], B = [10,5,2,1,5,2] +输出:3 +示例 3: + +输入:A = [1,3,7,1,7,5], B = [1,9,2,5,1] +输出:2 + +提示: + +1 <= A.length <= 500 +1 <= B.length <= 500 +1 <= A[i], B[i] <= 2000 + +### 前置知识 + +- 数组 +- 动态规划 + +### 思路 + +从图中可以看出,如果想要不相交,则必然相对位置要一致,换句话说就是:**公共子序列**。因此和上面的 `1143.最长公共子序列` 一样,属于换皮题,代码也是一模一样。 + +### 关键点解析 + +- dp 建模套路 + +### 代码 + +> 你看代码多像 + +代码支持:Python + +Python Code: + +```py +class Solution: + def longestCommonSubsequence(self, A: str, B: str) -> int: + m, n = len(A), len(B) + ans = 0 + dp = [[0 for _ in range(n + 1)] for _ in range(m + 1)] + for i in range(1, m + 1): + for j in range(1, n + 1): + if A[i - 1] == B[j - 1]: + dp[i][j] = dp[i - 1][j - 1] + 1 + ans = max(ans, dp[i][j]) + else: + dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]) + return ans +``` + +**复杂度分析** + +- 时间复杂度:$O(m * n)$,其中 m 和 n 分别为 A 和 B 的 长度。 +- 空间复杂度:$O(m * n)$,其中 m 和 n 分别为 A 和 B 的 长度。 + +## 总结 + +第一道是“子串”题型,第二和第三则是“子序列”。不管是“子串”还是“子序列”,状态定义都是一样的,不同的只是一点小细节。 + +**只有熟练掌握基础的数据结构与算法,才能对复杂问题迎刃有余。** 基础算法,把它彻底搞懂,再去面对出题人的各种换皮就不怕了。相反,如果你不去思考题目背后的逻辑,就会刷地很痛苦。题目稍微一变化你就不会了,这也是为什么很多人说**刷了很多题,但是碰到新的题目还是不会做**的原因之一。关注公众号力扣加加,努力用清晰直白的语言还原解题思路,并且有大量图解,手把手教你识别套路,高效刷题。 + +更多题解可以访问我的 LeetCode 题解仓库:https://github.com/azl397985856/leetcode 。 目前已经 30K star 啦。 + +![](https://tva1.sinaimg.cn/large/007S8ZIlly1gfcuzagjalj30p00dwabs.jpg) diff --git a/selected/LIS.md b/selected/LIS.md new file mode 100644 index 0000000..345bca3 --- /dev/null +++ b/selected/LIS.md @@ -0,0 +1,305 @@ +# 穿上衣服我就不认识你了?来聊聊最长上升子序列 + +最长上升子序列是一个很经典的算法题。有的会直接让你求最长上升子序列,有的则会换个说法,但最终考察的还是最长上升子序列。那么问题来了,它穿上衣服你还看得出来是么? + +如果你完全看不出来了,说明抽象思维还不到火候。经常看我的题解的同学应该会知道,我经常强调`抽象思维`。没有抽象思维,所有的题目对你来说都是新题。你无法将之前做题的经验迁移到这道题,那你做的题意义何在? + +虽然抽象思维很难练成,但是幸好算法套路是有限的,经常考察的题型更是有限的。从这些入手,或许可以让你轻松一些。本文就从一个经典到不行的题型《最长上升子序列》,来帮你进一步理解`抽象思维`。 + +> 注意。 本文是帮助你识别套路,从横向上理清解题的思维框架,并没有采用最优解,所有的题目给的解法都不是最优的,但是都可以通过所有的测试用例。如果你想看最优解,可以直接去讨论区看。或者期待我的`深入剖析系列`。 + +## 300. 最长上升子序列 + +### 题目地址 + +https://leetcode-cn.com/problems/longest-increasing-subsequence + +### 题目描述 + +``` +给定一个无序的整数数组,找到其中最长上升子序列的长度。 + +示例: + +输入: [10,9,2,5,3,7,101,18] +输出: 4 +解释: 最长的上升子序列是 [2,3,7,101],它的长度是 4。 +说明: + +可能会有多种最长上升子序列的组合,你只需要输出对应的长度即可。 +你算法的时间复杂度应该为 O(n2) 。 +进阶: 你能将算法的时间复杂度降低到 O(n log n) 吗? + +``` + +### 思路 + +> 美团和华为都考了这个题。 + +题目的意思是让我们从给定数组中挑选若干数字,这些数字满足: `如果 i < j 则 nums[i] < nums[j]`。问:一次可以挑选最多满足条件的数字是多少个。 + +![](https://tva1.sinaimg.cn/large/007S8ZIlly1gfyyu7187bj31ku0igq6f.jpg) + +这种子序列求极值的题目,应该要考虑到贪心或者动态规划。这道题贪心是不可以的,我们考虑动态规划。 + +按照动态规划定义状态的套路,我们有**两种常见**的定义状态的方式: + +- dp[i] : 以 i 结尾(一定包括 i)所能形成的最长上升子序列长度, 答案是 max(dp[i]),其中 i = 0,1,2, ..., n - 1 +- dp[i] : 以 i 结尾(可能包括 i)所能形成的最长上升子序列长度,答案是 dp[-1] (-1 表示最后一个元素) + +容易看出第二种定义方式由于无需比较不同的 dp[i] 就可以获得答案,因此更加方便。但是想了下,状态转移方程会很不好写,因为 dp[i] 的末尾数字(最大的)可能是 任意 j < i 的位置。 + +第一种定义方式虽然需要比较不同的 dp[i] 从而获得结果,但是我们可以在循环的时候顺便得出,对复杂度不会有影响,只是代码多了一点而已。因此我们**选择第一种建模方式**。 + +![](https://tva1.sinaimg.cn/large/007S8ZIlly1gfyyz18gu6j31t40dy77l.jpg) + +由于 dp[j] 中一定会包括 j,且以 j 结尾, 那么 nums[j] 一定是其所形成的序列中最大的元素,那么如果位于其后(意味着 i > j)的 nums[i] > nums[j],那么 nums[i] 一定能够融入 dp[j] 从而形成更大的序列,这个序列的长度是 dp[j] + 1。因此状态转移方程就有了:`dp[i] = dp[j] + 1 (其中 i > j, nums[i] > nums[j])` + +以 `[10, 9, 2, 5, 3, 7, 101, 18]` 为例,当我们计算到 dp[5]的时候,我们需要往回和 0,1,2,3,4 进行比较。 + +![](https://tva1.sinaimg.cn/large/007S8ZIlly1gfzzp18iyej311i0o8dk8.jpg) + +具体的比较内容是: + +![](https://tva1.sinaimg.cn/large/007S8ZIlly1gfzzqeaen1j30um0fwwhd.jpg) + +最后从三个中选一个最大的 + 1 赋给 dp[5]即可。 + +![](https://tva1.sinaimg.cn/large/007S8ZIlly1gfzzt54n5wj30ys05g74x.jpg) + +**记住这个状态转移方程,后面我们还会频繁用到。** + +### 代码 + +```py +class Solution: + def lengthOfLIS(self, nums: List[int]) -> int: + n = len(nums) + if n == 0: return 0 + dp = [1] * n + ans = 1 + for i in range(n): + for j in range(i): + if nums[i] > nums[j]: + dp[i] = max(dp[i], dp[j] + 1) + ans = max(ans, dp[i]) + return ans +``` + +**复杂度分析** + +- 时间复杂度:$O(N ^ 2)$ +- 空间复杂度:$O(N)$ + +## 435. 无重叠区间 + +### 题目地址 + +https://leetcode-cn.com/problems/non-overlapping-intervals/ + +### 题目描述 + +``` +给定一个区间的集合,找到需要移除区间的最小数量,使剩余区间互不重叠。 + +注意: + +可以认为区间的终点总是大于它的起点。 +区间 [1,2] 和 [2,3] 的边界相互“接触”,但没有相互重叠。 +示例 1: + +输入: [ [1,2], [2,3], [3,4], [1,3] ] + +输出: 1 + +解释: 移除 [1,3] 后,剩下的区间没有重叠。 +示例 2: + +输入: [ [1,2], [1,2], [1,2] ] + +输出: 2 + +解释: 你需要移除两个 [1,2] 来使剩下的区间没有重叠。 +示例 3: + +输入: [ [1,2], [2,3] ] + +输出: 0 + +解释: 你不需要移除任何区间,因为它们已经是无重叠的了。 + +``` + +### 思路 + +我们先来看下最终**剩下**的区间。由于剩下的区间都是不重叠的,因此剩下的**相邻区间的后一个区间的开始时间一定是不小于前一个区间的结束时间的**。 比如我们剩下的区间是`[ [1,2], [2,3], [3,4] ]`。就是第一个区间的 2 小于等于 第二个区间的 2,第二个区间的 3 小于等于第三个区间的 3。 + +不难发现如果我们将`前面区间的结束`和`后面区间的开始`结合起来看,其就是一个**非严格递增序列**。而我们的目标就是删除若干区间,从而**剩下最长的非严格递增子序列**。这不就是上面的题么?只不过上面是严格递增,这不重要,就是改个符号的事情。 上面的题你可以看成是删除了若干数字,然后剩下**剩下最长的严格递增子序列**。 **这就是抽象的力量,这就是套路。** + +如果对区间按照起点或者终点进行排序,那么就转化为上面的最长递增子序列问题了。和上面问题不同的是,由于是一个区间。因此实际上,我们是需要拿**后面的开始时间**和**前面的结束时间**进行比较。 + +![](https://tva1.sinaimg.cn/large/007S8ZIlly1gfyzp8n59cj31000a2jse.jpg) + +而由于: + +- 题目求的是需要移除的区间,因此最后 return 的时候需要做一个转化。 +- 题目不是要求严格递增,而是可以相等,因此我们的判断条件要加上等号。 + +> 这道题还有一种贪心的解法,其效率要比动态规划更好,但由于和本文的主题不一致,就不在这里讲了。 + +### 代码 + +**你看代码多像** + +```py +class Solution: + def eraseOverlapIntervals(self, intervals: List[List[int]]) -> int: + n = len(intervals) + if n == 0: return 0 + dp = [1] * n + ans = 1 + intervals.sort(key=lambda a: a[1]) + + for i in range(len(intervals)): + for j in range(i - 1, -1, -1): + if intervals[i][0] >= intervals[j][1]: + dp[i] = max(dp[i], dp[j] + 1) + # 由于我事先进行了排序,因此倒着找的时候,找到的第一个一定是最大的数,因此不用往前继续找了。 + # 这也是为什么我按照结束时间排序的原因。 + break + dp[i] = max(dp[i], dp[i - 1]) + ans = max(ans, dp[i]) + + return n - ans +``` + +**复杂度分析** + +- 时间复杂度:$O(N ^ 2)$ +- 空间复杂度:$O(N)$ + +## 646. 最长数对链 + +### 题目地址 + +https://leetcode-cn.com/problems/maximum-length-of-pair-chain/ + +### 题目描述 + +``` +给出 n 个数对。 在每一个数对中,第一个数字总是比第二个数字小。 + +现在,我们定义一种跟随关系,当且仅当 b < c 时,数对(c, d) 才可以跟在 (a, b) 后面。我们用这种形式来构造一个数对链。 + +给定一个对数集合,找出能够形成的最长数对链的长度。你不需要用到所有的数对,你可以以任何顺序选择其中的一些数对来构造。 + +示例 : + +输入: [[1,2], [2,3], [3,4]] +输出: 2 +解释: 最长的数对链是 [1,2] -> [3,4] +注意: + +给出数对的个数在 [1, 1000] 范围内。 + +``` + +### 思路 + +和上面的`435. 无重叠区间`是换皮题,唯一的区别这里又变成了严格增加。没关系,我们把等号去掉就行了。并且由于这道题求解的是最长的长度,因此转化也不需要了。 + +> 当然,这道题也有一种贪心的解法,其效率要比动态规划更好,但由于和本文的主题不一致,就不在这里讲了。 + +### 代码 + +**这代码更像了!** + +```py +class Solution: + def findLongestChain(self, pairs: List[List[int]]) -> int: + n = len(pairs) + dp = [1] * n + ans = 1 + pairs.sort(key=lambda a: a[0]) + for i in range(n): + for j in range(i): + if pairs[i][0] > pairs[j][1]: + dp[i] = max(dp[i], dp[j] + 1) + ans = max(ans, dp[i]) + return ans +``` + +**复杂度分析** + +- 时间复杂度:$O(N ^ 2)$ +- 空间复杂度:$O(N)$ + +## 452. 用最少数量的箭引爆气球 + +### 题目地址 + +https://leetcode-cn.com/problems/minimum-number-of-arrows-to-burst-balloons/ + +### 题目描述 + +``` +在二维空间中有许多球形的气球。对于每个气球,提供的输入是水平方向上,气球直径的开始和结束坐标。由于它是水平的,所以y坐标并不重要,因此只要知道开始和结束的x坐标就足够了。开始坐标总是小于结束坐标。平面内最多存在104个气球。 + +一支弓箭可以沿着x轴从不同点完全垂直地射出。在坐标x处射出一支箭,若有一个气球的直径的开始和结束坐标为 xstart,xend, 且满足  xstart ≤ x ≤ xend,则该气球会被引爆。可以射出的弓箭的数量没有限制。 弓箭一旦被射出之后,可以无限地前进。我们想找到使得所有气球全部被引爆,所需的弓箭的最小数量。 + +Example: + +输入: +[[10,16], [2,8], [1,6], [7,12]] + +输出: +2 + +解释: +对于该样例,我们可以在x = 6(射爆[2,8],[1,6]两个气球)和 x = 11(射爆另外两个气球)。 + +``` + +### 思路 + +把气球看成区间,几个箭可以全部射爆,意思就是有多少不重叠的区间。注意这里重叠的情况也可以射爆。这么一抽象,就和上面的`646. 最长数对链`一模一样了,不用我多说了吧? + +> 当然,这道题也有一种贪心的解法,其效率要比动态规划更好,但由于和本文的主题不一致,就不在这里讲了。 + +### 代码 + +**代码像不像?** + +```py +class Solution: + def findMinArrowShots(self, points: List[List[int]]) -> int: + n = len(points) + if n == 0: return 0 + dp = [1] * n + cnt = 1 + points.sort(key=lambda a:a[1]) + + for i in range(n): + for j in range(0, i): + if points[i][0] > points[j][1]: + dp[i] = max(dp[i], dp[j] + 1) + cnt = max(cnt, dp[i]) + return cnt +``` + +**复杂度分析** + +- 时间复杂度:$O(N ^ 2)$ +- 空间复杂度:$O(N)$ + +## More + +其他的我就不一一说了。比如 [673. 最长递增子序列的个数](https://leetcode-cn.com/problems/number-of-longest-increasing-subsequence/) (滴滴面试题)。 不就是求出最长序列,之后再循环比对一次就可以得出答案了么? + +[491. 递增子序列](https://leetcode-cn.com/problems/increasing-subsequences/) 由于需要找到所有的递增子序列,因此动态规划就不行了,妥妥回溯就行了,套一个模板就出来了。回溯的模板可以看我之前写的[回溯专题](https://github.com/azl397985856/leetcode/blob/master/problems/90.subsets-ii.md "回溯专题")。 + +大家把我讲的思路搞懂,这几个题一写,还怕碰到类似的题不会么?**只有熟练掌握基础的数据结构与算法,才能对复杂问题迎刃有余。** 最长上升子序列就是一个非常经典的基础算法,把它彻底搞懂,再去面对出题人的各种换皮就不怕了。相反,如果你不去思考题目背后的逻辑,就会刷地很痛苦。题目稍微一变化你就不会了,这也是为什么很多人说**刷了很多题,但是碰到新的题目还是不会做**的原因之一。关注公众号力扣加加,努力用清晰直白的语言还原解题思路,并且有大量图解,手把手教你识别套路,高效刷题。 + +更多题解可以访问我的 LeetCode 题解仓库:https://github.com/azl397985856/leetcode 。 目前已经 30K star 啦。 + +![](https://tva1.sinaimg.cn/large/007S8ZIlly1gfcuzagjalj30p00dwabs.jpg) diff --git a/selected/LSS.md b/selected/LSS.md new file mode 100644 index 0000000..2bed9d6 --- /dev/null +++ b/selected/LSS.md @@ -0,0 +1,387 @@ +# 一文看懂《最大子序列和问题》 + +最大子序列和是一道经典的算法题, leetcode 也有原题《53.maximum-sum-subarray》,今天我们就来彻底攻克它。 + +## 题目描述 + +求取数组中最大连续子序列和,例如给定数组为 A = [1, 3, -2, 4, -5], 则最大连续子序列和为 6,即 1 + 3 +(-2)+ 4 = 6。 +去 + +首先我们来明确一下题意。 + +- 题目说的子数组是连续的 +- 题目只需要求和,不需要返回子数组的具体位置。 +- 数组中的元素是整数,但是可能是正数,负数和 0。 +- 子序列的最小长度为 1。 + +比如: + +- 对于数组 [1, -2, 3, 5, -3, 2], 应该返回 3 + 5 = 8 +- 对于数组 [0, -2, 3, 5, -1, 2], 应该返回 3 + 5 + -1 + 2 = 9 +- 对于数组 [-9, -2, -3, -5, -3], 应该返回 -2 + +## 解法一 - 暴力法(超时法) + +一般情况下,先从暴力解分析,然后再进行一步步的优化。 + +### 思路 + +我们来试下最直接的方法,就是计算所有的子序列的和,然后取出最大值。 +记 Sum[i,....,j]为数组 A 中第 i 个元素到第 j 个元素的和,其中 0 <= i <= j < n, +遍历所有可能的 Sum[i,....,j] 即可。 + +我们去枚举以 0,1,2...n-1 开头的所有子序列即可, +对于每一个开头的子序列,我们都去枚举从当前开始到 n-1 的所有情况。 + +这种做法的时间复杂度为 O(N^2), 空间复杂度为 O(1)。 + +### 代码 + +JavaScript: + +```js +function LSS(list) { + const len = list.length; + let max = -Number.MAX_VALUE; + let sum = 0; + for (let i = 0; i < len; i++) { + sum = 0; + for (let j = i; j < len; j++) { + sum += list[j]; + if (sum > max) { + max = sum; + } + } + } + + return max; +} +``` + +Java: + +```java +class MaximumSubarrayPrefixSum { + public int maxSubArray(int[] nums) { + int len = nums.length; + int maxSum = Integer.MIN_VALUE; + int sum = 0; + for (int i = 0; i < len; i++) { + sum = 0; + for (int j = i; j < len; j++) { + sum += nums[j]; + maxSum = Math.max(maxSum, sum); + } + } + return maxSum; + } +} +``` + +Python 3: + +```python +import sys +class Solution: + def maxSubArray(self, nums: List[int]) -> int: + n = len(nums) + maxSum = -sys.maxsize + sum = 0 + for i in range(n): + sum = 0 + for j in range(i, n): + sum += nums[j] + maxSum = max(maxSum, sum) + + return maxSum + +``` + +空间复杂度非常理想,但是时间复杂度有点高。怎么优化呢?我们来看下下一个解法。 + +## 解法二 - 分治法 + +### 思路 + +我们来分析一下这个问题, 我们先把数组平均分成左右两部分。 + +此时有三种情况: + +- 最大子序列全部在数组左部分 +- 最大子序列全部在数组右部分 +- 最大子序列横跨左右数组 + +对于前两种情况,我们相当于将原问题转化为了规模更小的同样问题。 + +对于第三种情况,由于已知循环的起点(即中点),我们只需要进行一次循环,分别找出 +左边和右边的最大子序列即可。 + +所以一个思路就是我们每次都对数组分成左右两部分,然后分别计算上面三种情况的最大子序列和, +取出最大的即可。 + +举例说明,如下图: + +![](https://tva1.sinaimg.cn/large/007S8ZIlly1gds543yp2cj31400u0myf.jpg) +(by [snowan](https://github.com/snowan)) + +这种做法的时间复杂度为 O(N\*logN), 空间复杂度为 O(1)。 + +### 代码 + +JavaScript: + +```js +function helper(list, m, n) { + if (m === n) return list[m]; + let sum = 0; + let lmax = -Number.MAX_VALUE; + let rmax = -Number.MAX_VALUE; + const mid = ((n - m) >> 1) + m; + const l = helper(list, m, mid); + const r = helper(list, mid + 1, n); + for (let i = mid; i >= m; i--) { + sum += list[i]; + if (sum > lmax) lmax = sum; + } + + sum = 0; + + for (let i = mid + 1; i <= n; i++) { + sum += list[i]; + if (sum > rmax) rmax = sum; + } + + return Math.max(l, r, lmax + rmax); +} + +function LSS(list) { + return helper(list, 0, list.length - 1); +} +``` + +Java: + +```java +class MaximumSubarrayDivideConquer { + public int maxSubArrayDividConquer(int[] nums) { + if (nums == null || nums.length == 0) return 0; + return helper(nums, 0, nums.length - 1); + } + private int helper(int[] nums, int l, int r) { + if (l > r) return Integer.MIN_VALUE; + int mid = (l + r) >>> 1; + int left = helper(nums, l, mid - 1); + int right = helper(nums, mid + 1, r); + int leftMaxSum = 0; + int sum = 0; + // left surfix maxSum start from index mid - 1 to l + for (int i = mid - 1; i >= l; i--) { + sum += nums[i]; + leftMaxSum = Math.max(leftMaxSum, sum); + } + int rightMaxSum = 0; + sum = 0; + // right prefix maxSum start from index mid + 1 to r + for (int i = mid + 1; i <= r; i++) { + sum += nums[i]; + rightMaxSum = Math.max(sum, rightMaxSum); + } + // max(left, right, crossSum) + return Math.max(leftMaxSum + rightMaxSum + nums[mid], Math.max(left, right)); + } +} + +``` + +Python 3 : + +```python +import sys +class Solution: + def maxSubArray(self, nums: List[int]) -> int: + return self.helper(nums, 0, len(nums) - 1) + def helper(self, nums, l, r): + if l > r: + return -sys.maxsize + mid = (l + r) // 2 + left = self.helper(nums, l, mid - 1) + right = self.helper(nums, mid + 1, r) + left_suffix_max_sum = right_prefix_max_sum = 0 + sum = 0 + for i in reversed(range(l, mid)): + sum += nums[i] + left_suffix_max_sum = max(left_suffix_max_sum, sum) + sum = 0 + for i in range(mid + 1, r + 1): + sum += nums[i] + right_prefix_max_sum = max(right_prefix_max_sum, sum) + cross_max_sum = left_suffix_max_sum + right_prefix_max_sum + nums[mid] + return max(cross_max_sum, left, right) + +``` + +## 解法三 - 动态规划 + +### 思路 + +我们来思考一下这个问题, 看能不能将其拆解为规模更小的同样问题,并且能找出 +递推关系。 + +我们不妨假设问题 Q(list, i) 表示 list 中以索引 i 结尾的情况下最大子序列和, +那么原问题就转化为 Q(list, i), 其中 i = 0,1,2...n-1 中的最大值。 + +我们继续来看下递归关系,即 Q(list, i)和 Q(list, i - 1)的关系, +即如何根据 Q(list, i - 1) 推导出 Q(list, i)。 + +如果已知 Q(list, i - 1), 我们可以将问题分为两种情况,即以索引为 i 的元素终止, +或者只有一个索引为 i 的元素。 + +- 如果以索引为 i 的元素终止, 那么就是 Q(list, i - 1) + list[i] +- 如果只有一个索引为 i 的元素,那么就是 list[i] + +分析到这里,递推关系就很明朗了,即`Q(list, i) = Math.max(0, Q(list, i - 1)) + list[i]` + +举例说明,如下图: + +![53.maximum-sum-subarray-dp.png](https://tva1.sinaimg.cn/large/007S8ZIlly1gds544xidoj30pj0h2wew.jpg) +(by [snowan](https://github.com/snowan)) + +这种算法的时间复杂度 O(N), 空间复杂度为 O(1) + +### 代码 + +JavaScript: + +```js +function LSS(list) { + const len = list.length; + let max = list[0]; + for (let i = 1; i < len; i++) { + list[i] = Math.max(0, list[i - 1]) + list[i]; + if (list[i] > max) max = list[i]; + } + + return max; +} +``` + +Java: + +```java +class MaximumSubarrayDP { + public int maxSubArray(int[] nums) { + int currMaxSum = nums[0]; + int maxSum = nums[0]; + for (int i = 1; i < nums.length; i++) { + currMaxSum = Math.max(currMaxSum + nums[i], nums[i]); + maxSum = Math.max(maxSum, currMaxSum); + } + return maxSum; + } +} + +``` + +Python 3: + +```python +class Solution: + def maxSubArray(self, nums: List[int]) -> int: + n = len(nums) + max_sum_ending_curr_index = max_sum = nums[0] + for i in range(1, n): + max_sum_ending_curr_index = max(max_sum_ending_curr_index + nums[i], nums[i]) + max_sum = max(max_sum_ending_curr_index, max_sum) + + return max_sum + +``` + +## 解法四 - 数学分析 + +### 思路 + +我们来通过数学分析来看一下这个题目。 + +我们定义函数 S(i) ,它的功能是计算以 0(包括 0)开始加到 i(包括 i)的值。 + +那么 S(j) - S(i - 1) 就等于 从 i 开始(包括 i)加到 j(包括 j)的值。 + +我们进一步分析,实际上我们只需要遍历一次计算出所有的 S(i), 其中 i 等于 0,1,2....,n-1。 +然后我们再减去之前的 S(k),其中 k 等于 0,1,i - 1,中的最小值即可。 因此我们需要 +用一个变量来维护这个最小值,还需要一个变量维护最大值。 + +这种算法的时间复杂度 O(N), 空间复杂度为 O(1)。 + +其实很多题目,都有这样的思想, 比如之前的《每日一题 - 电梯问题》。 + +### 代码 + +JavaScript: + +```js +function LSS(list) { + const len = list.length; + let max = list[0]; + let min = 0; + let sum = 0; + for (let i = 0; i < len; i++) { + sum += list[i]; + if (sum - min > max) max = sum - min; + if (sum < min) { + min = sum; + } + } + + return max; +} +``` + +Java: + +```java +class MaxSumSubarray { + public int maxSubArray3(int[] nums) { + int maxSum = nums[0]; + int sum = 0; + int minSum = 0; + for (int num : nums) { + // prefix Sum + sum += num; + // update maxSum + maxSum = Math.max(maxSum, sum - minSum); + // update minSum + minSum = Math.min(minSum, sum); + } + return maxSum; + } +} + +``` + +Python 3: + +```python +class Solution: + def maxSubArray(self, nums: List[int]) -> int: + n = len(nums) + maxSum = nums[0] + minSum = sum = 0 + for i in range(n): + sum += nums[i] + maxSum = max(maxSum, sum - minSum) + minSum = min(minSum, sum) + + return maxSum + +``` + +## 总结 + +我们使用四种方法解决了`《最大子序列和问题》`, +并详细分析了各个解法的思路以及复杂度,相信下次你碰到相同或者类似的问题 +的时候也能够发散思维,做到`一题多解,多题一解`。 + +实际上,我们只是求出了最大的和,如果题目进一步要求出最大子序列和的子序列呢? +如果要题目允许不连续呢? 我们又该如何思考和变通?如何将数组改成二维,求解最大矩阵和怎么计算? +这些问题留给读者自己来思考。 diff --git a/selected/a-deleted.md b/selected/a-deleted.md new file mode 100644 index 0000000..20df2e5 --- /dev/null +++ b/selected/a-deleted.md @@ -0,0 +1,385 @@ +# 一招吃遍力扣四道题,妈妈再也不用担心我被套路啦~ + + +我花了几天时间,从力扣中精选了四道相同思想的题目,来帮助大家解套,如果觉得文章对你有用,记得点赞分享,让我看到你的认可,有动力继续做下去。 + +这就是接下来要给大家讲的四个题,其中 1081 和 316 题只是换了说法而已。 + +- [316. 去除重复字母](https://leetcode-cn.com/problems/remove-duplicate-letters/)(困难) +- [321. 拼接最大数](https://leetcode-cn.com/problems/create-maximum-number/)(困难) +- [402. 移掉 K 位数字](https://leetcode-cn.com/problems/remove-k-digits/)(中等) +- [1081. 不同字符的最小子序列](https://leetcode-cn.com/problems/smallest-subsequence-of-distinct-characters/)(中等) + +## 402. 移掉 K 位数字(中等) + +我们从一个简单的问题入手,识别一下这种题的基本形式和套路,为之后的三道题打基础。 + +### 题目描述 + +``` +给定一个以字符串表示的非负整数  num,移除这个数中的 k 位数字,使得剩下的数字最小。 + +注意: + +num 的长度小于 10002 且  ≥ k。 +num 不会包含任何前导零。 + + +示例 1 : + +输入: num = "1432219", k = 3 +输出: "1219" +解释: 移除掉三个数字 4, 3, 和 2 形成一个新的最小的数字 1219。 +示例 2 : + +输入: num = "10200", k = 1 +输出: "200" +解释: 移掉首位的 1 剩下的数字为 200. 注意输出不能有任何前导零。 +示例 3 : + +输入: num = "10", k = 2 +输出: "0" +解释: 从原数字移除所有的数字,剩余为空就是 0。 + +``` + +### 前置知识 + +- 数学 + +### 思路 + +这道题让我们从一个字符串数字中删除 k 个数字,使得剩下的数最小。也就说,我们要保持原来的数字的相对位置不变。 + +以题目中的 `num = 1432219, k = 3` 为例,我们需要返回一个长度为 4 的字符串,问题在于: 我们怎么才能求出这四个位置依次是什么呢? + +![](https://tva1.sinaimg.cn/large/007S8ZIlly1gfr0o3bz8aj30ya0he75v.jpg) + +(图 1) + +暴力法的话,我们需要枚举`C_n^(n - k)` 种序列(其中 n 为数字长度),并逐个比较最大。这个时间复杂度是指数级别的,必须进行优化。 + +一个思路是: + +- 从左到右遍历 +- 对于每一个遍历到的元素,我们决定是**丢弃**还是**保留** + +问题的关键是:我们怎么知道,一个元素是应该保留还是丢弃呢? + +这里有一个前置知识:**对于两个数 123a456 和 123b456,如果 a > b, 那么数字 123a456 大于 数字 123b456,否则数字 123a456 小于等于数字 123b456**。也就说,两个**相同位数**的数字大小关系取决于第一个不同的数的大小。 + +因此我们的思路就是: + +- 从左到右遍历 +- 对于遍历到的元素,我们选择保留。 +- 但是我们可以选择性丢弃前面相邻的元素。 +- 丢弃与否的依据如上面的前置知识中阐述中的方法。 + +以题目中的 `num = 1432219, k = 3` 为例的图解过程如下: + +![](https://tva1.sinaimg.cn/large/007S8ZIlly1gfr3me4mltj30u00xjgp5.jpg) + +(图 2) + +由于没有左侧相邻元素,因此**没办法丢弃**。 + +![](https://tva1.sinaimg.cn/large/007S8ZIlly1gfr3p4idahj30sk116dj7.jpg) + +(图 3) + +由于 4 比左侧相邻的 1 大。如果选择丢弃左侧的 1,那么会使得剩下的数字更大(开头的数从 1 变成了 4)。因此我们仍然选择**不丢弃**。 + +![](https://tva1.sinaimg.cn/large/007S8ZIlly1gfr3rtp1b1j30tk12etcr.jpg) + +(图 4) + +由于 3 比左侧相邻的 4 小。 如果选择丢弃左侧的 4,那么会使得剩下的数字更小(开头的数从 4 变成了 3)。因此我们选择**丢弃**。 + +。。。 + +后面的思路类似,我就不继续分析啦。 + +然而需要注意的是,如果给定的数字是一个单调递增的数字,那么我们的算法会永远**选择不丢弃**。这个题目中要求的,我们要永远确保**丢弃** k 个矛盾。 + +一个简单的思路就是: + +- 每次丢弃一次,k 减去 1。当 k 减到 0 ,我们可以提前终止遍历。 +- 而当遍历完成,如果 k 仍然大于 0。不妨假设最终还剩下 x 个需要丢弃,那么我们需要选择删除末尾 x 个元素。 + +上面的思路可行,但是稍显复杂。 + +![](https://tva1.sinaimg.cn/large/007S8ZIlly1gfk7m9z3elj30zk0i01kx.jpg) +(图 5) + +我们需要把思路逆转过来。刚才我的关注点一直是**丢弃**,题目要求我们丢弃 k 个。反过来说,不就是让我们保留 $n - k$ 个元素么?其中 n 为数字长度。 那么我们只需要按照上面的方法遍历完成之后,再截取前**n - k**个元素即可。 + +按照上面的思路,我们来选择数据结构。由于我们需要**保留**和**丢弃相邻**的元素,因此使用栈这种在一端进行添加和删除的数据结构是再合适不过了,我们来看下代码实现。 + +### 代码(Python) + +```py +class Solution(object): + def removeKdigits(self, num, k): + stack = [] + remain = len(num) - k + for digit in num: + while k and stack and stack[-1] > digit: + stack.pop() + k -= 1 + stack.append(digit) + return ''.join(stack[:remain]).lstrip('0') or '0' +``` + +**_复杂度分析_** + +- 时间复杂度:虽然内层还有一个 while 循环,但是由于每个数字最多仅会入栈出栈一次,因此时间复杂度仍然为 $O(N)$,其中 $N$ 为数字长度。 +- 空间复杂度:我们使用了额外的栈来存储数字,因此空间复杂度为 $O(N)$,其中 $N$ 为数字长度。 + +> 提示: 如果题目改成求删除 k 个字符之后的最大数,我们只需要将 stack[-1] > digit 中的大于号改成小于号即可。 + +## 316. 去除重复字母(困难) + +### 题目描述 + +``` +给你一个仅包含小写字母的字符串,请你去除字符串中重复的字母,使得每个字母只出现一次。需保证返回结果的字典序最小(要求不能打乱其他字符的相对位置)。 + +示例 1: + +输入: "bcabc" +输出: "abc" +示例 2: + +输入: "cbacdcbc" +输出: "acdb" +``` + +## 前置知识 + +- 字典序 +- 数学 + +### 思路 + +与上面题目不同,这道题没有一个全局的删除次数 k。而是对于每一个在字符串 s 中出现的字母 c 都有一个 k 值。这个 k 是 c 出现次数 - 1。 + +沿用上面的知识的话,我们首先要做的就是计算每一个字符的 k,可以用一个字典来描述这种关系,其中 key 为 字符 c,value 为其出现的次数。 + +具体算法: + +- 建立一个字典。其中 key 为 字符 c,value 为其出现的剩余次数。 +- 从左往右遍历字符串,每次遍历到一个字符,其剩余出现次数 - 1. +- 对于每一个字符,如果其对应的剩余出现次数大于 1,我们**可以**选择丢弃(也可以选择不丢弃),否则不可以丢弃。 +- 是否丢弃的标准和上面题目类似。如果栈中相邻的元素字典序更大,那么我们选择丢弃相邻的栈中的元素。 + +还记得上面题目的边界条件么?如果栈中剩下的元素大于 $n - k$,我们选择截取前 $n - k$ 个数字。然而本题中的 k 是分散在各个字符中的,因此这种思路不可行的。 + +不过不必担心。由于题目是要求只出现一次。我们可以在遍历的时候简单地判断其是否在栈上即可。 + +代码: + +```py +class Solution: + def removeDuplicateLetters(self, s) -> int: + stack = [] + remain_counter = collections.Counter(s) + + for c in s: + if c not in stack: + while stack and c < stack[-1] and remain_counter[stack[-1]] > 0: + stack.pop() + stack.append(c) + remain_counter[c] -= 1 + return ''.join(stack) +``` + +**_复杂度分析_** + +- 时间复杂度:由于判断当前字符是否在栈上存在需要 $O(N)$ 的时间,因此总的时间复杂度就是 $O(N ^ 2)$,其中 $N$ 为字符串长度。 +- 空间复杂度:我们使用了额外的栈来存储数字,因此空间复杂度为 $O(N)$,其中 $N$ 为字符串长度。 + +查询给定字符是否在一个序列中存在的方法。根本上来说,有两种可能: + +- 有序序列: 可以二分法,时间复杂度大致是 $O(N)$。 +- 无序序列: 可以使用遍历的方式,最坏的情况下时间复杂度为 $O(N)$。我们也可以使用空间换时间的方式,使用 $N$的空间 换取 $O(1)$的时间复杂度。 + +由于本题中的 stack 并不是有序的,因此我们的优化点考虑空间换时间。而由于每种字符仅可以出现一次,这里使用 hashset 即可。 + +### 代码(Python) + +```py +class Solution: + def removeDuplicateLetters(self, s) -> int: + stack = [] + seen = set() + remain_counter = collections.Counter(s) + + for c in s: + if c not in seen: + while stack and c < stack[-1] and remain_counter[stack[-1]] > 0: + seen.discard(stack.pop()) + seen.add(c) + stack.append(c) + remain_counter[c] -= 1 + return ''.join(stack) +``` + +**_复杂度分析_** + +- 时间复杂度:$O(N)$,其中 $N$ 为字符串长度。 +- 空间复杂度:我们使用了额外的栈和 hashset,因此空间复杂度为 $O(N)$,其中 $N$ 为字符串长度。 + +> LeetCode 《1081. 不同字符的最小子序列》 和本题一样,不再赘述。 + +## 321. 拼接最大数(困难) + +### 题目描述 + +``` +给定长度分别为  m  和  n  的两个数组,其元素由  0-9  构成,表示两个自然数各位上的数字。现在从这两个数组中选出 k (k <= m + n)  个数字拼接成一个新的数,要求从同一个数组中取出的数字保持其在原数组中的相对顺序。 + +求满足该条件的最大数。结果返回一个表示该最大数的长度为  k  的数组。 + +说明: 请尽可能地优化你算法的时间和空间复杂度。 + +示例  1: + +输入: +nums1 = [3, 4, 6, 5] +nums2 = [9, 1, 2, 5, 8, 3] +k = 5 +输出: +[9, 8, 6, 5, 3] +示例 2: + +输入: +nums1 = [6, 7] +nums2 = [6, 0, 4] +k = 5 +输出: +[6, 7, 6, 0, 4] +示例 3: + +输入: +nums1 = [3, 9] +nums2 = [8, 9] +k = 3 +输出: +[9, 8, 9] +``` + +### 前置知识 + +- 分治 +- 数学 + +### 思路 + +和第一道题类似,只不不过这一次是两个**数组**,而不是一个,并且是求最大数。 + +最大最小是无关紧要的,关键在于是两个数组,并且要求从两个数组选取的元素个数加起来一共是 k。 + +然而在一个数组中取 k 个数字,并保持其最小(或者最大),我们已经会了。但是如果问题扩展到两个,会有什么变化呢? + +实际上,问题本质并没有发生变化。 假设我们从 nums1 中取了 k1 个,从 num2 中取了 k2 个,其中 k1 + k2 = k。而 k1 和 k2 这 两个子问题我们是会解决的。由于这两个子问题是相互独立的,因此我们只需要分别求解,然后将结果合并即可。 + +假如 k1 和 k2 个数字,已经取出来了。那么剩下要做的就是将这个长度分别为 k1 和 k2 的数字,合并成一个长度为 k 的数组合并成一个最大的数组。 + +以题目的 `nums1 = [3, 4, 6, 5] nums2 = [9, 1, 2, 5, 8, 3] k = 5` 为例。 假如我们从 num1 中取出 1 个数字,那么就要从 nums2 中取出 4 个数字。 + +运用第一题的方法,我们计算出应该取 nums1 的 [6],并取 nums2 的 [9,5,8,3]。 如何将 [6] 和 [9,5,8,3],使得数字尽可能大,并且保持相对位置不变呢? + +实际上这个过程有点类似`归并排序`中的**治**,而上面我们分别计算 num1 和 num2 的最大数的过程类似`归并排序`中的**分**。 + +![](https://tva1.sinaimg.cn/large/007S8ZIlly1gfruuvyrn5j31mk0i8414.jpg) +(图 6) + +代码: + +> 我们将从 num1 中挑选的 k1 个数组成的数组称之为 A,将从 num2 中挑选的 k2 个数组成的数组称之为 B, + +```py +def merge(A, B): + ans = [] + while A or B: + bigger = A if A > B else B + ans.append(bigger[0]) + bigger.pop(0) + return ans + +``` + +这里需要说明一下。 在很多编程语言中:**如果 A 和 B 是两个数组,当前仅当 A 的首个元素字典序大于 B 的首个元素,A > B 返回 true,否则返回 false**。 + +比如: + +``` +A = [1,2] +B = [2] +A < B # True + +A = [1,2] +B = [1,2,3] +A < B # False +``` + +以合并 [6] 和 [9,5,8,3] 为例,图解过程如下: + +![](https://tva1.sinaimg.cn/large/007S8ZIlly1gfruxjfwlhj31cu0u07c0.jpg) +(图 7) + +具体算法: + +- 从 nums1 中 取 $min(i, len(nums1))$ 个数形成新的数组 A(取的逻辑同第一题),其中 i 等于 0,1,2, ... k。 +- 从 nums2 中 对应取 $min(j, len(nums2))$ 个数形成新的数组 B(取的逻辑同第一题),其中 j 等于 k - i。 +- 将 A 和 B 按照上面的 merge 方法合并 +- 上面我们暴力了 k 种组合情况,我们只需要将 k 种情况取出最大值即可。 + +### 代码(Python) + +```py +class Solution: + def maxNumber(self, nums1, nums2, k): + + def pick_max(nums, k): + stack = [] + drop = len(nums) - k + for num in nums: + while drop and stack and stack[-1] < num: + stack.pop() + drop -= 1 + stack.append(num) + return stack[:k] + + def merge(A, B): + ans = [] + while A or B: + bigger = A if A > B else B + ans.append(bigger[0]) + bigger.pop(0) + return ans + + return max(merge(pick_max(nums1, i), pick_max(nums2, k-i)) for i in range(k+1) if i <= len(nums1) and k-i <= len(nums2)) +``` + +**_复杂度分析_** + +- 时间复杂度:pick_max 的时间复杂度为 $O(M + N)$ ,其中 $M$ 为 nums1 的长度,$N$ 为 nums2 的长度。 merge 的时间复杂度为 $O(k)$,再加上外层遍历所有的 k 中可能性。因此总的时间复杂度为 $O(k^2 * (M + N))$。 +- 空间复杂度:我们使用了额外的 stack 和 ans 数组,因此空间复杂度为 $O(max(M, N, k))$,其中 $M$ 为 nums1 的长度,$N$ 为 nums2 的长度。 + +## 总结 + +这四道题都是删除或者保留若干个字符,使得剩下的数字最小(或最大)或者字典序最小(或最大)。而解决问题的前提是要有一定**数学前提**。而基于这个数学前提,我们贪心地删除栈中相邻的字符。如果你会了这个套路,那么这四个题目应该都可以轻松解决。 + +`316. 去除重复字母(困难)`,我们使用 hashmap 代替了数组的遍历查找,属于典型的空间换时间方式,可以认识到数据结构的灵活使用是多么的重要。背后的思路是怎么样的?为什么想到空间换时间的方式,我在文中也进行了详细的说明,这都是值得大家思考的问题。然而实际上,这些题目中使用的栈也都是空间换时间的思想。大家下次碰到**需要空间换取时间**的场景,是否能够想到本文给大家介绍的**栈**和**哈希表**呢? + +`321. 拼接最大数(困难)`则需要我们能够对问题进行分解,这绝对不是一件简单的事情。但是对难以解决的问题进行分解是一种很重要的技能,希望大家能够通过这道题加深这种**分治**思想的理解。 大家可以结合我之前写过的几个题解练习一下,它们分别是: + +- [【简单易懂】归并排序(Python)](https://leetcode-cn.com/problems/shu-zu-zhong-de-ni-xu-dui-lcof/solution/jian-dan-yi-dong-gui-bing-pai-xu-python-by-azl3979/) +- [一文看懂《最大子序列和问题》](https://lucifer.ren/blog/2019/12/11/LSS/) + +更多题解可以访问我的 LeetCode 题解仓库:https://github.com/azl397985856/leetcode 。 目前已经 30K star 啦。 + +大家也可以关注我的公众号《力扣加加》获取更多更新鲜的 LeetCode 题解 + +![](https://tva1.sinaimg.cn/large/007S8ZIlly1gfcuzagjalj30p00dwabs.jpg) diff --git a/selected/atMostK.md b/selected/atMostK.md new file mode 100644 index 0000000..2ea0189 --- /dev/null +++ b/selected/atMostK.md @@ -0,0 +1,622 @@ +# 【西法带你学算法】一次搞定前缀和 + +我花了几天时间,从力扣中精选了五道相同思想的题目,来帮助大家解套,如果觉得文章对你有用,记得点赞分享,让我看到你的认可,有动力继续做下去。 + +- [467. 环绕字符串中唯一的子字符串](https://leetcode-cn.com/problems/unique-substrings-in-wraparound-string/ "467. 环绕字符串中唯一的子字符串")(中等) +- [795. 区间子数组个数](https://leetcode-cn.com/problems/number-of-subarrays-with-bounded-maximum/ "795. 区间子数组个数")(中等) +- [904. 水果成篮](https://leetcode-cn.com/problems/fruit-into-baskets/ "904. 水果成篮")(中等) +- [992. K 个不同整数的子数组](https://leetcode-cn.com/problems/subarrays-with-k-different-integers/ "992. K 个不同整数的子数组")(困难) +- [1109. 航班预订统计](https://leetcode-cn.com/problems/corporate-flight-bookings/ "1109. 航班预订统计")(中等) + +前四道题都是滑动窗口的子类型,我们知道滑动窗口适合在题目要求连续的情况下使用, 而[前缀和](https://oi-wiki.org/basic/prefix-sum/ "前缀和")也是如此。二者在连续问题中,对于**优化时间复杂度**有着很重要的意义。 因此如果一道题你可以用暴力解决出来,而且题目恰好有连续的限制, 那么滑动窗口和前缀和等技巧就应该被想到。 + +除了这几道题, 还有很多题目都是类似的套路, 大家可以在学习过程中进行体会。今天我们就来一起学习一下。 + +## 前菜 + +我们从一个简单的问题入手,识别一下这种题的基本形式和套路,为之后的四道题打基础。当你了解了这个套路之后, 之后做这种题就可以直接套。 + +需要注意的是这四道题的前置知识都是 `滑动窗口`, 不熟悉的同学可以先看下我之前写的 [滑动窗口专题(思路 + 模板)](https://github.com/azl397985856/leetcode/blob/master/thinkings/slide-window.md "滑动窗口专题(思路 + 模板)") + +### 母题 0 + +有 N 个的正整数放到数组 A 里,现在要求一个新的数组 B,新数组的第 i 个数 B[i]是原数组 A 第 0 到第 i 个数的和。 + +这道题可以使用前缀和来解决。 前缀和是一种重要的预处理,能大大降低查询的时间复杂度。我们可以简单理解为“数列的前 n 项的和”。这个概念其实很容易理解,即一个数组中,第 n 位存储的是数组前 n 个数字的和。 + +对 [1,2,3,4,5,6] 来说,其前缀和可以是 pre=[1,3,6,10,15,21]。我们可以使用公式 pre[𝑖]=pre[𝑖−1]+nums[𝑖]得到每一位前缀和的值,从而通过前缀和进行相应的计算和解题。其实前缀和的概念很简单,但困难的是如何在题目中使用前缀和以及如何使用前缀和的关系来进行解题。 + +### 母题 1 + +如果让你求一个数组的连续子数组总个数,你会如何求?其中连续指的是数组的索引连续。 比如 [1,3,4],其连续子数组有:`[1], [3], [4], [1,3], [3,4] , [1,3,4]`,你需要返回 6。 + +一种思路是总的连续子数组个数等于:**以索引为 0 结尾的子数组个数 + 以索引为 1 结尾的子数组个数 + ... + 以索引为 n - 1 结尾的子数组个数**,这无疑是完备的。 + +![](https://tva1.sinaimg.cn/large/007S8ZIlly1gj6m27kgbsj306u06gt8u.jpg) + +同时**利用母题 0 的前缀和思路, 边遍历边求和。** + +参考代码(JS): + +```js +function countSubArray(nums) { + let ans = 0; + let pre = 0; + for (_ in nums) { + pre += 1; + ans += pre; + } + return ans; +} +``` + +**复杂度分析** + +- 时间复杂度:$O(N)$,其中 N 为数组长度。 +- 空间复杂度:$O(1)$ + +而由于以索引为 i 结尾的子数组个数就是 i + 1,因此这道题可以直接用等差数列求和公式 `(1 + n) * n / 2`,其中 n 数组长度。 + +### 母题 2 + +我继续修改下题目, 如果让你求一个数组相邻差为 1 连续子数组的总个数呢?其实就是**索引差 1 的同时,值也差 1。** + +和上面思路类似,无非就是增加差值的判断。 + +参考代码(JS): + +```js +function countSubArray(nums) { + let ans = 1; + let pre = 1; + for (let i = 1; i < nums.length; i++) { + if (nums[i] - nums[i - 1] == 1) { + pre += 1; + } else { + pre = 0; + } + + ans += pre; + } + return ans; +} +``` + +**复杂度分析** + +- 时间复杂度:$O(N)$,其中 N 为数组长度。 +- 空间复杂度:$O(1)$ + +如果我值差只要大于 1 就行呢?其实改下符号就行了,这不就是求上升子序列个数么?这里不再继续赘述, 大家可以自己试试。 + +### 母题 3 + +我们继续扩展。 + +如果我让你求出不大于 k 的子数组的个数呢?不大于 k 指的是子数组的全部元素都不大于 k。 比如 [1,3,4] 子数组有 `[1], [3], [4], [1,3], [3,4] , [1,3,4]`,不大于 3 的子数组有 `[1], [3], [1,3]` ,那么 [1,3,4] 不大于 3 的子数组个数就是 3。 实现函数 atMostK(k, nums)。 + +参考代码(JS): + +```js +function countSubArray(k, nums) { + let ans = 0; + let pre = 0; + for (let i = 0; i < nums.length; i++) { + if (nums[i] <= k) { + pre += 1; + } else { + pre = 0; + } + + ans += pre; + } + return ans; +} +``` + +**复杂度分析** + +- 时间复杂度:$O(N)$,其中 N 为数组长度。 +- 空间复杂度:$O(1)$ + +### 母题 4 + +如果我让你求出子数组最大值刚好是 k 的子数组的个数呢? 比如 [1,3,4] 子数组有 `[1], [3], [4], [1,3], [3,4] , [1,3,4]`,子数组最大值刚好是 3 的子数组有 `[3], [1,3]` ,那么 [1,3,4] 子数组最大值刚好是 3 的子数组个数就是 2。实现函数 exactK(k, nums)。 + +实际上是 exactK 可以直接利用 atMostK,即 atMostK(k) - atMostK(k - 1),原因见下方母题 5 部分。 + +### 母题 5 + +如果我让你求出子数组最大值刚好是 介于 k1 和 k2 的子数组的个数呢?实现函数 betweenK(k1, k2, nums)。 + +实际上是 betweenK 可以直接利用 atMostK,即 atMostK(k1, nums) - atMostK(k2 - 1, nums),其中 k1 > k2。前提是值是离散的, 比如上面我出的题都是整数。 因此我可以直接 减 1,因为 **1 是两个整数最小的间隔**。 + +![](https://tva1.sinaimg.cn/large/007S8ZIlly1gj8m692laxj30pz0grte9.jpg) + +如上,`小于等于 10 的区域`减去 `小于 5 的区域`就是 `大于等于 5 且小于等于 10 的区域`。 + +注意我说的是小于 5, 不是小于等于 5。 由于整数是离散的,最小间隔是 1。因此小于 5 在这里就等价于 小于等于 4。这就是 betweenK(k1, k2, nums) = atMostK(k1) - atMostK(k2 - 1) 的原因。 + +因此不难看出 exactK 其实就是 betweenK 的特殊形式。 当 k1 == k2 的时候, betweenK 等价于 exactK。 + +因此 atMostK 就是灵魂方法,一定要掌握,不明白建议多看几遍。 + +有了上面的铺垫, 我们来看下第一道题。 + +## 467. 环绕字符串中唯一的子字符串(中等) + +### 题目描述 + +``` +把字符串 s 看作是“abcdefghijklmnopqrstuvwxyz”的无限环绕字符串,所以 s 看起来是这样的:"...zabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcd....".  + +现在我们有了另一个字符串 p 。你需要的是找出 s 中有多少个唯一的 p 的非空子串,尤其是当你的输入是字符串 p ,你需要输出字符串 s 中 p 的不同的非空子串的数目。  + +注意: p 仅由小写的英文字母组成,p 的大小可能超过 10000。 + +  + +示例 1: + +输入: "a" +输出: 1 +解释: 字符串 S 中只有一个"a"子字符。 +  + +示例 2: + +输入: "cac" +输出: 2 +解释: 字符串 S 中的字符串“cac”只有两个子串“a”、“c”。. +  + +示例 3: + +输入: "zab" +输出: 6 +解释: 在字符串 S 中有六个子串“z”、“a”、“b”、“za”、“ab”、“zab”。. +  + +``` + +### 前置知识 + +- 滑动窗口 + +### 思路 + +题目是让我们找 p 在 s 中出现的非空子串数目,而 s 是固定的一个无限循环字符串。由于 p 的数据范围是 10^5 ,因此暴力找出所有子串就需要 10^10 次操作了,应该会超时。而且题目很多信息都没用到,肯定不对。 + +仔细看下题目发现,这不就是母题 2 的变种么?话不多说, 直接上代码,看看有多像。 + +> 为了减少判断, 我这里用了一个黑科技, p 前面加了个 `^`。 + +```py +class Solution: + def findSubstringInWraproundString(self, p: str) -> int: + p = '^' + p + w = 1 + ans = 0 + for i in range(1,len(p)): + if ord(p[i])-ord(p[i-1]) == 1 or ord(p[i])-ord(p[i-1]) == -25: + w += 1 + else: + w = 1 + ans += w + return ans +``` + +如上代码是有问题。 比如 `cac`会被计算为 3,实际上应该是 2。根本原因在于 c 被错误地计算了两次。因此一个简单的思路就是用 set 记录一下访问过的子字符串即可。比如: + +```py +{ + c, + abc, + ab, + abcd +} + +``` + +而由于 set 中的元素一定是连续的,因此上面的数据也可以用 hashmap 存: + +``` +{ + c: 3 + d: 4 + b: 1 +} + +``` + +含义是: + +- 以 b 结尾的子串最大长度为 1,也就是 b。 +- 以 c 结尾的子串最大长度为 3,也就是 abc。 +- 以 d 结尾的子串最大长度为 4,也就是 abcd。 + +至于 c ,是没有必要存的。我们可以通过母题 2 的方式算出来。 + +具体算法: + +- 定义一个 len_mapper。key 是 字母, value 是 长度。 含义是以 key 结尾的最长连续子串的长度。 + +> 关键字是:最长 + +- 用一个变量 w 记录连续子串的长度,遍历过程根据 w 的值更新 len_mapper +- 返回 len_mapper 中所有 value 的和。 + +比如: abc,此时的 len_mapper 为: + +```py +{ + c: 3 + b: 2 + a: 1 +} +``` + +再比如:abcab,此时的 len_mapper 依旧。 + +再比如: abcazabc,此时的 len_mapper: + +```py +{ + c: 4 + b: 3 + a: 2 + z: 1 +} +``` + +这就得到了去重的目的。这种算法是不重不漏的,因为最长的连续子串一定是包含了比它短的连续子串,这个思想和 [1297. 子串的最大出现次数](https://github.com/azl397985856/leetcode/issues/266 "1297. 子串的最大出现次数") 剪枝的方法有异曲同工之妙。 + +### 代码(Python) + +```py +class Solution: + def findSubstringInWraproundString(self, p: str) -> int: + p = '^' + p + len_mapper = collections.defaultdict(lambda: 0) + w = 1 + for i in range(1,len(p)): + if ord(p[i])-ord(p[i-1]) == 1 or ord(p[i])-ord(p[i-1]) == -25: + w += 1 + else: + w = 1 + len_mapper[p[i]] = max(len_mapper[p[i]], w) + return sum(len_mapper.values()) +``` + +**复杂度分析** + +- 时间复杂度:$O(N)$,其中 $N$ 为字符串 p 的长度。 +- 空间复杂度:由于最多存储 26 个字母, 因此空间实际上是常数,故空间复杂度为 $O(1)$。 + +## 795. 区间子数组个数(中等) + +### 题目描述 + +``` + +给定一个元素都是正整数的数组 A ,正整数 L  以及  R (L <= R)。 + +求连续、非空且其中最大元素满足大于等于 L  小于等于 R 的子数组个数。 + +例如 : +输入: +A = [2, 1, 4, 3] +L = 2 +R = 3 +输出: 3 +解释: 满足条件的子数组: [2], [2, 1], [3]. +注意: + +L, R  和  A[i] 都是整数,范围在  [0, 10^9]。 +数组  A  的长度范围在[1, 50000]。 + +``` + +### 前置知识 + +- 滑动窗口 + +### 思路 + +由母题 5,我们知道 **betweenK 可以直接利用 atMostK,即 atMostK(k1) - atMostK(k2 - 1),其中 k1 > k2**。 + +由母题 2,我们知道如何求满足一定条件(这里是元素都小于等于 R)子数组的个数。 + +这两个结合一下, 就可以解决。 + +### 代码(Python) + +> 代码是不是很像 + +```py +class Solution: + def numSubarrayBoundedMax(self, A: List[int], L: int, R: int) -> int: + def notGreater(R): + ans = cnt = 0 + for a in A: + if a <= R: cnt += 1 + else: cnt = 0 + ans += cnt + return ans + + return notGreater(R) - notGreater(L - 1) +``` + +**_复杂度分析_** + +- 时间复杂度:$O(N)$,其中 $N$ 为数组长度。 +- 空间复杂度:$O(1)$。 + +## 904. 水果成篮(中等) + +### 题目描述 + +``` +在一排树中,第 i 棵树产生 tree[i] 型的水果。 +你可以从你选择的任何树开始,然后重复执行以下步骤: + +把这棵树上的水果放进你的篮子里。如果你做不到,就停下来。 +移动到当前树右侧的下一棵树。如果右边没有树,就停下来。 +请注意,在选择一颗树后,你没有任何选择:你必须执行步骤 1,然后执行步骤 2,然后返回步骤 1,然后执行步骤 2,依此类推,直至停止。 + +你有两个篮子,每个篮子可以携带任何数量的水果,但你希望每个篮子只携带一种类型的水果。 + +用这个程序你能收集的水果树的最大总量是多少? + +  + +示例 1: + +输入:[1,2,1] +输出:3 +解释:我们可以收集 [1,2,1]。 +示例 2: + +输入:[0,1,2,2] +输出:3 +解释:我们可以收集 [1,2,2] +如果我们从第一棵树开始,我们将只能收集到 [0, 1]。 +示例 3: + +输入:[1,2,3,2,2] +输出:4 +解释:我们可以收集 [2,3,2,2] +如果我们从第一棵树开始,我们将只能收集到 [1, 2]。 +示例 4: + +输入:[3,3,3,1,2,1,1,2,3,3,4] +输出:5 +解释:我们可以收集 [1,2,1,1,2] +如果我们从第一棵树或第八棵树开始,我们将只能收集到 4 棵水果树。 +  + +提示: + +1 <= tree.length <= 40000 +0 <= tree[i] < tree.length + +``` + +### 前置知识 + +- 滑动窗口 + +### 思路 + +题目花里胡哨的。我们来抽象一下,就是给你一个数组, 让你**选定一个子数组, 这个子数组最多只有两种数字**,这个选定的子数组最大可以是多少。 + +这不就和母题 3 一样么?只不过 k 变成了固定值 2。另外由于题目要求整个窗口最多两种数字,我们用哈希表存一下不就好了吗? + +> set 是不行了的。 因此我们不但需要知道几个数字在窗口, 我们还要知道每个数字出现的次数,这样才可以使用滑动窗口优化时间复杂度。 + +### 代码(Python) + +```py +class Solution: + def totalFruit(self, tree: List[int]) -> int: + def atMostK(k, nums): + i = ans = 0 + win = defaultdict(lambda: 0) + for j in range(len(nums)): + if win[nums[j]] == 0: k -= 1 + win[nums[j]] += 1 + while k < 0: + win[nums[i]] -= 1 + if win[nums[i]] == 0: k += 1 + i += 1 + ans = max(ans, j - i + 1) + return ans + + return atMostK(2, tree) +``` + +**复杂度分析** + +- 时间复杂度:$O(N)$,其中 $N$ 为数组长度。 +- 空间复杂度:$O(k)$。 + +## 992. K 个不同整数的子数组(困难) + +### 题目描述 + +``` +给定一个正整数数组 A,如果 A 的某个子数组中不同整数的个数恰好为 K,则称 A 的这个连续、不一定独立的子数组为好子数组。 + +(例如,[1,2,3,1,2] 中有 3 个不同的整数:1,2,以及 3。) + +返回 A 中好子数组的数目。 + +  + +示例 1: + +输入:A = [1,2,1,2,3], K = 2 +输出:7 +解释:恰好由 2 个不同整数组成的子数组:[1,2], [2,1], [1,2], [2,3], [1,2,1], [2,1,2], [1,2,1,2]. +示例 2: + +输入:A = [1,2,1,3,4], K = 3 +输出:3 +解释:恰好由 3 个不同整数组成的子数组:[1,2,1,3], [2,1,3], [1,3,4]. +  + +提示: + +1 <= A.length <= 20000 +1 <= A[i] <= A.length +1 <= K <= A.length + + + +``` + +### 前置知识 + +- 滑动窗口 + +### 思路 + +由母题 5,知:exactK = atMostK(k) - atMostK(k - 1), 因此答案便呼之欲出了。其他部分和上面的题目 `904. 水果成篮` 一样。 + +> 实际上和所有的滑动窗口题目都差不多。 + +### 代码(Python) + +```py +class Solution: + def subarraysWithKDistinct(self, A, K): + return self.atMostK(A, K) - self.atMostK(A, K - 1) + + def atMostK(self, A, K): + counter = collections.Counter() + res = i = 0 + for j in range(len(A)): + if counter[A[j]] == 0: + K -= 1 + counter[A[j]] += 1 + while K < 0: + counter[A[i]] -= 1 + if counter[A[i]] == 0: + K += 1 + i += 1 + res += j - i + 1 + return res +``` + +**复杂度分析** + +- 时间复杂度:$O(N)$,中 $N$ 为数组长度。 +- 空间复杂度:$O(k)$。 + +## 1109. 航班预订统计(中等) + +### 题目描述 + +``` + +这里有  n  个航班,它们分别从 1 到 n 进行编号。 + +我们这儿有一份航班预订表,表中第  i  条预订记录  bookings[i] = [i, j, k]  意味着我们在从  i  到  j  的每个航班上预订了 k 个座位。 + +请你返回一个长度为 n 的数组  answer,按航班编号顺序返回每个航班上预订的座位数。 + + + +示例: + +输入:bookings = [[1,2,10],[2,3,20],[2,5,25]], n = 5 +输出:[10,55,45,25,25] + + + +提示: + +1 <= bookings.length <= 20000 +1 <= bookings[i][0] <= bookings[i][1] <= n <= 20000 +1 <= bookings[i][2] <= 10000 +``` + +### 前置知识 + +- 前缀和 + +### 思路 + +这道题的题目描述不是很清楚。我简单分析一下题目: + +[i, j, k] 其实代表的是 第 i 站上来了 k 个人, 一直到 第 j 站都在飞机上,到第 j + 1 就不在飞机上了。所以第 i 站到第 j 站的**每一站**都会因此多 k 个人。 + +理解了题目只会不难写出下面的代码。 + +```py +class Solution: + def corpFlightBookings(self, bookings: List[List[int]], n: int) -> List[int]: + counter = [0] * n + + for i, j, k in bookings: + while i <= j: + counter[i - 1] += k + i += 1 + return counter +``` + +如上的代码复杂度太高,无法通过全部的测试用例。 + +**注意到里层的 while 循环是连续的数组全部加上一个数字,不难想到可以利用母题 0 的前缀和思路优化。** + +![](https://tva1.sinaimg.cn/large/007S8ZIlly1gj8k7w0bqyj30qh07540b.jpg) + +一种思路就是在 i 的位置 + k, 然后利用前缀和的技巧给 i 到 n 的元素都加上 k。但是题目需要加的是一个区间, j + 1 及其之后的元素会被多加一个 k。一个简单的技巧就是给 j + 1 的元素减去 k,这样正负就可以抵消。 + +![](https://tva1.sinaimg.cn/large/007S8ZIlly1gj8k997nmbj30q9074dhm.jpg) + +### 代码(Python) + +```py +class Solution: + def corpFlightBookings(self, bookings: List[List[int]], n: int) -> List[int]: + counter = [0] * (n + 1) + + for i, j, k in bookings: + counter[i - 1] += k + if j < n: counter[j] -= k + for i in range(n + 1): + counter[i] += counter[i - 1] + return counter[:-1] +``` + +**复杂度分析** + +- 时间复杂度:$O(N)$,中 $N$ 为数组长度。 +- 空间复杂度:$O(N)$。 + +## 总结 + +这几道题都是滑动窗口和前缀和的思路。力扣类似的题目还真不少,大家只有多留心,就会发现这个套路。 + +前缀和的技巧以及滑动窗口的技巧都比较固定,且有模板可套。 难点就在于我怎么才能想到可以用这个技巧呢? + +我这里总结了两点: + +1. 找关键字。比如题目中有连续,就应该条件反射想到滑动窗口和前缀和。比如题目求最大最小就想到动态规划和贪心等等。想到之后,就可以和题目信息对比快速排除错误的算法,找到可行解。这个思考的时间会随着你的题感增加而降低。 +2. 先写出暴力解,然后找暴力解的瓶颈, 根据瓶颈就很容易知道应该用什么数据结构和算法去优化。 + +最后推荐几道类似的题目, 供大家练习,一定要自己写出来才行哦。 + +- [303. 区域和检索 - 数组不可变](https://leetcode-cn.com/problems/range-sum-query-immutable/description/ "303. 区域和检索 - 数组不可变") +- [1186.删除一次得到子数组最大和](https://lucifer.ren/blog/2019/12/11/leetcode-1186/ "1186.删除一次得到子数组最大和") +- [1310. 子数组异或查询](https://lucifer.ren/blog/2020/01/09/1310.xor-queries-of-a-subarray/ "1310. 子数组异或查询") +- [1371. 每个元音包含偶数次的最长子字符串](https://github.com/azl397985856/leetcode/blob/master/problems/1371.find-the-longest-substring-containing-vowels-in-even-counts.md "1371. 每个元音包含偶数次的最长子字符串") + +大家对此有何看法,欢迎给我留言,我有时间都会一一查看回答。 + +更多算法套路可以访问我的 LeetCode 题解仓库:https://github.com/azl397985856/leetcode 。 目前已经 36K star 啦。 + +大家也可以关注我的公众号《力扣加加》带你啃下算法这块硬骨头。 + +![](https://tva1.sinaimg.cn/large/007S8ZIlly1gfcuzagjalj30p00dwabs.jpg) diff --git a/selected/byte-dance-algo-ex-2017.md b/selected/byte-dance-algo-ex-2017.md new file mode 100644 index 0000000..6bf556d --- /dev/null +++ b/selected/byte-dance-algo-ex-2017.md @@ -0,0 +1,530 @@ +# 字节跳动的算法面试题是什么难度?(第二弹) + +由于 lucifer 我是一个小前端, 最近也在准备写一个《前端如何搞定算法面试》的专栏,因此最近没少看各大公司的面试题。都说字节跳动算法题比较难,我就先拿 ta 下手,做了几套 。这次我们就拿一套 `字节跳动2017秋招编程题汇总`来看下字节的算法笔试题的难度几何。地址:https://www.nowcoder.com/test/6035789/summary + +这套题一共 11 道题, 三道编程题, 八道问答题。本次给大家带来的就是这三道编程题。更多精彩内容,请期待我的搞定算法面试专栏。 + +![](https://tva1.sinaimg.cn/large/007S8ZIlly1gip1ab1tb9j311b0u0jzi.jpg) + +其中有一道题《异或》我没有通过所有的测试用例, 小伙伴可以找找茬,第一个找到并在公众号力扣加加留言的小伙伴奖励现金红包 10 元。 + +## 1. 头条校招 + +### 题目描述 + +``` +头条的 2017 校招开始了!为了这次校招,我们组织了一个规模宏大的出题团队,每个出题人都出了一些有趣的题目,而我们现在想把这些题目组合成若干场考试出来,在选题之前,我们对题目进行了盲审,并定出了每道题的难度系统。一场考试包含 3 道开放性题目,假设他们的难度从小到大分别为 a,b,c,我们希望这 3 道题能满足下列条件: +a<=b<=c +b-a<=10 +c-b<=10 +所有出题人一共出了 n 道开放性题目。现在我们想把这 n 道题分布到若干场考试中(1 场或多场,每道题都必须使用且只能用一次),然而由于上述条件的限制,可能有一些考试没法凑够 3 道题,因此出题人就需要多出一些适当难度的题目来让每场考试都达到要求,然而我们出题已经出得很累了,你能计算出我们最少还需要再出几道题吗? + +输入描述: +输入的第一行包含一个整数 n,表示目前已经出好的题目数量。 + +第二行给出每道题目的难度系数 d1,d2,...,dn。 + +数据范围 + +对于 30%的数据,1 ≤ n,di ≤ 5; + +对于 100%的数据,1 ≤ n ≤ 10^5,1 ≤ di ≤ 100。 + +在样例中,一种可行的方案是添加 2 个难度分别为 20 和 50 的题目,这样可以组合成两场考试:(20 20 23)和(35,40,50)。 + +输出描述: +输出只包括一行,即所求的答案。 +示例 1 +输入 +4 +20 35 23 40 +输出 +2 +``` + +### 思路 + +这道题看起来很复杂, 你需要考虑很多的情况。,属于那种没有技术含量,但是考验编程能力的题目,需要思维足够严密。这种**模拟的题目**,就是题目让我干什么我干什么。 类似之前写的囚徒房间问题,约瑟夫环也是模拟,只不过模拟之后需要你剪枝优化。 + +这道题的情况其实很多, 我们需要考虑每一套题中的难度情况, 而不需要考虑不同套题的难度情况。题目要求我们满足:`a<=b<=c b-a<=10 c-b<=10`,也就是题目难度从小到大排序之后,相邻的难度不能大于 10 。 + +因此我们的思路就是先排序,之后从小到大遍历,如果满足相邻的难度不大于 10 ,则继续。如果不满足, 我们就只能让字节的老师出一道题使得满足条件。 + +由于只需要比较同一套题目的难度,因此我的想法就是**比较同一套题目的第二个和第一个,以及第三个和第二个的 diff**。 + +- 如果 diff 小于 10,什么都不做,继续。 +- 如果 diff 大于 10,我们必须补充题目。 + +这里有几个点需要注意。 + +对于第二题来说: + +- 比如 1 **30** 40 这样的难度。 我可以在 1,30 之间加一个 21,这样 1,21,30 就可以组成一一套。 +- 比如 1 **50** 60 这样的难度。 我可以在 1,50 之间加 21, 41 才可以组成一套,自身(50)是无论如何都没办法组到这套题中的。 + +不难看出, 第二道题的临界点是 diff = 20 。 小于等于 20 都可以将自身组到套题,增加一道即可,否则需要增加两个,并且自身不能组到当前套题。 + +对于第三题来说: + +- 比如 1 20 **40**。 我可以在 20,40 之间加一个 30,这样 1,20,30 就可以组成一一套,自身(40)是无法组到这套题的。 +- 比如 1 20 **60**。 也是一样的,我可以在 20,60 之间加一个 30,自身(60)同样是没办法组到这套题中的。 + +不难看出, 第三道题的临界点是 diff = 10 。 小于等于 10 都可以将自身组到套题,否则需要增加一个,并且自身不能组到当前套题。 + +这就是所有的情况了。 + +有的同学比较好奇,我是怎么思考的。 我是怎么**保障不重不漏**的。 + +实际上,这道题就是一个决策树, 我画个决策树出来你就明白了。 + +![](https://tva1.sinaimg.cn/large/007S8ZIlly1gip1se8id8j31p60u0n6z.jpg) + +> 图中红色边框表示自身可以组成套题的一部分, 我也用文字进行了说明。#2 代表第二题, #3 代表第三题。 + +从图中可以看出, 我已经考虑了所有情况。如果你能够像我一样画出这个决策图,我想你也不会漏的。当然我的解法并不一定是最优的,不过确实是一个非常好用,具有普适性的思维框架。 + +需要特别注意的是,由于需要凑整, 因此你需要使得题目的总数是 3 的倍数向上取整。 + +![](https://tva1.sinaimg.cn/large/007S8ZIlly1gip1x5c97lj30cs0eoq3c.jpg) + +### 代码 + +```py +n = int(input()) +nums = list(map(int, input().split())) +cnt = 0 +cur = 1 +nums.sort() +for i in range(1, n): + if cur == 3: + cur = 1 + continue + diff = nums[i] - nums[i - 1] + if diff <= 10: + cur += 1 + if 10 < diff <= 20: + if cur == 1: + cur = 3 + if cur == 2: + cur = 1 + cnt += 1 + if diff > 20: + if cur == 1: + cnt += 2 + if cur == 2: + cnt += 1 + cur = 1 +print(cnt + 3 - cur) +``` + +**复杂度分析** + +- 时间复杂度:由于使用了排序, 因此时间复杂度为 $O(NlogN)$。(假设使用了基于比较的排序) +- 空间复杂度:$O(1)$ + +## 2. 异或 + +### 题目描述 + +``` +给定整数 m 以及 n 各数字 A1,A2,..An,将数列 A 中所有元素两两异或,共能得到 n(n-1)/2 个结果,请求出这些结果中大于 m 的有多少个。 + +输入描述: +第一行包含两个整数 n,m. + +第二行给出 n 个整数 A1,A2,...,An。 + +数据范围 + +对于 30%的数据,1 <= n, m <= 1000 + +对于 100%的数据,1 <= n, m, Ai <= 10^5 + +输出描述: +输出仅包括一行,即所求的答案 + +输入例子 1: +3 10 +6 5 10 + +输出例子 1: +2 +``` + +### 前置知识 + +- 异或运算的性质 +- 如何高效比较两个数的大小(从高位到低位) + +首先普及一下前置知识。 第一个是异或运算: + +异或的性质:两个数字异或的结果 a^b 是将 a 和 b 的二进制每一位进行运算,得出的数字。 运算的逻辑是如果同一位的数字相同则为 0,不同则为 1 + +异或的规律: + +1. 任何数和本身异或则为 0 + +2. 任何数和 0 异或是本身 + +3. 异或运算满足交换律,即: a ^ b ^ c = a ^ c ^ b + +同时建议大家去看下我总结的几道位运算的经典题目。 [位运算系列](https://leetcode-cn.com/problems/single-number/solution/zhi-chu-xian-yi-ci-de-shu-xi-lie-wei-yun-suan-by-3/ "位运算系列") + +其次要知道一个常识, 即比较两个数的大小, 我们是从高位到低位比较,这样才比较高效。 + +比如: + +``` +123 +456 +1234 + +``` + +这三个数比较大小, 为了方便我们先补 0 ,使得大家的位数保持一致。 + +``` +0123 +0456 +1234 +``` + +![](https://tva1.sinaimg.cn/large/007S8ZIlly1gip1zqrphpj30ae0cswex.jpg) + +先比较第一位,1 比较 0 大, 因此 1234 最大。再比较第二位, 4 比 1 大, 因此 456 大于 123,后面位不需要比较了。这其实就是剪枝的思想。 + +有了这两个前提,我们来试下暴力法解决这道题。 + +### 思路 + +暴力法就是枚举 $N^2 / 2$ 中组合, 让其两两按位异或,将得到的结果和 m 进行比较, 如果比 m 大, 则计数器 + 1, 最后返回计数器的值即可。 + +暴力的方法就如同题目描述的那样, 复杂度为 $N^2$。 一定过不了所有的测试用例, 不过大家实在没有好的解法的情况可以兜底。不管是牛客笔试还是实际的面试都是可行的。 + +接下来,让我们来**分析一下暴力为什么低效,以及如何选取数据结构和算法能够使得这个过程变得高效。** 记住这句话, 几乎所有的优化都是基于这种思维产生的,除非你开启了上帝模式,直接看了答案。 只不过等你熟悉了之后,这个思维过程会非常短, 以至于变成条件反射, 你感觉不到有这个过程, 这就是**有了题感。** + +其实我刚才说的第二个前置知识就是我们优化的关键之一。 + +我举个例子, 比如 3 和 5 按位异或。 + +3 的二进制是 011, 5 的二进制是 101, + +``` +011 +101 +``` + +按照我前面讲的异或知识, 不难得出其异或结构就是 110。 + +上面我进行了三次异或: + +1. 第一次是最高位的 0 和 1 的异或, 结果为 1。 +2. 第二次是次高位的 1 和 0 的异或, 结果为 1。 +3. 第三次是最低位的 1 和 1 的异或, 结果为 0。 + +那如何 m 是 1 呢? 我们有必要进行三次异或么? 实际上进行第一次异或的时候已经知道了一定比 m(m 是 1) 大。因为第一次异或的结构导致其最高位为 1,也就是说其最小也不过是 100,也就是 4,一定是大于 1 的。这就是**剪枝**, 这就是算法优化的关键。 + +> 看出我一步一步的思维过程了么?所有的算法优化都需要经过类似的过程。 + +因此我的算法就是从高位开始两两异或,并且异或的结果和 m 对应的二进制位比较大小。 + +- 如果比 m 对应的二进制位大或者小,我们提前退出即可。 +- 如果相等,我们继续往低位移动重复这个过程。 + +这虽然已经剪枝了,但是极端情况下,性能还是很差。比如: + +``` +m: 1111 +a: 1010 +b: 0101 +``` + +a,b 表示两个数,我们比较到最后才发现,其异或的值和 m 相等。因此极端情况,算法效率没有得到改进。 + +这里我想到了一点,就是如果一个数 a 的前缀和另外一个数 b 的前缀是一样的,那么 c 和 a 或者 c 和 b 的异或的结构前缀部分一定也是一样的。比如: + +``` +a: 111000 +b: 111101 +c: 101011 +``` + +a 和 b 有共同的前缀 111,c 和 a 异或过了,当再次和 b 异或的时候,实际上前三位是没有必要进行的,这也是重复的部分。这就是算法可以优化的部分, 这就是剪枝。 + +**分析算法,找到算法的瓶颈部分,然后选取合适的数据结构和算法来优化到。** 这句话很重要, 请务必记住。 + +在这里,我们用的就是剪枝技术,关于剪枝,91 天学算法也有详细的介绍。 + +回到前面讲到的算法瓶颈, 多个数是有共同前缀的, 前缀部分就是我们浪费的运算次数, 说到前缀大家应该可以想到前缀树。如果不熟悉前缀树的话,看下我的这个[前缀树专题](https://github.com/azl397985856/leetcode/blob/master/thinkings/trie.md "前缀树专题"),里面的题全部手写一遍就差不多了。 + +因此一种想法就是建立一个前缀树, **树的根就是最高的位**。 由于题目要求异或, 我们知道异或是二进制的位运算, 因此这棵树要存二进制才比较好。 + +反手看了一眼数据范围:m, n<=10^5 。 10^5 = 2 ^ x,我们的目标是求出 满足条件的 x 的 ceil(向上取整),因此 x 应该是 17。 + +树的每一个节点存储的是:**n 个数中,从根节点到当前节点形成的前缀有多少个是一样的**,即多少个数的前缀是一样的。这样可以剪枝,提前退出的时候,就直接取出来用了。比如异或的结果是 1, m 当前二进制位是 0 ,那么这个前缀有 10 个,我都不需要比较了, 计数器直接 + 10 。 + +![](https://tva1.sinaimg.cn/large/007S8ZIlly1gip21tqf5gj31vl0u0n61.jpg) + +> 我用 17 直接复杂度过高,目前仅仅通过了 70 % - 80 % 测试用例, 希望大家可以帮我找找毛病,我猜测是语言的锅。 + +### 代码 + +```py + +class TreeNode: + def __init__(self): + self.cnt = 1 + self.children = [None] * 2 +def solve(num, i, cur): + if cur == None or i == -1: return 0 + bit = (num >> i) & 1 + mbit = (m >> i) & 1 + if bit == 0 and mbit == 0: + return (cur.children[1].cnt if cur.children[1] else 0) + solve(num, i - 1, cur.children[0]) + if bit == 1 and mbit == 0: + return (cur.children[0].cnt if cur.children[0] else 0) + solve(num, i - 1, cur.children[1]) + if bit == 0 and mbit == 1: + return solve(num, i - 1, cur.children[1]) + if bit == 1 and mbit == 1: + return solve(num, i - 1, cur.children[0]) + +def preprocess(nums, root): + for num in nums: + cur = root + for i in range(16, -1, -1): + bit = (num >> i) & 1 + if cur.children[bit]: + cur.children[bit].cnt += 1 + else: + cur.children[bit] = TreeNode() + cur = cur.children[bit] + +n, m = map(int, input().split()) +nums = list(map(int, input().split())) +root = TreeNode() +preprocess(nums, root) +ans = 0 +for num in nums: + ans += solve(num, 16, root) +print(ans // 2) +``` + +**复杂度分析** + +- 时间复杂度:$O(N)$ +- 空间复杂度:$O(N)$ + +## 3. 字典序 + +### 题目描述 + +``` + +给定整数 n 和 m, 将 1 到 n 的这 n 个整数按字典序排列之后, 求其中的第 m 个数。 +对于 n=11, m=4, 按字典序排列依次为 1, 10, 11, 2, 3, 4, 5, 6, 7, 8, 9, 因此第 4 个数是 2. +对于 n=200, m=25, 按字典序排列依次为 1 10 100 101 102 103 104 105 106 107 108 109 11 110 111 112 113 114 115 116 117 118 119 12 120 121 122 123 124 125 126 127 128 129 13 130 131 132 133 134 135 136 137 138 139 14 140 141 142 143 144 145 146 147 148 149 15 150 151 152 153 154 155 156 157 158 159 16 160 161 162 163 164 165 166 167 168 169 17 170 171 172 173 174 175 176 177 178 179 18 180 181 182 183 184 185 186 187 188 189 19 190 191 192 193 194 195 196 197 198 199 2 20 200 21 22 23 24 25 26 27 28 29 3 30 31 32 33 34 35 36 37 38 39 4 40 41 42 43 44 45 46 47 48 49 5 50 51 52 53 54 55 56 57 58 59 6 60 61 62 63 64 65 66 67 68 69 7 70 71 72 73 74 75 76 77 78 79 8 80 81 82 83 84 85 86 87 88 89 9 90 91 92 93 94 95 96 97 98 99 因此第 25 个数是 120… + +输入描述: +输入仅包含两个整数 n 和 m。 + +数据范围: + +对于 20%的数据, 1 <= m <= n <= 5 ; + +对于 80%的数据, 1 <= m <= n <= 10^7 ; + +对于 100%的数据, 1 <= m <= n <= 10^18. + +输出描述: +输出仅包括一行, 即所求排列中的第 m 个数字. +示例 1 +输入 +11 4 +输出 +2 +``` + +### 前置知识 + +- 十叉树 +- 完全十叉树 +- 计算完全十叉树的节点个数 +- 字典树 + +### 思路 + +和上面题目思路一样, 先从暴力解法开始,尝试打开思路。 + +暴力兜底的思路是直接生成一个长度为 n 的数组, 排序,选第 m 个即可。代码: + +```py +n, m = map(int, input().split()) + +nums = [str(i) for i in range(1, n + 1)] +print(sorted(nums)[m - 1]) + +``` + +**复杂度分析** + +- 时间复杂度:取决于排序算法, 不妨认为是 $O(NlogN)$ +- 空间复杂度: $O(N)$ + +这种算法可以 pass 50 % case。 + +上面算法低效的原因是开辟了 N 的空间,并对整 N 个 元素进行了排序。 + +一种简单的优化方法是将排序换成堆,利用堆的特性求第 k 大的数, 这样时间复杂度可以减低到 $mlogN$。 + +我们继续优化。实际上,你如果把字典序的排序结构画出来, 可以发现他本质就是一个十叉树,并且是一个完全十叉树。 + +接下来,我带你继续分析。 + +![](https://tva1.sinaimg.cn/large/007S8ZIlly1gipczpnhjij32440u0h2w.jpg) + +如图, 红色表示根节点。节点表示一个十进制数, **树的路径存储真正的数字**,比如图上的 100,109 等。 这不就是上面讲的前缀树么? + +如图黄色部分, 表示字典序的顺序,注意箭头的方向。因此本质上,**求字典序第 m 个数, 就是求这棵树的前序遍历的第 m 个节点。** + +因此一种优化思路就是构建一颗这样的树,然后去遍历。 构建的复杂度是 $O(N)$,遍历的复杂度是 $O(M)$。因此这种算法的复杂度可以达到 $O(max(m, n))$ ,由于 n >= m,因此就是 $O(N)$。 + +实际上, 这样的优化算法依然是无法 AC 全部测试用例的,会超内存限制。 因此我们的思路只能是不使用 N 的空间去构造树。想想也知道, 由于 N 最大可能为 10^18,一个数按照 4 字节来算, 那么这就有 400000000 字节,大约是 381 M,这是不能接受的。 + +上面提到这道题就是一个完全十叉树的前序遍历,问题转化为求完全十叉树的前序遍历的第 m 个数。 + +> 十叉树和二叉树没有本质不同, 我在二叉树专题部分, 也提到了 N 叉树都可以用二叉树来表示。 + +对于一个节点来说,第 m 个节点: + +- 要么就是它本身 +- 要么其孩子节点中 +- 要么在其兄弟节点 +- 要么在兄弟节点的孩子节点中 + +究竟在上面的四个部分的哪,取决于其孩子节点的个数。 + +- count > m ,m 在其孩子节点中,我们需要深入到子节点。 +- count <= m ,m 不在自身和孩子节点, 我们应该跳过所有孩子节点,直接到兄弟节点。 + +这本质就是一个递归的过程。 + +需要注意的是,我们并不会真正的在树上走,因此上面提到的**深入到子节点**, 以及 **跳过所有孩子节点,直接到兄弟节点**如何操作呢? + +你仔细观察会发现: 如果当前节点的前缀是 x ,那么其第一个子节点(就是最小的子节点)是 x \* 10,第二个就是 x \* 10 + 1,以此类推。因此: + +- 深入到子节点就是 x \* 10。 +- 跳过所有孩子节点,直接到兄弟节点就是 x + 1。 + +ok,铺垫地差不多了。 + +接下来,我们的重点是**如何计算给定节点的孩子节点的个数**。 + +这个过程和完全二叉树计算节点个数并无二致,这个算法的时间复杂度应该是 $O(logN*logN)$。 如果不会的同学,可以参考力扣原题: [222. 完全二叉树的节点个数](https://leetcode-cn.com/problems/count-complete-tree-nodes/ "22. 完全二叉树的节点个数]") ,这是一个难度为中等的题目。 + +> 因此这道题本身被划分为 hard,一点都不为过。 + +这里简单说下,计算给定节点的孩子节点的个数的思路, 我的 91 天学算法里出过这道题。 + +一种简单但非最优的思路是分别计算左右子树的深度。 + +- 如果当前节点的左右子树高度相同,那么左子树是一个满二叉树,右子树是一个完全二叉树。 +- 否则(左边的高度大于右边),那么左子树是一个完全二叉树,右子树是一个满二叉树。 + +如果是满二叉树,当前节点数 是 2 ^ depth,而对于完全二叉树,我们继续递归即可。 + +```py +class Solution: + def countNodes(self, root): + if not root: + return 0 + ld = self.getDepth(root.left) + rd = self.getDepth(root.right) + if ld == rd: + return 2 ** ld + self.countNodes(root.right) + else: + return 2 ** rd + self.countNodes(root.left) + + def getDepth(self, root): + if not root: + return 0 + return 1 + self.getDepth(root.left) +``` + +**复杂度分析** + +- 时间复杂度:$O(logN * log N)$ +- 空间复杂度:$O(logN)$ + +而这道题, 我们可以更简单和高效。 + +比如我们要计算 1 号节点的子节点个数。 + +- 它的孩子节点个数是 。。。 +- 它的孙子节点个数是 。。。 +- 。。。 + +全部加起来即可。 + +它的孩子节点个数是 `20 - 10 = 10` 。 也就是它的**右边的兄弟节点的第一个子节点** 减去 它的**第一个子节点**。 + +![](https://tva1.sinaimg.cn/large/007S8ZIlly1gipel153igj31u40r8qd0.jpg) + +由于是完全十叉树,而不是满十叉树 。因此你需要考虑边界情况,比如题目的 n 是 15。 那么 1 的子节点个数就不是 20 - 10 = 10 了, 而是 15 - 10 + 1 = 16。 + +![](https://tva1.sinaimg.cn/large/007S8ZIlly1gipemlbs0cj31ty0mm79i.jpg) + +其他也是类似的过程, 我们只要: + +- Go deeper and do the same thing + +或者: + +- Move to next neighbor and do the same thing + +不断重复,直到 m 降低到 0 。 + +### 代码 + +```py + +def count(c1, c2, n): + steps = 0 + while c1 <= n: + steps += min(n + 1, c2) - c1 + c1 *= 10 + c2 *= 10 + return steps +def findKthNumber(n: int, k: int) -> int: + cur = 1 + k = k - 1 + while k > 0: + steps = count(cur, cur + 1, n) + if steps <= k: + cur += 1 + k -= steps + else: + cur *= 10 + k -= 1 + return cur +n, m = map(int, input().split()) +print(findKthNumber(n, m)) +``` + +**复杂度分析** + +- 时间复杂度:$O(logM * log N)$ +- 空间复杂度:$O(1)$ + +## 总结 + +其中三道算法题从难度上来说,基本都是困难难度。从内容来看,基本都是力扣的换皮题,且都或多或少和树有关。如果大家一开始没有思路,建议大家先给出暴力的解法兜底,再画图或举简单例子打开思路。 + +我也刷了很多字节的题了,还有一些难度比较大的题。如果你第一次做,那么需要你思考比较久才能想出来。加上面试紧张,很可能做不出来。这个时候就更需要你冷静分析,先暴力打底,慢慢优化。有时候即使给不了最优解,让面试官看出你的思路也很重要。 比如[小兔的棋盘](https://github.com/azl397985856/leetcode/issues/429 "小兔的棋盘") 想出最优解难度就不低,不过你可以先暴力 DFS 解决,再 DP 优化会慢慢帮你打开思路。有时候面试官也会引导你,给你提示, 加上你刚才“发挥不错”,说不定一下子就做出最优解了,这个我深有体会。 + +另外要提醒大家的是, 刷题要适量,不要贪多。要完全理清一道题的来龙去脉。多问几个为什么。 这道题暴力法怎么做?暴力法哪有问题?怎么优化?为什么选了这个算法就可以优化?为什么这种算法要用这种数据结构来实现? + +更多题解可以访问我的 LeetCode 题解仓库:https://github.com/azl397985856/leetcode 。 目前已经 36K+ star 啦。 + +关注公众号力扣加加,努力用清晰直白的语言还原解题思路,并且有大量图解,手把手教你识别套路,高效刷题。 + +![](https://tva1.sinaimg.cn/large/007S8ZIlly1gfcuzagjalj30p00dwabs.jpg) diff --git a/selected/byte-dance-algo-ex.md b/selected/byte-dance-algo-ex.md new file mode 100644 index 0000000..d159f0d --- /dev/null +++ b/selected/byte-dance-algo-ex.md @@ -0,0 +1,269 @@ +# 字节跳动的算法面试题是什么难度? + +由于 lucifer 我是一个小前端, 最近也在准备写一个《前端如何搞定算法面试》的专栏,因此最近没少看各大公司的面试题。都说字节跳动算法题比较难,我就先拿 ta 下手,做了几套 。这次我们就拿一套 `2018 年的前端校招(第四批)`来看下字节的算法笔试题的难度几何。地址:https://www.nowcoder.com/test/8536639/summary + +> 实际上,这套字节的前端岗位笔试题和后端以及算法岗位的笔试题也只有一道题目(红包的设计题被换成了另外一个设计题)不一样而已,因此也不需要担心你不是前端,题目类型和难度和你的岗位不匹配。 + +这套题一共四道题, 两道问答题, 两道编程题。 + +其中一道问答题是 LeetCode 426 的原题,只不过题型变成了找茬(改错)。可惜的是 LeetCode 的 426 题是一个会员题目,没有会员的就看不来了。不过,剑指 Offer 正好也有这个题,并且力扣将剑指 Offer 全部的题目都 OJ 化了。 这道题大家可以去 https://leetcode-cn.com/problems/er-cha-sou-suo-shu-yu-shuang-xiang-lian-biao-lcof 提交答案。简单说一下这个题目的思路,我们只需要中序遍历即可得到一个有序的数列,同时在中序遍历过程中将 pre 和 cur 节点通过指针串起来即可。 + +另一个问答是红包题目,这里不多说了。我们重点看一下剩下两个算法编程题。 + +![](https://tva1.sinaimg.cn/large/007S8ZIlly1gigxwqs84rj312d0u0the.jpg) + +> 两个问答题由于不能在线判题,我没有做,只做了剩下两个编程题。 + +## 球队比赛 + +第一个编程题是一个球队比赛的题目。 + +### 题目描述 + +有三只球队,每只球队编号分别为球队 1,球队 2,球队 3,这三只球队一共需要进行 n 场比赛。现在已经踢完了 k 场比赛,每场比赛不能打平,踢赢一场比赛得一分,输了不得分不减分。已知球队 1 和球队 2 的比分相差 d1 分,球队 2 和球队 3 的比分相差 d2 分,每场比赛可以任意选择两只队伍进行。求如果打完最后的 (n-k) 场比赛,有没有可能三只球队的分数打平。 + +### 思路 + +假设球队 1,球队 2,球队 3 此时的胜利次数分别为 a,b,c,球队 1,球队 2,球队 3 总的胜利次数分别为 n1,n2,n3。 + +我一开始的想法是只要保证 n1,n2,n3 相等且都小于等于 n / 3 即可。如果题目给了 n1,n2,n3 的值就直接: + +``` +print(n1 == n2 == n3 == n / 3) +``` + +可是不仅 n1,n2,n3 没给, a,b,c 也没有给。 + +实际上此时我们的信息仅仅是: + +``` +① a + b + c = k +② a - b = d1 or b - a = d1 +③ b - c = d2 or c - b = d2 +``` + +其中 k 和 d1,d2 是已知的。a ,b,c 是未知的。 也就是说我们需要枚举所有的 a,b,c 可能性,解方程求出合法的 a,b,c,并且 合法的 a,b,c 都小于等于 n / 3 即可。 + +> 这个 a,b,c 的求解数学方程就是中学数学难度, 三个等式化简一下即可,具体见下方代码区域。 + +- a 只需要再次赢得 n / 3 - a 次 +- b 只需要再次赢得 n / 3 - b 次 +- c 只需要再次赢得 n / 3 - c 次 + +``` +n1 = a + n / 3 - a = n / 3 +n2 = b + (n / 3 - b) = n / 3 +n3 = c + (n / 3 - c) = n / 3 +``` + +### 代码(Python) + +> 牛客有点让人不爽, 需要 print 而不是 return + +```py +t = int(input()) +for i in range(t): + n, k, d1, d2 = map(int, input().split(" ")) + if n % 3 != 0: + print('no') + continue + abcs = [] + for r1 in [-1, 1]: + for r2 in [-1, 1]: + a = (k + 2 * r1 * d1 + r2 * d2) / 3 + b = (k + -1 * r1 * d1 + r2 * d2) / 3 + c = (k + -1 * r1 * d1 + -2 * r2 * d2) / 3 + a + r1 + if 0 <= a <= k and 0 <= b <= k and 0 <= c <= k and a.is_integer() and b.is_integer() and c.is_integer(): + abcs.append([a, b, c]) + flag = False + for abc in abcs: + if len(abc) > 0 and max(abc) <= n / 3: + flag = True + break + if flag: + print('yes') + else: + print('no') +``` + +**复杂度分析** + +- 时间复杂度:$O(t)$ +- 空间复杂度:$O(t)$ + +### 小结 + +感觉这个难度也就是力扣中等水平吧,力扣也有一些数学等式转换的题目, 比如 [494.target-sum](https://github.com/azl397985856/leetcode/blob/master/problems/494.target-sum.md "494.target-sum") + +## 转换字符串 + +### 题目描述 + +有一个仅包含’a’和’b’两种字符的字符串 s,长度为 n,每次操作可以把一个字符做一次转换(把一个’a’设置为’b’,或者把一个’b’置成’a’);但是操作的次数有上限 m,问在有限的操作数范围内,能够得到最大连续的相同字符的子串的长度是多少。 + +### 思路 + +看完题我就有种似曾相识的感觉。 + +> 每次对妹子说出这句话的时候,她们都会觉得好假 ^\_^ + +不过这次是真的。 ”哦,不!每次都是真的“。 这道题其实就是我之前写的滑动窗口的一道题[【1004. 最大连续 1 的个数 III】滑动窗口(Python3)](https://leetcode-cn.com/problems/max-consecutive-ones-iii/solution/1004-zui-da-lian-xu-1de-ge-shu-iii-hua-dong-chuang/ "【1004. 最大连续 1 的个数 III】滑动窗口(Python3)")的换皮题。 专题地址:https://github.com/azl397985856/leetcode/blob/master/thinkings/slide-window.md + +所以说,如果这道题你完全没有思路的话。说明: + +- 抽象能力不够。 +- 滑动窗口问题理解不到位。 + +第二个问题可以看我上面贴的地址,仔细读读,并完成课后练习即可解决。 + +第一个问题就比较困难了, 不过多看我的题解也可以慢慢提升的。比如: + +- [《割绳子》](https://leetcode-cn.com/problems/jian-sheng-zi-ii-lcof/ "《割绳子》") 实际上就是 [343. 整数拆分](https://lucifer.ren/blog/2020/05/16/343.integer-break/ "343. 整数拆分") 的换皮题。 + +- 力扣 230 和 力扣 645 就是换皮题,详情参考[位运算专题](https://lucifer.ren/blog/2020/03/24/bit/ "位运算专题") + +- 以及 [你的衣服我扒了 - 《最长公共子序列》](https://lucifer.ren/blog/2020/07/01/LCS/) + +- 以及 [穿上衣服我就不认识你了?来聊聊最长上升子序列](https://lucifer.ren/blog/2020/06/20/LIS/) + +- 以及 [一招吃遍力扣四道题,妈妈再也不用担心我被套路啦~](https://lucifer.ren/blog/2020/06/13/%E5%88%A0%E9%99%A4%E9%97%AE%E9%A2%98/) + +- 等等 + +回归这道题。其实我们只需要稍微抽象一下, 就是一个纯算法题。 抽象的另外一个好处则是将很多不同的题目返璞归真,从而可以在茫茫题海中逃脱。这也是我开启[《我是你的妈妈呀》](https://lucifer.ren/blog/2020/08/03/mother-01/) 的原因之一。 + +如果我们把 a 看成是 0 , b 看成是 1。或者将 b 看成 1, a 看成 0。不就抽象成了: + +``` + +给定一个由若干 0 和 1 组成的数组 A,我们最多可以将 m 个值从 0 变成 1 。 + +返回仅包含 1 的最长(连续)子数组的长度。 + +``` + +这就是 力扣 [1004. 最大连续 1 的个数 III](https://leetcode-cn.com/problems/max-consecutive-ones-iii/solution/1004-zui-da-lian-xu-1de-ge-shu-iii-hua-dong-chuang/) 原题。 + +因此实际上我们要求的是上面两种情况: + +1. a 表示 0, b 表示 1 +2. a 表示 1, b 表示 0 + +的较大值。 + +> lucifer 小提示: 其实我们也可以仅仅考虑一种情况,比如 a 看成是 0 , b 看成是 1。这个时候, 我们操作变成了两种情况,0 变成 1 或者 1 变成 0,同时求解的也变成了最长连续 0 或者 最长连续 1 。 由于这种抽象操作起来更麻烦, 我们不考虑。 + +问题得到了抽象就好解决了。我们只需要记录下加入窗口的是 0 还是 1: + +- 如果是 1,我们什么都不用做 +- 如果是 0,我们将 m 减 1 + +相应地,我们需要记录移除窗口的是 0 还是 1: + +- 如果是 1,我们什么都不做 +- 如果是 0,说明加进来的时候就是 1,加进来的时候我们 m 减去了 1,这个时候我们再加 1。 + +> lucifer 小提示: 实际上题目中是求**连续** a 或者 b 的长度。看到连续,大家也应该有滑动窗口的敏感度, 别管行不行, 想到总该有的。 + +我们拿 A = [1, 1, 0, 1, 0, 1], m = 1 来说。看下算法的具体过程: + +> lucifer 小提示: 左侧的数字表示此时窗口大小,黄色格子表示修补的墙,黑色方框表示的是窗口。 + +![](https://tva1.sinaimg.cn/large/007S8ZIlly1gih11ey3hhj30ks05o0sx.jpg) + +这里我形象地将 0 看成是洞,1 看成是墙, 我们的目标就是补洞,使得连续的墙最长。 + +![](https://tva1.sinaimg.cn/large/007S8ZIlly1gih12xgf04j30ik054dfx.jpg) + +每次碰到一个洞,我们都去不加选择地修补。由于 m 等于 1, 也就是说我们最多补一个洞。因此需要在修补超过一个洞的时候,我们需要调整窗口范围,使得窗口内最多修补一个墙。由于窗口表示的就是连续的墙(已有的或者修补的),因此最终我们返回窗口的最大值即可。 + +![](https://tva1.sinaimg.cn/large/007S8ZIlly1gih1588r5kj30xe0dm770.jpg) + +> 由于下面的图窗口内有两个洞,这和”最多补一个洞“冲突, 我们需要收缩窗口使得满足“最多补一个洞”的先决条件。 + +![](https://tva1.sinaimg.cn/large/007S8ZIlly1gih1ac1v5ij30o60ba76r.jpg) + +因此最大的窗口就是 max(2, 3, 4, ...) = 4。 + +> lucifer 小提示: 可以看出我们不加选择地修补了所有的洞,并调整窗口,使得窗口内最多有 m 个修补的洞,因此窗口的最大值就是答案。然而实际上,我们并不需要真的”修补“(0 变成 1),而是仅仅修改 m 的值即可。 + +我们先来看下抽象之后的**其中一种情况**的代码: + +```py +class Solution: + def longestOnes(self, A: List[int], m: int) -> int: + i = 0 + for j in range(len(A)): + m -= 1 - A[j] + if m < 0: + m += 1 - A[i] + i += 1 + return j - i + 1 + +``` + +因此**完整代码**就是: + +```py +class Solution: + def longestOnes(self, A: List[int], m: int) -> int: + i = 0 + for j in range(len(A)): + m -= 1 - A[j] + if m < 0: + m += 1 - A[i] + i += 1 + return j - i + 1 + def longestAorB(self, A:List[int], m: int) -> int: + return max(self.longestOnes(map(lambda x: 0 if x == 'a' else 1, A) ,m), self.longestOnes(map(lambda x: 1 if x == 'a' else 0, A),m)) +``` + +这里的两个 map 会生成两个不同的数组。 我只是为了方便大家理解才新建的两个数组, 实际上根本不需要,具体见后面的代码. + +### 代码(Python) + +```py +i = 0 +n, m = map(int, input().split(" ")) +s = input() +ans = 0 +k = m # 存一下,后面也要用这个初始值 +# 修补 b +for j in range(n): + m -= ord(s[j]) - ord('a') + if m < 0: + m += ord(s[i]) - ord('a') + i += 1 +ans = j - i + 1 +i = 0 +# 修补 a +for j in range(n): + k += ord(s[j]) - ord('b') + if k < 0: + k -= ord(s[i]) - ord('b') + i += 1 +print(max(ans, j - i + 1)) + +``` + +**复杂度分析** + +- 时间复杂度:$O(N)$ +- 空间复杂度:$O(1)$ + +### 小结 + +这道题就是一道换了皮的力扣题,难度中等。如果你能将问题抽象,同时又懂得滑动窗口,那这道题就很容易。我看了题解区的参考答案, 内容比较混乱,不够清晰。这也是我写下这篇文章的原因之一。 + +## 总结 + +这一套字节跳动的题目一共四道,一道设计题,三道算法题。 + +其中三道算法题从难度上来说,基本都是中等难度。从内容来看,基本都是力扣的换皮题。但是如果我不说他们是换皮题, 你们能发现么? 如果你可以的话,说明你的抽象能力已经略有小成了。如果看不出来也没有关系,关注我。 手把手扒皮给你们看,扒多了慢慢就会了。切记,不要盲目做题!如果你做了很多题, 这几道题还是看不出套路,说明你该缓缓,改变下刷题方式了。 + +更多题解可以访问我的 LeetCode 题解仓库:https://github.com/azl397985856/leetcode 。 目前已经 36K+ star 啦。 + +关注公众号力扣加加,努力用清晰直白的语言还原解题思路,并且有大量图解,手把手教你识别套路,高效刷题。 + +![](https://tva1.sinaimg.cn/large/007S8ZIlly1gfcuzagjalj30p00dwabs.jpg) diff --git a/selected/construct-binary-tree.md b/selected/construct-binary-tree.md new file mode 100644 index 0000000..914b5d5 --- /dev/null +++ b/selected/construct-binary-tree.md @@ -0,0 +1,259 @@ +# 构造二叉树系列 + +构造二叉树是一个常见的二叉树考点,相比于直接考察二叉树的遍历,这种题目的难度会更大。截止到目前(2020-02-08) LeetCode 关于构造二叉树一共有三道题目,分别是: + +- [105. 从前序与中序遍历序列构造二叉树](https://leetcode-cn.com/problems/construct-binary-tree-from-preorder-and-inorder-traversal/) +- [106. 从中序与后序遍历序列构造二叉树](https://leetcode-cn.com/problems/construct-binary-tree-from-inorder-and-postorder-traversal/) +- [889. 根据前序和后序遍历构造二叉树](https://leetcode-cn.com/problems/construct-binary-tree-from-preorder-and-postorder-traversal/) + +今天就让我们用一个套路一举攻破他们。 + +## 105. 从前序与中序遍历序列构造二叉树 + +### 题目描述 + +``` +根据一棵树的前序遍历与中序遍历构造二叉树。 + +注意: +你可以假设树中没有重复的元素。 + +例如,给出 + +前序遍历 preorder = [3,9,20,15,7] +中序遍历 inorder = [9,3,15,20,7] +返回如下的二叉树: + + 3 + / \ + 9 20 + / \ + 15 7 +``` + +### 思路 + +我们以题目给出的测试用例来讲解: +![](https://pic.leetcode-cn.com/584db66158d2b497b9fdd69b5dc10c3a76db6e2c0f6cff68789cfb79807b0756.jpg) + +前序遍历是`根左右`,因此 preorder 第一个元素一定整个树的根。由于题目说明了没有重复元素,因此我们可以通过 val 去 inorder 找到根在 inorder 中的索引 i。 +而由于中序遍历是`左根右`,我们容易找到 i 左边的都是左子树,i 右边都是右子树。 + +我使用红色表示根,蓝色表示左子树,绿色表示右子树。 + +![](https://pic.leetcode-cn.com/faea3d9a78c1fa623457b28c8d20e09a47bb0911d78ff53f42fab0e463a7755d.jpg) + +根据此时的信息,我们能构造的树是这样的: + +![](https://pic.leetcode-cn.com/261696c859c562ca31dface08d3020bcd20362ab2205d614473cca02b1635eb0.jpg) + +我们 preorder 继续向后移动一位,这个时候我们得到了第二个根节点”9“,实际上就是左子树的根节点。 + +![](https://pic.leetcode-cn.com/eb8311e01ed86007b23460d6c933b53ad14bec2d63a0dc01f625754368f22376.jpg) + +我们 preorder 继续向后移动一位,这个时候我们得到了第二个根节点”20“,实际上就是右子树的根节点。其中右子树由于个数大于 1,我们无法确定,我们继续执行上述逻辑。 + +![](https://pic.leetcode-cn.com/d90dc9bae9d819da997eb67d445524c8ef39ce2a4a8defb16b5a3b6b2a0fc783.jpg) + +根据此时的信息,我们能构造的树是这样的: + +![](https://pic.leetcode-cn.com/f8553f668bed9f897f393a24d78e4469c4b5503c4ba8c59e90dca1b19acf4de5.jpg) + +我们不断执行上述逻辑即可。简单起见,递归的时候每次我都开辟了新的数组,这个其实是没有必要的,我们可以通过四个变量来记录 inorder 和 preorder 的起始位置即可。 + +### 代码 + +代码支持:Python3 + +Python3 Code: + +```python +class Solution: + def buildTree(self, preorder: List[int], inorder: List[int]) -> TreeNode: + # 实际上inorder 和 postorder一定是同时为空的,因此你无论判断哪个都行 + if not preorder: + return None + root = TreeNode(preorder[0]) + + i = inorder.index(root.val) + root.left = self.buildTree(preorder[1:i + 1], inorder[:i]) + root.right = self.buildTree(preorder[i + 1:], inorder[i+1:]) + + return root +``` + +**复杂度分析** + +- 时间复杂度:由于每次递归我们的 inorder 和 preorder 的总数都会减 1,因此我们要递归 N 次,故时间复杂度为 $O(N)$,其中 N 为节点个数。 +- 空间复杂度:我们使用了递归,也就是借助了额外的栈空间来完成, 由于栈的深度为 N,因此总的空间复杂度为 $O(N)$,其中 N 为节点个数。 + +> 空间复杂度忽略了开辟数组的内存消耗。 + +## 106. 从中序与后序遍历序列构造二叉树 + +如果你会了上面的题目,那么这个题目对你来说也不是难事,我们来看下。 + +### 题目描述 + +``` +根据一棵树的中序遍历与后序遍历构造二叉树。 + +注意: +你可以假设树中没有重复的元素。 + +例如,给出 + +中序遍历 inorder = [9,3,15,20,7] +后序遍历 postorder = [9,15,7,20,3] +返回如下的二叉树: + + 3 + / \ + 9 20 + / \ + 15 7 +``` + +### 思路 + +我们以题目给出的测试用例来讲解: +![](https://pic.leetcode-cn.com/fb9d700a67d70b5e68461fa1f0438d9c5c676557a776eda4cd1b196c41ce65a1.jpg) + +后序遍历是`左右根`,因此 postorder 最后一个元素一定整个树的根。由于题目说明了没有重复元素,因此我们可以通过 val 去 inorder 找到根在 inorder 中的索引 i。 +而由于中序遍历是`左根右`,我们容易找到 i 左边的都是左子树,i 右边都是右子树。 + +我使用红色表示根,蓝色表示左子树,绿色表示右子树。 + +![](https://pic.leetcode-cn.com/10176eec270c90d8e0bd4640a628e9320b7d5c30f3c62ffdb1fd2800d87c6f7b.jpg) + +根据此时的信息,我们能构造的树是这样的: + +![](https://pic.leetcode-cn.com/261696c859c562ca31dface08d3020bcd20362ab2205d614473cca02b1635eb0.jpg) + +其中右子树由于个数大于 1,我们无法确定,我们继续执行上述逻辑。我们 postorder 继续向前移动一位,这个时候我们得到了第二个根节点”20“,实际上就是右子树的根节点。 + +![](https://pic.leetcode-cn.com/e6cac2b6a956c09d977c4cfd7883268644b42bdd0531a509d24b4aafebc147c4.jpg) + +根据此时的信息,我们能构造的树是这样的: + +![](https://pic.leetcode-cn.com/f8553f668bed9f897f393a24d78e4469c4b5503c4ba8c59e90dca1b19acf4de5.jpg) + +我们不断执行上述逻辑即可。简单起见,递归的时候每次我都开辟了新的数组,这个其实是没有必要的,我们可以通过四个变量来记录 inorder 和 postorder 的起始位置即可。 + +### 代码 + +代码支持:Python3 + +Python3 Code: + +```python +class Solution: + def buildTree(self, inorder: List[int], postorder: List[int]) -> TreeNode: + # 实际上inorder 和 postorder一定是同时为空的,因此你无论判断哪个都行 + if not inorder: + return None + root = TreeNode(postorder[-1]) + i = inorder.index(root.val) + root.left = self.buildTree(inorder[:i], postorder[:i]) + root.right = self.buildTree(inorder[i+1:], postorder[i:-1]) + + return root +``` + +**复杂度分析** + +- 时间复杂度:由于每次递归我们的 inorder 和 postorder 的总数都会减 1,因此我们要递归 N 次,故时间复杂度为 $O(N)$,其中 N 为节点个数。 +- 空间复杂度:我们使用了递归,也就是借助了额外的栈空间来完成, 由于栈的深度为 N,因此总的空间复杂度为 $O(N)$,其中 N 为节点个数。 + +> 空间复杂度忽略了开辟数组的内存消耗。 + +## 889. 根据前序和后序遍历构造二叉树 + +### 题目描述 + +``` +返回与给定的前序和后序遍历匹配的任何二叉树。 + + pre 和 post 遍历中的值是不同的正整数。 + +  + +示例: + +输入:pre = [1,2,4,5,3,6,7], post = [4,5,2,6,7,3,1] +输出:[1,2,3,4,5,6,7] +  + +提示: + +1 <= pre.length == post.length <= 30 +pre[] 和 post[] 都是 1, 2, ..., pre.length 的排列 +每个输入保证至少有一个答案。如果有多个答案,可以返回其中一个。 + +``` + +### 思路 + +我们以题目给出的测试用例来讲解: +![](https://pic.leetcode-cn.com/584db66158d2b497b9fdd69b5dc10c3a76db6e2c0f6cff68789cfb79807b0756.jpg) + +前序遍历是`根左右`,因此 preorder 第一个元素一定整个树的根,preorder 第二个元素(如果存在的话)一定是左子树。由于题目说明了没有重复元素,因此我们可以通过 val 去 postorder 找到 pre[1]在 postorder 中的索引 i。 +而由于后序遍历是`左右根`,因此我们容易得出。 postorder 中的 0 到 i(包含)是左子树,preorder 的 1 到 i+1(包含)也是左子树。 + +其他部分可以参考上面两题。 + +### 代码 + +代码支持:Python3 + +Python3 Code: + +```python +class Solution: + def constructFromPrePost(self, pre: List[int], post: List[int]) -> TreeNode: + # 实际上pre 和 post一定是同时为空的,因此你无论判断哪个都行 + if not pre: + return None + node = TreeNode(pre[0]) + if len(pre) == 1: + return node + i = post.index(pre[1]) + + node.left = self.constructFromPrePost(pre[1:i + 2], post[:i + 1]) + node.right = self.constructFromPrePost(pre[i + 2:], post[i + 1:-1]) + + return node +``` + +**复杂度分析** + +- 时间复杂度:由于每次递归我们的 postorder 和 preorder 的总数都会减 1,因此我们要递归 N 次,故时间复杂度为 $O(N)$,其中 N 为节点个数。 +- 空间复杂度:我们使用了递归,也就是借助了额外的栈空间来完成, 由于栈的深度为 N,因此总的空间复杂度为 $O(N)$,其中 N 为节点个数。 + +> 空间复杂度忽略了开辟数组的内存消耗。 + +## 总结 + +如果你仔细对比一下的话,会发现我们的思路和代码几乎一模一样。注意到每次递归我们的两个数组个数都会减去 1,因此我们递归终止条件不难写出,并且递归问题规模如何缩小也很容易,那就是数组总长度减去 1。 + +我们拿最后一个题目来说: + +```python +node.left = self.constructFromPrePost(pre[1:i + 2], post[:i + 1]) +node.right = self.constructFromPrePost(pre[i + 2:], post[i + 1:-1]) + +``` + +我们发现 pre 被拆分为两份,pre[1:i + 2]和 pre[i + 2:]。很明显总数少了 1,那就是 pre 的第一个元素。 也就是说如果你写出一个,其他一个不用思考也能写出来。 + +而对于 post 也一样,post[:i + 1] 和 post[i + 1:-1],很明显总数少了 1,那就是 post 最后一个元素。 + +这个解题模板足够简洁,并且逻辑清晰,大家可以用我的模板试试~ + +## 关注我 + +更多题解可以访问我的 LeetCode 题解仓库:https://github.com/azl397985856/leetcode 。 目前已经 30K star 啦。 + +大家也可以关注我的公众号《力扣加加》获取更多更新鲜的 LeetCode 题解 + +![](https://tva1.sinaimg.cn/large/007S8ZIlly1gfcuzagjalj30p00dwabs.jpg) diff --git a/selected/mother-01.md b/selected/mother-01.md new file mode 100644 index 0000000..1a782f3 --- /dev/null +++ b/selected/mother-01.md @@ -0,0 +1,410 @@ +# 《我是你的妈妈呀》 - 第一期 + +记得我初中的时候,学校发的一个小册子的名字就是母题啥的。 + +![](https://tva1.sinaimg.cn/large/007S8ZIlly1ghbhlyhaadj308c08c3yv.jpg) + +大概意思是市面上的题(尤其是中考题)都是这些母题生的,都是它们的儿子。 + +熟悉我的朋友应该知道,我有一个风格:”喜欢用通俗易懂的语言以及图片,还原解题过程“。包括我是如何抽象的,如何与其他题目建立联系的等。比如: + +- [一招吃遍力扣四道题,妈妈再也不用担心我被套路啦~](https://leetcode-cn.com/problems/smallest-subsequence-of-distinct-characters/solution/yi-zhao-chi-bian-li-kou-si-dao-ti-ma-ma-zai-ye-b-6/) +- [超级详细记忆化递归,图解,带你一次攻克三道 Hard 套路题(44. 通配符匹配)](https://leetcode-cn.com/problems/wildcard-matching/solution/chao-ji-xiang-xi-ji-yi-hua-di-gui-tu-jie-dai-ni-yi/) +- [穿上衣服我就不认识你了?来聊聊最长上升子序列](https://leetcode-cn.com/problems/minimum-number-of-arrows-to-burst-balloons/solution/chuan-shang-yi-fu-wo-jiu-bu-ren-shi-ni-liao-lai-3/) +- [扒一扒这种题的外套(343. 整数拆分)](https://leetcode-cn.com/problems/integer-break/solution/ba-yi-ba-zhe-chong-ti-de-wai-tao-343-zheng-shu-cha/) + +如果把这个思考过程称之为自顶向下的话,那么实际上能写出来取决于你: + +- 是否有良好的抽象能力 +- 是否有足够的基础知识 +- 是否能与学过的基础知识建立联系 + +如果反着呢? 我先把所有抽象之后的纯粹的东西掌握,也就是母题。那么遇到新的题,我就往上套呗?这就是我在《LeetCode 题解仓库》中所说的**只有熟练掌握基础的数据结构与算法,才能对复杂问题迎刃有余。** 这种思路就是**自底向上**。(有点像动态规划?) 市面上的题那么多,但是题目类型就是那几种。甚至出题人出题的时候都是根据以前的题目变个条件,变个说法从而搞出一个“新”的题。 + +这个专题的目标就是从反的方向来,我们先学习和记忆底层的被抽象过的经典的题目。遇到新的题目,就往这些母题上套即可。 + +那让我们来自底向上看下第一期的这八道母题吧~ + +## 母题 1 + +### 题目描述 + +给你两个有序的非空数组 nums1 和 nums2,让你从每个数组中分别挑一个,使得二者差的绝对值最小。 + +### 思路 + +- 初始化 ans 为无限大 +- 使用两个指针,一个指针指向数组 1,一个指针指向数组 2 +- 比较两个指针指向的数字的大小,并更新较小的那个的指针,使其向后移动一位。更新的过程顺便计算 ans +- 最后返回 ans + +### 代码 + +```py +def f(nums1, nums2): + i = j = 0 + ans = float('inf') + while i < len(nums1) and j < len(nums2): + ans = min(ans, abs(nums1[i] - nums2[j])) + if nums1[i] < nums2[j]: + i += 1 + else: + j += 1 + return ans + +``` + +**复杂度分析** + +- 时间复杂度:$O(N)$ +- 空间复杂度:$O(1)$ + +## 母题 2 + +### 题目描述 + +给你两个非空数组 nums1 和 nums2,让你从每个数组中分别挑一个,使得二者差的绝对值最小。 + +### 思路 + +数组没有说明是有序的,可以选择暴力。两两计算绝对值,返回最小的即可。 + +代码: + +```py +def f(nums1, nums2): + ans = float('inf') + for num1 in nums1: + for num2 in nums2: + ans = min(ans, abs(num1 - num2)) + return ans +``` + +**复杂度分析** + +- 时间复杂度:$O(N ^ 2)$ +- 空间复杂度:$O(1)$ + +由于暴力的时间复杂度是 $O(N^2)$,因此其实也可以先排序将问题转换为母题 1,然后用母题 1 的解法求解。 + +**复杂度分析** + +- 时间复杂度:$O(NlogN)$ +- 空间复杂度:$O(1)$ + +## 母题 3 + +### 题目描述 + +给你 k 个有序的非空数组,让你从每个数组中分别挑一个,使得二者差的绝对值最小。 + +### 思路 + +继续使用母题 1 的思路,使用 k 个 指针即可。 + +**复杂度分析** + +- 时间复杂度:$O(klogM)$,其中 M 为 k 个非空数组的长度的最小值。 +- 空间复杂度:$O(1)$ + +我们也可以使用堆来处理,代码更简单,逻辑更清晰。这里我们使用小顶堆,作用就是选出最小值。 + +### 代码 + +```py +def f(matrix): + ans = float('inf') + max_value = max(nums[0] for nums in matrix) + heap = [(nums[0], i, 0) for i, nums in enumerate(nums)] + heapq.heapify(heap) + + while True: + min_value, row, idx = heapq.heappop(heap) + if max_value - min_value < ans: + ans = max_value - min_value + if idx == len(matrix[row]) - 1: + break + max_value = max(max_value, matrix[row][idx + 1]) + heapq.heappush(heap, (matrix[row][idx + 1], row, idx + 1)) + + return ans + + +``` + +**复杂度分析** + +建堆的时间和空间复杂度为 $O(k)$。 + +while 循环会执行 M 次 ,其中 M 为 k 个非空数组的长度的最小值。heappop 和 heappush 的时间复杂度都是 logk。因此 while 循环总的时间复杂度为 $O(Mlogk)$。 + +- 时间复杂度:$O(max(Mlogk, k))$,其中 M 为 k 个非空数组的长度的最小值。 +- 空间复杂度:$O(k)$ + +## 母题 4 + +### 题目描述 + +给你 k 个非空数组,让你从每个数组中分别挑一个,使得二者差的绝对值最小。 + +### 思路 + +先排序,然后转换为母题 3 + +## 母题 5 + +### 题目描述 + +给你两个有序的非空数组 nums1 和 nums2,让你将两个数组合并,使得新的数组有序。 + +LeetCode 地址: https://leetcode-cn.com/problems/merge-sorted-array/ + +### 思路 + +和母题 1 类似。 + +### 代码 + +```py +def f(nums1, nums2): + i = j = 0 + ans = [] + while i < len(nums1) and j < len(nums2): + if nums1[i] < nums2[j]: + ans.append(nums1[i]) + i += 1 + else: + ans.append(nums2[j]) + j += 1 + if nums1: + ans += nums2[j:] + else: + ans += nums1[i:] + return ans +``` + +**复杂度分析** + +- 时间复杂度:$O(N)$ +- 空间复杂度:$O(1)$ + +## 母题 6 + +### 题目描述 + +给你 k 个有序的非空数组 nums1 和 nums2,让你将 k 个数组合并,使得新的数组有序。 + +### 思路 + +和母题 5 类似。 只不过不是两个,而是多个。我们继续套用堆的思路。 + +### 代码 + +```py +import heapq + + +def f(matrix): + ans = [] + heap = [] + for row in matrix: + heap += row + heapq.heapify(heap) + + while heap: + cur = heapq.heappop(heap) + ans.append(cur) + + return ans + + +``` + +**复杂度分析** + +建堆的时间和空间复杂度为 $O(N)$。 + +heappop 的时间复杂度为 $O(logN)$。 + +- 时间复杂度:$O(NlogN)$,其中 N 是矩阵中的数字总数。 +- 空间复杂度:$O(N)$,其中 N 是矩阵中的数字总数。 + +## 母题 7 + +### 题目描述 + +给你两个有序的链表 root1 和 root2,让你将两个链表合并,使得新的链表有序。 + +LeetCode 地址:https://leetcode-cn.com/problems/merge-two-sorted-lists/ + +### 思路 + +和母题 5 类似。 不同的地方在于数据结构从数组变成了链表,我们只需要注意链表的操作即可。 + +这里我使用了迭代和递归两种方式。 + +> 大家可以把母题 5 使用递归写一下。 + +### 代码 + +```py +# Definition for singly-linked list. +class ListNode: + def __init__(self, x): + self.val = x + self.next = None + +class Solution: + def mergeTwoLists(self, l1: ListNode, l2: ListNode) -> ListNode: + if not l1: return l2 + if not l2: return l1 + if l1.val < l2.val: + l1.next = self.mergeTwoLists(l1.next, l2) + return l1 + else: + l2.next = self.mergeTwoLists(l1, l2.next) + return l2 +``` + +**复杂度分析** + +- 时间复杂度:$O(N)$,其中 N 为两个链表中较短的那个的长度。 +- 空间复杂度:$O(N)$,其中 N 为两个链表中较短的那个的长度。 + +```py +# Definition for singly-linked list. +class ListNode: + def __init__(self, x): + self.val = x + self.next = None + +class Solution: + def mergeTwoLists(self, l1: ListNode, l2: ListNode) -> ListNode: + if not l1: return l2 + if not l2: return l1 + ans = cur = ListNode(0) + while l1 and l2: + if l1.val < l2.val: + cur.next = l1 + cur = cur.next + l1 = l1.next + else: + cur.next = l2 + cur = cur.next + l2 = l2.next + + + if l1: + cur.next = l1 + else: + cur.next = l2 + return ans.next +``` + +**复杂度分析** + +- 时间复杂度:$O(N)$,其中 N 为两个链表中较短的那个的长度。 +- 空间复杂度:$O(1)$ + +## 母题 8 + +### 题目描述 + +给你 k 个有序的链表,让你将 k 个链表合并,使得新的链表有序。 + +LeetCode 地址:https://leetcode-cn.com/problems/merge-k-sorted-lists/ + +### 思路 + +和母题 7 类似,我们使用递归可以轻松解决。其实本质上就是 + +### 代码 + +```py +# Definition for singly-linked list. +class ListNode: + def __init__(self, x): + self.val = x + self.next = None + +class Solution: + def mergeTwoLists(self, l1: ListNode, l2: ListNode) -> ListNode: + if not l1: return l2 + if not l2: return l1 + if l1.val < l2.val: + l1.next = self.mergeTwoLists(l1.next, l2) + return l1 + else: + l2.next = self.mergeTwoLists(l1, l2.next) + return l2 + def mergeKLists(self, lists: List[ListNode]) -> ListNode: + if not lists: return None + if len(lists) == 1: return lists[0] + return self.mergeTwoLists(lists[0], self.mergeKLists(lists[1:])) + + +``` + +**复杂度分析** + +mergeKLists 执行了 k 次,每次都执行一次 mergeTwoLists,mergeTwoLists 的时间复杂度前面已经分析过了,为 $O(N)$,其中 N 为两个链表中较短的那个的长度。 + +- 时间复杂度:$O(k * N)$,其中 N 为两个链表中较短的那个的长度 +- 空间复杂度:$O(max(k, N))$ + +```py +# Definition for singly-linked list. +class ListNode: + def __init__(self, x): + self.val = x + self.next = None + +class Solution: + def mergeTwoLists(self, l1: ListNode, l2: ListNode) -> ListNode: + if not l1: return l2 + if not l2: return l1 + if l1.val < l2.val: + l1.next = self.mergeTwoLists(l1.next, l2) + return l1 + else: + l2.next = self.mergeTwoLists(l1, l2.next) + return l2 + def mergeKLists(self, lists: List[ListNode]) -> ListNode: + if not lists: return None + if len(lists) == 1: return lists[0] + return self.mergeTwoLists(self.mergeKLists(lists[:len(lists) // 2]), self.mergeKLists(lists[len(lists) // 2:])) +``` + +**复杂度分析** + +mergeKLists 执行了 logk 次,每次都执行一次 mergeTwoLists,mergeTwoLists 的时间复杂度前面已经分析过了,为 $O(N)$,其中 N 为两个链表中较短的那个的长度。 + +- 时间复杂度:$O(Nlogk)$,其中 N 为两个链表中较短的那个的长度 +- 空间复杂度:$O(max(logk, N))$,其中 N 为两个链表中较短的那个的长度 + +## 全家福 + +最后送大家一张全家福: + +![](https://tva1.sinaimg.cn/large/007S8ZIlly1ghbq8s05y0j31620u0gs0.jpg) + +## 子题 + +实际子题数量有很多,这里提供几个供大家练习。一定要练习,不能眼高手低。多看我的题解,多练习,多总结,你也可以的。 + +- [面试题 17.14. 最小 K 个数](https://leetcode-cn.com/problems/smallest-k-lcci/) +- [1200. 最小绝对差](https://leetcode-cn.com/problems/minimum-absolute-difference/) +- [632. 最小区间](https://leetcode-cn.com/problems/smallest-range-covering-elements-from-k-lists/) +- 两数和,三数和,四数和。。。 k 数和 + +## 总结 + +母题就是**抽象之后的纯粹的东西**。如果你掌握了母题,即使没有掌握抽象的能力,依然有可能套出来。但是随着题目做的变多,“抽象能力”也会越来越强。因为你知道这些题背后是怎么产生的。 + +本期给大家介绍了八道母题, 大家可以在之后的刷题过程中尝试使用母题来套模板。之后会给大家带来更多的母题。 + +更多题解可以访问我的 LeetCode 题解仓库:https://github.com/azl397985856/leetcode 。 目前已经 35K star 啦。 + +关注公众号力扣加加,努力用清晰直白的语言还原解题思路,并且有大量图解,手把手教你识别套路,高效刷题。 + +![](https://tva1.sinaimg.cn/large/007S8ZIlly1gfcuzagjalj30p00dwabs.jpg) diff --git a/selected/schedule-topic.md b/selected/schedule-topic.md new file mode 100644 index 0000000..c4b5c82 --- /dev/null +++ b/selected/schedule-topic.md @@ -0,0 +1,518 @@ +《我的日程安排表》截止目前(2020-02-03)在 LeetCode 上一共有三道题,其中两个中等难度,一个困难难度,分别是: + +- [729. 我的日程安排表 I](https://leetcode-cn.com/problems/my-calendar-i) +- [731. 我的日程安排表 II](https://leetcode-cn.com/problems/my-calendar-ii) +- [732. 我的日程安排表 III](https://leetcode-cn.com/problems/my-calendar-iii) + +另外 LeetCode 上有一个类似的系列《会议室》,截止目前(2020-02-03)有两道题目。其中一个简单一个中等,分别是: + +- [252. 会议室](https://leetcode-cn.com/problems/meeting-rooms/) +- [253. 会议室 II](https://leetcode-cn.com/problems/meeting-rooms-ii/) + +今天我们就来攻克它们。 + +# 729. 我的日程安排表 I + +## 题目地址 + +https://leetcode-cn.com/problems/my-calendar-i + +## 题目描述 + +实现一个 MyCalendar 类来存放你的日程安排。如果要添加的时间内没有其他安排,则可以存储这个新的日程安排。 + +MyCalendar 有一个 book(int start, int end)方法。它意味着在 start 到 end 时间内增加一个日程安排,注意,这里的时间是半开区间,即 [start, end), 实数  x 的范围为,  start <= x < end。 + +当两个日程安排有一些时间上的交叉时(例如两个日程安排都在同一时间内),就会产生重复预订。 + +每次调用 MyCalendar.book 方法时,如果可以将日程安排成功添加到日历中而不会导致重复预订,返回 true。否则,返回 false  并且不要将该日程安排添加到日历中。 + +请按照以下步骤调用 MyCalendar 类: MyCalendar cal = new MyCalendar(); MyCalendar.book(start, end) + +示例 1: + +MyCalendar(); +MyCalendar.book(10, 20); // returns true +MyCalendar.book(15, 25); // returns false +MyCalendar.book(20, 30); // returns true +解释: +第一个日程安排可以添加到日历中. 第二个日程安排不能添加到日历中,因为时间 15 已经被第一个日程安排预定了。 +第三个日程安排可以添加到日历中,因为第一个日程安排并不包含时间 20 。 +说明: + +每个测试用例,调用  MyCalendar.book  函数最多不超过  100 次。 +调用函数  MyCalendar.book(start, end)时, start 和  end 的取值范围为  [0, 10^9]。 + +## 暴力法 + +### 思路 + +首先我们考虑暴力法。每插入一个元素我们都判断其是否和已有的`所有`课程重叠。 + +我们定一个函数`intersected(calendar, calendars)`,其中 calendar 是即将要插入的课程,calendars 是已经插入的课程。 只要 calendar 和 calendars 中的任何一个课程有交叉,我们就返回 True,否则返回 False。 + +对于两个 calendar,我们的判断逻辑都是一样的。假设连个 calendar 分别是`[s1, e1]`和`[s2, e2]`。那么如果`s1 >= e2 or s2 <= e1`, 则两个课程没有交叉,可以预定,否则不可以。如图,1,2,3 可以预定,剩下的不可以。 + +![image.png](http://ww1.sinaimg.cn/large/e9f490c8ly1gbj1o8hvivj20w20ra76f.jpg) + +代码是这样的: + +```python + def intersected(calendar, calendars): + for [start, end] in calendars: + if calendar[0] >= end or calendar[1] <= start: + continue + else: + return True + + return False +``` + +复杂度分析: + +- 时间复杂度:$O(N^2)$。N 指的是日常安排的数量,对于每个新的日常安排,我们检查新的日常安排是否发生冲突来决定是否可以预订新的日常安排。 + +- 空间复杂度: $O(N)$。 + +这个代码写出来之后整体代码就呼之欲出了,全部代码见下方代码部分。 + +### 代码 + +代码支持 Python3: + +Python3 Code: + +```python +# +# @lc app=leetcode.cn id=729 lang=python3 +# +# [729] 我的日程安排表 I +# + +# @lc code=start + + +class MyCalendar: + + def __init__(self): + self.calendars = [] + + def book(self, start: int, end: int) -> bool: + def intersected(calendar, calendars): + for [start, end] in calendars: + if calendar[0] >= end or calendar[1] <= start: + continue + else: + return True + + return False + if intersected([start, end], self.calendars): + return False + self.calendars.append([start, end]) + return True + + # Your MyCalendar object will be instantiated and called as such: + # obj = MyCalendar() + # param_1 = obj.book(start,end) + # @lc code=end +``` + +实际上我们还可以换个角度,上面的思路判断交叉部分我们考虑的是“如何不交叉”,剩下的就是交叉。我们也可以直接考虑交叉。还是上面的例子,如果两个课程交叉,那么一定满足`s1 < e2 and e1 > s2`。基于此,我们写出下面的代码。 + +代码支持 Python3: + +Python3 Code: + +```python +# +# @lc app=leetcode.cn id=729 lang=python3 +# +# [729] 我的日程安排表 I +# + +# @lc code=start + + +class MyCalendar: + + def __init__(self): + self.calendars = [] + + def book(self, start: int, end: int) -> bool: + for s, e in self.calendars: + if start < e and end > s: + return False + self.calendars.append([start, end]) + return True + + # Your MyCalendar object will be instantiated and called as such: + # obj = MyCalendar() + # param_1 = obj.book(start,end) + # @lc code=end +``` + +## 二叉查找树法 + +### 思路 + +和上面思路类似,只不过我们每次都对 calendars 进行排序,那么我们可以通过二分查找日程安排的情况来检查新日常安排是否可以预订。如果每次插入之前都进行一次排序,那么时间复杂度会很高。如图,我们的[s1,e1], [s2,e2], [s3,e3] 是按照时间顺序排好的日程安排。我们现在要插入[s,e],我们使用二分查找,找到要插入的位置,然后和插入位置的课程进行一次比对即可,这部分的时间复杂度是 O(logN)\$。 + +![image.png](http://ww1.sinaimg.cn/large/e9f490c8ly1gbj28k6v4gj21100c2754.jpg) + +我们考虑使用平衡二叉树来维护这种动态的变化,在最差的情况时间复杂度会退化到上述的$O(N^2)$,平均情况是$O(NlogN)$,其中 N 是已预订的日常安排数。 + +![image.png](http://ww1.sinaimg.cn/large/e9f490c8ly1gbj2dirnf0j20xs0fe75j.jpg) + +### 代码 + +代码支持 Python3: + +Python3 Code: + +```python +class Node: + def __init__(self, start, end): + self.start = start + self.end = end + self.left = self.right = None + + def insert(self, node): + if node.start >= self.end: + if not self.right: + self.right = node + return True + return self.right.insert(node) + elif node.end <= self.start: + if not self.left: + self.left = node + return True + return self.left.insert(node) + else: + return False + +class MyCalendar(object): + def __init__(self): + self.root = None + + def book(self, start, end): + if self.root is None: + self.root = Node(start, end) + return True + return self.root.insert(Node(start, end)) + +``` + +# 731. 我的日程安排表 II + +## 题目地址 + +https://leetcode-cn.com/problems/my-calendar-ii + +## 题目描述 + +实现一个 MyCalendar 类来存放你的日程安排。如果要添加的时间内不会导致三重预订时,则可以存储这个新的日程安排。 + +MyCalendar 有一个 book(int start, int end)方法。它意味着在 start 到 end 时间内增加一个日程安排,注意,这里的时间是半开区间,即 [start, end), 实数  x 的范围为,  start <= x < end。 + +当三个日程安排有一些时间上的交叉时(例如三个日程安排都在同一时间内),就会产生三重预订。 + +每次调用 MyCalendar.book 方法时,如果可以将日程安排成功添加到日历中而不会导致三重预订,返回 true。否则,返回 false 并且不要将该日程安排添加到日历中。 + +请按照以下步骤调用 MyCalendar 类: MyCalendar cal = new MyCalendar(); MyCalendar.book(start, end) + +示例: + +MyCalendar(); +MyCalendar.book(10, 20); // returns true +MyCalendar.book(50, 60); // returns true +MyCalendar.book(10, 40); // returns true +MyCalendar.book(5, 15); // returns false +MyCalendar.book(5, 10); // returns true +MyCalendar.book(25, 55); // returns true +解释: +前两个日程安排可以添加至日历中。 第三个日程安排会导致双重预订,但可以添加至日历中。 +第四个日程安排活动(5,15)不能添加至日历中,因为它会导致三重预订。 +第五个日程安排(5,10)可以添加至日历中,因为它未使用已经双重预订的时间 10。 +第六个日程安排(25,55)可以添加至日历中,因为时间 [25,40] 将和第三个日程安排双重预订; +时间 [40,50] 将单独预订,时间 [50,55)将和第二个日程安排双重预订。 + +提示: + +每个测试用例,调用  MyCalendar.book  函数最多不超过  1000 次。 +调用函数  MyCalendar.book(start, end)时, start 和  end 的取值范围为  [0, 10^9]。 + +## 暴力法 + +### 思路 + +暴力法和上述思路类似。但是我们多维护一个数组 intersectedCalendars 用来存储**二次预定**的日程安排。如果课程第一次冲突,我们将其加入 intersectedCalendars,如果和 intersectedCalendars 也冲突了,说明出现了三次预定,我们直接返回 False。 + +### 代码 + +代码支持 Python3: + +Python3 Code: + +```python +class MyCalendarTwo: + + def __init__(self): + self.calendars = [] + self.intersectedCalendars = [] + + def book(self, start: int, end: int) -> bool: + for [s, e] in self.intersectedCalendars: + if start < e and end > s: + return False + for [s, e] in self.calendars: + if start < e and end > s: + self.intersectedCalendars.append([max(start, s), min(end, e)]) + self.calendars.append([start, end]) + return True +``` + +## 二叉查找树法 + +和上面的题目类似,我们仍然可以使用平衡二叉树来简化查找逻辑。具体可以参考[这个 discussion]() + +每次插入之前我们都需要进行一次判断,判断是否可以插入。如果不可以插入,直接返回 False,否则我们进行一次插入。 插入的时候,如果和已有的相交了,我们判断是否之前已经相交了一次,如果是返回 False,否则返回 True。关于**如何判断是否和已有的相交**,我们可以在 node 节点增加一个字段的方式来标记,在这里我们使用 single_overlap,True 表示产生了二次预定,False 则表示没有产生过两次及以上的预定。 + +## 代码 + +代码支持 Python3: + +Python3 Code: + +```python +class Node: + def __init__(self, start, end): + self.start = start + self.end = end + self.left = None + self.right = None + self.single_overlap = False + +class MyCalendarTwo: + + def __init__(self): + self.root = None + + def book(self, start, end): + if not self.canInsert(start, end, self.root): + return False + + self.root = self.insert(start, end, self.root) + return True + + + def canInsert(self, start, end, root): + if not root: + return True + + if start >= end: + return True + + if end <= root.start: + return self.canInsert(start, end, root.left) + + elif start >= root.end: + return self.canInsert(start, end, root.right) + + else: + if root.single_overlap: + return False + elif start >= root.start and end <= root.end: + return True + else: + return self.canInsert(start, root.start, root.left) and self.canInsert(root.end, end, root.right) + + + + def insert(self, start, end, root): + if not root: + root = Node(start, end) + return root + + if start >= end: + return root + + if start >= root.end: + root.right = self.insert(start, end, root.right) + + elif end <= root.start: + root.left = self.insert(start, end, root.left) + + else: + root.single_overlap = True + a = min(root.start, start) + b = max(root.start, start) + c = min(root.end, end) + d = max(root.end, end) + root.start, root.end = b, c + root.left, root.right = self.insert(a, b, root.left), self.insert(c, d, root.right) + + return root + +# Your MyCalendarTwo object will be instantiated and called as such: +# obj = MyCalendarTwo() +# param_1 = obj.book(start,end) +``` + +# 732. 我的日程安排表 III + +## 题目地址 + +https://leetcode-cn.com/problems/my-calendar-iii/ + +## 题目描述 + +实现一个 MyCalendar 类来存放你的日程安排,你可以一直添加新的日程安排。 + +MyCalendar 有一个 book(int start, int end)方法。它意味着在 start 到 end 时间内增加一个日程安排,注意,这里的时间是半开区间,即 [start, end), 实数  x 的范围为,  start <= x < end。 + +当 K 个日程安排有一些时间上的交叉时(例如 K 个日程安排都在同一时间内),就会产生 K 次预订。 + +每次调用 MyCalendar.book 方法时,返回一个整数 K ,表示最大的 K 次预订。 + +请按照以下步骤调用 MyCalendar 类: MyCalendar cal = new MyCalendar(); MyCalendar.book(start, end) + +示例 1: + +MyCalendarThree(); +MyCalendarThree.book(10, 20); // returns 1 +MyCalendarThree.book(50, 60); // returns 1 +MyCalendarThree.book(10, 40); // returns 2 +MyCalendarThree.book(5, 15); // returns 3 +MyCalendarThree.book(5, 10); // returns 3 +MyCalendarThree.book(25, 55); // returns 3 +解释: +前两个日程安排可以预订并且不相交,所以最大的 K 次预订是 1。 +第三个日程安排[10,40]与第一个日程安排相交,最高的 K 次预订为 2。 +其余的日程安排的最高 K 次预订仅为 3。 +请注意,最后一次日程安排可能会导致局部最高 K 次预订为 2,但答案仍然是 3,原因是从开始到最后,时间[10,20],[10,40]和[5,15]仍然会导致 3 次预订。 +说明: + +每个测试用例,调用  MyCalendar.book  函数最多不超过  400 次。 +调用函数  MyCalendar.book(start, end)时, start 和  end 的取值范围为  [0, 10^9]。 + +## 二叉查找树法 + +### 思路 + +我们仍然可以使用上述的平衡二叉树的做法。只不过我们需要额外维护一个全局的最大值“k”,表示需要多少个预定。最终我们返回 k。 同时每一个 node 我们都增加一个属性 k,用来表示局部的最大值,对于每次插入,我们将 node 的 k 和全部的 k 进行比较,取出最大值即可。 + +### 代码 + +代码支持 Python3: + +Python3 Code: + +```python + +class Node(object): + def __init__(self, start, end, ktime=1): + self.k = ktime + self.s = start + self.e = end + self.right = None + self.left = None + +class MyCalendarThree(object): + + def __init__(self): + self.root = None + self.k = 0 + + def book(self, start, end): + self.root = self.insert(self.root, start, end, 1) + return self.k + def insert(self, root, start, end, k): + if start >= end: + return root + if not root: + self.k = max(self.k, k) + return Node(start, end, k) + else: + if start >= root.e: + root.right = self.insert(root.right, start, end, k) + return root + elif end <= root.s: + root.left = self.insert(root.left, start, end, k) + return root + else: + + a = min(root.s, start) + b = max(root.s, start) + c = min(root.e, end) + d = max(root.e, end) + + root.left = self.insert(root.left, a, b, a == root.s and root.k or k) + root.right = self.insert(root.right, c,d, d == root.e and root.k or k) + root.k += k + root.s = b + root.e = c + self.k = max(root.k, self.k) + return root + +``` + +## Count Map 法 + +### 思路 + +这个是我在看了 Discussion [[C++] Map Solution, beats 95%+](https://leetcode.com/problems/my-calendar-iii/discuss/176950/C%2B%2B-Map-Solution-beats-95%2B) 之后写的解法,解法非常巧妙。 + +我们使用一个 count map 来存储所有的预定,对于每次插入,我们执行`count[start] += 1`和`count[end] -= 1`。 count[t] 表示从 t 开始到下一个 t 我们有几个预定。因此我们需要对 count 进行排序才行。 我们维护一个最大值来 cnt 来表示需要的预定数。 + +比如预定[1,3]和[5,7],我们产生一个预定即可: + +![image.png](http://ww1.sinaimg.cn/large/e9f490c8ly1gbj50c37suj212q0bcq3t.jpg) + +再比如预定[1,5]和[3,7],我们需要两个预定: + +![image.png](http://ww1.sinaimg.cn/large/e9f490c8ly1gbj45oq6fhj213e0ca0tm.jpg) + +我们可以使用红黑树来简化时间复杂度,如果你使用的是 Java,可以直接使用现成的数据结构 TreeMap。我这里偷懒,每次都排序,时间复杂度会很高,但是可以 AC。 + +读到这里,你可能会发现: 这个解法似乎更具有通用型。对于第一题我们可以判断 cnt 是否小于等于 1,对于第二题我们可以判断 cnt 是否小于等于 2。 + +> 如果你不借助红黑树等数据结构直接使用 count-map 法,即每次都进行一次排序,第一题和第二题可能会直接超时。 + +### 代码 + +代码支持 Python3: + +Python3 Code: + +```python +class MyCalendarThree: + + def __init__(self): + self.count = dict() + + def book(self, start: int, end: int) -> int: + self.count[start] = self.count.get(start, 0) + 1 + self.count[end] = self.count.get(end, 0) - 1 + cnt = 0 + cur = 0 + + for k in sorted(self.count): + cur += self.count[k] + cnt = max(cnt, cur) + return cnt + + # Your MyCalendarThree object will be instantiated and called as such: + # obj = MyCalendarThree() + # param_1 = obj.book(start,end) +``` + +# 相关题目 + +LeetCode 上有一个类似的系列《会议室》,截止目前(2020-02-03)有两道题目。其中一个简单一个中等,解题思路非常类似,大家用这个解题思路尝试一下,检测一下自己是否已经掌握。两道题分别是: + +- [252. 会议室](https://leetcode-cn.com/problems/meeting-rooms/) +- [253. 会议室 II](https://leetcode-cn.com/problems/meeting-rooms-ii/) + +# 总结 + +我们对 LeetCode 上的专题《我的日程安排》的三道题进行了汇总。对于区间判断是否重叠,我们可以反向判断,也可以正向判断。 暴力的方法是每次对所有的课程进行判断是否重叠,这种解法可以 AC。我们也可以进一步优化,使用二叉查找树来简化时间复杂度。最后我们介绍了一种 Count-Map 方法来通用解决所有的问题,不仅可以完美解决这三道题,还可以扩展到《会议室》系列的两道题。 diff --git a/selected/serialize.md b/selected/serialize.md new file mode 100644 index 0000000..4ef9b76 --- /dev/null +++ b/selected/serialize.md @@ -0,0 +1,303 @@ +# 文带你看懂二叉树的序列化 + +我们先来看下什么是序列化,以下定义来自维基百科: + +> 序列化(serialization)在计算机科学的数据处理中,是指将数据结构或对象状态转换成可取用格式(例如存成文件,存于缓冲,或经由网络中发送),以留待后续在相同或另一台计算机环境中,能恢复原先状态的过程。依照序列化格式重新获取字节的结果时,可以利用它来产生与原始对象相同语义的副本。对于许多对象,像是使用大量引用的复杂对象,这种序列化重建的过程并不容易。面向对象中的对象序列化,并不概括之前原始对象所关系的函数。这种过程也称为对象编组(marshalling)。从一系列字节提取数据结构的反向操作,是反序列化(也称为解编组、deserialization、unmarshalling)。 + +可见,序列化和反序列化在计算机科学中的应用还是非常广泛的。就拿 LeetCode 平台来说,其允许用户输入形如: + +``` +[1,2,3,null,null,4,5] +``` + +这样的数据结构来描述一颗树: + +![](https://tva1.sinaimg.cn/large/007S8ZIlly1gh2dqqnyzwj30ba0baglw.jpg) + +([1,2,3,null,null,4,5] 对应的二叉树) + +其实序列化和反序列化只是一个概念,不是一种具体的算法,而是很多的算法。并且针对不同的数据结构,算法也会不一样。本文主要讲述的是二叉树的序列化和反序列化。看完本文之后,你就可以放心大胆地去 AC 以下两道题: + +- [449. 序列化和反序列化二叉搜索树(中等)](https://leetcode-cn.com/problems/serialize-and-deserialize-bst/) +- [297. 二叉树的序列化与反序列化(困难)](https://leetcode-cn.com/problems/serialize-and-deserialize-binary-tree/) + +## 前置知识 + +阅读本文之前,需要你对树的遍历以及 BFS 和 DFS 比较熟悉。如果你还不熟悉,推荐阅读一下相关文章之后再来看。或者我这边也写了一个总结性的文章[二叉树的遍历](https://github.com/azl397985856/leetcode/blob/master/thinkings/binary-tree-traversal.md),你也可以看看。 + +## 前言 + +我们知道:二叉树的深度优先遍历,根据访问根节点的顺序不同,可以将其分为`前序遍历`,`中序遍历`, `后序遍历`。即如果先访问根节点就是前序遍历,最后访问根节点就是后续遍历,其它则是中序遍历。而左右节点的相对顺序是不会变的,一定是先左后右。 + +> 当然也可以设定为先右后左。 + +并且知道了三种遍历结果中的任意两种即可还原出原有的树结构。这不就是序列化和反序列化么?如果对这个比较陌生的同学建议看看我之前写的[《构造二叉树系列》](https://lucifer.ren/blog/2020/02/08/%E6%9E%84%E9%80%A0%E4%BA%8C%E5%8F%89%E6%A0%91%E4%B8%93%E9%A2%98/) + +有了这样一个前提之后算法就自然而然了。即先对二叉树进行两次不同的遍历,不妨假设按照前序和中序进行两次遍历。然后将两次遍历结果序列化,比如将两次遍历结果以逗号“,” join 成一个字符串。 之后将字符串反序列即可,比如将其以逗号“,” split 成一个数组。 + +序列化: + +```py +class Solution: + def preorder(self, root: TreeNode): + if not root: return [] + return [str(root.val)] +self. preorder(root.left) + self.preorder(root.right) + def inorder(self, root: TreeNode): + if not root: return [] + return self.inorder(root.left) + [str(root.val)] + self.inorder(root.right) + def serialize(self, root): + ans = '' + ans += ','.join(self.preorder(root)) + ans += '$' + ans += ','.join(self.inorder(root)) + + return ans + +``` + +反序列化: + +这里我直接用了力扣 `105. 从前序与中序遍历序列构造二叉树` 的解法,一行代码都不改。 + +```py +class Solution: + def deserialize(self, data: str): + preorder, inorder = data.split('$') + if not preorder: return None + return self.buildTree(preorder.split(','), inorder.split(',')) + + def buildTree(self, preorder: List[int], inorder: List[int]) -> TreeNode: + # 实际上inorder 和 preorder 一定是同时为空的,因此你无论判断哪个都行 + if not preorder: + return None + root = TreeNode(preorder[0]) + + i = inorder.index(root.val) + root.left = self.buildTree(preorder[1:i + 1], inorder[:i]) + root.right = self.buildTree(preorder[i + 1:], inorder[i+1:]) + + return root + +``` + +实际上这个算法是不一定成立的,原因在于树的节点可能存在重复元素。也就是说我前面说的`知道了三种遍历结果中的任意两种即可还原出原有的树结构`是不对的,严格来说应该是**如果树中不存在重复的元素,那么知道了三种遍历结果中的任意两种即可还原出原有的树结构**。 + +聪明的你应该发现了,上面我的代码用了 `i = inorder.index(root.val)`,如果存在重复元素,那么得到的索引 i 就可能不是准确的。但是,如果题目限定了没有重复元素则可以用这种算法。但是现实中不出现重复元素不太现实,因此需要考虑其他方法。那究竟是什么样的方法呢? 接下来进入正题。 + +## DFS + +### 序列化 + +我们来模仿一下力扣的记法。 比如:`[1,2,3,null,null,4,5]`(本质上是 BFS 层次遍历),对应的树如下: + +> 选择这种记法,而不是 DFS 的记法的原因是看起来比较直观 + +![](https://tva1.sinaimg.cn/large/007S8ZIlly1gh2h5bhjryj30b40am74k.jpg) + +序列化的代码非常简单, 我们只需要在普通的遍历基础上,增加对空节点的输出即可(普通的遍历是不处理空节点的)。 + +比如我们都树进行一次前序遍历的同时增加空节点的处理。选择前序遍历的原因是容易知道根节点的位置,并且代码好写,不信你可以试试。 + +因此序列化就仅仅是普通的 DFS 而已,直接给大家看看代码。 + +Python 代码: + +```py +class Codec: + def serialize_dfs(self, root, ans): + # 空节点也需要序列化,否则无法唯一确定一棵树,后不赘述。 + if not root: return ans + '#,' + # 节点之间通过逗号(,)分割 + ans += str(root.val) + ',' + ans = self.serialize_dfs(root.left, ans) + ans = self.serialize_dfs(root.right, ans) + return ans + def serialize(self, root): + # 由于最后会添加一个额外的逗号,因此需要去除最后一个字符,后不赘述。 + return self.serialize_dfs(root, '')[:-1] +``` + +Java 代码: + +```java +public class Codec { + public String serialize_dfs(TreeNode root, String str) { + if (root == null) { + str += "None,"; + } else { + str += str.valueOf(root.val) + ","; + str = serialize_dfs(root.left, str); + str = serialize_dfs(root.right, str); + } + return str; + } + + public String serialize(TreeNode root) { + return serialize_dfs(root, ""); + } +} +``` + +`[1,2,3,null,null,4,5]` 会被处理为`1,2,#,#,3,4,#,#,5,#,#` + +我们先看一个短视频: + +![](https://tva1.sinaimg.cn/large/007S8ZIlly1gh2z5y87n0g30bo05vx6u.gif) + +(动画来自力扣) + +### 反序列化 + +反序列化的第一步就是将其展开。以上面的例子来说,则会变成数组:`[1,2,#,#,3,4,#,#,5,#,#]`,然后我们同样执行一次前序遍历,每次处理一个元素,重建即可。由于我们采用的前序遍历,因此第一个是根元素,下一个是其左子节点,下下一个是其右子节点。 + +Python 代码: + +```py + def deserialize_dfs(self, nodes): + if nodes: + if nodes[0] == '#': + nodes.pop(0) + return None + root = TreeNode(nodes.pop(0)) + root.left = self.deserialize_dfs(nodes) + root.right = self.deserialize_dfs(nodes) + return root + return None + + def deserialize(self, data: str): + nodes = data.split(',') + return self.deserialize_dfs(nodes) +``` + +Java 代码: + +```java + public TreeNode deserialize_dfs(List l) { + if (l.get(0).equals("None")) { + l.remove(0); + return null; + } + + TreeNode root = new TreeNode(Integer.valueOf(l.get(0))); + l.remove(0); + root.left = deserialize_dfs(l); + root.right = deserialize_dfs(l); + + return root; + } + + public TreeNode deserialize(String data) { + String[] data_array = data.split(","); + List data_list = new LinkedList(Arrays.asList(data_array)); + return deserialize_dfs(data_list); + } +``` + +**复杂度分析** + +- 时间复杂度:每个节点都会被处理一次,因此时间复杂度为 $O(N)$,其中 $N$ 为节点的总数。 +- 空间复杂度:空间复杂度取决于栈深度,因此空间复杂度为 $O(h)$,其中 $h$ 为树的深度。 + +## BFS + +### 序列化 + +实际上我们也可以使用 BFS 的方式来表示一棵树。在这一点上其实就和力扣的记法是一致的了。 + +我们知道层次遍历的时候实际上是有层次的。只不过有的题目需要你记录每一个节点的层次信息,有些则不需要。 + +这其实就是一个朴实无华的 BFS,唯一不同则是增加了空节点。 + +Python 代码: + +```py + +class Codec: + def serialize(self, root): + ans = '' + queue = [root] + while queue: + node = queue.pop(0) + if node: + ans += str(node.val) + ',' + queue.append(node.left) + queue.append(node.right) + else: + ans += '#,' + return ans[:-1] + +``` + +### 反序列化 + +如图有这样一棵树: + +![](https://tva1.sinaimg.cn/large/007S8ZIlly1gh2x3gj9n0j30j00gewfx.jpg) + +那么其层次遍历为 [1,2,3,#,#, 4, 5]。我们根据此层次遍历的结果来看下如何还原二叉树,如下是我画的一个示意图: + +![](https://tva1.sinaimg.cn/large/007S8ZIlly1gh2x55lh7qj31780t0gq8.jpg) + +容易看出: + +- level x 的节点一定指向 level x + 1 的节点,如何找到 level + 1 呢? 这很容易通过层次遍历来做到。 +- 对于给的的 level x,从左到右依次对应 level x + 1 的节点,即第 1 个节点的左右子节点对应下一层的第 1 个和第 2 个节点,第 2 个节点的左右子节点对应下一层的第 3 个和第 4 个节点。。。 +- 接上,其实如果你仔细观察的话,实际上 level x 和 level x + 1 的判断是无需特别判断的。我们可以把思路逆转过来:`即第 1 个节点的左右子节点对应第 1 个和第 2 个节点,第 2 个节点的左右子节点对应第 3 个和第 4 个节点。。。`(注意,没了下一层三个字) + +因此我们的思路也是同样的 BFS,并依次连接左右节点。 + +Python 代码: + +```py + + def deserialize(self, data: str): + if data == '#': return None + # 数据准备 + nodes = data.split(',') + if not nodes: return None + # BFS + root = TreeNode(nodes[0]) + queue = [root] + # 已经有 root 了,因此从 1 开始 + i = 1 + + while i < len(nodes) - 1: + node = queue.pop(0) + # + lv = nodes[i] + rv = nodes[i + 1] + i += 2 + # 对于给的的 level x,从左到右依次对应 level x + 1 的节点 + # node 是 level x 的节点,l 和 r 则是 level x + 1 的节点 + if lv != '#': + l = TreeNode(lv) + node.left = l + queue.append(l) + + if rv != '#': + r = TreeNode(rv) + node.right = r + queue.append(r) + return root +``` + +**复杂度分析** + +- 时间复杂度:每个节点都会被处理一次,因此时间复杂度为 $O(N)$,其中 $N$ 为节点的总数。 +- 空间复杂度:$O(N)$,其中 $N$ 为节点的总数。 + +## 总结 + +除了这种方法还有很多方案, 比如括号表示法。 关于这个可以参考力扣[606. 根据二叉树创建字符串](https://leetcode-cn.com/problems/construct-string-from-binary-tree/),这里就不再赘述了。 + +本文从 BFS 和 DFS 角度来思考如何序列化和反序列化一棵树。 如果用 BFS 来序列化,那么相应地也需要 BFS 来反序列化。如果用 DFS 来序列化,那么就需要用 DFS 来反序列化。 + +我们从马后炮的角度来说,实际上对于序列化来说,BFS 和 DFS 都比较常规。对于反序列化,大家可以像我这样举个例子,画一个图。可以先在纸上,电脑上,如果你熟悉了之后,也可以画在脑子里。 + +![](https://tva1.sinaimg.cn/large/007S8ZIlly1gh30bapydej30rq0tcad5.jpg) + +(Like This) + +更多题解可以访问我的 LeetCode 题解仓库:https://github.com/azl397985856/leetcode 。 目前已经 30K star 啦。 + +关注公众号力扣加加,努力用清晰直白的语言还原解题思路,并且有大量图解,手把手教你识别套路,高效刷题。 diff --git a/selected/zuma-game.md b/selected/zuma-game.md new file mode 100644 index 0000000..b95bdd0 --- /dev/null +++ b/selected/zuma-game.md @@ -0,0 +1,155 @@ +# 百度的算法面试题 - 祖玛游戏 + +这是一道百度的算法面试题, 让我来拷拷你~。 + +## 题目地址(488. 祖玛游戏) + +https://leetcode-cn.com/problems/zuma-game/ + +## 题目描述 + +``` +回忆一下祖玛游戏。现在桌上有一串球,颜色有红色(R),黄色(Y),蓝色(B),绿色(G),还有白色(W)。 现在你手里也有几个球。 + +每一次,你可以从手里的球选一个,然后把这个球插入到一串球中的某个位置上(包括最左端,最右端)。接着,如果有出现三个或者三个以上颜色相同的球相连的话,就把它们移除掉。重复这一步骤直到桌上所有的球都被移除。 + +找到插入并可以移除掉桌上所有球所需的最少的球数。如果不能移除桌上所有的球,输出 -1 。 + +示例: +输入: "WRRBBW", "RB" +输出: -1 +解释: WRRBBW -> WRR[R]BBW -> WBBW -> WBB[B]W -> WW (翻译者标注:手上球已经用完,桌上还剩两个球无法消除,返回-1) + +输入: "WWRRBBWW", "WRBRW" +输出: 2 +解释: WWRRBBWW -> WWRR[R]BBWW -> WWBBWW -> WWBB[B]WW -> WWWW -> empty + +输入:"G", "GGGGG" +输出: 2 +解释: G -> G[G] -> GG[G] -> empty + +输入: "RBYYBBRRB", "YRBGB" +输出: 3 +解释: RBYYBBRRB -> RBYY[Y]BBRRB -> RBBBRRB -> RRRB -> B -> B[B] -> BB[B] -> empty +标注: + +你可以假设桌上一开始的球中,不会有三个及三个以上颜色相同且连着的球。 +桌上的球不会超过20个,输入的数据中代表这些球的字符串的名字是 "board" 。 +你手中的球不会超过5个,输入的数据中代表这些球的字符串的名字是 "hand"。 +输入的两个字符串均为非空字符串,且只包含字符 'R','Y','B','G','W'。 + + +``` + +## 前置知识 + +- 回溯 +- 哈希表 +- 双指针 + +## 公司 + +- 百度 + +## 思路 + +面试题困难难度的题目常见的题型有: + +- DP +- 设计题 +- 图 +- 游戏 + +本题就是游戏类题目。 如果你是一个前端, 说不定还会考察你如何实现一个 zuma 游戏。这种游戏类的题目,可以简单可以困难, 比如力扣经典的石子游戏,宝石游戏等。这类题目没有固定的解法。我做这种题目的思路就是先暴力模拟,再尝试优化算法瓶颈。 + +注意下数据范围球的数目 <= 5,因此暴力法就变得可行。基本思路是暴力枚举手上的球可以消除的地方, 我们可以使用回溯法来完成暴力枚举的过程,在回溯过程记录最小值即可。由于回溯树的深度不会超过 5,因此这种解法应该可以 AC。 + +上面提到的`可以消除的地方`,指的是**连续相同颜色 + 手上相同颜色的球大于等于 3**,这也是题目说明的消除条件。 + +因此我们只需要两个指针记录连续相同颜色球的位置,如果可以消除,消除即可。 + +![](https://tva1.sinaimg.cn/large/007S8ZIlly1gjfehgw7lnj31880fydkr.jpg) + +如图,我们记录了连续红球的位置, 如果手上有红球, 则可以尝试将其清除,这一次决策就是回溯树(决策树)的一个分支。之后我们会撤回到这个决策分支, 尝试其他可行的决策分支。 + +以 board = RRBBRR , hand 为 RRBB 为例,其决策树为: + +![](https://tva1.sinaimg.cn/large/007S8ZIlly1gjfg7kykx3j30u00wc46o.jpg) + +其中虚线表示无需手动干预,系统自动消除。叶子节点末尾的黄色表示全部消除需要的手球个数。路径上的文字后面的数字表示此次消除需要的手球个数 + +> 如果你对回溯不熟悉,可以参考下我之前写的几篇题解:比如 [46.permutations](https://github.com/azl397985856/leetcode/blob/master/problems/46.permutations.md "46.permutations")。 + +可以看出, 如果选择先消除中间的蓝色,则只需要一步即可完成。 + +关于计算连续球位置的核心代码(Python3): + +```python +i = 0 +while i < len(board): + j = i + 1 + while j < len(board) and board[i] == board[j]: j += 1 + # 其他逻辑 + + # 更新左指针 + i = j +``` + +![](https://tva1.sinaimg.cn/large/007S8ZIlly1gjfegz0iwvj316e0my43t.jpg) + +具体算法: + +1. 用哈希表存储手上的球的种类和个数,这么做是为了后面**快速判断连续的球是否可以被消除**。由于题目限制手上求不会超过 5,因此哈希表的最大容量就是 5,可以认为这是一个常数的空间。 +2. 回溯。 + + 2.1 确认可以消除的位置,算法参考上面的代码。 + + 2.2 判断手上是否有足够相同颜色的球可以消除。 + + 2.3 回溯的过程记录全局最小值。 + +## 代码 + +代码支持:Python3 + +Python3 Code: + +```python +class Solution: + def findMinStep(self, board: str, hand: str) -> int: + def backtrack(board): + if not board: return 0 + i = 0 + ans = 6 + while i < len(board): + j = i + 1 + while j < len(board) and board[i] == board[j]: j += 1 + balls = 3 - (j - i) + if counter[board[i]] >= balls: + balls = max(0, balls) + counter[board[i]] -= balls + ans = min(ans, balls + backtrack(board[:i] + board[j:])) + counter[board[i]] += balls + i = j + return ans + + counter = collections.Counter(hand) + ans = backtrack(board) + return -1 if ans > 5 else ans + +``` + +**复杂度分析** + +- 时间复杂度:$O(2^(min(C, 5)))$,其中 C 为连续相同颜色球的次数,比如 WWRRRR, C 就是 2, WRBDD, C 就是 4。min(C, 5) 是因为题目限定了手上球的个数不大于 5。 +- 空间复杂度:$O(min(C, 5) * Board)$,其中 C 为连续相同颜色球的次数,Board 为 Board 的长度。 + +## 关键点解析 + +- 回溯模板 +- 双指针写法 + +大家对此有何看法,欢迎给我留言,我有时间都会一一查看回答。更多算法套路可以访问我的 LeetCode 题解仓库:https://github.com/azl397985856/leetcode 。 目前已经 36K star 啦。 +大家也可以关注我的公众号《力扣加加》带你啃下算法这块硬骨头。 + +![](https://tva1.sinaimg.cn/large/007S8ZIlly1gfcuzagjalj30p00dwabs.jpg) -- GitLab