C语言学习笔记20260625-约瑟夫环(2种方法)
·
C语言学习笔记20260625-约瑟夫环(2种方法)
一、学习目标
通过经典的“约瑟夫环”问题,掌握计算机解决循环淘汰类问题的两种核心范式。深入理解“物理模拟法”中动态数组的缩圈逻辑,并探究“数学递推法”如何将复杂的模拟过程降维成一行简洁的公式,体会算法从“直观”到“极致优化”的思维跃迁。
二、问题拆解与核心逻辑
本题要求 n个人(编号 0 - n-1)围成一圈,从编号 k开始报数,每报到m的人出局,直到剩下最后一人。核心约束条件为:
- 环形结构:队伍首尾相连,报数到队尾后需自动回到队首。
- 动态缩圈:有人出局后,队伍长度减 1,且下一轮报数从出局者的下一个人无缝衔接。
- 起始偏移:报数并非总是从编号 0 开始,而是从指定的编号 k开始。
三、方案一:模拟队列法(物理缩圈)
3.1 核心思路
使用一个连续数组 arr 来物理存储当前还在圈内的人。当有人出局时,直接将数组中该位置之后的所有元素向前移动一位,覆盖掉出局者。这种“缩圈”操作保证了数组始终是紧凑的,下标 0 - len-1 永远对应当前存活的len个人。
3.2 代码解析与难点突破
int len = n;
int cur = k; // 当前起始报数位置下标
while (len > 1)
{
// 核心公式:计算出局者在当前数组中的下标
cur = (cur + m - 1) % len;
// 物理删除:后面元素前移覆盖
for (int i = cur; i < len - 1; i++)
arr[i] = arr[i + 1];
len--; // 队伍长度减 1
}
3.3 核心技巧与优缺点
- 动态下标计算:
cur = (cur + m - 1) % len是此算法的精华。因为当前从cur开始报“1”,所以报到“m”的人相对于cur的偏移量是m-1。取模% len完美解决了环形越界问题。 - 无缝衔接:由于数组前移,出局者后面的那个人会自动填补到
cur这个下标位置。因此,下一轮循环开始时,cur不需要额外调整,直接作为新的起点即可。 - 优缺点:逻辑极其直观,但由于每次删除都需要移动元素,时间复杂度为 O(n^2),仅适用于n较小(如 n<1000)的场景。
四、方案二:数学公式法(极致优化)
4.1 核心思想:逆向递推
不模拟淘汰过程,而是通过数学归纳法寻找规律。假设我们知道n-1个人时幸存者的下标f(n-1),那么n个人时幸存者的下标f(n)可以通过相对位移推导出来。
4.2 递推公式推导
- 基础公式:设f(i)为i个人时最后存活的下标(默认从 0 开始报数)。
- f(1) = 0 (只剩 1 人时,幸存者下标必为 0)
- f(i) = (f(i-1) + m) % i (i>1) 时,幸存者位置等于上一轮结果向后偏移 m位,并对当前人数取模)
- **处理起始偏移 k:标准公式是基于从编号 0 开始报数的。本题从编号 k开始,相当于整个环旋转了k位。因此,最终结果需要加上偏移量 kkk 并对总人数 nnn 取模:
- ans = (f(n) + k) % n
4.3 代码实现
int res = 0;
// 从 2 个人开始,逐步递推到 n 个人
for (int i = 2; i <= n; i++)
res = (res + m) % i;
// 修正起始位置 k 的偏移
res = (res + k) % n;
printf("%d", res);
4.4 优缺点
- 优点:时间复杂度仅为 O(n),空间复杂度 O(1)。即使 n达到 10^8 甚至更大,也能瞬间算出结果。
- 缺点:需要较强的数学推导能力,代码虽然简短,但如果不理解背后的递推逻辑,很难直接写出。
五、总结
总结:
方案一展示了计算机“暴力模拟”的强大能力,通过数组的动态操作将复杂的环形淘汰具象化,是理解问题本质的基石。方案二则体现了数学在计算机科学中的降维打击能力,通过寻找规律将n轮复杂的淘汰过程浓缩为一个简单的迭代公式。在实际工程与算法竞赛中,我们应当根据数据规模灵活选择:小规模数据用模拟法确保正确与直观,大规模数据则必须依赖数学公式实现性能飞跃。
更多推荐



所有评论(0)