啥是约瑟夫环啊?
在非常规面试题里看到这玩意儿,搜了一下,发现妙哉
话不多说,直接拷打ChatGPT
约瑟夫环问题(Josephus Problem)是一个经典的数学和计算机科学问题。它的背景故事源于犹太历史学家约瑟夫斯(Josephus)的经历:
在罗马人占领乔塔帕特后,约瑟夫斯和他的朋友与39个犹太人躲在一个洞中。为了避免被敌人抓到,他们决定采用一种自杀方式:41个人排成一个圆圈,从第一个人开始报数,每数到第3个人时,该人就必须自杀,然后再由下一个人重新报数,直到所有人都自杀身亡为止。
不是哥么,没事儿自杀干嘛,我们来看看正规的数学描述
数学描述
有 ( n ) 个人围成一个圈,从第 ( k ) 个人开始报数,每数到第 ( m ) 个人时,该人出圈,接着从下一个人重新开始报数。这个过程一直持续,直到最后只剩下一个人。
递归公式为:f(n,m)=(f(n−1,m)+m) mod n
其中,f(n, m) 表示 n 个人报数,最后剩下的人的位置
f(n - 1, m) 表示 n - 1 个人报数,最后剩下的人的位置
具体到代码为
private int solve(int n, int k) {
int pos = 0;
for (int i = 2; i <= n; i++) {
pos = (pos + k) % n;
}
return pos + 1;
}
这么清晰简单? 那我背住不就完了?
NoNoNo,一般面试官问你:“给哥么讲讲思路呗.”
寄
所以我们还需要和他再扳扯两句
递推公式咋来的?
每隔 k 个人毙 1 个,到末尾再从起始位置开始数,这不就是最喜欢的循环链表吗?
还真是~
那么起始也可以模拟做,一直遍历循环链表,每遍历 k - 1 个,就把当前的节点,指向下一个节点的下一个节点,这就实现把第 k 个节点删除的效果,这样复杂度为O(kn)
那有没有更简单的办法呢?
按理说这玩意儿的结果就是确定的,有没有一套完美公式轻松计算得到结果呢?
例子
我们来看一个8个人,每3个人毙1个的例子
1 2 3 4 5 6 7 8
这里 3 没了,我们从 4 开始,即
4 5 6 7 8 1 2
同理,继续
7 8 1 2 4 5
2 4 5 7 8
7 8 2 4
4 7 8
4 7
7
OK, 致敬clearlove了,留下了 7
这里注意到我的写法了吗,每次遍历,我都以下一次遍历的起始位置为头,重新建立了一个队列
这样有啥用?
递推啊,bro
我要是从结果能到推到最终结果,那不就实现了O(n)的复杂度的解法吗?
注意每次遍历建队列的过程:“以下一次遍历的起始位置为头,重新建立了一个队列”
从第一行看,本来 1 打头,第二行 4 打头,是不是就是在第一行遍历了 3 个,然后把后面的数往前挪
再看第二行,同理,本来 4 打头,第三行 7 打头,又是往前挪了
那挪来挪去有啥用呢?
我们要是知道最终存活的(下标0)反过来挪的位置,不就知道最终挪的坐标在哪儿了吗?
同理,我们现在按照上面的过程再来一遍,同时我们只去注意最后存活的 7 的位置
1 2 3 4 5 6 7 8
4 5 6 7 8 1 2
7 8 1 2 4 5
2 4 5 7 8
7 8 2 4
4 7 8
4 7
7
注意到了吗,就是一个简简单单的索引减去3,然后对队列长度取余(针对越界)
那我反过来处理不就得到结果了吗
private int solve(int n, int k) {
int pos = 0;
for (int i = 2; i <= n; i++) {
pos = (pos + k) % n;
}
return pos + 1;
}
再看看这个递推,是不是就有点感觉了
那你后面+1干啥的?
哦,那是针对于题中是1 ~ n的情况,我们这里采用的0 ~ n-1,所以加了个1, 不是啥高级操作捏
如此,我们便简简单单的解决了这一难题,这样我们穿越到古罗马时候,就能成为最后活的那个叛徒了