算法 约瑟夫环问题
@author:Jingdai
@date:2021.06.02
约瑟夫环问题是一个非常经典的问题,希望看完这篇文章可以让你学会解决这个问题。
题目描述
n 个人围成一圈,从第一个开始报数,第 m 个将被杀掉,然后从 m 后面那个人开始重新报数,直到最后剩下一个人。求最后剩下的那个人的下标。
思路
常规的思路可以利用链表或者数组进行模拟删除过程,但是当 n 和 m 比较大的时候,这种方法就会超时,这里记录一种递推的方法。
以 n = 5,m = 3 为例,我们给每个人标记为 A、B、C、D、E,防止数字和下标弄混,删除的过程如下图。记 f(x, y) 表示 n = x,m = y时的最后剩下那个人的下标。
通过上图可以看出,每一次删除一个元素后,就从这个元素后面开始重新一轮,直到剩下最后一个元素,这里是D,其实题目就可以转换为剩下的最后一个元素在最初的队列中的下标是多少。
同时,当 n = 1 时,不管 m 等于多少,结果总是0,如果我们可以倒着从 n 等于 1 推导到 n 等于 n 不就可以解决这个问题了吗?以上面 n = 4 到n = 5为例,假设我们已经知道 f(4 ,3) = 0,想要推出 f(5,3)。如下图。
要想知道 D 在 n 等于5中的位置,首先要将n = 5到n = 4的过程中删除的C加上,加到哪里呢?每次重新编号都从删除的节点的后一个开始,所以C肯定是n = 4的队列的前面的一个元素,直接把C加到队列最前面。然后,由于n = 5到n = 4删除了C,所以C肯定是第m个元素,而不是第一个,所以还要从后面移m-1个元素到C的前面,即把 A 和 B 移到C的前面,过程如上图。相当于在D前面加了3个元素,也就是加了 m 个元素,即 D 的下标加上 m,得出f(5,3) = f(4,3) + 3。当然,还要考虑到越界的情况,所以最后的式子就是 f(5,3) = (f(4,3) + 3) % 5,改成通用的式子即f(n, m) = (f(n-1, m) + m) % n。有了这个式子,那代码就非常简单了,代码如下。
代码
public int lastRemaining(int n, int m) {
int last = 0;
for (int i = 2; i <= n; i++) {
last = (last + m) % i;
}
return last;
}