背景
有编号从1到N的N个小朋友在玩一种出圈的游戏。开始时N个小朋友围成一圈,编号为I+1的小朋友站在编号为I小朋友左边。编号为1的小朋友站在编号为N的小朋友左边。首先编号为1的小朋友开始报数,接着站在左边的小朋友顺序报数,直到数到某个数字K时就出圈。直到只剩下1个小朋友,则游戏完毕。
1、求最后出圈的人
为了方便进行取模,假设还剩n个人的时候编号为0~n-1
令一个人出圈以后从他的下一个开始编号为0,一直到他的前一个编号为n-2,那么不难发现,
每个人的旧编号都是新编号加上k mod n的结果,这是因为被删去的数在新的编号法则中编号为n-1。这相当于将k~(n-1)~0~k-1重新编号成0~n-1,再删去n-1,对所有剩余的编号都没有影响
这样,最后出圈的人在仅剩一人时编号为0,每次+k mod 人数 可以递推得剩更多人时胜利者的编号,直至n人
时间效率:O(n)
题目链接:
La3882
#include<cstdio>
using namespace std;
int n,k;
int f;
int main()
{
scanf("%d%d",&n,&k);
while(n&&k)
{
f=0;
for(int i=2;i<=n;i++)
f=(f+k)%i;
printf("%d\n",f+1);
scanf("%d%d",&n,&k);
}
}
2、求倒数第M个出圈的人
与上面的思路类似。
还剩M个人的时候,从0开始数,下一个出圈的编号为(k-1)%M,然后向上递推即可
时间效率 O(n)
题目链接:
LA4727
#include<cstdio>
#include<algorithm>
#include<cstring>
using namespace std;
int T,n,k,f;
int main()
{
scanf("%d",&T);
while(T--)
{
scanf("%d%d",&n,&k);
f=(k-1)%3;
for(int i=4;i<=n;i++)
f=(f+k)%i;
printf("%d ",f+1);
f=(k-1)%2;
for(int i=3;i<=n;i++)
f=(f+k)%i;
printf("%d ",f+1);
f=0;
for(int i=2;i<=n;i++)
f=(f+k)%i;
printf("%d\n",f+1);
}
}
3、输出完整的出圈顺序
1、模拟法
使用一个环状链表,真正进行报数操作,由于并不困难,在这里不给出代码
时间效率:O(n^2)
2、线段树
建一棵权值线段树,每个节点存储大小在【l,r】之间的数的个数,则可以在线段树上二分,找出剩余n个数字中的第k%n个然后删去
时间效率:O(nlogn)
#include<cstdio>
#include<algorithm>
#include<cstring>
using namespace std;
#define lch t*2
#define rch t*2+1
#define mid (l+r)/2
#define maxn 30005
int n,m;
int size[maxn*4];
void del(int x,int t=1,int l=1,int r=n)
{
size[t]--;
if(l==r) return;
x<=mid?del(x,lch,l,mid):del(x,rch,mid+1,r);
}
int kth(int k,int t=1,int l=1,int r=n)
{
if(l==r) return l;
if(k<=size[lch])
return kth(k,lch,l,mid);
else return kth(k-size[lch],rch,mid+1,r);
}
void build(int t=1,int l=1,int r=n)
{
size[t]=r-l+1;
if(l==r)return;
build(lch,l,mid);
build(rch,mid+1,r);
}
int main()
{
scanf("%d%d",&n,&m);
build();
int lastdel=0;
while(size[1])
{
lastdel=(lastdel+m)%size[1];
if(lastdel==0) lastdel=size[1];
int ans;
del(ans=kth(lastdel));
lastdel--;
printf("%d ",ans);
}
}