刷题笔记-小M的幸运数列变换 | 豆包MarsCode AI刷题

127 阅读6分钟

一、问题描述

www.marscode.cn/practice/vk…

小M有一个长度为 nn 的数组 aa,他希望通过一些操作将数组中的所有元素都变为数字 ww。每次操作,小M可以选择一个区间 [l,r][l, r],将该区间内的所有数字加 1。但为了增加挑战性,每次操作的 llrr 必须各不相同。需要计算有多少种不同的操作方案可以完成这个目标。注意,如果所有操作的区间相同,则视为同一种方案,操作顺序的不同不影响方案的唯一性。

输入:

  • 数组长度 nn
  • 目标数字 ww
  • 初始数组 aa

输出:

  • 可以将数组变为全 ww 的不同操作方案数

二、解题思路

1. 分析问题

首先,我们需要理解题目要求:

  • 操作限制: 每次操作选择的区间 [l,r][l, r]llrr在所有操作中必须各不相同。这意味着我们不能在不同的操作中使用相同的 llrr
  • 方案判定: 如果两个方案的操作区间集合相同,则视为同一种方案,操作的顺序不影响方案的唯一性。

2. 初步思考

  • 需要的增量: 对于数组中的每个元素 aia_i,需要增加 δi=wai\delta_i = w - a_i 次操作才能达到目标值 ww
  • 操作次数限制: 由于每次操作可以将一个区间内的所有元素加 1,因此我们需要设计一组操作,使得在满足 llrr 各不相同的情况下,累计的增量能够满足每个 δi\delta_i

3. 可行性分析

  • 元素差值为负数: 如果存在 δi<0\delta_i < 0,说明无法通过加法操作使其达到 ww,方案数为 0。
  • 操作次数与数组长度: 由于 llrr 各不相同,最多只能进行 nn 次操作(因为 llrr 的取值范围是 1 到 nn,且不能重复)。
  • 最大增量限制: 如果某个 δi\delta_i 大于 nn,则无法满足条件,因为无法对同一位置进行超过 nn 次的增量操作。

4. 方案计数思路

考虑到操作的 llrr 不能重复,我们需要在所有可能的区间中选择若干个,组合成操作方案。

  • 方案的独立性: 操作的顺序不重要,只要操作的区间集合相同,就视为同一种方案。
  • 覆盖需求: 我们需要选择若干个区间,使得对于每个位置 ii,这些区间覆盖它的次数等于 δi\delta_i

5. 解法设计

方法一:暴力搜索(适用于小规模数据)

  • 思路: 枚举所有可能的区间组合,检查哪些组合能够满足增量需求。
  • 实现:
    • 生成所有可能的区间 [l,r][l, r],其中 lrl \leq rllrr 在所有操作中各不相同。
    • 使用回溯法(Backtracking)尝试所有区间组合,记录每个位置被覆盖的次数。
    • 剪枝:如果当前覆盖次数已经超过了所需的 δi\delta_i,则停止当前路径的搜索。
  • 适用性: 由于组合数量随着 nn 的增大呈指数增长,该方法只适用于 nn 较小的情况(一般 n10n \leq 10)。

方法二:数学推导(寻找规律)

  • 思路: 由于操作次数和区间选择受到严格限制,尝试通过数学方法推导可能的方案数。
  • 特殊情况:
    • 所有 δi=0\delta_i = 0 数组已经是全 ww,方案数为 1(不需要操作)。
    • 所有 δi=k\delta_i = k 需要对整个数组进行 kk 次增量操作,且每次操作的区间必须不同。
  • 一般情况:
    • 如果所有 δi\delta_i 相同且等于 1,方案数为 nn,因为可以选择 nn 个不同的区间(单个元素),但由于 llrr 不能重复,实际方案数需要具体计算。
    • 如果 δi\delta_i 不全相同,且最大值不超过 nn,需要更复杂的组合计算。

方法三:动态规划(DP)

  • 思路: 定义状态表示已选择的 llrr,以及当前的覆盖情况,通过 DP 计算方案数。
  • 挑战: 状态空间可能过大,难以在时间和空间上实现。

6. 选择解法

鉴于题目可能设置了较小的 nn 值(从样例来看),我们可以采用方法一,即暴力搜索结合剪枝。

三、代码实现

def solution(n, w, array):
    from itertools import combinations

    delta = [w - a_i for a_i in array]
    if any(d < 0 for d in delta):
        return 0
    if all(d == 0 for d in delta):
        return 1  # 不需要操作

    max_delta = max(delta)
    total_increments = sum(delta)
    if max_delta > n:
        return 0  # 无法满足操作次数限制

    # 生成所有可能的区间,注意 l 和 r 不能重复
    positions = list(range(1, n + 1))
    intervals = []
    for l in positions:
        for r in positions:
            if l <= r:
                intervals.append((l, r))

    # 由于 l 和 r 不能重复,最多选择 n 个区间
    from collections import defaultdict

    result = set()

    def backtrack(idx, selected_intervals, used_l, used_r, current_delta):
        if idx == len(intervals):
            return
        # 尝试不选择当前区间
        backtrack(idx + 1, selected_intervals, used_l, used_r, current_delta)

        l, r = intervals[idx]
        if l in used_l or r in used_r:
            return
        # 更新 delta
        new_delta = current_delta[:]
        for i in range(l - 1, r):
            new_delta[i] += 1
        # 检查是否超出所需增量
        if any(new_delta[i] > delta[i] for i in range(n)):
            return
        # 添加当前区间
        new_selected_intervals = selected_intervals + [(l, r)]
        new_used_l = used_l | {l}
        new_used_r = used_r | {r}
        # 检查是否完成了所有增量
        if new_delta == delta:
            # 将区间集合转换为不可变类型,方便去重
            intervals_set = frozenset(new_selected_intervals)
            result.add(intervals_set)
            return
        # 继续搜索
        backtrack(idx + 1, new_selected_intervals, new_used_l, new_used_r, new_delta)

    # 初始化 delta
    current_delta = [0] * n
    backtrack(0, [], set(), set(), current_delta)

    return len(result)

四、代码说明

  • delta 计算: 计算每个位置需要增加的次数 δi\delta_i,如果存在负值,直接返回 0。
  • 初始检查:
    • 如果所有 δi=0\delta_i = 0,说明数组已经满足要求,方案数为 1。
    • 如果最大 δi>n\delta_i > n,无法在操作次数限制内完成,返回 0。
  • 区间生成: 生成所有可能的区间 [l,r][l, r],其中 lrl \leq r,并存储在 intervals 列表中。
  • 回溯函数 backtrack
    • 参数说明:
      • idx:当前处理的区间索引。
      • selected_intervals:已选择的区间列表。
      • used_lused_r:已使用的 llrr 值集合。
      • current_delta:当前各位置已增加的次数。
    • 递归逻辑:
      • 不选择当前区间: 直接递归处理下一个区间。
      • 选择当前区间:
        • 检查 llrr 是否未使用。
        • 更新 current_delta,并检查是否超出需要的增量。
        • 如果 current_delta 等于 delta,则找到一个合法方案,加入结果集合。
        • 否则,继续递归处理下一个区间。
  • 结果去重: 使用 frozenset 将区间集合转换为不可变类型,方便在 result 集合中去重。
  • 返回结果: 最终方案数为 result 集合的大小。

五、复杂度分析

  • 时间复杂度: 最坏情况下,回溯算法的时间复杂度为 O(2m)O(2^{m}),其中 mm 是区间数量,约为 n2n^2。由于有剪枝和限制条件,实际运行时间会比理论复杂度小很多。
  • 空间复杂度: 主要取决于递归栈的深度和结果集合的大小,均为多项式级别。

六、总结

这道题考察了在特定限制条件下,如何组合操作来达到目标状态。关键在于:

  • 理解题目限制: 操作的 llrr 必须各不相同。
  • 合理建模: 将问题转换为在所有可能的区间中选择若干个,使其覆盖需求。
  • 算法选择: 根据数据规模,选择合适的算法(本题中采用了回溯法)。
  • 剪枝优化: 在回溯过程中及时剪枝,减少不必要的计算。

通过这道题,我们锻炼了在复杂条件下进行组合计数的能力,以及如何在代码实现中有效地剪枝和去重。