约瑟夫环递推公式解析
在力扣做了面试题第62题,是一个约瑟夫环问题,作为一个转行java的,并不知道有约瑟夫环这种经典算法题,于是提交了java暴力的解法。
class Solution {
public int lastRemaining(int n, int m) {
List<Integer> ns = new ArrayList<Integer>();
for(int i = 0;i<n;i++){
ns.add(i);
}
int point = 0;
for(int i = 0;i<n-1;i++){
int mp = m+point;
point = (mp-1)%ns.size();
ns.remove(point);
}
return ns.get(0);
}
}
提交代码发现成绩令人汗颜,于是查看了别人的解法,才发现有递推公式的数学方法,不过较难理解,现在记录一片博客总结一下。
public static int lastRemaining(int n, int m) {
int flag = 0;
for(int i=2;i <=n;i++){
flag = (flag + m)%i;
}
return flag;
}
由代码可以看到核心递推公式为 % i ,其中总人数为n ,第n-1轮存活人的当前数组坐标为0 ,原数组坐标为
。
暴力解法的逻辑,是按照题意,每轮杀了谁,就让数组中这个人移除数组,直到最后一轮得出这个人的数组角标。递推公式的逻辑则相反,求的是最后存活下来的这个人在原数组中的位置。
1.以总5人 每次第3个人为例
轮数\位置 | 0 | 1 | 2 | 3 | 4 |
---|---|---|---|---|---|
0 | 0 | 1 | 2 | 3 | 4 |
1 | 3 | 4 | 0 | 1 | |
2 | 1 | 3 | 4 | ||
3 | 1 | 3 | |||
4 | 3 |
可以看到第一轮之后,原数组下标为3的元素的当前数组下标为0,第二轮要被杀掉的是当前数组下标为2原数组下标为0的元素。
可以看出每次,杀掉一个人之后,所有元素的当前下标-3,即左移三位,减3后小于0的按循环补位。
2.由以上思路可以反向思考,便可以得到递推公式。已知轮数是有总人数决定的,所以设最后存活的人在原数组的坐标为Y,可知第4轮为
轮数\位置 | 0 | 1 | 2 | 3 | 4 |
---|---|---|---|---|---|
4 | y |
可以知道y这个人的第4轮后当前数组坐标必为f(4)=0,逆推第3轮后,剩下的人的原数组坐标为x,y,此时可以逆推y的当前数组坐标为第4轮后的当前数组坐标+3即:
轮数\位置 | 0 | 1 | 2 | 3 | 4 |
---|---|---|---|---|---|
3 | x | y | x | y | |
4 | y |
所以第三轮y的当前数组坐标为 f(4)+3 = 3 ,又因为第3轮后最多只有2个元素,所以按照循环补齐f(3)=(f(3)+3)% 2 = 1 ,所以第3轮后y的当前数组坐标为1
同理第二轮 为 坐标为f(2) = f(2)+3 = 4
其中第二轮后最多只有3个元素,所以第二轮f(2) = (f(3)+3) % 3 = 1,所以第2轮后y所在当前数组坐标为1
同理,接下来就是第一轮后是y所在的数组位置
(黄色y为模拟右移3位的位置,黑色y为第1轮后y的实际位置,红色字母为补充位置方便展示循环补位)
f(1) = (f(2)+3) % 4 = 0
最后一步
f(0) = (f(1)+3)%5 = 3,此时的当前数组位置就是原数组位置,就可以知道最后存活下来的人在原数组的位置在哪里了。
总结递推公式为 % i 其中总人数为n ,第n-1轮存活人的当前数组坐标为0 ,原数组坐标为
,
i = n-完成轮数 例如:f(3) = (f(4) + 3) % 2
于是有了下面的代码来求存活下来的元素的原数组下标,i从2开始的,因为i为1时当前坐标必为0。
public static int lastRemaining(int n, int m) {
int flag = 0;
for(int i=2;i <=n;i++){
flag = (flag + m)%i;
}
return flag;
}