这篇文章是我通过灵感所写的是一种全新的理解约瑟夫环的方法,希望能帮到一些还没有理解的朋友。所以我会尽力详细的讲述这篇文章。
面试题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的索引值和留下来的数据是相等的,
所以输出索引值等于输出数值,其他情况做些加减便好
咔嚓!