约瑟夫问题详解

约瑟夫问题:

有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 n1

那么把第 k k k 个人干死之后(注意,第 k k k 个人的编号应该是 k − 1 k-1 k1),队伍变成了: 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,,n2,n1,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,,n3,n2

那么现在就变成了一个 n − 1 n-1 n1 阶的约瑟夫问题,假设我们知道 n − 1 n-1 n1 阶约瑟夫问题的解(也就是最后谁会活下来),那么将这个解——即活下来的人的编号—— + k +k +k,就可以得到 n n n 阶约瑟夫问题的解了。

那么 n − 1 n-1 n1 阶约瑟夫问题的解从哪里知道呢?从 n − 2 n-2 n2 阶约瑟夫问题的解那里得到。于是就得到了一个 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 ansi

那么这个就是一个简单的追击问题,然后模拟即可,这样的话程序就跑的飞快了。

代码如下:

#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
}

你可能感兴趣的:(其他知识的一些小结)