把每个性格值剥掉立方因子得到立方核,乘积为完全立方等价于两核互补;冲突退化为互补核配对,逐对取人数多的一侧、核 1 只留 1 人。
OJ: roj
题目 ID: 10002
难度:普及+/提高-
标签:数论质因数分解贪心python
创建: 2026-10-02 17:15
更新: 2026-10-10 12:16
形式化题目
给定
题面中的「乘积开三次方根是正整数」就是乘积为完全立方数,此时两人性格不合。例如
样例
输入:
4
4 2 16 27输出:
3正解
思路
朴素做法是枚举全部数对,逐对判断乘积是否为完全立方数,再在冲突关系上求最大的两两相容子集。但
第一步:立方核归约。 把每个数写成「完全立方数 × 立方核」,其中立方核是剥掉全部立方因子后剩下的部分——即每个素因子的指数对 3 取余后连乘。因为
第二步:冲突结构。 设
- 同核两两相容(
): 中每个出现的素因子指数为 ,都不是 3 的倍数,故不是立方数。 - 补核唯一:要
为立方数, 的每个素因子指数必须与 的指数之和为 3 的倍数,所以 中指数为 的素数 在 中只能取 , 中不出现的素数在 中也不出现,即 。这个 自身也是立方核,且「取补」是对合运算(补的补是 )。因此 只与唯一的一个 冲突,与其他任何核都不冲突。 - 核 1 特殊:核为 1 的数本身就是完全立方数,它们两两乘积仍是立方数,彼此全部冲突,只能留 1 人;但它与任何核
的数都相合(立方数乘 为立方 )。
于是冲突图退化成若干互不相交的结构:每对互补核
第三步:高效求核与补。 isqrt 判断是否完全平方即可区分——
下表把样例 1 的每个性格值分解成立方核并标出补核,展示冲突如何按核配对:
| 性格值 | 素因子分解 | 立方核 |
补核 |
|---|---|---|---|
| 4 | 4 | 2 | |
| 2 | 2 | 4 | |
| 16 | 2 | 4 | |
| 27 | 1 | 1 |
分组结果:核 2 有 2 人(性格值 2、16),核 4 有 1 人,核 1 有 1 人。配对
最后结算:用字典统计各核人数并记录每个核的补,遍历时用 taken 集合保证每对互补核只结算一次(补运算是对合,先遇到哪一侧结果相同),核 1 特判加 1,其余加
代码
Python 版:
#!/usr/bin/env python3
# Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
# rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
# rainboy的学习导航网站: https://idx.roj.ac.cn
# create_at: 2026-10-02 17:23
# update_at: 2026-10-02 17:23
import sys
from collections import Counter
from math import isqrt
# a_i ≤ 2×10⁹,而 p³ ≤ 2×10⁹ ⟺ p ≤ 1259:立方因子只可能来自这些素数
CUBE_PRIMES = [p for p in range(2, 1260) if all(p % d for d in range(2, isqrt(p) + 1))] # 205 个
def kernel_pair(x: int) -> tuple[int, int]:
"""把性格值 x 拆成 (核, 补):核是剥掉全部立方因子的 x,补使 核×补 恰为完全立方数。"""
f = g = 1
for p in CUBE_PRIMES:
if x % p:
if p * p > x: # 更小的素数都除尽了,剩下的 x 只能是 1 或素数
break
continue
e = 0
while x % p == 0: # 数清指数,顺便把 x 里的 p 除干净
x //= p
e += 1
e %= 3 # 立方部分剥掉,核里只留对 3 取余的指数
f *= p ** e
if e:
g *= p ** (3 - e) # 补的指数补到 3,乘积里每个素因子的指数都是 3 的倍数
# 收尾:剩余因子都 > 1259,x 只可能是 1、p、p²、p·q(p²q 已超过 2×10⁹)
if x > 1:
f *= x
root = isqrt(x)
g *= root if root * root == x else x * x # p² 的补是 p;p 与 p·q 的补是各自的平方
return f, g
def count_group(cnt: Counter[int], comp: dict[int, int]) -> int:
"""按互补核配对求和:每对取人数多的一侧,核为 1 的人两两冲突只能留 1 人。"""
taken: set[int] = set()
ans = 0
for f, c in cnt.items():
if f in taken:
continue
g = comp[f] # comp 是对合:补的补还是 f,每对只在第一次遇到时结算
taken.update((f, g))
ans += 1 if f == 1 else max(c, cnt.get(g, 0))
return ans
def solve() -> None:
data = iter(map(int, sys.stdin.buffer.read().split()))
n = next(data)
cnt: Counter[int] = Counter()
comp: dict[int, int] = {}
for _ in range(n):
a = next(data)
f, g = kernel_pair(a)
cnt[f] += 1
comp[f] = g # 同一个核的补唯一,重复赋值无害
print(count_group(cnt, comp))
if __name__ == "__main__":
solve()C++ 版(同一算法):
/**
* Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2026-10-04 21:53
* update_at: 2026-10-04 21:53
*/
#include <cstdio>
#include <cmath>
#include <map>
#include <set>
#include <vector>
#include <iostream>
using namespace std;
typedef long long ll;
const int MAXP = 1260; // 只需试除到 1259
vector<int> primes; // 1260 以内的素数表
// 一个数的立方核与其互补核
struct Core {
ll f; // 立方核:剥掉所有立方因子后剩下的部分
ll g; // 互补核:与 f 相乘得到完全立方数的唯一立方核
};
// 筛出 2..1259 的所有素数
void init_primes() {
static bool vis[MAXP];
for (int i = 2; i < MAXP; i++) {
if (!vis[i]) {
primes.push_back(i);
if ((ll)i * i < MAXP) {
for (int j = i * i; j < MAXP; j += i) vis[j] = true;
}
}
}
}
// 对 x 做质因数分解,去掉所有立方因子得到立方核 f,并算出互补核 g
Core reduce(ll x) {
ll f = 1, g = 1;
for (int i = 0; i < (int)primes.size(); i++) {
int p = primes[i];
if ((ll)p * p > x) break; // 更小的素数都已除尽,x 剩余部分为 1 或大于 p 的素数
if (x % p != 0) continue;
int e = 0;
while (x % p == 0) {
x /= p;
e++;
}
int r = e % 3;
if (r == 1) { // 指数模 3 余 1:核留 p,补需要 p^2
f *= p;
g *= (ll)p * p;
} else if (r == 2) { // 指数模 3 余 2:核留 p^2,补需要 p
f *= (ll)p * p;
g *= p;
}
}
// 剩余部分所有素因子都大于 1259,只可能是 1、p、p^2 或 p*q
if (x > 1) {
ll s = sqrt((long double)x);
while ((s + 1) * (s + 1) <= x) s++;
while (s * s > x) s--;
if (s * s == x) { // x = p^2,核指数取 2,补需要 p
f *= x;
g *= s;
} else { // x = p 或 p*q,所有素因子指数均为 1
f *= x;
__int128 t = (__int128)g * x * x;
g = (ll)t;
}
}
return (Core){f, g};
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
init_primes();
int n;
if (!(cin >> n)) return 0;
map<ll, int> cnt; // 每个立方核出现的次数
for (int i = 0; i < n; i++) {
ll a;
cin >> a;
cnt[reduce(a).f]++;
}
ll ans = 0;
set<ll> taken; // 已经结算过的核,避免互补对重复计算
for (map<ll, int>::iterator it = cnt.begin(); it != cnt.end(); ++it) {
ll core = it->first;
int c = it->second;
if (taken.count(core)) continue;
if (core == 1) { // 核为 1 的数两两冲突,最多留 1 个
ans += 1;
taken.insert(1);
continue;
}
ll other = reduce(core).g; // core 的互补核
int c2 = 0;
map<ll, int>::iterator jt = cnt.find(other);
if (jt != cnt.end()) c2 = jt->second;
ans += max(c, c2); // 每对互补核取人数较多的一侧
taken.insert(core);
taken.insert(other);
}
cout << ans << "\n";
return 0;
}复杂度
- 时间:每个数最多试除
个素数,总复杂度 ;分组与配对结算各 。与 C++ 标准解(试除 的素数 + map计数)同阶。 - 空间:
,用于输入数组与核计数字典,实测峰值约 36MB。 - Python 风险:最坏情形(
个接近 的素数,每个数走满 205 次取模)实测 0.70s,OJ 限时 1000ms 下有 TLE 风险,本地放宽限时内全部数据通过。
总结
本题的核心是把「乘积为完全立方数」这一冲突条件翻译成代数结构:先剥立方因子归约到立方核,冲突判据就只取决于核;再发现每个核有唯一互补核,冲突图随之退化为若干互不相交的二分团,最大兴趣组按团贪心即得。
三个容易踩的边界:
- 核 1 的人两两冲突,只能留 1 人,但与所有其他人都相容;
- 取补后要把两侧人数取较大值,补核可能根本没在数据中出现(按 0 人处理);
- 只试除
就够了,剩余部分靠 isqrt判平方区分与 、 ,无需大范围试除。