喵~ 主人,欢迎来到我的题解小屋!(ฅ'ω'ฅ)
今天我们要一起解决的是一道关于排列组合的有趣问题: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)的筛法变体,已经足够快啦!
希望这篇题解能帮助主人理解这道题目!如果还有其他问题,随时可以再来找我哦~ 喵~ (。・ω・。)ノ♡