关于完美洗牌问题的若干思考

本文深入探讨了完美洗牌算法及其逆变换,并扩展至多序列的洗牌问题。通过具体的算法实现,包括循环移位、逆变换等操作,解决了不同场景下的洗牌需求。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >


前面学习了完美洗牌问题

完美洗牌算法学习

又写了一个证明

完美洗牌问题的证明


进一步思考了其他的一些问题:

完美洗牌问题: 给定的输入a1, a2, a3, ……aN, b1,b2,……bN,输出b1,a1,b2,a2,b3,a3…… bN,aN

(1) 如果要求输出是a1,b1,a2,b2……aN,bN怎么办?

这个问题在学习的时候已经考虑过,只是觉得如果先把a部分和b部分交换掉,或者最后再交换相邻的一组两个位置的方法不够美观。

现在想想可以这样,原数组第一个和最后一个不变,中间的2 * (n - 1)项用原始的标准完美洗牌算法做就可以了。

(2) 完美洗牌问题的逆问题:

给定b1,a1,b2,a2,……bN,aN, 输出a1,a2,a3,……aN,b1,b2,b3,……bN

这相当于把偶数位上的数放到一起,奇数位上的数放到一起。

关键问题: 我们需要把cycle_leader算法改一下,沿着圈换回去。改造后的叫reverse_cycle_leader,代码如下:

  1. //逆变换,数组下标从1开始,from是圈的头部,mod是要取模的数mod应该为2*n+1,时间复杂度O(圈长)
  2. voidreverse_cycle_leader(int*a,intfrom,intmod){
  3. intlast=a[from],next,i;
  4. for(i=from;;i=next){
  5. next=i*2%mod;
  6. if(next==from){
  7. a[i]=last;
  8. break;
  9. }
  10. a[i]=a[next];
  11. }
  12. }

按照完美洗牌算法,我们同样把数分为m和(n - m)两部分。

假设我们把前面若干项已经置换成先a后b的形式了,现在把这m项也置换成先a后b的形式,我们需要把这m项中的a部分换到前面去,这里需要一个循环右移,还要知道以前处理了多长。总之,这个逆shuffle算法需要小心实现一下,代码如下:

  1. //逆shuffle时间O(n),空间O(1)
  2. voidreverse_perfect_shuffle3(int*a,intn){
  3. intn2,m,i,k,t,done=0;
  4. for(;n>1;){
  5. //step1
  6. n2=n*2;
  7. for(k=0,m=1;n2/m>=3;++k,m*=3)
  8. ;
  9. m/=2;
  10. //2m=3^k-1,3^k<=2n<3^(k+1)
  11. for(i=0,t=1;i<k;++i,t*=3){
  12. reverse_cycle_leader(a,t,m*2+1);
  13. }
  14. if(done){
  15. right_rotate(a-done,m,done+m);//移位
  16. }
  17. a+=m*2;
  18. n-=m;
  19. done+=m;
  20. }
  21. //n=1
  22. right_rotate(a-done,1,done+2);
  23. }

总体算法(含变换和逆变换、还有测试代码)如下,注意所有的下标均从1开始:

  1. #include<cstdio>
  2. #include<cstring>
  3. #include<string>
  4. usingnamespacestd;
  5. //数组下标从1开始,from是圈的头部,mod是要取模的数mod应该为2*n+1,时间复杂度O(圈长)
  6. voidcycle_leader(int*a,intfrom,intmod){
  7. intlast=a[from],t,i;
  8. for(i=from*2%mod;i!=from;i=i*2%mod){
  9. t=a[i];
  10. a[i]=last;
  11. last=t;
  12. }
  13. a[from]=last;
  14. }
  15. //翻转字符串时间复杂度O(to-from)
  16. voidreverse(int*a,intfrom,intto){
  17. intt;
  18. for(;from<to;++from,--to){
  19. t=a[from];
  20. a[from]=a[to];
  21. a[to]=t;
  22. }
  23. }
  24. //循环右移num位时间复杂度O(n)
  25. voidright_rotate(int*a,intnum,intn){
  26. reverse(a,1,n-num);
  27. reverse(a,n-num+1,n);
  28. reverse(a,1,n);
  29. }
  30. //时间O(n),空间O(1)
  31. voidperfect_shuffle3(int*a,intn){
  32. intn2,m,i,k,t;
  33. for(;n>1;){
  34. //step1
  35. n2=n*2;
  36. for(k=0,m=1;n2/m>=3;++k,m*=3)
  37. ;
  38. m/=2;
  39. //2m=3^k-1,3^k<=2n<3^(k+1)
  40. //step2
  41. right_rotate(a+m,m,n);
  42. //step3
  43. for(i=0,t=1;i<k;++i,t*=3){
  44. cycle_leader(a,t,m*2+1);
  45. }
  46. //step4
  47. a+=m*2;
  48. n-=m;
  49. }
  50. //n=1
  51. t=a[1];
  52. a[1]=a[2];
  53. a[2]=t;
  54. }
  55. //逆变换,数组下标从1开始,from是圈的头部,mod是要取模的数mod应该为2*n+1,时间复杂度O(圈长)
  56. voidreverse_cycle_leader(int*a,intfrom,intmod){
  57. intlast=a[from],next,i;
  58. for(i=from;;i=next){
  59. next=i*2%mod;
  60. if(next==from){
  61. a[i]=last;
  62. break;
  63. }
  64. a[i]=a[next];
  65. }
  66. }
  67. //逆shuffle时间O(n),空间O(1)
  68. voidreverse_perfect_shuffle3(int*a,intn){
  69. intn2,m,i,k,t,done=0;
  70. for(;n>1;){
  71. //step1
  72. n2=n*2;
  73. for(k=0,m=1;n2/m>=3;++k,m*=3)
  74. ;
  75. m/=2;
  76. //2m=3^k-1,3^k<=2n<3^(k+1)
  77. for(i=0,t=1;i<k;++i,t*=3){
  78. reverse_cycle_leader(a,t,m*2+1);
  79. }
  80. if(done){
  81. right_rotate(a-done,m,done+m);//移位
  82. }
  83. a+=m*2;
  84. n-=m;
  85. done+=m;
  86. }
  87. //n=1
  88. right_rotate(a-done,1,done+2);
  89. }
  90. //测试代码
  91. intmain(){
  92. constintN=100000;
  93. inta[N*2+1],i;
  94. for(i=1;i<=2*N;++i){
  95. a[i]=i;
  96. }
  97. perfect_shuffle3(a,N);
  98. reverse_perfect_shuffle3(a,N);
  99. for(i=1;i<=2*N;++i){
  100. printf("%d\n",a[i]);
  101. }
  102. for(i=1;i<=2*N;++i){
  103. if(a[i]!=i){
  104. puts("NO");
  105. return0;
  106. }
  107. }
  108. puts("YES");
  109. return0;
  110. }


(3) 如果输入是a1,a2,……aN, b1,b2,……bN, c1,c2,……cN,要求输出是c1,b1,a1,c2,b2,a2,……cN,bN,aN怎么办?

这个问题也不是我凭空想像出来的,这是在careercup上看到过的面试题。

我研究了下这个问题,对于任意位置i = 1..3 * N 我们发现

原始1 <= i <= N 时,即a部分, 转移到的位置是 3 * i

原始N < i <= 2 * N 时 即b部分,转移到的位置是 3 * i - (3 * N + 1)

原始2 * N < i <= 3 * N时,即c部分转移到的位置是 3 * i - 2 * (3 * N + 1)

于是我们得到映射位置 i' = i mod (3 * N + 1)

之所以要把a,b,c的顺序反过来,因为有如上这么好的形式。

剩下的问题和学习完美洗牌算法差不多,我们试图对一个特定的长度解决掉。

仿照完美洗牌算法的思路,我验证了3是7的原根,是49的原根,于是3是7^k的原根。于是,我们可以把原来的圈按照截取出一个m,满足3 * m = 7 ^ k - 1,截取出一个m长度后,我们同样需要循环移位,使得(a1..am)(b1..bm)(c1..cm)在一起,这里要循移位两次。算法的步骤如下:

step 1 找到 3 * m = 7^k - 1 使得 7^k <= 3 * n < 7^(k +1)

step 2 把a[m + 1..n + m]那部分循环移m位,再把a[m * 2 + 1..2 * n + m]那部分循环右移m位,这样把数组分成了m和(n - m)两部分。

step 3 对每个i = 0,1,2..k - 1,7^i是个圈的头部,做cycle_leader算法,数组长度为m,所以对3 * m + 1取模。

step 4 对数组的后面部分a[3 * m + 1.. 3 * n]继续使用本算法,这相当于n减小了m。

代码:

  1. //翻转字符串时间复杂度O(to-from)
  2. voidreverse(int*a,intfrom,intto){
  3. intt;
  4. for(;from<to;++from,--to){
  5. t=a[from];
  6. a[from]=a[to];
  7. a[to]=t;
  8. }
  9. }
  10. //循环右移num位时间复杂度O(n)
  11. voidright_rotate(int*a,intnum,intn){
  12. reverse(a,1,n-num);
  13. reverse(a,n-num+1,n);
  14. reverse(a,1,n);
  15. }
  16. //数组下标从1开始,from是圈的头部,mod是要取模的数mod应该为3*n+1,时间复杂度O(圈长)
  17. voidcycle_leader(int*a,intfrom,intmod){
  18. intlast=a[from],t,i;
  19. for(i=from*3%mod;i!=from;i=i*3%mod){
  20. t=a[i];
  21. a[i]=last;
  22. last=t;
  23. }
  24. a[from]=last;
  25. }
  26. //时间O(n),空间O(1)
  27. voidperfect_shuffle3n(int*a,intn){
  28. intn3,m,i,k,t;
  29. for(;n>2;){
  30. //step1
  31. n3=n*3;
  32. for(k=0,m=1;n3/m>=7;++k,m*=7)
  33. ;
  34. m/=3;
  35. //3m=7^k-1,7^k<=3n<7^(k+1)
  36. //step2
  37. right_rotate(a+m,m,n);
  38. right_rotate(a+m*2,m,n*2-m);
  39. //step3
  40. for(i=0,t=1;i<k;++i,t*=7){
  41. cycle_leader(a,t,m*3+1);
  42. }
  43. //step4
  44. a+=m*3;
  45. n-=m;
  46. //printf("n=%dm=%d\n",n,m);
  47. //getchar();
  48. }
  49. if(n==2){
  50. cycle_leader(a,1,7);
  51. }
  52. elseif(n==1){
  53. t=a[1];
  54. a[1]=a[3];
  55. a[3]=t;
  56. }
  57. }



# T643023 R6 - B 打扑克 ## 题目背景 ~~寝室里打牌氛围超棒的说~~ ## 题目描述 小 Z 得到了 $n$ 张扑克牌,这 $n$ 张牌的编号为 $1,2,\cdots ,n$,接下来小 Z 会对其进行“洗牌”操作,具体步骤如下: - 将 $n$ 张牌按顺序叠放在桌子上,编号从上到下依次递增。另外还有一堆已洗完的牌,初始为空。 - 选择 $1$ 个正整数 $k(1\le k\le n)$,选择 $k+1$ 个整数 $x_0,x_1,x_2,\cdots ,x_{k-1},x_k(0=x_0< x_1<x_2<\cdots<x_{k-1}<x_k=n)$,然后将这叠牌切分成 $k$ 堆,第 $i$ 个部分为编号在 $(x_{i-1},x_i]$ 中的所有牌。例如当 $n=7$ 时,小 Z 可以选择将这些牌切分为三个部分 $\{1,2,3\},\{4,5\},\{6,7\}$。注意,在切分完之后,每一堆牌仍然保持着编号从上到下依次递增的性质。 - 将这 $k$ 堆牌以任意顺序排成一行。 - 从左到右不断进行观察,如果当前牌堆中仍有牌,则取出最上面的一张放到已洗完的牌堆**底部**。重复操作直到所有牌均被放入已洗完的牌堆。 为了更好地理解洗牌的过程,下面是一个具体的例子: - $n=8$,初始牌堆为 $\{1,2,3,4,5,6,7,8\}$。 - 选择 $k=3$ 并切分牌堆为三个部分 $\{1,2,3\},\{4,5\},\{6,7,8\}$。 - 将这三堆牌排成一行 $\{4,5\},\{6,7,8\},\{1,2,3\}$。 - 从左到右第一轮,取出的牌为 $4,6,1$,已洗完的牌堆为 $\{4,6,1\}$,三堆牌剩余为 $\{5\},\{7,8\},\{2,3\}$。 - 从左到右第二轮,取出的牌为 $5,7,2$,已洗完的牌堆为 $\{4,6,1,5,7,2\}$,三堆牌剩余为 $\emptyset,\{8\},\{3\}$。 - 从左到右第三轮,取出的牌为 $8,3$,已洗完的牌堆为 $\{4,6,1,5,7,2,8,3\}$,三堆牌均为空,洗牌结束。 现在小 Z 拿到了最终已洗完的牌堆从上到下的编号,他想知道有多少种不同的洗牌方式使得最终能得到这个顺序。 称两种洗牌方式不同,当且仅当两种洗牌方式中的切分出的牌堆个数,牌堆切分方式,牌堆排列顺序有任意一个是不同的。 小 Z 还会给出 $q$ 次操作,每次操作会交换排列中两个位置的数,并请你回答上述问题。 ## 输入格式 **本题包含多组数据** 第一行包含一个正整数 $T$,表示数据组数。 接下来 $T$ 组数据,每组数据第一行包含两个正整数 $n,q$,含义如上。 第二行包含 $n$ 个正整数,表示一个 $1,2,\cdots,n$ 的排列。 接下来 $q$ 行,每行包含两个正整数 $x,y$,表示交换这两个位置的数。 ## 输出格式 对每组数据,输出 $q+1$ 行整数,分别表示初始情况和每次操作结束后的答案。 ## 输入输出样例 #1 ### 输入 #1 ``` 2 4 3 1 3 2 4 1 2 2 3 1 4 5 5 1 4 3 2 5 2 3 1 3 4 2 5 1 4 3 ``` ### 输出 #1 ``` 3 2 2 2 3 3 2 2 3 2 ``` ## 说明/提示 #### 样例输入/输出 2 详见下发文件下的 ex_poker2.in/out。 这个数据满足第 $1,2,3$ 个测试点的限制。 #### 样例输入/输出 3 详见下发文件下的 ex_poker3.in/out。 这个数据满足第 $4,5,6$ 个测试点的限制。 #### 数据规模与约定 对所有数据,保证 $1\le T\le 3,1\le n\le 10^5,1\le q\le 1.5\times 10^5,x\not=y$。 | 测试点编号 | $n\le$ | $q\le$ | | ---------- | ------ | ---------------- | | $1,2,3$ | $200$ | $300$ | | $4,5,6$ | $2000$ | $3000$ | | $7,8,9,10$ | $10^5$ | $1.5\times 10^5$ |
最新发布
07-31
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值