这份题解大概是去年10月份打完组队训练写的,之前存在本地没发到博客,大概是昨天吃饭的时候和小伙伴聊到这题,和他口胡了一下大意,他叫我发给他(x),于是就有了这篇博客
K.Let the Flames Begin
题意
约瑟夫问题,(n)个人,报数到(k)出去,问第(m)个出去的人的初始位置,保证(min{m,k}leq 10^6)
做法
呜呜呜场内没写出来
开始处理之前先把编号改一改,改成(0,1,2,...,n-1)比较好搞(
- (f(n,m)=[f(n-1,m-1)+k] \%n)
- (f(n,m))表示(n)个人报数第(m)个出去的人的标号
- 转移成(n-1)个人,对应的第(m-1)个出去的人,从他开始继续报到(k),对应的就是第(m)个出去的人的标号
- 对(n)取模
- 边界条件(f(n,1)=(k-1)\%n)
基于这一点能想到对(m)小的情形直接(O(m))暴力
而对于(kleq m)的情形呢?
-
(k)小,(n)大((mleq n)嘛)意味着间隔很小的距离就跳一次,需要取模的情况会比较少!
即我们考虑从(n-m+1)往回推式子(f(n,m)=[f(n-1,m-1)+k]\%n) ,直观分析,需要跳很多步才会遇到一次需要取模的情况。 -
而我们只要算出最多能连续跳并且不触发取模的步数(t),然后连着跳(t+1)步再对相应的(n')进行取模,这个过程可以(O(1))地实现
代码
#include<bits/stdc++.h>
using namespace std;
#define rep(i,a,b) for(register ll i=(a);i<=(b);i++)
typedef long long ll;
inline ll solve(ll n,ll m,ll k)
{
if(k==1)return m-1;
if(m<=k)
{
ll res=(k-1)%(n-m+1);
rep(i,n-m+2,n)res=(res+k)%i;
return res;
}
ll len=n-m+1;
ll res=(k-1)%(n-m+1);
m--;
while(m>0)
{
ll t=(len-res)/(k-1);
if(m<=t)
{
res=(res+m*k)%(len+m);
m=0;
}else
{
res=(res+t*k)%(len+t);
res=(res+k)%(len+t+1);
m-=t+1;
len+=t+1;
}
}
return res;
}
int main()
{
int T;scanf("%d",&T);
ll n,m,k;
rep(t,1,T)
{
scanf("%lld%lld%lld",&n,&m,&k);
printf("Case #%lld: %lld
",t,solve(n,m,k)+1);
}
return 0;
}
核心代码是
ll len=n-m+1;
ll res=(k-1)%(n-m+1);
m--;
while(m>0){
ll t=(len-res)/(k-1);
if(m<=t){
res=(res+m*k)%(len+m);
m=0;
}else{
res=(res+t*k)%(len+t);
res=(res+k)%(len+t+1);
m-=t+1;
len+=t+1;
}
}
这一段。
用(len)表示当前的(n'),res记录答案,用递推式从小到大倒推回去
每次算出的最多能跳的步数(t)注意和(m)进行比较
*复杂度分析
(这一部分证明思路来自@Mobius Meow大神x)
- 考虑记(y)为当前的(n'),记(x)为当前进行迭代的次数。
- 对于(y)我们可以以(y=tk)进行估算,同时注意到(egin{aligned}frac{dy}{dx}=frac{tk-k}{k}=t-1end{aligned}) 这样一个近似的关系
- 得到方程(egin{aligned}frac{dy}{dx}-frac{y}{k}=-1end{aligned})
- 这个方程很好解,最后再把(y)换成题目里的n,得到(x=kln(n-k)-Ck),
- 所以整个算法时间复杂度为优秀的(O(klog(n)))