约瑟夫环问题递归解法的一点理解



先说明一点,如果有什么不对的地方,欢迎大家批评指正。


先来看这个类型的某个题目描述:

约瑟夫生者死者游戏

约瑟夫游戏的大意:30个游客同乘一条船,因为严重超载, 加上风浪大作,危险万分。因此船长告诉乘客,只有将全船 一半的旅客投入海中,其余人才能幸免于难。无奈,大家只 得同意这种办法,并议定30 个人围成一圈,由第一个人数起,依次报数,数到第9人,便把他投入大海中,然后再从 他的下一个人数起,数到第9人,再将他投入大海中,如此 循环地进行,直到剩下 15 个游客为止。问:哪些位置是将 被扔下大海的位置? 


不失一般性,将 30 改为一个任意输入的正整数 n,而报数 上限(原为9)也为一个任选的正整数k


第一次看到这个题目,我首先想到的是用 链表 或者是 数组 模拟,但是当我写完之后,与大神对答案,发现他的c语言代码是这么写的:

int ysfdg ( int sum, intvalue, intn)
{
    if ( n == 1 )
        return ( sum + value - 1 ) %sum;
    else
        return
( ysfdg ( sum-1, value,n-1 ) +value ) %sum;
}

7行。。。。。。再见(已经不能做朋友了吗。。。)

sum指的是总人数,value指的是每次最大报到的数值,n是第n次,该函数每次可以求出第n次扔海里的人的编号,( ysfdg指的是约瑟夫递归 ) 。

大神飘然而去,而我懵逼了一天,才明白了这段代码的意思。


举个栗子:

总人数sum为10人,从0开始,每报到4就把一人扔下去(value=4)。

初始情况为:

0   1   2   3   4   5   6   7   8   9

扔下去一个之后:

0   1   2        4   5   6   7   8   9

此时,这些编号已经不能组成一个环,但是可以看出4至2之间还是连着的(4 5 6 7 8 9 0 1 2),且下一次报数将从4开始。但是,之后的报数将总要考虑原编号3处的空位问题。

如何才能避免已经产生的空位对报数所造成的影响呢?

可以将剩下的9个连续的数组成一个新的环(将2、4连接),这样报数的时候就不用在意3的空位了。但是新产生的环的数字并非连续的,报数时不像之前那样好处理了(之前没人被扔海里时下一个报数的人的编号可以递推,即(当前编号+1)%sum ),无法不借助存储结构得知下一个应该报数的现存人员编号。

如何使新环上的编号能够递推来简化我们之后的处理呢?

可以建立一种有确定规则的映射,要求映射之后的数字可以递推,且可以将在新环中继续按原规则报数得到的结果逆推出在旧环中的对应数字。

方法:将它与  sum-1 个人组成的(0 ~ sum-1)环一 一映射。

比如之前的栗子,将剩余的 9 人与  9 人环(0~8)一 一映射。既然 3 被扔到海里之后,报数要从4开始 (4 其实在数值上等于最大报数值),那么就将4映射到0~8的新环中0的位置,也就是说在新环中从0开始报数即可,且新环中没有与3对应的数字,因此不必担心有空位的问题。从旧环的 4 开始报数等效于从新环中的 0 开始报数。

原始   0   1   2   3   4   5   6   7   8   9

旧环   0   1   2        4   5   6   7   8   9

新环   6   7   8        0   1   2   3   4   5

新环有这么一个优势:  相比于旧环中2与4之间在数学运算上的不连续性,新环8和0之间在对9取余的运算中是连续的,也就是说根本不需要单独费心设计用以记录并避开已产生的空位(如 编号3)的机制 ,新环的运算不受之前遗留结果的掣肘。同时只要能将新环与旧环的映射关系逆推出来,就能利用在新环中报数的结果退出之前旧环中的报数结果

以下是新环与旧环中下一个要人扔海里的人位置:

旧环   0   1   2        4   5   6   7   8   9

                                             ^

新环   6   7   8        0   1   2   3   4   5

                                             ^

如何由新环中的 3 得到旧环中的 7 呢。其实可以简单地逆推回去 : 新环是由  (旧环中编号-最大报数值)%旧总人数  得到的,所以逆推时可以由 ( 新环中的数字 + 最大报数值 )% 旧总人数 取得。即 old_number = ( new_number + value ) % old_sum.

  如 : ( 3 + 4 ) % 10 =7 .

也就是说在,原序列( sum ) 中第二次被扔入海中编号可以由新序列( sum - 1) 第一次扔海里的编号通过特定的逆推运算得出。

而新序列 (sum -1)也是(从0开始)连续的,它的第二次被扔入海中的编号由可以由(sum - 2)的第一次扔入海里的编号通过特定的逆推运算得出,并且它的第二次被扔入海中的编号又与原序列中的第三次被扔入海里的编号是有对应关系的。

也求是说有以下推出关系:

(sum-2)环的第1次出环编号 >>>(sum-1)环的第2次出环编号 >>>(sum)环的第3次出环编号

即 在以 k 为出环报数值的约瑟夫环中, m人环中的第n次出环编号可以由 (m-1) 人环中的第 (n-1) 次出环编号通过特定运算推出。

幸运的是,第一次出环的编号是可以直接求出的,也就是说,对于任意次出环的编号,我们可以将之一直降到第一次出环的编号问题,再一  一 递推而出。

注意 以下图示中的环数字排列都是顺序的,且从编号0开始。


由图知,10人环中最后入海的是4号,现由其在1人环中的对应编号0来求解。

                                                                                                                          

通过以上运算,其实我们已经求出分别位于9个环中九个特定次数的结果,只不过我们需要的是10人环的结果罢了。

这种方法既可以写成递归也可以写成循环,它对于求特定次数的出环编号效率较高。

递归就比较好写了,出口即是当次数为1时。

实际编号是从1开始,而不是0,输出时要注意转换。

借此就可以看懂那个大神的代码了。

以下是三种约瑟夫环解法(数组,链表,递归)的c语言代码,作者水平不高,将就看看吧 ╮(╯_╰)╭



  
  1. #include<stdio.h>
  2. #include<stdlib.h>
  3. #define FAIL 0
  4. #define SUCCESS 1
  5. typedef struct gamenode
  6. {
  7. int number;
  8. struct gamenode* next;
  9. } node;
  10. //读入初值
  11. int getvalue(int* sum,int* count,int* alive)
  12. {
  13. printf( "请输入要参与约瑟夫生存游戏的人数:(人数>0)\n");
  14. while( 1)
  15. {
  16. scanf( "%d",sum);
  17. if(*sum> 0)
  18. break;
  19. printf( "输入无效,请重新输入。\n");
  20. };
  21. printf( "请输入能报到的最大数字:(1<=数字)\n");
  22. while( 1)
  23. {
  24. scanf( "%d",count);
  25. if(*count>= 1)
  26. break;
  27. printf( "输入无效,请重新输入。\n");
  28. };
  29. printf( "请输入要求最后存活的人数:(0<=人数<=%d)\n",*sum);
  30. while( 1)
  31. {
  32. scanf( "%d",alive);
  33. if(*alive>= 0&&*alive<=*sum)
  34. break;
  35. printf( "输入无效,请重新输入。\n");
  36. };
  37. return SUCCESS;
  38. }
  39. //数组解法
  40. int ysfsz(int n,int k,int s)
  41. {
  42. int i= 0,*p= NULL,sum=n-k,j= 1,o= 0,pr= 0;
  43. if ((p=( int*) malloc( sizeof( int)*n))== NULL)
  44. {
  45. printf( "FAIL!\n");
  46. return FAIL;
  47. }
  48. for(i= 0;i<n;i++)
  49. {
  50. p[i]= 1;
  51. }
  52. i= 0;
  53. while( 1)
  54. {
  55. if (sum== 0)
  56. break;
  57. if (j==s&&p[i]== 1)
  58. {
  59. p[i]= 0;
  60. j= 1;
  61. --sum;
  62. }
  63. else if(p[i]== 1)
  64. {
  65. j++;
  66. }
  67. i++;
  68. i=i%n;
  69. }
  70. printf( "\n生存下来的人的位置是:");
  71. for(i= 0;i<n;i++)
  72. {
  73. if(p[i]== 1)
  74. printf( "%d ",i+ 1);
  75. }
  76. printf( "\n");
  77. printf( "\n扔海里的位置是:");
  78. for(i= 0;i<n;i++)
  79. {
  80. if(p[i]== 0)
  81. printf( "%d ",i+ 1);
  82. }
  83. printf( "\n");
  84. free(p);
  85. return SUCCESS;
  86. }
  87. //链表解法
  88. int ysflb(int n,int k,int s)
  89. {
  90. node *h= NULL,*p= NULL,*q= NULL;
  91. int i= 0;
  92. if ((h=(node*) malloc( sizeof(node)*n))== NULL)
  93. {
  94. printf( "FAIL!\n");
  95. return FAIL;
  96. }
  97. h->number= 1;
  98. h->next=h;
  99. q=h;
  100. for(i= 1;i<n;i++)
  101. {
  102. if ((p=(node*) malloc( sizeof(node)*n))== NULL)
  103. {
  104. printf( "FAIL!\n");
  105. exit ( -1);
  106. }
  107. else
  108. {
  109. p->next= NULL;
  110. p->number=i+ 1;
  111. q->next=p;
  112. q=q->next;
  113. }
  114. }
  115. q->next=h;
  116. p=h;
  117. i= 1;
  118. k=n-k;
  119. while(k> 0)
  120. {
  121. if(i==s)
  122. {
  123. if(p!=p->next)
  124. {
  125. q->next=p->next;
  126. printf( "%d号已被扔进海里。\n",p->number);
  127. free(p);
  128. p=q->next;
  129. }
  130. else
  131. {
  132. printf( "%d号已被扔进海里。\n",p->number);
  133. p=q=h= NULL;
  134. }
  135. --k;
  136. }
  137. else
  138. {
  139. p=p->next;
  140. q=q->next;
  141. }
  142. ++i;
  143. if(i>=s+ 1)
  144. i= 1;
  145. }
  146. if(p!= NULL)
  147. {
  148. printf( "幸存下来的人有:");
  149. h=p;
  150. p=p->next;
  151. while(p!=h)
  152. {
  153. q=p->next;
  154. printf( "%d ",p->number);
  155. free(p);
  156. p=q;
  157. }
  158. printf( "%d ",h->number);
  159. free(h);
  160. }
  161. else
  162. printf( "全部扔海里去了。\n");
  163. return SUCCESS;
  164. }
  165. //递归解法
  166. int ysfdg(int sum,int value,int n)
  167. {
  168. if(n== 1)
  169. return (sum+value -1)%sum;
  170. else
  171. return (ysfdg(sum -1,value,n -1)+value)%sum;
  172. }
  173. //主函数
  174. int main(void)
  175. {
  176. int sum= 0,count= 0,alive= 0,i= 0;
  177. //读入总人数,报数值,存活人数
  178. getvalue(&sum,&count,&alive);
  179. /*------------------------------------------------------------*/
  180. //1.约瑟夫环的数组解法
  181. //ysfsz(sum,alive,count);
  182. //2.约瑟夫环的链表解法
  183. //ysflb(sum,alive,count);
  184. //3. 约瑟夫环递归解法
  185. for(i= 1;i<=sum-alive;i++)
  186. printf( "第%2d个被扔海里人的编号:%2d\n",i,ysfdg(sum,count,i)+ 1);
  187. return 0;
  188. }


posted @ 2022-11-21 18:54  TwcatL_tree  阅读(32)  评论(0编辑  收藏  举报