剑指 Offer 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
限制:
1 <= n <= 10^5
1 <= m <= 10^6
解题思路
- 使用列表模拟叫号、删除数字的过程
如n=5, m=3
下表下划线元素,为每次删除的元素,一次删除一个。
原始数据 | 0 | 1 | 2 | 3 | 4 |
---|---|---|---|---|---|
第1次删除 | 0 | 1 | 2 | 3 | 4 |
剩余元素位置左移 | 0 | 1 | 3 | 4 | |
第2次删除 | 0 | 1 | 3 | 4 | |
剩余元素位置左移 | 1 | 3 | 4 | ||
第3次删除 | 1 | 3 | 4 | ||
剩余元素位置左移 | 1 | 3 | |||
第4次删除 | 1 | 3 | |||
剩余元素位置左移 | 3 |
注意到第i次删除操作,删除元素位置都是 (i + m -1 ) % n, n是当前元素个数(表长);
如果使用java编程,可以使用ArrayList自动维护 “剩余元素位置左移” 的操作。
伪码
lastRemaining(N, m)
// 新建list
let list[0..n-1] = 0,1,2,...,n-1
i = 0
while list.size > 1
i = (i + m - 1) % list.size
list.remove( i )
return list[0]
- 考虑第k次删除,和第k-1次删除之间的数学关系
位置 | 0 | 1 | 2 | ... | m-2 | m-1 | m | m+1 | ... | n-1 |
---|---|---|---|---|---|---|---|---|---|---|
第k-1次操作后 | a0 | a1 | a2 | ... | a(m-2) | a(m-1) | a(m) | a(m+1) | ... | a(n-1) |
第k次删除 | a0 | a1 | a2 | ... | a(m-2) | a(m-1) | a(m) | a(m+1) | ... | a(n-1) |
左移剩余元素 | a(m) | a(m+1) | ... | a(n-1) | a0 | a1 | a2 | ... | a(m-2) |
第k删除的元素的位置为 (m-1) % n, n = N-k,其中,N是元素初始个数,n是当前剩余元素个数(第k-1次操作后)。
相当于在第k-1次删除后的基础上,a[m..n-1]元素都往左移m个位置,a[0..m-2]右移 n-1 - m + 1 = n - m个元素。
记f(i, k),表示初始位置为i的元素,经历k次操作后的位置(前提是前k-1次,该元素都未被删除),有
\[f(i,k)=
\begin{cases}
f(i, k-1) - m& \text{f(i, k-1) > m-1}\\
f(i, k-1) + n - m& \text{f(i, k-1) < m-1}
\end{cases}\]
上式可以综合到一起,
\[f(i,k)=
[f(i, k-1) - m] \% n], 其中, f(i, k-1) ≠ m-1,n = N - (k-1)
\]
已知什么,要求什么?
已知的是第k次删除元素后,只剩1个元素位置为0,即为所求元素。
也就是说,已知f(k=N-1) = 0,要求f(0),而a[f(0)]即为所求元素值。把f(k-1)写成f(k)的表达式。
推导过程,
\[\begin{aligned}
&∵f(k) = [f(k-1)-m] \% n, n = N - (k-1)\\
&∴f(k-1) - m = xn + f(k), x∈Z\\
&∴f(k-1) = xn + f(k) + m, 式子两边对n求余\\
&f(k-1) \% n = [f(k) + m] \% n, 而f(k-1)就是元素在长度为n(n=N-(k-1))时的位置,\\
&也就是说, f(k-1)∈[0, n) \\
&故f(k-1) = f(k-1) \% n = [f(k) + m] \% n
\end{aligned}
\]
问题转化为
\[\begin{aligned}
f(k=N-1) &= 0 \\
f(k-1) &= [f(k) + m] \% 2 \\
f(k-2) &= [f(k-1) + m] \% 3 \\
...&\\
f(1) &= [f(0) + m] \% (N-1) \\
f(0) &= [f(1) + m] \% (N)
\end{aligned}
\]
这样,就可以利用程序进行循环/递归求解了。
伪代码
lastRemaining(N, m)
k = N - 1
pos = 0
while k > 0
pos= (pos + m) % (N - k + 1)
k = k-1
return pos