BogoSort - 题解
标签与难度
标签: 置换群, 循环分解, 最小公倍数(LCM), 高精度计算, 数论, 图论 难度: 2200
题目大意喵~
你好呀,指挥官!今天我们遇到了一个叫 Tonnnny 的可爱猴子,他“改进”了一下著名的 Bogo Sort(猴子排序)算法,喵~
原本的 Bogo Sort 是不断地随机打乱数组,直到数组有序为止。但 Tonnnny 不喜欢随机,所以他选了一个自己最喜欢的、长度为 的固定排列 ,用它来代替随机打乱。
Tonnnny 的 shuffle 操作是这样的:对于一个数组 a,新的数组 a_new 的第 i 个元素 a_new[i] 会被赋值为旧数组 a 的第 p[i] 个元素。他会重复这个操作,直到数组变得有序(即 a[0] <= a[1] <= ... <= a[N-1])为止。
我们作为 Tonnnny 的好朋友,想让他尽可能久地开心下去。所以,我们的任务是计算出,总共有多少个不同的初始数组,可以通过 Tonnnny 的排序算法最终变得有序。
因为答案可能会非常非常大,所以我们需要将结果对 取模。 最大可以到 1000 哦!
解题思路分析
这道题看起来有点绕,但别担心,跟着我的思路一步步来,很快就能理清啦,喵~
第一步:理解 Tonnnny 的排序过程
首先,我们来分析一下 Tonnnny 的 shuffle 操作。a_new[i] = a_old[p[i]]。这是一个确定性的操作。如果我们把初始数组叫做 ,经过一次 shuffle 后得到 ,再 shuffle 一次得到 ,以此类推。
一个初始数组 如果能被排序,就意味着在序列 中,至少有一个数组 是有序的。
有序的条件是:对于所有的 ,都有 。 我们来追溯一下 的元素和 的关系: ... 经过 次操作后,p 这个置换作用了 次,我们记作 。所以 。
于是,数组 能被排序,等价于存在一个非负整数 ,使得对于所有 ,都满足:
这个不等式链告诉我们,初始数组 的元素值,必须在按照 p^k 作用后的下标序列 (p^k(0), p^k(1), ..., p^k(N-1)) 上是单调不减的。
第二步:一个关键的简化!
题干中说“一个不同的数组”,但没有规定数组里的数必须是什么。如果我们可以从整数集合 中任意取值,那么满足 x <= y 的数组有无限多个,这样一来,可排序的数组也会有无限多个。这显然不是题目的本意,喵~
我们注意到,答案需要对 取模。这是一个非常强的暗示!它告诉我们,答案是一个具体的、非常大的整数。这引导我们去思考一个更受限制、但更典型的场景:如果初始数组是 {1, 2, ..., N} 的一个排列呢?
让我们来分析一下这个简化后的情况。 假设我们的数组包含的是 个不同的值,不妨设为 。那么,一个有序的数组必须是 (v_1, v_2, ..., v_N)。
如果一个初始排列 π (一个数组) 经过 k 次 shuffle 后变得有序,那就意味着:
结合 ,我们得到:
这说明,对于一个给定的 k,初始数组 a_0 是唯一确定的!它的第 j 个元素 a_0[j] 应该是哪个 值呢?令 ,则 ,所以 。
这意味着,对于每个 k,都对应着一个可能的可排序的初始数组。这些数组会不会重复呢? 两个数组 和 相同,当且仅当置换 和 相同。这等价于 是单位置换(即所有元素都映射到自身)。 这只有在 是置换 的阶 (order) 的倍数时才会发生。
置换 的阶,是指最小的正整数 m 使得 是单位置换。所以,在 的范围内,我们能得到 个不同的可排序初始数组。
因此,问题的核心就转化为了求置换 p 的阶!
第三步:计算置换的阶
一个置换的阶怎么求呢?这需要我们将置换分解成不相交的循环(disjoint cycles),喵~ 一个置huan的阶,等于它所有不相交循环的长度的最小公倍数 (LCM)。
举个例子:如果 , (1-indexed)。 我们来找循环:
- 从 1 开始:。这是一个长度为 3 的循环 (1 2 3)。
- 从 4 开始:。这是一个长度为 2 的循环 (4 5)。
这个置换 被分解成了两个循环,长度分别为 3 和 2。 那么 ord(p) = lcm(3, 2) = 6。 在这个例子里,就有 6 个不同的初始排列可以被 Tonnnny 排序。
第四步:处理大数计算
现在我们的任务清单很清晰了:
- 找到置换
p的所有不相交循环的长度 。 - 计算 。
- 将结果对 取模。
计算 LCM 的经典方法是利用质因数分解:
其中 是 的质因数分解中质数 的指数。
所以,我们的算法是:
- 找出所有循环长度:用一个
visited数组,遍历0到N-1,如果一个下标i没被访问过,就从它开始沿着p走,直到回到i,记录下一个循环的长度。 - 质因数分解:对每个循环长度 ,进行质因数分解。
- 统计最大指数:用一个 map 或者哈希表
max_exponents记录每个质数出现过的最大指数。例如,如果 ,,那么对于质数 2,最大指数是 2;对于质数 3,最大指数也是 2。 - 计算最终结果:最终的 LCM 就是所有
q^max_exponents[q]的乘积。
由于 高达 1000,模数是 ,这是一个非常大的数。我们需要用高精度计算(大数运算)来实现乘法。我们可以用 string 来存储大整数,然后手写一个大数乘法函数。每次乘法后,如果结果的位数超过 ,我们只需要保留末尾的 位即可,这正是对 取模的效果!
好啦,思路已经很清晰了,准备好和我一起实现它了吗?Let's go, meow!
代码实现
#include <iostream>
#include <vector>
#include <string>
#include <numeric>
#include <algorithm>
#include <map>
// 高精度乘法:大数(string) * 整数(int)
// 结果需要对 10^N 取模,通过只保留末尾N位实现
std::string big_num_multiply_int(const std::string& num_str, int factor, int N) {
if (factor == 0) return "0";
if (factor == 1) return num_str;
std::string result = "";
int carry = 0;
// 从低位到高位(字符串末尾到开头)计算
for (int i = num_str.length() - 1; i >= 0; --i) {
int digit = num_str[i] - '0';
int temp = digit * factor + carry;
result += std::to_string(temp % 10);
carry = temp / 10;
}
while (carry > 0) {
result += std::to_string(carry % 10);
carry /= 10;
}
// 翻转得到正常顺序的数
std::reverse(result.begin(), result.end());
// 对 10^N 取模,即截取末尾 N 位
if (result.length() > N) {
return result.substr(result.length() - N);
}
return result;
}
// 高精度乘法:大数(string) * 大数(string)
// 结果需要对 10^N 取模
std::string big_num_multiply_str(const std::string& num1, const std::string& num2, int N) {
if (num1 == "0" || num2 == "0") return "0";
int len1 = num1.length();
int len2 = num2.length();
// 结果最多 len1 + len2 位
std::vector<int> res_vec(len1 + len2, 0);
for (int i = len1 - 1; i >= 0; i--) {
for (int j = len2 - 1; j >= 0; j--) {
int mul = (num1[i] - '0') * (num2[j] - '0');
// 乘积加到结果的对应位置
int sum = res_vec[i + j + 1] + mul;
res_vec[i + j + 1] = sum % 10;
res_vec[i + j] += sum / 10;
}
}
std::string result_str = "";
for (int digit : res_vec) {
// 去掉前导0
if (!(result_str.empty() && digit == 0)) {
result_str += std::to_string(digit);
}
}
if (result_str.empty()) return "0";
// 对 10^N 取模
if (result_str.length() > N) {
return result_str.substr(result_str.length() - N);
}
return result_str;
}
// 高精度快速幂:(base^exp) mod 10^N
std::string big_num_power(int base, int exp, int N) {
std::string result = "1";
std::string base_str = std::to_string(base);
while (exp > 0) {
if (exp % 2 == 1) {
result = big_num_multiply_str(result, base_str, N);
}
base_str = big_num_multiply_str(base_str, base_str, N);
exp /= 2;
}
return result;
}
int main() {
std::ios_base::sync_with_stdio(false);
std::cin.tie(NULL);
int N;
std::cin >> N;
// 题目给的是1-indexed,我们转成0-indexed方便处理
std::vector<int> p(N);
for (int i = 0; i < N; ++i) {
std::cin >> p[i];
p[i]--;
}
// 1. 找出所有循环长度
std::vector<int> cycle_lengths;
std::vector<bool> visited(N, false);
for (int i = 0; i < N; ++i) {
if (!visited[i]) {
int current = i;
int length = 0;
while (!visited[current]) {
visited[current] = true;
current = p[current];
length++;
}
cycle_lengths.push_back(length);
}
}
// 2. 找出所有质数,并对每个循环长度进行质因数分解
std::map<int, int> max_exponents;
for (int len : cycle_lengths) {
int temp_len = len;
for (int i = 2; i * i <= temp_len; ++i) {
if (temp_len % i == 0) {
int count = 0;
while (temp_len % i == 0) {
temp_len /= i;
count++;
}
max_exponents[i] = std::max(max_exponents[i], count);
}
}
if (temp_len > 1) { // 剩余的是一个大质数
max_exponents[temp_len] = std::max(max_exponents[temp_len], 1);
}
}
// 3. 用高精度计算LCM
std::string lcm_result = "1";
for (auto const& [prime, exponent] : max_exponents) {
std::string prime_power = big_num_power(prime, exponent, N);
lcm_result = big_num_multiply_str(lcm_result, prime_power, N);
}
// 4. 格式化输出,补足前导0
std::string final_answer = lcm_result;
while (final_answer.length() < N) {
final_answer = "0" + final_answer;
}
std::cout << final_answer << std::endl;
return 0;
}复杂度分析
时间复杂度:
- 寻找循环: 我们遍历了每个元素一次,所以是 。
- 质因数分解: 对每个循环长度 分解,最坏情况是 。所有循环长度之和为 ,但这不是一个紧凑的上界。一个更好的方法是,我们可以先用 的筛法预处理出质数,然后对每个 进行分解,这会快很多。总的分解时间大约在 级别。
- 高精度计算: 设小于等于 的质数有 个()。对于每个质数 和其最大指数 ,我们计算 再乘入总结果。
power操作中的乘法是小数乘法,很快。主要开销是lcm_result = multiply(lcm_result, prime_power)。lcm_result的长度最多为 ,prime_power的长度相对较小。一次乘法最坏是 。总的来说,高精度部分的时间复杂度近似为 ,或者更宽松地写为 。 - 总时间复杂度由高精度计算主导,约为 。
空间复杂度:
- 我们存储了置换
p,visited数组,循环长度等,都需要 的空间。 max_exponents这个 map 最多存储 个质数,也是 。- 高精度计算中,
string的长度最大为 。 - 所以总空间复杂度为 。
- 我们存储了置换
知识点总结
这真是一次有趣的冒险,喵~ 我们从一个看似简单的排序问题,一路探索到了数论和高精度计算的深处!
- 问题转化: 关键在于将“可排序数组的数量”这个问题,通过分析和简化,转化为了计算一个置换的“阶”。这是解题的突破口!
- 置换群与阶: 理解了置换、循环分解和阶的概念。一个置换的阶等于其所有不相交循环长度的最小公倍数(LCM)。
- LCM的计算: 当数值很大时,计算LCM的最佳方式是通过质因数分解,找到每个质因子的最高次幂,然后相乘。
- 高精度计算: 当常规数据类型(如
long long)无法存储结果或模数时,就需要自己实现大数运算。本题中对 取模,可以通过字符串操作,只保留末尾 位来实现,这是一种常见的高精度取模技巧。
希望这篇题解能帮到你,指挥官!下次遇到难题,也请随时来找我哦,喵~