据说著名犹太历史学家Josephus有过以下的故事:在罗马人占领乔塔帕特后,39 个犹太人与Josephus及他的朋友躲到一个山洞中,39个犹太人决定宁愿死也不要被敌人抓到,于是决定了一个自杀方式,41个人排成一个圆圈,由第1个人开始报数,每报数到第3人该人就必须自杀,然后再由下一个重新报数,直到所有人都自杀身亡为止。Josephus要他的朋友先假装遵从,他将朋友与自己安排在第16个与第31个位置,于是逃过了这场死亡游戏。
我们将问题简化,n个人围成一个圆圈,每个人的编号依次是1、2、3、……、n。从第一个人开始数数,数到m的人出圈。然后由出圈的下一个人重新数数,数到m的人又出圈……输入n和m(1 \le n \le 100,1 \le m \le 1,000,000),依次输出出圈人的编号。
一、借助数组记录圈里人的编号
最朴素的做法,借助数组记录圈里人的编号。模拟整个过程,有人出圈就将数组对应位置元素删除,可知模拟过程中整个数组实时记录了圈里人的所有编号。此外还需要借助一个变量模拟数数的过程,特别是有人出圈后,下一个人继续数数(而不是重新数数)。这里给出比较原始的参考程序:
//开始时n个人都在圈里,第i个人的编号i存储在a[i]中
for(i=1;i<=n;i++) a[i] = i;
//初始值设置为0(进入模拟过程执行p++,正好第1个人数1)
//n次循环n个人依次出圈,控制变量i正好是圈里人的数量
//当前圈里有i个人,从编号p的人开始,m个人依次数数
//发现是第(i+1)个人数数,修正成第1个人数数(构成一个圈)
//删除数组元素a[p](数组依次前移1位,覆盖掉a[p])
for(j=p;j<i;j++) a[j] = a[j+1];
//出圈后,下次数数要从新的a[p]开始,执行p--
//这样下次数数第一次执行p++时,正好这个位置的人数1
#include<iostream>
using namespace std;
const int N = 100;
//数组a记录圈里人的编号
int a[N+1];
int main()
{
int n,m,i,j,p;
cin>>n>>m;
//开始时n个人都在圈里,第i个人的编号i存储在a[i]中
for(i=1;i<=n;i++) a[i] = i;
//变量p用来记录数数的人对应数组中的下标
//初始值设置为0(进入模拟过程执行p++,正好第1个人数1)
p = 0;
//n次循环n个人依次出圈,控制变量i正好是圈里人的数量
for(i=n;i>=1;i--){
//当前圈里有i个人,从编号p的人开始,m个人依次数数
for(j=1;j<=m;j++){
p++;
//发现是第(i+1)个人数数,修正成第1个人数数(构成一个圈)
if(p==i+1) p=1;
}
cout<<a[p]<<" ";
//删除数组元素a[p](数组依次前移1位,覆盖掉a[p])
for(j=p;j<i;j++) a[j] = a[j+1];
//出圈后,下次数数要从新的a[p]开始,执行p--
//这样下次数数第一次执行p++时,正好这个位置的人数1
p--;
}
return 0;
}
#include<iostream>
using namespace std;
const int N = 100;
//数组a记录圈里人的编号
int a[N+1];
int main()
{
int n,m,i,j,p;
cin>>n>>m;
//开始时n个人都在圈里,第i个人的编号i存储在a[i]中
for(i=1;i<=n;i++) a[i] = i;
//变量p用来记录数数的人对应数组中的下标
//初始值设置为0(进入模拟过程执行p++,正好第1个人数1)
p = 0;
//n次循环n个人依次出圈,控制变量i正好是圈里人的数量
for(i=n;i>=1;i--){
//当前圈里有i个人,从编号p的人开始,m个人依次数数
for(j=1;j<=m;j++){
p++;
//发现是第(i+1)个人数数,修正成第1个人数数(构成一个圈)
if(p==i+1) p=1;
}
cout<<a[p]<<" ";
//删除数组元素a[p](数组依次前移1位,覆盖掉a[p])
for(j=p;j<i;j++) a[j] = a[j+1];
//出圈后,下次数数要从新的a[p]开始,执行p--
//这样下次数数第一次执行p++时,正好这个位置的人数1
p--;
}
return 0;
}
很容易发现,每次m个人数数,其实可以不用一个循环来模拟,这里是可以直接计算出来出圈人的下标:
//开始时n个人都在圈里,第i个人的编号i存储在a[i]中
for(i=1;i<=n;i++) a[i] = i;
//初始值设置为0(进入模拟过程执行p++,正好第1个人数1)
//n次循环n个人依次出圈,控制变量i正好是圈里人的数量
//当前圈里有i个人,从编号p的人开始,m个人依次数数
//删除数组元素a[p](数组依次前移1位,覆盖掉a[p])
for(j=p;j<i;j++) a[j] = a[j+1];
//出圈后,下次数数要从新的a[p]开始,执行p--
//这样下次数数第一次执行p++时,正好这个位置的人数1
#include<iostream>
using namespace std;
const int N = 100;
//数组a记录圈里人的编号
int a[N+1];
int main()
{
int n,m,i,j,p;
cin>>n>>m;
//开始时n个人都在圈里,第i个人的编号i存储在a[i]中
for(i=1;i<=n;i++) a[i] = i;
//变量p用来记录数数的人对应数组中的下标
//初始值设置为0(进入模拟过程执行p++,正好第1个人数1)
p = 0;
//n次循环n个人依次出圈,控制变量i正好是圈里人的数量
for(i=n;i>=1;i--){
//当前圈里有i个人,从编号p的人开始,m个人依次数数
//计算本次出圈(数到m的人)的下标p
p = (p+m)%i;
if(p==0) p = i;
cout<<a[p]<<" ";
//删除数组元素a[p](数组依次前移1位,覆盖掉a[p])
for(j=p;j<i;j++) a[j] = a[j+1];
//出圈后,下次数数要从新的a[p]开始,执行p--
//这样下次数数第一次执行p++时,正好这个位置的人数1
p--;
}
return 0;
}
#include<iostream>
using namespace std;
const int N = 100;
//数组a记录圈里人的编号
int a[N+1];
int main()
{
int n,m,i,j,p;
cin>>n>>m;
//开始时n个人都在圈里,第i个人的编号i存储在a[i]中
for(i=1;i<=n;i++) a[i] = i;
//变量p用来记录数数的人对应数组中的下标
//初始值设置为0(进入模拟过程执行p++,正好第1个人数1)
p = 0;
//n次循环n个人依次出圈,控制变量i正好是圈里人的数量
for(i=n;i>=1;i--){
//当前圈里有i个人,从编号p的人开始,m个人依次数数
//计算本次出圈(数到m的人)的下标p
p = (p+m)%i;
if(p==0) p = i;
cout<<a[p]<<" ";
//删除数组元素a[p](数组依次前移1位,覆盖掉a[p])
for(j=p;j<i;j++) a[j] = a[j+1];
//出圈后,下次数数要从新的a[p]开始,执行p--
//这样下次数数第一次执行p++时,正好这个位置的人数1
p--;
}
return 0;
}
上面的代码计算本次出圈人的下标使用的核心代码p = (k+m)%i;
使用了模运算,原因是随着出列人数的增多,k+m会大于i。其实原始数据输入时也有可能m远大于n,即使是第1段示例代码借助循环来模拟每次数数,也完全不用每次数到m。
二、借助数组记录每个人的状态(是否出圈)
上面使用数组来记录圈里人的编号,当有人出圈时,要借助大量的数组元素前移来删除数组元素,降低了效率。其实我们还可以用数组来记录每个人的状态,a[p]记录编号为p的人是否出圈,0表示没有出圈,1表示出圈。此时要模拟整个过程,对于数组里的每个元素都要考察,发现没有出圈才数数。可以在循环里嵌套一个循环,一次性把这一轮出圈的人计算出来:
//a[p]记录编号为p的人是否已经出圈:0-没有出圈,1-已经出圈
//全局数组a所有元素会被自动初始化成0,也就表示最开始时所有人都没有出圈(都在圈里)
//注意每个人(不管有没有出圈)都要考察,但是只有还在圈里的人才数数
//p初始值设置成0,后面一进循环p++,正好考察编号为1的人
for(int i=n;i>=1;i--){ //n次循环,每次出圈一人
int tot = 0; //tot记录数数数到几
if(a[p] == 0) { //编号为p的人还在圈里
cout<<p<<" "; //编号为p的人出圈,输出p
#include<iostream>
using namespace std;
const int N = 100;
//a[p]记录编号为p的人是否已经出圈:0-没有出圈,1-已经出圈
//全局数组a所有元素会被自动初始化成0,也就表示最开始时所有人都没有出圈(都在圈里)
int a[N+1];
int main()
{
int n,m;
cin>>n>>m;
//变量p记录正在考察的人的编号
//注意每个人(不管有没有出圈)都要考察,但是只有还在圈里的人才数数
//p初始值设置成0,后面一进循环p++,正好考察编号为1的人
int p = 0;
for(int i=n;i>=1;i--){ //n次循环,每次出圈一人
int tot = 0; //tot记录数数数到几
while(tot!=m){
p++; //考察下一个人
if(p>n) p=1; //回到编号为1的人
if(a[p] == 0) { //编号为p的人还在圈里
tot++;
}
}
//出循环后,编号为p的人数到了m
a[p] = 1; //记录编号为p的人出圈
cout<<p<<" "; //编号为p的人出圈,输出p
}
return 0;
}
#include<iostream>
using namespace std;
const int N = 100;
//a[p]记录编号为p的人是否已经出圈:0-没有出圈,1-已经出圈
//全局数组a所有元素会被自动初始化成0,也就表示最开始时所有人都没有出圈(都在圈里)
int a[N+1];
int main()
{
int n,m;
cin>>n>>m;
//变量p记录正在考察的人的编号
//注意每个人(不管有没有出圈)都要考察,但是只有还在圈里的人才数数
//p初始值设置成0,后面一进循环p++,正好考察编号为1的人
int p = 0;
for(int i=n;i>=1;i--){ //n次循环,每次出圈一人
int tot = 0; //tot记录数数数到几
while(tot!=m){
p++; //考察下一个人
if(p>n) p=1; //回到编号为1的人
if(a[p] == 0) { //编号为p的人还在圈里
tot++;
}
}
//出循环后,编号为p的人数到了m
a[p] = 1; //记录编号为p的人出圈
cout<<p<<" "; //编号为p的人出圈,输出p
}
return 0;
}
m如果比当前圈内剩余人数(n-p)大很多,这里会数好多圈,效率低下,可以使用模运算来提高效率:
//a[p]记录编号为p的人是否已经出圈:0-没有出圈,1-已经出圈
//全局数组a所有元素会被自动初始化成0,也就表示最开始时所有人都没有出圈(都在圈里)
//注意每个人(不管有没有出圈)都要考察,但是只有还在圈里的人才数数
//p初始值设置成0,后面一进循环p++,正好考察编号为1的人
for(int i=n;i>=1;i--){ //n次循环,每次出圈一人
int md = m%i; //i是当前还在圈里的人数
int tot = 0; //tot记录数数数到几
if(a[p] == 0) { //编号为p的人还在圈里
cout<<p<<" "; //编号为p的人出圈,输出p
#include<iostream>
using namespace std;
const int N = 100;
//a[p]记录编号为p的人是否已经出圈:0-没有出圈,1-已经出圈
//全局数组a所有元素会被自动初始化成0,也就表示最开始时所有人都没有出圈(都在圈里)
int a[N+1];
int main()
{
int n,m;
cin>>n>>m;
//变量p记录正在考察的人的编号
//注意每个人(不管有没有出圈)都要考察,但是只有还在圈里的人才数数
//p初始值设置成0,后面一进循环p++,正好考察编号为1的人
int p = 0;
for(int i=n;i>=1;i--){ //n次循环,每次出圈一人
//md是修正后的本轮次要数到多少结束
int md = m%i; //i是当前还在圈里的人数
if(md==0) md = i;
int tot = 0; //tot记录数数数到几
while(tot!=md){
p++; //考察下一个人
if(p>n) p=1; //回到编号为1的人
if(a[p] == 0) { //编号为p的人还在圈里
tot++;
}
}
//出循环后,编号为p的人数到了m
a[p] = 1; //记录编号为p的人出圈
cout<<p<<" "; //编号为p的人出圈,输出p
}
return 0;
}
#include<iostream>
using namespace std;
const int N = 100;
//a[p]记录编号为p的人是否已经出圈:0-没有出圈,1-已经出圈
//全局数组a所有元素会被自动初始化成0,也就表示最开始时所有人都没有出圈(都在圈里)
int a[N+1];
int main()
{
int n,m;
cin>>n>>m;
//变量p记录正在考察的人的编号
//注意每个人(不管有没有出圈)都要考察,但是只有还在圈里的人才数数
//p初始值设置成0,后面一进循环p++,正好考察编号为1的人
int p = 0;
for(int i=n;i>=1;i--){ //n次循环,每次出圈一人
//md是修正后的本轮次要数到多少结束
int md = m%i; //i是当前还在圈里的人数
if(md==0) md = i;
int tot = 0; //tot记录数数数到几
while(tot!=md){
p++; //考察下一个人
if(p>n) p=1; //回到编号为1的人
if(a[p] == 0) { //编号为p的人还在圈里
tot++;
}
}
//出循环后,编号为p的人数到了m
a[p] = 1; //记录编号为p的人出圈
cout<<p<<" "; //编号为p的人出圈,输出p
}
return 0;
}
这样的处理方式避免了数组大量元素的移动,但是数组的每个元素(即使对应的人已经出圈)都要考虑是否数数,同样会耗费大量的时间。如果不用考察已经出圈的人,程序的效率还可以进一步提升,更优化的程序我们在后面的章节会介绍到。