PTA习题8-4报数游戏:用两种C语言解法搞定约瑟夫环(附详细思路拆解)

张开发
2026/4/21 17:14:36 15 分钟阅读

分享文章

PTA习题8-4报数游戏:用两种C语言解法搞定约瑟夫环(附详细思路拆解)
PTA习题8-4报数游戏两种C语言解法深度解析约瑟夫环问题约瑟夫环问题在计算机科学和数学领域堪称经典它模拟了一个古老而残酷的生存游戏。n个人围成一圈从某个指定位置开始报数数到m的那个人就被淘汰出局然后从下一个人重新开始报数直到所有人都被淘汰。这道题目不仅考察编程能力更考验对循环、数组和模运算的理解深度。下面我们将通过两种截然不同的C语言实现方式带你彻底掌握这个算法的精髓。1. 约瑟夫环问题本质与解题思路约瑟夫问题(Josephus problem)最早由犹太历史学家弗拉维奥·约瑟夫斯记载。在计算机科学中它成为了研究循环数据结构和算法设计的经典案例。问题的核心在于如何高效地模拟这个淘汰过程并记录每个人的出局顺序。理解约瑟夫环需要把握三个关键点循环结构虽然使用数组存储人员编号但逻辑上需要构成一个环淘汰机制每次数到第m个人就将其标记为已淘汰终止条件当只剩最后一人时结束游戏在实际编程中我们通常用数组来模拟这个环状结构通过取模运算实现循环遍历。PTA习题8-4要求我们编写CountOff函数接收三个参数n参与游戏的总人数m报数的淘汰阈值out[]用于存储每个人出局顺序的数组2. 数组标记法直观的模拟解法第一种解法采用最直接的模拟方式我们称之为数组标记法。这种方法的核心思想是维护一个状态数组记录每个人是否还在游戏中。void CountOff(int n, int m, int out[]) { int item n, i, num 0, j 1; int s[MAXN]; for(i 0; i n; i) s[i] i 1; while(item) { for(i 0; i n; i) { if(s[i] ! 0) { num; if(num m) { s[i] 0; out[i] j; item--; j; num 0; } } } } }2.1 代码逐层解析初始化阶段item变量初始化为n表示当前剩余人数数组s存储每个人的初始编号(1到n)num用于计数j记录出局顺序主循环逻辑外层while循环以item(剩余人数)为条件内层for循环遍历整个数组只有当s[i] ! 0时(表示该人未被淘汰)才进行计数淘汰机制当num累加到m时执行淘汰操作将s[i]置0标记为已淘汰在out[i]中记录出局顺序重置num为0开始新一轮计数2.2 关键技巧与优化空间这种方法的优势在于直观易懂但存在明显的效率问题即使大部分人已被淘汰仍需完整遍历数组对于未被淘汰的人每次都要重复判断s[i] ! 0提示在实际应用中当n较大时这种方法的性能会明显下降。但对于PTA习题的小规模测试用例完全够用。我们可以做一个小优化在内存循环中当item为0时立即break避免不必要的遍历while(item) { for(i 0; i n item; i) { // ...原有逻辑... } }3. 计数法更高效的实现方式第二种解法采用了不同的思路我们称之为计数法。它通过更聪明的计数方式减少了不必要的遍历。void CountOff(int n, int m, int out[]) { int i, count 0, j 0, item 0; for(i 0; i n; i) out[i] 0; i 0; while(1) { if(out[i] 0) count; if(count m) { item; out[i-1] item; count 0; } if(item n) break; i i % n; } }3.1 代码精妙之处初始化差异直接使用out数组作为状态标记(0表示未淘汰)count记录当前报数item记录已淘汰人数核心逻辑使用i在判断的同时移动指针只有out[i]为0时才增加count当count达到m时标记当前位置的出局顺序循环控制i i % n确保索引始终在有效范围内当item n(所有人已淘汰)时退出循环3.2 i与i的深入探讨代码中使用了out[i]的写法这实际上是利用了C语言的后置递增特性表达式先使用i的当前值然后再递增因此需要out[i-1]来修正位置如果改用前置递增代码可以改写为while(1) { if(out[i] 0) count; i; // ...其余逻辑不变... }两种写法各有优劣写法优点缺点out[i]代码紧凑需要后续修正(i-1)分开写逻辑更清晰多一行代码3.3 模运算的关键作用i i % n这行代码看似简单实则至关重要。考虑当i不断增加时当i n时i % n 0回到数组开头当i n时确保索引不会越界当i n时i % n i不影响正常索引这种写法比条件判断更简洁高效是处理循环数组的常用技巧。4. 两种解法的对比分析与选择建议为了更清晰地理解两种方法的差异我们通过一个具体例子(n5, m3)来演示4.1 执行过程对比数组标记法初始数组[1,2,3,4,5]第一轮淘汰3 → [1,2,0,4,5], out[2]1第二轮淘汰1 → [0,2,0,4,5], out[0]2第三轮淘汰5 → [0,2,0,4,0], out[4]3第四轮淘汰2 → [0,0,0,4,0], out[1]4最后剩下4 → out[3]5计数法初始out[0,0,0,0,0]第一轮i0→2淘汰3 → out[2]1第二轮i3→0→1淘汰1 → out[0]2第三轮i1→4淘汰5 → out[4]3第四轮i0→1→2(已淘汰)→3→4(已淘汰)→0→1淘汰2 → out[1]4最后剩下4 → out[3]54.2 性能与适用场景分析特性数组标记法计数法时间复杂度O(n×m)O(n×m)空间复杂度O(n)O(1)额外空间代码复杂度较低中等适合场景n较小m较小n较大m中等优势直观易懂减少状态数组劣势重复遍历指针控制较复杂在实际编程练习中我通常推荐初学者先掌握数组标记法因为它更符合直觉。当对问题有深入理解后再尝试实现计数法体验不同思维路径带来的编码差异。5. 常见错误与调试技巧在实现约瑟夫环问题时有几个常见的陷阱需要注意数组索引越界忘记对索引取模导致访问非法内存解决方案始终使用i i % n确保安全计数重置时机错误应该在淘汰人后立即重置计数错误示例将num0放在循环外部出局顺序记录错误混淆了人员编号和出局顺序确保out[i]记录的是顺序而非编号调试时可以添加打印语句实时观察数组状态// 调试打印 printf(Step %d: , step); for(int k 0; k n; k) { printf(%d , out[k]); } printf(\n);对于PTA的在线评测如果遇到答案错误可以尝试以下测试用例n1, m1 (边界情况)n5, m1 (每次淘汰第一个)n5, m5 (淘汰第五个)n100, m10 (较大规模测试)

更多文章