约瑟夫环问题
问题描述
个编号从 的人逆时针站成一圈,开始从 号开始,每次从当前人开始数 个,然后这个人出局,求最后一个人编号多少?
该问题由约瑟夫 (Titus Flavius Josephus),于公元一世纪提出,他当时求解的是 的情况 (maybe (o゚v゚)ノ
但他还是很强呀)。
线性算法
设 表示规模为 的Josephus问题的解(注:这里编号是从0开始计数的,所以最终要将答案+1),于是有如下递推式:
该递推式不难理解,从 开始数 个, 号出局,则 就是 个人的局中开始的那个,于是相对来说他在 的局中又作为 号位开始,于是 局中的解在做相对位移 后就是 的局中的答案了。
//递归版
int rec(int n, int k) {
if (n == 1) return 0;
return (rec(n-1, k) + k) % n;
}
//循环版
int josephus(int n, int k) {
int res = 0;
for (int i = 1; i <= n; i++) res = (res + k) % i;
return res + 1;
}
优化
当 比较小, 比较大的情况,线性算法就显得有点力不从心了。
观察递推式 可以发现,如果当 非常大的时候, 在该式中就无法发挥作用,那么是不是就可以考虑对 直接求和,在保证 不起作用的前提下。
考虑一个顺推,设 , 为当前状态 能最多往后面直接累加 个 ,则有:
可以证明,这样做的复杂度为 ,证明见 OI-Wiki 约瑟夫问题 对数算法。
int josephus(int n, int k) {
if (k == 1) return n;
int sz = 1, pos = (k-1) % sz;
while (sz < n) {
if (pos + k >= sz + 1) { //线性算法
sz++;
pos = (pos + k) % sz;
continue;
}
int x = min((sz - pos - 1) / (k - 1), n - sz);
pos += x * k;
sz += x;
}
return pos + 1;
}
仔细观察这个写法的时间复杂度其实是 ,因为 while
中间的 if
判断了当 较小的情况。
练习
约瑟夫环问题
https://wty-yy.github.io/posts/7922/