约瑟夫问题的数学角度分析 C 数组实现 循环链表实现 递归实现时间复杂度O(logN)

在网上找了一些资料,具体包括约瑟夫斯问题wiki、【整理】约瑟夫问题的数学方法、约瑟夫问题(猴子选大王)循环链表C语言实现、約瑟夫問題的兩個O(log n)解法。自己做了总结吸收,尽量是数学证明的在简单的copy工作外加上自己的理解;是代码实现的,就自己做实验。虽然是拿来主义,但是也要学点东西不是?

废话不多说,先百科。约瑟夫斯问题(有时也称为约瑟夫斯置换),是一个出现在计算机科学和数学中的问题。在计算机编程的算法中,类似问题又称为约瑟夫环。有n个囚犯站成一个圆圈,准备处决。首先从一个人开始,越过k-2个人(因为第一个人已经被越过),并杀掉第k个人。接着,再越过k-1个人,并杀掉第k个人(这里的接着是说,如果k问题是,给定了n和k,一开始要站在什么地方才能避免被处决?

嗯,也可以称作猴子选大王问题,题目描述:n只猴子要选大王,选举方法如下:所有猴子按 1,2 ……… n 编号并按照顺序围成一圈,从第 k 个猴子起,由1开始报数,报到m时,该猴子就跳出圈外,下一只猴子再次由1开始报数,如此循环,直到圈内剩下一只猴子时,这只猴子就是大王。输入数据:猴子总数n,起始报数的猴子编号k,出局数字m。输出数据:猴子的出队序列和猴子大王的编号。

反正都一样嘛!

1 WIKI百科怎么说

k=2时的问题

设f(n)为一开始有n个人时,生还者的位置(注意:最终的生还者只有一个)。走了一圈以后,所有偶数号码的人被杀。再走第二圈,则新的第二、第四、……个人被杀,等等;就像没有第一圈一样如果一开始有偶数个人,则第二圈时位置为x的人一开始在第2x-1个位置。因此第二圈位置为f(n)的人在第一圈的位置为2f(n)-1。这便给出了以下的递推公式:f(2n)=2f(n)-1.
如果一开始有奇数个人,则走了一圈以后,最终是号码为
1的人被杀。于是同样地,再走第二圈时,新的第二、第四、……个人被杀,等等。在这种情况下,位置为x的人原先位置为2x+1。这便给出了以下的递推公式:f(2n+1)=2f(n)+1.
如果我们把n和f(n)的值列成表,我们可以看出一个规律:
n 1 2 3 4 5 6 7 8 9 10 1112131415 16
f(n) 1 1 3 1 3 5 7 1 3 5 7 91113151

从中可以看出,f(n)是一个递增的奇数数列,每当n是2的幂时,便重新从f(n)=1开始。因此,如果我们选择m和l,使得n=2^m+l且0< l<2^{m},那么f(n)=2*l+1。显然,表格中的值满足这个方程。我们用数学归纳法给出一个证明。
定理:如果n=2^m+l且0<=l<2^m,则f(n)=2l+1。
证明:对n应用数学归纳法。n=1的情况显然成立。我们分别考虑n是偶数和n是奇数的情况。
如果n是偶数,则我们选择l1和m1,使得n/2=2^(m1)+l1,且0<=l1<2^(m1)。注意l1=l/2;m1=m-1。我们有f(n)=2f(n/2)-1=2((2*l1+1)-1=2l+1,其中第二个等式从归纳假设推出。
如果n是奇数,则我们选择l1和m1,使得(n-1)/2=2^(m1)+l1,且0<=l1<2^(m1)。注意l1=(l-1)/2;m1=m-1。我们有f(n)=2f((n-1)/2)+1=2((2*l1)+1)+1=2l+1,其中第二个等式从归纳假设推出。证毕。

答案的最漂亮的形式,与n的二进制表示有关:把n的第一位移动到最后,便得到f(n)。如果n的二进制表示为n=b0_b1_b2_b3_...bm,则f(n)=b1_b2_b3_...bm_b0。这可以通过把n表示为2^{m}+l来证明。(真心漂亮)

k!=2时

这个问题的最简单的解决方法是使用动态规划。利用这种方法,我们可以得到以下的递推公式:
f(n,k)=(f(n-1,k)+k) mod  n,f(1,k)=0
如果考虑生还者的号码从n-1到n是怎样变化的,则这个公式是明显的。这种方法的运行时间是O(n),但对于较小的k和较大的n,有另外一种方法,这种方法也用到了动态规划,但运行时间为O(k\log n)。它是基于把杀掉第k、2k、……、2[n/k]个人视为一个步骤,然后把号码改变。

2问题描述:n个人(编号1~n),从1开始报数,报到m的退出,剩下的人继续从1开始报数。求胜利者的编号。数学解法复杂度:O(n)

无论是用链表实现还是用数组实现都有一个共同点:要模拟整个游戏过程,不仅程序写起来比 较烦,而且时间复杂度高达O(nm),当nm非常大(例如上百万,上千万)的时候,几乎是没有办法在短时间内出结果的。我们注意到原问题仅仅是要求出最 后的胜利者的序号,而不是要读者模拟整个过程。因此如果要追求效率,就要打破常规,实施一点数学策略。

为了讨论方便,先把问题稍微改变一下,并不影响原意:

问题描述:n个人(编号0~(n-1)),从0开始报数,报到(m-1)的退出,剩下的人继续从0开始报数。求胜利者的编号。

我们知道第一个人(编号一定是m%n-1) 出列之后,剩下的n-1个人组成了一个新的约瑟夫环(以编号为k=m%n的人开始):
   k   k+1   k+2   ... n-2, n-1, 0, 1, 2, ... k-2
并且从k开始报0

现在我们把他们的编号做一下转换:
k     --> 0
k+1 --> 1
k+2 --> 2
...
...
k-2 --> n-2
k-1 --> n-1

变换后就完完全全成为了(n-1)个人报数的子问题,假如我们知道这个子问题的解:例如x是最终的胜利者,那么根据上面这个表把这个x变回去不刚好就是n个人情况的解吗?!!变回去的公式很简单,相信大家都可以推出来:x'=(x+k)%n(这个步骤当时我的理解:x‘是变换前的编号;x是变换后的编号。如上转换,每个号码都减去k形成新号码,所以x+k就是原来的号码。这里明显x+kf[i]=(f[i-1]+m)%i。这里m不一定比n小,取余运算更合理。并且,这个思路与WIKI百科上的区别在于:维基上k=2时是一定小于n,所以wiki上是对一个数列把所有隔一个的全部淘汰掉,然后还是原来的数列顺序不变,只是剩下的抽取出来组成新的数列;这里的每次从k重新开始,是默认数列循环的

如何知道(n-1)个人报数的问题的解?对,只要知道(n-2)个人的解就行了。(n-2)个人的解呢?当然是先求(n-3)的情况 ---- 这显然就是一个倒推问题!好了,思路出来了,下面写递推公式:

f[i]表示i个人玩游戏报m退出最后胜利者的编号,最后的结果自然是f[n]

递推公式
f[1]=0;
f[i]=(f[i-1]+m)%i;   (i>1)
(这里在计算时候容易误解是对n取余,其实是对i取余。因为每次数列的长度都是变化的)

有了这个公式,我们要做的就是从1-n顺序算出f[i]的数值,最后结果是f[n]。因为实际生活中编号总是从1开始,我们输出f[n]+1

由于是逐级递推,不需要保存每个f[i],程序也是异常简单:

#include
int main()
{
   int n, m, i, s=0;
   printf ("N M = "); scanf("%d %d", &n, &m);
   for (i=2; i<=n; i++) s=(s+m)%i;
   printf ("The winner is %d\n", s+1);
}


这个算法的时间复杂度为O(n),相对于模拟算法已经有了很大的提高。算n,m等于一百万,一千万的情况不是问题了。

上面的程序可以对WIKI的理论做一个验证,特别是m=2的。很简单。

3猴子选大王问题(输入数据:猴子总数n,起始报数的猴子编号k,出局数字m     输出数据:猴子的出队序列和猴子大王的编号)循环单链表实现

#include 
#include 

typedef struct Node * PtrToNext;
struct Node
{
	 int Data;
	 PtrToNext Next;
};
int main(void)
{
	 int n, m, i,j;
	 PtrToNext Head,P,tmpCell;

	 Head=(PtrToNext)malloc(sizeof(struct Node));
	 P=(PtrToNext)malloc(sizeof(struct Node));
	 P=Head;
	 P->Next=P;
	 printf ("N M = ?\n Please enter the number\n"); 
	 scanf("%d %d", &n, &m);

	 for (i=0; iData=i+1;
		P->Next=tmpCell;		
		P=tmpCell;
	 }
	 Head=Head->Next;
	 P->Next=Head;

	 P=Head;
	 j=1;
	 while(P->Next!=P)
	 {
		  i=1;
		  
		  while(++iNext;
		  }
		  tmpCell=P->Next;
		  printf("When the loop=%d,the ID=%d is eliminated.\n",j,tmpCell->Data);
		  P->Next=tmpCell->Next;
		  P=tmpCell->Next;
		  free(tmpCell);//对于舍弃的结构体要及时free,避免浪费空间
		  j++;
	 }

	 printf ("The winner is %d\n", P->Data);

	 free(P);//前面的tmpCell已经free掉,P和head指向同一个地址,所以只能对P做此操作。free(head)报错
	 
	 return 0;
}

习题3.10的解答

对于第一种方法,花费时间O(n),空间复杂度为O(1);第二种方法,花费时间O(mn),空间复杂度是O(n)。 (PS:所以说,学好数学多么重要!!!)
M=1时,逐个删除,时间复杂度为O(N)。



你可能感兴趣的:(C/C++,面试题)