【NOIP 2017】Day2 T3 列队
Problem
Description
\(Sylvia\) 是一个热爱学习的女孩子。
前段时间,\(Sylvia\) 参加了学校的军训。众所周知,军训的时候需要站方阵。
\(Sylvia\) 所在的方阵中有\(n*m\)名学生,方阵的行数为 \(n\),列数为 \(m\)。
为了便于管理,教官在训练开始时,按照从前到后,从左到右的顺序给方阵中 的学生从 \(1\) 到 \(n*m\) 编上了号码(参见后面的样例)。即:初始时,第 \(i\) 行第 \(j\) 列 的学生的编号是\((i-1)*m + j\)。
然而在练习方阵的时候,经常会有学生因为各种各样的事情需要离队。在一天 中,一共发生了 \(q\) 件这样的离队事件。每一次离队事件可以用数对\((x,y)(1≤x≤n,1≤y≤m)\)描述,表示第 \(x\) 行第 \(y\) 列的学生离队。
在有学生离队后,队伍中出现了一个空位。为了队伍的整齐,教官会依次下达 这样的两条指令:
向左看齐。这时第一列保持不动,所有学生向左填补空缺。不难发现在这条 指令之后,空位在第 \(x\) 行第 \(m\) 列。
向前看齐。这时第一行保持不动,所有学生向前填补空缺。不难发现在这条 指令之后,空位在第 \(n\) 行第 \(m\) 列。
教官规定不能有两个或更多学生同时离队。即在前一个离队的学生归队之后, 下一个学生才能离队。因此在每一个离队的学生要归队时,队伍中有且仅有第 \(n\) 行 第 \(m\) 列一个空位,这时这个学生会自然地填补到这个位置。
因为站方阵真的很无聊,所以 \(Sylvia\) 想要计算每一次离队事件中,离队的同学 的编号是多少。
注意:每一个同学的编号不会随着离队事件的发生而改变,在发生离队事件后 方阵中同学的编号可能是乱序的。
Input Format
输入共 \(q+1\) 行。
第 1 行包含 3 个用空格分隔的正整数 \(n,m,q\),表示方阵大小是 \(n\) 行 \(m\) 列,一共发 生了 \(q\) 次事件。
接下来 \(q\) 行按照事件发生顺序描述了 \(q\) 件事件。每一行是两个整数 \(x,y\),用一个空 格分隔,表示这个离队事件中离队的学生当时排在第 \(x\) 行第 \(y\) 列。
Output Format
按照事件输入的顺序,每一个事件输出一行一个整数,表示这个离队事件中离队学 生的编号。
Sample
Input:
2 2 3
1 1
2 2
1 2
Output:
1
1
4
Explanation
Explanation for Input
列队的过程如上图所示,每一行描述了一个事件。 在第一个事件中,编号为 \(1\) 的同学离队,这时空位在第一行第一列。接着所有同学 向左标齐,这时编号为 \(2\) 的同学向左移动一步,空位移动到第一行第二列。然后所有同 学向上标齐,这时编号为 \(4\)的同学向上一步,这时空位移动到第二行第二列。最后编号 为 \(1\) 的同学返回填补到空位中。
Range
测试点编号 | $n$ | $m$ | $q$ | 其他约定 |
---|---|---|---|---|
$1\sim 6$ | $\le 1000$ | $\le 1000$ | $\le 500$ | 无 |
$7\sim 10$ | $\le 5 \times 10^{4}$ | $\le 5 \times 10^{4}$ | ||
$11,12$ | $=1$ | $\le 10^{5}$ | $\le 10^{5}$ | 所有事件 $x=1$ |
$13,14$ | $\le 3\times 10^{5}$ | $\le 3\times 10^{5}$ | ||
$15,16$ | $\le 3 \times 10^{5}$ | |||
$17,18$ | $\le 10^{5}$ | $\le 10^{5}$ | $\le 10^{5}$ | 无 |
$19,20$ | $\le 3 \times 10^{5}$ | $\le 3 \times 10^{5}$ | $\le 3 \times 10^{5}$ |
数据保证每一个事件满足\(1\leq x \leq n,1\leq y \leq m\)
Algorithm
- 线段树/树状数组,就看你操作
骚不骚强不强。如果不会线段树操作,那么平衡树也是很可以的。。。
Notice
-
线段树的赋值维护仔细点= =
-
注意编号要开\(long\ long\)
-
思维一定要清晰,不然会被搞懵的。
Mentality
我觉得这是\(NOIP\)史上最\(shi\)的\(Day2~T3\)= =。\(18\)年的都没这个玩意\(shi\)。
首先看到数据范围,\(3e5\) 是挺坑的。。接着我们可以想到,因为每次出队的人必定都会去到最后一列,那么我们是不是可以维护最后一列,最后只需要输出最后一列所有历史状态的并呢?
当然,并不需要维护一个历史状态,其实我们只需要做到当某一个数向左看齐进入某一行的\(m-1\)列时不删除这个数,只是当做这个数不存在了,也就是让它对后面的数不再产生影响即可。可以理解为把出队元素全部放入一个栈。大致如下:
我们假设如下有五个操作:
OPT1:(2,1)出队
这里我们注意,对于新进入的元素\(3\),我们不应该把它叫做\(3\),而是一个指针 \(->1\),代表它指向总最后一列中的第一个位置,如下:
OPT2:(2,2)出队
同理,我们把 \(9\) 也写成一个指针 \(->3\) :
OPT3:(2,2)出队
OPT4:
OPT5:
对于总最后一列,我们这样处理:
- Step 0
依次由下往上将指针修改为指向位置的值:
-
Step 1
-
Step 2
由于统计所有出队的人即可,我们输出总最后一列中的\(m+1\) ~ \(m+q\)位即可
虽然关于向左看齐的维护用平衡树可以快乐地做到,但是不会啊。。。
接着想想办法维护这个总的最后一列。我们会发现,最后一列必定由原本的最后一列的元素加上每次离队的元素得到。那么接着考虑一下发现,最后一列里的元素可能会有重复。即原本已经出过队的人再一次出队。这我们可以让新离队补充的位置指向之前的位置,最后输出的时候判断更新即可。那么问题在于,我们如何维护最后一列?
我们可以观察到,用平衡树依次维护每次最后一列的向左看齐是可以做到保证能知道当时出列的那个数处于总最后列的哪个位置。那么线段树怎么做呢?我们考虑为每个数都赋一个权值,如果这个数在当前的第\(m\)列的\(n\)个数中,那么它的权值为\(1\),反之为\(0\)。每次向左看齐,如果一个元素脱离了第\(m\)列,那么它在总最后一列中的权值赋为\(0\),对于下一个进入最后一列的元素赋值为\(1\)。如果我们需要寻找此刻最后一列的第\(k\)个元素,我们只需要在总最后一列中寻找第\(k\)个权值为\(1\)的数。这个通过记录\(sum\)在线段树上查找即可。转换成之前的总最后一列的图,我们可以理解为:权值为1的点为红色,否则为黑色,那么序列中的第\(k\)个元素即为从下到上第\(k\)个红色的点。只不过对于每一行我们是横向维护,所以是从左到右第\(k\)个红色元素为当前第\(k\)个
首先,我们根据询问顺序来看,处理出每次询问后当前最后一列里出去了哪个数并记录在相关行,然后我们就可以知道每次向左看齐离开最后一列的数在总最后一列的位置了。
那么接着我们离线处理询问,以\(x\)为第一关键词,询问为第二关键词排序处理,对于每一行,我们单独进行处理。我们把每次向左看齐进来的数记录它在总序列中的位置。当我们处理一个询问时,就像对最后一列的处理一样维护一个\(01\)的权值串即可找出是在当前这一行第\(k\)个位置,这一行原来的数加上新添的数的总序列中的第几个。如果这个元素是这一行中本来就有的元素,我们直接将它加入总最后一列的相应位置,对于第\(i\)个询问,这个位置显然是\(n+i\)。否则,我们在总最后一列相应位置放一个标记,同时值为这个外来的元素之前本身在最后一列中的所在。
最后输出询问,假设我维护的最后一列为数组\(s\),指向标记为\(book\),那么输出如下:
for(int i=n+1;i<=n+q;i++)
{
if(book[i])
s[i]=s[s[i]];
printf("%lld\n",s[i]);
}
完成!
Code
#include <algorithm>
#include <cstdio>
#include <iostream>
using namespace std;
int n, m, Q, sum[2400001], x, adv[2400001], bj[2400001], L, R;
long long s[600001];
bool book[600001];
struct node {
int x, y, d, next;
} k[300002];
bool cmp(node a, node b) {
if (a.x == b.x) return a.d < b.d;
return a.x < b.x;
}
void pushdown(int o, int l, int r) {
if (adv[o] != -1) {
int mid = (l + r) / 2;
adv[o * 2] = adv[o * 2 + 1] = adv[o];
sum[o * 2] = (mid - l + 1) * adv[o];
sum[o * 2 + 1] = (r - mid) * adv[o];
adv[o] = -1;
}
}
void pushup(int o) { sum[o] = sum[o * 2] + sum[o * 2 + 1]; }
void add(int o, int l, int r) {
if (l >= L && r <= R) {
sum[o] = x * (r - l + 1);
adv[o] = x;
return;
}
pushdown(o, l, r);
int mid = (l + r) / 2;
if (mid >= L) add(o * 2, l, mid);
if (mid < R) add(o * 2 + 1, mid + 1, r);
pushup(o);
}
void query(int o, int l, int r) {
int mid = (l + r) / 2;
if (l == r) {
x = r;
return;
}
pushdown(o, l, r);
if (sum[o * 2] < R) {
R -= sum[o * 2];
query(o * 2 + 1, mid + 1, r);
} else
query(o * 2, l, mid);
pushup(o);
}
void work(int l, int r, int X) {
sum[1] = 0;
adv[1] = 0;
L = 1, R = m;
x = 1;
add(1, 1, m + Q);
int xl[300001];
for (int i = l; i <= r; i++)
xl[i - l] = k[i].next; //即将在询问前加入的r-l+1个外来数的位置指针
for (int i = l; i <= r; i++) {
R = k[i].y;
query(1, 1, m + Q); //查询第k个元素
if (x < m)
s[k[i].d + n] = (long long)(X - 1) * m + x; //队内元素加入总最后一列
else {
book[k[i].d + n] = 1; //标记这个位置是指针
s[k[i].d + n] = xl[x - m]; //指针赋值
}
L = x, R = x;
x = 0;
add(1, 1, m + Q); //权值清零
L = m + i - l + 1, R = m + i - l + 1;
x = 1;
add(1, 1, m + Q); //权值赋1
}
}
int main() {
cin >> n >> m >> Q;
for (int i = 1; i <= max(n, m) + Q; i++) adv[i] = -1;
L = 1, R = n, x = 1;
add(1, 1, n + Q);
for (int i = 1; i <= n; i++)
s[i] = (long long)i * (long long)m; //总最后一列的前m个数
for (int i = 1; i <= Q; i++) {
scanf("%d%d", &k[i].x, &k[i].y);
k[i].d = i;
R = k[i].x;
query(1, 1, n + Q); //查找第k个在序列中的元素
k[i].next = x;
L = x, R = x;
x = 0;
add(1, 1, n + Q); //元素出队,权值清零
L = n + i, R = n + i;
x = 1;
add(1, 1, n + Q); //元素入队,权值为1
}
sort(k + 1, k + Q + 1, cmp); //离线询问
int head = 1;
for (int i = 2; i <= Q + 1; i++)
if (k[i].x != k[i - 1].x) {
work(head, i - 1, k[i - 1].x); //对于每一行单独处理
head = i;
}
for (int i = n + 1; i <= n + Q; i++) {
if (book[i]) s[i] = s[s[i]]; //指针定位
printf("%lld\n", s[i]); //输出
}
}