【待解惑问题(已解决)】约瑟夫(Josephus)环问题

周末到了,首先祝大家周末愉快。可怜我周末都没得休息,继续努力 :)大家该哈皮的去哈皮吧。

昨天,其实从前天开始就开始准备约瑟夫环,昨晚就准备开始写,不过想来想去对其数学推导出递推式还是没有想明白。大家有空帮忙看看,到底这个递推式是怎么出来的呢?我下面会一步步将自己的思考列下来。多谢多谢。

一开始将题目和模拟法稍微写一下,之后是关于数学方法的疑惑,大家可以直接略过上面的一些内容,跳到关键部分。 

【题目】 

约瑟夫环问题,每一个学习过数据结构或者C语言的人应该都会遇到过的问题。不论是在课本的教授内容中,还是在习题里面,都应该会出现过它的身影。 

 

基本的题目内容如下:

n个人围成一圈,编号从1n,报数123,从编号为1的人开始报数,报到3的离开,下面的人接着从1开始报数,依次下去,写程序求最后剩下来的一个人编号是几。

这里题目中报数为到3为止,其实可以扩展到m。也就是n个人,报到m的人出列,同时也可以扩展到以某一个人为开始,而并不是从第1个人开始。

【模拟法】

一般在数据结构课上遇到这个问题的时候,是讲解循环链表的时候,这个题目是用来检验循环链表学习的结果的。所以一开始我们用最“朴素”的方法来解决。

一般这种方法,被称之为模拟法,也就是模拟问题发生的过程,直到得到最后的结果,这种解法是最容易想到的,但是一般也是效率最低的。

使用模拟法来解决,我们可以使用循环链表,也可以使用数组来模拟循环。这里我们使用循环链表,循环链表尽管比数组要写得麻烦,但是思考起来简单直观。

解决问题的核心步骤如下:

1.  建立一个循环链表,将每个节点进行编号

2.  开始报数,当报到3,删除该节点,然后接着报数

3.  直到只剩下最后一个节点的时候结束,将该节点的值输出

Code

 

用数组的话,就是要实现循环,也就是当数到n的时候,不会再进行n+1,而是从1再开始。这个我们可以用取余数来得到。

开始模拟,首先第一个退出的人肯定是m%n,然后需要将之后的位置进行调整,(是否可以不进行调整?用标志位似乎也不是特别好)此时如果还要数组循环的话,就需要与n-1进行取余操作。然后一直这样继续。这里就不给出代码了。

这里我们只是输出了最后一个人的号码,其实也可以将出列的顺序打印出来,还可以将出列的节点保存起来,以后使用。

这里的时间复杂度为O(m*n) 

 

【数学方法】

对于约瑟夫环问题,最简单的模拟法研究了意义也不是很大。我们的目的是找到更快的算法。也就是不使用模拟法,而是使用数学解法。

多谢园子中兄弟elite_lcf和Ivony的回复,终于搞明白了。下面加上自己理解后的内容,蓝色就是新加上的理解内容。

我差不多花了半天多的时间专门在这个上面(恩,其实还有晚上的部分时间。。。),在google上换了n个关键词(其实夸张了,也就四个,“约瑟夫环问题”,“约瑟夫环数学”,“约瑟夫环 数学原理”,“约瑟夫环 推导”)。搜到的结果包括关于约瑟夫环的国内的博客,百度知道(汗,其实还有搜搜问问),以及各个论坛都看了个差不多,基本上都是几篇帖子转来转去,对于关键的那个地方,大家基本统一的用“显而易见”来一句概括。(啊!!!我最讨厌这句了,我怎么就不能一眼看出,显而易见呢,各位神人啊,下次能不能讲得更明白些让我这种人也能够懂啊。。。) 

不过也有收获,就是找到几个不错的网站,同时也知道了原来《具体数学》中介绍过约瑟夫环,但是似乎只是介绍了m=2的情况,而将扩展没有介绍。不过现在也没有这本书,同时这段时间看来是没有空去啃了,等有空了,要读一读这本书,原来还是Knuth写的。

其实网上所有的说法,其答案是一致的,解释从本质上也是一致的,但是就是让人理解很难理解,当然,肯定有人理解的。。。

网上的一种说法,列出也是园子里面兄弟的链接:

http://www.cnblogs.com/woodfish1988/archive/2007/02/18/652251.html 

我这里就不摘录了,大家有兴趣可以按照链接过去看。我这里就开始写自己的理解。其实相差不大。

【思考过程】 

 

首先一开始的序列

序列1 1, 2, 3, 4, …, n-2, n-1, n

此时出队列的第一个人,位置为k,号码肯定是m%n。这个应该没有问题,也就是取余操作使得数组类似能够有循环的功能。

此时序列2 1, 2, 3, 4, … k-1, k+1, …, n-2, n-1, n

此时k出队列,序列2中为n-1个人了。

 

根据序列2,得到序列3

k+1, k+2, k+3, …, n-2, n-1, n, 1, 2, 3,…, k-2, k-1

从序列2得到序列3很简单,也就是将k+1作为开始,然后将1连到n的后面,其实只是位置的不同,但是本质两个序列是一样的。所以同样,这里也是n-1个元素。

序列3可以映射得到序列4:

1, 2, 3, 4, …, 5, 6, 7, 8, …, n-2, n-1

这里我就乱掉了,这个映射是可以做,但是映射关系是怎样的?

OK,先不管映射关系,我们继续来推导。

对于序列4,我们假设能够得到解,也就是最后一个退出的人的号码,设置为x。如果我们能够通过映射关系,将x在序列3中对应的号码x’求出来,那我们就可以得到序列3的解,因为序列3其实和序列2是同一个,所以序列2的解我们也就得到了,序列2就是序列1去掉一个k,这个k对于序列1的解没有任何影响,所以序列1的答案就是求出来的x’。

那关键就是如何通过x得到x’ ,也就是映射关系的问题。

对于序列4,如果我们要得到1到n-1序列中的值,我们也是做取余操作,只不过除数变为n-1了。


但是如何得到关系为 x'=(x+m)%n,从而得到递推式

f(i)=(f(i-1)+m)%i

还是没法理解出来。

f(1)=1肯定是

 

有没有谁能帮忙解惑一下,谢谢。。。 

对于序列3中的x'来说,x'对于n之前的数,肯定是为x+k,而对于n之后的数,其实就是要做一个循环,而一开始疑惑就是循环做不起来,因为少了一个k,只有n-1个数,那中间就是断的。那既然少了一个k,我们就将k加上去,其实前面已经分析出来,多一个k也不影响解,因为k是第一个出列的数。

那就可以得到映射关系为x'=(x+k)%n。而k=m%n,所以x'=(x+m%n)%n=(x+m)%n。

而序列4,是一个连续的n-1个数的序列,作为下一个递归的开始,所以下一个递归的除数也就是n-1,而不是n。

这样我们就得到递推式

f(i)=(f(i-1)+m)%n了。

 

多谢大家帮助解惑,稍后再补上程序和优化部分。

在代码实现的时候发现,犯了一个很大的错误,这里序列的开始节点应该是0而不是1,当时为了与题目依靠紧密而使用1,以为不影响,但是其实取余会取到0,所以还是要从0开始,这样得到的结果就需要+1,才是题目中需要的结果。

递归操作的代码:

 1 int Josephus(int n, int m)
 2 {
 3     if (n==1)
 4     {
 5         return 0;
 6     }
 7     return (Josephus(n-1,m)+m)%n;
 8 }
 9 
10 int main()
11 {
12     int ret=Josephus(5,3);
13     cout<<ret+1<<endl;
14 }
15 

非递归操作的代码,也就是递推

 1 int josefus(int n,int m) 
 2 {
 3     int l=0,c;
 4     for(c=1;c<=n;c++)
 5         l=(l+m-1)%c+1;
 6     return l;
 7 }
 8 
 9 int main()
10 {
11     int ret=josefus(5,3);
12     cout<<ret<<endl;
13 }
14 


【优化】

上面的算法的时间复杂度会精简到O(n)。已经是很快了。不过还有更快的,可以到O(m),如果m和n相差很大,m很小的话,速度又会提升。具体大家可以看园子里面兄弟的帖子。

http://www.cppblog.com/guyuecanhui/articles/76443.html

 

【扩展】

约瑟夫环问题的扩展很多,大多都是相同的问题,只不过人换成了猴子,有一个猴子推选猴王的题目;然后ZOJ1088题也是约瑟夫环的应用,如果用模拟做,好像比较难AC

【总结】

约瑟夫环可以考察的地方还是算不少,首先可以考察循环链表的知识,同时还可以考察链表的删除。然后用数组来实现,也可以查看编程细节。

在数学方法,其实递推式出来,也就是考察了递归。当然,解决的时候,可以用递归来写,也可以用递推来写。

同时如果大家了解过一点hash的话,比如我,就只了解一点儿。。。可以注意到对m%n这种取余,也就是让m落到n的范围内,这不就是最简单的哈希函数吗?

而哈希函数的使用场合就比较多了,能够想到的加密解密领域。当然,这个我就没有再去深入了,大家有兴趣可以讨论讨论。

 

 

 

posted on 2009-10-31 10:09  cnyao  阅读(3011)  评论(3编辑  收藏  举报