Skip to content

喵~ 主人,欢迎来到我的题解小屋!(ฅ'ω'ฅ)

今天我们要一起解决的是一道关于排列组合的有趣问题:F. Minimize Fixed Points。这道题需要我们动动小脑筋,构造一个满足特殊条件的排列,还要让不动点的数量最少最少。别担心,只要跟着我的思路走,问题就会迎刃而解啦!

题目大意

简单来说,题目给了我们一个整数 n,需要我们完成以下任务:

  1. 构造一个长度为 n排列 p。排列的意思就是,数组里 1nn 个数字,每个都只出现一次。
  2. 这个排列 p 必须是 “好” 的。什么叫“好”呢?就是对于所有 i2np[i]i 的最大公约数(gcd(p[i], i))都必须 大于 1
  3. 在所有满足条件的“好”排列中,我们要找到一个 不动点 数量最少的。
  4. 最后,把这个不动点最少的“好”排列打印出来就可以啦。

喵呜~ 补充一下,不动点 就是指排列中满足 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

对于 i2n,要让 p[i]i 的最大公约数大于 1,一个最直接的想法就是:p[i]i 拥有一个共同的质因数

基于这个想法,我们可以想出一个绝妙的策略:分组

我们可以把 2n 的所有数字,按照它们的 最大质因数 (Largest Prime Factor, LPF) 来分组。比如,对于 n=10

  • 4 的最大质因数是 2
  • 6 的最大质因数是 3
  • 8 的最大质因数是 2
  • 9 的最大质因数是 3
  • 10 的最大质因数是 5

那么,48 就会被分到 LPF=2 的组里,69 就会被分到 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 这个最大质因数。

总结

我们的最终策略就是:

  1. 固定 p[1] = 1
  2. 2n 的所有数字,根据它们的最大质因数进行分组。
  3. 对于每个分组,如果组内多于一个元素,就将它们构造成一个环,避免不动点。
  4. 如果组内只有一个元素,它就只能成为一个不动点。

这样构造出来的排列,既满足“好”排列的条件,其不动点数量也是理论上最少的!


题解

下面我们来看看如何用代码实现这个思路吧!

cpp
#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;
}

代码讲解

  1. sieve_lpf() 函数:

    • 这是一个预处理步骤,它使用类似埃拉托斯特尼筛法(Sieve of Eratosthenes)的思路来计算 2MAXN 之间所有数的最大质因数。
    • 它从 i = 2 开始遍历,如果 i 是一个质数,就遍历 i 的所有倍数 j,并将 largest_prime_factor[j] 更新为 i。因为 i 是从小到大遍历的,所以最后一次更新 largest_prime_factor[j]i 一定是 j 的最大质因数。
    • 这个预处理只需要在所有测试用例开始前执行一次,非常高效!
  2. 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): 排列就是把 1n 的数字重新排个队,要求每个数字都必须出现,且只能出现一次。它是组合数学中的一个基本概念。

  • 最大公约数 (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))预处理出 1N 所有数的最小或最大质因数,为后续的计算提供极大的便利。代码中使用的就是一种 O(N log N) 的筛法变体,已经足够快啦!

希望这篇题解能帮助主人理解这道题目!如果还有其他问题,随时可以再来找我哦~ 喵~ (。・ω・。)ノ♡

Released under the MIT License.