问题背景
据说著名犹太历史学家 Josephus有过以下的故事:在罗马人占领乔塔帕特后,39 个犹太人与Josephus及他的朋友躲到一个洞中,39个犹太人决定宁愿死也不要被敌人抓到,于是决定了一个自杀方式,41个人排成一个圆圈,由第1个人开始报数,每报数到第3人该人就必须自杀,然后再由下一个重新报数,直到所有人都自杀身亡为止。然而Josephus 和他的朋友并不想遵从。首先从一个人开始,越过k-2个人(因为第一个人已经被越过),并杀掉第k个人。接着,再越过k-1个人,并杀掉第k个人。这个过程沿着圆圈一直进行,直到最终只剩下一个人留下,这个人就可以继续活着。问题是,给定了和,一开始要站在什么地方才能避免被处决?Josephus要他的朋友先假装遵从,他将朋友与自己安排在第16个与第31个位置,于是逃过了这场死亡游戏。
问题研究
1.问题描述:n个人围成一圈(编号从1到n),从第1个人开始报数,报到m的人出列,从下一个人再重新报数,报到m的人出列,如此下去,直至所有人都出列。求最后一个出列的人的编号。例如:
N=6 M=5 开始:1 2 3 4 5 6 结果:5 4 6 2 3 1 :最后一个编号为1
2.数据结构
捋清楚待研究的问题之后,考虑数据结构。按照要求我们可以方便的利用一个数组去存储对应数据。每一个数组元素类型为:
enum status{
Die, //0
Live //1
};
typedef struct element{
size_t index;
enum status stu;
}element;
index标记每个人的编号,枚举变量stu标记每个人的状态(Die or Live)。所以我们直接可以用一个
private:
vector<element> _arr;
来组织所有参与游戏的选手。
源码
Joseph.h
#ifndef _JOSEPH_H_
#define _JOSEPH_H_
#define MaxSize 100
#include <iostream>
using namespace std;
#include <vector>
enum status{
Die, //0
Live //1
};
typedef struct element{
size_t index;
enum status stu;
}element;
//应该是不需要先完整的实现循环队列的操作,目前分析并不涉及到队列的重新插入操作
class Joseph{
//typedef struct element element;
public:
Joseph(int value);
int calc(int index,int M); //计算每一轮需要淘汰的index
void clean(int index,int &step); //清除需要淘汰项
bool check(int num); //检查是否退出
void func(int M,int num);// 核心函数;M是间隔数字
bool IsLegal(size_t Index); //判断元素合法性
int add(int& Index); //合法的+1
void print();
private:
vector<element> _arr;
vector<int> _res;
int N;
};
#endif
Joseph.cpp
#include "joseph.h"
Joseph::Joseph(int value)
{
_arr.resize(value);
N=value;
for(int i=0;i<value;++i){
_arr[i].index=i+1;
_arr[i].stu=Live;
}
}
bool Joseph::check(int num){
int count=0;
for(int i=0;i<_arr.size();++i){
if(_arr[i].stu==Live){
++count;
}
}
if(count>num)
return false;
if(count==num)
return true;
}
bool Joseph::IsLegal(size_t Index){
if(_arr[Index-1].stu==Live)
return true;
return false;
}
//这个add操作也应该考虑剩余元素的状态
int Joseph::add(int& Index){ //6 ---》1
int N=_arr.size();
Index=++Index;
if(Index>N){
Index%=N;
}
while(_arr[Index-1].stu==Die){
++Index;
if(Index>N){
Index%=N;
}
}
return Index;
}
void Joseph::clean(int index,int &step){
_arr[index-1].stu=Die;
_res.push_back(index);
//step不能单纯的++,要先进行判断
step=add(index); //每个位置的+1操作都需要用add()接口去替换
while(_arr[step-1].stu==Die){
int temp=add(step);
}
}
void Joseph::print(){
/*for(int i=0;i<_res.size();++i){
cout<<_res[i]<<" ";
} */
//cout<<_res.back()<<endl;
//N=6 M=5 开始:1 2 3 4 5 6 结果:5 4 6 2 3 1
vector<int>v(100);
for(int i=0;i<_res.size();++i){
v[ _res[i] ]++;
}
int j=0;
for(j=1;j<=N;++j){
if(v[j]==0){
cout<<j<<" ";
continue;
}
}
}
int Joseph::calc(int index,int M){ //6,5--->4 //确定下一个要杀死的人的位置
int count=1;
int tmp=0;
while(1){
if(_arr[index-1].stu==Live){
tmp=add(index);
++count;
}
if(IsLegal(tmp) && count==M )
break;
}
return index;
}
void Joseph::func(int M,int num){ //num是最后保留的数字
//游戏开始直到只剩下一个人存活结束
int _count=N-num;
int _step=1; //下一轮报数的第一个人
int _index=0;
int N=_arr.size();
int tmp=M;
//N=6 M=5 开始:1 2 3 4 5 6 结果:5 4 6 2 3 1
//41 :3 tip:数组下标是从0开始,对应的index+1;
//123456789 2 4 6 8
//Bug: 如果说M=1
while(M<N && _count<=1){ //循环条件与num的限制也有关系
clean(M,_step);
//2468 369
M=M+tmp;
++_count;
}
while(!check(num)){
_index=calc(_step,tmp); //预期:_index=4 _step=6 ,tmp=5
clean(_index,_step); //_step=1
}
}
Main.cpp
#include <iostream>
#include "joseph.h"
//using namespace std;
int main(){
//测试用例不能全部达标,
//优化,输入最后存活的x人编号,x=?; 改进一下check()函数
Joseph jos(41);
jos.func(3,2);
jos.print();
system("pause");
return 0;
}
运行结果:
代码调用解读:
1.第一行我们先创建一个Joseph类对象,传参一个int类型的值用于构造函数初始化。
Joseph jos(41); //41:对应有41个人参与游戏
2.第二行是算法的核心部分。调用func成员函数执行,第一个参数是间隔号,也就是报数的最大值。第二个参数是保留人数。
jos.func(3,2);
意味着报数到 3 截至,游戏进行直到剩余 2 人存活。
3.第三行就是输出剩余存活人的编号
jos.print();
Tip: 核心的算法也就是在func()函数中,代码并不长,大家可以参照源代码逐步理解。代码已经通过测试,如果有什么疑问或者发现什么bug也欢迎评论区补充指正。