列队
像这种大题,我们可以先直接按正解想,如果没啥思路,就转而考虑部分分,部分分会给我们提示的
最小的部分分就不说了,纯暴力
看一下\(x_i=1\)的部分分
显然除了第一行,其他都是摆设,所以把第一行和最后一列放在一起考虑
然后就转化为了“谜一样的牛”这一道题目,时间复杂度\(O(nlogn)\)
然后考虑正解
我们刚才是单独考虑了第一行和最后一列,所以正解中也单独考虑最后一列
问题是行怎么办?我们依葫芦画瓢,也单独考虑每一行
稍微模拟一下就知道,对某一行,其他行的操作只会改变这一行最后一位人的编号(从这里也可以看出最后一列是非常特殊的,我们需要单独拿出来考虑),所以我们考虑对同一行一起处理
所以我们离线处理,将询问排序,按照\(x_i\)为第一关键字,询问编号为第二关键字,这样同一行的询问就被放在一起了
我们对同一行的询问建立一个树状数组(具体实现),对每个询问算出其真实想删除的人在数组里面的真实位置(“谜一样的牛”)
然后我们又把询问排序回去,依次考虑每个询问
在处理询问的过程中,我们用\(n\)个vector存储每一行历史上加入进来的人的编号
比如(\(m=4\))
1 2 3 4 7 5 3
表示从最开始到现在,进入过这一行的编号是以上,其中前四个是最开始的四个人(没有用实际的数据结构存储),后面三个是后面加入进来的(在vector当中),而在这个时刻,真实的队列编号可能是
1 2 7 3
或其他一些可能的情况
对当前的这个询问
如果有标记,就在最后一列上进行操作
否则考虑其询问的真实位置\(x\),如果比\(m\)小,那么就可以直接通过计算输出,否则由于没有标记,直接输出vector中第\(x-m\)个数(注意下标从\(0\)开始)
注意维护vector和最后一列即可
update 2024.5.14
说一个自己想出来的新做法:
对最后一列的处理仍然像上面这样,现在主要是处理行的问题
我们仍然考虑对每一行怎么处理,显然我们没办法存储每一个\(1\),于是我们可以反过来,存储每一个\(0\)
具体来说,对每一行开一个vector,存储每个\(0\)的位置(当然还要开一个vector存储新放进来的人),每次查询这一行时,我们直接对这个vector进行二分(显然真实位置是在两个\(0\)之间的某一个\(1\)),这个二分的check函数可以很轻松的通过vector的下标相减得出来(比如现在队列中一共有\(k\)个人,包含新加进来的人,然后存储\(0\)的位置的vector当前\(mid\)(下标)指向的值(注意不是这个vector的下标)是\(x\),那么就代表在下标\([1,k]\)中,下标\(x\)的值是\(0\),下标\([1,x]\)中\(1\)的总个数就是\(x\)减去\(mid\))
其实这两种做法的核心思路就是不去维护\(1\)而是去维护\(0\)以及用一个vector存储每一行新进入的人,因为我们知道这道题目\(n\times m\)太大了,遇到这种题目我们不可能去维护所有的信息,这种情况下我们只能维护每次操作所产生的新信息(这个思想非常常见);当然第一种做法的离线放在一起处理的原因是因为我们如果不离线处理的话,我们就需要开\(n\)个树状数组,但是我们显然不能开这么多树状数组,而我们有一个trick即给树状数组标上tag进行维护,于是我们想到将相同行的信息放在一起处理,这样就可以避免开\(n\)个树状数组并且可以利用tag进行优化了