约瑟夫问题是信息学奥赛中的一类经典且重要的题型,在平常的测试中屡屡出现。

  

  通常题设可抽象为:一开始有 $n $个人围成一个圈, 从 $1 $开始顺时针报数, 报出 $m $的人被踢出游戏.。然后下一个人再从$ 1 $开始报数,直到只剩下一个人。

个人在他身边,然而现在只剩他一个人。$Who$  $are$  $you$$?$  $Who$ $am$ $I$$?$  $Why$ $am$ $I$ $here$$? $走的越来越慢,人越来越少,可终于还是只剩一个了呢。他们围成一圈,随机了一个人作为$1$号,然后逆时针依次编号。$1$号开始报数,报到 $1$,他走了;然后$2$号开始报数,$2$号报了$1$,$3$ $i$的人出局。直到只剩他一个人。却早已不记得他自己是谁。

 

  针对不同的数据范围,可以存在如下几种做法:

1. $O(nm)$

  $O(nm)$的复杂度适用于$n,m$都在$30000$以内的情况,此类题型较少,例如“约瑟夫游戏”一题,$n,m<=30000$,由于随着游戏的不断进行,需要枚举的人数越少,所以复杂度实际低于$O(nm)$。算法思路:暴力模拟即可。

  

Java约瑟夫问题的实现 约瑟夫问题程序_#include

Java约瑟夫问题的实现 约瑟夫问题程序_#define_02

#include<bits/stdc++.h>
using namespace std;
int T,N,M; bool v[1000100];
void wk(){
    memset(v,0,sizeof(v));
    scanf("%d%d",&N,&M);
    int t=0,num=0,pos=1;
    while(1){
        if(v[pos]){
            ++pos;
            if(pos==N+1) pos=1;
            continue;
        }
        ++num;
        if(num==M){
            if(t==N-1){
                printf("%d\n",pos);
                return;
            }
            v[pos]=1,++t,num=0;
        }
        ++pos;
        if(pos==N+1) pos=1;
    }
}
int main(){
    scanf("%d",&T);
    while(T--) wk();
    return 0;
}

暴力模拟约瑟夫问题

 

2.$O(n)$

  $O(n)$算法已经适用于大多数约瑟夫问题,让$n<=1e7$的数据范围可以被轻松解决,考虑以任意一人为起点,选出第$m$个人后的编号变化,设起始$id==0$,选出第$m$个人后,$id->(id+m)$,再回归到原来的圆形,设$i$表示第$i$轮游戏,那么整体的公式即为$(id+m)$%$(n-i+1)$。倒序枚举即可。也可以用$dp$方式实现,或者正序枚举,将公式改变为$(id+m)$%$(i+1)$,最后答案即为$id+1$。

  

Java约瑟夫问题的实现 约瑟夫问题程序_#include

Java约瑟夫问题的实现 约瑟夫问题程序_#define_02

#include<bits/stdc++.h>
#define re register
using namespace std;
int T,n,ans,m;
inline int read(){
    re int a=0,b=1; re char ch=getchar();
    while(ch<'0'||ch>'9')
        b=(ch=='-')?-1:1,ch=getchar();
    while(ch>='0'&&ch<='9')
        a=(a<<3)+(a<<1)+(ch^48),ch=getchar();
    return a*b;
}
signed main(){
    T=read();
    while(T--){
        n=read(),m=read(),ans=0;
        if(m==1){printf("%d\n",n);continue;}
        for(re int i=n;i>=1;--i)
            ans=(ans+m)%(n-i+1);
        printf("%d\n",ans+1);
    }
    return 0;
}

O(n)递推约瑟夫问题

 

3.$O(mlogn)$

毒瘤出题人缘故,针对$n<=1e9,m<=1e5$类型的数据范围,我们不得不采用特别的递推方式,通过打表可以发现,保持$m$不变,$n$每加一,答案在模$n$意义下加$m$,注意:此时的$n$是一个变化的$n$,那么可以通过对$n$的递推处理,将$O(n)$级别的枚举,转化为在答案值域区间上的选择性跳跃,从而将以$n$为基础的算法转向以$m$为基础的算法,可以处理该类毒瘤问题。

  

Java约瑟夫问题的实现 约瑟夫问题程序_#include

Java约瑟夫问题的实现 约瑟夫问题程序_#define_02

#include<bits/stdc++.h>
#define int long long
#define re register
using namespace std;
int t,n,m;
inline int read(){
    re int a=0,b=1; re char ch=getchar();
    while(ch<'0'||ch>'9')
        b=(ch=='-')?-1:1,ch=getchar();
    while(ch>='0'&&ch<='9')
        a=(a<<3)+(a<<1)+(ch^48),ch=getchar();
    return a*b;
}
signed main(){
    t=read();
    while(t--){
        n=read(),m=read();
        re int now=1,ans=1,nxt;
        while(now<=n){
            nxt=(now-ans)/(m-1);
            if(now+nxt>=n){
                ans=ans+(n-now)*m;
                break;
            }
            now=now+nxt+1;
            ans=(ans+(nxt+1)*m-1)%now+1;
        }
        printf("%lld\n",ans);
    }
    return 0;
}

O(mlogn) 基于值域的约瑟夫问题

 

4.$O(log_m^n)$

  此类算法极其不常见,仅适用于$n$个人围成一圈,从$1$号开始依次报数,当报到$m$时,报$1$、$2$、…、$m-1$的人出局,下一个人接着从$1$开始报,保证$(n-1)$是$(m-1)$的倍数,最后剩的一个人获胜的情况。通过打表,可以发现,$f[m^a+m+1]=m$,其余的$f[n]$都满足$f[n][n-m+1]$,不妨$ n=m^a+(m-1)*k(m^a<n≤m^{a+1})$,则$f[n]=k*m$。时间复杂度$O(log_m^n)$

 

Java约瑟夫问题的实现 约瑟夫问题程序_#include

Java约瑟夫问题的实现 约瑟夫问题程序_#define_02

1 #include<cstdio>
 2 using namespace std;
 3 long long n,m;
 4 signed main(){
 5     long long i;
 6     scanf("%lld%lld",&n,&m);
 7     for(i=1;i<n/m;i*=m);
 8     printf("%lld\n",(n-i)/(m-1)*m);
 9     return 0;
10 }

约瑟夫问题

 

 

 

  至此,通过不同的数据范围选择不同的算法,一般的约瑟夫问题已经可以完全解决。

$Over$