【Leetcode】2940-2327
前言
不会做的一个题目,但是看了一下题目的提示还是做出来了,不过做出来的算法的效率差了一些。此外一样的思路,也是可以写出来完全不一样的代码,甚至时间复杂度都不一样
思路
首先对于某个查询而言,是查找包含他们自身在内比自己更大的重合的位置
- 第一种情况,如果位置靠右了(称之为\(y\))所在的值比位置靠左的(称之为\(x\))值更大,此时\(x\)就可以调到\(y\)的位置,实现重合。
- 第二种情况是当\(x=y\)时,他们的位置就是相遇的位置
- 剩余第三种情况就是,当\(heights[x]>heights[y]\)的时候,我们的目的是找到\(nums[y+1:]\)上第一个比\(heights[x]\)和\(heights[y]\)的索引
要找到所有查询的结果,最简单直接的办法就是针对于每一种查询的第三种情况直接进行遍历,但是这样的时间复杂度为\(O(n*q)\)
1. 排序+单调栈二分(离线)
根据题目中,第一个XXX,很容易想到的就是单调栈。对于这个题目而言,倒序设计单调栈,
而排序的目的是优先考虑位置靠后的,这样子查询是依次往前走的,单调栈就尾随其后
class Solution:
def leftmostBuildingQueries(self, heights: List[int], queries: List[List[int]]) -> List[int]:
n = len(heights)
# 根据每次查询中右边位置 降序排序
q = sorted(list(i for i in range(len(queries))),key=lambda x:-max(queries[x]))
ans = [-1]*len(queries)
stack =deque() # 递减的单调栈
cur = n-1
for i in q:
l,r = min(queries[i]),max(queries[i])
# 更新单调栈
for j in range(cur,r,-1):
while stack and heights[stack[0]]<=heights[j]:
stack.popleft()
stack.appendleft(j)
cur = r
# 可以达到r的位置
if heights[r]>heights[l] or l==r:
ans[i] = r
else:
# 二分查找
idx = bisect_right(stack, heights[l],key=lambda x:heights[x])
if idx<len(stack):
ans[i] = stack[idx]
return ans
假定\(heights\)的长度为\(h\),\(queries\)长度为\(q\)。
算法的时间复杂度是\(O(q\text{ log }q+q\text{ log }h+h)\)
关于这个时间复杂度的计算,排序的时间复杂度为\(O(q\text{ log }q)\)
后续的循环中,对于单调栈的更新操作,实际上与循环本身没有关系,每个元素最多进出一次,因此这部分的时间复杂度是\(O(h)\),二分查找的时间复杂度是\(O(q\text{ log }h)\)
2. 单调栈二分
考虑使用类似于基数排序的方式以一换直接的排序。
这样循环的本身就从排序后的查询变成了数组本身
class Solution:
def leftmostBuildingQueries(self, heights: List[int], queries: List[List[int]]) -> List[int]:
ans = [-1] * len(queries)
qs = [[] for _ in heights]
for i, (a, b) in enumerate(queries):
if a > b:
a, b = b, a # 保证 a <= b
if a == b or heights[a] < heights[b]:
ans[i] = b # a 直接跳到 b
else:
qs[b].append((heights[a], i)) # 离线询问
st = []
for i in range(len(heights) - 1, -1, -1):
for ha, qi in qs[i]:
j = bisect_left(st, -ha, key=lambda i: -heights[i]) - 1
if j >= 0:
ans[qi] = st[j]
while st and heights[i] >= heights[st[-1]]:
st.pop()
st.append(i)
return ans
算法的时间复杂度为\(O(h+q \text{ log }h)\)。
对于这个时间复杂度的计算,同样的道理,第一个循环的时间复杂度为\(O(h)\),第二个循环中,二分的时间复杂度与外层的循环没有关系,因此其单独为查询的个数与二分算法的乘积为\(O(q\text{ log }h)\),外层的循环实际上只对于单调栈的进出有关系,上述也分析过,单调栈的进出每个元素最多一次。
3. 最小堆
class Solution:
def leftmostBuildingQueries(self, heights: List[int], queries: List[List[int]]) -> List[int]:
ans = [-1] * len(queries)
qs = [[] for _ in heights]
for i, (a, b) in enumerate(queries):
if a > b:
a, b = b, a # 保证 a <= b
if a == b or heights[a] < heights[b]:
ans[i] = b # a 直接跳到 b
else:
qs[b].append((heights[a], i)) # 离线询问
h = []
for i, x in enumerate(heights):
while h and h[0][0] < x:
# 堆顶的 heights[a] 可以跳到 heights[i]
ans[heappop(h)[1]] = i
for q in qs[i]:
heappush(h, q) # 后面再回答
return ans
这个代码的解释是,如果当前最小的都找不到可以更新的位置,那么更大的无论如何都是不能作为更新的位置的,不需要先考虑其位置关系。

浙公网安备 33010602011771号