约瑟夫环问题:
约瑟夫问题是个著名的数学问题,N个人围成一圈,从第一个人开始数,杀掉第M个人。然后从第M+1个人再开始数,数到第M个人再杀掉,然后再从第M+1个人开始数,数到第M个人再次杀掉,反复进行,直到这圈人只剩下最后一个人。游戏结束。
那么问题来了,如何快速的判断最后留下的那个人是谁呢?这就是约瑟夫环要研究的问题。
我们以数组为例研究,假设 N=8 , M=3, F(8 , 3) = 7 (这个7是 7这个人的序号)
1 , 2 , 3 , 4 , 5 , 6 , 7, 8
第一个回合:杀掉第三个人数组变成 F(7 , 3) = 4 (这个1是 7这个人的序号)
4 , 5 , 6 , 7, 8, 1, 2
第二个回合:杀掉第三个人数组变成 F(6 , 3) = 1 (这个5是 7这个人的序号)
7, 8, 1, 2, 4, 5
第三个回合:杀掉第三个人数组变成 F(5 , 3) = 4 在 (这个2是 7这个人的序号)
2, 4, 5, 7, 8
第四个回合:杀掉第三个人数组变成 F(4 , 3) = 1 (这个4是 7这个人的序号)
7, 8, 2, 4
第五个回合:杀掉第三个人数组变成 F(3 , 3) = 2 (这个1是 7这个人的序号)
4, 7, 8
第六个回合:杀掉第三个人数组变成(因为是个环,所以数完7是第二个时候,就重新回到4数作为第三个,因此下一个是删掉的4) F(2 , 3) = 2 (这个1是 7这个人的序号)
4, 7
第七个回合:杀掉第三个人数组变成 F(1 , 3) = 1 (这个1是 7这个人的序号)
7
我们可以看到最后剩下的是7号,暴力的办法是模拟整个过程,然后得到最终留下的人,但是在程序里这样做会导致时间复杂度非常高,具体的可以看我的下面python的暴力程序。
约瑟夫环的方法类似于找个规律,让我们可以快速的确定那个人可以留到最后,我们可以看到
F(8 , 3) = 7 , F(7 , 3) = 4 , F(8 , 3) = F(7 , 3) + 3,因为每次杀掉第三个人之后,前两位也位移到数组最后面了,所以7号人的位置会左移三位,由7移动到4,相当于所在的下标 -3 。
这就是我们找到的规律,如果我们使用(倒推思想)让最后的一个函数 F(1 , 3)不停的+3 ,是不是就可以很快找到活到最后的人。
那么我们来这样做,由上面可知 F(1 , 3) = 1, 那么按照我们找到的规律可得
F(2 , 3) = F(1 , 3) + 3 = 1 + 3 = 4
但是我们发现F(2 , 3)的列表长度一共才2,序号4从何谈起呢?因此我们需要对我们每次的+3操作再加上一个操作,即对当前列表的长度进行取余。等式变成如下
F(2 , 3) = (F(1 , 3) + 3)%2 = (1 + 3 )% 2 = 4%2 = 2
因为每次移动到最后之后如果数组没有了,但是还没移动完,就回到第一个重新移动,这就是取余的原因,因为我们这个是个围成圈的嘛!
F(3 , 3) = (F(2 , 3) + 3)% 3 = (2 + 3 )% 3 = 5%3 = 2
F(4 , 3) = (F(3 , 3) + 3)% 4 = (2 + 3 )% 4 = 5%4 = 1
F(5 , 3) = (F(4 , 3) + 3)% 5 = (1 + 3 )% 5 = 5%5= 4
F(6 , 3) = (F(5 , 3) + 3)% 6 = (4 + 3 )% 6 = 7%6 = 1
F(7 , 3) = (F(6 , 3) + 3)% 7 = (1 + 3 )% 7 = 4%7 = 4
F(8 , 3) = (F(7 , 3) + 3)% 8 = (4 + 3 )% 8 = 7%8 = 7
我想到这里,这个规律我们已经看的很清楚了,即
F(N , M) = (F(N-1 , M) + M)%N
没错,这个公式就是我们的约瑟夫环的递推公司,用这个方法求解下面的剑指offer的习题即可
剑指 Offer 62. 圆圈中最后剩下的数字
难度简单610收藏分享切换为英文接收动态反馈
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
限制:
1 <= n <= 10^5
1 <= m <= 10^6
通过次数148,821提交次数226,172
第一种暴力法:
构造队列,deque,rotate函数可以旋转队列,使它真的变成圈
每次计算余数,然后进行队列将m之前的数放到队列最后,然后弹出最左边的数
时间有点长
Python代码:
class Solution: def lastRemaining(self, n: int, m: int) -> int: ans = collections.deque(list(range(n))) while len(ans)!=1: ans.rotate(-(m%len(ans))+1)#队列左移 ans.popleft() #移动完之后,弹出 return ans[0]
第二种约瑟夫环法-递归思想代码:
Python代码:
class Solution: def lastRemaining(self, n: int, m: int) -> int: f = 0 for i in range(2 , n+1): f = (f+m)%i return f
C++代码:
class Solution { public: int lastRemaining(int n, int m) { int f=0; for(int i=2; i<n+1; i++) f = (f+m)%i; return f; } };