概率加密
【题目描述】
小葱同学为了庆祝题目套数突破150,自创了概率加密算法。现在小葱同学有一个N 位密码,密码的每一位都是一个1—M中的数。现在小葱自创的随机加密算法会给你M个数\(x_i\),代表每次加密的时候,所有为 i 的位都会变成\(x_i\),而当N位密码全部变成和它一开始一样的时候,加密算法停止。所有,给定密码,然后求需要进行多少次加密是一个非常困难的问题,为了简化这个问题,现在小葱假定我们并不知道输入的密码,输入的密码是一个随机的密码,即总共\(M^N\)种可能性,每种可能性的概率位\(M^{-N}\)。小葱同学想知道,在这种情况下,这个随机密码期望需要多少次加密操作。
【输入格式】
一行两个数N, M
加下来一行M个数代表\(x_i\)
【输出格式】
输出一行一个整数,代表期望的次数乘以\(M^N\)模\(10^9 +7\) 的结果
【样例1】
2 2
1 2
【输出1】
4
【样例2】
2 2
2 1
【输出2】
8
【数据规模与约定】
对于40%的数据,\(N,M \leq 5\)
对于70%的数据,\(N,M \leq 20\)
对于100%的数据,\(1 \leq N, M \leq 100, 1 \leq x_i \leq M\),保证所有\(X_i\)互不相等。
Solution
显然每一种序列的答案就是整个序列中每个数的循环节的长度的最小公倍数。
循环节长度就是一个数变成自己所需的次数。
那么怎样快速计算所有的情况呢?
显然枚举所有的排列不现实,但是可以发现,每一种排列的答案与其顺序没有关系,只与其包含几种循环节有关。于是就想能不能状压暴力枚举所有循环节的可能性呢?那我们看一看总共有多少种循环节。
假设第一个数的循环节是1,之后两个的循环节是2,它们两个相互转换,再往后三个,四个……我们发现\(1 +2 + 3 +……+14 > 100\),也就是说,最多只会有14种循环节。因为我们假设有的循环节特别长,也就是说这个循环节中包含很多的数,那么总共的循环节种类就会少。最多只能有14种。同时一个循环节内的数的周期是一样的,就相当于一个圈,每次转一定角度,当它转回到最一开始的时候,所有点转的度数是一样的,因此可以一块考虑。
于是我们状压了所有的排列,但是怎么统计每种排列有多少个呢?
假设当前排列有x个循环节,这些循环节里包含sum个数,那么当前排列的种数就是\(sum ^ N\),这显然是不对的,因为有可能这N个数选的都是一种循环节,所有还要减去所有包含两种循环节的排列情况,但这也不对……于是就发现这是个容斥。
然后这题就做完了。
#include <iostream>
#include <cstdio>
using namespace std;
inline long long read() {
long long x = 0; int f = 0; char c = getchar();
while (c < '0' || c > '9') f |= c == '-', c= getchar();
while (c >= '0' && c <= '9') x = (x << 1) + (x << 3) + (c ^ 48), c = getchar();
return f ? -x : x;
}
const int mod = 1e9 + 7;
int n, m, x[105], c[105], z[2][105];
bool vis[105];
inline int gcd(int a, int b) {
if (!b) return a;
return gcd(b, a % b);
}
inline int mul(int a, int b) {
int ans = 1;
while (b) {
if (b & 1) ans = 1ll * ans * a % mod;
a = 1ll * a * a % mod; b >>= 1;
}
return ans;
}
int main() {
freopen("prob.in", "r", stdin);
freopen("prob.out", "w", stdout);
n = read(); m = read();
for (int i = 1; i <= m; ++i) x[i] = read();
for (int i = 1; i <= m; ++i)
if (!vis[i]) {//计算循环节
int p = x[i], cnt = 1;
while (p != i) vis[p] = 1, p = x[p], cnt++;
vis[i] = 1; c[cnt] += cnt;
}
int p = 0;
for (int i = 1; i <= m; ++i)
if (c[i])//记录每个循环节的长度以及这个长度的点的个数
z[0][p] = i, z[1][p] = c[i], p++;
long long ans = 0;
for (int i = 1; i < (1 << p); ++i) {//枚举所有情况
int num_1 = 0, lcm = 1;long long sum = 0;
for (int j = 0; j < p; ++j)
if (i & (1 << j)) {//计算有多少种循环节
num_1++;
lcm = lcm * z[0][j] / gcd(lcm, z[0][j]);
}
for (int j = i; j; j = (j - 1) & i) {//枚举子集
int num_2 = 0, tot = 0, lcm = 1;
for (int k = 0; k < p; ++k)
if (j & (1 << k)) {
num_2++;
lcm = lcm * z[0][k] / gcd(lcm, z[0][k]);
tot += z[1][k];
}
//判断子集是加还是减
if ((num_1 & 1) != (num_2 & 1)) sum = (0ll + sum - mul(tot, n) + mod) % mod;
else sum = (0ll + sum + mul(tot, n)) % mod;
}
ans = (0ll + ans + sum * lcm) % mod;
}
printf("%lld\n", ans);
return 0;
}