约瑟夫斯问题(有时也称为约瑟夫斯置换),是一个出现在计算机科学和数学中的问题。在计算机编程的算法中,类似问题又称为约瑟夫环。
有个囚犯站成一个圆圈,准备处决。首先从一个人开始,越过
个人(因为第一个人已经被越过),并杀掉第k个人。接着,再越过
个人,并杀掉第k个人。这个过程沿着圆圈一直进行,直到最终只剩下一个人留下,这个人就可以继续活着。
问题是,给定了和
,一开始要站在什么地方才能避免被处决?
比较简单的做法是用循环单链表模拟整个过程,时间复杂度是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