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