【2018沈阳现场赛k】
Let the Flames Begin- 约瑟夫环数学推导
题意:有 n 个人围成一个圈,从 1 开始报到第 k 个人出环,问第 m 个出环的人是谁,n、m、k <= 1e18 且 min(m,k)<= 2e6。
Sample Input
20 10 1 2 10 2 2 10 3 2 10 4 2 10 5 2 10 6 2 10 7 2 10 8 2 10 9 2 10 10 2 10 1 3 10 2 3 10 3 3 10 4 3 10 5 3 10 6 3 10 7 3 10 8 3 10 9 3 10 10 3
Sample Output
Case #1: 2 Case #2: 4 Case #3: 6 Case #4: 8 Case #5: 10 Case #6: 3 Case #7: 7 Case #8: 1 Case #9: 9 Case #10: 5 Case #11: 3 Case #12: 6 Case #13: 9 Case #14: 2 Case #15: 7 Case #16: 1 Case #17: 8 Case #18: 5 Case #19: 10 Case #20: 4
刚开始看到这道题的时候,感觉与计蒜客上有关队列的练习题十分类似,所以便利用队列的知识来解决,对于题目给出的测试案例,运行结果是正确的,可是提交上去
显示超时,百思不得其解,最终只有放弃。
超时代码:
#include<cstdio> #include<iostream> #include<queue> using namespace std; int n,k,m; int main() { int n,m,t,k,cas=0; cin>>t; while(t--){ queue<int>q; cin>>n>>m>>k; for(int i=1;i<=n;i++){ q.push(i); } int cnt=0,cntm=0; while(q.size()>0){ ++cnt; if(cnt==k){ int ans=q.front(); q.pop(); ++cntm; //cout<<"cntm="<<cntm<<endl; cnt=0; if(cntm==m){ cout<<"Case #"<<++cas<<": "<<ans<<endl; break; } }else{ int temp=q.front(); //cout<<"temp="<<temp<<endl; q.pop(); q.push(temp); } } } return 0; }
后来看了题解才知道要使用约瑟夫环问题的数学解,
但是这里是最后一个出列的人的情况(n个人第m个出列)
考虑一下n个人第m个出列,容易得出O(m)的递推公式 f[n][m] = (f[n-1][m-1] + k - 1)% n + 1,f[n][m]表示最终结果:n个人中第m个退出圈的人。其中,初始状态 f[n-m+1][1]容易得出(例如当n=200,m=50时,f[n-m+1][1]=f[151][1],在200个人中,想作为第50个退出圈的人,当退到49个人的时候,圈中还剩下151个人,所以对于第50个退出去的人,他在剩下的151个人中是作为第一个退出圈的人,现在的目的是要求得f[n][m],即f[200][50]),当 m 小的时候用该公式计算。考虑 k 小 m 大的情况下,递推式的取膜很多情况下没有用到,可以用乘法代替加法加速递推的过程:
当前状态为f[a][b] = c, 经过 x 次加法后的状态为 f[a+x][b+x] = c + k * x,假设经过 x 次加法之后需要取模,有
c + k * x > a + x → x > (a - c)/ (k - 1)
可以理解为:经过x次加法后需要取模,即加过后的值(退出圈的人的位置),比总人数a+x大,所以 c + k * x > a + x;
得到该不等式后便可以计算出另一种情况了,还要注意 k = 1 需要特判。
代码:
#include <bits/stdc++.h> using namespace std; #define ll long long const int INF = 0x3f3f3f3f; const double eps = 1e-6; const int MAXN = 2e6 + 10; const int MAXM = 1e8 + 10; const ll mod = 1e9 + 7; ll f[MAXN]; int main() { int cas = 1; int t; scanf("%d",&t); while(t--) { ll n,m,k; scanf("%lld%lld%lld",&n,&m,&k); printf("Case #%d: ",cas++); if(m <= k) { f[1] = k % (n - m + 1); if(f[1] == 0) f[1] = n - m + 1; for(ll i = 2; i <= m; i++) f[i] = (f[i - 1] + k - 1) % (n - m + i) + 1; printf("%lld\n",f[m]); } else { if(k == 1) printf("%lld\n",m);//k=1时,依次退出,因此直接输出m就行 else { ll a = n - m + 1, b = 1;//模拟初始状态 f[n-m+1][1] ll c = k % a, x = 0;//c的初始值为第一个退出圈的人的位置 if(c == 0) c = a; while(b + x <= m) {//最多判断到是否是第m个人出去,不能多退出人。 //b+x表示当前状态下,作为第b+x个人退出去 a = a + x, b = b + x, c = c + k * x; c %= a; if(c == 0) c = a; x = (a - c) / (k - 1) + 1; } c = c + (m - b) * k; c %= n; if(c == 0) c = n; printf("%lld\n",c); } } } return 0; }