约瑟夫问题
有 n 个小朋友围成一圈玩游戏,小朋友从 1 至 n 编号,2 号小朋友坐在 1 号小朋友的顺时针方向,3 号小朋友坐在 2 号小朋友的顺时针方向,……,1 号小朋友坐在 n 号小朋友的顺时针方向。
游戏开始,从 1 号小朋友开始顺时针报数,接下来每个小朋友的报数是上一个小朋友报的数加 1。若一个小朋友报的数为 k 的倍数或其末位数(即数的个位)为 k,则该小朋友被淘汰出局,不再参加以后的报数。当游戏中只剩下一个小朋友时,该小朋友获胜。
例如,当 n=5, k=2 时:
1 号小朋友报数 1;
2 号小朋友报数 2 淘汰;
3 号小朋友报数 3;
4 号小朋友报数 4 淘汰;
5 号小朋友报数 5;
1 号小朋友报数 6 淘汰;
3 号小朋友报数 7;
5 号小朋友报数 8 淘汰;
3 号小朋友获胜。
给定 n 和 k,请问最后获胜的小朋友编号为多少?
输入格式
输入一行,包括两个整数 n 和 k,意义如题目所述。
输出格式 输出一行,包含一个整数,表示获胜的小朋友编号。
样例输入
5 2
样例输出
3
样例输入
7 3
样例输出
4
数据规模和约定
对于所有评测用例,1 ≤ n ≤ 1000,1 ≤ k ≤ 9。
解法一
双向链表法
#include <iostream>
#include <cstdlib>
typedef struct child{
int data;
struct child*prior,* next;
}*linklist;//创建小朋友节点
linklist loopInit(linklist &c, int n){
c = (linklist)malloc(sizeof(child));
if(c == NULL){
std::cerr << "Memory allocation failed!" << std::endl;
exit(EXIT_FAILURE);
}
c->data = 1;
child*s,*r = c;
for(int i = 2;i<=n;i++){
s = (linklist)malloc(sizeof(child));
if (s == NULL) {
std::cerr << "Memory allocation failed!" << std::endl;
exit(EXIT_FAILURE);
}
s->data = i;
s->prior = r;
r->next = s;
r = s;
}
r->next = c;
c->prior = r;
return c;
}//创建小朋友环
void loopdelete(linklist &c){
linklist temp = c->next;
c->prior->next = temp;
temp->prior = c->prior;
free(c);
c = temp;
}//淘汰小朋友
int solution(linklist c,int n, int k){
int count = 1;
while(n>1){
if(count%k==0||(count%10)==k){
loopdelete(c);
n--;
}else{
c = c->next;
}
count++;
}
return c->data;
}//执行整个报数流程
int main() {
int n,k;
std::cin >> n >> k;
linklist c = nullptr;
c = loopInit(c, n);
int result = solution(c,n,k);
std::cout << result << std::endl;
free(c);
return 0;
}
代码解释
- 初始化循环链表:
loopInit
函数创建一个包含n
个小朋友的循环链表,并初始化他们的编号。 - 删除节点处理:
loopdelete
函数删除当前节点并调整指针,保持链表结构的完整性。 - 游戏逻辑:
solution
函数模拟游戏过程,通过count
变量追踪当前节点的编号。- 每当
count
是k
的倍数或count
的个位数是k
时,删除当前节点。
- 每当
- 内存释放:在主函数结束前释放所有分配的内存,避免内存泄漏。
通过这种方式,我们可以正确处理任意位数的 count
并判断其个位数是否符合条件。
解法二
循环数组法
#include <iostream>
#include <vector>
int solution(int n, int k) {
std::vector<bool> eliminated(n, false);
int count = 0; // 报数计数器
int remaining = n; // 剩余的小朋友数量
int currentIndex = 0; // 当前小朋友的索引
while (remaining > 1) {
if (!eliminated[currentIndex]) {
count++;
if (count % k == 0 || (count % 10) == k) {
eliminated[currentIndex] = true;
remaining--;
}
}
currentIndex = (currentIndex + 1) % n; // 环形处理
}
// 找到最后一个未被淘汰的小朋友
for (int i = 0; i < n; ++i) {
if (!eliminated[i]) {
return i + 1; // 返回编号(从1开始)
}
}
return -1; // 理论上不应该到达这里
}
int main() {
int n, k;
std::cin >> n >> k;
int result = solution(n, k);
std::cout << result << std::endl;
return 0;
}
代码解释
- 初始化:创建一个大小为
n
的布尔数组eliminated
来标记每个小朋友是否被淘汰,初始值都为false
。 - 主循环:当剩余的小朋友数量大于 1 时,进行循环。
- 如果当前小朋友未被淘汰,则计数器
count
增加。 - 检查当前计数器是否满足淘汰条件:
count
是k
的倍数或count
的个位数是k
。 - 如果满足淘汰条件,则将当前小朋友标记为已淘汰,并减少剩余的小朋友数量。
- 如果当前小朋友未被淘汰,则计数器
- 更新索引:将当前索引移动到下一个小朋友(环形处理)。
- 找出最后一个未被淘汰的小朋友:循环结束后,遍历数组找出最后一个未被淘汰的小朋友,并返回其编号(从1开始)。
- 输入检查:确保输入的
n
和k
在规定的范围内,否则输出错误信息并退出。
优点
- 时间复杂度:使用数组进行标记和环形遍历,时间复杂度为 O(n^2),虽然看似较高,但由于每次操作都是常数时间,实际运行速度会比链表实现快很多。
- 空间复杂度:使用一个大小为
n
的布尔数组来存储淘汰标记,空间复杂度为 O(n)。
这个方法通过减少复杂的内存操作和指针操作,提高了代码的运行效率。在实际应用中,这种方法更容易理解和维护。