F. Firefly's Queries - 题解
比赛与标签
比赛: Codeforces Round 971 (Div. 4) 标签: bitmasks, data structures, flows, math 难度: *1700
题目大意喵~
主人你好呀~!这道题是关于一个叫做 Firefly 的小可爱和她神奇的数组变换的喵~
事情是这样的:
- 首先,我们有一个长度为
n的数组a。 - 然后,我们要生成
n个a的循环移位版本。第i个循环移位c_i就是把a的前i-1个元素挪到末尾去。 - 接着,把这
n个循环移位后的数组c_1, c_2, ..., c_n按顺序拼接起来,形成一个超级长~的数组b。这个b的长度会是n * n哦! - 最后,会有
q次询问。每次询问会给出一个区间[l, r],我们需要计算出b数组在这个区间里的所有元素的和是多少。
因为 n 可以很大,所以 b 数组的长度最大能到 (2*10^5)^2,也就是 4 * 10^{10}!直接把 b 数组造出来是绝对不行的说,会把内存和时间都撑爆的喵!所以,我们必须找到一个更聪明的办法呐。
解题思路分析呐
面对这么大的一个数组,我们肯定不能硬来,要巧取才行!解决区间求和问题,我们的老朋友前缀和就要登场啦~
如果我们能快速算出一个函数 F(x),它表示 b 数组前 x 个元素的和,那么对于任何查询 [l, r],答案就是 F(r) - F(l-1),对吧?
那么,关键问题就变成了:如何高效地计算 F(x) 呢?
让我们来仔细看看 b 数组的结构。b 是由 n 个长度为 n 的数组块(c_1, c_2, ...)拼接而成的。
要求前 x 个元素的和,我们可以把这 x 个元素分成两部分:
- 若干个完整的数组块
- 最后一个不完整的数组块(的开头部分)
举个例子,假设我们要计算 F(x):
x里面包含了多少个完整的长度为n的块呢?这个数量是k = (x-1) / n。(这里用x-1是因为编程里用0-based索引更方便,而题目是1-based的,喵~)- 剩下的部分有多少个元素呢?这个数量是
rem+1,其中rem = (x-1) % n。这些元素来自第k+1个循环移位块c_{k+1}。
第一部分:完整块的和
每个循环移位 c_i 只是原数组 a 的元素换了换位置,所以它们的元素总和都等于 a 的总和。我们先把 a 的总和算出来,记作 total_sum。 k 个完整块的总和就是 k * total_sum。很简单对吧!
第二部分:不完整块的和
这部分稍微有点绕,但别怕,有本猫娘在呢!我们需要计算第 k+1 个循环移位块 c_{k+1} 的前 rem+1 个元素的和。
c_{k+1} 是从原数组 a 的第 k 个元素(0-based索引)开始的循环移位。也就是说,c_{k+1} 的开头是 a_k, a_{k+1}, ..., a_{n-1}, a_0, ...。 我们需要求和的,就是 a_k, a_{k+1}, ..., a_{k+rem} 这一段(所有下标都要对 n 取模)。
处理循环数组的求和问题,有一个超级好用的技巧:数组翻倍! 我们创建一个新数组 A,它等于 a 和 a 自己拼接起来,A = a + a。A 的长度是 2n。 这样一来,原来在 a 上的任何一个长度不超过 n 的循环连续子段,都变成了 A 上的一个普通的连续子段。 比如,a_k, ..., a_{k+rem} 这一段,就正好是 A 数组里从下标 k 到 k+rem 的那一段!
为了快速求 A 上任意区间的和,我们可以再对 A 预处理一个前缀和数组 P。 P[i] 表示 A 数组前 i 个元素的和。 那么,A[k...k+rem] 的和就是 P[k+rem+1] - P[k] 啦!
整合起来! 我们的 F(x) 函数的计算公式就出来啦 (使用0-based索引 x' = x-1): k = x' / nrem = x' % nF(x) = k * total_sum + (P[k+rem+1] - P[k])
这样,每次查询我们只需要做几次算术运算和查表,时间复杂度就是 O(1) 的说!
最终步骤总结喵~
- 读入
n和q。 - 读入数组
a,并计算出它的总和total_sum。 - 创建一个长度为
2n的数组A,内容是a拼接a。 - 计算
A的前缀和数组P。 - 对于每个查询
[l, r],计算F(r) - F(l-1)作为答案。记得要用long long来存结果哦,不然会溢出的!
代码实现喵~
#include <iostream>
#include <vector>
using namespace std;
typedef long long ll; // 使用 long long 防止求和时溢出喵
int main() {
// 加速输入输出,让程序跑得更快一点!
ios::sync_with_stdio(false);
cin.tie(0);
int t;
cin >> t;
while (t--) {
ll n, q;
cin >> n >> q;
vector<ll> a(n);
ll total = 0; // 用来存储数组 a 的总和
for (ll i = 0; i < n; i++) {
cin >> a[i];
total += a[i];
}
// 技巧一:数组翻倍,处理循环问题
// 把 a 复制一份拼在后面,方便处理循环移位的区间和
vector<ll> A(2 * n);
for (ll i = 0; i < 2 * n; i++) {
A[i] = a[i % n];
}
// 技巧二:预处理前缀和
// P[i] 存储 A 数组前 i 个元素的和
vector<ll> P(2 * n + 1, 0);
for (ll i = 1; i <= 2 * n; i++) {
P[i] = P[i - 1] + A[i - 1];
}
// 定义一个 lambda 函数 F(x) 来计算 b 数组前 x 个元素的和
// 这是整个解法的核心所在!
auto F = [&](ll x) -> ll {
if (x == 0) {
return 0; // 前0个元素的和当然是0啦
}
// 题目是1-based,我们转成0-based计算更方便
x--;
// k 是完整的块数
ll k = x / n;
// rem 是最后一个不完整块的最后一个元素的下标
ll rem = x % n;
// F(x) = (k个完整块的和) + (不完整块的和)
// k个完整块的和 = k * total
// 不完整块是 c_{k+1} 的前 rem+1 个元素,其和等于 A[k...k+rem] 的和
// A[k...k+rem] 的和 = P[k+rem+1] - P[k]
return k * total + (P[k + rem + 1] - P[k]);
};
// 处理 q 次查询
while (q--) {
ll l, r;
cin >> l >> r;
// 区间 [l, r] 的和 = 前 r 个元素的和 - 前 l-1 个元素的和
cout << F(r) - F(l - 1) << '\n';
}
}
return 0;
}复杂度分析的说
时间复杂度: O(n + q) 的说。 对于每个测试用例,我们需要 O(n) 的时间来读取输入、构建翻倍数组
A和计算前缀和数组P。这部分是预处理。之后,每次查询都调用F函数,这个函数内部只有几次数学运算和数组访问,是 O(1) 的。所以q次查询总共需要 O(q) 的时间。总时间复杂度就是 O(n + q) 啦,非常高效!空间复杂度: O(n) 的说。 我们主要需要额外的空间来存储原数组
a(O(n)),翻倍数组A(O(2n)),以及前缀和数组P(O(2n))。总的来说,空间开销是和n成正比的,所以是 O(n) 的喵。
知识点与总结喵~
这道题真是一道非常好的思维训练题呢!它教会了我们如何处理那些看起来大到吓人的 "隐式" 数组。
核心的知识点和技巧总结一下:
- 前缀和思想: 这是解决静态区间和问题的万能钥匙!
sum(l, r) = prefix(r) - prefix(l-1)这个思想一定要牢牢记住呐。 - 转化与分解: 面对复杂问题,尝试把它分解成几个更简单的子问题。我们成功地把
b数组的前缀和分解成了 "完整块" 和 "不完整块" 两部分。 - 循环数组的处理: "数组翻倍" 是一个非常优雅的技巧,能把循环的问题变成线性问题,大大简化了后续的计算。
- 数学建模: 将问题的结构抽象成数学公式
F(x)是解题的关键一步。这需要我们有敏锐的观察力和清晰的逻辑。 - 注意数据范围: 看到求和问题和较大的
n,就要立刻想到long long,这是一个好习惯的说!
希望这篇题解能帮助到你,让你也爱上这种用智慧解决难题的感觉!加油哦,你一定可以的!喵~ >ω<