「前缀和」k倍区间

发布时间 2023-07-15 15:02:16作者: 星双子

本题蓝桥OJ第97题的题解(蓝桥OJ上的相同题解也是我发的)

题面

题目描述

给定一个长度为N的数列,\(A_1,A_2,\dots ,A_N\) ,如果其中一段连续的子序列 \(A_i,A_{i+1},\dots,A_j(i\leq j)\) 之和是K的倍数,我们就称这个区间 \([i,j]\) 是K倍区间。

你能求出数列中总共有多少个K倍区间吗?

输入

第一行包含两个整数N和K( \(1\leq N,K \leq 10^5\) )。

以下N行每行包含一个整数 \(A_i\) ( \(1\leq A_i\leq 10^5\) )

输出

输出一个整数,代表K倍区间的数目。

样例输入

5 2
1
2
3
4
5

样例输出

6

思路分析

本题需要求连续子序列和,所以显然是要使用前缀和来快速求和的.但如果直接使用`前缀和``,时间复杂度是 \(O(N^2)\) ,依旧会超时.所以我们需要通过一定的方式去掉其中的一个循环,换句话说,我们要用另一种方式来确定区间的一个端点.

思考时,我想起了力扣上的两数之和问题.这道两数之和通过一个哈希表来记录每个数的数量,以此来去掉一重循环.只需遍历其中一个数,然后每次看需要加的那个数有几个就好了.本题也可以采取相同的方法.

显然,一个区间可以被区间左右界的前缀和(前闭后开)来唯一确定,所以,我们只需要把区间左界的遍历改成在哈希表中查找满足条件的左界有几个即可.那需要满足的条件是什么呢?我们确定的左界需要让整一个区间的和能被K整除,而确定左界实际上就是在从整个序列开头到右界的区间里,减去从整个序列开头到左界(不包括左界)的区间.也即,我们需要在前者的和中减掉一个数,这个数可以让它变成K的倍数.显然,我们只需要减去他对K的余数即可.

所以我们只需要一次遍历,每次遍历时从哈希表中取出前缀和为当前余数的元素的数量,同时更新这个数量(因为当前这个前缀和也满足这一条件了),即可得出所求的总数.

此处K不是很大,因此我使用桶来代替哈希表,还能省一个log的时间.两者的本质是一样的.

参考代码

时间复杂度: \(O(N)\)

空间复杂度: \(O(N)\)

#pragma GCC optimize(1)
#pragma GCC optimize(2)
#pragma GCC optimize(3, "Ofast", "inline")
#include <bits/stdc++.h>

using namespace std;

using i64 = long long;

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    cout.tie(nullptr);

    int n, k;
    cin >> n >> k;

    i64 *pref = new i64[n + 1]; // 前缀和
    pref[0] = 0;

    i64 *cnt = new i64[k]; // 维护每个余数在前面的前缀和中出现的次数
    memset(cnt, 0, sizeof(i64) * k);
    cnt[0] = 1;
    i64 res = 0;

    for (int i = 1; i <= n; i++) {
        cin >> pref[i];
        pref[i] += pref[i - 1]; // 计算前缀和

        res += cnt[pref[i] % k]; // 当前右界对应的满足要求的左界数量,就是前缀和余数和当前相同的元素数量
        cnt[pref[i] % k]++; // 更新具有当前余数的元素的数量
    }
    
    cout << res << "\n";
    delete[] pref;
    return 0;
}

"正是我们每天反复做的事情,最终造就了我们,优秀不是一种行为,而是一种习惯" ---亚里士多德