寻找下一个人的过程可以用线段树优化。具体地,开一个 0,1,⋯,n−1 的线段树,然后记录区间内剩下的人的个数。寻找当前的人的位置以及之后的第 k 个人可以在线段树上二分做。
线性算法
设 Jn,k 表示规模分别为 n,k 的约瑟夫问题的答案。我们有如下递归式
Jn,k=(Jn−1,k+k)modn
这个也很好推。你从 0 开始数 k 个,让第 k−1 个人出局后剩下 n−1 个人,你计算出在 n−1 个人中选的答案后,再加一个相对位移 k 得到真正的答案。这个算法的复杂度显然是 Θ(n) 的。
实现
int josephus(int n, int k) { int res = 0; for (int i = 1; i <= n; ++i) res = (res + k) % i; return res;}
对数算法
对于 k 较小 n 较大的情况,本题还有一种复杂度为 Θ(klogn) 的算法。
考虑到我们每次走 k 个删一个,那么在一圈以内我们可以删掉 ⌊kn⌋ 个,然后剩下了 n−⌊kn⌋ 个人。这时我们在第 ⌊kn⌋⋅k 个人的位置上。而你发现它等于 n−nmodk。于是我们继续递归处理,算完后还原它的相对位置。还原相对位置的依据是:每次做一次删除都会把数到的第 k 个人删除,他们的编号被之后的人逐个继承,也即用 n−⌊kn⌋ 人环算时每 k 个人即有 1 个人的位置失算,因此在得数小于 0 时,用还没有被删去 k 倍数编号的 n 人环的 的 n 求模,在得数大于等于 0 时,即可以直接乘 k−1k, 于是得到如下的算法:
实现
int josephus(int n, int k) { if (n == 1) return 0; if (k == 1) return n - 1; if (k > n) return (josephus(n - 1, k) + k) % n; // 线性算法 int res = josephus(n - n / k, k); res -= n % k; if (res < 0) res += n; // mod n else res += res / (k - 1); // 还原位置 return res;}