喵~ 主人你好呀!今天本喵要给你讲解一道非常有趣的数论题哦,是来自 Codeforces 的 1225D - Power Products。这道题呀,就像是帮数字们寻找它们的“天命真子”,让它们相乘之后能成为一个完美的 k 次方数,是不是听起来就很浪漫喵?
别担心,只要跟着本喵的思路走,你一定能轻松掌握它的喵!
题目大意
我们拿到了一串数字 a_1, a_2, ..., a_n,还有一个整数 k。我们的任务呢,就是要找出有多少对 (i, j),满足 1 ≤ i < j ≤ n,并且它们的乘积 a_i * a_j 恰好等于某个整数 x 的 k 次方,也就是 a_i * a_j = x^k。
举个例子,如果 k=3,a_i=2,a_j=4,那么 a_i * a_j = 8,而 8 = 2^3,所以这就是一对满足条件的组合啦!我们要做的就是把所有这样的组合都数出来,喵~
解题方法
这道题的核心,就在于 a_i * a_j = x^k 这个神奇的等式。我们来把它掰开揉碎了分析一下喵。
一个数如果能表示成 x^k,那它有什么特点呢?根据算术基本定理,任何一个正整数都可以唯一地分解成一堆质数的乘积。比如说 24 = 2^3 * 3^1。
如果 a_i * a_j 是一个完美的 k 次方数,那么把它质因数分解后,每一个质因子的指数都必须是 k 的倍数!
比如说,k=3,a_i = 2^1 * 3^2, a_j = 2^2 * 3^1。 它们的乘积就是 (2^1 * 3^2) * (2^2 * 3^1) = 2^(1+2) * 3^(2+1) = 2^3 * 3^3 = (2*3)^3 = 6^3。 你看,质因子 2 的指数是 1+2=3,3 的指数是 2+1=3,都是 3 的倍数,所以它们的乘积就是一个立方数。
这给了我们一个绝妙的思路!对于每个数 a_i,我们可以把它进行质因数分解,看看它的每个质因子 p 的指数 e。我们关心的其实不是 e 本身,而是它模 k 的余数,也就是 e % k。
为了让 a_i * a_j 的质因子 p 的总指数 e_i + e_j 成为 k 的倍数,必须满足 (e_i + e_j) % k = 0。 这意味着,如果 a_i 的质因子 p 的指数 e_i 模 k 的余数是 r(r = e_i % k),那么 a_j 的质因子 p 的指数 e_j 模 k 的余数就必须是 k - r (如果 r=0,那余数也得是0)。
这样一来,问题就转化成了一个配对游戏,喵!
定义“特征”: 我们可以为每个数字
a_i定义一个“特征”。这个特征就是它所有质因子p和对应的指数模k的余数e % k的组合。为了简化,我们只记录那些e % k != 0的部分。比如k=3,a_i = 2^4 * 3^3 * 5^1。4 % 3 = 1,3 % 3 = 0,1 % 3 = 1。那么它的特征就可以表示为{(2, 1), (5, 1)}。定义“互补特征”: 对于一个特征,我们可以找到它的“互补特征”。如果
a_i的特征是{(p, r)},那么能和它配对的a_j就需要有{(p, k-r)}这样的特征。对于上面的例子,a_i的特征是{(2, 1), (5, 1)},那它的互补特征就是{(2, 3-1), (5, 3-1)},也就是{(2, 2), (5, 2)}。计数: 我们可以遍历整个数组。对于每个数
a_i,我们先计算出它的“互补特征”,然后去查一查我们之前已经遇到过多少个数字拥有这个“互补特征”。把这个数量加到我们的总答案里。查完之后,再把a_i自己的“特征”记录下来,供后面的数字查询。
为了高效地记录和查询这些“特征”,我们可以使用一个哈希表(在 C++ 中就是 std::map 或者 std::unordered_map),它的键(Key)是“特征”,值(Value)是这个特征出现的次数。
是不是很简单喵?让本喵来给你梳理一下具体的算法步骤吧!
- 预处理: 为了快速地对数字进行质因数分解,我们可以先用类似筛法的方式预处理出
1到10^5每个数的最小质因子(Smallest Prime Factor, SPF)。 - 遍历: 遍历输入的
n个数字a_1, ..., a_n。 - 处理当前数字 a_i: a. 对
a_i进行质因数分解,得到它的“特征”S_i。S_i是一个(质数, 指数 % k)的列表,只包含那些指数 % k != 0的项。 b. 根据S_i计算出它的“互补特征”S_complement。 c. 在哈希表中查找S_complement出现的次数,加到总答案ans中。 d. 将S_i在哈希表中的计数加一。 - 输出答案: 遍历结束后,
ans就是我们要求的总对数啦!
题解代码解析
下面是这道题的 C++ 代码,本喵来逐段给你解释一下它是怎么实现我们刚刚的思路的,喵~
#include <iostream>
#include <vector>
#include <map>
using namespace std;
const int MAX = 100000;
int main() {
// 提高一下输入输出效率,喵~
ios::sync_with_stdio(false);
cin.tie(0);
int n, k;
cin >> n >> k;
vector<int> a(n);
for (int i = 0; i < n; i++) {
cin >> a[i];
}
// --- 预处理:最小质因子筛 (SPF) ---
// spf[i] 存储 i 的最小质因子
vector<int> spf(MAX + 1);
for (int i = 1; i <= MAX; i++) {
spf[i] = i; // 初始化,每个数的最小质因子是它自己
}
for (int i = 2; i * i <= MAX; i++) {
if (spf[i] == i) { // i 是一个质数
for (int j = i * i; j <= MAX; j += i) {
if (spf[j] == j) { // 如果 j 还没被标记过
spf[j] = i; // 那么 i 就是 j 的最小质因子
}
}
}
}
// --- 核心逻辑:计数 ---
// M 用来存储每个“特征”出现的次数
// 特征用 vector<pair<int, int>> 表示,即 {(p1, e1%k), (p2, e2%k), ...}
map<vector<pair<int, int>>, long long> M;
long long ans = 0;
for (int i = 0; i < n; i++) {
int x = a[i];
vector<pair<int, int>> signature; // 当前数字的特征
// --- 分解质因数并计算特征 ---
int temp = x;
while (temp > 1) {
int p = spf[temp]; // 获取最小质因子
int count = 0;
while (temp % p == 0) {
count++;
temp /= p;
}
int exponent_mod = count % k;
if (exponent_mod != 0) { // 只记录余数不为0的项
signature.push_back({p, exponent_mod});
}
}
// --- 计算互补特征 ---
vector<pair<int, int>> comp_signature;
for (auto &p_exp : signature) {
comp_signature.push_back({p_exp.first, k - p_exp.second});
}
// --- 查找配对并更新答案 ---
// M[comp_signature] 会返回 comp_signature 出现的次数
// 如果不存在,会默认构造一个 0,非常方便喵~
ans += M[comp_signature];
// --- 将当前数字的特征存入 map ---
M[signature]++;
}
cout << ans << '\n';
return 0;
}知识点介绍
这道题用到了一些非常基础但重要的数论和算法知识,本喵来给你总结一下:
质因数分解 (Prime Factorization)
- 概念: 算术基本定理告诉我们,任何大于1的自然数,都可以唯一地分解成有限个质数的乘积。这是数论的基石!
- 应用: 题目中判断一个数是否是
k次方幂,就是通过检查其所有质因子的指数是否都是k的倍数。
模运算 (Modular Arithmetic)
- 概念: 就是我们常说的求余数运算。
a % k表示a除以k的余数。 - 应用: 在这道题里,我们并不关心质因子的具体指数有多大,只关心它除以
k的余数。(e_i + e_j) % k == 0是解题的关键。
- 概念: 就是我们常说的求余数运算。
最小质因子筛 (Sieve for Smallest Prime Factor)
- 概念: 这是一种高效的预处理方法,是埃氏筛(Sieve of Eratosthenes)的变种。它可以在 O(N log log N) 的时间内预处理出
1到N所有数的最小质因子。 - 应用: 有了
spf数组,我们就可以在 O(log x) 的时间内对任意数x进行质因数分解,比试除法 O(sqrt(x)) 要快得多,对于需要多次分解质因数的题目来说是必备的优化技巧喵。
- 概念: 这是一种高效的预处理方法,是埃氏筛(Sieve of Eratosthenes)的变种。它可以在 O(N log log N) 的时间内预处理出
哈希表 / 映射 (Hash Map / Map)
- 概念: 一种键-值(Key-Value)对的数据结构,可以快速地通过键来存取值。C++ 中的
std::map(基于红黑树) 和std::unordered_map(基于哈希表) 都是它的实现。 - 应用: 我们用它来存储和查询数字的“特征”。“特征”本身(一个
vector)作为键,出现的次数作为值。这使得我们能高效地完成配对查找。
- 概念: 一种键-值(Key-Value)对的数据结构,可以快速地通过键来存取值。C++ 中的
希望本喵的题解对你有帮助哦,如果还有不懂的地方,随时可以再来问我!喵~