每周LeetCode回顾-1
456. 132模式
思路:
从后往前遍历,维护一个单调递减的栈,同时使用
k
记录所有出栈元素的最大值。
class Solution { |
- 时间复杂度:O(n)
- 空间复杂度:O(n)
189. 轮转数组
思路:
)
class Solution { |
- 时间复杂度:O(n)
- 空间复杂度:O(1)
73. 矩阵置零
思路:
使用与行列数量相等的数组记录置零信息。
class Solution { |
- 时间复杂度:O(m × n)
- 空间复杂度:O(m + n)
238. 除自身以外数组的乘积
思路:
answer[i]
等于 nums
中除了
nums[i]
之外其余各元素的乘积。换句话说,如果知道了
i
左边所有数的乘积,以及 i
右边所有数的乘积,就可以算出 answer[i]
。
于是:
定义
pre[i]
表示从nums[0]
到nums[i−1]
的乘积。定义
suf[i]
表示从nums[i+1]
到nums[n−1]
的乘积。
我们可以先计算出从 nums[0]
到 nums[i−2]
的乘积 pre[i−1]
,再乘上 nums[i−1]
,就得到了
pre[i]
,即 \[
pre[i]=pre[i−1]⋅nums[i−1]
\] 同理有
\[
suf[i]=suf[i+1]⋅nums[i+1]
\]
初始值:pre[0]=suf[n−1]=1
。按照上文的定义,pre[0]
和 suf[n−1]
都是空子数组的元素乘积,我们规定这是 1,因为 1
乘以任何数 x
都等于 x
,这样可以方便递推计算
pre[1]
,suf[n−2]
等。
算出 pre
数组和 suf
数组后,有
\[ answer[i]=pre[i]⋅suf[i] \]
class Solution { |
- 时间复杂度:O(n)
- 空间复杂度:O(n)
2134. 最少交换次数来组合所有的 1 II
思路:
将问题转化为 定长滑动窗口内 0 的最小个数
class Solution { |
- 时间复杂度:O(n)
- 空间复杂度:O(n)
1297. 子串的最大出现次数
思路:
只需统计长度为 minSize 的子串,而不需要统计长度为 maxSize 的字串,因为 “abc” 肯定会覆盖 “a”,“ab”,即长的肯定会覆盖短的,只需要考虑最短的即可。
class Solution { |
- 时间复杂度:**O(n*minSize)**
- 空间复杂度:**O(n*minSize)**
2389. 和有限的最长子序列
思路:
- 贪心:由于元素和有上限,为了能让子序列尽量长,子序列中的元素值越小越好。
对于本题来说,元素在数组中的位置是无关紧要的(因为我们计算的是元素和),所以可以排序了。把
nums
从小到大排序后,再从小到大选择尽量多的元素(相当于选择一个前缀),使这些元素的和不超过询问值。
- 既然求的是前缀的元素和(前缀和),那么干脆把每个前缀和都算出来。做法是递推:前
i
个数的元素和,等于前i−1
个数的元素和,加上第i
个数的值。
例如 [4,5,2,1]
排序后为
[1,2,4,5]
,从左到右递推计算前缀和,得到
[1,3,7,12]
。
- 由于
nums[i]
都是正整数,前缀和是严格单调递增的,这样就能在前缀和上使用二分查找:找到大于queries[i]
的第一个数的下标,由于下标是从0
开始的,这个数的下标正好就是前缀和小于等于queries[i]
的最长前缀的长度。
例如在 [1,3,7,12]
二分查找大于 3
的第一个数(7
),得到下标
2
,这正好就是前缀和小于等于 3
的最长前缀长度。对应到 nums
中,就是选择了 2
个数(1
和 2
)作为子序列中的元素。
class Solution { |
- 时间复杂度:O((n + m) log n)
- 空间复杂度:O(n + m)
2140. 解决智力问题
思路:
一、寻找子问题
在示例1
中,我们要解决的问题(原问题)是:
- 剩余问题的下标为
[0, 3]
,计算从这些问题中可以获得的最大分数。
讨论 questions[0]
选或不选的情况:
- 如果不选
questions[0]
子问题变为:剩余问题的下标为[1, 3]
,计算从这些问题中可以获得的最大分数。 - 如果选,接下来的
2
个问题(brainpower[0] = 2
)都不能选,子问题变为:剩余问题的下标为[3, 3]
,计算从这些问题中可以获得的最大分数。
二、状态定义与递推公式
根据上面的讨论,定义 dp(i)
表示:剩余问题的下标为
[i, n-1]
时,能获得的最大分数(n
是
questions
的长度)。
讨论 questions[i]
选或不选的情况:
- 如果不选,子问题为
dp(i + 1)
(剩余问题下标[i+1, n-1]
)。 - 如果选,跳过
brainpower[i]
个问题,子问题为dp(i + brainpower[i] + 1)
(剩余问题下标[i + brainpower[i] + 1, n-1]
),并累加当前分数points[i]
。
递推公式: \[ dp(i)=max(dp(i+1),dp(i+brainpower[i]+1)+points[i]) \]
class Solution { |
- 时间复杂度:O(n)
- 空间复杂度:O(n)
53. 最大数组和
思路 1:前缀和 + 贪心
由于子数组的元素和等于两个前缀和的差,所以求出 nums
的前缀和,问题就变成 121.
买卖股票的最佳时机
了。本题子数组不能为空,相当于一定要交易一次。
class Solution { |
- 时间复杂度:O(n)
- 空间复杂度:O(n)
思路 2:动态规划
动态规划定义
定义 dp[i]
表示以 nums[i]
结尾的最大子数组和。
状态转移方程
分两种情况讨论:
nums[i]
单独成子数组dp[i] = nums[i]
。nums[i]
与前面的子数组拼接dp[i] = dp[i-1] + nums[i]
。
综合两种情况,取最大值: \[ f(x) = \begin{cases} nums[i], & i = 0 \\ max(dp[i-1],0)+nums[i], & i \geq 0 \end{cases} \]
class Solution { |
- 时间复杂度:O(n)
- 空间复杂度:O(1)
2606. 找到最大开销的子字符串
思路:
按照题目要求,把 s
中的字母映射到数字上,设映射后变成了数组
a
,那么题目相当于求 a
的 53.最大子数组和(允许子数组为空)。
定义 dp[i]
为以 a[i]
结尾的最大子数组和,考虑是否接在以 a[i−1]
结尾的最大子数组之后:
接:\(dp[i]=dp[i−1]+a[i]\)
不接:\(dp[i]=a[i]\)
二者取最大值,得 \[ dp[i]=max(dp[i−1],0)+a[i] \] 答案为 \(max(dp)\)。
class Solution { |
- 时间复杂度:O(n + m)
- 空间复杂度:O(n + m)
2192. 有向无环图中一个节点的所有祖先
思路:正向 DFS
例如示例 1,从 2 出发 DFS,可以访问到 4,6,7,那么把 2 加到
answer[4]
,answer[6]
,answer[7]
中。
依次从起点 start=0,1,2,⋯,n−1
出发 DFS,途中把
start
加到能访问到的点的 answer
中。由于
start
从小到大枚举,所以 answer[i]
列表自然就是有序的了。
例如:
- 从 0 出发访问到 5,把 0 加到
answer[5]
中,现在answer[5]=[0]
。 - 从 1 出发访问到 5,把 1 加到
answer[5]
中,现在answer[5]=[0,1]
。 - 从 3 出发访问到 5,把 3 加到
answer[5]
中,现在answer[5]=[0,1,3]
。
小 tips :正常情况下,每次从父节点出发都要重置 visited
数组,由于题目特性无需每次重置。我们会跑 n
个 DFS,每个 DFS
的 start
都是不同的。利用这一条件,当访问到节点
x
时,标记 vis[x]=start
,表示 x
是本轮 DFS 中访问到的节点。当我们访问到某个节点 y
时,如果发现 vis[y]=start
,就表示 y
访问过了,否则没有访问过。
class Solution { |
- 时间复杂度:O(n²)
- 空间复杂度:O(n²)
2653. 滑动子数组的美丽值
思路:
由于值域很小,所以借鉴计数排序,用一个 cnt
数组维护窗口内每个数的出现次数。然后遍历 cnt
去求第
x
小的数。
什么是第 x
小的数?
设它是 \(num\),那么 \(<num\) 的数有 \(<x\) 个,\(≤num\) 的数有 \(≥x\) 个,就说明 \(num\) 是第 \(x\) 小的数。
比如 [−1,−1,−1]
中,第 1,2,3
小的数都是
−1
。
class Solution { |
- 时间复杂度:O(n)
- 空间复杂度:O(n)
2080. 区间内查询数字的频率
思路:
对于 arr
中的每个数,计算其在 arr
中的出现位置(下标)。例如 arr=[1,2,1,1,2,2]
,其中数字
2
的下标为 [1,4,5]
。
知道了下标,那么对于 query
来说,问题就变成了:
下标列表中,满足 \(left≤i≤right\)
的下标 i
的个数。 例如 query(3,5,2)
,由于数字
2
的下标列表 [1,4,5]
中的下标 4
和 5
都在区间 [3,5]
中,所以返回
2
。
把下标列表记作数组 a
,由于 a
是有序数组,我们可以用二分查找快速求出:
a 中的第一个 \(≥left\) 的数的下标,设其为
p
。如果不存在,则p
等于a
的长度。a 中的第一个 \(>right\) 的数的下标,设其为
q
。如果不存在,则q
等于a
的长度。
a 中的下标在 [p,q−1]
内的数都是满足要求的,这有
q−p
个。特别地,如果 a
中没有满足要求的下标,那么 q−p=0
,这仍然是正确的。
class RangeFreqQuery { |
- 查询时间复杂度:O(log m)
- 空间复杂度:O(n)
962. 最大宽度坡
思路:
- 首先正序遍历数组
A
,将以A[0]
开始的递减序列的元素下标依次存入栈中。以[6,1,8,2,0,5]
为例,由于(6,1,0)
是递减的,所以栈中存的元素应该为:\((栈顶 -> (4,1,0)<-栈底)\)。 - 此时栈
stack:(4(0), 1(1), 0(6))
:然后逆序排列数组A
,若以栈顶元素为下标A[stack.peek()]
小于等于当前遍历的元素A[i]
,即A[stack.peek()] <= A[i]
。此时就是一个满足条件的坡的宽度,并且这个宽度一定是栈顶这个坡底 i 能形成的最大宽度,将栈顶元素出栈并计算当前坡的宽度,保留最大值即可。
class Solution { |
- 时间复杂度:O(n)
- 空间复杂度:O(n)
1749. 任意子数组和的绝对值的最大值
思路 1:动态规划
问题可以转换成求 53. 最大子数组和 以及「最小子数组和的绝对值(相反数)」,这二者中的最大值就是答案。
考虑以 nums[i]
结尾的最大子数组和:
如果子数组只有一个数,那么最大子数组和就是
nums[i]
。如果把
nums[i]
和前面的子数组拼起来,那么问题变成求「以nums[i−1]
结尾的最大子数组和」。
这启发我们得到下面的状态定义和状态转移方程。
定义 f[i]
表示以 nums[i]
结尾的最大子数组和:
如果子数组只有一个数:f[i]=nums[i]
。 如果把
nums[i]
和前面的子数组拼起来:f[i]=f[i−1]+nums[i]
。
这两种情况取最大值,即 \[
f[i]=max(nums[i],f[i−1]+nums[i])=max(f[i−1],0)+nums[i]
\] 枚举子数组的最后一个数,最大子数组和就是 \[
max(max(f),0)
\] 这里与 0 取最大值是因为子数组可以为空。
最小子数组和的计算方法与最大子数组和类似。
class Solution { |
- 时间复杂度:O(n)。
- 空间复杂度:O(1)。
思路 2:前缀和
设 nums
的前缀和数组为
s
,根据前置知识,子数组的和等于两个前缀和的差
s[j]−s[i]
。所以子数组和的绝对值等于
\[
∣s[j]−s[i]∣
\] s[i]
和 s[j]
相差越大,上式也就越大。
给你一堆数,哪两个数相差最大?这堆数字中的最大值和最小值相差最大。
所以,最大的差来自 s
中的最大值和最小值,所以答案为
\[ max(s)−min(s) \] 你也可以这样理解:
如果最大前缀和在最小前缀和的右边,那么上式算的是最大子数组和。
如果最大前缀和在最小前缀和的左边,那么上式算的是最小子数组和的绝对值。最小的负数取绝对值可以得到最大的正数。
class Solution { |
优化版本:
class Solution { |
- 时间复杂度:O(n)
- 空间复杂度:O(1)
3387. 两天自由外汇交易后的最大货币数
思路:
- 根据
pairs1
和rates1
建图。 - 从
initialCurrency
开始,自顶向下 DFS 这张图,递归的同时维护金额。记录把initialCurrency
兑换成其他货币的金额day1Amount
。 - 根据
pairs2
和rates2
建图。 - 同样地,从
initialCurrency
开始,自顶向下 DFS 这张图,递归的同时维护金额。记录把initialCurrency
兑换成其他货币的金额day2Amount
。金额的倒数,就是从其他货币兑换成initialCurrency
的金额。
枚举中转货币 x,答案为 \(\frac{day2Amount[x]}{day1Amount[x]}\) 的最大值。
class Solution { |
- 时间复杂度:O((n+m)L),其中 n 是
pairs1
的长度,m 是pairs2
的长度,L 是单个字符串的长度(不超过 3)。 - 空间复杂度:O((n+m)L)。