问题的提出
有n个人围成一个圈,按顺时针编号为1到n。现在从编号为1的人开始报1,下一个人报2,如此类推,报到m的人就退出,接着下一个人继续从1开始报。问最后一个剩下的人是谁。
解法1
可以通过链表的形式来模拟这个过程,时间复杂度为 O ( n m ) O(nm) O(nm)。
解法2
我们把每个人的编号都-1,也就是编号变为了从0到n-1,显然这两个问题是等价的。
第一个出去的人的编号肯定是
(
m
−
1
)
 
m
o
d
 
n
(m-1)\bmod n
(m−1)modn,然后接着从
k
=
m
 
m
o
d
 
n
k=m\bmod n
k=mmodn开始报。
那么剩下的人就是
k
,
k
+
1
,
.
.
.
,
n
−
1
,
0
,
.
.
.
,
k
−
3
,
k
−
2
k,k+1,...,n-1,0,...,k-3,k-2
k,k+1,...,n−1,0,...,k−3,k−2。
考虑对这些人的编号做一下变换:
k
变
为
0
k变为0
k变为0
k
+
1
变
为
1
k+1变为1
k+1变为1
…
n
−
1
变
为
n
−
k
−
1
n-1变为n-k-1
n−1变为n−k−1
0
变
为
n
−
k
0变为n-k
0变为n−k
…
k
−
2
变
为
n
−
2
k-2变为n-2
k−2变为n−2
k
−
1
变
为
n
−
1
k-1变为n-1
k−1变为n−1
不难发现其实就是把
i
i
i变为
i
−
k
i-k
i−k。
那么当我们把第
k
−
1
k-1
k−1个人去掉后,剩下的游戏就等价于一个大小为
n
−
1
n-1
n−1的约瑟夫环。设其最后答案为
x
′
x'
x′,大小为n的约瑟夫环的答案为
x
x
x,不难发现有
x
=
(
x
′
+
m
)
x=(x'+m)%n
x=(x′+m)。
通过这种思路不断地递归下去不难发现有如下递推式:
设
f
[
n
]
f[n]
f[n]表示大小为n的约瑟夫环每次走
m
m
m步的最终答案。
f
[
1
]
=
0
f[1]=0
f[1]=0
f
[
n
]
=
(
f
[
n
−
1
]
+
m
)
 
m
o
d
 
n
f[n]=(f[n-1]+m)\bmod n
f[n]=(f[n−1]+m)modn
至此我们得到了一个比算法1要优秀的
O
(
n
)
O(n)
O(n)解法。
小拓展
当n很大而m很小的时候,比如说
n
<
=
1
0
9
,
m
<
=
1
0
6
n<=10^9,m<=10^6
n<=109,m<=106,这时就有一些比较特殊的解法。
当模数逐渐变大的时候,我们发现可能一次可以跳很多步且在这期间实际并不用取模,这时就可以一次性把这些位置都处理掉,从而大大的节约时间。
设当前模数为i,每一步跳k个位置,我们一次性跳t步。
不难得到
w
+
t
∗
k
<
i
+
t
−
1
w+t*k<i+t-1
w+t∗k<i+t−1化简后有
t
<
i
−
w
−
1
k
−
1
t<\frac{i-w-1}{k-1}
t<k−1i−w−1。
那就是说这t步我们是可以一次性处理掉的。
代码
int solve(int n,int k)
{
int i=2,w=0;
while (i<=n)
{
int t=(i-w-1)/(k-1);
if ((i-w-1)%(k-1)==0) t--;
if (i+t>n) {w+=(n-i+1)*k;break;}
w+=t*k;i+=t;
w=(w+k)%i;i++;
}
return w;
}
在维基百科上面还有一种更优秀的解法:
至于如何证明,这个坑还是留着以后再填吧。据说 具体数学 里面有关于这条式子的证明。