K Let the Flames Begin
首先,对于经典的约瑟夫环问题,我们记 f ( n , m ) f(n,m) f(n,m)表示初始有 n n n个人,第 m m m个出队的人是谁(从0号开始报数)。则有递推式 f ( n , m ) = ( f ( n − 1 , m − 1 ) + k ) % n f(n,m)=(f(n-1,m-1)+k)\ \%\ n f(n,m)=(f(n−1,m−1)+k) % n 其中 k k k表示每报数 k k k次一个人出队,注意编号从0开始。
递推式的证明: 考虑现在有 n n n个人围成一圈,然后从0开始报数。假设第一个出队的人是 x x x ,这时还有 n − 1 n-1 n−1个人,我们从刚刚出去的那个人的下一个人从0重新编号,那么以当前局面重新开始,第 m − 1 m-1 m−1个出队的人是初始所求的同一个人,但编号不同,差多少呢?即 f ( n , m ) = ( f ( n − 1 , m − 1 ) + k ) % n f(n,m)=(f(n-1,m-1)+k)\ \%\ n f(n,m)=(f(n−1,m−1)+k) % n 。 +1 -1 细节手玩一下。
回到本题,由于 m , k m,k m,k可能会很大,但不会同时很大,当 m m m较小的时候( m < = k m<=k m<=k),直接递推即可。
下面考虑 m > k m>k m>k的情况,会发现模数大部分情况下远大于 k k k,也就是说可以用乘法代替多次加法,这样可以降低时间复杂度。具体代替多少次呢?考虑 f ( a , b ) = a n s f(a,b)=ans f(a,b)=ans, 假设代替 x x x次,则 f ( a + x , b + x ) = a n s + x ∗ k f(a+x,b+x) = ans+x*k f(a+x,b+x)=ans+x∗k 进行取模的等价条件是 a n s + x ∗ k > = a + x ans+x*k > =a+x ans+x∗k>=a+x,即 x > = a − a n s k − 1 x>=\frac{a-ans}{k-1} x>=k−1a−ans 即代替次数确定了(整除不整除,快加到 m m m了等细节注意一下即可)。
时间复杂度 O ( 感 觉 能 过 ) O(感觉能过) O(感觉能过)
code
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn=2e6+10;
ll m,n,k;
ll f[maxn];
int main()
{
//freopen("in.txt","r",stdin);
int t;
cin>>t;
int T=0;
while(t--)
{
T++;
cin>>n>>m>>k;
cout<<"Case #"<<T<<": ";
if(m<=k){//直接递推
ll tp=(n-m+1);
f[1] = (k-1) % tp; //编号从0开始
for(int i=2;i<=m;++i) f[i] = (f[i-1]+k)%(++tp);
cout<<f[m]+1<<endl;
}
else{
if(k==1) cout<<m<<endl;
else{
ll tp = n-m+1;
ll ans = (k-1) % tp; //编号从0开始
ll now = 1;
while(1){
if((tp-ans)%(k-1)==0){
ll x = (tp-ans)/(k-1) -1 ;
x = min(x,m-now);
ans += x*k;
now += x;
tp += x;
if(now==m) break;
ans =(ans + k) % (tp+1);
now +=1;
tp +=1;
if(now==m) break;
assert(now <=m);
}
else{
ll x = (tp-ans)/(k-1);
x = min(x,m-now);
ans += x*k;
now += x;
tp +=x;
if(now==m) break;
ans =(ans + k) % (tp+1);
now +=1;
tp +=1;
if(now==m) break;
assert(now <=m);
}
}
cout<<ans+1<<endl;
}
}
}
return 0;
}