约瑟夫问题 线段树Timus OJ 1521

 

约瑟夫问题:n个人围成一圈,从1号开始报数,报到m就退出,剩下的人从下一个人开始继续报数。。。问最后剩下的是谁?更难的就是问所有人依次退出的顺 序。

例如,5个人,编号1..5,数3退出,以下是模拟

1 2 3 4 5 第一轮3退出,之后从4继续

1 2 4 5 第二轮1退出,从2继续

2 4 5 第三轮5退出,从2继续

2 4 第四轮2退出,从4继续

4 4退出

最终退出顺序:3 1 5 2 4

约瑟夫问题的难点在于,每一轮都不能通过简单的运算得出下一轮谁淘汰,因为中间有人已经退出了。因此一般只能模拟,效率很低。

现在考虑,如果不在原始编号上计算,而是每一轮都令所有剩下的人从左到右编号,例如上例,在第2轮开始时,场面上还剩下1、2、4、5,则给1新 编号1,2新编号2,4新编号3,5新编号4。

不妨称这个编号为“剩余队列编号”,这个编号的优点在于,如果知道这轮开始报数者的剩余队列编号,那么就可以直接运算得出此轮淘汰者的剩余队列编 号。而且淘汰掉他之后,形成新的剩余队列编号进行下一轮,此时开始报数的人在这个新剩余队列中的编号正好是上轮淘汰者在原剩余队列中的编号。(当然,边界 的话要循环一下)

因此只考虑剩余队列编号,这个过程是可以O(N)的时间完成的,即:我们可以用O(N)的时间得出每一轮淘汰者在当轮剩余队列中的编号位置。下面 用这个方法模拟上例,括号内为原始编号

1(1) 2(2) 3(3) 4(4) 5(5) --> 剩余队列编号3淘汰,对应原编号3

1(1) 2(2) 3(4) 4(5) --> 剩余队列编号1淘汰,对应原编号1

1(2) 2(4) 3(5) --> 剩余队列编号3淘汰,对应原编号5

1(2) 2(4) --> 剩余队列编号1淘汰,对应原编号2

1(4) --> 剩余队列编号1滔天,对应原编号4

因此,如果每次淘汰时,我们都能快速的通过被淘汰者在当前剩余队列中的编号求出原编号,就解决问题了。(显然不能每轮淘汰后修改编号,这样是平方 级别了,没有优化)

一个人在当前剩余队列中编号为i,则说明他是从左到右数第i个人,这启发我们可以用线段树来解决问题。用线段树维护原编号i..j内还有多少人没 有被淘汰,这样每次选出被淘汰者后,在当前线段树中查找位置就可以了。

例如我们有5个原编号,当前淘汰者在剩余队列中编号为3,先看左子树,即原编号1..3区间内,如果剩下的人不足3个,则说明当前剩余编号为3的 这个人原编号只能是在4..5区间内,继续在4..5上搜索;如果1..3内剩下的人大于等于3个,则说明就在1..3内,也继续缩小范围查找,这样既可 在logn时间内完成对应。问题得到圆满的解决。

具体细节请参见程序,题目在Timus OJ 1521.http://acm.timus.ru/problem.aspx?space=1&num=1521

 

其实也就是建好线段树,然后查找第m+1个数的位置,然后把这个位置到根的路径都-1.

 

#include <iostream>

using namespace std;

struct SegTree
{
	int l, r, m;
	int num; 
};

SegTree ltree[5000000];

int n, m, ln;

void init(int nowat, int tl, int tr)
{
	ltree[nowat].l = tl;
	ltree[nowat].r = tr;
	ltree[nowat].m = (tl + tr) >> 1;
	ltree[nowat].num = tr - tl + 1;
	if (tl < tr)
	{
		init(nowat * 2, tl, ltree[nowat].m);
		init(nowat * 2 + 1, ltree[nowat].m + 1, tr);
	}
}

void del(int nowat, int tw)
{
	--ltree[nowat].num;
	if (ltree[nowat].l < ltree[nowat].r)
	{
		if (tw <= ltree[nowat].m) del(nowat * 2, tw); 
		else del(nowat * 2 + 1, tw);
	}
}

int findcode(int tcode)
{
	int i = 1;
	int sum = 0;
	while (ltree[i].l < ltree[i].r)
	{
		if (sum + ltree[i+i].num < tcode)
		{
			sum += ltree[i+i].num;
			i = i + i + 1;              
		} else {	
			i = i + i;
		}
	}
	return ltree[i].r;
}

int main()
{
	while(scanf("%d%d",&n,&m) != EOF) {
		ln = 0;
		init(1, 1, n);
		int i, j, k;
		k = 0;
		for (i = 1; i <= n; ++i)
		{
			k = (k + m - 1) % (n - i + 1);
			j = findcode(k+1);
			printf("%d",j);
			if (i != n) printf(" ");
			del(1, j);
			if (i != n) k = k % (n - i);
		}
		printf("\n");
	}
	return 0;
}

 类似的还有pku3750

#include <iostream>
using namespace std;

const int N = 70;


struct SegTree
{
	int l,r;
	int m;
	int num;
};
SegTree t[N*4];
char s[N][30];

void create(int k,int l,int r)
{
	t[k].l = l;
	t[k].r = r;
	t[k].m = (l + r)>>1;
	t[k].num = r - l +1;
	if(l < r) {
		create(k+k,l,t[k].m);
		create(k+k+1,t[k].m+1,r);
	}
}

void del(int k,int v)
{
	--t[k].num;
	if(t[k].l < t[k].r) {
		if( v <= t[k].m ) del(k+k,v);
		else del(k+k+1,v);
	}
}

int findi(int v)
{
	int k = 1;
	int sum = 0;
	while(t[k].l < t[k].r) {
		if(sum + t[k+k].num < v) {
			sum += t[k+k].num;
			k = k+k+1;
		} else {
			k = k+k;
		}
	}
	return t[k].l;
}

int main()
{
	int n,i,j,w,m,k;
	while(scanf("%d",&n) != EOF) {
		for(i=1;i<=n;++i) {
			scanf("%s",s[i]);
		}
		scanf("%d,%d",&w,&m);
		create(1,1,n);
		k = w-1;
		for(i=1;i<=n;++i) {
			k = ( k + m - 1) % (n - i + 1);
			j = findi(k + 1);
			printf("%s",s[j]);
			//printf("%d",j);
			printf("\n");
			del(1,j);
			if(i != n) k = k % (n-i);
		}
	}
	return 0;
}

 

约瑟夫问题

从1~n中每隔e个出人,求出列顺序

1 求第i个出列的人

int jsp(int n,int e,int i)

{

if(i==0)return (e+n-1)%n+1;

if(n==1)return 1;//safe?

return (jsp(n-1,e,i-1)+e -1)%n +1;

}

2 求指定的数出列序号i

int jsp(int sum,int every,int num)//sum是总人数,every是每隔几个人

{

if(sum==1)return 1;

int now=every%sum;

if(now!=num%sum)//直接从0开始

{

if(num>now)return 1+jsp(sum-1,every,num-now);

return 1+jsp(sum-1,every,num+sum-every);

}

return 1;

}

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值