约瑟夫问题的个人理解

昨天遇见了约瑟夫环这个问题。当时只想到了一种解决方法,用环模拟整个过程。后来又在网上看见了约瑟夫问题的数学解法,看了几遍,都没看懂,直到遇见“每天都满满的太阳”这篇博客:http://wewe39.blogbus.com/logs/121389435.html ,才完全理解了约瑟夫问题。那篇博客讲解了约瑟夫问题数学解法的各个细节,还提出了理解上可能遇见的几个困难。文章很好,但有三个关键疑点,博主还是语焉不详。所以,本文希望能够在那篇博客的基础上解释那几个关键疑点,最好先阅读原博客再来看我这篇水文,若能完全理解原博客,那么恭喜你,你很聪明;若暂时不能理解,也不要气馁,再聪明的人也有被卡住的时候,希望我这篇水文能带你绕过障碍。下文红色部分为引用原博客的,黑色部分为我的理解。

 

疑点1

"""

【思考过程】

 

首先一开始的序列

序列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

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

"""

从序列3映射到序列4,这个过程有些巧妙,但并不复杂。所谓映射,等于有一圈人,每个人身上贴一个号码,按照序列3的规律排列,再将每个人身上的号码重新排一次,排成序列4,但是保持每个人不动以及人与人之间的前后关系不变。照此,可画个映射图:

k+1 ----> 1

k+2 ----> 2

.................

.................

.................

n-2 -----> n-k-2

n-1 -----> n-k-1

n ------->  n-k  "序列3从k+1开始,到n-1,映射过程就是减k的过程”

1 ------->  n-k+1

2 ------> n-k+2

.................

.................

.................

k-2 -----> n-k+1+(k-3)=n-2

k-1 -----> n-k+1+(k-2)=n-1  "序列3从1到k-1,映射过程就是两者之间的距离加上n-k“

从上面可以看出,序列4与序列3存在映射关系。

 

疑点2

“”“”

对于序列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

还是没法理解出来。

“”“”

上述问题,可以归纳为,假设某人在序列4的位置为x,相应地,他在序列3的位置应该为多少呢?假设为x'。如何求出来x'呢?

仔细看上面我推出的映射图最后一项来看,序列4的位置为n-1,序列3的位置为k-1,两者之间有个取模的关系式,即((n-1)+k)%n=k-1(k<n),序列4的其他项也存在同样的规律,用符号替换一下上述关系式,即(x+k)%n=x',而用函数表达式形容这个关系,也就是f(i)=(f(i-1)+m)%i。f(i)为某人在序列中的位置。当然,有人会问,如果k>=n,怎么破,其实k>=n,结果也一样,这也就是原博客中出现了m%n的原因,m表示第m个人被拖出去斩了,直接用m%n代替k,上述关系式就变成了(x+m%n)%n=x',也就是(x+m)%n=x',还是一样的公式。

得出了这个数学公式,就可以根据这个数学公式写代码了。

 

 

疑点3

原博客中的代码些地方不好理解,我用黑色字体注释的方式加以说明。

”“”

递归操作的代码:

1 int Josephus(int n, int m)
2 {
3     if (n==1)
4      {
5         return0;  /* return 0 代表了从0开始报数,因此在最后的f(i)计算出来后,还要再加1,才能模拟从1开始报数的情况,请见代码13行中ret+1 */
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;  /* m-1代表了从0开始报数的情况,从0开始报数,则原来从1开始报数为m的人应该报数为m-1,而c所以从1开始,请看f(n)=(f(n-1)+m)%n这个表达式,所以必须从1开始递推。若不然,n=0,n-1=-1,就出错了。 因为是递推,这个递推公式每次都模拟了一个从1开始报数的情况,所以需要加1,才能准确模拟从1开始报数。 */
6     return l;
7 }
8
9 int main()
10 {
11     int ret=josefus(5,3);
12      cout<<ret<<endl;
13 }
14

 “”“

 

【循环链表解法】

 

#ifdef __cplusplus

extern "C" {

#endif



#include <stdio.h>

#include <stdlib.h>

#include <assert.h>





struct _node

{

	int key;

	struct _node *next;

};





int main()

{

	int i, N, M;

	struct _node *pnow, *head;

	while(1)

	{

	pnow = (struct _node *)malloc(sizeof(struct _node));

	pnow->key  = 1;

	head = pnow;

	



	printf("Please enter two numbers:\t");

	scanf_s("%d %d", &N, &M);



	if(N <= 0 || M <= 0)

	{

		printf("N < 0, or M < 0, it's wrong\n");

		return -1;

	}



	/* initial the circle linklist */

	for(i = 2; i < N; i++)

	{

		pnow->next = (struct _node *)malloc(sizeof(struct _node));

		assert(pnow->next != NULL);

		pnow = pnow->next;

		pnow->key = i;

		

	}

	pnow->next = head;





	/* delete the M'th node */

	while(pnow->next != pnow)

	{

		for(i = 1; i < M; i++)

		{

			pnow = pnow->next;

		}

		printf("this time deletes %d\n", pnow->next->key);

		head = pnow->next;

		pnow->next = pnow->next->next;

		free(head);



	}



	printf("this time deletes %d\n", pnow->key);

	free(pnow);

	}

	return 0;

}



#ifdef __cplusplus

}

#endif

 

 

 

 

【致敬】

  希望我的讲解能够有助于快速理解约瑟夫问题,但是,这一切都是建立在”每天都满满的阳光”的文章的基础上。非常感谢“每天都满满的太阳”,“每天都满满的太阳“,如果你看到了这篇文章,请原谅我这样“糟蹋”你的博文。

 

 【附录】

  http://blog.csdn.net/solofancy/article/details/4211770 这篇博客补充了约瑟夫问题的另外几种解法。

 

 

 

你可能感兴趣的:(问题)