题目描述:
原题为《剑指offer》圆圈中最后一个数,这里是简化描述
约瑟夫问题是一个非常著名的趣题,即由n个人坐成一圈,按顺时针由0开始给他们编号。然后由第一个人开始报数,数到m-1的人出局。现在需要求的是最后一个出局的人的编号。
给定两个int n和m,代表游戏的人数。请返回最后一个出局的人的编号。保证n和m小于等于1000。
这篇文章分析的情况是编号为0~n-1,报数为0~m-1。
数组模拟
看见这个问题第一反应就是画图进行模拟,转换为代码即使用一个循环链表来解决,这里简化一下使用数组进行模拟。模拟需要注意以下事项。
- 为了保证围成一个圈,每当指针到数组结尾时,就把指针重置到开头。
- 数组中删除元素比较麻烦,那么将元素记为-1代表删除,遇见了就直接跳过
代码如下:
public class Solution {
public int LastRemaining_Solution(int n, int m) {
if(m == 0 || n == 0)return -1;
int a[] = new int[n];
//i作为数组指针,由于循环开始i需要加1,所以初始值为-1
int i = -1;
//数组中还剩下的元素个数
int count = n;
//记录每次喊的数字,从0开始报数,所以初始值应该为-1
int step = -1;
//循环报数过程,直到只剩下一个数
while(count > 0){
i++;
//当i指向数组末尾时,i变为0
if(i == n){
i = 0;
}
//如果遇见已经出列的数,跳过后面的步骤
if(a[i] == -1)continue;
//喊的数字加一
step++;
//当喊到规定数字时,剔除该元素
if(step == m - 1){
count--;//元素个数减少
step = -1;//记录数字还原为-1
a[i] = -1;//剔除的元素记为-1
}
}
return i;
}
}
数学推论
队伍总数为n(下标为0~n-1),报数为m时,模拟队列删除过程。那么第一个剔除的数字下标是(m - 1) % n
,设为k - 1
,即k = m % n
此时按照报数顺序,队列下标为 k,k+1…n-1,0,1,…,k-2
,该队列最后剩下的数字下标也应该可以用关于m和n的函数来表示,记为f'(n-1,m)
。
假设初始队列的结果为f(n,m)
,由于初始队列和删除第一个数字后的队列最终剩下的数字下标一定相同,(这是模拟删除的一个步骤),所以f(n,m) = f'(n-1,m)
。
下标可以做一个转换,
k --> 0
k+1 --> 1
…
k-2 --> n-2
原队列变成了0 ~ n-2
设映射为p(x)
, p(x) = (x - k) % n
,逆映射(即变回原来)为p'(x) = (x + k) % n
我们可以发现,变化之后,这个就变成了编号为0 ~ n-2报数的问题,解可以记为f(n-1,m)
,现在假设0 ~ n-2的结果下标为x,即x = f(n-1,m)
,那么这个 x 在k,k+1…n-1,0,1,…,k-2
中对应的位置(即此队列的结果)可以根据上面的逆映射公式得到,为f'(n-1,m) = (x + k) % n
,综合上面所有公式
x = f(n-1,m)
k = m % n
可得f(n,m) = f'(n-1,m) = [f(n-1,m) + m] % n
代码为:
public class Solution {
public int fun(int n, int m){
if(n == 1)return 0;
return (fun(n-1,m)+m)%n;
}
public int LastRemaining_Solution(int n, int m) {
if(m == 0 || n == 0)return -1;
return fun(n,m);
}
}