约瑟夫问题,
是一个计算机科学和数学中的问题,
在计算机编程的算法中,
类似问题又称为约瑟夫环,
又称“丢手绢问题”
(来自百度百科词条:约瑟夫问题)。
约瑟夫问题定义
约瑟夫问题是个有名的问题:
N个人围成一圈,从第一个开始报数,
第M个将被杀掉,最后剩下一个,
其余人都将被杀掉。例如N=6,M=5,
被杀掉的顺序是:5,4,6,2,3。
约瑟夫问题分析
这个问题我遇到过几次了,
时间跨度相当长,
回顾一下当时的心路历程,
也算我入CSDN一年的纪念文吧。毕竟,
从当初的不会做,后续陆陆自己写出来多种解法。
同时这也是我用markdown写的第一篇文章ω
第一次遇到约瑟夫问题,
应该是大一上学期举行的程序设计新生赛,
那时的我刚开始上《程序设计基础》课,
连C语言中数组的概念都没搞清楚,
试图解决此题自然是不行的,
但是当时看榜做出这道题的人还挺多的,
同是新生怎么差距那么大呢?
当时的题目大概数据范围是最多到20个人,
当我后面知道他们是一个个手算打表,
一个个情况用if else输出 😃
好吧,那时我才知道,
这种比赛与其称作是程序设计竞赛,
不如算法竞赛叫的直观,
要灵活的解决问题,
而不是以为可以通过计算机强大的计算能力,
就去忽略了问题的本质。
通过新生赛的教训,我对编程的理解也有了提升,
伴随着C语言课上完,对语法逐渐熟悉,加上我对编程的兴趣,
又接触到了我们学校的OJ系统,开始在上面做老师推荐的题单
(其实就是一本通基础篇的题目),再次遇到了这个问题
在这里我就把题目整个放出来
题目描述
N 个人围成一圈,从第一个人开始报数,
数到M的人出圈;再由下一个人开始报数,
数到M的人出圈;…输出依次出圈的人的编号。
输入
输入N和M。
输出
输出一行,依次出圈的人的编号。
输入样例
8 5
输出样例
5 2 8 7 1 4 6 3
提示
【数据范围】
对于所有数据,2≤N,M≤1000。
虽然说当时学完了C语言,但毕竟基本都是学的语法,
没有接触到什么思维题,
所以说这题看了半天也不会,
网上一搜,说这道题要用链表,
于是很SAD这道题就被我跳过了
大一下学期,我选了一门ACM相关的课程,
在介绍STL的一节课上这道题又被当做了vector的例题,
虽然说题目改动成了输出最后一个出圈人的编号,
但思路都是一样的,
那时候我已经看出这个题不用什么链表,
就是一个模拟,
自己想出了两个解法。
这两个代码跟我最近写的链表的解法码风还是有很大区别的,
足以看出我在这半年内的变化哈哈。
并且我们老师还说,
如果一个学生能够独立地做出这个题,
那他就有参加ACM比赛的潜力,笑。
解法一:【模拟】标记数组
因为我当时写的码风不太规范,也没有注释,所以写这篇文章时
我再写了一遍
初学者代码,大家图一乐,请看下下面代码
#include<stdio.h>
int main()
{
int n,m,i,start,end,t,s;
int a[1001],flag[1001]={0};
scanf("%d %d",&n,&m);
start=0;
for (i=0;i<n;i++)
a[i]=i+1;
for (s=0;s<n;s++)
{
t=0;
for (i=start; ;i++)
{
if (flag[i%n]==0)
t++;
if (t==m)
break;
}
i%=n;
//printf("%d\n",i);
flag[i]=1;
printf("%d ",a[i]);
if (s<n-1)
{
start=(i+1)%n;
for (i=start; ;i++)
{
if (flag[i%n]==0)
break;
else
start++;
}
start%=n;
}
}
return 0;
}
我们为了模拟出圈的过程,这里看作他们这个圈的大小始终是m,
但是出圈的人在报数过程中要跳过
- 维护一个出圈人数变量out和一个报数计数器cnt
- 使用一个标记数组,初始值每个人都在圈内,
- 若报数报到m且这个人对应的编号没有出圈,
将该编号打上标记并给out变量加一,再清零报数计数。
并输出编号 - 使用now代表当前遍历到的编号,无论其是否在圈内,
都要继续往下报数,now+1,由于这是一个环,当到达n号
位置时,要重新从1开始计数,利用取模操作可以完成 - 当全部出圈即out=n时,跳出循环,注意内层循环的判断
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 1e5 + 10;
#define endl '\n'
int main()
{
ios::sync_with_stdio(false);
cin.tie(0);
int n, m;
cin >> n >> m;
bool flag[n + 1]; //创建标记数组标记出圈状态
memset(flag, false, sizeof(flag)); //在栈区创建要初始化为false
int cnt = 0, out = 0, now = 1; // cnt用于报数计数,out统计出圈人数,now是当前学生编号
while (out < n)
{
while (cnt < m && out < n)
{
if (!flag[now]) //已经出圈的元素就不用报数了
{
cnt++;
if (cnt == m)
{
cout << now << " ";
flag[now] = true; //打上出圈标记
out++;
cnt = 0; //报数计数清零
}
}
now++;
if (now > n) //当前学生编号大于n成环重新从1开始
now %= n;
}
}
return 0;
}
解法二:【模拟】移动数组
上一个解法中我们并没有实际改变圈的大小,
因为我们并没有建立圈的数据结构,
仅是通过题目中数据有序递增这一特点来推断当前编号,
也就没办法让一个元素在循环过程中真正出圈,
通过标记数组来模拟出圈过程。
所以说解法一是圈不动人动,
解法二和解法三是圈和人都动。
- 在这个解法中,
我们建立一个数组存储圈,
当一个编号需要出圈后,
我们把该编号的后续元素向前移一位,
直到数组为空。 - 我们用一个变量start来表示报数起点的下标,
报数到m要出圈的那个数的下标为(start+m-1)%len,
输出其下标对应的编号, - 然后从这个下标开始到数组尾部数组元素往前移一位,
需要正序更新元素,同时这里的数组大小由于该元素已经出圈,
长度减一,最后更新起点下标为当前报数的下标 - 外层循环表示出圈的次数,
当圈内人数为0时跳出循环
这是我当时的写法,还是具有一定的可读性的,
当然使用vector更为方便。
不过Python List相当于C++中的vector,
这里就另外给一个python的代码。
c
#include <iostream>
using namespace std;
int main()
{
int n, m;
while (cin >> n >> m)
{
if (n == 0 && m == 0)
break;
int i, j, start = 0, len;
int a[n];
len = n;
for (i = 0; i < n; i++)
a[i] = i + 1;
for (i = 0; i < n; i++)
{
printf("%d ", a[(start + m - 1) % len]);
for (j = (start + m - 1) % len; j < len - 1; j++)
a[j] = a[j + 1];
start = (start + m - 1) % len;
len--;
}
}
return 0;
}
python
n,m=map(int,input().split())
circle=[]
for i in range(n):
circle.append(i+1)
print("")
t=0
i=-1
while len(circle)>=1:
t+=1
if i>=len(circle)-1:
i=0
else:
i+=1
if t==m:
print(circle[i],end=' ')
circle.remove(circle[i])
t=0
i-=1
解法三:【链表】模拟链表
- 为了方便, 这里用数组模拟链表。
- 我们知道这是一个环,所以可以用环形链表来模拟
- 当计数器到m需要出队该元素时,需要从链表中删除该元素
- 删除元素需要使该元素的上一个元素指向该元素的下一个元素
,所以我们还需要一个变量来存储当前元素的前驱元素 - 链表是由数据域和指针域两部分组成的,
由于该问题中编号是从1开始依次递增的
,所以只需要一个数组记录指针域,
存储该元素的下一个元素,
通过一个变量now来表示当前元素,
若需要访问下一个元素,
更新now变成now的下一个元素,
所以说数组中下标对应当前元素的编号,
数组的值为下一个元素的编号
除了需要像之前的解法维护一个计数器变量,怎么判断跳出循环呢?
我们在出圈一个元素时同步将它的下一个元素置为0,
这样如果所有元素都已经出圈,最后一个元素会被循环多次,
它的下一个元素也会变成0,
但在环非空时是无论如何不会访问到下一个元素是0为元素的,
若出现了,跳出循环。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 1e5 + 10;
#define endl '\n'
int a[N];
int main()
{
ios::sync_with_stdio(false);
cin.tie(0);
int n, m;
cin >> n >> m;
//a数组是当前编号的下一个编号
for (int i = 1; i < n; i++)
a[i] = i + 1;
a[n] = 1; //n元素的下一个元素是1号
int cnt = 0, now = 1, prev = n; //同时创建prev保存前驱元素
while (a[now])
{
// cout << prev << " " << now << " " << a[now] << endl;
cnt++;
if (cnt != m)
{
prev = now;
now = a[now];
}
else
{
cout << now << " ";
int nxt = a[now]; //需要临时变量存储下一个元素
//如果该元素需要出队而前驱元素未出队,更新前驱元素的下一个元素
if (a[prev])
a[prev] = nxt;
//从链表中移除当前元素
a[now] = 0;
prev = now; //如果m大于1不需要这句
now = nxt;
cnt = 0;
}
}
return 0;
}
虽然约瑟夫问题解决的思路不是很难,
但在实际编写代码中,
需要对循环结构加以控制是不容易的,
只有通过自己的逐步调试,
才能掌握程序设计的方法和精髓,
真正地解决此类问题。