【題目描述】
小蔥同學爲了慶祝題目套數突破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;
}