C/C++百题打卡[3/100]——约瑟夫问题


⌛️ 约瑟夫问题的由来:【最后我们会对其进行检验,看对不对】

  据说著名犹太历史学家 约瑟夫 有过以下的故事:在罗马人占领乔塔帕特后,39个犹太人与 约瑟夫及他的朋友 躲到一个洞中,39个犹太人决定宁愿死也不要被敌人抓到,于是决定了一个自杀方式,41个人排成一个圆圈,由第1个人开始报数,每报数到第3人该人就必须自杀,然后再由下一个重新报数,直到所有人都自杀身亡为止。然而约瑟夫和他的朋友并不想遵从。一开始要站在什么地方才能避免被处决呢?聪明的约瑟夫将朋友与自己安排在第16个与第31个位置,于是逃过了这场死亡游戏。



约瑟夫 ☁️

上一题链接: C/C++百题打卡[2/100]——考四六级的笨小猴⭐️⭐️ 考查字符串.
下一题链接: C/C++百题打卡[4/100]——融合最大数⭐️⭐️ 考查数学.

百题打卡总目录: 🚧 🚧 …


一、题目总述

n n n 个人围成一圈,他们的编号一开始分别为 1 、 2 、 . . . 、 n 1、2、...、n 12...n。从第一个人开始报数,数到 m m m 的人出列,再由下一个人重新从 1 1 1 开始报数,数到 m m m 的人再出圈,依次类推,直到所有的人都出圈,请输出依次出圈人的编号。

输入描述
输入两个整数 n , m n,m nm

输出描述
输出一行 n n n 个整数,按顺序输出每个出圈人的编号。

  注意:其中, 1 ≤ m , n ≤ 100 1≤m,n≤100 1m,n100。运行限制——>最大运行时间: 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     

  • 9
    点赞
  • 34
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

一支王同学

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值