DAY 2 青训营笔记:连续子串和的整除问题 | 豆包MarsCode AI刷题

115 阅读7分钟

学习心得

一、问题解析

1. 连续子序列和的整除性

在数学与算法中,子序列子数组是两个常见的概念。具体来说,“连续子序列”或“连续子数组”是指数组中按照位置连续的元素。例如,给定一个数组 [1, 2, 3, 4, 5],它的连续子数组包括 [1][1, 2][2, 3, 4][3, 4, 5],以及整个数组 [1, 2, 3, 4, 5] 等。我们的任务是计算这些子数组的和,检查它们是否能够被某个整数 b 整除。

对于这个问题,我们需要关注以下两个要点:

  • 子数组的和:对于一个子数组的和,我们可以通过计算前缀和来高效地获得其和。前缀和数组 prefix[i] 记录的是数组从第一个元素到第 i 个元素的和。这使得我们可以在常数时间内计算任意子数组的和。

  • 整除性:检查子数组的和是否能被 b 整除,可以通过余数(取模)来判断。一个子数组的和能够被 b 整除,当且仅当该子数组和除以 b 的余数为零。

2. 算法思路

对于本问题,传统的暴力解法是枚举所有可能的子数组,计算其和并判断是否能整除。这种方法的时间复杂度为 O(n²),其中 n 是数组的长度。显然,这对于较大的数据规模是不可行的。为了提高效率,我们可以采用更高效的算法来解决这个问题。

前缀和取模是解决该问题的关键。具体来说,我们可以通过以下步骤来优化解法:

  1. 计算前缀和:定义一个前缀和数组 prefix_sum[i],其中 prefix_sum[i] 记录的是数组从第一个元素到第 i 个元素的和。通过前缀和,我们可以在常数时间内计算任何子数组的和。

  2. 取余运算:我们关心的是前缀和对 b 的余数。对于任何两个前缀和 prefix[i]prefix[j],如果它们的余数相同,则从 i+1j 之间的子数组的和能够被 b 整除。也就是说,我们只需要记录前缀和对 b 的余数出现的次数,利用余数的频率来判断有多少子数组的和是可以被 b 整除的。

  3. 频率计数:使用一个数组或哈希表来记录前缀和的余数出现次数。对于每一个新计算出来的前缀和余数 mod,如果该余数已经出现过,那么从上次该余数出现的位置到当前的位置之间的子数组和能被 b 整除。

通过这种方式,我们可以将问题的时间复杂度从 O(n²) 降低到 O(n),这是一个显著的优化。

二、算法实现

以下是基于前缀和与取余的算法实现:

def count_divisible_subarrays(arr, b):
    n = len(arr)
    prefix_sum = 0
    count = 0
    mod_count = [0] * b
    mod_count[0] = 1  # 处理前缀和为0的情况

    for num in arr:
        prefix_sum += num
        mod = prefix_sum % b
        # 处理负数情况
        if mod < 0:
            mod += b
        
        # 增加当前余数出现的次数
        count += mod_count[mod]
        mod_count[mod] += 1

    return count

# 示例
arr = [1, 2, 3, 4, 5]
b = 3
print(count_divisible_subarrays(arr, b))  # 输出符合条件的子序列个数

代码分析

  • prefix_sum:变量 prefix_sum 用于存储当前遍历到的前缀和。
  • mod_count:数组 mod_count 用于存储每个余数(0 到 b-1)出现的次数。初始化时,mod_count[0] 设为 1,是因为前缀和为 0 时,子数组的和就是当前的前缀和,这样可以处理从头开始的子数组。
  • 遍历数组:在遍历数组时,首先更新前缀和 prefix_sum,然后计算 prefix_sum % b,并根据该余数更新计数器 mod_count。如果该余数已经出现过,表示从上次出现该余数的位置到当前位置之间的子数组和能够被 b 整除,累加计数。

时间与空间复杂度

  • 时间复杂度:O(n),因为我们只遍历一次数组,进行常数时间的操作。
  • 空间复杂度:O(b),用于存储余数的计数数组。对于较小的 b,这是一个常数空间的操作。

这种算法相较于暴力解法,具有显著的时间效率优势,能够在较大的数据规模下快速求解。

三、知识总结

通过解决这一问题,我学习到了以下几个重要的编程技巧和算法思想:

  1. 前缀和的应用:前缀和是一个非常有效的技巧,尤其在需要频繁计算子数组和时。通过前缀和,我们可以将问题从 O(n²) 降到 O(n),这对于大规模数据处理至关重要。

  2. 取模运算:取模操作是处理整除性问题的关键。通过计算前缀和对某个数的余数,我们可以将问题转化为一个频率计数问题,这大大简化了问题的复杂度。

  3. 哈希表的应用:使用数组或哈希表记录频率是很多算法中常见的优化技巧。在这个问题中,我们利用哈希表记录前缀和的余数频率,避免了暴力枚举每一个子数组,从而提高了算法效率。

  4. 空间复杂度优化:尽管我们使用了一个额外的数组 mod_count 来存储余数的频率,但是该数组的大小为 b,对于大部分实际问题来说,这个空间消耗是可以接受的,且时间复杂度仅为 O(n)。

四、学习计划

为了更深入地理解这个问题并拓宽我的算法知识,我计划进行如下学习:

  1. 多做相关题目:继续通过在线编程平台进行类似题目的练习,尝试更多的变种问题。例如,改进版的题目可能会要求计算子数组和的最大值或最小值,或者要求处理不同的约束条件。

  2. 学习复杂度分析:对于每一个解法,都进行时间与空间复杂度的分析,学习如何优化算法的性能。特别是对于一些高级数据结构(如平衡树、堆、哈希表等)的运用,如何将问题从暴力解法的 O(n²) 提升到 O(n log n) 或 O(n) 的复杂度。

  3. 复习取余及模运算:深入研究取余运算的性质,尤其是在大数算法、数论算法等领域的应用。例如,如何处理大数模运算,如何高效地计算大整数的余数等。

  4. 模拟题目和代码实现:通过编写代码来模拟题目情境,逐步提升自己对代码逻辑的理解和编程能力。尝试从简单的题目逐步挑战更复杂的问题。

  5. 算法与数据结构的结合:在理解和实现前缀和、哈希表等基础算法技巧的基础上,结合其他数据结构(如树、图、堆等)解决更高难度的问题,提高解决复杂问题的能力。

五、豆包AI刷题功能的运用

豆包AI的刷题功能能够帮助我快速找到相关题目进行练习,以下是我打算如何利用这一工具进行进一步学习:

  1. 搜索相似题目:通过豆包AI的搜索功能,寻找与“连续子序列和的整除性”相关的题目,以增加练习的数量。比如,搜索关键字“连续子数组”和“子数组和”来扩展我的解题经验。

  2. 查看解题思路:通过查看豆包AI的解题思路,了解不同的解法。例如,可能会有一些巧妙的数学推导或更优的算法思路,可以从中获得启发。

  3. 总结与复习:每次做完一道题,都要去总结。