约瑟夫环问题的几种解法

来源:互联网 发布:r语言与数据挖掘 谢 编辑:程序博客网 时间:2024/06/08 08:54
    一、问题的来历
    据说著名犹太历史学家 Josephus有过以下的故事:在罗马人占领乔塔帕特后,39个犹太人与Josephus及他的朋友躲在一个洞中,39个犹太人决定宁愿死也不要被敌人抓到,于是决定了一个自杀方式,41个人排成一个圆圈,由第1个人开始报数,每报数到第3人该人就必须自杀,然后再由下一个重新报数,直到所有人都自杀身亡为止。然而Josephus 和他的朋友并不想遵从。问题是,给定了总人数n和报数值m,一开始要站在什么地方才能避免被处决?Josephus要他的朋友先假装遵从,他将朋友与自己安排在第16个与第31个位置,于是逃过了这场死亡游戏。
    二、问题的基本描述
    n个人围成圈,依次编号为1、2、3、...、n,从1号开始依次报数,当报到m时,报m的人退出,下一个人重新从1报起,当报到m时,报m的人退出,如此循环下去,问最后剩下的那个人的编号是多少?
    三、解题方法
    (一)模拟法
    这个问题的解决,最容易想到的方法就是模拟法,利用一个循环链表,节点的数值部分存储整数1至n,每次遍历m步,把第m个节点数值打印输出后删除该节点,如此循环下去,直至只剩一个节点,(只剩一个节点的判断方法是:节点的指向节点自己,也就是p->next=p),节点的数值部分就是最后那个人的编号。下面是c语言程序:
#include <stdio.h>#include <stdlib.h>typedef struct node  /*声明一个链表节点*/{    int number;    struct node *next;}Node;Node* CreatNode(int x)  /*创建链表节点的函数*/{    Node *p;    p=(Node*)malloc(sizeof(Node));    p->number=x;    p->next=NULL;    return p;}Node* CreatJoseph(int n)  /*创建环形链表,存放整数1到n*/{    Node *head,*p,*q;    int i;    for(i=1;i<=n;i++)    {        p=CreatNode(i);        if(i==1)            head=p;        else            q->next=p;            q=p;    }    q->next=head;    return head;}void RunJoseph(int n,int m) /*模拟运行约瑟夫环,每数到一个数,将它从环形链表中摘除,并打印出来*/{    Node *p,*q;    p=CreatJoseph(n);    int i;    while(p->next!=p)    {        for(i=1;i<m-1;i++)        {            p=p->next;        }        q=p->next;        p->next=q->next;        p=p->next;        printf("%d--",q->number);        free(q);    }    printf("\n最后剩下的数为:%d\n",p->number);}int main(){    int n,m;    scanf("%d %d",&n,&m);    RunJoseph(n,m);    return 0;}

 

(二)递归法

    要想用到递归法就必须找到f(n)和f(n-1)之间的关系,那么约瑟夫环有没有这样一个规律关系在呢,答案是有的。
    我们假设n个人,报数到m的退出,最后剩下人的编号为x。那么第一次报数后,编号为m的人退出,那么剩下的人从编号为M+1继续报数,如果我们把m+1看成1,m+2看成2,....,n看成n-m,1看成是1-m+n,2看成是2-m+n,...,m-1看成是(m-1)-m+n也就是n-1,那么这就变成了一个n-1个人报数为m的约瑟夫环的问题,而且这里最后剩下人就是原来编号为x的那个人,按前面的对应关系f(n)=(f(n-1)+m)%n,这里有个例外,就是如果x=n的话,就会出现f(n)=n,而(f(n-1)+m)%n=((n-m)+m)%n=n%n=0,所以我们把编号加点小技巧,如果n个人编号从0编到n-1,那么f(n)=(f(n-1)+m)%n成立,如果换算成1到n编号,f(n)-1=((f(n-1)-1)+m)%n,也就是f(n)=((f(n-1)-1)+m)%n+1。
    有了f(n)=(f(n-1)-1+m)%n+1这个公式,另外我们知道,当n=2时,m为奇数时最后留下的是2,m为偶数时最后留下的是1,我们就可以写出递归程序了,下面是递归法的C语言程序:
#include <stdio.h>int Joseph(int n,int m)/*计算约瑟夫环的递归函数*/{    if(n<=1||m<=1)        return -1;    if(n==2)    {        if(m%2==0)            return 1;        else            return 2;    }    else    {        return (Joseph(n-1,m)+m-1)%n+1;    }}int main(){    int n,m,x;    scanf("%d %d",&n,&m);    x=Joseph(n,m);    printf("最后一个数为:%d\n",x);    return 0;}


    (三)迭代法
    有递归法,我们看看有没有相应的迭代法,用的还是刚才的那个公式,方法就是从总人数为2开始一步一步推导到总人数为n时最后应该留下谁。下面是迭代法的C语言程序:
#include <stdio.h>int Josephus(int n,int m)/*计算约瑟夫环问题的迭代法函数*/{    int i;    int x,y;    if(n<=1||m<=1)        return -1;    if(m%2==0)        y=1;    else        y=2;           for(i=3;i<=n;i++)    {        x=(y-1+m)%i+1;        y=x;    }    return y;} int main(){    int n,m,x;    scanf("%d %d",&n,&m);    x=Josephus(n,m);    printf("最后一个的编号是: %d\n",x);    return 0;}   


    四、总结
    模拟法的好处是,可以模拟过程,可以将出列人员按次序输出出来,不足之处是算法复杂度为O(mn),当m和n数值较大时,计算量太大。递归法和迭代法的好处是算法复杂度为O(n),计算量小,运算速度快,但无法模拟过程,无法按次序输出先后的出列人员。
0 0