练习题 —— 约瑟夫环问题

练习题 —— 约瑟夫环问题

我们今天来看一个比较头痛的问题——约瑟夫环

约瑟夫环的背景

约瑟夫环的背景是来自一个故事:

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

约瑟夫环有各种变形的题目:猴子选大王破冰游戏等等:

https://leetcode.cn/problems/yuan-quan-zhong-zui-hou-sheng-xia-de-shu-zi-lcof/

在这里插入图片描述
这道题的的难点就是每次重新开始报数的位置不固定,因为有循环的取模的操作,导致每次重新报数的位置变化可能很大。

一般操作

如果是刚开始接触这个问题,可以用链表或者数组模拟,这里给出伪代码:

#include <iostream> // 确保包含输入输出流头文件

using namespace std;

int main()
{
    bool a[101] = {0}; // 初始化一个大小为101的布尔数组,用于标记位置是否有人,默认为无人

    int n, m, i, f = 0, t = 0, s = 0; // 初始化变量
    cin >> n >> m; // 输入总人数n和报数到m时杀人
    // 开始循环,直到所有人都被杀死
    do
    {
        // 逐个枚举圈中的位置,模拟报数过程
        ++t; // 递增位置计数器
        if (t > n) // 如果超过总人数,则回到起始位置(环状结构)
            t = 1;

        // 如果当前位置无人(a[t]为false),则报数并递增报数计数器
        if (!a[t])
        {
            s++; // 报数

            // 如果报数到m,则执行杀人操作
            if (s == m)
            {
                s = 0; // 报数计数器重置为0,开始新一轮报数
                cout << t<< "号位置" << endl; // 输出被杀的人的编号
                a[t] = 1; // 标记当前位置已无人
                f++; // 死亡人数加1
            }
        }
        // 注意:如果当前位置已有人(a[t]为true),则不执行报数和杀人操作,直接继续下一个位置
    } while (f != n); // 循环直到所有人都被杀死

    return 0; // 程序正常结束
}

这样有个问题,如果人很多,我就要开一个很大的数组,很消耗空间,并且容易超时,那该怎么办呢?

其实约瑟夫环有专门的公式,我们下面就来推导一下:

公式推导

在这里插入图片描述
我们知道一开始,从0开始报数:
在这里插入图片描述
然后2号位置被淘汰:
在这里插入图片描述然后我们往后走:
在这里插入图片描述
这个时候,我们做一个变换,为了方便我们观察,我们把0,1号位置的人放在后面:
在这里插入图片描述
我们这个时候重新给各个位置的人编号:
在这里插入图片描述

这样,通过重新编号之后,我们可以得到一个事实最后活下来的是0号位置(这点很重要)
在这里插入图片描述这个时候我们的0号位是最终的结果,现在我们可以根据这个结果反推当两个人的时候,胜利者在上一轮的编号为多少

我们是数到3淘汰,只有2个人,有循环的操作,所以我们要加取模操作:
在这里插入图片描述
我们知道了倒数第二轮的,就可以推出倒数第三轮的:
在这里插入图片描述
这样我们可以得出一个递推公式:

当前存活位置 = (上一轮存活位置 + 夺命数)% 上一轮人数

什么时候停止呢?恢复到一开始的人数就行了:

#include <iostream> 
using namespace std;

void function(int number_people,int kill_number)
{
    int survivor = 0; // 最后存活位置
    for(int i = 2; i <=number_people; i++) //从倒数第二轮开始复盘
    {
        survivor = (survivor + kill_number) % i;
        cout << survivor << "号位置存活" << endl; // 输出被杀的人的编号
    }
}

int main()
{
    function(10,3);
}

在这里插入图片描述

递归版本

可以用迭代,一般也可以用递归:

#include <iostream> 
using namespace std;

int function2(int number_people,int kill_number)
{
    if(number_people == 1)
    {
        return 0;
    }

    int survivor = function2(number_people - 1,kill_number);
    
    return (survivor + kill_number) % number_people;
}

int main()
{
    cout << function2(3,3) << "号位置存活" << endl;
}

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值