背景
有编号从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); } }