问题描述

约瑟夫环(Josephus Problem)是一个经典的数学和计算机科学问题。问题描述如下:

📋 问题定义

n 个人围成一个圆圈,从第 1 个人开始报数,每次数到 m 的人出列,然后从下一个人重新开始报数。如此循环,直到只剩下一个人为止。求最后剩下的人在原始圆圈中的位置。

例如,当 n=5, m=2 时:

  • 初始:[1, 2, 3, 4, 5] 围成一圈
  • 第 1 轮:数到 2,2 号出列 → [1, 3, 4, 5]
  • 第 2 轮:从 3 开始数,数到 4,4 号出列 → [1, 3, 5]
  • 第 3 轮:从 5 开始数,数到 1,1 号出列 → [3, 5]
  • 第 4 轮:从 3 开始数,数到 5,5 号出列 → [3]
  • 幸存者:3 号
暴力模拟
O(n²)
递归解法
O(n)
数学公式
O(n)

历史背景

约瑟夫环问题得名于犹太历史学家 弗拉维奥·约瑟夫斯(Flavius Josephus)。根据传说,在公元 1 世纪的犹太 - 罗马战争期间,约瑟夫斯和他的 40 名战友被罗马军队包围在一个山洞中。

1
宁死不屈的誓言
战友们宁愿自杀也不愿被罗马人俘虏,于是决定围成一圈,从某个人开始报数,每数到 3 就自杀,直到只剩一人
2
约瑟夫斯的计算
约瑟夫斯通过快速计算,找到了幸存者的位置(第 31 位),成功活了下来并最终投降罗马人
3
数学问题的诞生
这个问题后来被数学家们研究,形成了经典的约瑟夫环问题,成为递归和循环结构的经典案例

数学推导与递归公式

约瑟夫环问题有一个优雅的递归解法。我们用 J(n, m) 表示 n 个人、步长为 m 时幸存者的位置(从 0 开始编号)。

🧮 递归公式

基础情况:

J(1, m) = 0

递归关系:

J(n, m) = (J(n-1, m) + m) mod n

推导思路:当第一个人出列后,问题转化为 n-1 个人的约瑟夫环问题。由于编号发生了偏移,需要通过加 m 再取模来映射回原始编号。

递归调用树示例 (n=5, m=2)

J(5,2)


(J(4,2)+2)%5


((J(3,2)+2)%4+2)%5


(((J(2,2)+2)%3+2)%4+2)%5


((((J(1,2)+2)%2+2)%3+2)%4+2)%5


J(1,2) = 0



J(2,2) = (0+2)%2 = 0


J(3,2) = (0+2)%3 = 2


J(4,2) = (2+2)%4 = 0


J(5,2) = (0+2)%5 = 2

幸存者位置:2(从 0 开始),即第 3 个人

⚠️ 注意:编号方式

上述公式使用 从 0 开始 的编号(0, 1, 2, ..., n-1)。如果使用从 1 开始的编号(1, 2, 3, ..., n),最终结果需要加 1。

可视化演示

通过下面的交互式演示,观察约瑟夫环的消除过程:

约瑟夫环模拟器

消除记录将显示在这里...

代码实现

// 方法 1: 递归解法(从 0 开始编号)
function josephusRecursive(n, m) {
    if (n === 1) {
        return 0;
    }
    return (josephusRecursive(n - 1, m) + m) % n;
}

// 方法 2: 迭代解法(空间优化)
function josephusIterative(n, m) {
    let survivor = 0; // J(1, m) = 0
    for (let i = 2; i <= n; i++) {
        survivor = (survivor + m) % i;
    }
    return survivor;
}

// 方法 3: 循环链表模拟
function josephusSimulation(n, m) {
    // 创建人员数组(从 1 开始编号)
    const people = Array.from({ length: n }, (_, i) => i + 1);
    let index = 0;
    const eliminationOrder = [];

    while (people.length > 1) {
        // 计算要消除的位置
        index = (index + m - 1) % people.length;
        eliminationOrder.push(people[index]);
        people.splice(index, 1);
    }

    return {
        survivor: people[0],
        eliminationOrder
    };
}

// 使用示例
const n = 5, m = 2;
console.log(`递归解法:幸存者位置 = ${josephusRecursive(n, m) + 1}`);
console.log(`迭代解法:幸存者位置 = ${josephusIterative(n, m) + 1}`);
console.log(`模拟结果:`, josephusSimulation(n, m));
# 方法 1: 递归解法(从 0 开始编号)
def josephus_recursive(n: int, m: int) -> int:
    if n == 1:
        return 0
    return (josephus_recursive(n - 1, m) + m) % n

# 方法 2: 迭代解法(空间优化)
def josephus_iterative(n: int, m: int) -> int:
    survivor = 0  # J(1, m) = 0
    for i in range(2, n + 1):
        survivor = (survivor + m) % i
    return survivor

# 方法 3: 循环链表模拟
def josephus_simulation(n: int, m: int) -> dict:
    people = list(range(1, n + 1))  # 从 1 开始编号
    index = 0
    elimination_order = []

    while len(people) > 1:
        # 计算要消除的位置
        index = (index + m - 1) % len(people)
        elimination_order.append(people[index])
        people.pop(index)

    return {
        'survivor': people[0],
        'elimination_order': elimination_order
    }

# 使用示例
if __name__ == "__main__":
    n, m = 5, 2
    print(f"递归解法:幸存者位置 = {josephus_recursive(n, m) + 1}")
    print(f"迭代解法:幸存者位置 = {josephus_iterative(n, m) + 1}")
    print(f"模拟结果:{josephus_simulation(n, m)}")
package main

import "fmt"

// 方法 1: 递归解法(从 0 开始编号)
func josephusRecursive(n, m int) int {
    if n == 1 {
        return 0
    }
    return (josephusRecursive(n-1, m) + m) % n
}

// 方法 2: 迭代解法(空间优化)
func josephusIterative(n, m int) int {
    survivor := 0 // J(1, m) = 0
    for i := 2; i <= n; i++ {
        survivor = (survivor + m) % i
    }
    return survivor
}

// 方法 3: 循环链表模拟
type Node struct {
    val  int
    next *Node
}

func josephusSimulation(n, m int) (int, []int) {
    // 创建循环链表
    head := &Node{val: 1}
    prev := head
    for i := 2; i <= n; i++ {
        node := &Node{val: i}
        prev.next = node
        prev = node
    }
    prev.next = head // 形成环

    // 模拟消除过程
    eliminationOrder := []int{}
    current := head
    for n > 1 {
        // 找到第 m-1 个节点
        for i := 1; i < m-1; i++ {
            current = current.next
        }
        // 移除第 m 个节点
        eliminated := current.next
        eliminationOrder = append(eliminationOrder, eliminated.val)
        current.next = eliminated.next
        current = current.next
        n--
    }

    return current.val, eliminationOrder
}

func main() {
    n, m := 5, 2
    fmt.Printf("递归解法:幸存者位置 = %d\n", josephusRecursive(n, m)+1)
    fmt.Printf("迭代解法:幸存者位置 = %d\n", josephusIterative(n, m)+1)
    survivor, order := josephusSimulation(n, m)
    fmt.Printf("模拟结果:幸存者=%d, 消除顺序=%v\n", survivor, order)
}
#include <iostream>
#include <vector>
#include <list>
using namespace std;

// 方法 1: 递归解法(从 0 开始编号)
int josephusRecursive(int n, int m) {
    if (n == 1) {
        return 0;
    }
    return (josephusRecursive(n - 1, m) + m) % n;
}

// 方法 2: 迭代解法(空间优化)
int josephusIterative(int n, int m) {
    int survivor = 0; // J(1, m) = 0
    for (int i = 2; i <= n; i++) {
        survivor = (survivor + m) % i;
    }
    return survivor;
}

// 方法 3: 使用 list 模拟
pair<int, vector<int>> josephusSimulation(int n, int m) {
    list<int> people;
    for (int i = 1; i <= n; i++) {
        people.push_back(i);
    }

    vector<int> eliminationOrder;
    auto it = people.begin();

    while (people.size() > 1) {
        // 移动 m-1 步
        for (int i = 1; i < m; i++) {
            it++;
            if (it == people.end()) {
                it = people.begin();
            }
        }
        eliminationOrder.push_back(*it);
        it = people.erase(it);
        if (it == people.end()) {
            it = people.begin();
        }
    }

    return {people.front(), eliminationOrder};
}

int main() {
    int n = 5, m = 2;
    cout << "递归解法:幸存者位置 = " << josephusRecursive(n, m) + 1 << endl;
    cout << "迭代解法:幸存者位置 = " << josephusIterative(n, m) + 1 << endl;
    
    auto [survivor, order] = josephusSimulation(n, m);
    cout << "模拟结果:幸存者=" << survivor << ", 消除顺序=[";
    for (size_t i = 0; i < order.size(); i++) {
        cout << order[i] << (i < order.size() - 1 ? ", " : "");
    }
    cout << "]" << endl;
    
    return 0;
}

算法思维

约瑟夫环问题不仅是一个具体的算法题目,更是培养算法思维的绝佳案例。通过这个问题,我们可以学习到以下几种重要的算法思维方式:

🔄 递归思维

将大规模问题转化为相同结构的小规模问题,找到递推关系。

约瑟夫环中的体现:

n 个人的问题 → n-1 个人的问题
J(n,m) = (J(n-1,m) + m) % n

实际应用场景:

  • 分治算法(归并排序、快速排序)
  • 树的遍历(前序、中序、后序)
  • 动态规划的状态转移

📐 数学建模

将实际问题抽象为数学公式,通过数学推导找到最优解。

约瑟夫环中的体现:

报数过程 → 模运算
位置偏移 → 加法后取模
位置映射:(旧位置 + m) % 当前人数

实际应用场景:

  • 哈希函数的设计
  • 循环缓冲区的索引计算
  • 密码学中的置换算法

🎯 问题转化

当直接求解困难时,通过变换视角或重新定义问题来简化求解。

约瑟夫环中的体现:

从 1 开始编号 → 从 0 开始编号
求幸存者 → 求位置索引
最终结果 +1 即可还原

实际应用场景:

  • 图论中的最短路问题转化
  • NP 完全问题的近似求解
  • 反向思考(正难则反)

复杂度优化

从暴力解法出发,逐步优化时间和空间复杂度。

约瑟夫环的优化路径:

O(n²) 暴力模拟(链表删除)
O(n×m) 循环链表
O(n) 递归/迭代
O(1) 空间(迭代)

实际应用场景:

  • 算法面试中的优化思维
  • 大数据场景下的性能调优
  • 空间换时间/时间换空间的权衡

🔗 数据结构选择

根据问题特点选择合适的数据结构,往往能事半功倍。

约瑟夫环中的选择:

• 数组:删除 O(n)
• 循环链表:删除 O(1)
• 纯数学:无需结构

实际应用场景:

  • 频繁查找 → 哈希表
  • 范围查询 → 树结构
  • 先进先出 → 队列

🎲 模拟与抽象

理解问题时先模拟具体过程,再抽象出一般规律。

约瑟夫环的学习路径:

1️⃣ 手动模拟小规模案例
2️⃣ 观察消除规律
3️⃣ 归纳递推公式
4️⃣ 证明并实现

实际应用场景:

  • 调试时的手动执行追踪
  • 算法竞赛中的打表找规律
  • 系统设计前的原型验证
💡 思维总结

约瑟夫环问题的求解过程展示了完整的算法思维链条:理解问题 → 暴力模拟 → 观察规律 → 数学抽象 → 优化实现。这种思维方式不仅适用于算法竞赛,也是解决实际工程问题的核心能力。

🧩
分解问题
化繁为简
🔍
寻找规律
归纳总结
🎯
抽象建模
提炼本质
持续优化
追求高效

算法复杂度对比

方法 时间复杂度 空间复杂度 优点 缺点
暴力模拟 O(n²) O(n) 直观易懂,可记录消除顺序 效率低,不适合大规模数据
循环链表 O(n×m) O(n) 结构清晰,适合可视化 m 较大时效率低
递归解法 O(n) O(n) 代码简洁,数学优雅 递归深度大时可能栈溢出
迭代解法 O(n) O(1) 最优解,无递归开销 无法记录消除顺序