平方数 (SNOI 2024)

First Post:

Last Update:

Word Count:
1.3k

Read Time:
5 min

Page View: loading...

神秘结论题,感觉不太符合当代 OI 风格。

题目链接

给定一列正整数 ,求区间乘积为完全平方数的区间个数,并输出前 个区间。


首先可以想到对每个数暴力质因数分解,使用 Pollard-Pho 算法,总复杂度

如果能做的话,可以给每个质数随机赋权,统计一下异或和为 的区间。

可惜出题人甚至不愿意放一个 的点。


注意到二次剩余(可以理解为模意义下完全平方数)这个东西。

直觉上讲,我们已知对于一个奇质数 ,模 意义下有 个数是二次剩余,而完全平方数在任何奇质数模数下总是二次剩余。

因此直觉上讲,对于一个非完全平方数,通过随机取质数再判定二次剩余的方法,我们只有 的概率会判断错误,因此取 个随机质数大概就可以保证正确。

这里可以先给出模奇质数二次剩余的判别法:

二次剩余判定

为奇质数时, 在模 意义下是二次剩余,当且仅当

略证

对于必要性,假设 ,根据 Fermat 小定理,有 ,进而


对于充分性,考虑模 的简化剩余系

对于每一个 ,可以求出 使得

若不存在 ,则 中所有数可以两两配对,即 ,与 Wilson 定理矛盾。

至于二次剩余存在 个,可以从 解释。


我不太想引入 Legendre 符号等内容,但发现后文不太好描述,因此这里给出定义,对于奇质数 ,有:

显然

仅仅从这个判定方式来看,明显 对于 是完全积性的,因此显然两个二次剩余或两个非二次剩余相乘是二次剩余;一个二次剩余和一个非二次剩余相乘是非二次剩余(只考虑 )。

不妨维护每个数模 意义下 的值,压成一个二进制数,为 则记 ,为 则记 ,为 时该位无效,也记为

这样若一段区间的异或和为 ,则这一段区间的乘积被选择的所有质数均判定为完全平方数,可以认为找到了一个解。

这样的做法是 的,这里 为质数数量,根据前文分析,可以取


较小,可以筛法预处理全部二次剩余,遗憾的是这种做法可以被卡掉。

方法很显然,例如算法取前 个质数 ,则可以另取 个大质数 ,将它们判定结果计算出来,这可以看作 维向量,显然是线性相关的。

则一定可以取若干 ,使得 会被每一个 判定为完全平方数,但实际上只是若干互异质数的积。

若取前 小的质数,则构造复杂度是 的。

具体内容见 EI 的文章

已经有人构造出了 以内质数的 Hack 数据

文件不太大,我决定留个档


洛谷选择加入 Hack 数据后开大时限,Loj 好像没加 Hack 也没开时限,不过卡一卡可以使用同一份代码在两边都过了。

还是 Loj 快,洛谷我是没跑进 1.5s 的,但是也卡成最优解了

一个卡常技巧是我把对每个质数求 Euler 判定的过程展开了,fast_power 对每个质数生成特定版本的快速幂,每个质数都是编译期常数,取模可以特化;在洛谷上从 8s+ 干到 1s+;在 Loj 从 T 到 A。

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
90
91
92
93
94
95
96
97
98
99
100
101
102
103
#include <bits/stdc++.h>
using namespace std;

istream &operator>>(istream &is, __int128 &x) {
string s;
is >> s;
x = 0;
for (char c : s)
x = x * 10 + (c - '0');
return is;
}

template <uint32_t mod> uint64_t fast_power(uint64_t base) {
uint32_t power = (mod - 1) / 2;
uint64_t ret = 1;
for (; power; (base *= base) %= mod, power >>= 1)
if (power & 1)
(ret *= base) %= mod;
return ret;
}

uint64_t get_hash(__int128 s) {
uint64_t val = 0;
#define RUN_HASH(prime) \
val <<= 1, val |= fast_power<prime>(s % prime) != 1
RUN_HASH(1070951741U);
RUN_HASH(1170130207U);
RUN_HASH(1183971839U);
RUN_HASH(1245300233U);
RUN_HASH(1268569829U);
RUN_HASH(1298746283U);
RUN_HASH(1301668909U);
RUN_HASH(1302872999U);
RUN_HASH(1354681189U);
RUN_HASH(1375924369U);
RUN_HASH(1405604119U);
RUN_HASH(1563442729U);
RUN_HASH(1614890171U);
RUN_HASH(1668696331U);
RUN_HASH(1717435817U);
RUN_HASH(1849065671U);
RUN_HASH(1930615781U);
RUN_HASH(2056247341U);
RUN_HASH(2077402687U);
RUN_HASH(2167628587U);
RUN_HASH(2312421899U);
RUN_HASH(2439093781U);
RUN_HASH(2464088981U);
RUN_HASH(2530010933U);
RUN_HASH(2558376851U);
RUN_HASH(2664181133U);
RUN_HASH(2708760871U);
RUN_HASH(2825940619U);
RUN_HASH(2896962637U);
RUN_HASH(2955116233U);
RUN_HASH(3024086707U);
RUN_HASH(3111206317U);
RUN_HASH(3119513693U);
RUN_HASH(3137383213U);
RUN_HASH(3249720911U);
RUN_HASH(3413798779U);
RUN_HASH(3445601713U);
RUN_HASH(3450314113U);
RUN_HASH(3497939789U);
RUN_HASH(3502169413U);
#undef RUN_HASH
return val;
}

int main() {
cin.tie(nullptr)->sync_with_stdio(false);
size_t n;
cin >> n;

vector<uint64_t> hash_val(n + 1);
for (size_t i = 1; i <= n; ++i) {
__int128 x;
cin >> x, hash_val[i] = get_hash(x);
}
for (size_t i = 1; i <= n; ++i)
hash_val[i] ^= hash_val[i - 1];

unordered_map<uint64_t, pair<size_t, vector<size_t>>> hash_map;
hash_map.reserve(n * 1.2);
for (size_t i = 0; i <= n; ++i)
hash_map[hash_val[i]].second.push_back(i);

size_t ans = 0;
for (const auto &[key, val] : hash_map)
ans += val.second.size() * (val.second.size() - 1) >> 1;
cout << ans << '\n';

size_t output_limit = 1e5;
for (size_t i = 0; i < n; ++i) {
auto &[st, vec] = hash_map[hash_val[i]];
for (size_t i = ++st; i < vec.size(); ++i) {
cout << vec[st - 1] + 1 << ' ' << vec[i] << '\n';
if (!--output_limit)
return 0;
}
}
return 0;
}
xxxx

我不要求你的题总是开多倍时限,我不是恶魔。

但是,std 赛后被 Hack 叉掉是怎么回事?你的验题观念怎么了?18 岁可以给 CCF 供假的 std,36 岁开 不让人 Pollard-Pho,72 岁你就变成 74TrAkToR 了。

作为一名 OI 选手,我可能得喷死你,真的。