学习周报-02-算法
前言:
本周已学习:
- 最大公约数、同余定理
- 对数器与暴力打表找规律
- 技巧:根据数据规模猜题解
- 前缀树
- 前缀和、差分
- 滑动窗口
- 双指针
- 二分答案
- 单调栈、单调队列
- 并查集
- 洪水填充
其中涉及一些关于数论的结论,一些经验结论,以及两个新的数据结构(前缀树、并查集),和原有数据结构的新用法(单调栈、单调队列),同时学习了如何通过构建前缀信息以快速查询,还有通过差分数组传播影响,而洪水填充是一个dfs算法,真的很符合直觉,法如其名。
最大公约数、同余定理
怎么求最大公约数和最小公倍数?- 辗转相除法
证明辗转相除法就是证明如下关系:gcd(a, b) = gcd(b, a % b
假设a % b = r,即需要证明的关系为:gcd(a, b) = gcd(b, r)
证明过程:
因为a % b = r,所以如下两个等式必然成立
1) a = b * q + r,q为0、1、2、3….中的一个整数
2) r = a − b * q,q为0、1、2、3….中的一个整数
假设u是a和b的公因子,则有: a = s * u, b = t * u
把a和b带入2)得到,r = s * u - t * u * q = (s - t * q) * u
这说明 : u如果是a和b的公因子,那么u也是r的因子
假设v是b和r的公因子,则有: b = x * v, r = y * v
把b和r带入1)得到,a = x * v * q + y * v = (x * q + y) * v
这说明 : v如果是b和r的公因子,那么v也是a的公因子
综上,a和b的每一个公因子 也是 b和r的一个公因子,反之亦然
所以,a和b的全体公因子集合 = b和r的全体公因子集合
即gcd(a, b) = gcd(b, r)
代码实现:
1 | // cpp实现 |
例题分析:
1 | 题目:魔数的数量 |
同余定理
数学上的证明就不多说了,
总之就是用在大数据需要返回mod大质数结果的情况下,
我们可以通过对每个中间结果mod也能得出答案。
除法需要逆元特殊处理。
对数器与暴力打表找规律
使用场景:入参是简单类型,出参也是简单类型。
一般而言,用最暴力的实现进行求解,并在入参不大的情况下,
打表找规律,最后把规律变为代码。
例题分析:
1 | 题目: 要求只有一个长度>=2的回文子串,求所有长度为n的red字符串中好串的数量 |
分析:我们先分析基础情况,然后暴力打表(递归暴力搜索),打印出n在前100的答案,然后观察规律
最后发现了规律:
1 | static long long num(long long n) { |
根据数据量猜解法
C/C++运行时间1s,
java/python/go等其他语言运行时间1s~2s,
对应的常数指令操作量是 10^7 ~ 10^8,
不管什么测试平台,不管什么cpu,都是这个数量级。
所以可以根据这个基本事实,
来猜测自己设计的算法最终有没有可能在规定时间内通过
也可以自己猜测一下大概的步数。
时间复杂度的估计很多时候并不是一个入参决定,
可能是多个入参共同决定。比如O(nm), O(n+m)等
所以最关键的就是记住常数指令操作量是 10^7 ~ 10^8,
然后方法是什么复杂度就可以估计能否通过了
问题规模 | logn | n | n*logn | n*根号n | n^2 | 2^n | n! |
---|---|---|---|---|---|---|---|
n <= 11 | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
n <= 25 | Yes | Yes | Yes | Yes | Yes | Yes | No |
n <= 5000 | Yes | Yes | Yes | Yes | Yes | No | No |
n <= 10^5 | Yes | Yes | Yes | Yes | No | No | No |
n <= 10^6 | Yes | Yes | Yes | No | No | No | No |
n <= 10^7 | Yes | Yes | No | No | No | No | No |
n >= 10^8 | Yes | No | No | No | No | No | No |
例题分析:
题目1 : 最优的技能释放顺序
https://www.nowcoder.com/practice/d88ef50f8dab4850be8cd4b95514bbbd
1 | 现在有一个打怪类型的游戏,这个游戏是这样的,你有n个技能 |
n的规模暗示了可接受的算法复杂度是n的阶乘,那么很容易就能想到是全排列。
题目2:超级回文数的数目
1 | 如果一个正整数自身是回文数,而且它也是一个回文数的平方,那么我们称这个数为超级回文数。 |
10^18,这很大啊,这就让我们不得不去想一些特殊解法
如果把题目问题看为主问题,那么求我们是不是可以先求超级回文数的开方是否回文从而逆向验证主问题呢?(这好像叫种子问题?)
如果用这个思路,规模直接下降到了sqrt(10^18)变成了10^9。
好像还是有点大了,我们能不能再次找到种子问题二,从而再度下降问题规模呢?
有点兄弟有的,我们只需要构造种子问题一的前半数据并验证即可:
1 | 对于前半部分数据:123 |
此时规模又缩减sqrt(10^9) 变成了 10^5。
整理思路:
1 | sd2() -> sd1() -> main() |
通过打表可以优化为直接返回列表中的记录~:
1 | // 预先计算的超级回文数的平方值列表 |
前缀树
前缀树又叫字典树,英文名trie,
这个名字来源于英文单词“retrieval”(检索),因为前缀树是一种专门用于高效检索和存储字符串的数据结构。它的命名是为了突出其在信息检索中的应用。
每个样本都从头节点开始根据前缀字符或者前缀数字建出来的一棵大树,就是前缀树
前缀树的使用场景:需要根据前缀信息来查询的场景
前缀树的优点:根据前缀信息选择树上的分支,可以节省大量的时间
前缀树的缺点:比较浪费空间,和总字符数量有关,字符的种类有关
前缀树的定制:pass、end等信息
对于前缀树的建立有两种方法,
一种是通过动态结构建立前缀树,一种是通过静态数组建立。
在工程上我们一般通过动态结构来确保工程上的安全性,
而比赛中为了追求极致效率,一般我们就直接用静态数组构建前缀树。
总结就是:没有路就新建节点;已经有路了,就复用节点。
1 | graph TD |
例题分析:
题目1 接头密匙
https://www.nowcoder.com/practice/c552d3b4dfda49ccb883a6371d9a6932
1 | 牛牛和他的朋友们约定了一套接头密匙系统,用于确认彼此身份 |
将数字序列的差分数组转换为字符串表示
例如原差分[1, -3, 9] -> "1#-3#9#"
这样就可以防止大量Path消耗大量内存
题目2 数组中两个数的最大异或值
https://leetcode.cn/problems/maximum-xor-of-two-numbers-in-an-array/
1 | 给你一个整数数组 nums ,返回 nums[i] XOR nums[j] 的最大运算结果,其中 0<=i<=j<=n |
把x数字看作一个二进制串
得到了一个类似”00100…..001110”的东西。
所谓 x xor y 得到最大,
就是从高位向下检索,尽可能走的满足 x 与 y的当前相等位状态相反的路径。
这就是天然的前缀思路,故前缀树解法显现。
题目3 在二维字符数组中搜索可能的单词
1 | 给定一个 m x n 二维字符网格 board 和一个单词(字符串)列表 words |
一眼trie + dfs, 图遍历嘛,用dfs很好理解,但trie在哪?
trie在于把给定要搜索的单词作为路径放入前缀树中,
相当于dfs在搜索的时候持有一个地图,如果地图上显示没路可走了,那就直接cut。
trie妙用:
1. end属性存储字符串便于收获结果
2. 在收获答案往回退时顺便减去pass属性,如果下次再来,访问到pass属性== 0就cut
前缀和、差分
前缀和
前缀和是通过滚动求和记录当前位置累加和所构建的数组,
是一种通过保留前缀信息以减少查询时间的技巧。
所谓前缀和实际上就是区间和。
记录的是 01、02、03直到0n的区间的累加和。
而由两个大小区间相减我们便可以得到所需区间的值。
例如我们如果需要45区间的值,完全可以拿05 - 03区间的值,5区间的值。
这样便剩下了4
例题分析:构建前缀信息的技巧-解决子数组相关问题
解决如下问题,时间复杂度O(n)
题目1 : 构建 前缀和数组。快速解决子数组范围求和的问题
测试链接 : https://leetcode.cn/problems/range-sum-query-immutable/题目2 : 构建 前缀和 最早出现的位置。返回 无序数组中 累加和为给定值的 最长子数组长度
测试链接 : https://www.nowcoder.com/practice/36fb0fd3c656480c92b569258a1223d5题目3 : 构建 前缀和 出现的次数。返回 无序数组中 累加和为给定值的 子数组数量
测试链接 : https://leetcode.cn/problems/subarray-sum-equals-k/题目4 : 构建 前缀和 最早出现的位置。返回 无序数组中 正数和负数个数相等的 最长子数组长度
测试链接 : https://www.nowcoder.com/practice/545544c060804eceaed0bb84fcd992fb题目5 : 构建 前缀和 最早出现的位置。表现良好的最长时间段问题
测试链接 : https://leetcode.cn/problems/longest-well-performing-interval/题目6 : 构建 前缀和余数 最晚出现的位置。移除的最短子数组长度,使得剩余元素的累加和能被p整除
测试链接 : https://leetcode.cn/problems/make-sum-divisible-by-p/题目7 : 构建 前缀奇偶状态 最早出现的位置。每个元音包含偶数次的 最长子串长度
测试链接 : https://leetcode.cn/problems/find-the-longest-substring-containing-vowels-in-even-counts/
差分数组
差分实际上是在前缀和技巧的基础上传播影响的技巧
例如你想要在数组上对 n~m区间加上一个数 x,其他区间不变。
实际上你只需要在差分数组上在n位置加上x,而在m + 1位置减去 x,然后使用前缀和构建数组后你会发现,n位置上的影响会自动传播,覆盖n ~ 结尾的区间,所以也就是为什么我们需要在m + 1 的位置减去x,这样可以覆盖掉来自n位置对m ~ 结尾的影响。
对于二维也是如此。
一维差分:太简单了,没有理解难度。不支持边操作、边查询。
等差数列差分问题描述:
一开始1n范围上的数字都是0。接下来一共有m个操作。r范围上依次加上首项s、末项e、公差d的数列
每次操作:l
最终1~n范围上的每个数字都要正确得到
等差数列差分的过程:
每个操作调用set方法
所有操作完成后在arr上生成两遍前缀和,即调用build方法
arr里就是最终1~n范围上的每个数字
例题分析:
题目1
航班预订统计 测试链接:https://leetcode.cn/problems/corporate-flight-bookings/
1 | 这里有 n 个航班,它们分别从 1 到 n 进行编号。 |
标准差分做法,建立差分数组,然后通过前缀和技巧收集答案。
题目2
等差数列差分模版 测试链接:https://www.luogu.com.cn/problem/P4231
1 | 一开始1~n范围上的数字都是0,一共有m个操作,每次操作为(l,r,s,e,d) |
题目3
等差数列差分经典题目 测试链接 : https://www.luogu.com.cn/problem/P5026
1 | 一群人落水后求每个位置的水位高度 |
如果把一般差分对后续区间的影响看做一条直线,那么等差数列差分便是一条斜线线。
二维前缀和
为什么我们要构建二维前缀和?
目的是预处理出一个结构,以后每次查询二维数组任何范围上的累加和都是O(1)的操作
1 | 1 根据原始状况,生成二维前缀和数组sum, |
二维差分
在二维数组中,如果经历如下的过程
1 批量的做如下的操作,每个操作都有独立的a、b、c、d、v
void add(a, b, c, d, v) : 左上角(a,b)到右下角(c,d)范围上,每个数字+v,怎么快速处理?
2 操作做完后,如何正确得到二维数组中每个位置的值?
这就是二维差分的主要工作,add时候快速处理,最后build得到每个位置的值,修改操作必须集中在一起,不能边修改边查询。
1)add方法实现,比较巧妙!
2)build方法实现,和处理前缀和类似
3)真实数据用一圈0包裹起来,可以减少很多边界讨论
例题分析:
题目1
二维前缀和模版
https://leetcode.cn/problems/range-sum-query-2d-immutable/
题目2
边框为1的最大正方形
测试链接 : https://leetcode.cn/problems/largest-1-bordered-square/
1 | 给你一个由若干 0 和 1 组成的二维网格 grid |
就是整个正方形扣掉中间那一块计算出来的值是不是等于边界全由一组成的值就行。
题目3
二维差分模版
https://www.nowcoder.com/practice/50e1a93989df42efb0b1dec386fb4ccc
https://www.luogu.com.cn/problem/P3397
题目4
用邮票贴满网格图
测试链接 : https://leetcode.cn/problems/stamping-the-grid/
1 | 给你一个 m * n 的二进制矩阵 grid |
遍历所有可能放置邮票的左上角位置 (i, j)
计算 (i, j) 到 (i + stampHeight - 1, j + stampWidth - 1) 这个区域的和
计算 cnt 的前缀和,用于快速查询某个区域是否有邮票
检查所有的 0 是否都被覆盖
题目5
重要!包含离散化技巧!
最强祝福力场
测试链接 : https://leetcode.cn/problems/xepqZ5/
1 | 小扣在探索丛林的过程中,无意间发现了传说中"落寞的黄金之都" |
由于本题可能要我们处理类似1.5、0.5之类的值,但是我们期望继续使用整型处理,
这里使用离散化技巧,我们可以让所有的坐标倍增一个倍数,类似于整体拉长整个坐标,于是我们就可以发现,原来的不好处理的数字现在落到了好处理的坐标上,而且所有的相对位置都没有发生改变。
有种分辨率变大的感觉~
本题就是通过构建二维差分数组,计算叠加每次立场的强度,然后最后找到最大强度即可。
但是坑点在于数字很大,n会来到10^9的规模,怎么办呢?
这时候就要处理数据,
我们可以记录每一个点x坐标所在位置
例如1、73、103、1009、114514
然后将其映射到:1、2、3、4、5
,同理处理y坐标。
这样我们就发现我们可以省下很多没用到的坐标。
滑动窗口
滑动窗口:维持左、右边界都不回退的一段范围,来求解很多子数组(串)的相关问题
滑动窗口的关键:找到 范围 和 答案指标 之间的 单调性关系(类似贪心)
滑动过程:滑动窗口可以用 简单变量 或者 结构 来 维护信息
求解大流程:求子数组在 每个位置 开头 或 结尾 情况下的答案(开头还是结尾在于个人习惯)
例题分析:
题目1
累加和大于等于target的最短子数组长度
测试链接 : https://leetcode.cn/problems/minimum-size-subarray-sum/
1 | 给定一个含有 n 个正整数的数组和一个正整数 target |
维持一个累加和大于 target的滑窗,然后沿途记录滑窗size的最大值即可。
题目2
无重复字符的最长子串
测试链接 : https://leetcode.cn/problems/longest-substring-without-repeating-characters/
1 | 给定一个字符串 s ,请你找出其中不含有重复字符的 最长子串 的长度。 |
维持一个不重复字符的滑窗,沿途收集最大size。
题目3
最小覆盖子串
测试链接 : https://leetcode.cn/problems/minimum-window-substring/
1 | 给你一个字符串 s 、一个字符串 t 。返回 s 中涵盖 t 所有字符的最小子串 |
维持一个含有t中所有字符的滑窗(可以用used数组实现),然后记录最小size即可
题目4
加油站
测试链接 : https://leetcode.cn/problems/gas-station/
1 | 在一条环路上有 n 个加油站,其中第 i 个加油站有汽油 gas[i] 升。 |
环形数组滑窗:
- 如果总油量不小于总消耗量,则一定有解。
- 当
currentSum < 0
时,说明此前的起始点不可行,更新startIndex
。 - 最终返回
startIndex
作为起点,或者返回-1
表示无法绕一圈。
题目5
测试链接 : https://leetcode.cn/problems/replace-the-substring-for-balanced-string/
1 | 替换子串得到平衡字符串 |
要点:不够四分之一要补足,够了四分之一就不用关了。
题目6
K个不同整数的子数组
测试链接 : https://leetcode.cn/problems/subarrays-with-k-different-integers/
1 | 给定一个正整数数组 nums和一个整数 k,返回 nums 中 「好子数组」 的数目。 |
维护cnt数组辅助记录出现次数信息,在入窗时检测次数合法性。
题目7
至少有K个重复字符的最长子串
测试链接 : https://leetcode.cn/problems/longest-substring-with-at-least-k-repeating-characters/
1 | 给你一个字符串 s 和一个整数 k ,请你找出 s 中的最长子串 |
每次要求子串必须含有 require 种字符,每种字符都必须>=k次,这样的最长子串是多长
collect : 窗口中一共收集到的种类数
satisfy : 窗口中达标的种类数(次数>=k)
双指针
设置两个指针的技巧,其实这种说法很宽泛,似乎 没什么可总结的
1)有时候所谓的双指针技巧,就单纯是代码过程用双指针的形式表达出来而已。
没有单调性(贪心)方面的考虑
2)有时候的双指针技巧包含单调性(贪心)方面的考虑,牵扯到可能性的取舍。
对分析能力的要求会变高。其实是先有的思考和优化,然后代码变成了 双指针的形式。
3)所以,双指针这个“皮”不重要,分析题目单调性(贪心)方面的特征,这个能力才重要。
常见的双指针类型:
1)同向双指针
2)快慢双指针
3)从两头往中间的双指针
4)其他
例题分析:
题目1
按奇偶排序数组II
测试链接 : https://leetcode.cn/problems/sort-array-by-parity-ii/
1 | 给定一个非负整数数组 nums。nums 中一半整数是奇数 ,一半整数是偶数 |
第一眼我们可能会想到,可以开一个辅助数组,然后遍历原数组,
将对应的数放在对应位置上。
双指针则是优化了空间,我们其实不需要开这个数组,
我们可以一个指针固定看数组最后一位的偶数位,
然后以另一指针指向奇数位,如果位置上已经奇数则跳下一个奇数位,如果位置上是偶数则发货至偶数位,偶数位指针往前跳。
题目2
寻找重复数
测试链接 : https://leetcode.cn/problems/find-the-duplicate-number/
1 | 给定一个包含 n + 1 个整数的数组 nums , |
实际上本题可以转换为图的连通分量求环
直接使用经典双指针算法:Floyd 判环法,寻找相遇点。
题目3
接雨水
测试链接 : https://leetcode.cn/problems/trapping-rain-water/
1 | 给定 n 个非负整数表示每个宽度为 1 的柱子的高度图, |
经典接雨水。
接雨水的本质就是对于某一个柱子i,会有一个水线,
而水线来自左右区间的极值较小值,
所蓄的水量则为高度减去水线。
我们可以构建两个数组用于存储左->右区间的极值和左<-右区间的极值,用于快速查询。
而双指针妙就妙在优化掉了这两个辅助数组。
双指针一个指向1位置,另一个指向末尾减去2的位置,然后分别设定极值为0位置和末尾位置的值。对于左右指针,左指针的左极值是真实的,而右区间极值是待定的,反之则反。
可以看出,如果左指针的左极值要成为计算水线的值,那么左指针就可以马上收获答案,反之则反。
这样就不需要额外维护两个数组了。
题目4
救生艇
测试链接 : https://leetcode.cn/problems/boats-to-save-people/
1 | 给定数组 people |
注意到排序不会影响答案,果断排序。
排完序之后,直接贪心算法,尽量大匹配尽量小。
如果两者匹配之后能坐下一艘船就坐,坐不下就让大的坐。
转换为代码就是两枚指针,l指向开头,r指向末尾,
左右指针不断的用以上逻辑往中间移动即可。
最后结束循环再判断一下l == r ? 如果相等就意味着还有一个没被分配,
为其单独分配一艘船,ans++即可。
题目5
盛最多水的容器
测试链接 : https://leetcode.cn/problems/container-with-most-water/
1 | 给定一个长度为 n 的整数数组 height |
思路是这样的,左右指针往开头结尾一放,往中间走,谁小移动谁,移动时记录极值。
至于为什么,实际上是大的被待定了,它期望小的往下走能遇到更大的。
题目6
供暖器
测试链接 : https://leetcode.cn/problems/heaters/
1 | 冬季已经来临。 你的任务是设计一个有固定加热半径的供暖器向所有房屋供暖。 |
也是先排序,然后两个数组分别放置指针指向开头。
然后往后匹配,每个房屋都尽可能的匹配能遇到的最往右的最短距离取暖器,并在其中记录最远距离。
题目7
缺失的第一个正数
测试链接 : https://leetcode.cn/problems/first-missing-positive/
1 | 给你一个未排序的整数数组 nums ,请你找出其中没有出现的最小的正整数。 |
我们把整个区间分为
(整理区) l (待定区) r (垃圾区)
l的左边,都是做到i位置上放着i+1的区域
永远盯着l位置的数字看,看能不能扩充(l++)
最好的状况下,认为1~r是可以收集全的,每个数字收集1个,不能有垃圾
有垃圾呢?预期就会变差(r–)
在所有元素都被整理完毕之后(垃圾去往垃圾区,期望元素进入整理区)
答案自然就出现了。
二分答案
二分答案法
1)估计 最终答案可能的范围 是什么,可以定的粗略,反正二分不了几次
2)分析 问题的答案 和 给定条件 之间的 单调性,大部分时候只需要用到 自然智慧
3)建立一个check函数,当答案固定的情况下,判断 给定的条件是否达标
4)在 最终答案可能的范围上不断二分搜索,每次用check函数判断,直到二分结束,找到最合适的答案
核心点:分析单调性、建立check函数
为什么要这样做?很多时候我们面对的问题没有一个确切的目标不好求,
但是如果我们有了一个确切目标,例如例题一中我们假设能够达到速度k,
你会发现我们就很好去设计代码验证答案。
这个技巧常用且重要,一定要引起重视,非常的美、精妙!
例题分析:
题目1
爱吃香蕉的珂珂
测试链接 : https://leetcode.cn/problems/koko-eating-bananas/
1 | 珂珂喜欢吃香蕉。这里有 n 堆香蕉,第 i 堆中有 piles[i] 根香蕉 |
经典二分答案
(…不能满足…)…ans…(…能满足…)
题目2
分割数组的最大值(画匠问题)
测试链接 : https://leetcode.cn/problems/split-array-largest-sum/
1 | 给定一个非负整数数组 nums 和一个整数 m |
一样的,只是check函数的设计需要巧妙设计。if (check(nums, mid) <= k)
作为核心点
1 | int check(vector<int> &nums, int limit) { |
题目3
机器人跳跃问题
测试链接 : https://www.nowcoder.com/practice/7037a3d57bbd4336856b8e16a9cafd71
1 | 机器人正在玩一个古老的基于DOS的游戏,游戏中有N+1座建筑,从0到N编号,从左到右排列 |
1 | bool check(vector<int> &buildings, long long e) { |
题目4
找出第K小的数对距离
测试链接 : https://leetcode.cn/problems/find-k-th-smallest-pair-distance/
1 | 数对 (a,b) 由整数 a 和 b 组成,其数对距离定义为 a 和 b 的绝对差值。 |
if (check(nums, mid) >= k)
1 | int check(vector<int> &nums, int distace) { |
题目5
同时运行N台电脑的最长时间
测试链接 : https://leetcode.cn/problems/maximum-running-time-of-n-computers/
1 | 你有 n 台电脑。给你整数 n 和一个下标从 0 开始的整数数组 batteries |
难点在于考虑到碎片电池的使用策略,实际上也就是贪心算法的范畴了。
1 | bool check(vector<int> &batteries, long long limit, int n) { |
题目6
计算等位时间
1 | 给定一个数组arr长度为n,表示n个服务员,每服务一个客人的时间 |
谷歌的面试,这个题连考了2个月
check函数检验当来到时刻n时,服务员们能给多少位人 开始 服务。
题目7
刀砍毒杀怪兽问题
1 | 怪兽的初始血量是一个整数hp,给出每一回合刀砍和毒杀的数值cuts和poisons |
真实大厂算法笔试题
check检验来到n回合时能否斩杀怪物,当然以贪心思维去设计检验机制。
单调栈、单调队列
单调栈、单调队列都是以维护结构单调性的方式,保存答案的可能性。
单调栈
单调栈最经典的用法是解决如下问题:
每个位置都求:
0)当前位置的 左侧比当前位置的数字小,且距离最近的位置 在哪
1)当前位置的 右侧比当前位置的数字小,且距离最近的位置 在哪
或者
每个位置都求:
0)当前位置的 左侧比当前位置的数字大,且距离最近的位置 在哪
1)当前位置的 右侧比当前位置的数字大,且距离最近的位置 在哪
用单调栈的方式可以做到:求解过程中,单调栈所有调整的总代价为O(n),单次操作的均摊代价为O(1)
除了单调栈最经典的用法之外,在很多问题里单调栈还可以 维持求解答案的可能性
1)单调栈里的所有对象按照 规定好的单调性来组织
2)当某个对象进入单调栈时,
会从 栈顶开始 依次淘汰单调栈里 对后续求解答案没有帮助 的对象
3)每个对象从栈顶弹出的时 结算当前对象参与的答案,随后这个对象 不再参与后续求解答案的过程
4)其实是 先有对题目的分析!进而发现单调性,然后利用 单调栈的特征 去实现
例题分析:
题目1
单调栈最经典用法的模版
测试链接 : https://www.nowcoder.com/practice/2a2c00e7a88a498693568cef63a4b7bb
题目2
每日温度
测试链接 : https://leetcode.cn/problems/daily-temperatures/
1 | 给定一个整数数组 temperatures ,表示每天的温度,返回一个数组 answer |
经典到不能再经典的单调栈标准题目。
题目3
子数组的最小值之和
测试链接 : https://leetcode.cn/problems/sum-of-subarray-minimums/
1 | 给定一个整数数组 arr,找到 min(b) 的总和,其中 b 的范围为 arr 的每个(连续)子数组。 |
注意这道题答案很大,要求取模
很好理解,维持大压小,来到破坏单调性的位置意味着收获答案,然后逐层收集即可。
题目4
柱状图中最大的矩形
测试链接 : https://leetcode.cn/problems/largest-rectangle-in-histogram
1 | 给定 n 个非负整数,用来表示柱状图中各个柱子的高度 |
对于 i号柱子 他能往左右伸展的最大距离取决于他左右能碰到的第一个小于他的 柱子,
这一点就是破题关键。
题目5
最大矩形
测试链接 : https://leetcode.cn/problems/maximal-rectangle/
1 | 给定一个仅包含 0 和 1 、大小为 rows * cols 的二维二进制矩阵 |
利用 压缩矩阵 的技巧,转化为.lc.84. 柱状图中最大的矩形问题
题目6
最大宽度坡
测试链接 : https://leetcode.cn/problems/maximum-width-ramp/
1 | 给定一个整数数组 A,坡是元组 (i, j),其中 i < j 且 A[i] <= A[j] |
难点在于怎么想到用单调栈
本质上还是维护答案的候选者,淘汰不可能成为答案的候选。
在此基础上寻找尽可能长的坡
题目7
去除重复字母保证剩余字符串的字典序最小
测试链接 : https://leetcode.cn/problems/remove-duplicate-letters/
1 | 给你一个字符串 s ,请你去除字符串中重复的字母,使得每个字母只出现一次 |
额外维护一个词频表和一个入栈表。
- 根据词频表和入栈表已经题目要求决定当前栈顶字母是否能被弹出,能被弹出的字母应当是后续仍然可以遍历到的,且字典序大于当前待入栈字母字典序的。
- 如果一个字母已经入栈,那么就不能再入栈了。
题目8
大鱼吃小鱼问题
测试链接 : https://www.nowcoder.com/practice/77199defc4b74b24b8ebf6244e1793de
1 | 给定一个数组arr,每个值代表鱼的体重 |
依然是维护答案的可能性。
从右往左看,维护一个信息,当这个鱼被吃时,至少需要过了多少回合,后面吃掉他的鱼继承他的回合数。
题目9
统计全1子矩形的数量
测试链接 : https://leetcode.cn/problems/count-submatrices-with-all-ones/
1 | 给你一个 m * n 的矩阵 mat,其中只有0和1两种值 |
两个技巧:1. 矩阵压缩、2. 从上往下滚动计算==即我们来到i行时,只关注以i行为底边的矩形
然后问题就又转化为.lc.84. 柱状图中最大的矩形问题
单调队列
单调队列最经典的用法是解决如下问题:
滑动窗口在滑动时,r++代表右侧数字进窗口,l++代表左侧数字出窗口
这个过程中,想随时得到当前滑动窗口的 最大值 或者 最小值
窗口滑动的过程中,单调队列所有调整的总代价为O(n),单次操作的均摊代价为O(1)
除了单调队列最经典的用法之外,在很多问题里单调队列还可以 维持求解答案的可能性
1)单调队列里的所有对象按照 规定好的单调性来组织
2)当某个对象从队尾进入单调队列时,
会从 队头 或者 队尾 依次淘汰单调队列里,对后续求解答案没有帮助 的对象
3)每个对象一旦从单调队列弹出,可以结算此时这个对象参与的答案,
随后这个对象 不再参与后续求解答案的过程
4)其实是 先有对题目的分析!进而发现单调性,然后利用 单调队列的特征 去实现
例题分析:
题目1
滑动窗口最大值(单调队列经典用法模版)
测试链接 : https://leetcode.cn/problems/sliding-window-maximum/
1 | 给你一个整数数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧 |
题目2
绝对差不超过限制的最长连续子数组
测试链接 : https://leetcode.cn/problems/longest-continuous-subarray-with-absolute-diff-less-than-or-equal-to-limit/
1 | 给你一个整数数组 nums ,和一个表示限制的整数 limit |
题目3
接取落水的最小花盆
测试链接 : https://www.luogu.com.cn/problem/P2698
1 | 老板需要你帮忙浇花。给出 N 滴水的坐标,y 表示水滴的高度,x 表示它下落到 x 轴的位置 |
先排个序。
感觉很像滑窗)
题目4
和至少为K的最短子数组
测试链接 : https://leetcode.cn/problems/shortest-subarray-with-sum-at-least-k/
1 | 给定一个数组arr,其中的值有可能正、负、0 |
注意:本题用到构建前缀和的技巧
要点在于构建了前缀和之后可以很方便的查询区间和。
题目5
满足不等式的最大值
测试链接 : https://leetcode.cn/problems/max-value-of-equation/
1 | 给你一个数组 points 和一个整数 k |
要点在于分析题目,将限制条件拆成(Yl - Xl) + (Yr + Xr)==即左端点的YX差加上右端点的YX和。
题目6
你可以安排的最多任务数目
测试链接 : https://leetcode.cn/problems/maximum-number-of-tasks-you-can-assign/
1 | 给你 n 个任务和 m 个工人。每个任务需要一定的力量值才能完成 |
注意:本题大思路用到二分答案法
大思路是二分答案,而check函数的实现则同时利用了贪心思路和单调队列(好像只用到了队列性质。。。)。
1 | bool check(int tl, int tr, int wl, int wr, int s, int pills) { |
并查集
并查集的使用是如下的场景
1)一开始每个元素都拥有自己的集合,在自己的集合里只有这个元素自己
2)find(i):查找i所在集合的代表元素,代表元素来代表i所在的集合
3)boolean isSameSet(a, b):判断a和b在不在一个集合里
4)void union(a, b):a所在集合所有元素 与 b所在集合所有元素 合并成一个集合
5)各种操作单次调用的均摊时间复杂度为O(1)
并查集的小扩展
可以定制信息:并查集目前有多少个集合,以及给每个集合打上标签信息
神奇优化:为什么并查集是O(1)的操作?
并查集的两个优化,都发生在find方法里
1)扁平化(路径压缩)(一定要做)
2)小挂大(可以不做,原论文中是秩的概念,可以理解为 粗略高度 或者 大小)
并查集时间复杂度的理解
作为如此简单、小巧的结构,
感性理解单次调用的均摊时间复杂度为O(1)即可,其实为α(n),反阿克曼函数。
当n=10^80次方即可探明宇宙原子量,α(n)的返回值也不超过6,那就可以认为是O(1)
并查集的发明者Bernard A. Galler和Michael J. Fischer,
从1964年证明到1989年才证明完毕。
例题分析:
题目1
并查集模版(牛客)
测试链接 : https://www.nowcoder.com/practice/e7ed657974934a30b2010046536a5372
路径压缩 + 小挂大
题目2
并查集模版(洛谷)
测试链接 : https://www.luogu.com.cn/problem/P3367
用递归函数实现路径压缩
一般情况下小挂大的优化可以省略的写法
题目3
情侣牵手
测试链接 : https://leetcode.cn/problems/couples-holding-hands/
1 | n对情侣坐在连续排列的 2n 个座位上,想要牵到对方的手 |
将所有情侣对(先不管配不配对)视为单元素集合。
然后合并所有未配对集合。
题目4
相似字符串组
测试链接 : https://leetcode.cn/problems/similar-string-groups/
1 | 如果交换字符串 X 中的两个不同位置的字母,使得它和字符串 Y 相等 |
开始时每个字符串都是单元素集合,然后根据相似关系例如:
{str1->str2->str3},
以此构建关联集合。
题目5
岛屿数量
测试链接 : https://leetcode.cn/problems/number-of-islands/
1 | 给你一个由 '1'(陆地)和 '0'(水)组成的的二维网格,请你计算网格中岛屿的数量 |
注意:本题还可以用洪水填充算法求解
没啥好说的,将所有相邻的1合并为一个集合就行,最后统计剩下的集合数就好。
题目6
移除最多的同行或同列石头
测试链接 : https://leetcode.cn/problems/most-stones-removed-with-same-row-or-column/
1 | n 块石头放置在二维平面中的一些整数坐标点上。每个坐标点上最多只能有一块石头 |
我们需要统计来到 (i , j) 位置时,当前位置是否已经有了石头?有则合并 。
所以额外维护一个行列石头最早出现表就行
题目7
找出知晓秘密的所有专家
链接测试 : https://leetcode.cn/problems/find-all-people-with-secret/
1 | 给你一个整数 n ,表示有 n 个专家从 0 到 n - 1 编号 |
先给会议序列按照来到时刻排序,然后合并同时刻的集合,标记被传染的集合,未被传染的集合拆分回原样。
题目8
好路径的数目
测试链接 : https://leetcode.cn/problems/number-of-good-paths/
1 | 给你一棵 n 个节点的树(连通无向无环的图) |
关键在于边处理顺序:
将待处理边按照关联点权值的max按从小大大排序,
然后合并同权集合时结算答案,不同权集合则不结算。
这样的顺序可以保证我们再处理到类似 {max: 1} ~ {max: 2} ~ {max: 1}时,不会重复计算。
题目9
尽量减少恶意软件的传播 II
测试链接 : https://leetcode.cn/problems/minimize-malware-spread-ii/
1 | 给定一个由 n 个节点组成的网络,一定是无向图,用 n * n 个邻接矩阵 graph 表示 |
先合并所有正常点。
然后根据正常集合链接的传染源判断答案。
洪水填充
洪水填充是一种很简单的技巧,设置路径信息进行剪枝和统计,类似感染的过程
路径信息不撤销,来保证每一片的感染过程可以得到区分
看似是暴力递归过程,其实时间复杂度非常好,遍历次数和样本数量的规模一致
例题分析:
题目1
岛屿数量
测试链接 : https://leetcode.cn/problems/number-of-islands/
1 | 给你一个由 '1'(陆地)和 '0'(水)组成的的二维网格,请你计算网格中岛屿的数量 |
没啥好说的,标准靶场)
就是遇到 ‘1’ 就洪水填充为 ‘0’, 然后计数。
题目2
被围绕的区域
测试链接 : https://leetcode.cn/problems/surrounded-regions/
1 | 给你一个 m x n 的矩阵 board ,由若干字符 'X' 和 'O' ,找到所有被 'X' 围绕的区域 |
我tm的把O看成0了。
一样的,先把边界处的O洪水填充标注为F
然后遍历将 O 改 X, F 改 O 就行。
题目3
最大人工岛
测试链接 : https://leetcode.cn/problems/making-a-large-island/
1 | 给你一个大小为 n * n 二进制矩阵 grid 。最多 只能将一格 0 变成 1 。 |
只是比前面的题目多了要求,你需要额外计算洪水填充填了多少格子,可以放在洪水填充中计算,也可以后面遍历一遍统计。
题目4
打砖块
测试链接 : https://leetcode.cn/problems/bricks-falling-when-hit/
1 | 有一个 m * n 的二元网格 grid ,其中 1 表示砖块,0 表示空白 |
这道题的精髓在于 时间回溯。
从 后 往 前 逆向分析,然后配合洪水填充标记砖块。