⌛️ 约瑟夫问题的由来:【最后我们会对其进行检验,看对不对】
据说著名犹太历史学家 约瑟夫 有过以下的故事:在罗马人占领乔塔帕特后,39
个犹太人与 约瑟夫及他的朋友 躲到一个洞中,39
个犹太人决定宁愿死也不要被敌人抓到,于是决定了一个自杀方式,41
个人排成一个圆圈,由第1
个人开始报数,每报数到第3
人该人就必须自杀,然后再由下一个重新报数,直到所有人都自杀身亡为止。然而约瑟夫和他的朋友并不想遵从。一开始要站在什么地方才能避免被处决呢?聪明的约瑟夫将朋友与自己安排在第16
个与第31
个位置,于是逃过了这场死亡游戏。
文章目录
约瑟夫 ☁️
上一题链接: C/C++百题打卡[2/100]——考四六级的笨小猴⭐️⭐️ 考查字符串.
下一题链接: C/C++百题打卡[4/100]——融合最大数⭐️⭐️ 考查数学.
百题打卡总目录: 🚧 🚧 …
一、题目总述
● n n n 个人围成一圈,他们的编号一开始分别为 1 、 2 、 . . . 、 n 1、2、...、n 1、2、...、n。从第一个人开始报数,数到 m m m 的人出列,再由下一个人重新从 1 1 1 开始报数,数到 m m m 的人再出圈,依次类推,直到所有的人都出圈,请输出依次出圈人的编号。
● 输入描述:
输入两个整数
n
,
m
n,m
n,m。
● 输出描述:
输出一行
n
n
n 个整数,按顺序输出每个出圈人的编号。
注意:其中, 1 ≤ m , n ≤ 100 1≤m,n≤100 1≤m,n≤100。运行限制——>最大运行时间: 1 s 1s 1s,最大运行内存: 128 M 128M 128M
● 输入样例:
10 3
● 输出样例:
3 6 9 2 7 1 8 5 10 4
二、思考空白区
● 题目难度:⭐️⭐️⭐️
● 建议思考时间:⌛️ ⌛️
三、题目解析
● 这是一道考查链表
的题。可以用数组也可以用链表,这里考虑用链表来做。(主要是可以温习链表的知识点)
- 首先,我们先简单地温习一下链表的知识:
① 链表是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。
② 链表由一系列结点(链表中每一个元素称为结点)组成,结点可以在运行时动态生成。
③ 每个结点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域。
● 单链表结构的简单样例:
● 双向链表结构的简单样例:
● 说明:这里要说明的是,指针不全是什么 “int*、double*、float*
” 等,也就是说 “并不是带*
号的才是指针”。而是说:只要能指示到某个数据的变量就是 “指针”(广义的指针)。
● 算法设计:
[1]
设计好 输入输出模块。
[2]
设计好 “人”的结构体。
[3]
在 报数模块 下设计好两个子分支:
<1>
报数人正好报到数字 “
m
m
m ”
<2>
报数人未报到数字 “
m
m
m ”
第[1]步:输入输出模块
● 这是易如反掌的事:
#include <stdio.h>
int main()
{
/* 输入模块 */
int n, m;
int ans[110]; // ans[]:存储答案的数组。所开的数组空间大于 100 即可
scanf("%d%d", &n, &m);
/* 算法设计 */
...
while (...) // 报数循坏体
{ ... }
/* 输出模块 */
for (int i = 1; i <= n; i++)
printf("%d ", ans[i]);
printf("\n");
return 0;
}
第[2]步:“人”的结构体
● 当这些人围成一个圈时,他们自然而然就有了3
个属性:
① 他一开始拥有的自己的编号。
② 在他左边的人的编号。
③ 在他右边的人的编号。
typedef struct List_Node // typedef: 取别名为后面的 “Node”
{
int own_number; // 自己的编号
int left_ind; // 左边的人的编号
int right_ind; // 右边的人的编号
}Node;
● 设计好 “人” 的结构体后,在把他们 “连接” 起来(也就是进行初始化操作,即排成一个圈)。
#include <stdio.h>
typedef struct List_Node // typedef: 取别名为后面的 “Node”
{
int own_number; // 自己的编号
int left_ind; // 左边的人的编号
int right_ind; // 右边的人的编号
}Node;
int main()
{
/* 输入模块 */
...
/* 结构体初始化模块 */
Node a[110];
for (int i = 1; i <= n; i++)
{
a[i].own_number = i; // 自己知道自己的编号
a[i].left_ind= i - 1; // 自己也会知道自己左边人的编号
a[i].right_ind= i + 1; // 自己也会知道自己右边人的编号
}
a[1].left_ind= n; // 记得给第 1 个人的左手边赋值为第 n 个人的编号
a[n].right_ind= 1; // 因为他们围成的是一个圈
/* 算法设计 */
...
while (...) // 报数循坏体
{ ... }
/* 输出模块 */
...
return 0;
}
● 我们可以通过下面这段代码来简单检查一下,我们的结构体是否构造成功:
for (int i = 1; i <= n; i++)
printf("%d <-- [我的左边是],[我的编号是: %d ],[我的右边是]--> %d\n", a[i].front_ind, a[i].own_number, a[i].next_ind);
● 运行结果:【可见,他们10
个人已经成功 “围” 成额一个圈】
第[3]步:报数模块
● 当我们设计好结构体后,那么他们就通过 “一根无形的链子” 连接了起来。在刚开始报数的时候,我们需用一个 指针 指向第1
个人。
● 报数模块框架如下:
#include <stdio.h>
typedef struct List_Node // typedef: 取别名为后面的 “Node”
{
...
}Node;
int main()
{
/* 输入模块 */
...
/* 结构体初始化模块 */
...
/* 报数模块 */
int ans[110]; // 记录依次出圈的人的编号
int ans_i = 1; // 在保存答案时的动态下标
int fp = 1; // 指针, 一开始指向第一个人
int cnt = 1; // 计数器, 每当报到 m 时, fp 指向的那个人就出圈
while ( ans_i <= n ) // 报数循坏体,
{
if ( cnt == m ) // 报数人正好报到 “ m ”
{
...
ans[ans_i++] = a[fp].own_number; // 答案赋值
...
}
else // 报数人未报到 “ m ”
{ ... }
}
/* 输出模块 */
...
return 0;
}
<1> 报数人正好报到数字 “ m ”
● 当某人需要出去时,比如在第一圈报数时,第m
个人就需要出圈,他出圈后,相应的,他左边的人的右边就不再是他了;他右边的人的左边也不再是他了。所以他出圈后,我们还要进行后续的处理,即更新链表。
● 关于 “更新链表” 这个小机制,我也做了可视化的图如下,便于理解:
● 最后该 指针 指向该 “出圈人” 的right_ind
(即他当前的右手边人的编号)。代码如下:
/* 报数模块 */
int ans[110]; // 记录依次出圈的人的编号
int ans_i = 1; // 在保存答案时的动态下标
int fp = 1; // 指针, 一开始指向第一个人
int cnt = 1; // 计数器, 每当报到 m 时, fp 指向的那个人就出圈
while ( ans_i <= n ) // 报数循坏体
{
if ( cnt == m ) // 报数人正好报到 “ m ”
{
/* 更新链表 */
int left = a[fp].left_ind; // 获取左边人的编号
int right = a[fp].right_ind; // 获取右边人的编号
a[left].right_ind = a[right].own_number; // 见前面的图例, 好理解
a[right].left_ind = a[left].own_number; // 见前面的图例, 好理解
/* 答案保存 + 后续处理 */
cnt = 1; // 计数器重新回到 1
ans[ans_i++] = a[fp].own_number; // 答案赋值
fp = a[fp].right_ind; // fp 指向下一个人
}
else // 报数人未报到 “ m ”
{ ... }
}
<2> 报数人未报到数字 “ m ”
● 当他不用出去时,就直接让该 指针 指向他的right_ind
即可(即他当前的右手边人的编号)。
/* 报数模块 */
int ans[110]; // 记录依次出圈的人的编号
int ans_i = 1; // 在保存答案时的动态下标
int fp = 1; // 指针, 一开始指向第一个人
int cnt = 1; // 计数器, 每当报到 m 时, fp 指向的那个人就出圈
while ( ans_i <= n ) // 报数循坏体,
{
if ( cnt == m ) // 报数人正好报到 “ m ”
{ ... }
else // 报数人未报到 “ m ”
{
cnt++; // 报数 + 1
fp = a[fp].right_ind; // fp 指向下一个人
}
}
● 至此,我们便解决了问题。随便测试文章开头的那个历史故事:
● 历史学家约瑟夫牛啊!
四、做题小结与反思
● 其实可以不用 “双向链表”,用一个单向的就可以,但是需要多设置一个指针(用于指向 fp 的前一个人)。
● 结合了链表(数据结构)和结构体的知识来解决问题。
五、完整代码(C和C++版)
● C 语言版本:
#include<stdio.h>
typedef struct List_Node
{
int own_number; // 自己的编号
int left_ind; // 左边的人的编号
int right_ind; // 右边的人的编号
}Node;
int main()
{
/* 输入模块 */
int n, m;
scanf("%d%d", &n, &m);
/* 初始化模块 */
Node a[110]; // ans[]:存储答案的数组。所开的数组空间大于 100 即可
for (int i = 1; i <= n; i++)
{
a[i].own_number = i; // 自己知道自己的编号
a[i].left_ind = i - 1; // 自己也会知道自己左边人的编号
a[i].right_ind = i + 1; // 自己也会知道自己右边人的编号
}
a[1].left_ind = n; // 记得给第 1 个人的左手边赋值为第 n 个人的编号
a[n].right_ind = 1; // 因为他们围成的是一个圈
/* 报数模块 */
int ans[110]; // 记录依次出圈的人的编号
int ans_i = 1; // 在保存答案时的动态下标
int fp = 1; // 指针, 一开始指向第一个人
int cnt = 1; // 计数器, 每当报到 m 时, fp 指向的那个人就出圈
while (ans_i <= n)
{
if (cnt == m) // 报数人正好报到 “ m ”
{
/* 更新链表 */
int left = a[fp].left_ind; // 获取左边人的编号
int right = a[fp].right_ind; // 获取右边人的编号
a[left].right_ind = a[right].own_number; // 见后面的图例, 好理解
a[right].left_ind = a[left].own_number; // 见后面的图例, 好理解
/* 答案保存 + 后续处理 */
cnt = 1; // 计数器重新回到 1
ans[ans_i++] = a[fp].own_number; // 答案赋值
fp = a[fp].right_ind; // fp 指向下一个人
}
else // 报数人未报到 “ m ”
{
cnt++; // 报数 + 1
fp = a[fp].right_ind; // fp 指向下一个人
}
}
/* 输出模块 */
for (int i = 1; i <= n; i++)
printf("%d ", ans[i]);
return 0;
}
● 运行结果:
● C++ 版本:
#include<iostream>
using namespace std;
typedef class List_Node // typedef: 取别名为后面的 “Node”
{
public:
int own_number; // 自己的编号
int left_ind; // 左边的人的编号
int right_ind; // 右边的人的编号
}Node;
int main()
{
/* 输入模块 */
int n, m;
cin >> n >> m;
/* 初始化模块 */
Node a[110]; // ans[]:存储答案的数组。所开的数组空间大于 100 即可
for (int i = 1; i <= n; i++)
{
a[i].own_number = i; // 自己知道自己的编号
a[i].left_ind = i - 1; // 自己也会知道自己左边人的编号
a[i].right_ind = i + 1; // 自己也会知道自己右边人的编号
}
a[1].left_ind = n; // 记得给第 1 个人的左手边赋值为第 n 个人的编号
a[n].right_ind = 1; // 因为他们围成的是一个圈
/* 报数模块 */
int ans[110]; // 记录依次出圈的人的编号
int ans_i = 1; // 在保存答案时的动态下标
int fp = 1; // 指针, 一开始指向第一个人
int cnt = 1; // 计数器, 每当报到 m 时, fp 指向的那个人就出圈
while (ans_i <= n)
{
if (cnt == m) // 报数人正好报到 “ m ”
{
/* 更新链表 */
int left = a[fp].left_ind; // 获取左边人的编号
int right = a[fp].right_ind; // 获取右边人的编号
a[left].right_ind = a[right].own_number; // 见后面的图例, 好理解
a[right].left_ind = a[left].own_number; // 见后面的图例, 好理解
/* 答案保存 + 后续处理 */
cnt = 1; // 计数器重新回到 1
ans[ans_i++] = a[fp].own_number; // 答案赋值
fp = a[fp].right_ind; // fp 指向下一个人
}
else // 报数人未报到 “ m ”
{
cnt++; // 报数 + 1
fp = a[fp].right_ind; // fp 指向下一个人
}
}
/* 输出模块 */
for (int i = 1; i <= n; i++)
cout << ans[i] << " ";
return 0;
}
六、参考附录
[1] 原题地址:https://www.luogu.com.cn/problem/P1996.
上一题链接: C/C++百题打卡[2/100]——考四六级的笨小猴⭐️⭐️ 考查字符串.
下一题链接: C/C++百题打卡[4/100]——融合最大数⭐️⭐️ 考查数学.
百题打卡总目录: 🚧 🚧 …
C/C++百题打卡[3/100]——约瑟夫问题 [题目源自 洛谷 ] ⭐️ ⭐️
标签:模拟、数组、链表
因为最近有重要考试所以缓更一周
2021/12/5