具体数学读书笔记之约瑟夫环
背景
什么是约瑟夫问题
约瑟夫问题又被称为约瑟夫环,他起源于1世纪的一名***历史学家。他在自己的***中写道,他和他的40个战友被罗马军队包围在洞中。他们讨论是***还是被俘,最终决定***,并以抽签的方式决定谁杀掉谁。约瑟夫斯和另外一个人是最后两个留下的人。约瑟夫斯说服了那个人,他们将向罗马军队投降,不再***。约瑟夫斯把他的存活归因于运气或天意,他不知道是哪一个。 我们这个问题略有变化,从围成标有记号1到n的圆圈的n个人开始,每隔1一个删去一个人,知道只有一个人幸存
暴力模拟
最简单的方法是用暴力跑模拟,尽管会耗费很多时间甚至空间,但它显然是有效的。
第一种模拟方法:用一个vis数组(一维)标记这个人是否还活着,把2当作是起点(如果n等于0输出0,如果n等于1输出1,如果n小于0输出-1代表不可能)用一个变量cur指向它,先把当前指向的人杀死然后cur再指向下一个要被杀死的人,一个cnt变量代表存活人数,初始化为n,当存活人数等于1时结束该过程,输出当前指向的编号
算法性能分析:这个方法实现起来比较简单,但无论是时间还是空间复杂度都很高,每次寻找都需要花费O(N)的时间去查找下一个将要被杀死的人在哪,而且要有一个大小为n的数组,这个数组不会随着算法的推进而减小,查找的时间却会随算法的推进而增加,所以这个程序的效率很低,但如果n的范围很小,出于容易编写的目的这个算法也是很合适的
C/C++参考代码
#include <stdio.h> #include <string.h> #define N 1100 int n, cur, cnt; int vis[N]; int main() { while (scanf("%d", &n) != EOF) { if (n < 0) puts("-1"); else if (n == 0) puts("0"); else if (n == 1) puts("1"); else { cur = 1; cnt = n; //下标从0开始,故下标比位序小1 memset(vis, 0, sizeof(int) * (n + 10)); while (cnt != 1) { vis[cur] = 1; cnt--; while (vis[cur]) cur = (cur + 1) % n; //下一个活着的人 cur = (cur + 1) % n; while (vis[cur]) cur = (cur + 1) % n; } printf("%d\n", cur + 1); } } return 0; }
链表模拟法:链表模拟法原理与第一种模拟方法差不多,只不过上面用数组这里用链表
算法性能分析:就复杂度来说链表模拟法与上面的方法是一样的,但实际上由于链表是变长的所以它的效率要比第一种要高一些,但对于一些数据规模比较大的问题仍然无法解决
C/C++参考代码
#include <stdio.h> #include <stdlib.h> //循环链表的结构体定义 typedef struct LNode { int id; struct LNode *next; }LNode, *LinkList; LinkList create(int n) { //创建一个长度为n的循环链表 LinkList list = (LinkList)malloc(sizeof(LNode)); list->id = 1; list->next = list; LinkList p = list; for (int i = 2; i <= n; ++i) { LinkList q = (LinkList)malloc(sizeof(LNode)); q->id = i; q->next = p->next; p->next = q; p = p->next; } return list; } int main() { int n; while (scanf("%d", &n) != EOF) { LinkList p = create(n); while (p->next != p) { //即只剩下一个元素 LinkList q = p->next; //删除被杀死的人 p->next = q->next; free(q); p = p->next; } printf("%d\n", p->id); } return 0; }
递归求解
分析:首先我们用J(n)来表示有n个人时幸存者的号码,假设一开始有2 n个人,则经过第一轮后剩下的是1, 3,5,···, 2n - 3, 2n - 1这时还有n的人,与1,2, 3,···,n比较可以发现他们之间存在映射关系2 * i - 1,后面的序列通过这个映射关系可以得到前面的序列,所以我们可以进一步推出J(2 n) = 2 * J(n) - 1。
同理可得到n为奇数时,j(2 * n + 1) = 2 * J(n) + 1
再将J(1) = 1带入可以得到以下递归式:
J(1) = 1
J(2 * n) = 2 J(n) - 1, n >= 1
J(2 * n + 1) = 2 J (n) + 1, n >= 1
时间复杂度:O(logN)
C/C++参考代码
#include <stdio.h> typedef long long ll; ll cal(ll n) { if (n == 1) return 1; if (n & 1) return 2 * cal(n / 2) + 1; else return 2 * (cal(n / 2)) - 1; } int main() { ll n; while (scanf("%lld", &n) != EOF) { printf("%lld\n", cal (n)); } return 0; }