wikioi-天梯-进入省队-线段树-1282:约瑟夫问题

有编号从1NN个小朋友在玩一种出圈的游戏。开始时N个小朋友围成一圈,编号为I+1的小朋友站在编号为I小朋友左边。编号为1的小朋友站在编号为N的小朋友左边。首先编号为1的小朋友开始报数,接着站在左边的小朋友顺序报数,直到数到某个数字M时就出圈。直到只剩下1个小朋友,则游戏完毕。

现在给定N,M,求N个小朋友的出圈顺序。

唯一的一行包含两个整数N,M。(1<=N,M<=30000

唯一的一行包含N个整数,每两个整数中间用空格隔开,第I个整数表示第I个出圈的小朋友的编号。

5 3

3 1 5 2 4

类型:线段树  难度:3

题意:n个人围成一圈,顺时针编号1-n,给出一个m,从第1个人开始数,数到第m个人就出圈,然后从出圈的下一个人继续数m个。。。求出圈的顺序。

分析:用线段树存储每个区间中剩余的人数,即在圈内则置为1,出圈的置为0,用realm表示当前查找的是还在圈中的第几个人,即线段树(1,x)区间的和为realm,那么x就是这次出圈的人。例:若当前的序列为10110,realm=2,那么这次出圈的人是3。

计算realm:realm初始化为1,每次更新realm = (realm-2+m)%i+1,i=n,...,1,这个式子的解释:首先考虑(realm+m)%i为下一个查找realm,发现每次去掉一个人后,相当于从realm之前的一个人开始数,所以变成(realm-1+m)%i,但是%i的结果可能为0,所以在最后+1,前面再-1。

查找x满足(1,x)的和为realm,且x为1:类似二分查找,先查找线段树左子树的和,若小于等于realm,则x在左子树,递归查找;若大于realm,查找右子树,realm更新为realm-sum[left],直到查找区间的左右边界相等,即返回结果。

代码:

#include<iostream>
#include<cstring>
#include<cstdlib>
#include<cstdio>
using namespace std;

#define lson l , m , rt << 1
#define rson m + 1 , r , rt << 1 | 1
const int maxn = 100010;
int n,m;
int sum[maxn<<2];

void PushUP(int rt) {		//把当前结点的信息更新到父结点 
	sum[rt] = sum[rt<<1] + sum[rt<<1|1];
}

void build(int l,int r,int rt) {	//建立线段树
	if (l == r) {
		sum[rt] = 1;
		return ;
	}
	int m = (l + r) >> 1;
	build(lson);
	build(rson);
	PushUP(rt);
}

void update(int p,int v,int l,int r,int rt) {	//单点增减
	if (l == r) {
		sum[rt] = v;
		return ;
	}
	int m = (l + r) >> 1;
	if (p <= m) update(p , v , lson);
	else update(p , v , rson);
	PushUP(rt);
}

int findpos(int len,int l,int r,int rt) {	//区间求和
	if (l==r) {
		return l;
	}
	int left = rt<<1;
	int m = (l+r)>>1;
	
	if(len <= sum[left])
        return findpos(len,lson);
	return findpos(len-sum[left],rson);
}

int main()
{
    scanf("%d%d",&n,&m);
    build(1,n,1);
    int realm = 1,now;
    for(int i=n; i>0; i--)
    {
        realm = (realm-2+m)%i+1;
        now = findpos(realm,1,n,1);
        printf("%d ",now);
        update(now,0,1,n,1);
    }
    printf("\n");
}


 

你可能感兴趣的:(线段树,WIKIOI,天梯)