类似约瑟夫的问题又称为约瑟夫环。又称“丢手绢问题”。
这个问题来自于这样的一个关于著名犹太历史学家 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一个人。
解决问题:
就像题中描述的一样直接翻译成代码是(拯救约瑟夫的代码):
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
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 << "\n死亡人数:" << f << " " << endl; 26 //最后就剩下16和31没有被杀 27 }
考虑到时间复杂度的问题,我们有下面一种改良的做法(约瑟夫递推算法):
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
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 }
猴子选王
题意:一堆猴子都有编号,编号是1,2,3 ...m,这群猴子(m个)按照1-m的顺序围坐一圈,从第1开始数,每数到第N个,该猴子就要离开此圈,这样依次下来,直到圈中只剩下最后一只猴子,则该猴子为大王。
解题思路<肯定比百度上面丰富>:
①、单向循环链表实现:
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
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:\n"); 50 scanf("%d", &m); 51 printf("每次数猴子的个数n:\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\n", king); 71 return 0; 72 }
单向链表的尾节点的下一个练到头节点上,这样实现循环,然后就判断删除即可。
②、数组模仿链表--有点并查集的思想:
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
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:\n"); 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("\nThe king is:\n%d\n", node);//输出最终大王的的编号 26 return 0; 27 }
创建数组的时候下标从1开始填数,1上面填2,2上面填3,...,最后n上面填1,这样达到循环的效果,然后以monkey[i] = i;为条件进行猴子的出队操作。
③、数组实现:
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
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("\n猴子编号:\n"); 16 for(i = 0; mok[i] != '\0'; i++) 17 printf("%d\t", mok[i]);//输出为踢出之前的编号,测试用 18 */ 19 20 21 for(i = k; mok[i] != '\0'; i++) 22 { 23 mok[i] = mok[i+1]; 24 }//一个循环,将k以后的元素前移 25 26 27 /*putchar('\n'); 28 for(i = 0; mok[i] != '\0'; i++) 29 printf("%d\t", mok[i]);//输出踢出之后的编号,测试用 30 printf("\n按回车继续下一轮:\n"); 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("请输入猴子的总数:\n"); 44 scanf("%d", &n);//输入猴子的总数 45 for(i = 1; i < n; i++) 46 { 47 mok[i] = i + 1; 48 }//对猴子进行编号 49 mok[n] = '\0';//用0来表示数组的结尾 50 printf("请输入循环单位:\n"); 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("\n它出列了:%d\n", mok[k]); //测试用 63 fre(mok, k);//用于元素前移的函数 64 b--; 65 j = 1; 66 }//将猴子从数组中踢出,并重置计数器J。 67 68 //--------------------------------------当删除的是最后一个数字时另行考虑. 69 70 if (mok[k] == '\0') 71 j--; 72 73 //--------------------------------------- 74 75 if(mok[k+1] == '\0') 76 k=-1;//重置计数器k,因为后面有k++所以k要在重置基础上-1. 77 }//判断是否为数组最后元素,重置计数器k。 78 printf("\n最终大王是他:%d\n", b); 79 return 0; 80 }
只是使用一个数组,然后单独定义下标和计数器解决问题。
④、约瑟夫数学算法:
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
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 }
寻找m和n之间的关系,初学者可以不掌握这种算法。
⑤、约瑟夫递推算法:
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
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 }
寻找删除的时候的规律,利用递推关系写出递推式,很轻松的解决问题。
⑥、双向链表实现:
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
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 ("被删除的节点是:\n"); 51 printf ("\n新大王是:%4d\n", work(head, m)->data); 52 return 0; 53 }
主要是练习双向链表的结构而已,原理和单向循环链表类似。
⑦、其实还有队列也可以实现,不过利用队列实现和方法③的思想就相似了,有想法的初学者小码友可以自己去操作一下。
先总结这些,再有灵感再往上面加.欢迎批评。