喵~ 主人,欢迎来到我的题解小屋!(ฅ'ω'ฅ)
今天我们要一起解决的是一道关于排列组合的有趣问题:F. Minimize Fixed Points。这道题需要我们动动小脑筋,构造一个满足特殊条件的排列,还要让不动点的数量最少最少。别担心,只要跟着我的思路走,问题就会迎刃而解啦!
题目大意
简单来说,题目给了我们一个整数 n
,需要我们完成以下任务:
- 构造一个长度为
n
的 排列p
。排列的意思就是,数组里1
到n
这n
个数字,每个都只出现一次。 - 这个排列
p
必须是 “好” 的。什么叫“好”呢?就是对于所有i
从2
到n
,p[i]
和i
的最大公约数(gcd(p[i], i)
)都必须 大于 1。 - 在所有满足条件的“好”排列中,我们要找到一个 不动点 数量最少的。
- 最后,把这个不动点最少的“好”排列打印出来就可以啦。
喵呜~ 补充一下,不动点 就是指排列中满足
p[j] = j
的位置j
哦。就像小猫照镜子,看到的就是自己一样!
题解方法
这道题的关键在于如何构造这个排列 p
,既要满足 gcd
条件,又要最小化不动点。让本喵来带你一步步分析吧!
关键点一:数字 1 的位置
首先,我们来思考一下最特殊的数字 1
应该放在哪里捏? 排列 p
中必须有 1
这个值。假设我们把它放在 j
位置上,即 p[j] = 1
。
- 如果
j >= 2
,那么根据“好”排列的定义,需要满足gcd(p[j], j) > 1
。但是gcd(1, j)
永远等于1
,这不就违反条件了嘛! - 所以,值
1
不能放在任何j >= 2
的位置上。它唯一能去的地方就是位置1
。
因此,我们得到了第一个结论:p[1]
必须等于 1
。这意味着 1
永远是一个不动点,这是我们无法避免的第一个不动点呢。
关键点二:如何满足 gcd(p[i], i) > 1
对于 i
从 2
到 n
,要让 p[i]
和 i
的最大公约数大于 1,一个最直接的想法就是:让 p[i]
和 i
拥有一个共同的质因数。
基于这个想法,我们可以想出一个绝妙的策略:分组!
我们可以把 2
到 n
的所有数字,按照它们的 最大质因数 (Largest Prime Factor, LPF) 来分组。比如,对于 n=10
:
4
的最大质因数是2
。6
的最大质因数是3
。8
的最大质因数是2
。9
的最大质因数是3
。10
的最大质因数是5
。
那么,4
和 8
就会被分到 LPF=2 的组里,6
和 9
就会被分到 LPF=3 的组里。
如果我们规定,p[i]
的值必须从和 i
同一个分组的数字里选,那么 gcd
条件就自然满足啦!因为同一个分组里的任意两个数,它们的最大质因数相同,所以它们的最大公约数也必然大于 1
。
关键点三:如何最小化不动点
分组之后,事情就简单多啦!为了最小化不动点,我们希望 p[i] ≠ i
尽可能多地成立。
对于一个拥有多个成员的组,比如
{c₁, c₂, ..., cₖ}
,我们可以让它们手拉手转个圈圈,形成一个环。也就是构造p[c₁] = c₂
,p[c₂] = c₃
, ...,p[cₖ] = c₁
。这样一来,这个组里的所有元素都不是不动点,耶!我们成功避免了k
个不动点。对于一个只拥有一个成员的组,比如
{i}
,那就没办法啦。根据我们的策略,p[i]
必须从它自己的组里选,那唯一的选择就是p[i] = i
。这就成了另一个无法避免的不动点啦,喵呜~ 这种情况通常发生在那些比较“孤独”的数上,比如一个大于n/2
的质数p
,在[2, n]
的范围内,只有它自己拥有p
这个最大质因数。
总结
我们的最终策略就是:
- 固定
p[1] = 1
。 - 对
2
到n
的所有数字,根据它们的最大质因数进行分组。 - 对于每个分组,如果组内多于一个元素,就将它们构造成一个环,避免不动点。
- 如果组内只有一个元素,它就只能成为一个不动点。
这样构造出来的排列,既满足“好”排列的条件,其不动点数量也是理论上最少的!
题解
下面我们来看看如何用代码实现这个思路吧!
#include <iostream>
#include <vector>
#include <numeric>
#include <map>
#include <algorithm>
// 预处理最大质因数,直到 MAXN
const int MAXN = 100005;
std::vector<int> largest_prime_factor(MAXN);
void sieve_lpf() {
std::iota(largest_prime_factor.begin(), largest_prime_factor.end(), 0);
for (int i = 2; i < MAXN; ++i) {
if (largest_prime_factor[i] == i) { // i 是一个质数
for (long long j = 2LL * i; j < MAXN; j += i) {
largest_prime_factor[j] = i;
}
}
}
}
void solve() {
int n;
std::cin >> n;
std::vector<int> p(n + 1);
// 1. 将 2 到 n 的数字按最大质因数分组
// 使用 map 来存储分组,key 是质因数,value 是数字列表
std::map<int, std::vector<int>> groups;
for (int i = 2; i <= n; ++i) {
groups[largest_prime_factor[i]].push_back(i);
}
// 2. p[1] 必须是 1
p[1] = 1;
// 3. 遍历每个分组,构造排列
for (auto const& [prime, group_vec] : groups) {
if (group_vec.size() == 1) {
// 如果组里只有一个元素,它必须映射到自身,成为不动点
p[group_vec[0]] = group_vec[0];
} else {
// 如果组里有多个元素,构造成一个环来避免不动点
// p[c₁] = c₂, p[c₂] = c₃, ..., p[cₖ] = c₁
for (size_t i = 0; i < group_vec.size() - 1; ++i) {
p[group_vec[i]] = group_vec[i+1];
}
p[group_vec.back()] = group_vec[0];
}
}
// 4. 打印结果
for (int i = 1; i <= n; ++i) {
std::cout << p[i] << (i == n ? "" : " ");
}
std::cout << "\n";
}
int main() {
std::ios_base::sync_with_stdio(false);
std::cin.tie(NULL);
// 在所有测试用例前,一次性预处理好最大质因数
sieve_lpf();
int t;
std::cin >> t;
while (t--) {
solve();
}
return 0;
}
代码讲解
sieve_lpf()
函数:- 这是一个预处理步骤,它使用类似埃拉托斯特尼筛法(Sieve of Eratosthenes)的思路来计算
2
到MAXN
之间所有数的最大质因数。 - 它从
i = 2
开始遍历,如果i
是一个质数,就遍历i
的所有倍数j
,并将largest_prime_factor[j]
更新为i
。因为i
是从小到大遍历的,所以最后一次更新largest_prime_factor[j]
的i
一定是j
的最大质因数。 - 这个预处理只需要在所有测试用例开始前执行一次,非常高效!
- 这是一个预处理步骤,它使用类似埃拉托斯特尼筛法(Sieve of Eratosthenes)的思路来计算
solve()
函数:- 分组: 我们用一个
std::map
,名为groups
,来存储分组结果。map
的键(key)是最大质因数,值(value)是一个std::vector
,里面装着所有拥有这个最大质因数的数字。 p[1] = 1
: 根据我们的分析,这是必须的。- 构造排列:
- 我们遍历
groups
里的每一个分组group_vec
。 - 如果
group_vec.size() == 1
,说明这个组里只有一个孤零零的数字,我们只能让它成为不动点,即p[x] = x
。 - 如果
group_vec.size() > 1
,我们就执行一个“循环位移”来构造一个环。代码p[group_vec[i]] = group_vec[i+1]
和p[group_vec.back()] = group_vec[0]
就是在做这件事。这样就完美地避免了不动点!
- 我们遍历
- 输出: 最后,循环打印出我们精心构造的排列
p
就大功告成啦!(^• ω •^)
- 分组: 我们用一个
知识点介绍
这道题涉及了几个有趣的数学和算法概念,本喵来给你科普一下~
排列 (Permutation): 排列就是把
1
到n
的数字重新排个队,要求每个数字都必须出现,且只能出现一次。它是组合数学中的一个基本概念。最大公约数 (Greatest Common Divisor, GCD): 两个或多个整数共有的约数中最大的一个。比如
gcd(12, 18) = 6
。这是数论的基础。不动点 (Fixed Point): 在一个排列
p
中,如果p[i] = i
,那么i
就是一个不动点。在很多领域,比如数学和物理中,不动点都是一个非常重要的概念。筛法求最大质因数 (Sieve for LPF): 这是一个非常实用的预处理技巧。如果我们需要对一个范围内的很多数进行质因数相关的计算,逐个分解效率会很低。使用筛法,我们可以在接近线性的时间复杂度内(
O(N log log N)
或O(N)
)预处理出1
到N
所有数的最小或最大质因数,为后续的计算提供极大的便利。代码中使用的就是一种O(N log N)
的筛法变体,已经足够快啦!
希望这篇题解能帮助主人理解这道题目!如果还有其他问题,随时可以再来找我哦~ 喵~ (。・ω・。)ノ♡