Loading

约瑟夫环问题详解

很久以前,有个叫Josephus的老头脑袋被门挤了,提出了这样一个奇葩的问题:

已知n个人(以编号1,2,3...n分别表示)围坐在一张圆桌周围。从编号为k的人开始报数,数到m的那个人出列;他的下一个人又从1开始报数,数到m的那个人又出列;依此规律重复下去,直到圆桌周围的人全部出列                                                                   

                 

这就是著名的约瑟夫环问题,这个问题曾经风靡一时,今天我们就来探讨一下这个问题。

 

这个算法并不难,都是纯模拟就能实现的。

思路一:

用两个数组,mark[10000]和num[10000],mark这个数组用来标记是否出队,num这个用来存值,然后每次到循环节点的时候就判断mark是否为0(未出队),为0则输出num[i],并标记mark[i]=1,直到所有的num都出队。

附上C++代码:

#include<iostream>
#include<cstring>
#include<cstdio>
#include<cstdlib>
#include<cctype>
#include<cmath>
#include<algorithm>
using namespace std;
char num[1000];
bool mark[1000];
int main()
{
    while(true)
    {
        memset(mark,0,sizeof(mark));
        int n;
        int k,m;
        int i,j;
        int del=k-1;
        cin>>n>>k>>m;
        for(i=0;i<n;++i)
        {
            cin>>num[i];
        }
        int cnt=n;
        for(i=cnt;i>1;--i)
        {
            for(int j=1;j<=m;)
            {
                del=(del+1)%n;
                if(mark[del]==0)
                    j++;
            }
            cout<<num[del]<<" ";
            mark[del]=1;
        }
        for(i=0;i<n;++i)
        {
            if(mark[i]==0)
                break;
        }
        cout<<endl<<"The final winner is:"<<num[i]<<endl;
    }
    return 0;
}

 

思路二:

用一个数组就可,每次到了循环节点了就将num[i]输出,然后将后面的值往前移动一位,直到所有的节点出列。

虽然说这种方法比第一种方法所用的内存有所优化,但时间上却增加了好多,这也体现了程序设计中用空间来换时间的思想。

附上代码:

#include<iostream>
#include<cstring>
#include<cstdio>
#include<cstdlib>
#include<cctype>
#include<queue>
#include<cmath>
#include<algorithm>
using namespace std;
char num[1000];
int main()
{
    while(true)
    {
        int n;
        int k,m;
        int i,j;
        cin>>n>>k>>m;
        int del=k;
        for(i=0;i<n;++i)
        {
            cin>>num[i];
        }
        int cnt=n;
        for(i=cnt;i!=1;--i)
        {
            del=(del+m-1)%i;
            cout<<num[del]<<" ";
            for(j=del;j<=i-2;++j)
            {
                num[j]=num[j+1];
            }
        }
        cout<<endl<<"The final winner is:"<<num[0]<<endl;
    }
    return 0;
}

 

思路三:

也就是书上说的用循环链表,因为整个过程都是一个完整的环,所以用循环链表是一个不错的选择,但是就我个人来说我很不喜欢用链表,因为现在的程序设计题目中一般都是卡时间,基本上卡内存的很少,所以链表的优势根本体现不出来,当然方法还是要掌握的.

思路:构建一个循环链表,每个结点的编号为0, 1, ...... n-1。每次从当前位置向前移动m-1步,然后删除这个结点。最后剩下的结点就是胜利者。给出两种方法实现,一种是自定义链表操作,另一种用是STL库的单链表。不难发现,用STL库可以提高编写速度。

创建的循环链表的结构图:

http://pic002.cnblogs.com/images/2012/198486/2012101015571941.jpg

解决约瑟夫环问题的过程:

http://pic002.cnblogs.com/images/2012/198486/2012101016082279.jpg

附上我的代码:(好久没写指针,感觉有点生疏了,挣扎了半天才弄出来)

#include<iostream>
#include<cstring>
#include<cstdio>
#include<cstdlib>
#include<cctype>
#include<queue>
#include<cmath>
#include<algorithm>
using namespace std;
struct Node
{
    char num;
    Node *next;
};
Node *head,*p1,*p2;
void Make_Linklist(int n)//构造循环链表
{
    int i,j;
    head=p1=(Node *)malloc(sizeof(Node));//开辟一段大小为sizeof(Node)的空间,并将地址给p1和head
    cin>>(*p1).num;
    (*head).num=(*p1).num;
    for(i=2;i<=n;i++)
    {
        //p2用来开辟结点,p1指向最后一个结点,对所指的内容赋值前必须先对指针分配内存
        p2=(Node*)malloc(sizeof(Node));
        //Node 新的内存空间,并把地址给p2
        cin>>(*p2).num;
        p1->next=p2;
        p1=p1->next;
    }
    p1->next=head;//将带链表首尾连起来生成循环链表
}

void Josephus(int n,int k,int m)
{
    int cnt=0;
    int i,j;
    if(n==1)
    {
        cout<<(*p1).num;
    }
    else
        {
//            for(i=0;i<k;i++)//移动到第k个位置,从第k个位置开始
//            {
//                head->next=head->next->next;
//            }
            while(cnt<n-1)
                {
                    for(i=1;i<=m-2;i++)//找到要出队的上一个head
                    {
                        head = head->next;
                    }
                    Node *temp;
                    temp=head->next;
                    printf("%c ",(*temp).num);
                    head->next = head->next->next;//通过将指针指向移至下一个,来删除该节点
                    head = head->next;
                    cnt++;
                }
                cout<<endl<<"The winner is:"<<(*head).num<<endl;
        }
}

int main()
{
    while(true)
    {
        int n,k,m;
        cin>>n>>k>>m;
        Make_Linklist(n);
        Josephus(n,k,m);
    }
    return 0;
}

方法二:数学推导(这个有一定的难度,看了好久的博客才看懂)

推导:

O                     (0+5)%2 = 0
o O                   (0+5)%2 = 1
O o o                 (1+5)%3 = 0
o O o o               (0+5)%4 = 1
o O o o o             (1+5)%5 = 1
O o o o o o           (1+5)%6 = 0
o o o o o O o         (0+5)%7 = 5
o o O o o o o o       (5+5)%8 = 2
o o o o o o o O o     (2+5)%9 = 7

前面的字母代表当前有多少个节点,后面的算式代表当前删除的节点在重映射后的位置。

考察约瑟夫环的变化过程,当第k个节点存在的时候,序列理应如此:1,2,3,4,5,6,7,8,9

将元素左移3次,得到3,4,5,6,7,8,9,1,2

接着删除第一个,得到4,5,6,7,8,9,1,2

继续左移3次,得到6,7,8,9,1,2,4,5

删除第一个:7,8,9,1,2,4,5

左移:9,1,2,4,5,7,8

删除:1,2,4,5,7,8

左移:4,5,7,8,1,2

删除:5,7,8,1,2

左移:8,1,2,5,7

删除:1,2,5,7

左移:5,7,1,2

删除:7,1,2

左移:2,7,1

删除:7,1

左移:7,1

删除:1

按照这个手法,不断移动并删除,元素最终会减少到1。检查某个点何时被删除的测试也可以如此进行,不断左移,排在第一个的元素就是被删除的。

因此,思考逆过程,从1个元素开始,每个元素就应该是右移k位,不断右移,最终就能找回原始的约瑟夫环的位置:

1元素:1<------------------>1,                      (1+3)%1 eq 1

2元素:1,2<---------------->7,1,                   (1+3)%2 eq 2

3元素:1,2,3<-------------->7,1,2,                (2+3)%3 eq 2

4元素:1,2,3,4<------------>1,2,5,7,             (2+3)%4 eq 1

5元素:1,2,3,4,5<---------->5,7,8,1,2,           (1+3)%5 eq 4

6元素:1,2,3,4,5,6<-------->1,2,4,5,7,8,        (4+3)%6 eq 1

7元素:1,2,3,4,5,6,7<------>7,8,9,1,2,4,5,      (1+3)%7 eq 4

8元素:1,2,3,4,5,6,7,8<---->4,5,6,7,8,9,1,2,   (4+3)%8 eq 7

9元素:1,2,3,4,5,6,7,8,9<-->1,2,3,4,5,6,7,8,9,(7+3)%9 eq 1

而做这个事情的,就是(num+k)%line_length。

 

另外还有双向约瑟夫环问题,比这个要复杂一些,在这里就不展开了。

整理了一上午,终于弄好了,不过还是很值得的.

 

 

posted @ 2014-04-16 12:55  北岛知寒  阅读(481)  评论(0编辑  收藏  举报