算法-数学-约瑟夫环(孩子们的游戏)

文章讲述了约瑟夫环问题的数学解法,涉及如何通过递推公式(f[i]=(m+f[i-1])%i)解决该问题,以及递归和迭代两种算法的实现。作者通过实例解析了公式背后的逻辑和时间复杂度分析。
摘要由CSDN通过智能技术生成

约瑟夫环问题-孩子们的游戏

背景

今天牛客上刷到一题"孩子们的游戏"算法题,在看题解时,发现数学思路的解法特别简洁,但是理解起来很困难,在经历了一番头脑风暴的理解后,决定将它记录下来,希望能给到大家和自己一些帮助。

题目描述

每年六一儿童节,牛客都会准备一些小礼物和小游戏去看望孤儿院的孩子们。其中,有个游戏是这样的:首先,让 n 个小朋友们围成一个大圈,小朋友们的编号是0~n-1。然后,随机指定一个数 m ,让编号为0的小朋友开始报数。每次喊到 m-1 的那个小朋友要出列唱首歌,然后可以在礼品箱中任意的挑选礼物,并且不再回到圈中,从他的下一个小朋友开始,继续0… m-1报数…这样下去…直到剩下最后一个小朋友,可以不用表演,并且拿到牛客礼品,请你试着想下,哪个小朋友会得到这份礼品呢?
在这里插入图片描述
在这里插入图片描述
示例1
输入:5,3
返回值:3

示例2
输入:2,3
返回值:1
对示例2说明:
有2个小朋友编号为0,1,第一次报数报到3的是0号小朋友,0号小朋友出圈,1号小朋友得到礼物

示例3
输入:10,17
返回值:2

题目分析

看到了题目和题解,了解到这是一道典型的约瑟夫环的问题,有兴趣的朋友可以自己百度一下。它的描述形式有很多,但最总结出来就是:
给定一个数n,将它排列成0-n-1个数,然后组成个环。再给定一个数m,从环的0开始数,数到第m个数移除,每次去掉第m个数,下一次从去掉数的下一个开始,直到剩下最后一个数,求这个数。

题解

抽丝剥茧后,看清了题目的本质,接下来就看需要怎么解题了。网上大致提供了两种解题思路,

  • 第一种是比较容易理解的模拟游戏过程的解法。
  • 第二种是利用数学归纳的思想总结规律,再进行求解。

本文主要对第二种解法的思路做一下整理和分析。
第二种解法中,最重要的就是公式 (m+x)%n,那它是怎么来的,又怎么理解它,我们继续往下看。

我们且将上面的题概括为从 f(n,m)中找数字T的问题。数字T就是我们从0到n-1个数字中,数第m个数,数了很多轮后,最后剩下的那个数字。
这个数字T我们暂时找不到,除非我们一轮轮的模拟下去,很显然这不是我们想要的。那我们能不能试试把规模缩小一点,把n个数字变成n-1个数字。那么我们就从0到n-2个数字中,数第m个数,数了很多轮后,找到最后剩下的那个数字,我可以记为f(n-1,m)=X。显然X我们也找不到。但是我们能否找到X和T之间的关系呢?如果找到了,对我们有什么帮助呢?

一旦我们找到了X和T的关系,那么我们是不是就可以利用这个关系式从n=1的时候开始递推,最终找到我们想要的那个T。

那么我们接下来分析一下如何找他们之间的关系。

我们先来模拟一轮游戏,我们将n个数字用编号写开来后,就是
0,1,2,3,……n-2,n-1
如果我们要数到第m个数字,那么m的位置就在(m-1)%n,因为m可能会大于n,所以需要取余数,且是从0开始数的,所以要减1。所以我们排列顺序可以写成
0,1,2,3,……(m-1)%n,m%n,(m+1)%n……n-2,n-1,n
这时候我们要拿掉(m-1)%n这个位置,刚刚分析过因为是从0开始报的,第m个对应的是(m-1)%n的位置。然后就剩下n-1个数字了,可以写成
0,1,2,3,……m%n,(m+1)%n……n-2,n-1,n
接下来如果我们要得到下一个拿掉的数字,我们得从m%n的位置开始数,排列顺序就是
m%n,(m+1)%n……n-2,n-1,n,0,1,2,3……m-2
为了方便计算,我们把n-1个数字重新编号,并作对比

n-1个数m%n(m+1)%n(m+2)%n(m+3)%nm-3m-2
重新编号0123n-3n-2

我们惊奇的发现,重新编号后的数字和编号前的数字正好可以用一个函数式表示,我们记编号后的为x,那么重新编号前的数字可以用f(x)=(m+x)%n这个函数来表达.

此时,我们可以把重新编号后的n-1个数字当作一个新的子问题来求解决,假设我们得到的最终结果是X,那么n个数字的最终结果不就是**(m+X)%n**么.

有的人可能不理解为什么n-1个数字得到结果是X,n个数字得到的结果就一定是(m+X)%n呢.这里需要解释一下,上面两行所有的数字标记的都是位置,重新编号后位置从0,1,2开始计数了,而它原本对应的位置其实是m%n,(m+1)%n,(m+2)%n.
我们假设重新编号后的数列是一个单独的题目,题目为求0,1,2,3……n-3,n-2的最终剩余的数.重新编号前的数列也是一个单独的题目,题目为求m%n,(m+1)%n,(m+2)%n,(m+3)%n,……m-3,m-2的最终剩余的数字.这两个题目现在是不是都是从第一个位置开始数的,都要数m个,那最终所得的结果的位置是不是一定是相同的.如果得出了求0,1,2,3……n-3,n-2的最终剩余的数的位置,那么是不是就得出了求m%n,(m+1)%n,(m+2)%n,(m+3)%n,……m-3,m-2的最终剩余的数字的位置.求m%n,(m+1)%n,(m+2)%n,(m+3)%n,……m-3,m-2的最终剩余的数的位置得出了结果,那求n个数最终剩余数字的位置不也是这个结果吗?,因为你求n数最终剩余数字的位置第二轮不就是求m%n,(m+1)%n,(m+2)%n,(m+3)%n,……m-3,m-2的最终剩余的数吗?有一点绕,写的这么详细,相信大家一定可以理解.

我们可以用一个简单的例子来解释一下:
比如n=3,m=2.
那么排列就是0,1,2.很明显,移除的先是1,然后排列就是剩下0,2了.我们先不继续算下去了.
我们再看一下另一个更简单的例子
比如n=2,m=2
那么排列就是0,1很明显,移除的先是1,最后就剩0了,也就是第0个位置.

我们再回去算n=3,m=2的情况,刚刚算到了排列剩0,2的情况,因为要从移除的下一个位置开始数,所以排列就变成了2,0,数2个,那么0就被移除了.最后剩下的就是2,也就是第0个位置.
从这里我们很容易看出,n=3的子问题,0,2最后得出的位置和n=2的问题,0,1,最后求得的位置一定是一样的.因为这时候两个问题的n都是2,m都是2,所以最后的位置一定是一样的.所以我们只要利用(m+x)%n=(2+0)%3=2,就得出了n=3的时候剩余的位置在2这个数字的位置了.
也就是说利用(m+x)%n这个公式我们就是能找到父问题解所在的位置.

说了这么多,其实就是为了说明(m+x)%n这个公式的由来和原因.有了这个公式我们就很容易想到了递推公式,也就是

{ f ( 1 ) = 0 f [ i ] = ( m + f [ i − 1 ] ) m o d    i ( i > 1 ) \begin{cases} f(1)=0\\ f[i]=(m+f[i-1])\mod{i} (i>1) \end{cases} {f(1)=0f[i]=(m+f[i1])modi(i>1

有了递推公式,那么我们就很容易想到利用递归或者迭代去完成此问题的算法
下面直接上代码:

递归:

    public int LastRemaining_Solution (int n, int m) {
            if(n == 0 || m==0){
                return -1;
            }
            return findChildResult(n,m);
    }
 
    public int findChildResult(int n,int m){
            if(n==1){
                return 0;
            }
            int childResult = findChildResult(n-1,m);
            return (m+childResult)%n;
    }

迭代:

    public int LastRemaining_Solution (int n, int m) {
            if(n==0 || m==0){
                return -1;
            }
            if(n == 1){
                return 0;
            }
            int res = 0;
            for(int i=2;i<=n;i++){
                res = (m+res)%i;
            }
            return res;
    }

从代码块中我们能看出来,利用公式的算法,它的时间复杂度为o(n),空间复杂度为o(1).

对于第一种模拟整个游戏的算法,这里就不再过度解释了,网上有一大堆讲解相信大家都能看懂.这里贴上我自己在牛客通过的代码块供大家参考.

    public int LastRemaining_Solution (int n, int m) {
    	//初始化模拟的列表
        List<Integer> list = new ArrayList();
        for(int i=0;i<n;i++){
            list.add(i);
        }
        int start = 0;
        //直到剩最后一个才返回
        while(list.size() !=1){
            int size = list.size();
            //计算每次要移除的坐标,就是
            //上一次的起始坐标start+这次数的新坐标m%size
            //再将这两个和取余%size,不然可能会超出size范围
            //最终减去1就是要移除的位置
            start = (start+m%size)%size-1;
            //需要考虑如果二者之和size如果相同的话,那么余数是0的特殊情况
            start = start<0?size-1:start;
            //移除后进行下一个循环
            list.remove(start);
        }
        //返回剩下的最后一个即可
        return list.get(0);
    }
  • 22
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值