整除问题与连续子序列和的求解 | 豆包MarsCode AI刷题

98 阅读5分钟

整除问题与连续子序列和的求解

在这篇文章中,我们将解析一个关于连续子序列和整除的经典问题。问题的目标是:给定一个整数序列,小M想知道有多少个连续子序列的和能够被给定的正整数整除。

问题描述

小M写下了一个长度为 n 的正整数序列 a_0, a_1, ..., a_{n-1}。然后他想知道有多少个连续子序列的和能够被一个给定的正整数 b 整除。我们需要设计一个算法来求出这个结果。

输入:

  • 一个长度为 n 的正整数序列 a
  • 一个正整数 b

输出:

  • 输出能够被 b 整除的连续子序列的个数。

思路解析

1. 问题的基本理解

对于给定的数组 a,我们需要找到所有可能的连续子序列并计算其和。然后,我们检查这些和是否能被给定的整数 b 整除。如果能整除,我们就计数。

直接使用暴力方法(双重循环)计算所有连续子序列的和并判断是否能整除,虽然简单,但会存在性能问题,特别是当 n 较大时,暴力解法的时间复杂度是 O(n^2),不适用于较大的输入规模。

2. 优化思路:前缀和与余数性质

为了提高效率,我们可以利用前缀和的思想。前缀和是指从数组的起始位置到某个位置的和。设 prefix[i] 为数组 a0i-1 的和,那么:

sum(i, j) = prefix[j+1] - prefix[i]

其中 sum(i, j) 是从第 i 到第 j 个元素的和。

要判断某个子数组 sum(i, j) 是否能被 b 整除,我们可以通过求余数来简化判断:

sum(i, j) % b == 0

这可以转化为:

(prefix[j+1] - prefix[i]) % b == 0

进一步简化,得到:

prefix[j+1] % b == prefix[i] % b

即,如果前缀和的余数相等,那么对应的子数组和就能被 b 整除。

3. 利用哈希表加速查找

为了实现这一点,我们可以使用哈希表来记录每个前缀和余数出现的次数。遍历每个位置时,计算当前前缀和的余数,并查找此前是否有相同余数的前缀。如果有,说明从该位置到当前的位置之间的子序列和能够被 b 整除。

算法步骤

  1. 初始化一个哈希表 count,用来记录每个前缀和的余数出现的次数。初始时,哈希表中 count[0] = 1,因为一个空的子序列的和可以视为 0,肯定能被任何数整除。
  2. 遍历数组,计算当前前缀和的余数,并查看该余数在哈希表中出现的次数。如果出现过,说明当前子序列能够被 b 整除,将计数加上该余数出现的次数。
  3. 将当前前缀和的余数加入哈希表。

代码实现

from collections import defaultdict

def count_divisible_subarrays(a, b):
    # 哈希表用于记录余数出现次数
    count = defaultdict(int)
    # 初始时,余数0出现了1次(表示空子数组的和)
    count[0] = 1
    
    prefix_sum = 0  # 当前前缀和
    result = 0
    
    # 遍历数组,计算每个前缀和的余数
    for num in a:
        prefix_sum += num  # 更新当前的前缀和
        remainder = prefix_sum % b  # 计算当前前缀和对b的余数
        
        # 如果余数为负数,调整为正数
        if remainder < 0:
            remainder += b
        
        # 如果当前余数在哈希表中出现过,说明有符合条件的子序列
        result += count[remainder]
        
        # 更新哈希表,记录当前余数出现次数
        count[remainder] += 1
    
    return result

# 示例
a = [1, 2, 3, 4, 5]
b = 5
print(count_divisible_subarrays(a, b))  # 输出:7

代码详解

  1. 哈希表 count

    • 哈希表 count 用来记录每个前缀和余数出现的次数。我们初始化 count[0] = 1,表示初始时有一个余数为 0 的前缀和。
  2. 前缀和计算与余数

    • prefix_sum 用来存储当前的前缀和。
    • 对于每个元素,我们计算当前前缀和对 b 的余数 remainder
  3. 查找符合条件的子序列

    • 如果当前余数在 count 中已经出现过,说明之前有一些前缀和的余数与当前余数相等,那么从那些位置到当前的位置之间的子序列和一定能被 b 整除。
  4. 更新哈希表

    • 将当前余数 remainder 更新到哈希表 count 中,表示当前前缀和余数的出现次数。
  5. 返回结果

    • 最后返回符合条件的子序列的个数。

时间复杂度

  • 遍历数组的时间复杂度是 O(n),每个元素的处理都只需要常数时间。
  • 哈希表的操作(查找和更新)平均时间复杂度为 O(1)

因此,整体的时间复杂度是 O(n),比暴力解法的 O(n^2) 要高效得多。

示例

输入:

a = [1, 2, 3, 4, 5]
b = 5

输出:

7

解释:

  • 子序列 [5], [1, 4], [2, 3], [1, 2, 3], [3, 2], [4, 1], [5] 都能被 5 整除。

总结

通过利用前缀和与余数的性质,并结合哈希表,我们能够高效地解决这个连续子序列和整除的问题。这种方法不仅优化了时间复杂度,而且思路简洁,适用于各种类似的连续子数组和求解问题。在实际应用中,这种优化方法可以大大提高程序的运行效率。