[POI 2011] SEJ-Strongbox
原题目:
luogu-P3518
简要描述:
这是一道非常经典的数论题目,考察了对模运算、最大公约数(GCD)以及群论基础(循环群子群)的理解。
下面我将从数学原理、解题思路到具体的算法优化为你详细分析这道题。
1. 数学模型与题意转化
题目要求找一个密码集合
核心性质:
- 子群结构:
的任何子群都是循环群,且由 的某个因子 生成。 即 ,其中 。 集合的大小(密码个数)为 。 - 目标:我们要让密码个数
最大,等价于要让生成元 最小。
2. 限制条件分析
题目给出了一些尝试记录:
- 失败的尝试 (
):这些数不在集合 中。 这意味着它们不能被 整除。即: 。 - 成功的尝试 (
):这个数在集合 中。 这意味着它是 的倍数。即: 。
综合条件:
我们需要找到一个整数
是 的因子 ( )。 是 的因子 ( )。 - 由 1 和 2 可知,
必须是 的因子。记 。
- 由 1 和 2 可知,
- 对于所有
, 不是 的因子。 - 注意:因为
,判断 “ 不是 的因子” 等价于判断 “ 不是 的因子”。如果是 的因子,也就是 的因子。进一步地,因为我们只在 的因子中找 ,所以这等价于判断 不是 的因子。
- 注意:因为
3. 朴素算法 vs 优化算法
朴素思路:
找出
- 问题:虽然
的因子个数通常不多,但 的数量 高达 。每次枚举 都要遍历一遍 ,时间复杂度约为 ,可能会超时。
优化思路(利用因子的整除性质): 我们可以反向思考:哪些因子是不行的?
- 计算
,找出 的所有因子,存入数组 divs。 - 对于每个失败的尝试
,计算 。 显然也是 的因子。 - 如果
能整除 ,那么 一定能整除 。 - 因此,
以及 的所有因子都不能作为我们要找的 。
- 我们可以标记所有“被禁用”的因子。
- 首先标记所有出现的
。 - 然后进行传递:如果一个数被禁用了,它的所有因子也应该被禁用。为了高效,我们把
divs从大到小遍历,如果divs[j]被标记了,且divs[i]是divs[j]的因子,那么divs[i]也标记为不可用。
- 首先标记所有出现的
- 最后,在未被标记的因子中,取最小的那个作为
。
4. 算法流程详解
- 初始化:读取
和数组 。 - 计算基准 G:
。 - 获取因子:求出
的所有因子,存入数组 divs并排序。- 由于
, ,可以直接枚举求解。因子个数(记为 )对于 范围内的数通常很少(几千到一两万)。
- 由于
- 标记初始冲突:
- 建立一个布尔数组
bad[]对应divs。 - 遍历
到 : - 计算
。 - 在
divs中二分查找的位置,将该位置标记为 bad = true。
- 计算
- 建立一个布尔数组
- 向下传播标记(优化核心):
- 如果我们暴力两层循环判断
,复杂度是 ,当 时约为 ,有点危险但可能卡过。 - 更优做法:预处理
的所有质因子 。 - 从大到小遍历
divs的每个元素divs[i]。 - 如果
divs[i]是bad的:- 遍历所有质因子
。 - 如果
divs[i]能被整除,说明 divs[i] / p也是的因子。 - 在
divs中找到divs[i] / p的位置,将其标记为bad。
- 遍历所有质因子
- 这种做法复杂度大幅降低,接近
。
- 如果我们暴力两层循环判断
- 寻找答案:
- 从小到大遍历
divs,找到第一个bad为false的因子。 - 输出答案
。
- 从小到大遍历
5. 代码实现参考 (C++)
cpp
copy
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 <iostream>
#include <vector>
#include <algorithm>
#include <numeric>
using namespace std;
typedef long long ll;
// 计算最大公约数
ll gcd(ll a, ll b) {
while (b) {
a %= b;
swap(a, b);
}
return a;
}
int main() {
// 优化 I/O
ios_base::sync_with_stdio(false);
cin.tie(NULL);
ll n;
int k;
if (!(cin >> n >> k)) return 0;
vector<ll> m(k);
for (int i = 0; i < k; ++i) {
cin >> m[i];
}
// 1. 计算 G = gcd(m_k, n)
// 题目中 m 是 0-indexed 输入,所以第 k 次是 m[k-1]
ll mk = m[k - 1];
ll G = gcd(mk, n);
// 2. 找出 G 的所有因子
vector<ll> divs;
for (ll i = 1; i * i <= G; ++i) {
if (G % i == 0) {
divs.push_back(i);
if (i * i != G) {
divs.push_back(G / i);
}
}
}
sort(divs.begin(), divs.end());
// 3. 标记初始冲突
// bad[i] 表示 divs[i] 是否不能作为 d
vector<bool> bad(divs.size(), false);
for (int i = 0; i < k - 1; ++i) {
ll val = gcd(m[i], G);
// 二分查找 val 在 divs 中的位置
auto it = lower_bound(divs.begin(), divs.end(), val);
if (it != divs.end() && *it == val) {
int idx = distance(divs.begin(), it);
bad[idx] = true;
}
}
// 4. 传播标记
// 找出 G 的所有质因子,用于加速传播
vector<ll> prime_factors;
ll tempG = G;
for (ll i = 2; i * i <= tempG; ++i) {
if (tempG % i == 0) {
prime_factors.push_back(i);
while (tempG % i == 0) tempG /= i;
}
}
if (tempG > 1) prime_factors.push_back(tempG);
// 从大到小遍历
for (int i = divs.size() - 1; i >= 0; --i) {
if (bad[i]) {
ll current = divs[i];
for (ll p : prime_factors) {
if (current % p == 0) {
ll target = current / p;
// 二分查找 target 的位置
auto it = lower_bound(divs.begin(), divs.end(), target);
if (it != divs.end() && *it == target) {
bad[distance(divs.begin(), it)] = true;
}
}
}
}
}
// 5. 寻找最小的合法 d
for (int i = 0; i < divs.size(); ++i) {
if (!bad[i]) {
cout << n / divs[i] << endl;
return 0;
}
}
return 0;
}
总结
- 复杂度:
- 求因子:
(实际上是 )。 - 初始标记:
。 - 传播标记:
。 - 整体能够轻松通过
和 的数据规模。
- 求因子:
- 关键点:
- 意识到题目求的是
的子群。 - 利用
缩小搜索范围。 - 利用
也不合法的性质,从大到小筛选因子。
- 意识到题目求的是