1. 题目数据 (Problem Metadata)

2. 题意简述 (Problem Summary)

给定正整数 NN 和长度-NN 的整数序列 A=(A1,A2,,AN)A=(A_1,A_2,\dots,A_N),定义子数组 Al,Al+1,,ArA_l,A_{l+1},\dots,A_r 的算术平均值为 f(l,r)=1rl+1i=lrAif(l,r)=\frac{1}{r-l+1}\sum_{i=l}^{r}A_i。要求计算

Ans=1lrNf(l,r)(mod998244353)\text{Ans}=\sum_{1\le l\le r\le N} f(l,r) \pmod{998244353}

其中模 998244353998244353 下的分数 PQ\frac{P}{Q}Q≢0(mod998244353)Q\not\equiv0\pmod{998244353})转化为 PQ1(mod998244353)P\cdot Q^{-1}\pmod{998244353} 输出,其中 Q1Q^{-1} 表示在模意义下的逆元 。1N5×1051\le N\le 5\times10^50Ai<9982443530\le A_i<998244353

3. 朴素解法 (Brute-Force)

枚举所有 O(N2)O(N^2) 个子数组,对每个子数组计算平均值并累加。N=5×105N=5\times10^5 时子数组数量级为 101110^{11},完全无法在规定时间内完成。

瓶颈在于重复计算平均值的分母。所有子数组的平均值之和可以交换求和顺序,拆分为每个元素的独立贡献,从而将问题降维。

4. 核心解法 (Main Solution)

特殊性质

平均值之和可以交换求和顺序,拆分为每个元素的独立贡献。位置 ii 的贡献仅由 iiNN 的相对关系决定,与 AiA_i 的具体值无关。通过逆元,平均数求和操作可以转化为对取模有分配律的加法和乘法。

关键突破

O(N2)O(N^2) 的瓶颈出发,利用"每个元素的贡献可分离"这一性质,将问题转化为对每个位置计算一个仅依赖下标的系数 WiW_i,最终答案为 Ans=i=1NAiWi\text{Ans}=\sum_{i=1}^N A_i\cdot W_i

推导过程

第 1 步:枚举区间长度,合并分子求和。
对每个固定的长度 kk,先计算所有长度为 kk 的子数组的分子之和,再乘以 k1k^{-1} 得到该长度下所有子数组的平均值之和:

1kres(k)=1kl=1Nk+1i=ll+k1Ai\frac1k\text{res}(k)=\frac{1}{k}\sum_{l=1}^{N-k+1}\sum_{i=l}^{l+k-1}A_i

第 2 步:固定位置 ii,分析包含它的子数组。 对固定的 iikk,满足 lil+k1l\le i\le l+k-1 的左端点个数为

cnt(i,k)=min(k,  i,  Ni+1,  Nk+1)cnt(i,k)=\min(k,\;i,\;N-i+1,\;N-k+1)

该公式可由区间 l[max(1,ik+1),min(i,Nk+1)]l\in[\max(1,i-k+1),\,\min(i,N-k+1)] 的长度直接得到。

交换求和顺序,固定位置 ii,统计 ii 在多少个长度为 kk 的子数组中出现:

1ki=1NAimin(k,i,Ni+1,Nk+1)\frac{1}{k}\sum_{i=1}^{N}A_i\cdot \min(k,i,N-i+1,N-k+1)

对所有 kk 累加即得最终答案:

Ans=k=1N1ki=1NAimin(k,i,Ni+1,Nk+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)

再交换内外层求和,拆出每个位置 ii 的独立贡献 AiWiA_i\cdot W_i

Ans=i=1NAik=1Nmin(k,i,Ni+1,Nk+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}

第 3 步:定义系数 WiW_i

Wi=k=1Ncnt(i,k)kW_i=\sum_{k=1}^{N}\frac{cnt(i,k)}{k}

第 4 步:对称性简化。 注意到 Wi=WNi+1W_i=W_{N-i+1},且 cnt(i,k)cnt(i,k) 关于 iikk 均呈"三段线性"结构。通过枚举 kk,令 tp=min(k,Nk+1)tp=\min(k,N-k+1),可将 WiW_i 的求和转化为

Wi=k=1Nmin(i,Ni+1,tp,Ntp+1)kW_i=\sum_{k=1}^{N}\frac{\min(i,N-i+1,\,tp,\,N-tp+1)}{k}

第 5 步:前缀和优化。 对枚举变量 kk,分子部分 j=1Nmin(j,Nj+1,tp,Ntp+1)Aj\sum_{j=1}^{N}\min(j,N-j+1,tp,N-tp+1)\cdot A_j 可按三段拆分为:

  • 左段 [1,tp][1,tp]:系数为 jj,即 j=1tpjAj\sum_{j=1}^{tp}j\cdot A_j
  • 中段 [tp+1,Ntp][tp+1,N-tp]:系数为 tptp,即 tpj=tp+1NtpAjtp\cdot\sum_{j=tp+1}^{N-tp}A_j
  • 右段 [Ntp+1,N][N-tp+1,N]:系数为 Nj+1N-j+1,即 j=Ntp+1N(Nj+1)Aj\sum_{j=N-tp+1}^{N}(N-j+1)\cdot A_j

预处理三个前缀和数组 ff(普通前缀和)、gg(左段加权和)、hh(右段加权和),即可在 O(1)O(1) 内算出当前 kk 对应的分子值 res(k)\text{res}(k)。对于 j=1tpjAj\sum_{j=1}^{tp}j\cdot A_j 这种加权和,我们在使用差分树状数组实现区间修改区间查询时见过,详见:【数据结构】树状数组 学习笔记。最终答案:

Ans=k=1Nres(k)k1(mod998244353)\text{Ans}=\sum_{k=1}^{N}\text{res}(k)\cdot k^{-1}\pmod{998244353}

5. 正确性证明 (Proof of Correctness)

交换求和顺序:所有项均为有限值,且模意义下分母 Q≢0(mod998244353)Q\not\equiv0\pmod{998244353},故逆元存在。由求和交换律,先对 ii 求和或先对 l,rl,r 求和结果相同。

cnt(i,k)cnt(i,k) 公式:对固定 i,ki,k,左端点 ll 需满足 lil+k1l\le i\le l+k-11lNk+11\le l\le N-k+1,即 l[max(1,ik+1),min(i,Nk+1)]l\in[\max(1,i-k+1),\,\min(i,N-k+1)]。该区间长度恰为 min(k,i,Ni+1,Nk+1)\min(k,i,N-i+1,N-k+1),证毕。

前缀和公式:对枚举变量 kk,令 tp=min(k,Nk+1)tp=\min(k,N-k+1)。将数组按 tptp 分为左、中、右三段,各段系数分别为下标 jjtptpNj+1N-j+1。因此 res(k)=j=1NcjAj\text{res}(k)=\sum_{j=1}^{N}c_j\cdot A_j,其中

cj={jjtptptp<jNtpNj+1j>Ntpc_j=\begin{cases}j & j\le tp\\tp & tp<j\le N-tp\\N-j+1 & j>N-tp\end{cases}

该系数恰好等于 min(j,Nj+1,tp,Ntp+1)\min(j,N-j+1,tp,N-tp+1)。预处理 gghh 后,res(k)\text{res}(k) 的每一段均可 O(1)O(1) 合并。综上所述,整个算法正确。

通俗地,每个位置 ii 的贡献权重 WiW_i 可以看作"以 ii 为中心的对称衰减",而前缀和 gghh 分别维护了从左到右和从右到左的加权累积,使得每一轮 kk 的分子计算只需三次数组访问。

6. 复杂度分析 (Complexity)

  • 时间复杂度:预处理三个前缀和数组 O(N)O(N)。主循环 NN 次,每次需 O(logMOD)O(\log MOD) 计算模逆元(快速幂),总计 O(NlogMOD)O(N \log MOD)log299824435330\log_2 998244353 \approx 30N=5×105N=5\times10^5 时约 1.5×1071.5\times10^7 次运算,可轻松通过。若预处理逆元数组,可降至严格 O(N)O(N)
  • 空间复杂度O(N)O(N)。存储原数组及三个前缀和数组,每个数组长度 N+5N+5,总计约 4×5×105×8 bytes16 MB4\times 5\times10^5\times 8\text{ bytes}\approx 16\text{ MB},远低于内存限制。

7. 实现细节与避坑指南 (Implementation Details)

  • 模逆元:使用快速幂计算 k1k9982443532(mod998244353)k^{-1}\equiv k^{998244353-2}\pmod{998244353}998244353998244353 是素数,逆元恒存在。
  • 整数范围ff 数组存储真实前缀和(未取模),其差值 f[Ntp]f[tp]f[N-tp]-f[tp] 最大约为 N9982443535×1014N\cdot 998244353\approx 5\times10^{14},取模后小于 998244353998244353,再乘以 tp5×105tp\le 5\times10^5,乘积仍在 6464 位整数范围内,不会溢出。
  • 非负性:由于 tp=min(k,Nk+1)tp=\min(k,N-k+1),总有 NtptpN-tp\ge tp,故 f[Ntp]f[tp]0f[N-tp]-f[tp]\ge0,无需额外处理负数取模问题。这也是我对原始前缀和不取模,并计算这个 tptp 的原因。
  • 奇偶分类
    • NN 为偶数:中间位置不存在,所有 kk 统一使用左段+右段+中段公式;当 tp=N/2tp=N/2 时中段为空,跳过中间段累加。
    • NN 为奇数:中间位置 mid=(N+1)/2mid=(N+1)/2 需要特殊处理,因为此时中段为空,需拆分为左段前缀 g[mid1]g[mid-1] + 右段后缀 h[mid+1]h[mid+1] + 中间元素 AmidmidA_{mid}\cdot mid
  • 下标习惯:数组下标从 11 开始,与题目描述一致,避免 00 下标带来的边界混淆。
坑点 说明
模逆元计算 快速幂每次 O(logMOD)O(\log MOD),主循环 NN 次,总复杂度 O(NlogMOD)O(N \log MOD)log299824435330\log_2 998244353 \approx 30N=5×105N=5\times10^5 时约 1.5×1071.5\times10^7 次运算,可轻松通过。若需严格 O(N)O(N),可预处理逆元数组 inv[i] = mod - mod/i * inv[mod%i] % modi=1..Ni=1..N),O(N)O(N) 初始化后每次 O(1)O(1) 查询。
中间位置处理 NN 为奇数时,tp=(N+1)/2tp=(N+1)/2 对应中间元素,此时中段 [tp+1,Ntp][tp+1,N-tp] 为空。代码中通过 if (tp != (n+1)/2) 分支避免访问空区间,否则会重复计入中间元素。
负数取模 f[n-tp]-f[tp] 理论上非负,但 C++ 中 % 对负数的行为取决于编译器。为保险起见,可以在参考代码中加了 if (ans < 0) ans += MOD;(pref[n - tp] - pref[tp] + MOD) % MOD。实际上由于 ntptpn-tp \ge tp,该差值始终非负,不加也正确。

8. 参考代码 (Reference Code)

基本上是我最早实现的赛时代码,采用「前缀和 f,g,hf,g,h + 分类讨论 NN 奇偶」的思路。思路完全正确,已通过 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;

// 快速幂求模逆元:计算 base^(mod-2) % mod
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

// Don't stop. Don't hide. Follow the light, and you'll find tomorrow.

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)
{
// N 为偶数:遍历所有区间长度 k,计算当前长度的贡献
for (int k = 1; k <= n; k++)
{
int inv_k = qmi(k, mod - 2); // 当前 k 的模逆元,即 1/k
int tp = min(k, n - k + 1); // tp = min(k, n-k+1),确定当前对称段的边界
int res = (g[tp] + h[n - tp + 1]) % mod; // 左段 + 右段贡献

// 中段贡献:当 tp < n/2 时中段非空,否则中段为空跳过
if (tp < n / 2)
res = (res + (f[n - tp] - f[tp]) % mod * tp) % mod;

ans = (ans + res * inv_k) % mod;
}
}
else
{
// N 为奇数:中间位置需要单独处理
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)
{
// 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(N2)O(N^2) 的枚举压缩至 O(N)O(N)。方法经典,具有学习的价值。
  • NN 更大(如 10610^6),此 O(N)O(N) 算法依然高效。模逆元部分可进一步用线性预处理优化常数,但非必需。
  • 赛时代码采用全局数组与 #define int long long 的典型竞赛写法,思路清晰且已通过验证。全局数组是 OI 赛制下的常见习惯,本题中主要是计算前(后)缀和利用了头(尾)默认值为 00#define int long long 统一处理溢出问题,也是算法竞赛中常用的写法。参考代码相比赛时代码,调整了变量命名与边界条件的可读性,添加了详细注释,算法本质完全一致,也提交 AtCoder 进行测试。
  • 本题与 AtCoder ABC268 F - SST 等"子数组贡献拆分"类题目思路一脉相承,核心都是将 O(N2)O(N^2) 的枚举转化为每个元素的独立系数计算。本题独特的点是需要使用具有对称性的前缀和分段实现区间求和。

本站由 zaochen 使用 Stellar 1.33.1 主题创建。
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。
全站访问量 - 次 · 访客数 - 人 · 本页面浏览 -