题目描述
给定一个正整数序列,统计其中所有连续子序列(有时也称为子串,与子序列不同,子序列可以跳过某些元素)的和能被给定整数整除的数量。这些子序列可以重叠。
例如,序列:
2,1,2,1,1,2,1,22, 1, 2, 1, 1, 2, 1, 22,1,2,1,1,2,1,2
包含 666 个和能被 444 整除的连续子序列:
- 第一个到第八个数
- 第二个到第四个数
- 第二个到第七个数
- 第三个到第五个数
- 第四个到第六个数
- 第五个到第七个数
输入格式
第一行是一个整数 ccc (1≤c≤2001 \leq c \leq 2001≤c≤200),表示测试用例的数量。
每个测试用例包含两行:
- 第一行是两个整数 ddd (1≤d≤1,000,0001 \leq d \leq 1,000,0001≤d≤1,000,000) 和 nnn (1≤n≤50,0001 \leq n \leq 50,0001≤n≤50,000),分别表示用于整除的除数和序列的长度
- 第二行是序列的元素,每个元素是介于 111 到 1,000,000,0001,000,000,0001,000,000,000 之间的整数
输出格式
对于每个测试用例,输出一个整数,表示满足条件的连续子序列的数量。
题目分析
问题理解
我们需要找到所有连续子序列(子数组),使得这些子序列的元素和能够被给定的除数 ddd 整除。
最直观的解法是枚举所有可能的连续子序列,计算它们的和,然后检查是否能被 ddd 整除。然而,这种方法的时间复杂度为 O(n2)O(n^2)O(n2),对于 nnn 最大为 50,00050,00050,000 的情况来说是不可行的。
关键思路
利用前缀和和同余定理来优化:
- 前缀和定义:设 prefixSum[i]prefixSum[i]prefixSum[i] 表示前 iii 个元素的和
- 子序列和:子序列 [i,j][i, j][i,j] 的和可以表示为 prefixSum[j]−prefixSum[i−1]prefixSum[j] - prefixSum[i-1]prefixSum[j]−prefixSum[i−1]
- 整除条件:如果子序列 [i,j][i, j][i,j] 的和能被 ddd 整除,那么:
(prefixSum[j]−prefixSum[i−1])%d=0(prefixSum[j] - prefixSum[i-1]) \% d = 0(prefixSum[j]−prefixSum[i−1])%d=0
根据同余定理,这等价于:
prefixSum[j]%d=prefixSum[i−1]%dprefixSum[j] \% d = prefixSum[i-1] \% dprefixSum[j]%d=prefixSum[i−1]%d
算法设计
基于上述观察,我们可以设计如下算法:
- 计算前缀和数组,但不需要存储所有前缀和,只需要维护当前前缀和
- 使用一个计数数组记录每个余数出现的次数
- 特别地,初始时 count[0]=1count[0] = 1count[0]=1,表示空前缀(和为 000,余数为 000)
- 遍历序列,对于每个位置:
- 更新当前前缀和
- 计算当前前缀和模 ddd 的余数
- 增加对应余数的计数
- 最后,对于每个余数 rrr,如果有 kkk 个前缀和的余数为 rrr,那么这些前缀和中任意两个之间的子序列都满足条件,数量为组合数 C(k,2)=k(k−1)2C(k, 2) = \frac{k(k-1)}{2}C(k,2)=2k(k−1)
为什么这样工作?
- 当两个前缀和 prefixSum[i]prefixSum[i]prefixSum[i] 和 prefixSum[j]prefixSum[j]prefixSum[j] (i<ji < ji<j) 的余数相同时,它们之间的子序列 [i+1,j][i+1, j][i+1,j] 的和 prefixSum[j]−prefixSum[i]prefixSum[j] - prefixSum[i]prefixSum[j]−prefixSum[i] 必然能被 ddd 整除
- 初始的 count[0]=1count[0] = 1count[0]=1 保证了从序列开头开始的子序列也能被正确计数
时间复杂度
- 每个测试用例的时间复杂度:O(n)O(n)O(n)
- 总体时间复杂度:O(c×n)O(c \times n)O(c×n)
- 空间复杂度:O(d)O(d)O(d)
这完全在题目约束范围内。
代码实现
// Divisible Subsequences
// UVa ID: 12220
// Verdict: Accepted
// Submission Date: 2025-11-29
// UVa Run Time: 0.150s
//
// 版权所有(C)2025,邱秋。metaphysis # yeah dot net
#include <bits/stdc++.h>
using namespace std;
int main() {
int testCaseCount;
cin >> testCaseCount;
while (testCaseCount--) {
int divisor, sequenceLength;
cin >> divisor >> sequenceLength;
vector<long long> prefixModCount(divisor, 0);
prefixModCount[0] = 1; // 初始前缀和 0 的余数为 0
long long prefixSum = 0;
for (int i = 0; i < sequenceLength; i++) {
long long num;
cin >> num;
prefixSum += num;
int remainder = prefixSum % divisor;
prefixModCount[remainder]++;
}
long long result = 0;
for (int i = 0; i < divisor; i++) {
long long count = prefixModCount[i];
result += count * (count - 1) / 2; // 组合数 C(count, 2)
}
cout << result << endl;
}
return 0;
}
总结
本题的关键在于将问题转化为寻找余数相同的前缀和对。通过利用前缀和和同余定理,我们将原本 O(n2)O(n^2)O(n2) 的暴力解法优化到了 O(n)O(n)O(n) 的高效解法。这种技巧在处理与子序列和相关的问题时非常有用,特别是在涉及整除条件的情况下。

1601

被折叠的 条评论
为什么被折叠?



