约瑟夫问题:
有n个人,编号为1~n,从第一个人开始报数,从1开始报,报到m的人会死掉,然后从第m+1个人开始,重复以上过程。在死了n-1个人后,问最后一个人的编号是?
题目传送门
暴力都想不到就真是让人折服了。
暴力的话大模拟即可,不是重点,贴份代码就算了。
#include
int n,k;
struct node{
int x;
node *next;
node(int c):x(c),next(NULL){}
};
node *first=NULL;
int main()
{
scanf("%d %d",&n,&k);
node *last=new node(1);
first=last;
for(int i=2;i<=n;i++)
last->next=new node(i),last=last->next;
last->next=first;
for(int i=1;i<=n;i++)
{
for(int j=1;j<k;j++)
last=first,first=first->next;
printf("%d ",first->x);
last->next=first->next;
first=first->next;
}
}
题目传送门
这是进阶版的约瑟夫问题。
显然 O ( n k ) O(nk) O(nk) 的暴力无法满足我们的需要,所以我们要用更加优秀的做法。
发现这题和前一题有一个区别——不需要输出每次谁死了,也就是说,我们只关心最后谁活了下来。
为了方便,我们设一开始的 n n n 个人的编号为 0 0 0 ~ n − 1 n-1 n−1。
那么把第 k k k 个人干死之后(注意,第 k k k 个人的编号应该是 k − 1 k-1 k−1),队伍变成了: k , k + 1 , k + 2 , ⋯   , n − 2 , n − 1 , 0 , 1 , ⋯ k,k+1,k+2,\cdots,n-2,n-1,0,1,\cdots k,k+1,k+2,⋯,n−2,n−1,0,1,⋯
然后会从第 k k k 个人那里重复上面的过程。那么不妨将他们重新编号,使每个人的编号都减去 k k k,那么编号变成: 0 , 1 , 2 , ⋯   , n − 3 , n − 2 0,1,2,\cdots,n-3,n-2 0,1,2,⋯,n−3,n−2
那么现在就变成了一个 n − 1 n-1 n−1 阶的约瑟夫问题,假设我们知道 n − 1 n-1 n−1 阶约瑟夫问题的解(也就是最后谁会活下来),那么将这个解——即活下来的人的编号—— + k +k +k,就可以得到 n n n 阶约瑟夫问题的解了。
那么 n − 1 n-1 n−1 阶约瑟夫问题的解从哪里知道呢?从 n − 2 n-2 n−2 阶约瑟夫问题的解那里得到。于是就得到了一个 O ( n ) O(n) O(n) 的递推做法。
得到答案之后,还要 + 1 +1 +1,因为我们是从 0 0 0 开始编号的,然而题目要求从 1 1 1 开始编号。
从 0 0 0 开始编号是为了方便取模运算。
代码如下:
#include
int n,k;
int main()
{
scanf("%d %d",&n,&k);
int ans=0;
for(int i=1;i<=n;i++)
ans=(ans+k)%i;
printf("%d",ans+1);
}
题目传送门
发现这题的 n n n 变得巨大, O ( n ) O(n) O(n) 的优秀递推都会凉凉。
但是这题的 k k k 特别小,只有 1000 1000 1000,这就是这题的突破口了。
假如像上面一样枚举 i i i 的话,发现 i i i 大起来之后,很多时候 a n s + k ans+k ans+k 之后依然小于 i i i,也就是说,此时对 i i i 取模跟没取模的值是一样的,就是不需要进行取模,也就是说,每次可以直接加若干个 k k k,然后再对此时的 i i i 进行取模。
那么怎么知道每次要加多少个 k k k 呢?仔细想想,其实这就是一个追击问题:
a n s ans ans 每次加 k k k, i i i 每次加 1 1 1,问多少次之后 a n s ≥ i ans\geq i ans≥i。
那么这个就是一个简单的追击问题,然后模拟即可,这样的话程序就跑的飞快了。
代码如下:
#include
#include
#include
#define ll long long
ll n,k;
int main()
{
scanf("%lld %lld",&n,&k);
ll i=1,ans=0,p;
//本来i应该从0开始,但是0需要特判掉,懒得打特判,于是从1开始
while(i<n)
{
p=(i-ans)/(k-1)+((i-ans)%(k-1)!=0);//距离 除以 速度的差 得到 时间
//但是因为这里是整除,所以如果(i-ans)不是(k-1)的倍数的话,还需要多走一次,这个想想就能明白
if(i+p>n)p=n-i;//要判断是否大于n
ans+=p*k;//ans走
i+=p;//i走
ans%=i;//取模
}
printf("%lld",ans+1);//别忘了+1
}