每日一题.每日一练 .15圆圈中最后剩下的数字(为什么没有14,因为几何比约瑟夫环难多了,现在还没明白)(“铡刀思想”,或者叫咔擦思想)

这篇文章是我通过灵感所写的是一种全新的理解约瑟夫环的方法,希望能帮到一些还没有理解的朋友。所以我会尽力详细的讲述这篇文章。

面试题62. 圆圈中最后剩下的数字
0,1,n-1这n个数字排成一个圆圈,从数字0开始,每次从这个圆圈里删除第m个数字。求出这个圆圈里剩下的最后一个数字。

例如,0、1、2、3、4这5个数字组成一个圆圈,从数字0开始每次删除第3个数字,则删除的前4个数字依次是2、0、4、1,因此最后剩下的数字是3。

示例 1:

输入: n = 5, m = 3
输出: 3

示例 2:

输入: n = 10, m = 17
输出: 2

在这前我们先来了解一下约瑟夫环问题:

17世纪的法国数学家加斯帕在《数目的游戏问题》中讲了这样一个故事:15个教徒和15 个非教徒在深海上遇险,必须将一半的人投入海中,其余的人才能幸免于难,于是想了一个办法:30个人围成一圆圈,从第一个人开始依次报数,每数到第九个人就将他扔入大海,如此循环进行直到仅余15个人为止。问怎样排法,才能使每次投入大海的都是非教徒。(还有另一个版本但是他太长了==)

如果死去的人灵魂还能占着空位,这题会变的非常简单,如果我们每次扔进海里一个就重新从开头数数,这题也会非常简单,然而并不能,所以约瑟夫环的问题就有两个:
1:我们没有固定的环长,意味着我们数着的数肯定不成倍数增加(也就是没有什么明显的规律):
2:没有固定的开始地点,这个人被扔海里接着数后面的,所以我们之后的会受到环长的影响。

感谢在我尝试理解约瑟夫环的过程中,一位大佬的文章里提到了

每当一个人被推进海里时,所有的索引值会前进m,

也就是我们每次数到就要杀人的那个数,所有的索引值都要前进m,为什么,我们不是死了一个人吗,为什么索引值前进了那么多呢”但是,随着思考我突然冒出了一种新的思想,这种思想我称为“铡刀思想”(淦))

这种思想的根本在于,我们通常会把数数的过程看做指针的移动,当指针移动到符合条件时,我们删掉(杀掉)这个人,接着让指针往下移动,这是一般的想法,而"铡刀思想"并非如此,在“铡刀思想”里移动的不是“”铡刀”(指针),而是人(数组)

这种思想是这样,由于我们最后肯定会剩下一个数据,这个数据的索引值必定是0,所以我们在数组的开始设置一个铡刀,你可以认为是这样(这里我们用实例1的数据)

“”“”“”“”“”“”“”“”铡刀 0 , 1, 2 , 3 , 4(引号是为了对齐和下面的情况对齐,抱歉我是个菜鸟)

然后我们对着人(数组)喊,过来吧,由于n=3,所以每当过来到第三个人的时候,我们就咔嚓一刀

0 ,1 ,2(铡刀到3,咔擦!),3,4

2阵亡了,由于这是一个环,我们跟 0 和 1 说,你回去接着排队吧,0,1:诶好嘞

“”“”“”“”“”“”“”“”铡刀 3 ,4,0,1

然后我们对这个过程进行周而复始的循环,直到最后只剩一个人站在铡刀前,也就是索引值为0的位置

“”“”“”“”“”“”“”“”“铡刀 3

恭喜3获得了大逃杀的胜利(雾)

也就是说,“铡刀思想”的根本在于删除点一直在数组的最前端,而我们是通过让人走过铡刀,回到队尾来实行计数,每有走过铡刀的过程我们就记+1,如果加完变成n我们就咔擦一刀,然后归零。

那么现在,我们将所有过程反推,最后一个元素(在这个事例中是3)是怎么来到铡刀面前呢?(3的索引值变成0),而“铡刀思想”的妙处就在于,他给出了每一次在咔擦后索引值的变化,”由于每次数组都是整体移动,我们可以认为每个元素在两次咔嚓间都进行了m单位的左位移”(重点)

我们回到上一个过程,另一个元素在和3斗智斗勇的时候,他们两个经历了轮流走到后面的过程,我们就叫他甲好了,那么喊号过程是这样的

铡刀计数为零

铡刀, 甲, 3

铡刀计数为1,甲通过

铡刀 ,3 ,甲

铡刀计数为2,3通过

铡刀 ,甲 ,3

铡刀计数在甲走的时候变成了3 咔嚓一刀甲没了

铡刀 3 (你赢了)

我们看一下这个移动过程就会发现,尽管3进行了左m单位的位移,但是这里面出现了跑圈现象,3并不是没有经过铡刀,而是经过铡刀成功跑了一圈,于是我们将这个过程反推,把左位移用右位移倒放回去,注意此时3的索引值是零,我们假设一秒动一次

“”“”“”“”“”0, 1(索引值)
现在:3 ,甲(如果没死的话)
前1秒:甲,3
前2秒:3,甲
前3秒:甲,3

我们可以看到,3在每往右移两个单位就要回到原处,也就是说,3实际上发生的位移是3%2=1,跑一圈甚至几圈不会对3造成实际的位移。于是我们的上一时刻的索引值便有如下推导式

上一时刻索引值=(当前索引值+右移位移距离)%上一时刻圈长

我们代入数据验证一下

(0+3)%2=1

由此我们的两个问题就都解决了:
一方面,我们固定在开始删除人。
另一方面,我们把圈长的变化精确到了每个时刻。

于是我们可以利用递推不断推胜利者在上个时刻的位置,最后便可得到得到在最初时刻的时候,也就是圈长为n的时候,胜利者的索引值

好,说了这么多,我们来总结一下过程

整个过程在执行这三部分
队列整体移动
咔嚓(在开始删人)
圈长减小

移动的值是个定值这点是思想的核心。因此删掉的人是否占位只会影响圈长的大小,而不影响整体移动的量

于是代码如下:

class Solution:
    def lastRemaining(self, n: int, m: int) -> int:
        f = 0 #最后胜利者索引值为0
        for i in range(2, n + 1):     #对于每个上一时刻时刻的圈长的逆推,变化从2到n
            f = (m + f) % i     #m+f是将这时刻的索引值推回上一时刻,%i是不算跑圈后得到实际更新的索引值
        return f        #在最后的索引值,即全场为n,刚开始是胜利者的索引

而这题我们把n想成一个数组,会发现n的索引值和留下来的数据是相等的,
所以输出索引值等于输出数值,其他情况做些加减便好

咔嚓!

  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值