约瑟夫问题-Josephus--及实例说明

类似约瑟夫的问题又称为约瑟夫环。又称“丢手绢问题”。

  这个问题来自于这样的一个关于著名犹太历史学家 Josephus传说: 在罗马人占领乔塔帕特后,39 个犹太人与Josephus及他的朋友躲到一个洞中,39个犹太人决定宁愿死也不要被敌人抓到,于是决定了一个自杀方式,41个人排成一个圆圈,由第1个人开始报数,每报数到第3人该人就必须自杀,然后再由下一个重新报数,直到所有人都自杀身亡为止。然而Josephus 和他的朋友并不想遵从。首先从一个人开始,越过k-2个人(因为第一个人已经被越过),并杀掉第k个人。接着,再越过k-1个人,并杀掉第k个人。这个过程沿着圆圈一直进行,直到最终只剩下一个人留下,这个人就可以继续活着。问题是,给定了和,一开始要站在什么地方才能避免被处决?Josephus要他的朋友先假装遵从,他将朋友与自己安排在第16个与第31个位置,于是逃过了这场死亡游戏。

  约瑟夫问题是个有名的问题,比如6个人围一个圈,从第一个人开始123123的报数,最后一个报完了循环到第一个继续报数,报到数字3的人就退出游戏。这样退出的顺序就是: 3    6    4    2    5,最后剩下1一个人。

解决问题:

  就像题中描述的一样直接翻译成代码是(拯救约瑟夫的代码):

 1 #include<iostream>
 2 using namespace std;
 3 const int maxn = 1e6 + 10;
 4 main()
 5 {
 6     bool a[maxn] = {0};
 7     int n, m, i, f = 0, t = 0, s = 0;
 8     cin >> n >> m;
 9     do
10     {
11         ++t;//逐个枚举圈中的所有位置
12         if(t > n)
13             t = 1;//数组模拟环状,最后一个与第一个相连
14         if(!a[t])
15             s++;//第t个位置上有人则报数
16         if(s == m)//当前报的数是m
17         {
18             s = 0;//计数器清零
19             cout << t << " ";//输出被杀人编号
20             a[t] = 1;//此处人已死,设置为空
21             f++;//死亡人数+1
22         }
23     }
24     while(f != n - m + 1);//直到死亡得剩下他们两个人为止
25     cout << "
死亡人数:" << f << " " << endl;
26 //最后就剩下16和31没有被杀
27 }
View Code

  考虑到时间复杂度的问题,我们有下面一种改良的做法(约瑟夫递推算法):    

  为了讨论方便,先把问题稍微改变一下,并不影响原意:
 问题描述:n个人(编号0~(n-1)),从0开始报数,报到(m-1)的退出,剩下的人继续从0开始报数。求胜利者的编号。
 题解:我们知道第一个人(编号一定是(m-1)) 出列之后,剩下的n-1个人组成了一个新的约瑟夫环(以编号为k = m mod n的人开始报0继续计数)
这样我们就可以写出递推公式:
  f[1]=0;
  f=(f+m) mod i; (i>1)
代码展示:
 1 #include <iostream>
 2 #include <cstdio>
 3 using namespace std;
 4 //一圈一圈的循环的来回的转,最后剩一个人
 5 int main() {
 6     int n, m, f = 0;
 7     cin >> n >> m;
 8     for (int i = 1; i <= n; i++) f = (f + m) % i;
 9     cout << f + 1 << endl;
10     //输出最后活着的一个人.
11     return 0;
12 }
View Code

猴子选王

  题意:一堆猴子都有编号,编号是1,2,3 ...m,这群猴子(m个)按照1-m的顺序围坐一圈,从第1开始数,每数到第N个,该猴子就要离开此圈,这样依次下来,直到圈中只剩下最后一只猴子,则该猴子为大王。

解题思路<肯定比百度上面丰富>:

①、单向循环链表实现:

 1 #include <stdio.h>
 2 #include <malloc.h>
 3 #define LEN sizeof(struct monkey)//定义structmonkey这个类型的长度
 4 struct monkey
 5 {
 6     int num;
 7     struct monkey *next;
 8 };
 9 struct monkey *create(int m)
10 {
11     struct monkey *head, *p1, *p2;
12     int i;
13     p1 = p2 = (struct monkey*)malloc(LEN);
14     head = p1;
15     head->num = 1;
16     for(i=1, p1->num = 1; i < m; i++)
17     {
18         p1 = (struct monkey*)malloc(LEN);
19         p1->num = i + 1;
20         p2->next = p1;
21         p2 = p1;
22     }
23     p2->next = head;
24     return head;
25 }
26 struct monkey *findout(struct monkey *start, int n)
27 {
28     int i;
29     struct monkey *p;
30     i = n;
31     p = start;
32     for(i = 1; i < n - 1; i++)
33         p = p->next;
34     return p;
35 }
36 struct monkey *letout(struct monkey *last)
37 {
38     struct monkey *out, *next;
39     out = last->next;
40     last->next = out->next;
41     next = out->next;
42     free(out);
43     return next;
44 }
45 int main()
46 {
47     int m, n, i, king;
48     struct monkey *p1, *p2;
49     printf("请输入猴子的个数m:
");
50     scanf("%d", &m);
51     printf("每次数猴子的个数n:
");
52     scanf("%d", &n);
53     if(n == 1)
54     {
55         king=m;
56     }
57     else
58     {
59         p1 = p2 = create(m);
60         for(i = 1;i < m; i++)
61         {
62             p2 = findout(p1, n);
63             p1 = p2;
64             p2 = letout(p1);
65             p1 = p2;
66         }
67         king = p2->num;
68         free(p2);
69     }
70     printf("猴王的编号是:%d
", king);
71     return 0;
72 }
View Code

 单向链表的尾节点的下一个练到头节点上,这样实现循环,然后就判断删除即可。

②、数组模仿链表--有点并查集的思想:

 1 #include<stdio.h>
 2 #include<malloc.h>
 3 int main()
 4 {
 5     int *monkey, i, node, n, m;
 6     scanf("%d%d",&n, &m);
 7     monkey = (int*)malloc(sizeof(int)*(n+1));
 8     for(i=1;i<=n;i++)//初始化圈
 9     {
10         monkey[i] = i + 1;//i表示编号为i的猴, monkey[i]的值表示编号为i的猴的下一个猴的编号
11     }
12     monkey[n] = 1;//编号为n的下一个猴的编号是1
13     node = 1;
14     printf ("The murdered monkey are:
");
15     while(node != monkey[node])//当这个圈中只剩下一个猴的时候跳出循环.
16     {
17         for(i = 1; i < m - 1; i++)    //找出念m的那只猴
18         {
19             node = monkey[node];
20         }
21         printf("%d ", monkey[node]);//输出退出的猴编号
22         monkey[node] = monkey[monkey[node]];//修改退出的前一个猴的monkey[node]为退出的后一个猴的编号
23         node = monkey[node];//这句话中的node是退出的猴的后一个猴
24     }
25     printf("
The king is:
%d
", node);//输出最终大王的的编号
26     return 0;
27 }
View Code

 创建数组的时候下标从1开始填数,1上面填2,2上面填3,...,最后n上面填1,这样达到循环的效果,然后以monkey[i] = i;为条件进行猴子的出队操作。

③、数组实现:

 1 //猴子选大王问题(约瑟夫环问题)
 2 #include <stdio.h>
 3 #include <string.h>
 4 #include <stdlib.h>
 5 /*
 6  *
 7  * 屏蔽的代码部分为测试使用.
 8  * 为使删除的过程更加明确.
 9  *
10  */
11 int fre (char mok[],int k)
12 {
13     int i;
14 
15     /*printf("
猴子编号:
");
16     for(i = 0; mok[i] != ''; i++)
17         printf("%d	", mok[i]);//输出为踢出之前的编号,测试用
18     */
19     
20     
21     for(i = k; mok[i] != ''; i++)
22     {
23         mok[i] = mok[i+1];
24     }//一个循环,将k以后的元素前移
25 
26 
27     /*putchar('
');
28     for(i = 0; mok[i] != ''; i++)
29         printf("%d	", mok[i]);//输出踢出之后的编号,测试用
30     printf("
按回车继续下一轮:
");
31     system ("pause");    //暂停,测试使用。
32     */
33 
34     return 0;
35 }
36 int main()
37 {
38     char mok[50];
39     int i;
40     int n, s, b;//n表示猴子总数;s表示步进;b表示元素个数及大王编号
41     int j, k;//j,k都是计数器
42     mok[0] = 1;//初始化mok[0],让后面编号更简单的进行
43     printf("请输入猴子的总数:
");
44     scanf("%d", &n);//输入猴子的总数
45     for(i = 1; i < n; i++)
46     {
47         mok[i] = i + 1;
48     }//对猴子进行编号
49     mok[n] = '';//用0来表示数组的结尾
50     printf("请输入循环单位:
");
51     scanf("%d", &s);//单位长度
52     b = n;//统计猴子的个数
53     for(j = 1, k = 0;;j++,k++)
54     {
55         if(b == 1)
56         {
57             b = mok[0];
58             break;
59         }//如果元素只剩下一个,那么退出循环
60         if(j == s)
61         {
62             //printf("
它出列了:%d
", mok[k]); //测试用
63             fre(mok, k);//用于元素前移的函数
64             b--;
65             j = 1;
66         }//将猴子从数组中踢出,并重置计数器J。
67         
68         //--------------------------------------当删除的是最后一个数字时另行考虑.
69         
70         if (mok[k] == '')
71             j--;
72 
73         //---------------------------------------
74 
75         if(mok[k+1] == '')
76             k=-1;//重置计数器k,因为后面有k++所以k要在重置基础上-1.
77     }//判断是否为数组最后元素,重置计数器k。
78     printf("
最终大王是他:%d
", b);
79     return 0;
80 }
View Code

 只是使用一个数组,然后单独定义下标和计数器解决问题。

约瑟夫数学算法:

 1 #include <stdio.h>
 2 #include <conio.h>
 3 int main(void)
 4 {
 5     int n, i = 0, m, p;
 6     scanf("%d%d", &n, &m);//n总人数,m步长
 7     while(++i <= n)
 8     {
 9         p = i * m;
10         while(p > n)
11             p = p - n + (p - n - 1) / (m - 1);
12         printf("%d ", p);
13     }
14     getch();
15     return 0;
16 }
View Code

 寻找m和n之间的关系,初学者可以不掌握这种算法。

⑤、约瑟夫递推算法:

 1 #include <bits/stdc++.h>
 2 using namespace std;
 3 int king(int M,int N)
 4 {
 5     int k=0;
 6     for(int i = 2;i <= M; i++)
 7         k = (k + N) % i;
 8     return ++k;
 9 }
10 int main()
11 {
12     int n, m;
13     while (scanf("%d%d",&n, &m) && n && m)
14     {
15         cout << king(n, m) << endl;
16     }
17     return 0;
18 }
View Code

 寻找删除的时候的规律,利用递推关系写出递推式,很轻松的解决问题。

⑥、双向链表实现:

 1 #include <stdio.h>
 2 #include <malloc.h>
 3 #include <stdlib.h>
 4 struct node {
 5     int data;
 6     struct node *prior, *next;
 7 };
 8 struct node *creat (int n, struct node *h) {
 9     h = NULL;
10     struct node *s, *q;
11     for (int i = 1; i <= n; ++i) {
12         s =  (struct node *)malloc(sizeof (struct node));
13         s->data = i;
14         s->next = NULL;
15         if (h == NULL) {
16             h = s;
17             s->prior = h;
18             s->next = h;
19         } else {
20             s->next = q->next;
21             q->next = s;
22             s->prior = q;
23             h->prior = s;
24         }
25         q = s;
26     }
27     return h;
28 }
29 struct node *work (struct node *head, int m) {
30     struct node *p, *q;
31     p = head;
32     while (p->next != p) {
33         for (int i = 1;i < m; ++i) {
34             q = p;
35             p = p->next;
36         }
37         q->next = p->next;
38         p->next->prior = q;
39         printf ("%4d ", p->data);
40         free (p);
41         p = q->next;
42     }
43     return p;
44 }
45 int main () {
46     int m, n;    //n代表一共有n只猴子, m表示循环次数.
47     scanf ("%d%d", &n, &m);
48     struct node *head;
49     head = creat(n, head);
50     printf ("被删除的节点是:
");
51     printf ("
新大王是:%4d
", work(head, m)->data);
52     return 0;
53 }
View Code

 主要是练习双向链表的结构而已,原理和单向循环链表类似。

其实还有队列也可以实现,不过利用队列实现和方法③的思想就相似了,有想法的初学者小码友可以自己去操作一下。

  先总结这些,再有灵感再往上面加.欢迎批评。

 

原文地址:https://www.cnblogs.com/Ddlm2wxm/p/5808157.html