[英雄星球六月集训LeetCode解题日报] 第九日 二分
一、 327. 981. 基于时间的键值存储
链接: 981. 基于时间的键值存储
1. 题目描述
设计一个基于时间的键值数据结构,该结构可以在不同时间戳存储对应同一个键的多个值,并针对特定时间戳检索键对应的值。
实现 TimeMap 类:
TimeMap() 初始化数据结构对象
void set(String key, String value, int timestamp) 存储键 key、值 value,以及给定的时间戳 timestamp。
String get(String key, int timestamp)
返回先前调用 set(key, value, timestamp_prev) 所存储的值,其中 timestamp_prev <= timestamp 。
如果有多个这样的值,则返回对应最大的 timestamp_prev 的那个值。
如果没有值,则返回空字符串(“”)。
2. 思路分析
题目限制时间戳是严格递增的,不存在重复,因此内层不需要有序表,本身是有序的。
字典搜到直接二分即可。
3. 代码实现
class TimeMap:
def __init__(self):
self.db = collections.defaultdict(list)
def set(self, key: str, value: str, timestamp: int) -> None:
self.db[key].append((timestamp,value))
def get(self, key: str, timestamp: int) -> str:
strs = self.db.get(key)
if strs is None:
return ''
idx = bisect_right(strs,timestamp,key=lambda x:x[0])
if idx == 0:
return ''
return strs[idx-1][1]
二、 400. 第 N 位数字
链接: 400. 第 N 位数字
1. 题目描述
给你一个整数 n ,请你在无限的整数序列 [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, …] 中找出并返回第 n 位上的数字。
2. 思路分析
找到这个数在几位数上。
这题数据规模小,可以打表。
今天的每日一题类似,但不能打标。
497. 非重叠矩形中的随机点
3. 代码实现
class Solution:
def findNthDigit(self, n: int) -> int:
if n <= 9:
return n
DL = [0, 9, 189, 2889, 38889, 488889, 5888889, 68888889, 788888889]
d = bisect_left(DL,n) # n在d位数上
n -= DL[d-1]+1 # 砍掉位数少于d的前边数字,问题转化为求在d位数上的第n位,从0开始
x = 10**(d-1) + n//d
return int(str(x)[n%d])
三、1300. 转变数组后最接近目标值的数组和
1. 题目描述
给你一个整数数组 arr 和一个目标值 target ,请你返回一个整数 value ,使得将数组中所有大于 value 的值变成 value 后,数组的和最接近 target (最接近表示两者之差的绝对值最小)。
如果有多种使得和最接近 target 的方案,请你返回这些整数中的最小值。
请注意,答案不一定是 arr 中的数字。
2. 思路分析
- 先把数组排序,这样每次把大于value的数处理转变的时候可以直接操作一个区间。
- 既然要算区间和,先计算前缀和。那么转变后的和change=前半段和+后半段长度*value。
- 这样先二分value的pos,再二分转变后的和即可。
- 排序nlgn,计算前缀和n,两层二分lgnlgn.,总复杂度O(nlgn)
3. 代码实现
class Solution:
def findBestValue(self, arr: List[int], target: int) -> int:
n = len(arr)
top = max(arr)+1
arr.sort()
presum = list(accumulate(arr,initial=0))
def sum_interval(i,j):
return presum[j+1]-presum[i]
s = presum[n]
def change(x):
# 如果答案是x,计算改编后的数组和
pos = bisect_right(arr,x)
if pos >= n:
return presum[n]
if pos == 0:
return n*x
return sum_interval(0,pos-1)+x*(n-pos)
ans = bisect_right(range(top),target,key=change)
if abs(change(ans-1)-target) <= abs(change(ans)-target):
ans -= 1
return ans
四、 1713. 得到子序列的最少操作次数
1. 题目描述
给你一个数组 target ,包含若干 互不相同 的整数,以及另一个整数数组 arr ,arr 可能 包含重复元素。
每一次操作中,你可以在 arr 的任意位置插入任一整数。比方说,如果 arr = [1,4,1,2] ,那么你可以在中间添加 3 得到 [1,4,3,1,2] 。你可以在数组最开始或最后面添加整数。
请你返回 最少 操作次数,使得 target 成为 arr 的一个子序列。
一个数组的 子序列 指的是删除原数组的某些元素(可能一个元素都不删除),同时不改变其余元素的相对顺序得到的数组。比方说,[2,7,4] 是 [4,2,3,7,2,1,4] 的子序列(加粗元素),但 [2,4,2] 不是子序列。
2. 思路分析
求出最长公共子序列(LCS),然后补充剩下字符即可。答案=len(target)-len(lcs)。
可以直接套板
普通LCS的DP做法是n^2的,需要二分优化版本。
二分优化版的思路是通过下标转化后,问题转化为LIS,LIS存在二分优化版本。
这题由于s1本身是去重的,因此更简单。
参考我的刷题模板:
[python刷题模板] 最长递增子序列LIS、最长公共子序列 LCS
3. 代码实现
class Solution:
def minOperations(self, target: List[int], arr: List[int]) -> int:
'''
题意一看是找LCS(最长公共子序列)。然后给arr中补上target中剩下的数就行。
但是LCS标准DP是O(nm),过不了。
需要转化成LIS(最长上升子序列)来二分优化。
本题最长公共子序列的意思是target中的数,在arr中尽量找到的多,且相对位置保持不变。
位置就是下标,位置有序转换过去就是下标递增。
由于target本身是去重的,因此可以翻转下标,用下标唯一来代表这个数。
也就是说把target中每个数的下标映射到arr中,寻找子序列,下标是递增的,问题转化为LIS
arr中的不在target中的字符是没用的,可以先干掉。
'''
def lis_nlgn(nums):
# 最长上升序列的二分优化版本
n = len(nums)
# dp = [float('inf')] * (n+1) # dp[i] 为长度为i的上升子序列的可以的最小尾巴
dp = []
for num in nums:
if not dp or num > dp[-1]:
dp.append(num)
else:
pos = bisect_left(dp,num)
dp[pos] = num
return len(dp)
def lcs_nlgn_when_s1_unique(s1,s2):
# 当s1本身是去重的,用这个,下标不用拼list
s1_poses = {v:pos for pos,v in enumerate(target)}
cleaned = [s1_poses[x] for x in arr if x in s1_poses]
return lis_nlgn(cleaned)
def lcs_nlgn(s1,s2):
# 转lis利用二进制优化。
s1_poses = collections.defaultdict(list)
for i in range(len(s1)-1,-1,-1): # 需要逆序
s1_poses[s1[i]].append(i)
cleaned_s2_pos = list(itertools.chain.from_iterable([s1_poses[x] for x in s2 if x in s1_poses]))
return lis_nlgn(cleaned_s2_pos)
return len(target)-lcs_nlgn_when_s1_unique(target,arr)
五、附每日一题 497. 非重叠矩形中的随机点
链接: 497. 非重叠矩形中的随机点
1. 题目描述
给定一个由非重叠的轴对齐矩形的数组 rects ,其中 rects[i] = [ai, bi, xi, yi] 表示 (ai, bi) 是第 i 个矩形的左下角点,(xi, yi) 是第 i 个矩形的右上角点。设计一个算法来随机挑选一个被某一矩形覆盖的整数点。矩形周长上的点也算做是被矩形覆盖。所有满足要求的点必须等概率被返回。
在给定的矩形覆盖的空间内的任何整数点都有可能被返回。
请注意 ,整数点是具有整数坐标的点。
实现 Solution 类:
Solution(int[][] rects) 用给定的矩形数组 rects 初始化对象。
int[] pick() 返回一个随机的整数点 [u, v] 在给定的矩形所覆盖的空间内。
2. 思路分析
这题和今天第二题差不多,但是不能打表了。
每个方块能覆盖的整数点个数为(y2-y1+1)*(x2-x1+1)。
由于每个方块不重叠,因此可以计算前缀和。
得出一共有多少个点,在这个点范围随机一个数,然后找出这个点即可。
3. 代码实现
class Solution:
def __init__(self, rects: List[List[int]]):
self.rects = rects
self.rect_cnt = [(y2-y1+1)*(x2-x1+1) for x1,y1,x2,y2 in rects ] # 第i个矩形有几个点
self.presum = list(accumulate(self.rect_cnt,initial=0))
def pick(self) -> List[int]:
k = randrange(self.presum[-1]) # [0,n) 从n个点中随机取一个。
d = bisect_right(self.presum,k) # k 在第d个箱子上
k -= self.presum[d-1]
# 找这个箱子的第k个点(从0开始)
x1,y1,x2,y2 = self.rects[d-1]
return [x1+k//(y2-y1+1),y1+k%(y2-y1+1)]