【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;
}
View Code
后来看了题解才知道要使用约瑟夫环问题的数学解

但是这里是最后一个出列的人的情况(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;
}

 

 
posted @ 2019-05-13 09:38  里昂静  阅读(374)  评论(0编辑  收藏  举报