【突然冒泡】刷刷 Leetcode
可以祝我保研上岸吗ww
发现自己代码水平下降许多,还是得多刷刷 leetcode 啊!(虽然很喜欢某谷,但是这时候还是力扣更对口一点),不说废话了。
题目这边主攻【中等】和【困难】的题目,语言使用 Python。
tips:点击小标题可以查看题目哦~
6.26
盛最多水的容器
遇到这种题先排个序应该没啥问题,然后考虑从最短的枚举,当做瓶颈点,看看作为左端点和右端点时分别最大能有多少水。这时候需要维护一个左右两侧比当前这个数还大的最远的位置,因为比这个数小的地方不满足瓶颈假设,而且枚举瓶颈是完备的,复杂度 \(\mathcal O(n\log n)\):
class Solution(object):
def maxArea(self, height):
"""
:type height: List[int]
:rtype: int
"""
n = len(height)
vis = [0 for i in range(n)]
min_p, max_p = 0, n - 1
ans = -1
a = [[i, height[i]] for i in range(n)]
a.sort(key=lambda x: x[1])
for i in range(n):
now_bl = (a[i][0] - min_p) * a[i][1]
now_br = (max_p - a[i][0]) * a[i][1]
ans = max(ans, max(now_bl, now_br))
vis[a[i][0]] = 1
if i == n - 1:
break
if a[i][0] == min_p:
while True:
min_p += 1
if vis[min_p] == 0:
break
elif a[i][0] == max_p:
while True:
max_p -= 1
if vis[max_p] == 0:
break
return ans
这题竟然可以线性唉,就是维护一个双指针,从外往里扫,每次都动比较小的那个指针就好了,好巧妙。
接雨水
这个很经典了,直接统计前缀最大值和后缀最大值就行了。然后每个位置是两者的较小者,如果这边海拔低于这个数,那么就可以填进差值的水。时间复杂度 \(\mathcal O(n)\):
class Solution(object):
def trap(self, height):
"""
:type height: List[int]
:rtype: int
"""
n = len(height)
max_l, max_r = [0 for i in range(n)], [0 for i in range(n)]
for i in range(1, n):
max_l[i] = max(max_l[i - 1], height[i - 1])
for i in range(n - 2, -1, -1):
max_r[i] = max(max_r[i + 1], height[i + 1])
sumw = 0
for i in range(n):
if (height[i] < max_l[i]) & (height[i] < max_r[i]):
H = min(max_l[i], max_r[i])
sumw += (H - height[i])
return sumw
文本左右对齐
大模拟的压迫感,直接不断试探就好了,不断维护一下个开头的位置。有一个比较坑的地方就是必须要有至少一个空格,但是每行最后一个单词不和空格绑定,所以需要细化一下这个细节,其他的都很简单了。感觉力扣复杂度分析很难绷,应该是 \(\mathcal O(n)\) 的复杂度吧,它说是平方级别的:
class Solution(object):
def fullJustify(self, words, maxWidth):
"""
:type words: List[str]
:type maxWidth: int
:rtype: List[str]
"""
n = len(words)
ls = [len(words[i]) for i in range(n)]
ans = []
now = 0
while True:
t = now
sum_len = 0
end_sig = 0
while True:
if t > now:
sum_len += 1
if sum_len + ls[t] > maxWidth:
sum_len -= 1
break
else:
sum_len += ls[t]
t += 1
if t == n:
end_sig = 1
break
if not end_sig:
if t == now + 1:
ans.append(words[now] + " " * (maxWidth - ls[now]))
else:
extr = maxWidth - sum_len + (t - now - 1)
avg = extr // (t - now - 1)
op = extr - avg * (t - now - 1)
s_now = ""
for i in range(now, t):
s_now += words[i]
if i < t - 1:
s_now += " " * avg
if i - now < op:
s_now += " "
ans.append(s_now)
else:
s_now = ""
now_l = 0
for i in range(now, n):
s_now += words[i]
now_l += ls[i]
if i < n - 1:
s_now += " "
now_l += 1
s_now += " " * (maxWidth - now_l)
ans.append(s_now)
break
now = t
return ans
最小覆盖子串
维护一个双指针,这个东西的正确性仔细想想很容易证明。我觉得难点在于怎么实现不是 \(\mathcal O(52(n+m))\) 的算法,分两个阶段,第一个阶段,确定最前面的最小区间,我们可以维护一下目标串 t 里面有多少个已经满足了,够数了说明匹配完了。第二个阶段,就是后续的拓展阶段,这阶段动左端点只会在缺一个的时候停止,而动右端点的目标实际上就是把这个加回来,那么只用看改变的字母的统计是否刚好符合/不符合即可,时间复杂度 \(\mathcal O(n+m)\):
class Solution(object):
def minWindow(self, s, t):
"""
:type s: str
:type t: str
:rtype: str
"""
st = "abcdefghijklmnopqrstuvwxyz" + "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
std = dict([(st[i], i) for i in range(52)])
cnt = [0 for i in range(52)]
now_cnt = [0 for i in range(52)]
min_l, strt = 998244353, -1
n = len(s)
for c in t:
cnt[std[c]] += 1
l, r, mst, flg = 0, -1, len(t), 0
for i in range(n):
if now_cnt[std[s[i]]] < cnt[std[s[i]]]:
mst -= 1
now_cnt[std[s[i]]] += 1
if mst == 0:
r = i
break
if (len(t) > 0) & (r == -1):
return ""
else:
min_l = r + 1
strt = l
while True:
if (flg == 1) & (r == n - 1):
break
if flg == 0:
while True:
now_cnt[std[s[l]]] -= 1
if now_cnt[std[s[l]]] < cnt[std[s[l]]]:
flg = 1
l += 1
if min_l > r - l + 2:
min_l = r - l + 2
strt = l - 1
break
else:
l += 1
else:
while True:
if r == n - 1:
break
r += 1
now_cnt[std[s[r]]] += 1
if now_cnt[std[s[r]]] == cnt[std[s[r]]]:
flg = 0
if min_l > r - l + 1:
min_l = r - l + 1
strt = l
break
return s[strt: strt + min_l]
二叉树中的最大路径和
不会写邻接表/链式前向星了,感觉得补一补,虽然这个题不会用到。但是还是被他自己定义的 TreeNode 类背刺了,我以为要自己建树呢...
没啥难度,但是我好像做的有点冗余了,写了两次 DFS 的方法,应该一次就够,只需要搜左右两边最好的链和 0 做 MAX,然后加起来就好了,没必要先求最优子链,再扫一遍单链和多链情况,还是太生疏了,时间复杂度 \(\mathcal O(n)\):
class Solution(object):
def maxPathSum(self, root):
"""
:type root: Optional[TreeNode]
:rtype: int
"""
from collections import defaultdict
self.now = 0
self.dc = defaultdict(int)
self.dfs(root)
self.ans = -998244353
self.max_val = -998244353
self.dfs2(root)
if self.ans == 0:
self.ans = self.max_val
return self.ans
def dfs(self, p):
if (p.left == None) & (p.right == None):
self.dc[p] = max(0, p.val)
return
elif p.left != None:
self.dfs(p.left)
self.dc[p] = max(self.dc[p], self.dc[p.left] + p.val)
if p.right != None:
self.dfs(p.right)
self.dc[p] = max(self.dc[p], self.dc[p.right] + p.val)
self.dc[p] = max(self.dc[p], max(0, p.val))
return
def dfs2(self, p):
self.max_val = max(self.max_val, p.val)
if (p.left == None) & (p.right == None):
self.ans = max(self.ans, self.dc[p])
return
elif (p.left == None) & (p.right != None):
self.dfs2(p.right)
self.ans = max(self.ans, self.dc[p])
elif (p.left != None) & (p.right == None):
self.dfs2(p.left)
self.ans = max(self.ans, self.dc[p])
else:
self.dfs2(p.left)
self.dfs2(p.right)
self.ans = max(self.ans, self.dc[p])
self.ans = max(self.ans, self.dc[p.left] + self.dc[p.right] + p.val)
return
6.27
今天太晚了,所以这部分是 6.28 凌晨写的,不过还是归到 6.27 的部分吧。
正则表达式匹配
看数据范围,以为是搜索,然后爆搜调了好久。关键是要理清 * 可以使得前面那个字符只出现 0 次怎么处理,开始以为扫到 * 再说就行,但这是不行的,不是说前面按个字符匹配了,* 是可以代表 0~多个前面的字符,而是可以把潭门看成一个整体,一共出现 0 次也是可以的。
解决这个问题可以提前处理 p 字符串,标记一下哪一些字符是有 * 后缀的,并且消去重复的 *,然后再搜索的时候按照这个标记来分类就行了。结果竟然 TLE 了,想想也是,之前这个时间复杂度也是挺高的,可不是 \(\mathcal O(2^n)\),而是一个 \(\mathcal O(n^m)\) 级别的,不过可以记忆化搜索一下,顺着 dp 其实更好一些,不过感觉记忆化搜索更直白一些,时间复杂度实际上只有 \(\mathcal O(nm)\),感觉这个数据范围有点误导人了。
还有一个细节是结束的判断,为了保证 p 串的最后不是一个可以出现 0 次的后缀,我们再 s 和 p 后面都添加一个 "1" 后缀,用来迫使这个地方匹配,然后就不会出现复杂度判断情况了:
class Solution(object):
def isMatch(self, s, p):
"""
:type s: str
:type p: str
:rtype: bool
"""
self.s = s + "1"
self.lab = []
p_ = p + "1"
q = ""
for i in range(len(p_)):
if i == 0:
q += p_[i]
self.lab.append(0)
else:
if (p_[i] == "*") and (p_[i - 1] == "*"):
continue
elif p_[i] == "*":
self.lab[-1] = 1
else:
q += p_[i]
self.lab.append(0)
self.p = q
self.n = len(self.s)
self.m = len(self.p)
self.dp = [[-1 for j in range(self.m)] for i in range(self.n)]
return self.dfs(0, 0)
def dfs(self, i, j):
if (i == self.n) and (j == self.m):
return True
elif i == self.n:
return False
elif j == self.m:
return False
if self.dp[i][j] >= 0:
return bool(self.dp[i][j])
if self.p[j] == ".":
if self.lab[j] == 1:
ans = False
ans = ans or self.dfs(i + 1, j + 1)
ans = ans or self.dfs(i + 1, j)
ans = ans or self.dfs(i, j + 1)
self.dp[i][j] = int(ans)
return ans
else:
ans = self.dfs(i + 1, j + 1)
self.dp[i][j] = int(ans)
return ans
else:
if self.lab[j] == 1:
ans = False
if self.p[j] == self.s[i]:
ans = ans or self.dfs(i + 1, j + 1)
ans = ans or self.dfs(i + 1, j)
ans = ans or self.dfs(i, j + 1)
else:
ans = ans or self.dfs(i, j + 1)
self.dp[i][j] = int(ans)
return ans
else:
if self.p[j] == self.s[i]:
ans = self.dfs(i + 1, j + 1)
self.dp[i][j] = int(ans)
return ans
else:
ans = False
self.dp[i][j] = int(ans)
return ans
格雷编码
这很 CSP 了,感觉现在还是比高一的 OI 水平强一点哈。完全不记得了,只记得当时要递归,然后我就画了个树,发现直接分奇偶迭代就行了,第奇数个要先补 1,后补 0;第偶数个则是相反(从 0 开始编号),然后就做完了,时间复杂度是 \(\mathcal O(2^n)\):
class Solution(object):
def grayCode(self, n):
"""
:type n: int
:rtype: List[int]
"""
lst_ans = [0, 1]
ans = []
for i in range(1, n):
k = 1 << i
for j in range(k):
if j % 2 == 0:
ans.append((lst_ans[j] << 1))
ans.append(ans[-1] + 1)
else:
ans.append((lst_ans[j] << 1) + 1)
ans.append(ans[-1] - 1)
lst_ans = ans
ans = []
return lst_ans
7.2
前段时间准备面试所以停了几天 qwq
冗余连接
直接并查集即可,第一个有相同祖先的新边端点就是要求的,时间复杂度 \(O(n\alpha(n))\),主要是要会写并查集:
class Solution(object):
def findRedundantConnection(self, edges):
"""
:type edges: List[LisZt[int]]
:rtype: List[int]
"""
n = 0
for e in edges:
n = max(n, max(e[0], e[1]))
self.fa = [i for i in range(n + 1)]
for e in edges:
u, v = e[0], e[1]
if self.getf(u) == self.getf(v):
return e
else:
self.merge(u, v)
return edges[-1]
def getf(self, u):
if u == self.fa[u]:
return u
else:
self.fa[u] = self.getf(self.fa[u])
return self.fa[u]
def merge(self, u, v):
t1 = self.getf(u)
t2 = self.getf(v)
if t1 != t2:
self.fa[t2] = t1
冗余连接 II
好难啊思维题。想了好长时间并查集算法写的都是假的。然后只能从环的角度考虑 DFS,这就暴力好多了,三种情况:多余的边指向根,多余的边不指向根但是成环,多余的边不指向根而且也不成环。第一种情况,所有点入度都是 \(1\),先 DFS 找出环上一个点,然后再 DFS 一次,找到所有组成环的边,这时候去掉环里最晚出现的即可。后两种情况,先从入度为 \(2\) 的点 DFS 一边进行区分。对于第二种情况,仍然是 DFS 一次找环里的边,然后指向这个入度为 \(2\) 的边的删去,这时候根是确定,所以只有一个答案。第三种情况则删掉指向入度为 \(2\) 的点的两条边种出现晚的那条边即可,时间复杂度是 \(O(n)\)。好拙劣啊,写了一百多行,正解应该还是并查集的,但是暂时还不会:
class Solution(object):
def findRedundantDirectedConnection(self, edges):
"""
:type edges: List[List[int]]
:rtype: List[int]
"""
self.n = len(edges)
self.e = []
self.head = [-1 for i in range(self.n + 1)]
self.cnt = 0
self.ind = [0 for i in range(self.n + 1)]
root, p = 0, 0
for i, e in enumerate(edges):
u, v = e[0], e[-1]
self.add(u, v, i)
self.ind[v] += 1
# print(len(self.e))
for i in range(1, self.n + 1):
if self.ind[i] == 0:
root = i
if self.ind[i] == 2:
p = i
po = 0
if p > 0:
# self.s = []
self.t = 0
self.vis = [0 for i in range(self.n + 1)]
self.dfs1(p)
if self.t == 0:
for i in range(self.n - 1, -1, -1):
if edges[i][-1] == p:
return edges[i]
else:
self.vis = [0 for i in range(self.n + 1)]
self.s = []
po = self.dfs2(p, 0, 114514)
for e in self.s:
if e[1] == p:
return [e[0], e[1]]
else:
self.t = 0
self.vis = [0 for i in range(self.n + 1)]
ui = 1
for i in range(1, self.n):
if self.vis[i] == 0:
self.dfs3(i, ui)
if self.t > 0:
break
ui += 1
self.vis = [0 for i in range(self.n + 1)]
self.s = []
po = self.dfs2(self.t, 0, 114514)
idx = -1
for e in self.s:
u, v, w = e[0], e[1], e[2]
if w > idx:
idx = w
return edges[idx]
def add(self, u, v, w):
self.e.append([v, self.head[u], w])
self.head[u] = self.cnt
self.cnt += 1
def dfs1(self, u):
if self.vis[u] == 1:
self.t = u
return
self.vis[u] = True
i = self.head[u]
while True:
if i < 0:
break
# print(i)
v, nxt, w = self.e[i]
self.dfs1(v)
i = nxt
return
def dfs2(self, u, fa, w):
if self.vis[u] == 1:
self.s.append([fa, u, w])
return True
self.vis[u] = True
i = self.head[u]
ty = False
while True:
if i < 0:
break
v, nxt, w = self.e[i]
tyr = self.dfs2(v, u, w)
ty = ty or tyr
i = nxt
if ty == True:
if fa > 0:
self.s.append([fa, u, w])
return ty
def dfs3(self, u, ui):
if self.vis[u] == ui:
self.t = u
return
self.vis[u] = ui
i = self.head[u]
while True:
if i < 0:
break
v, nxt, w = self.e[i]
self.dfs1(v)
i = nxt
return
后面补了一下并查集做法:
class Solution(object):
def findRedundantDirectedConnection(self, edges):
"""
:type edges: List[LisZt[int]]
:rtype: List[int]
"""
n = len(edges)
ind = [0 for i in range(n + 1)]
self.fa = [i for i in range(n + 1)]
flg = 0
for e in edges:
u, v = e[0], e[1]
ind[v] += 1
if ind[v] == 2:
flg = v
if flg > 0:
for i in range(n - 1, -1, -1):
if edges[i][1] == flg:
if self.judge(n, i, edges) == 1:
return edges[i]
else:
continue
else:
self.init(n)
for i in range(n):
u, v = edges[i][0], edges[i][1]
if self.merge(u, v) == 1:
continue
else:
return edges[i]
def init(self, n):
self.fa = [i for i in range(n + 1)]
def getf(self, u):
if self.fa[u] == u:
return u
else:
self.fa[u] = self.getf(self.fa[u])
return self.fa[u]
def merge(self, u, v):
t1 = self.getf(u)
t2 = self.getf(v)
if t1 != t2:
self.fa[t2] = t1
return 1
return 0
def judge(self, n, d, e):
self.init(n)
for i in range(n):
if i == d:
continue
u, v = e[i][0], e[i][1]
if self.merge(u, v) == 1:
continue
else:
return 0
return 1
7.3
统计构造好字符串的方案数
dp 题。可以设 dp[i] 为长度为 \(i\) 的 \(01\) 串的数量,那么就有更新式:
递推即可,最后把 \([\text{low},\text{high}]\) 区间里的 dp 值加起来即为答案,时间复杂度 \(O(n)\)。
class Solution(object):
def countGoodStrings(self, low, high, zero, one):
"""
:type low: int
:type high: int
:type zero: int
:type one: int
:rtype: int
"""
dp = [0 for i in range(high + 1)]
dp[0] = 1
MOD = 1000000000 + 7
for i in range(high):
if i + zero <= high:
dp[i + zero] = (dp[i + zero] + dp[i]) % MOD
if i + one <= high:
dp[i + one] = (dp[i + one] + dp[i]) % MOD
ans = 0
for i in range(low, high + 1):
ans = (ans + dp[i]) % MOD
return ans
7.5
猜数字大小 II
经典的区间 dp 问题,考虑博弈的性质即可,我们设数字范围在 \([i,j]\) 时的最小确保获胜的现金数,那么根据博弈的最大最小性质,更新:
枚举长度、起点、分割点即可,时间复杂度 \(\mathcal O(n^3)\):
class Solution(object):
def getMoneyAmount(self, n):
"""
:type n: int
:rtype: int
"""
dp = [[998244353 for i in range(n + 1)] for j in range(n + 1)]
for i in range(1, n + 1):
dp[i][i] = 0
if i < n:
dp[i][i + 1] = i
for l in range(3, n + 1):
for i in range(1, n - l + 2):
j = l + i - 1
for k in range(i + 1, j):
dp[i][j] = min(dp[i][j], max(dp[i][k - 1], dp[k + 1][j]) + k)
return dp[1][n]
7.6
找出和为指定值的下标对
有点意思,std 是哈希,但是感觉不想写随机化算法。
那可以想象一个 log 的数据结构,观察到 \(nums1\) 的长度很小,在查询和为 \(tot\) 的数对数时,可以枚举 \(nums1\) 中的数字,在 \(nums2\) 中查找,维护一个支持 \(\mathcal O(\log m)\) 查询的数据结构,查找 \(tot-nums1[i]\) 的存在性,可以使用二叉搜索树。而修改时我们可以认为是删除原数字,再加上新数字。不过还是要维护树的平衡,写平衡树有点不现实了。
想一种丐版的方法,查询时使用二分查找,我们只需要维护一个有序的 \(nums2\) 复制版即可,再 \(add\) 时,二分查找到删除的位置和插入的位置,最大的问题在于需要插入和删除,这个不使用树结构的要求下,还需要可以二分,只能相信 Python 内置的 Insert 和 pop 的常数了,时间复杂度是 \(\mathcal O(nq\log m+mq)\),需要一些优化和卡常,终于还是过了:
class FindSumPairs(object):
def __init__(self, nums1, nums2):
"""
:type nums1: List[int]
:type nums2: List[int]
"""
self.nums1, self.nums2 = nums1, nums2
self.nums1.sort()
self.n, self.m = len(nums1), len(nums2)
p = [[i, nums2[i]] for i in range(self.m)]
p = sorted(p, key=lambda x: x[1])
self.m_ = []
for i in range(self.m):
self.m_.append(p[i][1])
return
def add(self, index, val):
"""
:type index: int
:type val: int
:rtype: None
"""
old_val = self.nums2[index]
self.nums2[index] += val
l, r = 1, self.m
mid = (l + r) >> 1
while l < r:
if self.m_[mid - 1] == old_val:
break
elif self.m_[mid - 1] < old_val:
l = mid + 1
else:
r = mid
mid = (l + r) >> 1
self.m_.pop(mid - 1)
if self.m == 1:
self.m_.append(self.nums2[index])
else:
new_val = self.nums2[index]
l, r = mid, self.m - 1
mid = (l + r) >> 1
while l < r:
if self.m_[mid - 1] >= new_val:
r = mid
else:
l = mid + 1
mid = (l + r) >> 1
if self.m_[mid - 1] < new_val:
self.m_.insert(mid, new_val)
# self.m_.append(new_val)
else:
self.m_.insert(mid - 1, new_val)
return val
def count(self, tot):
"""
:type tot: int
:rtype: int
"""
ans = 0
lef = 1
lst_ans = 0
# print(self.m_, self.nums1, tot)
for i in range(self.n - 1, -1, -1):
if i < self.n - 1:
if self.nums1[i] == self.nums1[i + 1]:
ans += lst_ans
continue
if lef > self.m:
break
if self.nums1[i] + self.m_[-1] < tot:
break
lst_ans = 0
a = tot - self.nums1[i]
l, r = lef, self.m
mid = (l + r) >> 1
while l < r:
if self.m_[mid - 1] >= a:
r = mid
else:
l = mid + 1
mid = (l + r) >> 1
lef = mid
# print(self.nums1[i], a, mid, self.m_[mid - 1])
if self.m_[lef - 1] != a:
continue
elif self.m_[lef - 1] == a:
lef += 1
lst_ans += 1
if lef <= self.m:
if self.m_[lef - 1] != a:
ans += lst_ans
continue
else:
l, r = lef, self.m
# print(a, l)
mid = (l + r) >> 1
while l < r:
if self.m_[mid - 1] == a:
l = mid + 1
else:
r = mid
mid = (l + r) >> 1
if self.m_[mid - 1] > a:
mid = mid - 1
lst_ans += (mid - lef + 1)
lef = mid + 1
ans += lst_ans
return ans
7.7
最多可以参加的会议数目
很经典的贪心问题,就是按结束时间排序,之后如何处理我觉得还是有点思维难度的,我们可以考虑先做最晚结束的那个任务,如果有好多任务的结束时间相同呢?那当然是选择开始的晚的那个,后面的那些相当于结束时间减了 \(1\),在和相同结束时间的比最晚开始的,这是一个动态的过程,每次都要找最大的结束时间,可以用堆维护,枚举时间,每个时间选择大根堆堆顶,如果还在开始时间之后,那么就用,否则继续 pop 直到堆为空或者出现匹配的。在枚举时间的时候用指针维护入堆到了哪里就行了,不需要全部扫描一遍,这个过程每个元素都出入堆一次,时间复杂度是 \(\mathcal O(n\log n)\):
from typing import List
class Solution:
def maxEvents(self, events: List[List[int]]) -> int:
from queue import PriorityQueue
events = sorted(events, key=lambda x: x[1])
n = len(events)
m = events[-1][1]
pq = PriorityQueue()
r = n - 1
ans = 0
for i in range(m, -1, -1):
while True:
if r < 0:
break
# print(events[r][1], r, i)
if events[r][1] == i:
pq.put((-events[r][0], r))
r -= 1
else:
break
if pq.empty() == True:
continue
while not pq.empty():
u, id = pq.get()
u = -u
if u <= i:
ans += 1
break
return ans
7.8
最多可以参加的会议数目 II
每日一题质量真的高,不得不说啊。
一样的设置,我们仍然考虑 dp,先按照结束时间排序,设 \(dp_{i,l}\) 是前 \(i\) 个任务中,完成恰好 \(l\) 个最多获得的价值,那么转移来源:如果做第 \(i\) 个任务,\(dp_{i,l}=\max_{1\leq j\leq J }dp_{j,l-1}+v_i\),其中 \(J\) 是能满足在第 \(i\) 个会议结束开始之前结束的最后一个会议,为了获得这个我们可以做二分查找;如果选择不做第 \(i\) 个任务,\(dp_{i,l}=dp_{i-1,l}\)。这样复杂度是 \(\mathcal O(n\log n+nk\log n)\)。观察到 \(J\) 取值和 \(l\) 无关,可以优化掉 \(\log\) 系数,变成 \(\mathcal O(n\log n+nk)\)。更深入地,我们考虑先对所有断点(包括起点和终点)排序,记录现在最大的终点,扫到起点就可以确定这个对应的 \(J\),提前与处理好,甚至不需要二分,时间复杂度 \(\mathcal O(n\log n+nk)\),这边采用的就是这种实现方式。做的有点麻烦,第二种情况是用的 \(dp_{i,l}=\max_{1\leq j\leq i-1} dp_{j,l}\) 更新,也能过,但是有点多余了,实际上想想是不需要的,都是直接继承了:
from typing import List
class Solution:
def maxValue(self, events: List[List[int]], k: int) -> int:
n = len(events)
ev = [[events[i][0], events[i][1], events[i][2], i] for i in range(n)]
dp = [[0 for l in range(k + 1)] for i in range(n + 1)]
ev.sort(key=lambda x: (x[1], x[3]))
r = []
for i in range(n):
r.append([ev[i][0], 0, ev[i][3], i])
r.append([ev[i][1], 1, ev[i][3], i])
r.sort(key=lambda x: (x[0], x[1], x[2]))
so = [-1 for i in range(n)]
lst_r = -1
for i in range(2 * n):
if r[i][1] == 1:
lst_r = r[i][3]
else:
so[r[i][3]] = lst_r
max_f = [0 for i in range(k + 1)]
for i in range(1, n + 1):
for l in range(1, min(i + 1, k + 1)):
dp[i][l] = max(dp[i][l], max_f[l])
if l == 1:
# print()
dp[i][l] = max(dp[i][l], ev[i - 1][2])
else:
if so[i - 1] >= 0:
dp[i][l] = max(dp[i][l], dp[so[i - 1] + 1][l - 1] + ev[i - 1][2])
max_f[l] = max(max_f[l], dp[i][l])
ans = 0
for i in range(k + 1):
ans = max(ans, max_f[i])
return ans
7.9
最多 K 次交换相邻数位后得到的最小整数
很有意思的题目,我们考虑贪心,尽可能的减小第一位,除非比当前值小的都不够步数,然后接着第二位,第三位,...。 这个过程我们可以给 \(0\) 到 \(9\) 分别维护一个列表,找最靠前的。但是如果存在一个后面的移到前面的,前面的这些数下标都会加 \(1\),这个过程看起来很那维护。不过可以考虑逆向维护,因为每个数字的列表都是有序的,我们可以对每个数字的列表维护一个对应的 offset 表,用于说明哪些位置前面的数下标要额外加一次,这样的话,记录总 offset 数,如果前面的用了,把比这个数位置靠前的 offset 都拿掉,同步更新这个数字的总 offset 就行了。比较遗憾的是 offset 插入并不是一定有序的,所以我们只能用优先队列维护这个 offset 表,好在每个数字最多当一次 offset,不存在多次出入队的操作,所以均摊复杂度是可以保证的,总的时间复杂度是 \(\mathcal O(n\log n)\):
class Solution:
def minInteger(self, num: str, k: int) -> str:
from queue import PriorityQueue
n = len(num)
rt = [[] for i in range(10)]
offset = [PriorityQueue() for i in range(10)]
# u = [-1 for i in range(10)]
head, sux = [0 for i in range(10)], [0 for i in range(10)]
now = 0
left_k = k
s = ""
for i in range(n):
rt[int(num[i])].append(i)
while True:
# print(s, now, left_k)
if now == n:
break
for i in range(10):
if head[i] == len(rt[i]):
continue
re = rt[i][head[i]] + sux[i] - now
# if now == 1:
# print(i, rt[i][head[i]], sux[i], re, left_k)
if re == 0:
s += str(i)
now += 1
head[i] += 1
if head[i] < len(rt[i]):
while not offset[i].empty():
u = offset[i].get()
if u <= rt[i][head[i]]:
sux[i] -= 1
else:
offset[i].put(u)
break
break
elif left_k >= re:
s += str(i)
now += 1
head[i] += 1
if head[i] < len(rt[i]):
while not offset[i].empty():
u = offset[i].get()
if u <= rt[i][head[i]]:
sux[i] -= 1
else:
offset[i].put(u)
break
left_k -= re
for j in range(10):
if head[j] < len(rt[j]):
if rt[j][head[j]] <= rt[i][head[i] - 1]:
offset[j].put(rt[i][head[i] - 1])
sux[j] += 1
# if left_k == 0:
# break
break
return s
重新安排会议得到最多空余时间 I
只需要认识到一个事实,就是这 \(k\) 次操作一定要操作连续的会议,才能取得尽可能大的空闲时间,而连续的 \(k\) 个会议都挪到尽可能的一侧即可,剩下的时间可以用前缀和计算,枚举所有 \(n-k+1\) 种可能即可,时间复杂度 \(\mathcal O(n)\):
from typing import List
class Solution:
def maxFreeTime(self, eventTime: int, k: int, startTime: List[int], endTime: List[int]) -> int:
start, end = [0], [0]
n = len(startTime)
for i in range(n):
start.append(startTime[i])
end.append(endTime[i])
start.append(eventTime)
end.append(eventTime)
sumt = [0]
for i in range(1, n + 2):
sumt.append(sumt[i - 1] + end[i] - start[i])
# print(sumt)
ans = 0
for i in range(n - k + 1):
lef = start[i + k + 1] - end[i] - (sumt[i + k] - sumt[i])
# print(start[i + k + 1] - end[i], sumt[i + k] - sumt[i], lef)
ans = max(ans, lef)
return ans
用邮票贴满网格图
反正不要求尽可能少的邮票来覆盖,我们就只用探讨存在性就行了,实际上所有解一定被所有可以放置有票的地方都放上有票这个方案覆盖,实际上最多的可能也只有 \((n-h+1)(m-w+1)\) 种可能,枚举即可,可行性判断实际上是一个二维前缀和为题,提前维护即可。而我们还需要关注的就是怎么打标签,直接每次找到可行的区域就对区域内的位置打一遍标签就是四次方的复杂度了,我们考虑到实际上只用在每一行最后一种覆盖都讨论完之后对这一行进行标签处理即可,而可行性是可以叠加的,维护一个可行性数组,表示当前行每一个位置有多少种覆盖方案,每次行加 \(1\) 时,减去不再覆盖当前行的方案即可(当然前 \(h\) 行不需要减),这样就动态维护起来了,时间复杂度为 \(\mathcal O(nm)\),一定要注意特判!被狠狠的坑了好几次!:
from typing import List
class Solution:
def possibleToStamp(self, grid: List[List[int]], stampHeight: int, stampWidth: int) -> bool:
n, m = len(grid), len(grid[0])
h, w = stampHeight, stampWidth
f = [[0 for j in range(m + 1)] for i in range(n + 1)]
allr = grid
flg = 0
for i in range(1, n + 1):
p = 0
for j in range(1, m + 1):
p += grid[i - 1][j - 1]
f[i][j] = f[i - 1][j] + p
if self.arcal(1, 1, n, m, f) == n * m:
return True
if h > n:
return False
if w > m:
return False
av = [[0 for i in range(m + 1)] for j in range(n + 1)]
sum_av = [0 for i in range(m + 1)]
for i in range(1, n - h + 2):
l = 0
for j in range(1, m - w + 2):
now_val = self.arcal(i, j, i + h - 1, j + w - 1, f)
if now_val == 0:
for k in range(max(j, l + 1), j + w):
av[i][k] = 1
l = j + w - 1
else:
continue
if i > h:
for j in range(1, m + 1):
sum_av[j] = sum_av[j] - av[i - h][j]
# print(i, sum_av)
for j in range(1, m + 1):
sum_av[j] = sum_av[j] + av[i][j]
if allr[i - 1][j - 1] == 1:
continue
else:
if sum_av[j] > 0:
allr[i - 1][j - 1] = 2
else:
flg = 1
break
# return False
if flg == 1:
break
if flg == 1:
return False
for i in range(n - h + 2, n + 1):
if i > h:
for j in range(1, m + 1):
sum_av[j] = sum_av[j] - av[i - h][j]
for j in range(1, m + 1):
if allr[i - 1][j - 1] == 1:
continue
else:
if sum_av[j] > 0:
allr[i - 1][j - 1] = 2
else:
flg = 1
break
if flg == 1:
break
if flg == 1:
return False
else:
return True
def arcal(self, x1, y1, x2, y2, f):
return f[x2][y2] - f[x1 - 1][y2] - f[x2][y1 - 1] + f[x1 - 1][y1 - 1]
花园的最大总美丽值
继续做贪心,实际上这个问题看到数据范围就懂了,可以枚举一下没有到达完善的花园的花的树木的最小值,然后计算一下把小的改到这个阈值需要多少株花,需要操作的花园个数我们排完序后是可以发现单调性的,就可以线性地做。然后后面剩下的花的余额能够实现多少花园到完善的程度可能没有单调性了,所以需要二分一下,这个二分比较麻烦,注意写的时候分类的精细性,时间复杂度是 \(\mathcal O((n+t)\log n)\):
from typing import List
class Solution:
def maximumBeauty(self, flowers: List[int], newFlowers: int, target: int, full: int, partial: int) -> int:
n = len(flowers)
flowers.sort()
f = [0 for i in range(n + 1)]
if flowers[0] >= target:
return n * full
yp = -1
for i in range(1, n + 1):
f[i] = f[i - 1] + flowers[i - 1]
if (flowers[i - 1] >= target) & (yp < 0):
yp = i - 2
if yp < 0:
yp = n - 1
minx = flowers[0]
l = 0
ans = 0
for i in range(minx, target + 1):
while True:
if l >= n:
break
if flowers[l] <= i:
l += 1
else:
break
res = i * l - f[l]
if i == target:
if newFlowers >= res:
ans = max(ans, n * full)
continue
if newFlowers < res:
break
else:
loc = self.find(yp, l - 1, newFlowers - res, i, f, target)
ans = max(ans, (n - loc) * full + i * partial)
return ans
def find(self, R, L, x, minv, f, target):
l, r = 1, R + 1
mid = (l + r) >> 1
while l < r:
if self.cal(mid, R, L, minv, f, target) > x:
l = mid + 1
else:
r = mid
mid = (l + r) >> 1
return mid
def cal(self, p, R, L, minv, f, target):
if p == R + 1:
return 0
elif p > L:
return target * (R - p + 1) - (f[R + 1] - f[p])
else:
return (target - minv) * (L - p + 1) + target * (R - L) - (f[R + 1] - f[L + 1])
有序队列
发现 \(k=1\) 时是很容易理解的 \(k\) 种情况,由于 \(n\leq 10^3\) 直接枚举判断即可,就算有更好的方法我也不太像考虑了/wul。\(k=2\) 时发现可以通过将两个相邻的位置推到最前面来先取第二个,后取第一个实现相邻数的交换,进而就能实现任意位置的交换,以实现任意排列,当然 \(k\geq 2\) 时只需要操作前两个数就可以实现任意排列了,更是简单,所以目标就是任意排列下的最小字典序,我们排个序就行了,这里练了一下 python 中堆的 API,使用了堆排序,时间复杂度 \(\mathcal O(n^2)\):
class Solution:
def orderlyQueue(self, s: str, k: int) -> str:
from queue import PriorityQueue
ab = "abcdefghijklmnopqrstuvwxyz"
self.d = dict([(ab[i], i) for i in range(26)])
n = len(s)
if k == 1:
mins = s
for i in range(1, n):
now = s[i: ] + s[: i]
# print(now)
op = self.judge(now, mins)
if op == -1:
mins = now
return mins
else:
qp = PriorityQueue()
for i in range(n):
qp.put(self.d[s[i]])
ans = ""
while not qp.empty():
u = qp.get()
ans = ans + ab[u]
return ans
def judge(self, a, b):
n = len(a)
for i in range(n):
if self.d[a[i]] < self.d[b[i]]:
return -1
elif self.d[a[i]] == self.d[b[i]]:
continue
else:
return 1
return 0
我能赢吗
练习一下对手搜索,然后可以状压,这个是需要发现的,否则就会超时,状压的复杂度是 \(\mathcal O(2^n)\),不状压的复杂度大概是 \(\mathcal O(n^{\sqrt{tot}})\),一定是过不了的,注意特判,一个是 \(tot\) 超出可行的总和一定没有必胜解,另一个是 \(tot\) 是 \(0\) 一定直接获胜:
class Solution:
def canIWin(self, maxChoosableInteger: int, desiredTotal: int) -> bool:
n = maxChoosableInteger
tot = desiredTotal
if tot > (n + 1) * n // 2:
return False
if tot == 0:
return True
self.vis = [0 for i in range(n + 1)]
self.dp = [-1 for i in range((1 << n))]
op = self.dfs(n, tot, 1)
return bool(op)
def dfs(self, n, tot, ty):
x = self.enc(n)
if self.dp[x] >= 0:
return self.dp[x]
if tot <= 0:
self.dp[x] = 0
return 0
op = 1
for i in range(1, n + 1):
if self.vis[i] == 0:
self.vis[i] = 1
op = op and self.dfs(n, tot - i, -ty)
self.vis[i] = 0
if not op:
self.dp[x] = 1
return 1
if op:
self.dp[x] = 0
return 0
def enc(self, n):
x = 0
for i in range(1, n + 1):
if self.vis[i]:
x = x | (1 << (i - 1))
return x
7.10
重新安排会议得到最多空余时间 II
这种题应该一遍过的呀。。。
我们发现可以随便放置一个会议的话,我们可以枚举这个修改啊时间的会议,分情况:如果其他空余位置有能力放下这个会议,那么得到这种情况下的最优解就是这个会议前后两段空闲;否则,与昨天的每日一题一样,将这个会议往左或往右推到一侧,剩下的空间就是最大空闲时间。注意判断最大的空闲是不是自己所在的两边的空闲,实际上我们发现只需要前三大的空闲就可以了,精细处理可以做到 \(\mathcal O(n)\),不过我懒了,直接排了序,复杂度 \(\mathcal O(n\log n)\):
from typing import List
class Solution:
def maxFreeTime(self, eventTime: int, startTime: List[int], endTime: List[int]) -> int:
n = len(startTime)
start, end = [0], [0]
for i in range(n):
start.append(startTime[i])
end.append(endTime[i])
start.append(eventTime)
end.append(eventTime)
em = []
for i in range(n + 1):
em.append([start[i + 1] - end[i], i])
em.sort(key=lambda x: x[0], reverse=True)
ans = em[0][0]
for i in range(1, n + 1):
rt = 0
for j in range(3):
if (em[j][1] == i - 1) | (em[j][1] == i):
continue
else:
rt = j
break
if em[rt][0] >= end[i] - start[i]:
ans = max(ans, start[i + 1] - end[i - 1])
else:
ans = max(ans, start[i + 1] - end[i - 1] - end[i] + start[i])
return ans
统计逆序对的数目
一开始感觉很吓人,但是发现这个约束条件其实挺紧的,限制了整个序列的逆序对不会太多,并且提供了整个序列的逆序对数,这样的话我们可以从后往前推,假设 \(dp_i\) 是当前时刻下,有 \(i\) 个逆序对的序列种数,有限制的位置只能更新一个值,其他位置则是要更新前后限制之间的值域,相邻的位置之间,逆序对数的差值唯一确定了后面那个数的相对大小,我们可以用滚动数组进行优化,时间复杂度 \(\mathcal O(n^2t)\),注意滚动数组的清空问题:
from typing import List
class Solution:
def numberOfPermutations(self, n: int, requirements: List[List[int]]) -> int:
m = len(requirements)
cnt = [-1 for i in range(n)]
mod = 1000000007
for i in range(m):
cnt[requirements[i][0]] = requirements[i][1]
minc = [0 for i in range(n)]
lst = 0
flg = 0
for i in range(n):
if cnt[i] >= 0:
if lst <= cnt[i]:
lst = cnt[i]
else:
flg = 1
break
minc[i] = lst
if flg:
return 0
maxn = cnt[n - 1]
dp = [[0 for i in range(2)] for j in range(maxn + 1)]
lst = maxn
dp[maxn][(n - 1) % 2] = 1
for i in range(n - 2, -1 , -1):
if cnt[i] >= 0:
lst = cnt[i]
for j in range(maxn + 1):
dp[j][i % 2] = 0
for j in range(minc[i], lst + 1):
for k in range(0, i + 2):
if j + k > maxn:
break
dp[j][i % 2] = (dp[j][i % 2] + dp[j + k][(i + 1) % 2]) % mod
return dp[0][0]
7.11
无需开会的工作日
维护一个栈,按照大小放入左右端点,左端点代表入栈,右端点代表出栈,如果一个左端点入栈前栈是空的,那么说明只到上一个右端点之前都是空闲的,只需要维护一下现在扫到的最大值即可(实际不需要,就是下标减 \(1\)),栈实际上不需要显式维护,维护一个 \(top\) 代表栈顶指针的位置即可代表栈现在是否为空,时间瓶颈在于排序,解决问题时的复杂度是线性的,总复杂度\(\mathcal O(n\log n)\):
from typing import List
class Solution:
def countDays(self, days: int, meetings: List[List[int]]) -> int:
n = len(meetings)
s = []
for i in range(n):
s.append([meetings[i][0], 0])
s.append([meetings[i][1], 1])
s.append([days + 1, 0])
top = 0
ans = 0
s.sort(key=lambda x: x[0])
lst = 0
for i in range(2 * n + 1):
if s[i][1] == 0:
if top == 0:
if s[i][0] - lst - 1 > 0:
ans += (s[i][0] - lst - 1)
top += 1
else:
top -= 1
lst = s[i][0]
return ans
找两个和为目标值且不重叠的子数组
边界条件!边界条件!边界条件!
先统计前缀和,前后缀分别统计是否有正好等于 \(target\) 的子数组,然后前后缀分别计算最小值,代表这个点之前/之后最短的和为 \(target\) 的子数组长度,两个不重叠的区间可以枚举间断点,左边用前缀、右边用后缀,不同情况前后相加取 \(\max\) 即可,时间复杂度 \(\mathcal O(n)\),因为前后缀计算时可以使用双指针的方式,这个地方一定要注意边界条件啊!
from typing import List
class Solution:
def minSumOfLengths(self, arr: List[int], target: int) -> int:
n = len(arr)
f = [0 for i in range(n + 1)]
for i in range(1, n + 1):
f[i] = f[i - 1] + arr[i - 1]
pre = [1919810 for i in range(n + 1)]
suf = [1919810 for i in range(n + 1)]
l = 0
for i in range(1, n + 1):
flg = 0
while True:
if f[i] - f[l] > target:
l += 1
if l > i:
break
elif f[i] - f[l] < target:
break
else:
flg = 1
break
if flg:
pre[i] = i - l
r = n
for i in range(n, 0, -1):
flg = 0
while True:
if f[r] - f[i - 1] > target:
r -= 1
if r < i:
break
elif f[r] - f[i - 1] < target:
break
else:
flg = 1
break
if flg:
suf[i] = r - i + 1
min_pre = [1919810 for i in range(n + 1)]
min_suf = [1919810 for i in range(n + 1)]
min_suf[n] = suf[n]
for i in range(1, n + 1):
min_pre[i] = min(min_pre[i - 1], pre[i])
for i in range(n - 1, 0, -1):
min_suf[i] = min(min_suf[i + 1], suf[i])
ans = 1919810
for i in range(1, n):
ans = min(ans, min_pre[i] + min_suf[i + 1])
if ans > n:
return -1
else:
return ans
7.12
最佳运动员的比拼回合
看到 \(n\) 值比较小,可以直接考虑搜索,不过容易发现是可以记忆化的,具体来说,\(dp_{i,j,k}\) 为第 \(i\) 个回合,两个最佳运动员分别在第 \(j\) 和第 \(k\) 个位置时(这里只考虑序数,不考虑实际编号),最大和最小的两个运动员相遇的回合数。我们发现通过中心对称性处理,可以将现在的 \(j\) 和 \(k\) 分布简化为两种情况:1. 都在左边;2. 一左一右,并且左边更靠近端点。两个都可以枚举 \(\mathcal O(n^2)\) 种下一状态,虽然不同运动员之间的胜负关系是 \(\mathcal O(2^{\frac{n}{2}})\) 种可能,但是很多情况对下一回合中两个最优运动员位置的作用是一致的,这样递归 \(\mathcal O(\log n)\) 层即可,记忆化一下,还是注意边界情况的处理是否正确,得到时间复杂度为 \(\mathcal O(n^4\log n)\):
from typing import List
class Solution:
def earliestAndLatest(self, n: int, firstPlayer: int, secondPlayer: int) -> List[int]:
self.n = n
self.f, self.s = firstPlayer, secondPlayer
self.leng = [n]
now_n = n
while True:
if now_n == 2:
break
if now_n % 2 == 1:
now_n = (now_n + 1) // 2
self.leng.append(now_n)
else:
now_n = now_n // 2
self.leng.append(now_n)
self.depth = len(self.leng)
self.dp_max = [[[0 for k in range(n)]for j in range(n)]for i in range(self.depth)]
self.dp_min = [[[n for k in range(n)] for j in range(n)] for i in range(self.depth)]
ans_min, ans_max = self.dfs(0, self.f, self.s)
return [ans_min, ans_max]
def nor(self, l, r, n):
mid = (1 + n) / 2
if r <= mid:
return (l, r, 0)
elif l >= mid:
return ((n + 1 - r), (n + 1 - l), 0)
else:
if l <= (n + 1 - r):
return (l, r, 1)
else:
return ((n + 1 - r), (n + 1 - l), 1)
def dfs(self, i, lq, rq):
if lq + rq == self.leng[i] + 1:
return 1, 1
l, r, ty = self.nor(lq, rq, self.leng[i])
if self.dp_min[i][l][r] < self.n:
return (self.dp_min[i][l][r], self.dp_max[i][l][r])
now_min, now_max = self.n, 0
d = r - l
if ty == 0:
for a in range(1, l + 1):
for b in range(a + 1, a + d + 1):
rt1, rt2 = self.dfs(i + 1, a, b)
now_min = min(now_min, rt1)
now_max = max(now_max, rt2)
else:
R = self.leng[i] + 1 - r
for a in range(1, l + 1):
for b in range(1, R - l + 1):
new_l = a
new_r = self.leng[i + 1] - (l - a + b) + 1
rt1, rt2 = self.dfs(i + 1, new_l, new_r)
now_min = min(now_min, rt1)
now_max = max(now_max, rt2)
self.dp_min[i][l][r], self.dp_max[i][l][r] = now_min + 1, now_max + 1
return now_min + 1, now_max + 1
再长博客园编辑器卡的一批,再开一个新的文章:刷刷 Leetcode(2)!

浙公网安备 33010602011771号