刷刷 Leetcode(3)
接上篇:刷刷 Leetcode(2)
7.20
根据质数下标分割数组
昨天的比赛题,今天稍微总结一下。
这个题就是线性筛就可以了,时间复杂度 \(\mathcal O(n)\):
from typing import List
from queue import PriorityQueue
class Solution:
def splitArray(self, nums: List[int]) -> int:
n = len(nums)
p = [0 for i in range(n)]
ps = []
if n == 1:
return abs(nums[0])
elif n == 2:
return abs(nums[0] + nums[1])
sa = nums[0] + nums[1]
sb = 0
for i in range(2, n):
if p[i] == 0:
sb += nums[i]
ps.append(i)
else:
sa += nums[i]
m = len(ps)
for pp in ps:
if i * pp >= n:
break
p[i * pp] = 1
if i % pp == 0:
break
return abs(sa - sb)
总价值可以被 K 整除的岛屿数目
我用的并查集,按照 \(i*n +j\) 给每个位置编号,从左上往右下扫描,如果左边或者上边是陆地,那么就对应的合并即可,合并完之后把根节点记录一下,作为岛屿的编号,然后扫一遍加到对应的岛屿总价值上即可,时间复杂度 \(\mathcal O(nm\alpha(nm))\)。实际上配合 vis 标签进行 BFS 可以做到 \(\mathcal O(nm)\):
from typing import List
from queue import PriorityQueue
class Solution:
def countIslands(self, grid: List[List[int]], k: int) -> int:
m, n = len(grid), len(grid[0])
self.fa = [i for i in range(m * n)]
self.ss = [0 for i in range(m * n)]
for i in range(m):
for j in range(n):
if grid[i][j] == 0:
continue
else:
u = i * n + j
if i > 0:
if grid[i - 1][j] > 0:
self.merge(u, u - n)
if j > 0:
if grid[i][j - 1] > 0:
self.merge(u, u - 1)
for i in range(m):
for j in range(n):
if grid[i][j] == 0:
continue
else:
u = i * n + j
f = self.getf(u)
self.ss[f] += grid[i][j]
ans = 0
for i in range(n * m):
if self.ss[i] > 0:
if self.ss[i] % k == 0:
ans += 1
return ans
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, t2 = self.getf(u), self.getf(v)
if t1 != t2:
self.fa[t2] = t1
位计数深度为 K 的整数数目 I
先做的第四题,事实证明是个正确的决定。发现所有数进行一次操作后都会变成最大 \(\left\lceil \log n\right\rceil\),我们可以先处理 \(\left [0, \left\lceil \log n\right\rceil\right]\) 之间数的 popcount-depth,然后如果 \(i\) 的 popcount-depth 为 \(k-1\),我们只需要统计 \([1, n]\) 之间 popcount 为 \(i\) 的数字个数即可,这个可以 dp 来做,但是我们只能 dp 最多 \(n\) 位,有 \(k\) 个二进制 \(1\) 的数字个数 \(dp_{ik}\),而实际上 \(n\) 基本都不是规整的正好是 \(2^{m}-1\) 的,所以我们要按位计算,比如 \(n\) 的二进制是 \(10100\),要寻找不大于 \(n\) 的数中,有多少个数有 \(2\) 个 \(1\),那么最终的答案应该是 \(dp_{4, 2}+dp_{2,1}\),后面这个第二维减 \(1\) 是因为第 \(5\) 位占了一个,我们第二项实际上是 \(10000\sim 10011\) 中满足要求的个数,时间复杂度好难算,不过大概是一个 \(\mathcal O(\log^2 n+k\log n)\) 的:
from typing import List
from queue import PriorityQueue
from math import log
class Solution:
def popcountDepth(self, n: int, k: int) -> int:
if k == 0:
return 1
self.m = 55
d = [0 for i in range(55)]
ans = 0
po = []
for i in range(1, 55):
d[i] = self.solv(i)
if d[i] == k - 1:
po.append(i)
dp = [[0 for i in range(self.m + 1)] for j in range(self.m + 1)]
dp[1][0] = 1
dp[1][1] = 1
for i in range(self.m + 1):
dp[i][0] = 1
for i in range(2, self.m + 1):
for j in range(1, i + 1):
dp[i][j] = dp[i - 1][j] + dp[i - 1][j - 1]
cc = []
for i in range(self.m + 1):
if (1 << i) > n:
break
if (1 << i) & n:
cc.append(1)
else:
cc.append(0)
l = len(cc)
for p in po:
now = p
for i in range(l - 1, -1, -1):
if cc[i] == 1:
ans += dp[i][now]
now -= 1
if now == 0:
ans += 1
break
if k == 1:
ans -= 1
return ans
def solv(self, x):
ans = 0
now = x
while True:
if now == 1:
return ans
else:
cnt = 0
for i in range(self.m + 1):
if now & (1 << i):
cnt += 1
if (1 << i) > now:
break
now = cnt
ans += 1
恢复网络路径
这个题纯属犯傻了,比赛没做出来还喜提罚时 \(15\text{min}\) T_T
首先是要二分答案,那么怎么判断解的可行性呢。实际上我们可以先跑一遍拓扑排序,正好可以顺便计算出 \(0\) 到 \(n-1\) 的最短路,如果大于 \(k\),直接返回 \(-1\)。然后如果要求最大的最小边权为(至少) \(x\),其实言外之意就是存在一条路径,所有边权都不小于 \(x\),然后总长度不大于 \(k\),其实我们只需要在开始特判拓扑排序的时候记录出拓扑序,然后按照拓扑序枚举点及其出边(不用再入队了,如果还是入度-入队方法的话,没有预处理拓扑序操作起来还是比较棘手的),如果边长大于等于 \(x\),就尝试用当前边松弛,得到限定下的最短路,最后如果到 \(n-1\) 的最短路还是不大于 \(k\) 的,说明当前解可行,否则不可行。至于在线这个性质,离线的点所连的边我们直接不加边就行了。时间复杂度 \(\mathcal O((n+m)\log \max w)\):
from typing import List
from queue import PriorityQueue, Queue
from math import log
class Edge:
def __init__(self, to, nxt, w):
self.to = to
self.nxt = nxt
self.w = w
class Solution:
def findMaxPathScore(self, edges: List[List[int]], online: List[bool], k: int) -> int:
n = len(online)
self.n = n
self.e1 = []
self.cnt1 = 0
self.head1 = [-1 for i in range(n)]
self.k = k
self.ind = [0 for i in range(n)]
maxw = -1
for ed in edges:
u, v, w = ed[0], ed[1], ed[2]
if (not online[u]) or (not online[v]):
continue
else:
self.add(u, v, w)
self.ind[v] += 1
maxw = max(maxw, w)
self.dist = [int(1e16) for i in range(n)]
self.dist[0] = 0
self.tpidx = []
q = Queue()
for i in range(self.n - 1):
if self.ind[i] == 0:
q.put(i)
while not q.empty():
u = q.get()
self.tpidx.append(u)
i = self.head1[u]
while True:
if i == -1:
break
v, nxt, w = self.e1[i].to, self.e1[i].nxt, self.e1[i].w
self.ind[v] -= 1
if self.ind[v] == 0:
q.put(v)
self.dist[v] = min(self.dist[v], self.dist[u] + w)
i = nxt
if self.dist[n - 1] > k:
return -1
l, r = 0, maxw
mid = (l + r + 1) >> 1
while l < r:
if self.judge(mid):
l = mid
else:
r = mid - 1
mid = (l + r + 1) >> 1
return mid
def judge(self, x):
d = [int(1e16) for i in range(self.n)]
d[0] = 0
for u in self.tpidx:
i = self.head1[u]
while i >= 0:
v, nxt, w = self.e1[i].to, self.e1[i].nxt, self.e1[i].w
if w >= x:
d[v] = min(d[v], d[u] + w)
i = nxt
if d[self.n - 1] > self.k:
return 0
else:
return 1
def add(self, u, v, w):
e = Edge(v, self.head1[u], w)
self.e1.append(e)
self.head1[u] = self.cnt1
self.cnt1 += 1
统计梯形的数目 II
怎么感觉越接近考试状态越烂呢。。。
首先可以想到保存斜率,然后我们需要注意到截距斜率如果都一样,那么是不能构成梯形的,那么就利用排序后扫描的方式,记录相同斜率、不同截距的点对数即可...吗?并不完全是,我是傻子,因为有时候如果是平行四边形,这个题目里也算梯形,但是它有两对平行边,我们就算了两变,有重复。那怎么办,总不能枚举四个节点,然后判断吧,那样就超时了。考虑到平行四边形的性质是对角线互相平分,那么可以记录所有点对的中点,如果一样的话,就可以构成平行四边形...吗?并不完全是,我们在统计平行线对时,没有计入截距相等的线对,但是记录中点的方法,也可能遇到这种情况,实际上我们只需要控制中点重合的基础上,斜率不同就行了,这个问题的结构和前面那部分一样,我们干脆一下子把斜率、截距、中点都记录下来,不同键值排序两次即可。还有一些细节,一是斜率无穷大的处理,我们可以记录所有有限斜率的最大值,\(+1\) 赋给无穷大斜率的线段。还有就是算截距的时候不用用斜率来算,要完全用坐标表示,否则浮点精度会 GG,时间复杂度 \(\mathcal O(n^2\log n)\):
from typing import List
class Solution:
def countTrapezoids(self, points: List[List[int]]) -> int:
n = len(points)
lines = []
d = []
maxk = -1e16
for i in range(n - 1):
for j in range(i + 1, n):
a, b = points[i], points[j]
if a[0] == b[0]:
d.append([i, j])
else:
k = (a[1] - b[1]) / (a[0] - b[0])
B = (a[0] * b[1] - b[0] * a[1]) / (a[0] - b[0])
maxk = max(maxk, k)
lines.append([k, B, (a[0] + b[0]), (a[1] + b[1]), i, j])
for p in d:
i, j = p[0], p[1]
lines.append([maxk + 1, points[i][0], (points[i][0] + points[j][0]), points[i][1] + points[j][1], i, j])
lines.sort()
ans = 0
lst_k = maxk + 114514
lst_b = -1e16
m = len(lines)
a = []
now_len = 0
b_len = 0
for i in range(m - 1, -1, -1):
k, b, _, __, I, J = lines[i][0], lines[i][1], lines[i][2], lines[i][3], lines[i][4], lines[i][5]
if k != lst_k:
if i == m - 1:
lst_k = k
lst_b = b
b_len = 1
now_len = 1
continue
a.append(b_len)
for x in a:
ans += 0.5 * x * (now_len - x)
b_len = 1
now_len = 1
a = []
lst_k = k
lst_b = b
else:
now_len += 1
if b != lst_b:
a.append(b_len)
b_len = 1
lst_b = b
else:
b_len += 1
a.append(b_len)
for x in a:
ans += 0.5 * x * (now_len - x)
lines = [[u[2], u[3], u[0], u[1], u[4], u[5]] for u in lines]
lines.sort()
lst_x, lst_y = -114514, 114514
lst_k = maxk + 2
a = []
now_len = 0
k_len = 0
for i in range(m - 1, -1, -1):
t, r, k, b, I, J = lines[i][0], lines[i][1], lines[i][2], lines[i][3], lines[i][4], lines[i][5]
if [t, r] != [lst_x, lst_y]:
if i == m - 1:
lst_x, lst_y = t, r
lst_k = k
k_len = 1
now_len = 1
continue
a.append(k_len)
for x in a:
ans -= 0.5 * x * (now_len - x)
k_len = 1
now_len = 1
a = []
lst_x, lst_y = t, r
lst_k = k
else:
now_len += 1
if k != lst_k:
a.append(k_len)
k_len = 1
lst_k = k
else:
k_len += 1
a.append(k_len)
for x in a:
ans -= 0.5 * x * (now_len - x)
return int(ans)
7.21
删除字符使字符串变好
扫一遍即可,实时记录当前段的字母是啥和持续长度,时间复杂度 \(\mathcal O(n)\):
class Solution:
def makeFancyString(self, s: str) -> str:
n = len(s)
ans = ""
lst = "A"
now = 0
for i in range(n):
if lst != s[i]:
if now >= 2:
ans += lst * 2
else:
ans += lst * now
now = 1
lst = s[i]
else:
now += 1
if now >= 2:
ans += lst * 2
else:
ans += lst * now
return ans
到达目标点的最小移动次数
一如既往的降智,我一开始就是不经意写成了倒推,虽然没有意识到倒推的必要性。但是由于没有仔细观察分类,所以写成了字典(哈希)+ 记忆化的形式,虽然深度被保证了,但是可能因为常数问题过不了。后来看了题解,发现自己做对了,看了一眼分类条件,确实是四个互补的分类,这样的话,实际上每一层只有一种选择,即可以在 \(\mathcal O(\log \frac{tx+ty}{sx+sy})\) 的复杂度内完成递归(包含较多 nt 注释):
from collections import defaultdict
from queue import Queue
class Solution:
def minMoves(self, sx: int, sy: int, tx: int, ty: int) -> int:
if sx == tx and sy == ty:
return 0
if sx == 0 and sy == 0:
return -1
if (sx > tx) or (sy > ty):
return -1
# self.dx = defaultdict(int)
# self.dy = defaultdict(int)
# self.dx, self.dy = dict([]), dict([])
# self.cntx, self.cnty = 1, 1
# self.dx[sx] = self.cntx
# self.dy[sy] = self.cnty
self.tx, self.ty = tx, ty
self.sx, self.sy = sx, sy
# self.dp = [[-1 for i in range(2005)] for j in range(2005)]
# self.dp[1][1] = 0
ans = self.dfs(tx, ty)
if ans >= 7 + 10 ** 9:
return -1
else:
return ans
def dfs(self, x, y):
# a, b = self.dx[x], self.dy[y]
# if a == 0:
# self.cntx += 1
# self.dx[x] = self.cntx
# a = self.cntx
# if b == 0:
# self.cnty += 1
# self.dy[y] = self.cnty
# b = self.cnty
# if x not in self.dx:
# self.cntx += 1
# self.dx[x] = self.cntx
# if y not in self.dy:
# self.cnty += 1
# self.dy[y] = self.cnty
# a, b = self.dx[x], self.dy[y]
#
# if self.dp[a][b] >= 0:
# return self.dp[a][b]
if x == self.sx and y == self.sy:
return 0
now = 1e10
if (x % 2 == 0) and (x // 2 >= y) and (x // 2 >= self.sx):
# print(x, y, 1)
now = min(now, self.dfs(x // 2, y) + 1)
if (x - y >= self.sx) and (x <= 2 * y):
# print(x, y, 2)
now = min(now, self.dfs(x - y, y) + 1)
if (y % 2 == 0) and (y // 2 >= x) and (y // 2 >= self.sy):
# print(x, y, 3)
now = min(now, self.dfs(x, y // 2) + 1)
if (y - x >= self.sy) and (y <= 2 * x):
# print(x, y, 4)
now = min(now, self.dfs(x, y - x) + 1)
return now
唯一中间众数子序列 I
暴力统计即可,看到数据范围比较良心,我们考虑先做离散化(这里是用 python 字典做的),然后记录每个数字的计数前缀和。然后枚举中间数,如果它作为众数,有四种可能:出现了两次、三次、四次、五次。对于第一种情况,比较麻烦,我们统计了出现两次总类别数之后,还需要排除剩下的数不能重复出现的情况,而其余的可能性中,当前数一定是绝对数量占优的,所以不太用管。排除的时候就可以枚举重复出现的其他数字,减去其出现两次或三次的可能即可,可以容斥,也可以直接减。最麻烦的地方就在于这个题需要对数字出现的位置前后进行精确枚举,比较考验精细性,也不要漏考虑,时间复杂度 \(\mathcal O(n^2)\):
from typing import List
class Solution:
def subsequencesWithMiddleMode(self, nums: List[int]) -> int:
n = len(nums)
dic = dict([])
mod = (10 ** 9) + 7
self.cnt = 0
for i in range(n):
if nums[i] not in dic:
dic[nums[i]] = self.cnt
self.cnt += 1
a = []
for i in range(n):
a.append(dic[nums[i]])
m = self.cnt
f = [[0 for j in range(n)] for i in range(m)]
f[a[0]][0] = 1
for i in range(1, n):
for j in range(m):
f[j][i] = f[j][i - 1]
f[a[i]][i] += 1
ans = 0
for i in range(2, n - 2):
x = a[i]
lx, ly = i - f[x][i - 1], n - i - 1 - f[x][n - 1] + f[x][i]
if f[x][i - 1] >= 1:
now = f[x][i - 1] % mod
k = (ly * (ly - 1) // 2) % mod * lx % mod
for j in range(m):
if j == x:
continue
p = f[j][n - 1] - f[j][i]
if (f[j][i - 1] > 0) and (p > 0):
k = (k - f[j][i - 1] * p % mod * (ly - p) % mod + mod) % mod
if p > 1:
k = (k - (p * (p - 1) // 2) % mod * (lx - f[j][i - 1]) % mod + mod) % mod
if (f[j][i - 1] > 0) & (p > 1):
k = (k - (p * (p - 1) // 2) % mod * f[j][i - 1] % mod + mod) % mod
ans = (ans + (k * now) % mod) % mod
if f[x][n - 1] - f[x][i] >= 1:
now = (f[x][n - 1] - f[x][i]) % mod
k = (lx * (lx - 1) // 2) % mod * ly % mod
for j in range(m):
if j == x:
continue
p = f[j][i - 1]
if (f[j][n - 1] - f[j][i] > 0) and (p > 0):
k = (k - (f[j][n - 1] - f[j][i]) * p % mod * (lx - p) % mod + mod) % mod
if p > 1:
k = (k - (p * (p - 1) // 2) % mod * (ly - f[j][n - 1] + f[j][i]) % mod + mod) % mod
if (f[j][n - 1] - f[j][i] > 0) & (p > 1):
k = (k - (p * (p - 1) // 2) % mod * (f[j][n - 1] - f[j][i]) % mod + mod) % mod
ans = (ans + (k * now) % mod) % mod
if f[x][i - 1] >= 2:
now = ((f[x][i - 1]) * (f[x][i - 1] - 1) // 2) % mod
ans = (ans + (ly * (ly - 1) // 2) % mod * now % mod) % mod
if f[x][n - 1] - f[x][i] >= 2:
now = ((f[x][n - 1] - f[x][i]) * (f[x][n - 1] - f[x][i] - 1) // 2) % mod
ans = (ans + (lx * (lx - 1) // 2) % mod * now % mod) % mod
if (f[x][n - 1] - f[x][i] >= 1) and (f[x][i - 1] >= 1):
now = (f[x][n - 1] - f[x][i]) * f[x][i - 1] % mod
ans = (ans + lx * ly % mod * now % mod) % mod
if (f[x][i - 1] >= 2) and (f[x][n - 1] - f[x][i] >= 1):
now = ((f[x][i - 1]) * (f[x][i - 1] - 1) // 2) % mod * (f[x][n - 1] - f[x][i]) % mod
ans = (ans + now * ly % mod) % mod
if (f[x][n - 1] - f[x][i] >= 2) and (f[x][i - 1] >= 1):
now = ((f[x][n - 1] - f[x][i]) * (f[x][n - 1] - f[x][i] - 1) // 2) % mod * f[x][i - 1] % mod
ans = (ans + now * lx % mod) % mod
if (f[x][n - 1] - f[x][i] >= 2) and (f[x][i - 1] >= 2):
now = ((f[x][n - 1] - f[x][i]) * (f[x][n - 1] - f[x][i] - 1) // 2) % mod
now = ((f[x][i - 1]) * (f[x][i - 1] - 1) // 2) % mod * now % mod
ans = (ans + now) % mod
return ans
7.22
删除子数组的最大得分
看到数字大小比较小,所以可以统计每个数字上次出现的位置,进而可以得到每个数字下一个出现的位置。然后就可以直接每个所有子数组开始的位置了,可以贪心到最后一个满足要求的位置,具体来说就是后面的数要满足不重复的最左边的位置,以及自己下一个出现的左侧位置,这个就要求我们倒序扫描,然后更新后缀的最后一个满足不冲突的坐标即可,时间复杂度 \(\mathcal O(n+\max a)\):
from typing import List
class Solution:
def maximumUniqueSubarray(self, nums: List[int]) -> int:
n = len(nums)
maxn = max(nums)
lst = [-1 for i in range(maxn + 1)]
maxt = [n for i in range(n)]
for i in range(n):
if lst[nums[i]] >= 0:
maxt[lst[nums[i]]] = i
lst[nums[i]] = i
f = [0 for i in range(n)]
f[0] = nums[0]
for i in range(1, n):
f[i] = f[i - 1] + nums[i]
ans = 0
p = n
for i in range(n - 1, -1, -1):
if maxt[i] < p:
p = maxt[i]
if i > 0:
ans = max(ans, f[p - 1] - f[i - 1])
else:
ans = max(ans, f[p - 1])
return ans
找出字符串中第一个匹配项的下标
学习 KMP,记住两部分是一样的,只需要记住在匹配完之后也跳转即可(虽然这个题没有要求),时间复杂度 \(\mathcal O(n+m)\):
class Solution:
def strStr(self, haystack: str, needle: str) -> int:
n, m = len(haystack), len(needle)
nxt = [0 for i in range(m)]
j = 0
for i in range(1, m):
while (j > 0) and (needle[i] != needle[j]):
j = nxt[j - 1]
if needle[i] == needle[j]:
j += 1
nxt[i] = j
j = 0
for i in range(n):
while (j > 0) & (haystack[i] != needle[j]):
j = nxt[j - 1]
if haystack[i] == needle[j]:
j += 1
if j == m:
return i - j + 1
return -1
二叉搜索树迭代器
构建中序遍历数组即可,接下来的操作只需要在数组中操作即可,时间复杂度 \(\mathcal O(n+q)\):
from typing import Optional
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
class BSTIterator:
def __init__(self, root: Optional[TreeNode]):
self.vals = []
self.p = -1
self.dfs(root)
self.n = len(self.vals)
def dfs(self, u):
if u.left is not None:
self.dfs(u.left)
self.vals.append(u.val)
if u.right is not None:
self.dfs(u.right)
def next(self) -> int:
self.p += 1
return self.vals[self.p]
def hasNext(self) -> bool:
if self.p < self.n - 1:
return True
else:
return False
这个题下面提到了如何用均摊 \(\mathcal O(1)\),空间 \(\mathcal O(h)\) 的方法解决,实际上,我们只需要按照 next 的要求划分整个中序遍历的过程即可,可以手动模拟递归栈,即当前点的路径,然后给路径上的点打上是否被经过的标签 vis,这样就不用记录是父亲的左儿子还是右儿子了,在 next 操作的时候,如果要模拟回溯,那么需要不断 pop 掉 vis 过的栈内点:
from typing import Optional
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
class BSTIterator:
def __init__(self, root: Optional[TreeNode]):
self.now = []
self.root = root
self.s = -1
self.vis = []
def pushdown(self, u):
self.now.append(u)
self.vis.append(0)
if u.left is None:
return
self.pushdown(u.left)
def next(self) -> int:
if self.s == -1:
self.pushdown(self.root)
self.s = 1
ans = 0
g = self.now[-1]
ans = g
if g.right is None:
self.now.pop()
self.vis.pop()
while True:
if len(self.vis) == 0:
break
if self.vis[-1] == 1:
self.now.pop()
self.vis.pop()
else:
break
else:
self.vis[-1] = 1
self.pushdown(g.right)
return ans.val
def hasNext(self) -> bool:
if (self.s == 1) and len(self.now) == 0:
return False
else:
return True
直线上最多的点数
考虑求出所有的斜率、截距,然后斜率截距都相同的连线都属于同一条直线,加入这样的组合有 \(m\) 个,那么一定满足共线的点数 \(n\) 有 \(n^2-n-2m=0\),利用求根公式求解 \(n=\frac{1+\sqrt{8m}}{2}\),直接扫一遍即可,时间复杂度瓶颈在于排序 \(\mathcal O(n^2\log n)\):
from typing import List
from math import sqrt
class Solution:
def maxPoints(self, points: List[List[int]]) -> int:
n = len(points)
if n == 1:
return 1
sp = []
maxn = -1e16
tod = []
for i in range(n - 1):
for j in range(i + 1, n):
a, b = points[i], points[j]
if a[0] - b[0] == 0:
tod.append([i, j])
continue
k = (a[1] - b[1]) / (a[0] - b[0])
maxn = max(maxn, k)
B = (a[0] * b[1] - b[0] * a[1]) / (a[0] - b[0])
sp.append((k, B))
for p in tod:
i, j = p[0], p[1]
sp.append((maxn + 1, points[i][0]))
sp.sort()
sp.reverse()
# print(sp)
m = len(sp)
lst_k, lst_b = maxn + 3, -114514
cnt = 0
ans = 2
for i in range(m):
k, b = sp[i]
if k != lst_k or b != lst_b:
lst_k = k
lst_b = b
if cnt > 1:
ans = max(ans, self.cal(cnt))
cnt = 1
else:
cnt += 1
if cnt > 1:
ans = max(ans, self.cal(cnt))
return ans
def cal(self, m):
return int(0.5 + 0.5 * sqrt(1 + 8 * m))
最长有效括号
使用栈维护,考虑不合法的两种可能:右括号无法匹配,此时栈内必定为空,记录上一个右括号为空的位置,两者间隔更新最大值;左括号多余,这样的样例会永久留在栈内,并且不考虑两边,中间元素之间的间隔则给出了所有极长的子串(不能再作用扩展了),而左侧要考虑和上一个匹配不上的右括号之间的子串为极长(没有则设为 \(-1\)),右侧则是到原串的末尾即可,时间复杂度 \(\mathcal O(n)\),注意 python 列表维护栈时插入的两种不同情况:
class Solution:
def longestValidParentheses(self, s: str) -> int:
n = len(s)
if n == 0:
return 0
top = -1
a = []
lst = -1
ans = 0
for i in range(n):
if s[i] == "(":
top += 1
if len(a) <= top:
a.append(i)
else:
a[top] = i
else:
if top >= 0:
top -= 1
else:
ans = max(i - lst - 1, ans)
lst = i
if top == -1:
ans = max(n - lst - 1, ans)
else:
ans = max(a[0] - lst - 1, ans)
top += 1
if len(a) <= top:
a.append(n)
else:
a[top] = n
for i in range(1, top + 1):
ans = max(ans, a[i] - a[i - 1] - 1)
return ans
划分字母区间
可以贪心,比如考虑包含第一个字母的段,占用最少字母的划分如果可能,只能是在到最后一次出现第一字母的位置,然后这个区间内的字母,我们会再有一个最左的合法右端点,然后这样不断找下去知道当前区间内所有字母都是自己最后出现的即可,后面剩下的字符串又可以同样做,时间复杂度 \(\mathcal O(n)\):
from typing import List
class Solution:
def partitionLabels(self, s: str) -> List[int]:
self.ab = "abcdefghijklmnopqrstuvwxyz"
self.abd = dict([[self.ab[i], i] for i in range(26)])
self.maxloc = [-1 for i in range(26)]
n = len(s)
self.s = s
self.n = n
for i in range(n):
self.maxloc[self.abd[s[i]]] = i
i = 0
ans = []
while True:
if i > n - 1:
break
j = self.findr(i)
ans.append(j - i + 1)
i = j + 1
return ans
def findr(self, l):
c = self.s[l]
max_r = self.maxloc[self.abd[c]]
now_r = l
while True:
if max_r == now_r:
break
t = max_r
for i in range(now_r, max_r):
sc = self.s[i]
t = max(t, self.maxloc[self.abd[sc]])
now_r = max_r
max_r = t
return max_r
7.23
删除子字符串的最大得分
考虑到所有 'ab' 串只能是“一堆 a,一堆 b,一堆 a,...”或者“一堆 b,一堆 a,一堆 b,...”的形式,那么我们假设删除 ‘ab’ 的得分大于 'ba' 的删除得分,那么我们可以从第一个 'a' 开始,尽量往后删,对于分段的角度,如果剩下 'a',那么就可以合并到下一段 'a' 中,剩下 'b' 就可以合并到最前面的 'b' 中,最后剩下的一定是“一堆 b,一堆 a”的格式,这样话,就只能用 'ba' 删除了,这样一定是最优的,因为除了最前面可能存在的 'b',剩下后面的 'b' 要不因为太多实在不能删除了,要不就都被 'ab' 删除了,所以是最优的,可以贪心来完成,时间复杂度 \(\mathcal O(n)\)(可能实现麻烦了,可以用栈来实现其实):
class Solution:
def maximumGain(self, s: str, x: int, y: int) -> int:
n = len(s)
lst = "@"
a = []
now = 0
flg = -1
ans = 0
for i in range(n):
if s[i] != lst:
if (s[i] == 'a') and (lst == "b"):
a.append(now)
now = 1
lst = s[i]
elif (s[i] == 'b') and (lst == 'a'):
a.append(now)
now = 1
lst = s[i]
elif (lst == 'a') or (lst == 'b'):
a.append(now)
ans += self.cal(x, y, a, flg)
a = []
now = 1
lst = s[i]
elif (s[i] == 'a') or (s[i] == 'b'):
flg = 1 if s[i] == 'a' else 0
now = 1
lst = s[i]
else:
lst = s[i]
now = 1
else:
now += 1
if (lst == 'a') or (lst == 'b'):
a.append(now)
ans += self.cal(x, y, a, flg)
return ans
def cal(self, x, y, a, flg):
p = 0
ans = 0
n = len(a)
if n < 2:
return 0
if x >= y:
if flg == 1:
fir = 0
else:
fir = a[0]
p = 1
lef = 0
while True:
if p == n - 1:
lef += a[p]
ans += y * min(lef, fir)
break
elif p > n - 1:
ans += y * min(lef, fir)
break
if a[p] + lef >= a[p + 1]:
lef = a[p] + lef - a[p + 1]
ans += x * a[p + 1]
else:
fir += a[p + 1] - a[p] - lef
ans += x * (a[p] + lef)
lef = 0
p += 2
else:
if flg == 1:
fir = a[0]
p = 1
else:
fir = 0
lef = 0
while True:
if p == n - 1:
lef += a[p]
ans += x * min(lef, fir)
break
elif p > n - 1:
ans += x * min(lef, fir)
break
if a[p] + lef >= a[p + 1]:
lef = a[p] + lef - a[p + 1]
ans += y * a[p + 1]
else:
fir += a[p + 1] - a[p] - lef
ans += y * (a[p] + lef)
lef = 0
p += 2
return ans
变成好标题的最少代价
第一次见 dp 难在输出最优解上,涨见识了。
递推的部分就是维护 \(dp_{i,c}\) 为前 \(i\) 个字符符合好标题条件并且以 \(c\) 字符结尾的最少操作次数,然后 \(dp_{i,c}\) 可以推到 \(dp_{i+1,c}\) 以及 \(dp_{i+3, c'}\)(\(c'\) 任意),对 \(dp_{i,c}\) 的第二维取一下 max 预处理就可以做到 \(\mathcal O(n|\Sigma|)\),其中 \(|\Sigma|\) 是字母表大小,这里即 \(26\)。
要输出修改后的标题,我们考虑记录当前 \(dp\) 最小时,字符持续的长度,这样就可以最后输出了,但是如何保证字典序呢?两个操作,一个是对原字符串翻转后做 dp,这样的好处就是每次都优先选最影响字典序的,如果 \(dp\) 值一致,可以放心选字典序最小的字符;另一个就是如果在更新时,具体说是 \(i+1\) 更新时,如果遇到更新后的 dp 值和原 dp 值一致,就很难讲了,我们需要判断哪种是更好的(字典序更小的),我想到的解决方案就是利用我们已经存储好的最大持续长度,找到两个字符第一个不一样的位置,看看哪种方案更好,真是太麻烦了,考试一定做不出来要 GG 了:
class Solution:
def minCostGoodCaption(self, caption: str) -> str:
n = len(caption)
Caption = ""
for i in range(n - 1, -1, -1):
Caption += caption[i]
if n < 3:
return ""
self.ab = "abcdefghijklmnopqrstuvwxyz"
self.abd = dict({(self.ab[i], i) for i in range(26)})
dp = [[998244353 for i in range(26)] for j in range(n)]
sel = [[0 for i in range(26)] for j in range(n)]
kl = [0 for i in range(n)]
for i in range(26):
dp[2][i] = self.cal(Caption[: 3], i)
sel[2][i] = 3
for i in range(2, n - 1):
mindp = 998244353
for j in range(26):
now = dp[i][j] + self.cal(Caption[i + 1], j)
if now < dp[i + 1][j]:
sel[i + 1][j] = sel[i][j] + 1
dp[i + 1][j] = now
elif now == dp[i + 1][j]:
ps = sel[i][j] + 1
if ps > sel[i + 1][j]:
if j < kl[i + 1 - sel[i + 1][j]]:
sel[i + 1][j] = sel[i][j] + 1
elif ps < sel[i + 1][j]:
if kl[i + 1 - ps] < j:
sel[i + 1][j] = sel[i][j] + 1
if mindp > dp[i][j]:
mindp = dp[i][j]
kl[i] = j
if i + 3 <= n - 1:
for j in range(26):
now = mindp + self.cal(Caption[i + 1: i + 4], j)
if now < dp[i + 3][j]:
if kl[i] == j:
sel[i + 3][j] = sel[i][j] + 3
else:
sel[i + 3][j] = 3
dp[i + 3][j] = now
ans = ""
i = n - 1
while True:
if i < 2:
break
mindp = 998244353
now = 0
for j in range(26):
if dp[i][j] < mindp:
now = j
mindp = dp[i][j]
ans = self.ab[now] * sel[i][now] + ans
i -= sel[i][now]
Ans = ""
for i in range(n - 1, -1, -1):
Ans += ans[i]
return Ans
def cal(self, s, i):
# print(len(s), s)
if len(s) == 1:
return abs(self.abd[s[0]] - i)
else:
return abs(self.abd[s[0]] - i) + abs(self.abd[s[1]] - i) + abs(self.abd[s[2]] - i)
删除一个冲突对后最大子数组数目
真是要被自己气死,这个题就是按照右端点排序,然后动态按照左端点删除,这个过程中可以动态记录删掉某个冲突对后会增加多少合法子序列,即另外记录第二小的还有效的右冲突点即可。注意两个细节,我都没注意:一是一个左端点可能对应着好多个右端点,二是这个没有要求给出的冲突对满足 \(a<b\),只说了两者相等,时间复杂度 \(\mathcal O(m\log m+n)\),若排序使用桶排序,可以做到 \(\mathcal O(n+m)\)(这里当然那还是使用了更好写的 sort):
from typing import List
class Solution:
def maxSubarrays(self, n: int, conflictingPairs: List[List[int]]) -> int:
m = len(conflictingPairs)
if m == 1:
return (n + 1) * n // 2
P = []
for i in range(m):
if conflictingPairs[i][0] < conflictingPairs[i][1]:
P.append([conflictingPairs[i][0] - 1, conflictingPairs[i][1] - 1])
else:
P.append([conflictingPairs[i][1] - 1, conflictingPairs[i][0] - 1])
P.sort(key=lambda x: x[1])
self.upval = [0 for i in range(m)]
self.cp = [[] for i in range(n)]
self.vis = [0 for i in range(m)]
for i in range(m):
self.cp[P[i][0]].append(i)
maxn = 0
l, r = 0, 1
ans = 0
for i in range(n):
if l < m:
ans += (P[l][1] - i)
else:
ans += (n - i)
if r < m:
self.upval[l] += (P[r][1] - P[l][1])
maxn = max(maxn, self.upval[l])
elif l < m:
self.upval[l] += (n - P[l][1])
maxn = max(maxn, self.upval[l])
if len(self.cp[i]) > 0:
for x in self.cp[i]:
self.vis[x] = 1
if r >= m:
pass
elif self.vis[r] == 1:
while True:
r += 1
if r >= m:
break
if self.vis[r] == 0:
break
if l >= m:
pass
elif self.vis[l] == 1:
while True:
l += 1
if l >= m:
break
if self.vis[l] == 0:
break
r = l
if r < m:
while True:
r += 1
if r >= m:
break
if self.vis[r] == 0:
break
return ans + maxn
最长递增子序列
经典题,可以维护一个数组,代表长度为 \(k\) 的递增子序列最小的最后一个数是多少,然后更新的时候可以在这个数组上二分(找第一个比当前数不小的位置,将这个位置的值替换为当前数即可),对这个数组不断地更新,时间复杂度 \(\mathcal O(n\log n)\):
from typing import List
class Solution:
def lengthOfLIS(self, nums: List[int]) -> int:
n = len(nums)
self.minnum = [1e5 for i in range(n + 1)]
self.R = 1
self.minnum[1] = nums[0]
for i in range(1, n):
now = self.find(nums[i])
if now > self.R:
self.R += 1
self.minnum[now] = nums[i]
return self.R
def find(self, x):
l, r = 1, self.R + 1
mid = (l + r) >> 1
while l < r:
if self.minnum[mid] >= x:
r = mid
else:
l = mid + 1
mid = (l + r) >> 1
return mid
零数组变换 IV
由于 \(n\) 很小,而且实际上每个位置互不影响,我们只需要看每个位置能不能减成 \(0\) 即可,然后每个位置需要的最后一个修改取 max 即可。每个位置都是一个 0-1 背包问题,可以进行 dp,只需要维护能不能到达的布尔运算即可,时间复杂度 \(\mathcal O(nq\max a)\):
from typing import List
class Solution:
def minZeroArray(self, nums: List[int], queries: List[List[int]]) -> int:
n, q = len(nums), len(queries)
ans = 0
for i in range(n):
if nums[i] == 0:
continue
dp = [0 for j in range(nums[i] + 1)]
dp[nums[i]] = 1
flg = 0
for p in range(1, q + 1):
if (i < queries[p - 1][0]) or (i > queries[p - 1][1]):
continue
val = queries[p - 1][2]
for w in range(nums[i] - val + 1):
dp[w] = dp[w + val] or dp[w]
if dp[0] == 1:
ans = max(ans, p)
flg = 1
break
if flg == 0:
return -1
return ans
从树中删除边的最小分数
数据范围是大概 \(\mathcal O(n^2)\) 的,我们可以考虑枚举删除哪两条边了,直接枚举做起来麻烦,因为两条边的相对关系太复杂。不过我们可以考虑先枚举一条边删除,这样变成了两棵树,然后分别 dfs,记录子树异或和。然后再枚举另一条边,那么得到的三个联通分量分别是某棵树的子树(异或和 dfs 计算),和上个子树一起的树的另一部分(用这棵树总的异或和异或上个子树异或和即是这部分的异或和),还有另一棵完整的树(dfs 时异或和已计算)。这样就可以了,时间复杂度 \(\mathcal O(n^2)\):
from typing import List
class Edges:
def __init__(self, to, nxt):
self.to, self.nxt = to, nxt
class Solution:
def minimumScore(self, nums: List[int], edges: List[List[int]]) -> int:
n = len(nums)
self.nums = nums
self.head = [-1 for i in range(n)]
self.cnt = 0
self.e = []
for i in range(n - 1):
u, v = edges[i][0], edges[i][1]
self.add(u, v)
self.add(v, u)
ans = 10 ** 10
self.dep = [0 for i in range(n)]
self.xor = [0 for i in range(n)]
self.bel = [0 for i in range(n)]
for i in range(n - 2):
u, v = edges[i][0], edges[i][1]
self.dfs(u, v, 0, 0)
self.dfs(v, u, 0, 1)
for j in range(i + 1, n - 1):
p, q = edges[j][0], edges[j][1]
if self.dep[p] > self.dep[q]:
p, q = q, p
if self.bel[p] == 0:
a, b, c = self.xor[v], self.xor[q], self.xor[u] ^ self.xor[q]
ans = min(ans, self.maxdel(a, b, c))
else:
a, b, c = self.xor[u], self.xor[q], self.xor[v] ^ self.xor[q]
ans = min(ans, self.maxdel(a, b, c))
return ans
def maxdel(self, a, b, c):
return max(a, max(b, c)) - min(a, min(b, c))
def dfs(self, u, fa, depth, flg):
self.dep[u] = depth
self.bel[u] = flg
i = self.head[u]
self.xor[u] = self.nums[u]
while i >= 0:
v, nxt = self.e[i].to, self.e[i].nxt
if v == fa:
i = nxt
continue
self.dfs(v, u, depth + 1, flg)
self.xor[u] = self.xor[u] ^ self.xor[v]
i = nxt
return
def add(self, u, v):
E = Edges(v, self.head[u])
self.e.append(E)
self.head[u] = self.cnt
self.cnt += 1
酿造药水需要的最少总时间
分析在完成第 \(i\) 个药水时(\(i>1\)),需要满足的条件,假如上个药水起始时间是 \(x\),该药水起始时间是 \(y\),则 \(\min y=x+\max_{0\leq j\leq n -1}\{f_{j+1}m_{i-1}-f_jm_i\}\),其中 \(f\) 是 \(skill\) 的前缀和,这个 max 里的东西我一开始以为有单峰性质,但是实际上没有,就不能二分了,最终的时间复杂度只能是 \(\mathcal O(nm)\)(看了题解有用计算几何做的算法,只是我不会,能过就行了):
from typing import List
class Solution:
def minTime(self, skill: List[int], mana: List[int]) -> int:
n, m = len(skill), len(mana)
self.n = n
now = [0 for i in range(n + 1)]
for i in range(1, n + 1):
now[i] = now[i - 1] + skill[i - 1] * mana[0]
if m == 1:
return now[n]
self.f = [0 for i in range(n + 1)]
for i in range(1, n + 1):
self.f[i] = self.f[i - 1] + skill[i - 1]
ans = 0
for i in range(1, m):
now = 0
for j in range(n):
now = max(now, self.f[j + 1] * mana[i - 1] - self.f[j] * mana[i])
ans += now
ans += self.f[n] * mana[-1]
return ans
柱状图中最大的矩形
使用单调栈,保证栈内数字越来越大,代表还能继续作为瓶颈值,如果新的高度比栈顶要低,说明栈顶已经不能继续作为瓶颈值了,我们只需要在入栈的时候递推好可行的左端点(初始是自己,如果有出栈,则用出栈值的左端点进行更新),这样就可以计算出以当前值为瓶颈值的最大区间以及对应面积了,时间复杂度 \(\mathcal O(n)\):
from typing import List
class Solution:
def largestRectangleArea(self, heights: List[int]) -> int:
n = len(heights)
sta = [0 for i in range(n)]
top = -1
left = [0 for i in range(n)]
ans = 0
for i in range(n):
mins = i
while top >= 0:
if sta[top] >= heights[i]:
mins = min(mins, left[top])
now = (i - left[top]) * sta[top]
# print(now)
ans = max(ans, now)
top -= 1
else:
break
top += 1
sta[top] = heights[i]
left[top] = mins
while top >= 0:
now = (n - left[top]) * sta[top]
ans = max(ans, now)
top -= 1
return ans
分割等和子集
先判断奇偶,再进行 0-1 背包即可,注意是倒序枚举和值,时间复杂度 \(\mathcal O(n\sum a)\):
from typing import List
class Solution:
def canPartition(self, nums: List[int]) -> bool:
n = len(nums)
sn = 0
for i in range(n):
sn += nums[i]
if sn % 2 == 1:
return False
else:
m = sn // 2
dp = [0 for i in range(m + 1)]
dp[0] = 1
for i in range(n):
val = nums[i]
if val > m:
continue
for w in range(m, val - 1, -1):
dp[w] = dp[w] or dp[w - val]
if dp[m] == 1:
return True
else:
return False
7.24
机试就考了一个最小编辑距离问题,感觉这么多白刷了,不过希望机试少错一点吧,数据处理考这么难真的是无语了。

浙公网安备 33010602011771号