[英雄星球七月集训LeetCode解题日报] 第9日 二分查找

[英雄星球七月集训LeetCode解题日报] 第9日 二分查找

    • 日报
    • 题目
      • 一、 1146. 快照数组
        • 1. 题目描述
        • 2. 思路分析
        • 3. 代码实现
      • 二、1498. 满足条件的子序列数目
        • 1. 题目描述
        • 2. 思路分析
        • 3. 代码实现
      • 三、 1802. 有界数组中指定下标处的最大值
        • 1. 题目描述
        • 2. 思路分析
        • 3. 代码实现
      • 四、2040. 两个有序数组的第 K 小乘积
        • 1. 题目描述
        • 2. 思路分析
        • 3. 代码实现

日报

题目

  • 今天4题都挺难的,做了很久。

一、 1146. 快照数组

链接: 1146. 快照数组

1. 题目描述

[英雄星球七月集训LeetCode解题日报] 第9日 二分查找_第1张图片

2. 思路分析

  • 本题的思路还是比较好想的,我分别用了多种方案实现(其实类似,思考角度不同),下面介绍最普通的两种。
  • 首先想到的是不实际建数组,只用dict储存每个set过的位置,由于需要快照,因此我们定义一个全局快照,每次set时去字典里检查这个位置的数组,尾部set或append,这里由于快照号是自增的,用list装元组即可,SortedDict慢一点。
  • 第二个思路实际表现更佳一点:建数组,每个位置建立一个字典,初始化都是{0:0},同样快照时快照号自增。查询时按索引找到对应的字典,检查快照号之前的那个位置的数值即可。
  • [英雄星球七月集训LeetCode解题日报] 第9日 二分查找_第2张图片

3. 代码实现

class SnapshotArray:

    def __init__(self, length: int):
        self.arr = [{0:0} for _ in range(length)]
        self.snap_id = 0


    def set(self, index: int, val: int) -> None:
        self.arr[index][self.snap_id] = val


    def snap(self) -> int:
        self.snap_id += 1
        return self.snap_id-1


    def get(self, index: int, snap_id: int) -> int:
        arr = self.arr 
        dct = arr[index]
        # print(sd)
        if snap_id in dct :
            return dct[snap_id]
        ks = list(dct.keys())
        pos = bisect_right(ks,snap_id)
        if pos < 0:
            return 0
        return dct[ks[pos-1]]

二、1498. 满足条件的子序列数目

链接: 1498. 满足条件的子序列数目

1. 题目描述

[英雄星球七月集训LeetCode解题日报] 第9日 二分查找_第3张图片

2. 思路分析

  • 本题求得是子序列的数量,元素可以重复,实际上是求下标序列的数量。
  • 由于下标序列本身是有序的,因此等同于讨论哪些下标的组合满足题意。
  • 那么我们先给nums排序,实际上下标是不会变的,因此没关系。
  • 我们每访问一个数字时,是想知道它可以和哪些数字进行组合,使这个集合中的最大最小值和<=target。
  • 由于排过序了,因此对每个数记为l,我们可以向后组合,到合法最大值r之间所有的数都可以组合起来,我们强制这个集合使用i,那么集合的种类数共有2(r-l)个,[i+1,r]都可以选或不选。
  • r的位置可以二分或者对向双指针。
  • 我这里用了对象双指针,算法总体复杂度O(n)。

  • 优化了很多次,发现卡时长的点竟然在于求2的n次幂,一开始直接python求了,大数计算很慢,后来中间过程取余直接10倍速。
  • 再后来手写快速幂取余,全局打表取余,飙升99%。
  • [英雄星球七月集训LeetCode解题日报] 第9日 二分查找_第4张图片

3. 代码实现

MOD = 10**9+7
@cache
def fastPower3(a,n):
    ans = 1
    base = a
    while n != 0:
        if (n & 1 != 0):
            ans *= base % MOD
        base *= base % MOD
        n >>= 1
    return ans % MOD
f = [1]
for i in range(10**5):
    f.append(f[-1]*2%MOD)
class Solution:
    def numSubseq(self, nums: List[int], target: int) -> int:
        nums.sort()
        n = len(nums)
        l,r=0,n-1
        ans = 0
        
        while l<=r :
            while r>=l and nums[l] + nums[r]>target:
                r -= 1
            if l>r:
                break
            # ans = (ans+2**(r-l))%MOD 
            ans = (ans+f[r-l])%MOD 
            # print(l,r,ans)
            l += 1
            
        return ans%MOD

三、 1802. 有界数组中指定下标处的最大值

链接: 1802. 有界数组中指定下标处的最大值

1. 题目描述

[英雄星球七月集训LeetCode解题日报] 第9日 二分查找_第5张图片

2. 思路分析

这题蛮有意思的,核心是快速计算元素和。

  • 我们先设计一个函数calc(k),作用是将计算在index上设置为k,那么这个数组最小和可以是几。
  • 有了这个calc(k)后,k和calc(k)是严格正相关的,显然我们就可以二分答案。
  • 那么如何设计calc(k)呢,也比较简单,显然我们要让总和最小,index两边的数应该尽可能小,本题又要求相邻的数差最大是1,因此最快下降速度是1,发现是个梯形,或者说等差数列求和,套公式即可;
  • 这里需要注意,最多下降到1,因此长度如果超过k了,分类讨论剩下的数是1.

3. 代码实现

class Solution:
    def maxValue(self, n: int, index: int, maxSum: int) -> int:
        # 设index上数是k,k∈[1,maxSum-n+1]
        # 那它左边要分配长度w=index个数,若w
        # 它右边要分配w=n-index-1个,若w
        # 为了两边数值最小,两边一定是山峰状最快下降(每次降1)。
        def calc(k):  # index设k时的元素和
            s = k
            w = index 
            s += (k-1+k-w)*w//2 if w<k else (k-1+1)*(k-1)//2 + w-k+1
            w = n-index-1
            s += (k-1+k-w)*w//2 if w<k else (k-1+1)*(k-1)//2 + w-k+1
            return s
        return bisect_right(range(maxSum-n+1+1),maxSum,lo=1,key=calc)-1        

四、2040. 两个有序数组的第 K 小乘积

链接: 2040. 两个有序数组的第 K 小乘积

1. 题目描述

[英雄星球七月集训LeetCode解题日报] 第9日 二分查找_第6张图片

2. 思路分析

这题写了一天,它值得一篇题解:[LeetCode解题报告] 2040. 两个有序数组的第 K 小乘积

  • 其实这题是乘法表的分类讨论加强版,给出两道类似的题目:668. 乘法表中第k小的数和719. 找出第 K 小的数对距离
    [英雄星球七月集训LeetCode解题日报] 第9日 二分查找_第7张图片
    更多的点击上方题解吧,比较长。
    [英雄星球七月集训LeetCode解题日报] 第9日 二分查找_第8张图片

3. 代码实现

class Solution:
    def kthSmallestProduct(self, nums1: List[int], nums2: List[int], k: int) -> int:
        m,n = len(nums1), len(nums2)
        pos1,neg1,zero1 = [x for x in nums1 if x>0],[x for x in nums1 if x < 0],[x for x in nums1 if x == 0]
        pos2,neg2,zero2 = [x for x in nums2 if x>0],[x for x in nums2 if x < 0],[x for x in nums2 if x == 0]
        m1,m2,m3 = len(pos1),len(neg1),len(zero1)  # 数组1中,正数的个数,负数的个数,0的个数
        n1,n2,n3 = len(pos2),len(neg2),len(zero2)
        tot_neg = m1*n2+m2*n1  # 总共的负数积数量
        tot_pos = m1*n1+m2*n2  # 总共的正数积数量
        tot_zero = m3*(n1+n2)+n3*(m1+m2) + m3*n3  # 总共的0数量
        min1,min2 = min(nums1),min(nums2)
        max1,max2 = max(nums1),max(nums2)
        def calc(x):
            # 计算乘积小于等于x的数对有多少个。
            if x == 0:  # 显然就是0的数量+负数的数量
                return tot_neg+tot_zero
            if x > 0:  # 0和负数都先算上,然后计算有多少个正数小于等于x:用两边正数结合,负数结合
                ret = tot_neg+tot_zero
                j = n1-1
                for i in pos1:
                    while j >= 0 and i * pos2[j] > x:
                        j -= 1
                    ret += j+1
                j = 0
                for i in neg1[::-1]:
                    while j<n2 and i * neg2[j] > x:
                        j += 1
                    ret += n2-j
                return ret 
            if x < 0:  # 用两边正结合负
                ret = 0
                j = 0
                for i in neg1:
                    while j < n1 and i * pos2[j] > x:
                        j += 1
                    ret += n1-j
                j = 0
                for i in neg2:
                    while j < m1 and i * pos1[j] > x:
                        j += 1
                    ret += m1-j
                return ret
       
        bound = [a*b for a,b in product([min1,max1],[min2,max2])]
        t = range(min(bound),max(bound)+1)
        pos =  bisect_left(t,k,key=calc)
        return t[pos]

你可能感兴趣的:(今天开刷leetcode,英雄星球七月集训,leetcode,算法,数据结构)