【NOIP2017D2T3】列队(Splay)(树状数组/线段树)

题意

一个 N ∗ M N*M NM的方阵。 Q Q Q次操作每次操作取出一个人,空出一个位置。在空格右边的人都向左移动一格,使得空格在行末,然后把下面几行的最后一格都向上移动,使得空格出现在最右下角。然后把这个取出的人放到最右下角。

求每次取出的人的编号。 ( N , M , Q ≤ 3 ∗ 1 0 5 ) (N,M,Q\le 3*10^5) (N,M,Q3105)

思路

1 Splay

首先想到的肯定是平衡树,因为每次操作只会影响到某一行和最后一列,操作又是在序列里delete和insert,这不就是平衡树该干的事情吗?

然后这题维护序列的思路和《方伯伯的OJ》又非常相似。在没有访问到的时候,把一个区间的点合并在同一个点上,要是需要用到其中的点,就先把序列split再做操作。

那么大体思路就成型了。用n个Splay维护每行的前m-1个位置,再用一个Splay维护最后一列。空间最多 O ( N ) O(N) O(N)(大概4倍常数),时间 O ( N l o g N ) O(NlogN) O(NlogN)(超大常数)。

2 线段树/树状数组

思路和上面一样,只不过更暴力了一点

使用动态开点线段树实现上面Splay的操作。对每个点记录一个0/1表示这个点是否真实存在于当前的序列中,查找第k个用线段树上二分,删除只要单点修改把1改成0就行,插入直接在序列末插入,空间开 O ( N + Q ) O(N+Q) O(N+Q)

所以最后复杂度仍旧是 O ( N l o g N ) O(NlogN) O(NlogN),但是常数小了很多。

线段树的这种操作还是应该学一学啊。毕竟Splay调了一个早上。

代码

Splay

#include<bits/stdc++.h>
using namespace std;
#define rdi read<int>
#define rdl read<long long>
template<typename T> inline T read(){
	T x = 0, fh = 1;
	char c = getchar();
	while (c < '0' || c > '9'){if (c == '-') fh = -1; c = getchar();}
	while (c >= '0' && c <= '9') x = (x<<3)+(x<<1)+c-'0', c = getchar();
	return x*fh;
}
typedef long long LL;
const int N = 2e6 + 10;
int n, m, q;
int cnt, root[N], fa[N], ch[N][2], len[N], sum[N];
LL val[N];

inline void push_up(int u)
{
	sum[u] = sum[ch[u][0]] + sum[ch[u][1]] + len[u];
}

inline int son(int u){return ch[fa[u]][1] == u;}

inline void rotate(int u)
{
	int v = fa[u], w = fa[v];
	int ws = son(v), vs = son(u);
	ch[w][ws] = u;
	fa[v] = u;
	ch[v][vs] = ch[u][vs^1];
	fa[ch[u][vs^1]] = v;
	ch[u][vs^1] = v;
	fa[u] = w;
	push_up(v); push_up(u);
}

inline void splay(int u, int &goal)
{
	int rt = fa[goal];
	while (fa[u] != rt){
		if (fa[fa[u]] != rt) rotate(son(u) == son(fa[u]) ? fa[u] : u);
		rotate(u);
	}
	if (rt == 0) goal = u;
}

inline int newnode(LL _val, int _len)
{
	val[++cnt] = _val;
	sum[cnt] = len[cnt] = _len;
	return cnt;
}

void init()
{
	if (m > 1)
		for (int i = 1; i <= n; ++ i)
			root[i] = newnode(1LL * (i - 1) * m + 1, m - 1);
	root[0] = newnode(m, 1);
	for (int i = 2; i <= n; ++ i){
		ch[root[0]][1] = newnode(1LL * i * m, 1);
		fa[ch[root[0]][1]] = root[0];
		splay(ch[root[0]][1], root[0]);
	}
}

LL Find(int rk, int &rt)
{
	if (rk > sum[rt]) rk = sum[rt];
	if (rk == 0) rk = 1;
	int u = rt;
	while (u){
		if (rk <= sum[ch[u][0]]){u = ch[u][0]; continue;}
		rk -= sum[ch[u][0]];
		if (rk <= len[u]) break;
		rk -= len[u];
		u = ch[u][1];
	}
	splay(u, rt);
	return val[u] + rk - 1;
}

void delet(int rk, int u, int &rt)
{
	rk -= sum[ch[u][0]];
	if ((rk == 1 || rk == len[u]) && len[u] != 1){
		len[u]--; sum[u]--;
		if (rk == 1) val[u]++;
	}
	else{
		if (len[u] == 1){
			if (ch[u][0] == 0 && ch[u][1] == 0) rt = 0;
			else if (ch[u][0] == 0){rt = ch[u][1]; fa[ch[u][1]] = 0;}
			else if (ch[u][1] == 0){rt = ch[u][0]; fa[ch[u][0]] = 0;}
			else{
				Find(sum[ch[u][0]], rt);
				Find(len[u]+1, ch[rt][1]);
				ch[fa[u]][0] = 0;
				splay(fa[u], rt);
			}
		}
		else{
			Find(sum[ch[u][0]], rt);
			Find(len[u]+1, u == rt ? rt : ch[rt][1]);
			ch[u][1] = newnode(val[u] + rk, len[u] - rk);
			fa[ch[u][1]] = u;
			len[u] = rk - 1;
			splay(ch[u][1], rt);
		}
	}
}

void insert(int u, int rk, int &rt)
{
	if (rt == 0){rt = u; return;}
	Find(rk, rt);
	ch[rt][1] = u;
	fa[u] = rt;
	splay(u, rt);
}

int main()
{
	n = rdi(); m = rdi(); q = rdi();
	init();
	for (int i = 1; i <= q; ++ i){
		int x, y; x = rdi(); y = rdi();
		if (y < m){
			LL now = Find(y, root[x]);
			printf("%lld\n", now);
			Find(x, root[0]);
			int del = root[0];
			delet(y, root[x], root[x]);
			delet(x, root[0], root[0]);
			ch[del][0] = ch[del][1] = fa[del] = 0;
			insert(del, m-2, root[x]);
			insert(newnode(now, 1), n-1, root[0]);
		}
		else{
			LL now = Find(x, root[0]);
			printf("%lld\n", now);
			delet(x, root[0], root[0]);
			insert(newnode(now, 1), n-1, root[0]);
		}
	}
	return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值