题目地址
https://www.luogu.com.cn/problem/P6280
题解
对于某个排列(A),题目的问题其实可以等价于对于每个(i),连一条((i,a_i))的边,求每个环大小的(lcm)。(因为要回到原位必然要绕圈,每个点都恰好绕一圈的步数就是( ext{lcm}),这样就可以恢复成原排列了)
但是转化后仍然难以处理本题的数据范围,每一个环的大小是任意的。
考虑这道题的特殊性质:因为是排列连边,所以一定只有(n)条边,不会有独立的点,一定全都是环,所以环的大小的和就是(n)。
那么问题可以再次转化为:将(n)拆分成若干个数,设(k)为这些数的(lcm),求所有不同(k)值的和。
其次考虑一下(lcm)的一个素数表达形式,(lcm{x_i}=prod {p_i^{max{k_i}}}),我们可以很自然地想到利用动态规划来计算答案。
不妨设(f[i][j])表示利用前(i)个素数,它们的和为(j)时的答案。这个形式又很像经典的背包dp的形式,因为空间限制的问题,我们可以借鉴01背包,设(f[i])表示所用的素数和为(j)时的答案,转移时倒序转移得到答案即可。
综上,转移方程即为(f[i]=sum {f[i-p^k]*p^k})
设(num_p)为([1,n])内质数个数,则复杂度为(O( num_p cdot n cdot log n))
代码实现如下:
#include <bits/stdc++.h>
using namespace std;
const int N = 1e4+5;
int n, mod, tot, f[N], p[N], v[N];
int main() {
cin >> n >> mod;
for(int i = 2; i <= n; ++i) {
if(!v[i]) p[++tot] = i;
for(int j = 1; i * p[j] <= n && j <= tot; ++j) {
v[i * p[j]] = 1;
if(i % p[j] == 0) break;
}
}
f[0] = 1;
for(int i = 1; i <= tot; ++i) {
for(int j = n; j >= p[i]; --j) {
int P = p[i];
for(; P <= j;) {
f[j] = (f[j] + 1LL * f[j - P] * P % mod) % mod;
P *= p[i];
}
}
}
int ans = 0;
for(int i = 0; i <= n; ++i) ans = (ans + f[i]) % mod;
printf("%d
", ans);
return 0;
}