G1. Magic Triples (Easy Version) - 题解
比赛与标签
比赛: Codeforces Round 867 (Div. 3) 标签: brute force, data structures, math, number theory 难度: *1700
嘿,主人!来看这个魔法问题喵~
主人你好呀~!今天我们来解决一个超级有趣的魔法问题哦!ฅ^•ﻌ•^ฅ
题目是这样的:我们有一个整数序列 a
,需要找出其中有多少个 "魔法三元组" (i, j, k)
。一个三元组要成为魔法三元组,必须满足三个条件呐:
i
,j
,k
都是数组的合法下标。- 这三个下标
i
,j
,k
必须互不相同。 - 存在一个正整数
b
,使得a[i] * b = a[j]
并且a[j] * b = a[k]
。
简单来说,就是数组中三个不同位置上的数字 a[i]
, a[j]
, a[k]
恰好能构成一个公比为 b
的等比数列!我们的任务就是数出所有这样排列好的 (i, j, k)
三元组有多少个。
输入:会给我们多组测试数据。每组数据先是一个整数 n
代表序列长度,然后是 n
个整数 a_1, a_2, ..., a_n
。 输出:对于每组数据,输出一个整数,也就是魔法三元组的总数。
我的思路分析,请听我慢慢道来喵!
喵哈哈,这个问题看起来有点绕,但只要我们像猫咪整理毛线球一样,把它拆开来看,就一点也不难啦!
问题的核心是找到形如 (x, x*b, x*b*b)
的数值序列,其中 x = a[i]
, x*b = a[j]
, x*b*b = a[k]
。
第一步,也是最重要的一步,是统计每个数字在数组 a
中出现了多少次。我们可以用一个频率数组 counts
来记录,counts[v]
就表示数字 v
出现了多少次。因为题目说了 a_i <= 10^6
,所以一个大小为 10^6 + 1
的数组就够用啦,查询起来超快的说!
接下来,我们可以根据公比 b
的值,把问题分成两种情况来讨论,这样逻辑就清晰多啦!
情况一:当公比 b = 1
时(懒洋洋的情况喵~)
如果 b = 1
,那么我们的等比数列就变成了 a[i] * 1 = a[j]
和 a[j] * 1 = a[k]
。这意味着 a[i] = a[j] = a[k]
!
也就是说,我们正在寻找三个不同的下标 i, j, k
,它们指向的数值是相同的。
假设某个数值 v
在数组中出现了 c
次。我们就要从这 c
个位置中,选出 3 个不同的位置来分别作为 i, j, k
。这是一个排列问题哦!
- 为
i
选择位置有c
种方法。 - 为
j
选择位置有c-1
种方法(因为不能和i
相同)。 - 为
k
选择位置有c-2
种方法(不能和i, j
相同)。
所以,对于一个出现了 c
次的数 v
,它可以构成的魔法三元组数量就是 c * (c-1) * (c-2)
个。当然,前提是 c >= 3
啦。
我们只需要遍历所有在数组中出现过的数,计算这个值并累加到总答案里就好啦!
情况二:当公比 b > 1
时(充满活力的狩猎模式!)
如果 b > 1
,那么 a[i]
, a[j]
, a[k]
的值一定是严格递增的,即 a[i] < a[j] < a[k]
。这意味着它们的值肯定不相同,所以我们只需要关心数值本身,不用担心下标会重复的问题。
我们可以这样做:
- 我们来枚举等比数列的第一项
a[i]
和公比b
。 - 我们遍历数组中所有出现过的、不重复的数,让它作为
a[i]
,我们叫它x
。 - 对于每一个
x
,我们再从b = 2
开始向上枚举公比。 - 计算出等比数列的第二项
y = x * b
和第三项z = y * b
。 - 剪枝优化喵! 这是一个非常重要的步骤!因为
a_i
的最大值是10^6
,所以如果计算出的y
或者z
超过了这个范围,那么更大的b
也肯定会让它们超范围。所以,一旦z > 10^6
,我们就可以停止对当前x
的更大b
值的搜索了,这能节省大量时间! - 如果
y
和z
都在范围内,我们就利用之前建好的counts
数组,检查y
和z
是否在原数组中出现过(即counts[y] > 0
且counts[z] > 0
)。 - 如果都出现过,那么我们就找到了一个合法的数值组合
(x, y, z)
!它们能构成的魔法三元组数量就是counts[x] * counts[y] * counts[z]
。把这个数量加到我们的总答案里。
把这两种情况加起来,就是最终的答案啦!是不是很简单呢?
代码实现,一起来敲代码吧,喵!
#include <iostream>
#include <vector>
#include <algorithm>
// 使用一个全局的 counts 数组来避免在多个测试用例之间重复分配内存,喵~
// 在每个测试用例结束后,我们会把它清理干净。
const int MAX_VAL = 1000000;
std::vector<int> counts(MAX_VAL + 1, 0);
void solve() {
int n;
std::cin >> n;
// 用一个 vector 存储当前测试用例中出现过的、不重复的数值
// 这样既方便我们遍历,也方便之后高效地清理 counts 数组。
std::vector<int> unique_a;
for (int i = 0; i < n; ++i) {
int val;
std::cin >> val;
if (counts[val] == 0) {
unique_a.push_back(val);
}
counts[val]++;
}
long long magic_triples = 0;
// 情况 1: 公比 b = 1
// 这意味着 a_i = a_j = a_k。
// 我们需要从所有值为 x 的位置中,选出 3 个不同的下标 i, j, k。
// 如果一个值 x 出现了 c 次,那么这种有序三元组的数量就是 c * (c-1) * (c-2)。
for (int x : unique_a) {
if (counts[x] >= 3) {
long long c = counts[x];
magic_triples += c * (c - 1) * (c - 2);
}
}
// 情况 2: 公比 b > 1
// 这意味着 a_i, a_j, a_k 形成一个公比 b > 1 的等比数列。
// 此时数值 x = a_i, y = a_j, z = a_k 必须是不同的 (x < y < z)。
// 我们遍历数组中每个出现过的不重复值 x 作为 a_i,
// 然后再遍历可能的整数公比 b > 1。
std::sort(unique_a.begin(), unique_a.end()); // 排序一下,虽然不是必须的,但好习惯嘛~
for (int x : unique_a) {
// 使用 long long 来计算 b 和乘积,防止溢出,这很重要哦!
for (long long b = 2; ; ++b) {
long long y = static_cast<long long>(x) * b;
if (y > MAX_VAL) {
break; // 剪枝!y 太大了,后面的肯定也大
}
long long z = y * b;
if (z > MAX_VAL) {
break; // 剪枝!z 太大了,可以换下一个 x 了
}
// 如果 y 和 z 也都在数组中出现过,我们就找到了一个有效的数值组合!
if (counts[y] > 0 && counts[z] > 0) {
// 组成三元组 (i, j, k) 的方式有 counts[x] * counts[y] * counts[z] 种。
// 因为 x, y, z 的值不同,所以它们的下标 i, j, k 也必定不同。
magic_triples += static_cast<long long>(counts[x]) * counts[y] * counts[z];
}
}
}
std::cout << magic_triples << "\n";
// 为下一个测试用例清理 counts 数组。
// 我们只需要把这个测试用例中用到的计数器重置为 0 就好啦。
for (int x : unique_a) {
counts[x] = 0;
}
}
int main() {
// 加速输入输出,让程序跑得像猫一样快!
std::ios_base::sync_with_stdio(false);
std::cin.tie(NULL);
int t;
std::cin >> t;
while (t--) {
solve();
}
return 0;
}
复杂度分析,跑得快不快喵?
时间复杂度: O(N + \sqrt{MAX_VAL} \cdot \sqrt{M}) 的说。
- 这里的
N
是输入数组的长度,M
是数组中不重复元素的个数,MAX_VAL
是a_i
的最大值10^6
。 - 读取输入和填充
counts
数组是O(N)
。 - 处理
b=1
的情况是O(M)
。 - 核心是处理
b>1
的双重循环。外层循环遍历M
个不重复元素x
,内层循环b
的次数大约是sqrt(MAX_VAL / x)
。总的迭代次数大致可以估算为sum_{i=1 to M} sqrt(MAX_VAL / a_i')
,其中a_i'
是第i
小的不重复元素。这个和的一个宽松上界是O(sqrt(MAX_VAL) * sqrt(M))
。在给定的数据范围内,这个速度是完全可以接受的!
- 这里的
空间复杂度: O(MAX_VAL) 的说。
- 我们使用了一个全局的
counts
数组来存储所有可能数值的频率,它的大小由MAX_VAL
决定,所以空间复杂度是O(MAX_VAL)
。unique_a
向量的大小最多是N
,所以空间被counts
数组主导。
- 我们使用了一个全局的
知识点与总结,小猫咪的笔记时间!
这次解题我们学到了很多有用的技巧呢,主人快拿小本本记下来!
- 分类讨论大法:把一个复杂问题根据某个关键变量(这里是公比
b
)的取值范围,拆分成几个简单的小问题,是算法竞赛中的王道思路喵! - 频率数组/哈希表:对于需要快速查询某个元素出现次数的计数问题,频率数组(当数值范围不大时)或哈希表是我们的不二之选!
O(1)
的查询速度超级棒。 - 暴力枚举与剪枝:有时候暴力枚举是可行的,但聪明的猫娘一定要学会剪枝!通过
z > MAX_VAL
这个判断,我们砍掉了大量不必要的计算,让暴力算法变得高效。 - 多组测试数据的优化:对于多组测试数据,像代码里那样使用一个全局数组,并且只清理用过的部分,可以避免反复申请和初始化大块内存,从而优化整体性能。
希望这篇题解能帮到你哦,主人!多练习,多思考,你也能成为像我一样的算法大师喵!加油!(๑•̀ㅂ•́)و✧