题目描述
难度分:1406
输入n(1≤n≤2×105)、m(1≤m≤2×105)和长为n的数组a(0≤a[i]≤109)。
定义f(b)=sum(b)%m。输出a的所有非空连续子数组b的f(b)之和,即Σnr=1Σrl=1(s[r]−s[l−1])%m,其中s[i]=Σij=1a[j]。
输入样例1
3 4
2 5 0
输出样例1
10
输入样例2
10 100
320 578 244 604 145 839 156 857 556 400
输出样例2
2736
算法
树状数组
根据目标函数的表达式Σnr=1Σrl=1(s[r]−s[l−1])%m,对于一个固定的子数组右端点,我们维护r=s[i]%m,由于取模满足加法分配率,所以此时我们需要知道前面j∈[1,i]时s[j−1]%m可以取哪些值?以便分类讨论。
构建两个长度为m的树状数组,tr1和tr2,索引i就是“前缀和%m”的余数。对于tr1,索引i上的值是余数为i的“前缀和%m”的累加和;对于tr2,索引i上的值是余数为i的前缀和个数。初始情况下,tr1所有索引的位置都是0,tr2除了tr2[0]=1(空数组前缀和为0),所有其他索引上的值都是0。这样一来,如果当前r=s[i]%m,那么就要知道前面有多少个s[l−1]%m<r,以及这些s[l−1]%m的累加和,假设有cnt1个,累加和为s1;同样的,还需要知道前面有多少个s[l−1]%m>r,以及这些s[l−1]%m的累加和,假设有cnt2个,累加和为s2。cnt1×r−s1可以直接减,但是cnt2×r−s2<0不能直接减,需要对r这个余数+m才行,相当于计算(a−b)%m时,在a≥b时可以直接化为a%m−b%m,在a<b时应该化为a%m−b%m+m。因此i位置对答案的贡献就是cnt1×r−s1+cnt2×(r+m)−s2。
其中s1、s2、cnt1、cnt2四个值都可以通过对tr1和tr2两个树状数组查询得到。计算完i位置的贡献,就把r加在tr1的r位置,1加在tr2的r位置。
复杂度分析
时间复杂度
遍历一遍数组a就可以求得答案,对于每个a[i],需要对树状数组进行查询和修改操作,时间复杂度为O(log2m)。因此,整个算法的时间复杂度为O(nlog2m)。
空间复杂度
空间消耗的瓶颈就在于树状数组,这两个树状数组都是O(m)规模的,因此整个算法的额外空间复杂度为O(m)。
C++ 代码
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
int n, m;
class Fenwick {
public:
explicit Fenwick(int n): sums_(n + 1) {}
int lowbit(int x) {
return x&-x;
}
void add(int idx, int val) {
for(++idx; idx < sums_.size(); idx += lowbit(idx)) {
sums_[idx] += val;
}
}
LL query(int idx) {
LL ans = 0;
for(++idx; idx > 0; idx -= lowbit(idx)) {
ans += sums_[idx];
}
return ans;
}
LL query(int left, int right) {
if(left > right) return 0LL;
return query(right) - query(left - 1);
}
private:
vector<LL> sums_;
};
int main() {
scanf("%d%d", &n, &m);
Fenwick tr1(m), tr2(m);
LL ans = 0, s = 0;
tr2.add(0, 1);
for(int i = 1; i <= n; i++) {
int a;
scanf("%d", &a);
s += a;
LL r = s % m;
LL cnt1 = tr2.query(0, r - 1), cnt2 = tr2.query(r + 1, m - 1);
ans += cnt1 * r - tr1.query(0, r - 1) + cnt2 * (r + m) - tr1.query(r + 1, m - 1);
tr1.add(r, r);
tr2.add(r, 1);
}
printf("%lld\n", ans);
return 0;
}