Loading [MathJax]/jax/output/CommonHTML/config.js
前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
社区首页 >专栏 >约瑟夫环的三种解法

约瑟夫环的三种解法

作者头像
海天一树
发布于 2018-07-25 06:20:44
发布于 2018-07-25 06:20:44
8.7K00
代码可运行
举报
文章被收录于专栏:海天一树海天一树
运行总次数:0
代码可运行

约瑟夫环(Josephus)问题是由古罗马的史学家约瑟夫(Josephus)提出的,他参加并记录了公元66—70年犹太人反抗罗马的起义。约瑟夫作为一个将军,设法守住了裘达伯特城达47天之久,在城市沦陷之后,他和40名死硬的将士在附近的一个洞穴中避难。在那里,这些叛乱者表决说“要投降毋宁死”。于是,约瑟夫建议每个人轮流杀死他旁边的人,而这个顺序是由抽签决定的。约瑟夫有预谋地抓到了最后一签,并且,作为洞穴中的两个幸存者之一,他说服了他原先的牺牲品一起投降了罗马。 约瑟夫环问题的具体描述是:设有编号为1,2,……,n的n(n>0)个人围成一个圈,从第1个人开始报数,报到m时停止报数,报m的人出圈,再从他的下一个人起重新报数,报到m时停止报数,报m的出圈,……,如此下去,直到所有人全部出圈为止。当任意给定n和m后,设计算法求n个人出圈的次序。

解法一:用循环链表实现

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
#include<stdio.h>
#include<stdlib.h>
typedef struct node
{
    int data;
    struct node *next;
}Node;
/**
 * @功能 约瑟夫环
 * @参数 total:总人数
 * @参数 from:第一个报数的人
 * @参数 count:出列者喊到的数
 * @作者 zheng
 * @更新 2013-12-5
 */
void JOSEPHUS(int total, int from, int count)
{
    Node *p1, *head;
    head = NULL;
    int i;
    // 建立循环链表
    for(i = 1; i <= total; i++)
    {
        Node *newNode = (Node *)malloc(sizeof(Node));
        newNode->data = i;
        if(NULL == head)
        {
            head = newNode;
        }
        else
        {
            p1->next = newNode;
        }
        p1 = newNode;
    }
    p1->next = head;      // 尾节点连到头结点,使整个链表循环起来
    p1 = head;            // 使pcur指向头节点
    // 把当前指针pcur移动到第一个报数的人
    // 若从第一个人开始报数,这一段可要可不要
    for(i = 1; i < from; i++)
    {
        p1 = p1->next;
    }
    Node *p2 = NULL;
    // 循环地删除队列中报到count的结点
    while(p1->next != p1)
    {
        for(i = 1; i < count; i++)
        {
            p2 = p1;
            p1 = p1->next;
        }
        p2->next = p1->next;
        printf("Delete number: %d\n", p1->data);   // 打印所要删除结点的数据
        free(p1);                      // 删除结点,从内存释放该结点占用的内存空间
        p1 = p2->next;      // p1指针指向新的结点p2->next,即原先的p1->next
    }
    printf("The last one is No.%d\n", p1->data);
}
int main()
{
    int total, from, count;
    scanf("%d%d%d", &total, &from, &count);
    JOSEPHUS(total, from, count);
    return 0;
}

运行结果:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
13 1 3
Delete number: 3
Delete number: 6
Delete number: 9
Delete number: 12
Delete number: 2
Delete number: 7
Delete number: 11
Delete number: 4
Delete number: 10
Delete number: 5
Delete number: 1
Delete number: 8
The last one is No.13

解法二:数组实现

思路:设数组a有n个变量,每个变量中初始放的标识数是1,表示这个人在队列里,若出列标识数就变为0。 现在计数器从1开始向后数,每报一个数即把累加器加1。这里累加器表示报数人数。累列到m时,报数的人要出列,标识数要变为0。下一个人从1开始重新报数。 报到最后一个人后,从第一个人开始继续报数。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
#include<stdio.h>
#include<memory.h>
int main()
{
    int n, m;
    scanf("%d%d", &n, &m);
    int flag[n + 1];    // 在队列里标记为1,出列标记为0
    memset(flag, 0, sizeof(flag));  //把数组每个元素置0,在memory.h中声明
    int i = 0;
    int outCnt = 0;     //  记录出列的人数
    int numoff = 0;     //  报数
    // 默认都标记为1
    for(i = 1; i <= n; i++)
    {
        flag[i] = 1;
    }
    while(outCnt < n - 1)
    {
        for(i = 1; i <= n; i++ )
        {
            if (1 == flag[i])
            {
                numoff++;
                if(numoff == m)
                {
                    printf("Dequeue:%d\n", i);
                    outCnt++;
                    flag[i] = 0;    // 已出列的人标记为0
                    numoff = 0;     // 从头开始报数
                }
            }
        }
    }
    for(i = 1; i <= n; i++)
    {
        if(1 == flag[i])
        {
            printf("The last one is: %d\n", i);
        }
    }
    return 0;
}

运行结果:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
13 3
Dequeue:3
Dequeue:6
Dequeue:9
Dequeue:12
Dequeue:2
Dequeue:7
Dequeue:11
Dequeue:4
Dequeue:10
Dequeue:5
Dequeue:1
Dequeue:8
The last one is: 13

解法三:用数学公式求解

上面编写的解约瑟夫环的程序模拟了整个报数的过程,因为N和M都比较小,程序运行时间还可以接受,很快就可以出计算结果。可是,当参与的总人数N及出列值M非常大时,其运算速度就慢下来。例如,当N的值有上百万,M的值为几万时,到最后虽然只剩2个人,也需要循环几万次(由M的数量决定)才能确定2个人中下一个出列的序号。显然,在这个程序的执行过程中,很多步骤都是进行重复无用的循环。 那么,能不能设计出更有效率的程序呢? 办法当然有。其中,在约瑟夫环中,只是需要求出最后的一个出列者最初的序号,而不必要去模拟整个报数的过程。因此,为了追求效率,可以考虑从数学角度进行推算,找出规律然后再编写程序即可。

为了讨论方便,先根据原意将问题用数学语言进行描述。 问题:将编号为0~(N–1)这N个人进行圆形排列,按顺时针从0开始报数,报到M–1的人退出圆形队列,剩下的人继续从0开始报数,不断重复。求最后出列者最初在圆形队列中的编号。

下面首先列出0~(N-1)这N个人的原始编号如下: 0 1 2 3 … N-3 N-2 N-1

根据前面曾经推导的过程可知,第一个出列人的编号一定是(M–1)%N。例如,在13个人中,若报到3的人出列,则第一个出列人的编号一定是(3–1)%13=2,注意这里的编号是从0开始的,因此编号2实际对应以1为起点中的编号3。根据前面的描述,m的前一个元素(M–1)已经出列,则出列1人后的列表如下: 0 1 2 3 … M-3 M-2 ○ M M+1 M+2 … N-3 N-2 N-1 注意,上面的圆圈表示被删除的数。

根据规则,当有人出列之后,下一个位置的人又从0开始报数,则以上列表可调整为以下形式(即以M位置开始,N–1之后再接上0、1、2……,形成环状): M M+1 M+2 … N-2 N-1 0 1 … M-3 M-2

按上面排列的顺序从0开始重新编号,可得到下面的对应关系: M M+1 M+2 … N-2 N-1 0 1 … M-3 M-2 0 1 2 … N-(M+2) N-(M+1) N-M N-(M-1) … N-3 N-2 这里,假设上一行的数为x,下一行的数为y,则对应关系为:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
y = (x - M + N) % N           公式【1

或者

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
x = (y + M) % N                公式【2

通过上表的转换,将出列1人后的数据重新组织成了0~(N–2)共N–1个人的列表,继续求N–1个参与人员,按报数到M–1即出列,求解最后一个出列者最初在圆形队列中的编号。

看出什么规律没有?通过一次处理,将问题的规模缩小了。即对于N个人报数的问题,可以分解为先求解(N–1)个人报数的子问题;而对于(N–1)个人报数的子问题,又可分解为先求[(N-1)-1]个人报数的子问题,……。

问题中的规模最小时是什么情况?就是只有1个人时(N=1),报数到(M–1)的人出列,这时最后出列的是谁?当然只有编号为0这个人。因此,可设有以下函数:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
F(1) = 0

那么,当N=2,报数到(M–1)的人出列,最后出列的人是谁?应该是只有一个人报数时得到的最后出列的序号加上M,因为报到M-1的人已出列,只有2个人,则另一个出列的就是最后出列者,利用公式【2】,可表示为以下形式:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
F(2) = [F(1) + M] % N = [F(1) + M] % 2

比如,N=2, M=3时,有F(2) = [F(1) + M]%N = (0 + 3)%2 = 1

根据上面的推导过程,可以很容易推导出,当N=3时的公式:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
F(3) = [F(2) + M] % N = [F(2) + M] % 3

于是,咱们可以得到递推公式:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
F(1) = 0
F(N) = [F(N - 1) + M] % N      (N>1)

有了此递推公式,可使用递归方法来设计程序:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
#include <iostream>
using namespace std;
int josephus(int n, int m)
{
    if(1 == n)
    {
        return 0;
    }
    return (josephus(n - 1, m) + m) % n;
}
int main()
{
    int n, m;
    cin >> n >> m;
    cout << "最后出列的人的编号为(从0开始编号):" << josephus(n, m) << endl;
    return 0;
}

运行结果:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
13 3
最后出列的人的编号为(从0开始编号):12

使用递归函数会占用计算机较多的内存,当递归层次太深时可能导致程序不能执行,比如64层的汉诺塔需要计算很长的时间。 因此,这里可以将程序直接编写为以下的递推形式:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
#include <iostream>
using namespace std;
int main()
{
    int n, m;
    cin >> n >> m;
    int out = 0;
    for(int i = 2; i <= n; i++)
    {
        out = (out + m) % i;
    }
    cout << "最后出列的人的编号为(从0开始编号):" << out << endl;
    return 0;
}

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2018-06-01,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 海天一树 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
约瑟夫环的循环链表解法和数学公式解法
约瑟夫环(Josephus)问题是由古罗马的史学家约瑟夫(Josephus)提出的,他参加并记录了公元66—70年犹太人反抗罗马的起义。约瑟夫作为一个将军,设法守住了裘达伯特城达47天之久,在城市沦陷之后,他和40名死硬的将士在附近的一个洞穴中避难。在那里,这些叛乱者表决说“要投降毋宁死”。于是,约瑟夫建议每个人轮流杀死他旁边的人,而这个顺序是由抽签决定的。约瑟夫有预谋地抓到了最后一签,并且,作为洞穴中的两个幸存者之一,他说服了他原先的牺牲品一起投降了罗马。 约瑟夫环问题的具体描述是:设有编号为1,2,……,n的n(n>0)个人围成一个圈,从第1个人开始报数,报到m时停止报数,报m的人出圈,再从他的下一个人起重新报数,报到m时停止报数,报m的出圈,……,如此下去,直到所有人全部出圈为止。当任意给定n和m后,设计算法求n个人出圈的次序。
海天一树
2018/07/25
2.3K0
算法科普:什么是约瑟夫环
约瑟夫环(约瑟夫问题)是一个数学的应用问题:已知 n 个人(以编号1,2,3…n分别表示)围坐在一张圆桌周围。从编号为 k 的人开始报数,数到 m 的那个人出圈;他的下一个人又从 1 开始报数,数到 m 的那个人又出圈;依此规律重复下去,直到剩余最后一个胜利者。
五分钟学算法
2019/05/06
1.3K0
算法科普:什么是约瑟夫环
约瑟夫斯环问题的几种经典解法
经典的约瑟夫斯 问题描述: 有n个人围成一圈,从1开始顺序排号。从第一个人开始报数(从1~3报数),凡报到3的人退出圈子,问最后留下的是原来的第几号? 数组循环模拟法 const int N = 10
WindSun
2019/08/31
1.4K0
【算法】约瑟夫环问题解析与实现
约瑟夫环(Josephus Problem)是一个经典的数学问题,涉及一个编号为 1 到 n 的人围成一圈,从第一个人开始报数,报到某个数字 m 的人出列,然后再从下一个人开始报数,如此循环,直到所有人都出列。本篇博客将详细解析约瑟夫环问题,并使用 Python 实现算法。
繁依Fanyi
2023/10/12
1K0
【数据结构与算法】【约瑟夫问题】还在用递归?教你用链表秒杀约瑟夫
本文原本是对链表学习的记录笔记 因为约瑟夫问题笔记经典就拿来做大题材了,要是没学过链表或者链表还不熟悉的伙伴可以慢慢读,要是以及学过链表了,纯粹来看全新的解题思路的 可以用目录传送门往下跳
苏泽
2024/03/01
2040
经典算法之约瑟夫问题
据说著名犹太历史学家 Josephus有过以下的故事:在罗马人占领乔塔帕特后,39 个犹太人与Josephus及他的朋友躲到一个洞中,39个犹太人决定宁愿死也不要被敌人抓到,于是决定了一个自杀方式,41个人排成一个圆圈,由第1个人开始报数,每报数到第3人该人就必须自杀,然后再由下一个重新报数,直到所有人都自杀身亡为止。 然而Josephus 和他的朋友并不想遵从。首先从一个人开始,越过k-2个人(因为第一个人已经被越过),并杀掉第k个人。接着,再越过k-1个人,并杀掉第k个人。这个过程沿着圆圈一直进行,直到最终只剩下一个人留下,这个人就可以继续活着。问题是,给定了和,一开始要站在什么地方才能避免被处决?
用户3467126
2019/11/14
1.7K0
约瑟夫问题方法总结
n个人围成一个圈,每个人分别标注为1、2、...、n,要求从1号从1开始报数,报到k的人出圈,接着下一个人又从1开始报数,如此循环,直到只剩最后一个人时,该人即为胜利者。例如当n=10,k=4时,依次出列的人分别为4、8、2、7、3、10,9、1、6、5,则5号位置的人为胜利者。给定n个人,请你编程计算出最后胜利者标号数。(要求用单循环链表完成。) 第一行为人数n;  第二行为报数k 10 4 对于约瑟夫问题当前实现方法大概有两种: 一:模拟: 链表模拟: 1 #include<stdio.h> 2
Angel_Kitty
2018/04/08
1.3K0
动态规划解决约瑟夫环问题
题目: 约瑟夫环(约瑟夫问题)是一个数学的应用问题:已知n个人(以编号0,1,2,3…n-1分别表示)围坐在一张圆桌周围。从编号为0的人开始报数,数到m的那个人出列;他的下一个人又从1开始报数,数到m的那个人又出列;依此规律重复下去,求最后一个出列的人。
恋喵大鲤鱼
2018/08/03
1.2K0
C++版 - 剑指Offer 面试题45:圆圈中最后剩下的数字(约瑟夫环问题,ZOJ 1088:System Overload类似)题解
剑指Offer 面试题45:圆圈中最后剩下的数字(约瑟夫环问题) 原书题目:0, 1, … , n-1 这n个数字排成一个圈圈,从数字0开始每次从圆圏里删除第m个数字。求出这个圈圈里剩下的最后一个数字
Enjoy233
2019/03/05
5700
太透彻了:约瑟夫环的三种解法
约瑟夫环问题是算法中相当经典的一个问题,其问题理解是相当容易的,并且问题描述有非常多的版本,并且约瑟夫环问题还有很多变形,这篇约瑟夫问题的讲解,一定可以带你理解通通!
bigsai
2021/04/12
3.6K0
约瑟夫环问题
约瑟夫环是一个数学的应用问题:已知n个人(以编号0,2,3...n-1分别表示)围坐在一张圆桌周围。从编号为k的人开始报数,数到m的那个人出列;他的下一个人又从1开始报数,数到m的那个人又出列;依此规律重复下去,直到圆桌周围的人全部出列。 解决方案: 约瑟夫环有递归和非递归两种解决方案。 1. 非递归可用数组和循环链表来解决。下面给出用数组解决的源码:
卡尔曼和玻尔兹曼谁曼
2019/01/25
6510
2024春晚纸牌魔术原理----环形链表的约瑟夫问题
https://www.nowcoder.com/practice/41c399fdb6004b31a6cbb047c641ed8a?tab=note
一枕眠秋雨
2024/03/11
950
2024春晚纸牌魔术原理----环形链表的约瑟夫问题
【动态规划篇】穿越算法迷雾:约瑟夫环问题的奇幻密码
当古老的传说与现代的智慧交织,一场数字的盛宴拉开帷幕。约瑟夫环问题宛如一座神秘的迷宫,静静矗立在算法的王国。它见证了无数思维的碰撞与火花的绽放,如今,我们将手持逻辑的利斧,斩断层层迷雾,深入其内核,探寻隐藏在数字循环背后的终极真相,开启一场扣人心弦的解谜冒险,让智慧之光穿透谜题的黑暗,照亮那最终的答案。
用户11458826
2025/01/23
1020
【动态规划篇】穿越算法迷雾:约瑟夫环问题的奇幻密码
Josephu(约瑟夫、约瑟夫环) 问题
Josephu 问题为:设编号为 1,2,… n 的 n 个人围坐一圈,约定编号为 k(1<=k<=n)的人从 1 开始报数,数到 m 的那个人出列,它的下一位又从 1 开始报数,数到 m 的那个人又出列,依次类推,直到所有人出列为止,由此产生一个出队编号的序列。
乐心湖
2021/01/18
8430
Josephu(约瑟夫、约瑟夫环) 问题
循环链表练习(一)--约瑟夫环
  约瑟夫环问题,是一个经典的循环链表问题,题意是:已知 n 个人(分别用编号 1,2,3,…,n 表示)围坐在一张圆桌周围,从编号为 k 的人开始顺时针报数,数到 m 的那个人出列;他的下一个人又从 1 开始,还是顺时针开始报数,数到 m 的那个人又出列;依次重复下去,直到圆桌上剩余一个人。 如图所示,假设此时圆周周围有 5 个人,要求从编号为 3 的人开始顺时针数数,数到 2 的那个人出列:
嵌入式与Linux那些事
2021/05/20
3820
循环链表练习(一)--约瑟夫环
python约瑟夫环「建议收藏」
约瑟夫环是一个经典的数学问题,我们不难发现这样的依次报数,似乎有规律可循。为了方便导出递推式,我们重新定义一下题目。 问题: N个人编号为1,2,……,N,依次报数,每报到M时,杀掉那个人,求最后胜利者的编号。
全栈程序员站长
2022/09/05
3870
约瑟夫问题
1、约瑟夫问题原题: n个小孩子手拉手围成一个圈,编号为k(1 <= k <= n )的人从1开始报数,报到m的那个人出列,它的下一位又从1开始报数,报到m的又出列……依此类推,直到所有人都出列,由此产生一个出队编号的序列。
贪挽懒月
2020/09/08
4780
约瑟夫环
已知 n 个人(以编号1,2,3...n分别表示)围坐在一张圆桌周围。. 从编号为 k 的人开始报数,数到 m 的那个人出圈;他的下一个人又从 1 开始报数,数到 m 的那个人又出圈;依此规律重复下去,直到剩余最后一个胜利者。. 例如:有10个人围成一圈进行此游戏,每个人编号为 1-10 。. 若规定数到 3 的人出圈。. 则游戏过程如下。(1)开始报数,第一个数到 3 的人为 3 号,3 号出圈。(2)从4号重新从1开始计数,则接下来数到3的人为6号,6号出圈。(3)按以上的方法依次类推。
算法与编程之美
2023/08/22
2110
约瑟夫环
约瑟夫环问题递归解法的一点理解
约瑟夫游戏的大意:30个游客同乘一条船,因为严重超载, 加上风浪大作,危险万分。因此船长告诉乘客,只有将全船 一半的旅客投入海中,其余人才能幸免于难。无奈,大家只 得同意这种办法,并议定30 个人围成一圈,由第一个人数起,依次报数,数到第9人,便把他投入大海中,然后再从 他的下一个人数起,数到第9人,再将他投入大海中,如此 循环地进行,直到剩下 15 个游客为止。问:哪些位置是将 被扔下大海的位置?
全栈程序员站长
2022/09/05
5370
从约瑟夫环看循环链表
编号为1,2,...,n的n个人按顺时针方向围坐一圈,每人持有一个密码(正整数)。现在给定一个随机数m>0,从编号为1的人开始,按顺时针方向1开始顺序报数,报到m时停止。报m的人出圈,同时留下他的密码作为新的m值,从他在顺时针方向上的下一个人开始,重新从1开始报数,如此下去,直至所有的人全部出圈为止。
phith0n
2020/10/16
5020
从约瑟夫环看循环链表
推荐阅读
相关推荐
约瑟夫环的循环链表解法和数学公式解法
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
查看详情【社区公告】 技术创作特训营有奖征文