[学习笔记]2018icpc沈阳K题-约瑟夫问题
这份题解大概是去年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)\)暴力
而对于\(k\leq m\)的情形呢?
-
\(k\)小,\(n\)大(\(m\leq 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\n",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\)进行估算,同时注意到\(\begin{aligned}\frac{dy}{dx}=\frac{tk-k}{k}=t-1\end{aligned}\) 这样一个近似的关系
- 得到方程\(\begin{aligned}\frac{dy}{dx}-\frac{y}{k}=-1\end{aligned}\)
- 这个方程很好解,最后再把\(y\)换成题目里的n,得到\(x=kln(n-k)-Ck\),
- 所以整个算法时间复杂度为优秀的\(O(klog(n))\)