一、介绍
约瑟夫环(Josephus Problem) 是一个著名的数学和计算机科学问题,大致意思是:N个人围成一圈,从第一个人开始报数,报到第M个人就将其淘汰出圈,然后从下一个人重新开始报数,如此循环,直到剩下最后一个人。求这个幸存者的初始位置。
题目链接:孩子们的游戏(圆圈中最后剩下的数)
二、解决方案
1. 模拟
该方法是最直观的,就是开辟一个数组记录哪些人被淘汰,一直遍历下去直至只剩下一个人。
cpp
class Solution {
public:
int LastRemaining_Solution(int n, int m) {
vector<bool> check(n, true);
int cnt = 0, idx = 0, alive = n;
while(alive > 1)
{
if(check[idx])
{
cnt++;
if(cnt == m)
{
cnt = 0;
alive--;
check[idx] = false;
}
}
if(++idx == n) idx = 0;
}
for(int i = 0; i < n; i++)
if(check[i])
return i;
return -1;
}
};
2. 递归
上述的方法确实可以解决问题,但是时间复杂度会有点高,下面介绍一下很巧妙的方法。它的核心是假设一共有 n 个人,在淘汰了一个之后就只剩下了 n - 1 个人,那么我们是不是可以将 n - 1 个人看作一个新的约瑟夫环问题。这个思想就是递归的核心!
上面我们在了解了思想后来想几个关键问题。当人数从 n 变成 n - 1 的时候,下一次淘汰人需要从这一次淘汰人的后一位开始计数,这时会发现好像下一个人的编号不一定是从 0 开始的呀!所以这里需要做一下映射。我们假设 k 是这一次淘汰的编号( k = (m - 1) % n ),那么下一次开始的位置就是 k + 1,它对应的新编号是 0、以此类推 k + 2 -> 1、k + 3 -> 2...。具体可以看下面 n=5, m=3 的例子:

黑色那一圈是初始 n 人旧编号,淘汰下标 k 之后,蓝色是剩下 n−1 人的新编号。所以可以推出:旧编号 = 新编号 + k + 1,但这对吗?很明显在上图中 k+3 已经超过原来的范围了所以实际是出:旧编号 = (新编号 + k + 1) % n 。这样看是不是就成了比原来更小一层的问题了。这里可以将 k = (m - 1) % n 公式合并到那个公式中,可以得到:旧编号 = (新编号 + m) % n。
想一个边界情况,如果 n 等于 1 呢?这时其实就可以直接返回因为只有一个人,而幸存者编号就是 0。所以递推公式如下( f(x)表示人数为 x 时幸存者的编号):

递归实现:
cpp
class Solution {
public:
int LastRemaining_Solution(int n, int m) {
if(n == 1)
return 0;
return (LastRemaining_Solution(n - 1, m) + m) % n;
}
};
dp实现:
cpp
class Solution {
public:
int LastRemaining_Solution(int n, int m) {
vector<int> dp(n + 1);
for(int i = 2; i <= n; i++)
dp[i] = (dp[i - 1] + m) % i;
return dp[n];
}
};