数据结构之约瑟夫环



约瑟夫斯问题(有时也称为约瑟夫斯置换),是一个出现在计算机科学和数学中的问题。在计算机编程的算法中,类似问题又称为约瑟夫环。

n个囚犯站成一个圆圈,准备处决。首先从一个人开始,越过k-2个人(因为第一个人已经被越过),并杀掉第k个人。接着,再越过k-1个人,并杀掉第k个人。这个过程沿着圆圈一直进行,直到最终只剩下一个人留下,这个人就可以继续活着。

问题是,给定了nk,一开始要站在什么地方才能避免被处决?


比较简单的做法是用循环单链表模拟整个过程,时间复杂度是O(n*m)。代码如下:

typedef struct node {
  int data;
  struct node *next;
} LNode, *LinkList;


//构建循环链表
LinkList Init(int n){
	 LinkList p,r;
	 LinkList list = NULL;
	 int i;
	 for(i=0;i < n;i++) {
	  p = (LinkList)malloc(sizeof(LNode));
	  p->data = i+1;
	  p->next = NULL;
	  if(!list) {
		 list = p;
	   } else {
		 r->next = p;
		}
		 r = p;
	  }
		p->next = list;
	  return list;
}

void joseph2(LinkList root,int n,int m){
	int flag,i = 1;
	LinkList pre,p;
	pre = p = root;
	flag = 0;

	while(1){

		if(i == m){
			pre->next = p->next;
			printf("%d ",p->data);
			p = p->next;
			i=1;
		}

		i++;
		p = p->next;

		if(++flag > 1)
			pre = pre->next;

		if(p == p->next){
			printf("%d\n",p->data);
			break;
		}

	}
}

第二种解法用到了动态规划:

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

为了讨论方便,先把问题稍微改变一下,并不影响原意:
问题描述: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,怎么把x转为x'呢,你会发现 (x+k) mod n 就行了。

我们记f(n,k)为还剩n个人时的幸存者编号。很明显,

n = 1时,f[1,k] = 0;(下标从0开始)

由刚才推出的公式,所以有

f[2,k] = (f[1,k] + k) % 2;

递推公式
f[1,k]=0;
f[n,k]=(f[n-1,k]+m) % n;  (n>1)

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

如果还不能理解,不妨看一个具体的例子

现在假设m=10,k=3

0 1 2 3  4 5 6 7 8 9    

第一个人出列后的序列为:

0 1 3 4 5 6 7 8 9

即:

3 4 5 6 7 8 9 0 1(*)

我们把该式转化为:

0 1 2 3 4 5 6 7 8 (**)

则你会发现: ((**)+3)%10则转化为(*)式了

也就是说,我们求出9个人中第9次出环的编号,最后进行上面的转换就能得到10个人第10次出环的编号了 。


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

void joseph(int n,int m){
	int i, s=0;
	for(i=2; i<=n; i++) 
		s=(s+m)%i;
	printf("The winner is %d\n", s+1);
}

完整代码如下:

#include "stdio.h"
#include "stdlib.h"

typedef struct node {
  int data;
  struct node *next;
} LNode, *LinkList;


//构建循环链表
LinkList Init(int n){
	 LinkList p,r;
	 LinkList list = NULL;
	 int i;
	 for(i=0;i < n;i++) {
	  p = (LinkList)malloc(sizeof(LNode));
	  p->data = i+1;
	  p->next = NULL;
	  if(!list) {
		 list = p;
	   } else {
		 r->next = p;
		}
		 r = p;
	  }
		p->next = list;
	  return list;
}

void ListDelNode(LinkList *root,int value){  //因为头节点可能被删除,可能改变L存储的地址,所以传入L的地址
	LinkList list,pre;
	int flag = 0;					//用于判断是否是第一次循环,用于设置pre的值
	list = pre = *root;
	while(list){
		if(list->data == value){

			if(list == pre){
				*root = (*root)->next;
				printf("删除掉的值为:%d \n",list->data);
				free(list);				//free node
				break;                  //break loop
			}

			pre->next = list->next;
			printf("%d ",list->data);
			free(list);
			break;					
		}

		if(++flag > 1)
			pre = pre->next;
		list = list->next;	
	}

}

void printList(LinkList root){
	
	LinkList list = root;
	while(list) {  
	   printf("%d ->",list->data);  
	   list = list->next;
	 }  
	printf("NULL\n");
}


//总共n个人,每次数m个
void joseph(int n,int m){
	int i, s=0;
	for(i=2; i<=n; i++) 
		s=(s+m)%i;
	printf("The winner is %d\n", s+1);
}


void joseph2(LinkList root,int n,int m){
	int flag,i = 1;
	LinkList pre,p;
	pre = p = root;
	flag = 0;

	while(1){

		if(i == m){
			pre->next = p->next;
			printf("%d ",p->data);
			p = p->next;
			i=1;
		}

		i++;flag++;
		p = p->next;

		if(flag > 1)
			pre = pre->next;

		if(p == p->next){
			printf("%d\n",p->data);
			break;
		}

	}
}

int main(){
	int n,m;
	LinkList L;
	printf("N="); scanf("%d", &n);
	printf("M="); scanf("%d", &m);
	L = Init(n);
	joseph2(L,n,m);
	joseph(n,m);

	//ListDelNode(&L,3);
	//printList(L);
}

Ref:

http://zh.wikipedia.org/wiki/%E7%BA%A6%E7%91%9F%E5%A4%AB%E6%96%AF%E9%97%AE%E9%A2%98

Thomas H. Cormen,Charles E. Leiserson, Ronald L. Rivest, and Clifford Stein. Introduction to Algorithms, Second Edition. MIT Press and McGraw-Hill, 2001.ISBN 0-262-03293-7. Chapter 14: Augmenting Data Structures, pp.318.

http://www.cnblogs.com/yangyh/archive/2011/10/30/2229517.html






  • 9
    点赞
  • 38
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值