约瑟夫环问题

约瑟夫环问题

百度百科中写道:"约瑟夫问题是个有名的问题:N个人围成一圈,从第一个开始报数,第M个将被杀掉,最后剩下一个,其余人都将被杀掉。"
其可以理解成有一个[0..N-1]的数组,从下标0开始,每次删掉第m个数,下一轮从被删掉的下一个数字开始,直至只剩下最后一个,那么最后剩下的一个是哪个数字?
我们以[0, 1, 2, 3, 4],m = 3为例。
从下标0开始,删掉第3个数,就是下标为m-1=2,那么删掉后就是[0, 1, 3, 4],下一次从3(下标为2)开始。
[0, 1, 3, 4]从3(下标2)开始就等价于[3, 4, 0, 1]从下标0开始,删掉第3个:0。
以此类推,下一个数组就是[1, 3, 4],删掉4。
再下一个数组就是[1, 3],删掉第三个,由于数组只有两个,所以删掉下标为 (2%2 = 0),也就是删掉1。
最后只剩下了数字3。

使用链表解决约瑟夫环问题

一个很自然的想法就是用循环链表来模拟删除的过程,看最后剩下的是哪个数字。
循环链表模拟约瑟夫环
我们将双向循环链表(定义为双向循环链表是为了方便删除当前节点)定义为:

struct bList {
bList(int val, std::shared_ptr<bList> backward = nullptr, std::shared_ptr<bList> forward = nullptr)
: val(val), forward(forward), backward(backward)
{}
int val;
std::shared_ptr<bList> forward;
std::shared_ptr<bList> backward;
};

使用双向链表模拟删除的过程:

int process(int m) {
auto head = std::make_shared<bList>(0);
auto cur = head;
for (int idx = 1; idx < 5; ++idx) {
cur->forward = std::make_shared<bList>(idx, cur);
cur = cur->forward;
}
cur->forward = head;
head->backward = cur;
auto ls = head;
int idx = 0;
while (ls->forward != ls) {
if (idx == m-1) {
// 下标从0开始,当下标为m-1 = 3-1 = 2时删除当前节点
std::cout << "removed: " << ls->val << '\n';
ls->backward->forward = ls->forward;
ls->forward->backward = ls->backward;
ls = ls->forward;
idx = 1;
} else {
ls = ls->forward;
++idx;
}
}
ls->backward = ls->forward = nullptr;
return ls->val;
}
int main() {
std::cout << process(3) << '\n';
}
// Output:
// removed: 2
// removed: 0
// removed: 4
// removed: 1
// 3

使用递推公式解决约瑟夫环问题

上面的解法虽然很简单,只需要模拟即可,但时间消耗却非常高,每查找一个数我们都需要遍历m次,即便我们可以将m对链表size取模减小,但时间复杂度仍是非常高的。于是我们试图找到一种更加高效的求解约瑟夫环的方法。
我们再回看我们手动解决例题的过程,其数组变化过程如下:

  • [0, 1, 2, 3, 4]
  • [3, 4, 0, 1]
  • [1, 3, 4]
  • [1, 3]
  • [3]

每次都删除第m个节点(下标为(m-1)%n,n为当前数组长度)。
可以确定地是最后幸存地数字在最后一个数组中的下标为0,而且此时数组的长度为1。
如果在我们知道第i+1个数组中最后幸存者的下标和数组长度的情况下,能够求出第i个数组中幸存者的下标和数组长度,我们是不是就可以从最后一个数组来倒推呢?第i个数组长度很明显是第i+1个数组长度+1,如何推导第i个数组中幸存者的下标呢?
首先我们假设第i+1个数组长度为L, 第i个数组长度为L+1;第i+1个数组中幸存者的下标为IdxN(意为idx next),第i个数组中幸存者的下标为IdxP(意为idx previous)。
递推公式示意图
i+1个数组中中幸存者的下标为N,那么必然有一个下表为0的节点。而该下标为0的节点在第i个数组中下标必然是m%(L+1)(因为它的上一个节点被删除了,而被删除的节点的下标必然是(m-1)%(L+1),那么被删除节点的下一个节点下标就是m%(L+1))。
我们就有如下等式:

IdxP - m = IdxN - 0 (mod L+1)
Idxp = IdxN + m (mod L+1)

(吐槽一下,博客园竟然不能显示公式。)

$$
\begin{equation}
\begin{split}
IdxP - m &= IdxN - 0\ (mod\ L+1) \\
IdxP &= IdxN + m\ (mod\ L+1)
\end{split}
\end{equation}
$$

上面的公式中第一个公式表示是在两个数组中两个数字的下标之差(距离)是相同的,因为被删除的数字在i[m%(L+1)]的紧贴着的左侧,i[m%(L+1)]向右看必然先看到i[IdxP],再看到i[(m-1)%(L+1)];而且IdxN-0是必然小于L+1的,因为第i+1个数组总长度只有L。
将第一个公式左边的m移到右边就得到了第二个公式,也就是从第i+1个数组中幸存者的下标递推出第i个数组中幸存者的下标的递推公式,其是第i个数组长度L+1、往后数的个数m和第i个数组中幸存者下标的函数。

将上述的过程转化为代码即为:

#include <iterator>
#include <memory>
#include <iostream>
#include <list>
// 这就是第二个公式,只是cnt表示的是L+1
// next是IdxN,m即为m
int process4_aux(int m, int next, int cnt) {
return (next + m)%cnt;
}
int process4(int m) {
int cnt = 1;
int next = 0;
while (cnt != 5) {
next = process4_aux(m, next, cnt+1);
++cnt;
std::cout << "at cnt = " << cnt << ", idx: " << next << '\n';
}
return next;
}
int main() {
std::cout << process4(3) << '\n';
}
// at cnt = 2, idx: 1
// at cnt = 3, idx: 1
// at cnt = 4, idx: 0
// at cnt = 5, idx: 3
// 3

关于网上的题解

我在百度上搜索约瑟夫环相关的问题时,LeetCode上的《约瑟夫环问题的三种解法讲解》这个帖子排名很高。但他的代码貌似有问题,其关于使用循环链表解题的前两个代码好像都错了,这里贴上我用C++写的(我认为)正确的版本:

#include <iterator>
#include <memory>
#include <iostream>
#include <list>
struct bList {
bList(int val, std::shared_ptr<bList> backward = nullptr, std::shared_ptr<bList> forward = nullptr)
: val(val), forward(forward), backward(backward)
{}
int val;
std::shared_ptr<bList> forward;
std::shared_ptr<bList> backward;
};
class Solution {
public:
int process( std::shared_ptr<bList> ls,int m) {
int idx = 1;
while (ls->forward != ls) {
if (idx == m) {
std::cout << "removed: " << ls->val << '\n';
ls->backward->forward = ls->forward;
ls->forward->backward = ls->backward;
auto tmp = ls;
ls = ls->forward;
tmp->forward = tmp->backward = nullptr;
idx = 1;
} else {
ls = ls->forward;
++idx;
}
}
ls->forward = ls->backward = nullptr;
return ls->val;
}
int process2( std::shared_ptr<bList> ls,int m) {
int idx = 1;
auto cur = ls;
int n = 1;
while (cur ->forward != ls) {
++n;
cur = cur->forward;
}
while (ls->forward != ls) {
if (idx == ((m+n-1)%n+1)) {
std::cout << "removed: " << ls->val << '\n';
ls->backward->forward = ls->forward;
ls->forward->backward = ls->backward;
auto tmp = ls;
ls = ls->forward;
tmp->forward = tmp->backward = nullptr;
idx = 1;
--n;
} else {
ls = ls->forward;
++idx;
}
}
ls->forward = ls->backward = nullptr;
return ls->val;
}
int process3(int m) {
std::list<int> list;
for (int idx = 0; idx < 5; ++idx) {
list.push_back(idx);
}
int idx = 0;
while (list.size() > 1) {
idx = (idx+m-1)%list.size();
auto itr = list.cbegin();
std::advance(itr, idx);
std::cout << "remove: " << *itr << '\n';
list.erase(itr);
}
return list.front();
}
int process4(int m) {
int cnt = 1;
int next = 0;
while (cnt != 5) {
next = process4_aux(m, next, cnt+1);
++cnt;
std::cout << "at cnt = " << cnt << ", idx: " << next << '\n';
}
return next;
}
private:
int process4_aux(int m, int next, int cnt) {
return (next + m)%cnt;
}
};
int main() {
auto head = std::make_shared<bList>(0);
auto cur = head;
for (int idx = 1; idx < 5; ++idx) {
cur->forward = std::make_shared<bList>(idx, cur);
cur = cur->forward;
}
cur->forward = head;
head->backward = cur;
Solution s ;
std::cout << s.process2(head, 3) << '\n';
std::cout << s.process3(3) << '\n';
std::cout << s.process4(3) << '\n';
}
posted @   流云cpp  阅读(324)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· SQL Server 2025 AI相关能力初探
· 单线程的Redis速度为什么快?
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
点击右上角即可分享
微信分享提示