剑指Offer62 -- 约瑟夫环
1. 题目描述
圆圈中最后剩下的数字
2. 约瑟夫环
人们站在一个等待被处决的圈子里。 计数从圆圈中的指定点开始,并沿指定方向围绕圆圈进行。 在跳过指定数量的人之后,处刑下一个人。 对剩下的人重复该过程,从下一个人开始,朝同一方向跳过相同数量的人,直到只剩下一个人,并被释放。
3. 思路
下面主要是对参考题解的补充说明。
现在正式开始😊
我们现在假设有一个长度为 n n n 的数组 a a a,数组下标从 0 0 0 开始,每数 m m m 个数就删除当前位置的元素,直到只剩下一个元素。第一次删除 a [ k ] a[k] a[k], k = ( m − 1 ) % n k=(m-1)\%n k=(m−1)%n(考虑 m > n m>n m>n的情况),那么删除 a [ k ] a[k] a[k],之后,原数组就变成了:
a [ k + 1 ] , a [ k + 2 ] , . . . , a [ n − 1 ] , a [ 0 ] , a [ 1 ] , . . . a [ k − 1 ] a[k + 1], a[k + 2], ... , a[n - 1], a[0], a[1], ... a[k - 1] a[k+1],a[k+2],...,a[n−1],a[0],a[1],...a[k−1](到了头就重新从下标 0 0 0 处开始)
仔细看的话,这里,我们没有标明 a [ k ] a[k] a[k],说明它的确被删除了。
哎,你可能会问,怎么可能真的把它删除了啊(况且代码中确实没有“删除”动作),我们在模拟的时候,对于“删除”的元素,都是跳过,既然都跳过了,说明它的的确确真真实实的存在啊!
错了!此删除非彼删除!先别急!
现在,我们删除了一个元素,新数组就是上面的样子,我们需要删除第二个元素,但是,他的位置不是 k = ( m − 1 ) % n k=(m-1)\%n k=(m−1)%n 了,而是 k = ( m − 1 ) % ( n − 1 ) k=(m-1)\%(n-1) k=(m−1)%(n−1)。
删除位置的变化非常非常关键!我们通过缩减数组的大小,使得我们通过忽略 a [ k ] a[k] a[k] 以达到删除 a [ k ] a[k] a[k] 的目的,因为 a [ k ] a[k] a[k] 如果存在的话,它就在 a [ k − 1 ] a[k-1] a[k−1] 的后面,也就是长度为 n n n 的数组的最后一个元素,但是现在数组长度为 n − 1 n-1 n−1 了,所以 a [ k ] a[k] a[k] 不就相当于被删除了吗?
往下同理,当我们删除了 c n t cnt cnt 个元素之后, k = ( m − 1 ) % ( n − c n t ) k=(m-1)\%(n-cnt) k=(m−1)%(n−cnt)。
好了,我们已经解释完了,通过什么样的方法来实现模拟过程中跳过被删除的元素,就是把它放到数组末尾,然后删除。
但是!问题来了!虽然说,我们可以找到
k
k
k 的位置,但是你没办法把数组
a
a
a 转换为下面形式:
删除一个元素之后的新数组:
a
[
k
+
1
]
,
a
[
k
+
2
]
,
.
.
.
,
a
[
n
−
1
]
,
a
[
0
]
,
a
[
1
]
,
.
.
.
a
[
k
−
1
]
a[k + 1], a[k + 2], ... , a[n - 1], a[0], a[1], ... a[k - 1]
a[k+1],a[k+2],...,a[n−1],a[0],a[1],...a[k−1]
因为,
a
[
0
]
a[0]
a[0] 就是
a
[
0
]
a[0]
a[0],
a
[
k
+
1
]
a[k+1]
a[k+1] 就是
a
[
k
+
1
]
a[k+1]
a[k+1],你说 原数组中的
a
[
k
+
1
]
a[k+1]
a[k+1] 相当于 新数组的
a
[
0
]
a[0]
a[0],他就是新数组中的
a
[
0
]
a[0]
a[0] 啊?
确实,我们假象,删除一个元素后,新数组的头是原数组的 a [ k + 1 ] a[k+1] a[k+1],但是事实是残酷的,数组的头仍然是原数组的 a [ 0 ] a[0] a[0]
那么,有问题就解决问题,怎么办呢?最简单的办法,用一个 f o r for for 循环移动元素,让 a [ 0 ] a[0] a[0] 真的等于 a [ k + 1 ] a[k+1] a[k+1]
但这也太笨了! o f f e r offer offer 是留给不走寻常路之人的!
我们可以通过映射大法,虚假的让原数组的 a [ k + 1 ] a[k+1] a[k+1] 等于新数组的 a [ 0 ] a[0] a[0],因为新数组的理论下标是连续的(从 0 0 0 到 n − 2 n-2 n−2)
我们指望被映射的下标也是连续的(取模意义下): k + 1 , k + 2 , . . . n − 1 , 0 , 1 , . . . k − 1 k+1, k+2, ... n-1, 0, 1, ...k-1 k+1,k+2,...n−1,0,1,...k−1
既然两个数组都是连续的,我们当然可以将我们希望的数组下标(从 k + 1 k+1 k+1 开始的)映射到实际的数组下标(从 0 0 0 开始的)了!
接下来映射部分参考题解就好了!
总之,思路很简单!只需要搞清楚两个问题:
- 如果模拟删除行为
- 如何将被删除元素的下一个位置开始的元素作为数组的第一个元素
另外,你可能会问为啥我们必须要映射呢?我们设置一个变量
s
t
a
r
t
I
n
d
e
x
=
k
+
1
startIndex=k+1
startIndex=k+1,然后让他递增(取模状态下)不久好了?这样不也能实现映射的行为吗?
也就与我们递归的意义有关了。
我们的递归函数
l
a
s
t
R
e
m
a
i
n
i
n
g
(
i
n
t
n
,
i
n
t
m
)
lastRemaining(int n, int m)
lastRemaining(intn,intm) 表示,删除从
0
0
0 到
n
−
1
n-1
n−1 一共
n
n
n 个元素中的
m
m
m 个数,最后剩下的数。
他的第一个数就是
0
0
0,最后一个数就是
n
−
1
n-1
n−1。。。
4. 代码
class Solution {
public:
int lastRemaining(int n, int m) {
if(n == 1) return 0;
return (lastRemaining(n - 1, m) + m) % n;
}
};
5. 参考
数学推理–映射
6. 其它题目
求出队序列