约瑟夫环问题:
有n个人,编号依次为:1,2,3...n,这n个人按照编号顺序排成一个圈,现指定一个起始编号start(1<=start<=n)和退出编号m(m>=1),表示从第start个人往后循环依次按照1,2...m的顺序叫号,叫到m的人退出当前圈,并且下一个人从0开始叫号,并下一次叫到m的人继续退出当前圈,以此往复,现在求最后一个退出的人的编号是多少?
解法一:
初始化数组,表示每个人是否还在当前圈中,之后模拟叫号的流程,并不断记录退出人的总数,当退出人的总数等于n,则得到最后一个退出的人的编号。
#include<bits/stdc++.h>
using namespace std;
int main()
{
//n个人,1-n编号,从star编号开始报第一个数1
cout<<"编号1~n"<<endl;
int n,m,start;
cin>>n>>m>>start; //人数,出列编号,start>=1
bool *is_alive = new bool[n+1];
for(int i=0;i<=10;i++)
is_alive[i]=true;
int count=0,quit=0,indx=start-1;
while(true)
{
indx=indx % n +1;
if (!is_alive[indx]) //当前编号的人已经退出了,那么开始下一个
continue;
quit++;
if (quit==m)
{
is_alive[indx]=false;
quit=0;
count++;
cout<<indx<<endl;
if(count==n)
{
cout<<"最后自杀的人编号是:"<<indx;
break;
}
}
}
return 0;
}
当前解法复杂度为O(nm)
解法二(仅考虑start=1的情况):
将编号全部减一变成0~n-1范围,且叫号从0叫到m-1。称此时的圈为n阶约瑟夫环,则所有人的编号依次为:
0,1,2,3,4,5...n-2,n-1且第一次出列的人的编号则是(m-1)%n,那么在第一个人出列之后,从他的下一个人又开始从0开始报数,
这里设下一个人的编号 m%n用k代替,则在第一个人出列之后,k是下一次新的编号序列的首位元素,得到的新的编号序列为:
k,k+1,k+2,k+3...n-2,n-1,0,1,2...k-3,k-2 (k-1第一次退出)
那么在这个新的序列中,第一个人依旧是从0开始报数,那么在这个新的序列中,如果不退出的话每个人报的相应数字为:
0,1,2,3....n-2
那么第二次每个人报的相应数字与第一次时自己相应的编号对应起来的关系则为:
0 --> k
1 --> k+1
2 --> k+2
...
n-2 ---> (k+n-2)%n
到了这里所有的作用只是得到n阶约瑟夫环和n-1阶约瑟夫环的下标对应关系:
即n-1阶约瑟夫环中某人的编号p映射到上一阶(即n阶约瑟夫环)中的编号是(k+p)%n=(m%n+p)%n=(m+p)%n
注意这里仅仅是上下阶约瑟夫环编号的对应关系,还未牵扯到最后一个退出的人编号 。
(这里的n始终都是相邻中高阶约瑟夫环的总人数。)
和"最后一个退出的人"有关的如下:
因为在n阶约瑟夫环中最后一个退出人就是最后1阶约瑟夫环的那个人,其编号在1阶约瑟夫环中的编号是0(因为就此时就只有一个编号0),这样根据公式不断向上反推,就能得到最后一个退出的人在n阶约瑟夫环中的编号 。
令f(x)为最后一个退出的人在第x阶约瑟夫环中的编号
则初始化:f(1)=0 (因为1阶约瑟夫环中只有唯一一个人,编号就是0)
转移方程:f(x)=(f(x-1)+m)%x
#include<bits/stdc++.h>
using namespace std;
int main()
{
//n个人,1-n编号,从star编号开始报第一个数1
cout<<"编号1~n DP"<<endl;
int n,m;
cin>>n>>m; //人数,出列编号
int res=0;
for (int i=2;i<=n;i++)
res=(res+m)%i;
cout<<"最后一个退出的人编号:"<<res+1<<endl;
return 0;
}
思考1:
使用解法二时,倒数第g个人肯定是在g阶约瑟夫环中退出的,这样可以像最后一个人一样(g=1)时不断向上反推,得到原n阶约瑟夫环中第g个退出的人的编号。
#include<bits/stdc++.h>
using namespace std;
int main()
{
//n个人,1-n编号,从star编号开始报第一个数1
cout<<"编号1~n DP"<<endl;
int n,m;
cin>>n>>m; //人数,出列编号
//推广倒数第last_num个人退出的编号在原约瑟夫环中的编号
for(int last_num=1;last_num<=n;last_num++)
{
//倒数第 last_num个人在 last_num阶约瑟夫环中退出,
//而第在第last_num阶约瑟夫环中退出的人(第一个数到m的人)在本阶中的编号为(m-1)%last_num
int res_last_num= (m-1)%last_num;
for (int i=last_num+1;i<=n;i++)
res_last_num=(res_last_num+m)%i; //像之前一样,不断倒推得到第last_num个退出的人在原n阶约瑟夫环中的编号
cout<<"倒数第"<<last_num<<"个人退出的编号是:"<< res_last_num+1<<endl; //将编号回归到1-n范围
}
return 0;
}
思考2:
如果使用解法二且start>1时,如果计算?
当start >1时,除了第n-1阶约瑟夫环的编号转换到第n阶约瑟夫环编号有变化外,其余相邻阶的编号映射关系仍然是不变的。
这里如果使用解法二,仍然先将原来1~n的下标转为0~n-1范围,这时如果从下标start-1(起始下标也要跟着减一)开始叫号,那么就从0~n-1在不退出的情况下依次叫号为:
n-(start-1),n-(start-1)+1,...n-1,0,1,2,3...n-start,这时原编号Sorg和叫号Sdst对应关系为:
0 --> n-(start-1)
1 --> n-(start-1)+1
...
start-1 --> 0
start --> 1
...
n-1 --> n-start
即Sorg=(Sdst+start-1)+n)%n
再回到解法二/思考1,我们根据解法二/思考1得到的编号起始是Sdst,即第一次不退出叫号的编号,然后变换到原始的编号即使用Sorg和Sdst的关系即可。
#include<iostream>
using namespace std;
int main()
{
//n个人,1-n编号,从star编号开始报第一个数
cout<<"编号1~n DP"<<endl;
int n,m,start;
cin>>n>>m>>start; //人数,出列编号,start>=1
//推广倒数第last_num个人退出的编号在原约瑟夫环中的编号
for(int last_num=1;last_num<=n;last_num++)
{
int res_last_num= (m-1)%last_num; //倒数第 last_num个人在 last_num阶约瑟夫环中退出,其在last_num阶约瑟夫环中编号为res_last_num
for (int i=last_num+1;i<=n;i++)
res_last_num=(res_last_num+m)%i; //像之前一样,不断倒推得到第last_num个退出的人在原n阶约瑟夫环中的编号
cout<<"倒数第"<<last_num<<"个人退出的编号是:"<< (res_last_num+(start-1)+n)%n+1<<endl; //将编号回归到1-n范围
}
return 0;
}