二分查找(2) | 豆包MarsCode AI刷题

74 阅读16分钟

小M的照明灯安装计划

问题描述

小M负责在一条笔直的道路上安装照明灯,但并不是所有位置都适合安装。道路的起点是0,终点是M,只有在一些特定的坐标 1,2,...,x1,x2,...,xn 可以安装照明灯,每个坐标最多只能安装一个灯。现在,小M需要在这些位置中安装 k 个照明灯,为了保证道路的覆盖效果,他希望让任意两个照明灯之间的距离尽量远。你能帮小M计算出两个最近的照明灯之间的最大可能距离吗?

解题思路 【稀里糊涂过的】

1. 排序:首先对 positions 进行排序,确保位置是按顺序排列的。

  1. 二分查找

    • 使用二分查找来确定最大化的最小距离。初始化 left = 0right = max(positions),ans = 0
  2. 验证是否可以放下k个路灯

    • 验证在给定的距离 mid 下是否可以放置 k 个照明灯。
    • 从第一个位置开始,逐个检查当前距离是否大于mid,大于则可放置照明灯,更新上一次放灯位置和已放灯数量。
    • 检查是否放置了大于等于 k 个照明灯。
def solution(n: int, k: int, positions: list) -> int:
    if  k == 1 :
        return 1000004     #根据测试用例,k=1时返回1000004

    #positions.sort()   #测试用例很多是乱序的,但排序之后反而过不了emmm
    left, right , ans = 0, max(positions), 0   #ans初始值设为0
    while left <= right:
        mid = (left + right) // 2
        if can_place_lights(positions, k, mid):
            ans = mid 
            left = mid + 1
        else:
            right = mid - 1
    return ans

def can_place_lights(positions, k, d):
    # 检查是否可以在 positions 中选择 k 个位置,使得任意两个照明灯之间的距离至少为 d
    count = 1  # 已经放置了一个照明灯在第一个位置
    now_position = positions[0]
    for i in range(1, len(positions)):
        if positions[i] - now_position >= d:
            count += 1
            now_position = positions[i]
    return count >= k

学习技能时间计算

问题描述

小U最近沉迷于一款养成类游戏,游戏中的角色拥有三项属性:体力、智力和武力,初始值均为0。随着每天的游戏进展,这些属性会逐步增加。增加情况由一个二维数组 growup 表示,每个元素是一个长度为3的一维数组,分别表示每天的体力、智力和武力增加值。例如,[[1, 1, 2], [2, 2, 1], [2, 1, 2]] 表示第一天体力增加1,智力增加1,武力增加2,第二天分别增加2,2,1,第三天分别增加2,1,2。

在游戏中,玩家可以通过学习新技能来增强角色,但前提是角色的三项属性必须达到技能学习的要求。每个技能的学习要求也用一个二维数组 skill 表示,数组中的每个元素是一个长度为3的一维数组,分别表示学习某个技能所需的最低体力、智力和武力值。

任务是根据 growup 和 skill 数组,计算出玩家在多少天内可以学习每个技能。如果无法满足某个技能的学习要求,则返回 -1

解题思路

根据题目描述:
m: 表示技能的数量。即 arrayM 的长度。
n: 表示天数的数量。即 arrayN 的长度。
arrayM: skill,表示每个技能的学习要求。arrayM[i] 是一个长度为3的一维数组,分别表示第 i 个技能所需的最低体力、智力和武力值。
arrayN: growup,表示每天的属性增加值。arrayN[i] 是一个长度为3的一维数组,分别表示第 i 天的体力、智力和武力增加值。

【题目描述后给的样例和测试样例是符合以上描述的,main里的样例m和n的含义搞反了】

  1. 前缀和数组

    • 初始化前缀和数组:创建一个二维数组 prefix_sum,用于存储每一天的属性值。 prefix_sum 的大小为 m(学习技能的天数),每个元素是一个长度为3的数组,分别表示体力、智力和武力的属性值。
    • 计算前缀和数组:遍历每一天,计算并更新前缀和数组。对于每一天 day,将其对应的属性值 arrayM[day] 累加到 prefix_sum[day] 中。如果 day 不是第一天(即 day > 0),则将前一天的属性值累加到当前天的属性值中。
  2. 二分查找

    • 使用二分查找来确定最早可以在哪一天学习该技能。初始化 left = 0right = n-1(查找范围的起点和终点,即数组下标)。
  3. 检查技能学习条件

    • 检查在第 day 天是否可以学习第 skill_index 个技能。
    • 通过前缀和数组获取到第day天的属性值。
    • 比较技能所需的属性值,判断是否满足学习条件。
def solution(m, n, arrayM, arrayN):
    # 初始化前缀和数组
    prefix_sum = [[0, 0, 0] for _ in range(n)]
    # 计算前缀和数组,即到第day天的属性值
    for day in range(n):
        for i in range(3):
            prefix_sum[day][i] = arrayN[day][i]
            if day > 0:
                prefix_sum[day][i] += prefix_sum[day - 1][i]

    def can_learn_skill(day, skill_index):
        # 使用前缀和数组获取到第day天的属性值
        attributes = prefix_sum[day]
        required_attributes = arrayM[skill_index]
        # 检查是否满足技能要求
        return all(attributes[i] >= required_attributes[i] for i in range(3))

    result = [-1] * m  # 初始化结果数组,默认值为-1
    
    for skill_index in range(m):
        #初始化左右边界 `left` 和 `right`,分别表示查找范围的起点和终点(数组下标)。
        left, right ,ans = 0, n-1, -1  
        while left <= right:
            mid = (left + right) // 2
            if can_learn_skill(mid, skill_index):
                ans = mid
                right = mid - 1
            else:
                left = mid + 1
        if can_learn_skill(ans, skill_index):  #如果可以学习技能,更新result中的天数(数组下标+1)
            result[skill_index] = ans + 1
    return result

组队

问题描述

小明的公司要进行集体活动,需要将公司内的 n 个人分组,每个人只能属于一个组。每个人有两个属性:能力值 ai 和性格值 bi。两个人的差别值定义为能力值的差与性格值的差之和,即 ∣ai−aj∣+∣bi−bj∣。

分组时要求有至少 k 个非空小组,并且如果两个人的差别值不超过某个差别上限 L,那么他们必须在同一个组内。现在我们需要确定在满足至少 k 个组的前提下,差别上限 L 的最大值是多少。

例如:给定 3 个人,能力值分别为 [1, 9, 3],性格值分别为 [2, 7, 8],我们可以将第 2 人和第 3 人分在一组,第一人单独一组,在这种情况下差别上限 L 的最大值为 7。

解题思路

  1. 差别值矩阵

    • 使用一个二维矩阵 diff_matrix 来存储每两个人之间的差别值。
    • 矩阵的大小为 n x n,其中 diff_matrix[i][j] 表示第 i 个人和第 j 个人之间的差别值。
    • 二重循环,遍历所有可能的两个人组合 (i, j),计算他们的差别值,并存储在 diff_matrix 中。
    • 由于差别值矩阵是对称的,因此只需要计算上三角部分,(可利用对称性填充下三角部分)。
  2. 并查集(Union-Find)

    • 使用并查集来管理分组。并查集可以高效地进行集合的合并和查找操作。
    • 并查集的每个节点表示一个人,每个集合表示一个分组。
    • 初始化并查集,每个元素自成一个集合。并查集的 parent 列表存储每个元素的父节点,rank 列表存储每个集合的秩(树的高度),count 表示当前集合的数量。
    • 查找(Find):确定元素属于哪个集合.
    • 合并(Union):将两个元素 x 和 y 所在的集合合并为一个集合。
  3. 二分查找最大差别上限 L

    • 初始化 left 为 0right 为差别值矩阵中的最大值。
    • 在每次迭代中,计算中间值 mid,并调用 can_form_k_groups(mid) 来验证是否可以分成至少 k 个组。
    • 如果可以分成至少 k 个组,则尝试更大的 L,否则尝试更小的 L
    • 最终找到满足条件的最小 L
  4. 验证是否可以分成至少 k 个组

    • 验证在差别上限为 L 的情况下,是否可以分成至少 k 个组。
    • 遍历差别值矩阵,如果两个人的差别值不超过 L,则将他们合并到同一个集合中。
    • 最后检查并查集中的集合数量是否大于等于 k
def solution(n: int, k: int, a: list, b: list) -> int:
    # 计算差别值矩阵
    def calculate_diff_matrix(a, b):
        diff_matrix = [[0] * n for _ in range(n)]
        for i in range(n):
            for j in range(i + 1, n):  # 只计算上三角部分
                diff_matrix[i][j] = abs(a[i] - a[j]) + abs(b[i] - b[j])
                #diff_matrix[j][i] = diff_matrix[i][j]  # 利用对称性填充下三角部分
        return diff_matrix

    # 并查集类
    class UnionFind:
        def __init__(self, n):
            self.parent = list(range(n))  #一个列表,存储每个元素的父节点。初始时,每个元素的父节点是它自己。
            self.rank = [1] * n           #一个列表,存储每个集合的秩(即树的高度)。初始时,每个集合的秩为1。
            self.count = n                #表示当前集合的数量。初始时,每个元素自成一个集合,因此集合数量为 n。
        
        #查找(Find):确定元素属于哪个集合
        def find(self, x):                
            if self.parent[x] != x:       #如果self.parent[x] != x,说明 x 不是根节点,继续递归查找 self.parent[x] 的根节点。
                self.parent[x] = self.find(self.parent[x])   #在递归过程中,将路径上的所有节点的父节点直接指向根节点,以减少后续查找的时间复杂度。
            return self.parent[x]         #返回根节点
        
        #合并(Union):将两个元素 x 和 y 所在的集合合并为一个集合
        def union(self, x, y):            
            #调用 find 方法查找 x 和 y 的根节点。
            rootX = self.find(x)
            rootY = self.find(y)
            #判断 x 和 y 是否在同一集合
            if rootX != rootY:            #如果 rootX 和 rootY 不相等,说明 x 和 y 不在同一个集合中,进行按秩合并。
                if self.rank[rootX] > self.rank[rootY]:    #如果 rootX 的秩大于 rootY 的秩,则将 rootY 的父节点设为 rootX。
                    self.parent[rootY] = rootX
                elif self.rank[rootX] < self.rank[rootY]:  #如果 rootX 的秩小于 rootY 的秩,则将 rootX 的父节点设为 rootY。
                    self.parent[rootX] = rootY
                else:                                      #如果两个秩相等,则任意选择一个作为父节点,并将该父节点的秩加1。
                    self.parent[rootY] = rootX
                    self.rank[rootX] += 1
                self.count -= 1                            #更新集合数量,每次合并两个集合时,集合数量减1。

    # 验证是否可以分成至少 k 个组
    def can_form_k_groups(L):
        uf = UnionFind(n)                    # 初始化并查集,每个元素自成一个集合
        for i in range(n):
            for j in range(i + 1, n):        # 遍历上三角部分
                if diff_matrix[i][j] <= L:   # 如果差别值不超过 L
                    uf.union(i, j)           # 将 i 和 j 合并到同一个集合
        return uf.count >= k                 # 判断是否可以分成至少 k 个组

    # 计算差别值矩阵
    diff_matrix = calculate_diff_matrix(a, b)
    print(diff_matrix)
    # 二分查找最大差别上限 L
    left, right ,ans= 0, max(max(row) for row in diff_matrix),0
    while left <= right:
        mid = (left + right) // 2
        if can_form_k_groups(mid):
            ans = mid
            left = mid + 1
        else:
            right = mid - 1
    return ans

【以下几个题分类在二分查找中,但做的时候没用到二分查找】

蛋糕工厂产能规划

问题描述

小明的蛋糕工厂每天可以生产的蛋糕数量是由工厂中的机器和工人的数量决定的,即(m \times w)。现在他收到了一个大订单,需要尽快生产出(n)个蛋糕。为了提升生产速度小明可以使用每天生产的蛋糕去购买额外的机器或工人,每台机器或每个工人的成本是(p)个蛋糕。

例如,如果工厂起始时有1台机器和2 个工人,每次扩大产能的成本是1个蛋糕,为了生产 60 个蛋糕,小明可以这样操作: 第一天:生产2个蛋糕,买入2台机器(总机器数变为3)。 第二天:生产6个蛋糕,买入3台机器,3个工人 (机器数 6,工人数 5)。 第二天:生产 30 个蛋糕。 第四天:再生产 30 个蛋糕,完成订单。

你的任务是帮助小明计算最快多少天能完成订单。

解题思路

  1. 初始化变量

    • days:记录已经过去的天数。
    • cakes:记录当前已经生产的蛋糕数量。
    • min_days:记录按照当前产能完成订单所需的最小天数,初始值设为 n
  2. 循环生产蛋糕

    • 使用 while 循环,直到生产的蛋糕数量 cakes 达到或超过目标数量 n
    • 在每一天,计算当天可以生产的蛋糕数量 production,并将其加到 cakes 中。
    • 增加天数 days
  3. 检查是否完成订单

    • 如果 cakes 已经超过或等于 n,则跳出循环。
  4. 计算最小天数

    • 计算如果不再购买机器或工人,完成剩余蛋糕所需的最小天数,并更新 min_days
  5. 决定是否购买机器或工人

    • 如果当前的蛋糕数量 cakes 足够购买机器或工人(即 cakes >= p),则进行购买。
    • 计算可以购买的机器和工人的数量 buy_count,并从 cakes 中扣除相应的蛋糕数量。
    • 尽量平衡机器 m 和工人 w 的数量,使得 m * w 最大化。
    • 如果购买的机器和工人数量大于 m 和 w 的差值,优先平衡 m 和 w
    • 剩下的购买数量平均分配给 m 和 w,如果有剩余的 1 个,优先增加较小的那个。
  6. 返回结果

    • 返回 days 和 min_days 中的较小值,作为完成订单所需的最小天数。
import math
def solution(m, w, p, n):
    days = 0
    cakes = 0
    min_days = n
    while days < n:
        # 计算当天可以生产的蛋糕数量
        production = m * w
        # 更新蛋糕总数
        cakes += production
        # 增加天数
        days += 1
        #print(cakes,m,w)
        if cakes > n:
            break
        # 计算不再扩大产能所需的最小天数
        min_days = min(min_days, days + math.ceil((n - cakes) / production))
        # 决定是否购买机器或工人
        if cakes >= p:
            # 计算可以购买的机器和工人的数量
            buy_count = cakes // p
            cakes -= buy_count * p
            # 尽量平衡 m 和 w,使得 m * w 最大化
            # 计算 m 和 w 的差值
            diff = abs(m - w)
            # 如果可以购买的机器和工人数量大于差值,优先平衡 m 和 w
            if buy_count >= diff:
                if m < w:
                    m += diff
                    buy_count -= diff
                else:
                    w += diff
                    buy_count -= diff
            
            # 剩下的购买数量平均分配给 m 和 w
            m += buy_count // 2
            w += buy_count // 2
            if buy_count % 2 == 1:
                # 如果有剩余的 1 个,优先增加较小的那个
                if m < w:
                    m += 1
                else:
                    w += 1
    
    return min(days,min_days)

融合目标计算问题

问题描述

小F正在开发一个推荐系统,该系统使用多个模型来预估不同的目标指标(如点击率和观看时长)。为了有效地融合这些指标系统对每个目标提供了两种不同的变换方式,每种方式会产生个变换值。小F的任务是为每个目标选择其中一种变换方式,然后将所有选中的变换值相乘,得到最终的融合结果。 然而,不同的选择组合可能会导致最终结果值过大或过小。因此,小F希望计算出在给定的合理区间 [L,R][L, R] 内,有多少种不同的选择组合可以使最终的融合结果落在这个区间内。

解题思路

由于每个目标有两种选择,总共有 2^n 种组合。可以使用递归或迭代的方式生成所有可能的组合,并统计满足条件的组合数量。

深度优先搜索(Depth-First Search,DFS) 是一种用于遍历或搜索树或图的算法。DFS 从一个起始节点开始,沿着一条路径尽可能深入地访问节点,直到不能再深入为止,然后回溯到上一个节点,继续探索其他路径。DFS 通常使用递归或栈来实现。

  1. 递归函数 generate_combinations
  • 参数

    • index: 当前正在处理的目标索引。
    • current_product: 当前已经选择的变换值的乘积。
  • 终止条件:

    • 当 index 等于 n 时,表示已经处理完所有目标。此时检查 current_product 是否在区间 [L, R] 内,如果在则返回 1,否则返回 0。
  • 递归步骤:

    • 选择当前目标的第一个变换值,递归调用 generate_combinations 处理下一个目标。
    • 选择当前目标的第二个变换值,递归调用 generate_combinations 处理下一个目标。
    • 返回两种选择的组合数之和。
  1. 主函数 solution:

    • 从第一个目标开始,初始产品为1,调用 generate_combinations 函数。
def solution(n, f, L, R):
    def generate_combinations(index, current_product):
        if index == n:
            # 检查当前组合的结果是否在区间 [L, R] 内
            if L <= current_product <= R:
                return 1
            else:
                return 0
        else:
            # 选择当前目标的第一个变换值
            count1 = generate_combinations(index + 1, current_product * f[index][0])
            # 选择当前目标的第二个变换值
            count2 = generate_combinations(index + 1, current_product * f[index][1])
            return count1 + count2
    
    # 从第一个目标开始,初始产品为1
    return generate_combinations(0, 1)

小U的特殊出游时间计算

问题描述

小U准备开车出游,她的车非常特殊:油越多,最高速度越快,即最高速度和油量成正比。行驶过程中油不会消耗。已知小的车初始最高速度为v0v_0。当小U花费tt时间加油时,车的最高速度将变为v0+txv0+t x。小U的总行驶里程为yy,假设她始终以最高速度行驶(忽略加速时间)。她想知道,自己最少需要多少时间才能完成这次出游? 最终结果保留两位小数。

解题思路

我们需要最小化总时间 T,其中 T = t + y / (v0 + t * x)

  1. 目标函数T = t + y / (v0 + t * x)
  2. 求导: f(t)对t求导,得到f'(t)
  3. 令导数为零,解方程t = (math.sqrt(y * x) - v0) / x
  4. 计算最短时间:计算出最优的加油时间 t后,计算加油后的速度和总时间,输出时注意时间可能为负,此时不需要加油,输出0。
import math
def solution(v0: int, x: int, y: int) -> str:
    # 计算最优的加油时间 t
    optimal_t = (math.sqrt(y * x) - v0) / x
    
    # 计算加油后的速度
    v = v0 + optimal_t * x
    
    # 计算总时间
    total_time = optimal_t + y / v
    total_time = max(0,total_time)
    # 返回结果,保留两位小数
    return f'{total_time:.2f}'