2. 题意简述 (Problem Summary)
给定正整数 N N N 和长度-N N N 的整数序列 A = ( A 1 , A 2 , … , A N ) A=(A_1,A_2,\dots,A_N) A = ( A 1 , A 2 , … , A N ) ,定义子数组 A l , A l + 1 , … , A r A_l,A_{l+1},\dots,A_r A l , A l + 1 , … , A r 的算术平均值为 f ( l , r ) = 1 r − l + 1 ∑ i = l r A i f(l,r)=\frac{1}{r-l+1}\sum_{i=l}^{r}A_i f ( l , r ) = r − l + 1 1 ∑ i = l r A i 。要求计算
Ans = ∑ 1 ≤ l ≤ r ≤ N f ( l , r ) ( m o d 998244353 ) \text{Ans}=\sum_{1\le l\le r\le N} f(l,r) \pmod{998244353}
Ans = 1 ≤ l ≤ r ≤ N ∑ f ( l , r ) ( m o d 9 9 8 2 4 4 3 5 3 )
其中模 998244353 998244353 9 9 8 2 4 4 3 5 3 下的分数 P Q \frac{P}{Q} Q P (Q ≢ 0 ( m o d 998244353 ) Q\not\equiv0\pmod{998244353} Q ≡ 0 ( m o d 9 9 8 2 4 4 3 5 3 ) )转化为 P ⋅ Q − 1 ( m o d 998244353 ) P\cdot Q^{-1}\pmod{998244353} P ⋅ Q − 1 ( m o d 9 9 8 2 4 4 3 5 3 ) 输出,其中 Q − 1 Q^{-1} Q − 1 表示在模意义下的逆元 。1 ≤ N ≤ 5 × 1 0 5 1\le N\le 5\times10^5 1 ≤ N ≤ 5 × 1 0 5 ,0 ≤ A i < 998244353 0\le A_i<998244353 0 ≤ A i < 9 9 8 2 4 4 3 5 3 。
3. 朴素解法 (Brute-Force)
枚举所有 O ( N 2 ) O(N^2) O ( N 2 ) 个子数组,对每个子数组计算平均值并累加。N = 5 × 1 0 5 N=5\times10^5 N = 5 × 1 0 5 时子数组数量级为 1 0 11 10^{11} 1 0 1 1 ,完全无法在规定时间内完成。
瓶颈在于重复计算平均值的分母。所有子数组的平均值之和可以交换求和顺序,拆分为每个元素的独立贡献,从而将问题降维。
4. 核心解法 (Main Solution)
特殊性质
平均值之和可以交换求和顺序,拆分为每个元素的独立贡献。位置 i i i 的贡献仅由 i i i 与 N N N 的相对关系决定,与 A i A_i A i 的具体值无关。通过逆元,平均数求和操作可以转化为对取模有分配律的加法和乘法。
关键突破
从 O ( N 2 ) O(N^2) O ( N 2 ) 的瓶颈出发,利用"每个元素的贡献可分离"这一性质,将问题转化为对每个位置计算一个仅依赖下标的系数 W i W_i W i ,最终答案为 Ans = ∑ i = 1 N A i ⋅ W i \text{Ans}=\sum_{i=1}^N A_i\cdot W_i Ans = ∑ i = 1 N A i ⋅ W i 。
推导过程
第 1 步:枚举区间长度,合并分子求和。
对每个固定的长度 k k k ,先计算所有长度为 k k k 的子数组的分子之和,再乘以 k − 1 k^{-1} k − 1 得到该长度下所有子数组的平均值之和:
1 k res ( k ) = 1 k ∑ l = 1 N − k + 1 ∑ i = l l + k − 1 A i \frac1k\text{res}(k)=\frac{1}{k}\sum_{l=1}^{N-k+1}\sum_{i=l}^{l+k-1}A_i
k 1 res ( k ) = k 1 l = 1 ∑ N − k + 1 i = l ∑ l + k − 1 A i
第 2 步:固定位置 i i i ,分析包含它的子数组。 对固定的 i i i 与 k k k ,满足 l ≤ i ≤ l + k − 1 l\le i\le l+k-1 l ≤ i ≤ l + k − 1 的左端点个数为
c n t ( i , k ) = min ( k , i , N − i + 1 , N − k + 1 ) cnt(i,k)=\min(k,\;i,\;N-i+1,\;N-k+1)
c n t ( i , k ) = min ( k , i , N − i + 1 , N − k + 1 )
该公式可由区间 l ∈ [ max ( 1 , i − k + 1 ) , min ( i , N − k + 1 ) ] l\in[\max(1,i-k+1),\,\min(i,N-k+1)] l ∈ [ max ( 1 , i − k + 1 ) , min ( i , N − k + 1 ) ] 的长度直接得到。
交换求和顺序,固定位置 i i i ,统计 i i i 在多少个长度为 k k k 的子数组中出现:
1 k ∑ i = 1 N A i ⋅ min ( k , i , N − i + 1 , N − k + 1 ) \frac{1}{k}\sum_{i=1}^{N}A_i\cdot \min(k,i,N-i+1,N-k+1)
k 1 i = 1 ∑ N A i ⋅ min ( k , i , N − i + 1 , N − k + 1 )
对所有 k k k 累加即得最终答案:
Ans = ∑ k = 1 N 1 k ∑ i = 1 N A i ⋅ min ( k , i , N − i + 1 , N − k + 1 ) \text{Ans}=\sum_{k=1}^{N}\frac{1}{k}\sum_{i=1}^{N}A_i\cdot \min(k,i,N-i+1,N-k+1)
Ans = k = 1 ∑ N k 1 i = 1 ∑ N A i ⋅ min ( k , i , N − i + 1 , N − k + 1 )
再交换内外层求和,拆出每个位置 i i i 的独立贡献 A i ⋅ W i A_i\cdot W_i A i ⋅ W i :
Ans = ∑ i = 1 N A i ⋅ ∑ k = 1 N min ( k , i , N − i + 1 , N − k + 1 ) k \text{Ans}=\sum_{i=1}^{N}A_i\cdot \sum_{k=1}^{N}\frac{\min(k,i,N-i+1,N-k+1)}{k}
Ans = i = 1 ∑ N A i ⋅ k = 1 ∑ N k min ( k , i , N − i + 1 , N − k + 1 )
第 3 步:定义系数 W i W_i W i 。
W i = ∑ k = 1 N c n t ( i , k ) k W_i=\sum_{k=1}^{N}\frac{cnt(i,k)}{k}
W i = k = 1 ∑ N k c n t ( i , k )
第 4 步:对称性简化。 注意到 W i = W N − i + 1 W_i=W_{N-i+1} W i = W N − i + 1 ,且 c n t ( i , k ) cnt(i,k) c n t ( i , k ) 关于 i i i 与 k k k 均呈"三段线性"结构。通过枚举 k k k ,令 t p = min ( k , N − k + 1 ) tp=\min(k,N-k+1) t p = min ( k , N − k + 1 ) ,可将 W i W_i W i 的求和转化为
W i = ∑ k = 1 N min ( i , N − i + 1 , t p , N − t p + 1 ) k W_i=\sum_{k=1}^{N}\frac{\min(i,N-i+1,\,tp,\,N-tp+1)}{k}
W i = k = 1 ∑ N k min ( i , N − i + 1 , t p , N − t p + 1 )
第 5 步:前缀和优化。 对枚举变量 k k k ,分子部分 ∑ j = 1 N min ( j , N − j + 1 , t p , N − t p + 1 ) ⋅ A j \sum_{j=1}^{N}\min(j,N-j+1,tp,N-tp+1)\cdot A_j ∑ j = 1 N min ( j , N − j + 1 , t p , N − t p + 1 ) ⋅ A j 可按三段拆分为:
左段 [ 1 , t p ] [1,tp] [ 1 , t p ] :系数为 j j j ,即 ∑ j = 1 t p j ⋅ A j \sum_{j=1}^{tp}j\cdot A_j ∑ j = 1 t p j ⋅ A j
中段 [ t p + 1 , N − t p ] [tp+1,N-tp] [ t p + 1 , N − t p ] :系数为 t p tp t p ,即 t p ⋅ ∑ j = t p + 1 N − t p A j tp\cdot\sum_{j=tp+1}^{N-tp}A_j t p ⋅ ∑ j = t p + 1 N − t p A j
右段 [ N − t p + 1 , N ] [N-tp+1,N] [ N − t p + 1 , N ] :系数为 N − j + 1 N-j+1 N − j + 1 ,即 ∑ j = N − t p + 1 N ( N − j + 1 ) ⋅ A j \sum_{j=N-tp+1}^{N}(N-j+1)\cdot A_j ∑ j = N − t p + 1 N ( N − j + 1 ) ⋅ A j
预处理三个前缀和数组 f f f (普通前缀和)、g g g (左段加权和)、h h h (右段加权和),即可在 O ( 1 ) O(1) O ( 1 ) 内算出当前 k k k 对应的分子值 res ( k ) \text{res}(k) res ( k ) 。对于 ∑ j = 1 t p j ⋅ A j \sum_{j=1}^{tp}j\cdot A_j ∑ j = 1 t p j ⋅ A j 这种加权和,我们在使用差分树状数组实现区间修改区间查询时见过,详见:【数据结构】树状数组 学习笔记 。最终答案:
Ans = ∑ k = 1 N res ( k ) ⋅ k − 1 ( m o d 998244353 ) \text{Ans}=\sum_{k=1}^{N}\text{res}(k)\cdot k^{-1}\pmod{998244353}
Ans = k = 1 ∑ N res ( k ) ⋅ k − 1 ( m o d 9 9 8 2 4 4 3 5 3 )
5. 正确性证明 (Proof of Correctness)
交换求和顺序 :所有项均为有限值,且模意义下分母 Q ≢ 0 ( m o d 998244353 ) Q\not\equiv0\pmod{998244353} Q ≡ 0 ( m o d 9 9 8 2 4 4 3 5 3 ) ,故逆元存在。由求和交换律,先对 i i i 求和或先对 l , r l,r l , r 求和结果相同。
c n t ( i , k ) cnt(i,k) c n t ( i , k ) 公式 :对固定 i , k i,k i , k ,左端点 l l l 需满足 l ≤ i ≤ l + k − 1 l\le i\le l+k-1 l ≤ i ≤ l + k − 1 且 1 ≤ l ≤ N − k + 1 1\le l\le N-k+1 1 ≤ l ≤ N − k + 1 ,即 l ∈ [ max ( 1 , i − k + 1 ) , min ( i , N − k + 1 ) ] l\in[\max(1,i-k+1),\,\min(i,N-k+1)] l ∈ [ max ( 1 , i − k + 1 ) , min ( i , N − k + 1 ) ] 。该区间长度恰为 min ( k , i , N − i + 1 , N − k + 1 ) \min(k,i,N-i+1,N-k+1) min ( k , i , N − i + 1 , N − k + 1 ) ,证毕。
前缀和公式 :对枚举变量 k k k ,令 t p = min ( k , N − k + 1 ) tp=\min(k,N-k+1) t p = min ( k , N − k + 1 ) 。将数组按 t p tp t p 分为左、中、右三段,各段系数分别为下标 j j j 、t p tp t p 、N − j + 1 N-j+1 N − j + 1 。因此 res ( k ) = ∑ j = 1 N c j ⋅ A j \text{res}(k)=\sum_{j=1}^{N}c_j\cdot A_j res ( k ) = ∑ j = 1 N c j ⋅ A j ,其中
c j = { j j ≤ t p t p t p < j ≤ N − t p N − j + 1 j > N − t p c_j=\begin{cases}j & j\le tp\\tp & tp<j\le N-tp\\N-j+1 & j>N-tp\end{cases}
c j = ⎩ ⎪ ⎪ ⎨ ⎪ ⎪ ⎧ j t p N − j + 1 j ≤ t p t p < j ≤ N − t p j > N − t p
该系数恰好等于 min ( j , N − j + 1 , t p , N − t p + 1 ) \min(j,N-j+1,tp,N-tp+1) min ( j , N − j + 1 , t p , N − t p + 1 ) 。预处理 g g g 与 h h h 后,res ( k ) \text{res}(k) res ( k ) 的每一段均可 O ( 1 ) O(1) O ( 1 ) 合并。综上所述,整个算法正确。
通俗地,每个位置 i i i 的贡献权重 W i W_i W i 可以看作"以 i i i 为中心的对称衰减",而前缀和 g g g 和 h h h 分别维护了从左到右和从右到左的加权累积,使得每一轮 k k k 的分子计算只需三次数组访问。
6. 复杂度分析 (Complexity)
时间复杂度 :预处理三个前缀和数组 O ( N ) O(N) O ( N ) 。主循环 N N N 次,每次需 O ( log M O D ) O(\log MOD) O ( log M O D ) 计算模逆元(快速幂),总计 O ( N log M O D ) O(N \log MOD) O ( N log M O D ) 。log 2 998244353 ≈ 30 \log_2 998244353 \approx 30 log 2 9 9 8 2 4 4 3 5 3 ≈ 3 0 ,N = 5 × 1 0 5 N=5\times10^5 N = 5 × 1 0 5 时约 1.5 × 1 0 7 1.5\times10^7 1 . 5 × 1 0 7 次运算,可轻松通过。若预处理逆元数组,可降至严格 O ( N ) O(N) O ( N ) 。
空间复杂度 :O ( N ) O(N) O ( N ) 。存储原数组及三个前缀和数组,每个数组长度 N + 5 N+5 N + 5 ,总计约 4 × 5 × 1 0 5 × 8 bytes ≈ 16 MB 4\times 5\times10^5\times 8\text{ bytes}\approx 16\text{ MB} 4 × 5 × 1 0 5 × 8 bytes ≈ 1 6 MB ,远低于内存限制。
7. 实现细节与避坑指南 (Implementation Details)
模逆元 :使用快速幂计算 k − 1 ≡ k 998244353 − 2 ( m o d 998244353 ) k^{-1}\equiv k^{998244353-2}\pmod{998244353} k − 1 ≡ k 9 9 8 2 4 4 3 5 3 − 2 ( m o d 9 9 8 2 4 4 3 5 3 ) 。998244353 998244353 9 9 8 2 4 4 3 5 3 是素数,逆元恒存在。
整数范围 :f f f 数组存储真实前缀和(未取模),其差值 f [ N − t p ] − f [ t p ] f[N-tp]-f[tp] f [ N − t p ] − f [ t p ] 最大约为 N ⋅ 998244353 ≈ 5 × 1 0 14 N\cdot 998244353\approx 5\times10^{14} N ⋅ 9 9 8 2 4 4 3 5 3 ≈ 5 × 1 0 1 4 ,取模后小于 998244353 998244353 9 9 8 2 4 4 3 5 3 ,再乘以 t p ≤ 5 × 1 0 5 tp\le 5\times10^5 t p ≤ 5 × 1 0 5 ,乘积仍在 64 64 6 4 位整数范围内,不会溢出。
非负性 :由于 t p = min ( k , N − k + 1 ) tp=\min(k,N-k+1) t p = min ( k , N − k + 1 ) ,总有 N − t p ≥ t p N-tp\ge tp N − t p ≥ t p ,故 f [ N − t p ] − f [ t p ] ≥ 0 f[N-tp]-f[tp]\ge0 f [ N − t p ] − f [ t p ] ≥ 0 ,无需额外处理负数取模问题。这也是我对原始前缀和不取模,并计算这个 t p tp t p 的原因。
奇偶分类 :
N N N 为偶数:中间位置不存在,所有 k k k 统一使用左段+右段+中段公式;当 t p = N / 2 tp=N/2 t p = N / 2 时中段为空,跳过中间段累加。
N N N 为奇数:中间位置 m i d = ( N + 1 ) / 2 mid=(N+1)/2 m i d = ( N + 1 ) / 2 需要特殊处理,因为此时中段为空,需拆分为左段前缀 g [ m i d − 1 ] g[mid-1] g [ m i d − 1 ] + 右段后缀 h [ m i d + 1 ] h[mid+1] h [ m i d + 1 ] + 中间元素 A m i d ⋅ m i d A_{mid}\cdot mid A m i d ⋅ m i d 。
下标习惯 :数组下标从 1 1 1 开始,与题目描述一致,避免 0 0 0 下标带来的边界混淆。
坑点
说明
模逆元计算
快速幂每次 O ( log M O D ) O(\log MOD) O ( log M O D ) ,主循环 N N N 次,总复杂度 O ( N log M O D ) O(N \log MOD) O ( N log M O D ) 。log 2 998244353 ≈ 30 \log_2 998244353 \approx 30 log 2 9 9 8 2 4 4 3 5 3 ≈ 3 0 ,N = 5 × 1 0 5 N=5\times10^5 N = 5 × 1 0 5 时约 1.5 × 1 0 7 1.5\times10^7 1 . 5 × 1 0 7 次运算,可轻松通过。若需严格 O ( N ) O(N) O ( N ) ,可预处理逆元数组 inv[i] = mod - mod/i * inv[mod%i] % mod(i = 1.. N i=1..N i = 1 . . N ),O ( N ) O(N) O ( N ) 初始化后每次 O ( 1 ) O(1) O ( 1 ) 查询。
中间位置处理
N N N 为奇数时,t p = ( N + 1 ) / 2 tp=(N+1)/2 t p = ( N + 1 ) / 2 对应中间元素,此时中段 [ t p + 1 , N − t p ] [tp+1,N-tp] [ t p + 1 , N − t p ] 为空。代码中通过 if (tp != (n+1)/2) 分支避免访问空区间,否则会重复计入中间元素。
负数取模
f[n-tp]-f[tp] 理论上非负,但 C++ 中 % 对负数的行为取决于编译器。为保险起见,可以在参考代码中加了 if (ans < 0) ans += MOD; 和 (pref[n - tp] - pref[tp] + MOD) % MOD。实际上由于 n − t p ≥ t p n-tp \ge tp n − t p ≥ t p ,该差值始终非负,不加也正确。
8. 参考代码 (Reference Code)
基本上是我最早实现的赛时代码,采用「前缀和 f , g , h f,g,h f , g , h + 分类讨论 N N N 奇偶」的思路。思路完全正确,已通过 AtCoder 评测。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 #include <bits/stdc++.h> using namespace std;#define int long long const int N = 5e5 + 5 , mod = 998244353 ;int n;int a[N], f[N], g[N], h[N];int ans = 0 ;int qmi (int p, int r) { int res = 1 ; while (r) { if (r & 1 ) res = res * p % mod; p = p * p % mod; r >>= 1 ; } return res; } signed main () { ios::sync_with_stdio (false ); #ifdef DEBUG clock_t t0 = clock (); freopen ("data.in" , "r" , stdin); freopen ("data.out" , "w" , stdout); #endif cin >> n; for (int i = 1 ; i <= n; i++) cin >> a[i], f[i] = f[i - 1 ] + a[i]; for (int i = 1 ; i <= n; i++) g[i] = (g[i - 1 ] + i * a[i]) % mod; for (int i = n; i; i--) h[i] = (h[i + 1 ] + (n - i + 1 ) * a[i]) % mod; if ((n & 1 ) == 0 ) { for (int k = 1 ; k <= n; k++) { int inv_k = qmi (k, mod - 2 ); int tp = min (k, n - k + 1 ); int res = (g[tp] + h[n - tp + 1 ]) % mod; if (tp < n / 2 ) res = (res + (f[n - tp] - f[tp]) % mod * tp) % mod; ans = (ans + res * inv_k) % mod; } } else { int mid = (n + 1 ) / 2 ; for (int k = 1 ; k <= n; k++) { int inv_k = qmi (k, mod - 2 ); int tp = min (k, n - k + 1 ); int res = 0 ; if (tp == mid) { res = (g[tp - 1 ] + h[tp + 1 ]) % mod; res = (res + a[tp] * tp) % mod; } else { res = (g[tp] + h[n - tp + 1 ]) % mod; res = (res + (f[n - tp] - f[tp]) % mod * tp) % mod; } ans = (ans + res * inv_k) % mod; } } cout << ans << endl; return 0 ; }
9. 补充说明 (Additional Notes)
本题是子数组平均值和 (Sum of Subarray Averages)的经典变种,核心技巧是"贡献拆分 + 前缀和优化",将 O ( N 2 ) O(N^2) O ( N 2 ) 的枚举压缩至 O ( N ) O(N) O ( N ) 。方法经典,具有学习的价值。
若 N N N 更大(如 1 0 6 10^6 1 0 6 ),此 O ( N ) O(N) O ( N ) 算法依然高效。模逆元部分可进一步用线性预处理优化常数,但非必需。
赛时代码采用全局数组与 #define int long long 的典型竞赛写法,思路清晰且已通过验证。全局数组是 OI 赛制下的常见习惯,本题中主要是计算前(后)缀和利用了头(尾)默认值为 0 0 0 ;#define int long long 统一处理溢出问题,也是算法竞赛中常用的写法。参考代码相比赛时代码,调整了变量命名与边界条件的可读性,添加了详细注释,算法本质完全一致,也提交 AtCoder 进行测试。
本题与 AtCoder ABC268 F - SST 等"子数组贡献拆分"类题目思路一脉相承,核心都是将 O ( N 2 ) O(N^2) O ( N 2 ) 的枚举转化为每个元素的独立系数计算。本题独特的点是需要使用具有对称性的前缀和分段实现区间求和。