约瑟夫环问题的几种解法

    一、问题的来历

    据说著名犹太历史学家 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),计算量小,运算速度快,但无法模拟过程,无法按次序输出先后的出列人员。

    原文作者:约瑟夫环问题
    原文地址: https://blog.csdn.net/ltxiaobing/article/details/50412403
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞