P1996 约瑟夫问题
题目链接:P1996 约瑟夫问题 - 洛谷
题目难度:普及-
目录
解法 1:队列模拟(STL queue)
解法 2:数组模拟
解法 3:链表模拟(单链表)
解法 4:递归公式
解法 5:迭代公式
解法 1:队列模拟(STL queue)
#include <iostream>
#include <queue>
using namespace std;int main() {int n, m;cin >> n >> m;queue<int> q;for (int i = 1; i <= n; ++i) q.push(i);while (!q.empty()) {for (int i = 1; i < m; ++i) { // 前m-1个移到队尾q.push(q.front());q.pop();}cout << q.front() << " "; // 第m个出队q.pop();}return 0;
}
讲解:
该方法利用 STL 队列的 FIFO 特性直观模拟约瑟夫环过程。先将 1 到 n 依次入队,形成初始的环形序列。每次循环时,为定位第 m 个待淘汰元素,需把前 m-1 个元素逐个弹出并重新压入队尾 —— 这一步完美复现了 “环” 的循环特性,即跳过前 m-1 个元素后,队首便是第 m 个目标。将目标元素弹出并输出后,重复操作直至队列清空。整个过程与问题描述高度一致,无需复杂逻辑转换,每步操作都对应着实际淘汰流程,适合新手通过可视化步骤理解约瑟夫环的核心逻辑。
解法 2:数组模拟
#include <iostream>
using namespace std;int main() {int n, m, cnt = 0, pos = 0;cin >> n >> m;bool vis[105] = {false};for (int i = 0; i < n; ++i) {int step = 0;while (step < m) { // 找第m个未出圈的pos = (pos + 1) % n;if (!vis[pos]) step++;}vis[pos] = true;cout << pos + 1 << " ";}return 0;
}
讲解:
此方法用布尔数组标记元素是否已出圈,通过位置偏移模拟环的循环。变量 pos 记录当前位置,每次循环需移动 m 步(遇到已出圈元素则跳过),确保找到第 m 个未出圈的元素。找到后标记该位置为已出圈并输出(注意转换为 1 基编号)。相比队列,数组无需频繁移动元素,仅通过标记和位置计算实现模拟,空间复杂度更低,且避免了队列操作的额外开销,适合理解 “标记 - 查找” 的高效模拟思路。
解法 3:链表模拟(单链表)
#include <iostream>
using namespace std;struct Node {int val;Node* next;Node(int x) : val(x), next(nullptr) {}
};int main() {int n, m;cin >> n >> m;Node* head = new Node(1);Node* cur = head;for (int i = 2; i <= n; ++i) { // 建环cur->next = new Node(i);cur = cur->next;}cur->next = head;while (n--) {for (int i = 1; i < m; ++i) cur = cur->next; // 移到第m-1个cout << cur->next->val << " ";Node* del = cur->next;cur->next = del->next; // 删除第m个delete del;}return 0;
}
讲解:
通过构建循环单链表模拟环形结构,每个节点存储一个数字,尾节点指向头节点形成闭环。每次淘汰第 m 个元素时,先将指针移动到该元素的前一个节点,再通过修改指针删除目标节点并输出其值。链表的动态删除特性恰好匹配约瑟夫环中 “元素逐个移除” 的需求,无需像数组那样预留空间或处理标记,能直观体现环的连续性和元素移除后的结构变化,适合理解链表在动态序列操作中的优势。
解法 4:递归公式
#include <iostream>
using namespace std;int f(int n, int m) {return n == 0 ? 0 : (f(n - 1, m) + m) % n;
}int main() {int n, m;cin >> n >> m;for (int i = 1; i <= n; ++i) { // 依次求每个出圈位置int pos = f(i, m) + 1; // 转换为1基cout << pos << " ";}return 0;
}
讲解:
基于约瑟夫环的数学递推关系:f (n,m) 表示 n 个元素时最后剩下的位置,其值为 (f (n-1,m)+m)% n(n=0 时为 0)。递归过程从 n=1 逐步向上计算,每次利用 n-1 规模的结果推导 n 规模的解。该方法跳出了模拟的思维框架,通过数学归纳将复杂问题分解为更小的子问题,时间复杂度降至 O (n),无需存储整个序列,仅通过函数调用栈传递中间结果,适合理解递归思想在数学建模中的应用。
解法 5:迭代公式
#include <iostream>
using namespace std;int main() {int n, m;cin >> n >> m;int* res = new int[n + 1];res[0] = 0;for (int i = 1; i <= n; ++i) { // 迭代计算res[i] = (res[i - 1] + m) % i;}for (int i = 1; i <= n; ++i) {cout << res[i] + 1 << " ";}delete[] res;return 0;
}
讲解:
将递归公式转化为迭代计算,用数组存储每个规模 i(1≤i≤n)的解。从 i=1 开始,依次计算 res [i]=(res [i-1]+m)% i,最后将结果转换为 1 基编号输出。迭代避免了递归可能的栈溢出问题,计算过程更可控,且数组存储的中间结果可直接复用,无需重复计算。这种方式兼顾了数学公式的高效性和迭代的稳定性,适合理解如何将递归逻辑转化为更实用的循环实现,尤其适合 n 较大的场景。。
笔者博客:jdlxx_dongfangxing-CSDN博客