这个题目的做法有很多种,大部分选择了找循环节的做法,但是个人感觉这样的做法有点暴力,所以使用了置换开方的做法来解决这个问题,时间复杂度O(n)。
首先评价一波:是个好题!
然后开始分析:给你N张卡片和一个洗牌机,如果位置I上的牌是J,位置J上的牌是K,那么洗牌一次后位置I上的牌就是K。首先建立一个位置->卡片的一个置换T,第一次洗牌就相当于T*T,第二次洗牌就是(T*T)*(T*T)……第K次洗牌就是T^(2^K),这就是置换的幂运算了。题目给出洗牌K次以后的置换,要求出原始置换,便需要开方运算。在kuangbin推荐的论文中我学习了开方运算,发现这是一种相对平方比较复杂的运算,因为在平方过程中,置换环会分裂,在开方往上合并的时候就难了,甚至答案都不是唯一的,但这里题目简化了,降低了开方的难度。首先牌的摆放决定了置换环长度为N,其次N为奇数,所以长度为N的置换环,经过一次平方,会分裂为gcd(2,N)个置换环,gcd(2,N)=1,所以这个置换环永远不会被分裂,这样开方就简单多了。
首先把题目给出的置换环写出来,假设是X1 -> X2 -> X3 -> X4 -> X5,但这是经过2^K运算之后的,X1还是那个X1,X1位置都是0,但是X2和X1中间却隔了2^K个数,也就是X2在目前置换环的位置为2,但在原置换环的位置却是2^K,X3在目前置换环的位置为3,但在原置换环的位置却是2^K+2^K…… Xi的原位置就是(i-1)*(2^K),所以通过这样的重新定位,便可以求出原先的置换环,那也就找到了原先的置换,输出答案就好了。
如果不懂位置为什么是这样变的,可以随便找一个置换环,对其做幂运算,就会发现相邻两值的差距变化了,论文中也有详细的介绍
总结:题目考察对置换环正负幂运算的理解,当然也可以理解为对置换幂运算具有循环节特性的考察。
代码如下:
#include<cstdio> #include<cstring> #include<iostream> #include<vector> #include<algorithm> using namespace std; #define LL long long const int N = 1005; bool vis[N]; vector<int > H; int n,s,to[N],ans[N],res[N]; int qpow(int x,int y,int Mod) { int res = 1; while(y) { if(y&1) { res = (res*x)%Mod; } x = (x*x)%Mod; y >>= 1; } return res; } int main() { while(EOF != scanf("%d%d",&n,&s)) { for(int i=1; i<=n; i++) { int y; scanf("%d",&y); to[i] = y; ///建立洗牌后的置换 } memset(vis,0,sizeof(vis)); int x = 1; H.clear(); while(!vis[x]) { H.push_back(x); ///用来存储洗牌后的置换环 vis[x] = 1; x = to[x]; } // for(int i=0;i<H.size();i++){ // cout<<H[i]<<" "; // }cout<<endl; int ss = qpow(2,s,n); for(int i=0; i<n; i++) { int j = (i*ss)%n; ///找到i位置的原位置j ans[j] = H[i]; ///给原置换环赋值 } for(int i=0; i<n; i++) { int id = ans[i]; int num = ans[(i+1)%n]; res[id] = num; ///将置换环恢复为置换的形式 } for(int i=1; i<=n; i++) { printf("%d\n",res[i]); } } return 0; }