别再暴力枚举了!用‘双因子前缀和+二分’优雅解决LeetCode风格问题:以乘积尾随零为例
2026/6/2 3:53:31 网站建设 项目流程

双因子前缀和与二分查找:优雅解决乘积尾随零问题

在算法竞赛和编程面试中,我们经常会遇到需要统计满足特定条件的子区间数量的问题。这类问题看似简单,但如果采用暴力枚举的方法,时间复杂度往往难以接受。本文将以"乘积尾随零"这一经典问题为例,介绍如何利用双因子前缀和二分查找的组合技巧,将时间复杂度从O(n²)优化到O(nlogn)。

1. 问题本质与数学建模

乘积尾随零的数量由乘数中2和5因子的最小值决定。例如,数字20可以分解为2²×5¹,因此贡献2个2因子和1个5因子。整个数组乘积的尾随零数量等于所有元素中2因子总数和5因子总数的较小值。

关键数学转化

  • 设原数组总2因子数为total2,总5因子数为total5
  • 删除区间中的2因子数为x2,5因子数为x5
  • 剩余部分需要满足:min(total2-x2, total5-x5) ≥ k
  • 这等价于两个独立不等式:x2 ≤ total2-k 且 x5 ≤ total5-k
def count_factors(num, factor): count = 0 while num % factor == 0 and num != 0: count += 1 num //= factor return count

2. 暴力解法的局限性

最直观的方法是枚举所有可能的删除区间,然后检查是否满足条件:

def brute_force(nums, k): n = len(nums) factor2 = [count_factors(num, 2) for num in nums] factor5 = [count_factors(num, 5) for num in nums] total2, total5 = sum(factor2), sum(factor5) res = 0 for i in range(n): curr2 = curr5 = 0 for j in range(i, n): curr2 += factor2[j] curr5 += factor5[j] if curr2 <= total2 - k and curr5 <= total5 - k: res += 1 else: break return res

这种方法的时间复杂度为O(n²),当n=1e5时完全无法接受。我们需要更高效的解决方案。

3. 前缀和与二分查找的优雅结合

3.1 前缀和预处理

首先,我们计算2因子和5因子的前缀和数组:

from itertools import accumulate from bisect import bisect_right def preprocess(nums): factor2 = [count_factors(num, 2) for num in nums] factor5 = [count_factors(num, 5) for num in nums] prefix2 = [0] + list(accumulate(factor2)) prefix5 = [0] + list(accumulate(factor5)) return prefix2, prefix5

3.2 双约束条件的转化

对于每个左端点i,我们需要找到最大的j,使得:

  • prefix2[j] - prefix2[i] ≤ total2 - k
  • prefix5[j] - prefix5[i] ≤ total5 - k

这可以转化为:

  • prefix2[j] ≤ prefix2[i] + (total2 - k)
  • prefix5[j] ≤ prefix5[i] + (total5 - k)

3.3 二分查找确定边界

使用bisect_right可以高效找到满足条件的最大j:

def solve(nums, k): prefix2, prefix5 = preprocess(nums) total2, total5 = prefix2[-1], prefix5[-1] n = len(nums) res = 0 for i in range(n+1): max2 = prefix2[i] + (total2 - k) max5 = prefix5[i] + (total5 - k) j2 = bisect_right(prefix2, max2) - 1 j5 = bisect_right(prefix5, max5) - 1 valid_j = min(j2, j5) if valid_j > i: res += valid_j - i return res

4. 算法复杂度分析

  • 时间复杂度

    • 预处理阶段:O(n)计算因子数量,O(n)构建前缀和数组
    • 查询阶段:n次二分查找,每次O(logn)
    • 总复杂度:O(nlogn)
  • 空间复杂度

    • 需要存储两个前缀和数组:O(n)

5. 模式扩展与应用

这种"双因子前缀和+二分查找"的技术可以推广到多种类似问题:

  1. 双约束子区间计数:任何需要同时满足两个独立条件的子区间统计问题
  2. 多因子扩展:如果问题需要考虑更多质因子,方法依然适用
  3. 其他运算:将乘积替换为加法、异或等运算,只要满足前缀和性质

对比表格

方法时间复杂度适用场景实现难度
暴力枚举O(n²)小规模数据简单
滑动窗口O(n)单约束条件中等
前缀和+二分O(nlogn)多约束条件较高

6. 实战技巧与优化建议

  1. 预处理优化:可以并行计算2和5因子,减少循环次数
  2. 边界处理:特别注意前缀和数组的初始0值
  3. 二分查找选择
    • bisect_right:返回第一个大于目标的位置
    • bisect_left:返回第一个大于等于目标的位置
  4. 提前终止:如果total2或total5小于k,直接返回0
# 优化后的完整解法 def optimized_solution(nums, k): n = len(nums) # 并行计算因子 factor2 = [0]*n factor5 = [0]*n for i in range(n): num = nums[i] while num % 2 == 0 and num != 0: factor2[i] += 1 num //= 2 while num % 5 == 0 and num != 0: factor5[i] += 1 num //= 5 # 计算前缀和 prefix2 = [0]*(n+1) prefix5 = [0]*(n+1) for i in range(n): prefix2[i+1] = prefix2[i] + factor2[i] prefix5[i+1] = prefix5[i] + factor5[i] total2, total5 = prefix2[-1], prefix5[-1] if min(total2, total5) < k: return 0 res = 0 for i in range(n+1): max2 = prefix2[i] + (total2 - k) max5 = prefix5[i] + (total5 - k) j2 = bisect_right(prefix2, max2) - 1 j5 = bisect_right(prefix5, max5) - 1 valid_j = min(j2, j5) if valid_j > i: res += valid_j - i return res

7. 常见错误与调试技巧

  1. 边界条件处理不当

    • 忘记前缀和数组的初始0
    • 没有处理total2或total5小于k的情况
  2. 二分查找使用错误

    • 混淆bisect_left和bisect_right
    • 忘记调整返回的索引
  3. 因子计算错误

    • 没有处理num为0的情况
    • 因子计数循环条件错误

调试建议

  • 先用小规模数据验证
  • 打印中间变量检查
  • 对比暴力解法的结果

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询