具体数学读书笔记之约瑟夫环

背景

参加牛客网的有书共读活动(很不错的一个活动),每周写一篇读书笔记
另外打个小广告,我的csdn博客链接:https://blog.csdn.net/DongChengRong/article/details/86744785

什么是约瑟夫问题

约瑟夫问题又被称为约瑟夫环,他起源于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;
}

练习题

LA3882

#笔记##读书笔记#
全部评论

相关推荐

10-06 12:46
门头沟学院 Java
跨考小白:定时任务启动
点赞 评论 收藏
分享
评论
点赞
收藏
分享
牛客网
牛客企业服务