约瑟夫环问题
约瑟夫环问题
百度百科中写道:"约瑟夫问题是个有名的问题: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'; }
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· SQL Server 2025 AI相关能力初探
· 单线程的Redis速度为什么快?
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码