題意:直接引用潘震皓的論文《置換群快速冪運算》。
[問題描述]
剴剴和凡凡有N張牌(依次標號為1,2,……,N)和一台洗牌機。假設N是奇數。洗牌機的功能是進行如下的操作:對所有位置I(1≤I≤N),如果位置I上的牌是J,而且位置J上的牌是K,那麼通過洗牌機後位置I上的牌將是K。
剴剴首先寫下一個1~N的排列ai,在位置ai處放上數值ai+1的牌,得到的順序x1,x2, ..., xN作為初始順序。他把這種順序排列的牌放入洗牌機洗牌S次,得到牌的順序為p1,p2, ..., pN。現在,剴剴把牌的最後順序和洗牌次數告訴凡凡,要凡凡猜出牌的最初順序x1,
x2,..., xN。
[輸入]
第一行為整數N和S。1≤N≤1000,1≤S≤1000。第二行為牌的最終順序p1,p2, ..., pN。
[輸出]
為一行,即牌的最初順序x1,x2, ..., xN。
註:在置換群中有一個定理:設,(T為一置換,e為單位置換(映射函數為的置換)),那麼k的最小正整數解是T的拆分的所有迴圈長度的最小公倍數。或者有個更一般的結論:設,(T為一迴圈,e為單位置換),那麼k的最小正整數解為T的長度。
[演算法分析]
很顯然,這題的一副撲克牌就是一個置換,而每一次洗牌就是這個置換的平方運算。由於牌的數量是奇數,並且一開始是一個大迴圈,所以做平方運算時候不會分裂。所以,在任意時間,牌的順序所表示的置換一定是一個大迴圈。
那麼根據文章開頭提到的定理:設,(T為一迴圈,e為單位置換),那麼k的最小正整數解為T的長度。
可以知道,這個迴圈的n次方是單位迴圈,換句話說,如果k mod n=1,那麼這個迴圈的k次方,就是它本身。我們知道,每一次洗牌是一次簡單的平方運算,洗x次就是原迴圈的2x次方。
因為n是奇數,2x mod n=1一定有一個<n的整數解,假設這個解是a;那也就是說,一幅牌,洗a次,就會回到原來的順序。使用最終順序不停地洗,直到回到原始順序,求出迴圈節長度a以後,再單純地向前類比a-s次,就可以得到原始順序了。
上面的演算法是出題方給出的標準演算法。顯然,時間複雜度為O(n2+logs)。
換一個方向:給定了結果和s以後,可以簡單地將這個目標置換用3.1節的方法開方s次得到結果。時間複雜度為O(n*s)。
或者可以更簡單地,算出2s,將目標置換直接開2s次方。這裡有一個技巧,因為在開方時只需要在迴圈中前進2s次,所以我們只關心(2s) mod n,也就免去了大數位運算。所以,計算2s需要O(logs),而開方需要O(n)。整個時間複雜度為O(n+logs)。
方法一:
#include<cstdio>#include<cstring>#include<algorithm>using namespace std;#define MAXN 1100int p[MAXN];bool flag[MAXN];void permute ( int n ) //類比置換{ int now, i, tmp[MAXN]; memset(flag,0,sizeof(flag)); for ( i = 1; i <= n; i++ ) { if ( flag[i] ) continue; now = i; while ( flag[now] == 0 ) { flag[now] = 1; tmp[now] = p[p[now]]; now = p[p[now]]; } } for ( i = 1; i <= n; i++ ) p[i] = tmp[i];}int mod_exp ( int a, int b, int n ){ int ret = 1; a = a % n; while ( b >= 1 ) { if ( b & 1 ) ret = ret * a % n; a = a * a % n; b >>= 1; } return ret;}int main(){ int n, s, a, i; scanf("%d%d",&n,&s); for ( i = 1; i <= n; i++ ) scanf("%d",&p[i]); for ( a = 1; mod_exp(2,a,n) != 1; a++ ); s = s % a; for ( i = 1; i <= a - s; i++ ) permute ( n ); for ( i = 1; i <= n; i++ ) printf("%d\n",p[i]); return 0;}