NOIP2017 列队
题目描述:https://www.luogu.org/problemnew/show/P3960
这题当时我在NOIP的时候只会暴力模拟,然后拿了30pts……
今天肖大佬给我们讲课的时候讲了这道题……正解有树状数组,有线段树,有splay……大佬给我们讲的是线段树做法。
首先先说一下,线段树怎么动态开点。这道题因为列队的长和宽都很大,达到了3*10^5,而询问次数也达到了3*10^5,直接把线段树全部建出来显然不现实。那我们就考虑动态开点,你只有在访问到他的时候你才会去使用这块空间。这个用指针比较好写(可是蒟蒻学不会指针)所以就学了数组模拟。对于每一次询问,你在线段树维护的时候只需要log大小的空间去维护,所以总空间并不是非常大。在老版线段树中,我们不记录一个点的左右儿子,而是直接用p<<1 , p<<1|1来做,但是其实大部分的空间是访问不到的,都会被浪费掉,而动态开点就是只使用要用的空间。对于每次传入的节点,如果这个节点目前不存在,就新建一个节点,并为其设置初值。
这个是怎么实现的呢,我们想,对于一棵线段树,我们在上面进行查找的时候,主要是用给定区间l,r和要找的位置,去进行二分查找,而每次传入的节点只是一个节点编号,他们之间不一定要存在什么关系(比如p<<1之类的),只要每次都记录一下当前节点的左右儿子编号都是谁就可以,这就大大节省了空间。
可能光说不好理解,上代码看一眼。
struct node//使用结构体记录元素信息 { ll lch, rch; ll cnt, id; node(ll _cnt = 0) : lch(0), rch(0), cnt(_cnt), id(0) { }//构造函数,对于每个新创的节点都赋初值 }pool[10000000]; if(!pool[u].lch) { pool[u].lch = ++pc,pool[pc] = node(mid - l + 1); pool[u].rch = ++pc,pool[pc] = node(r - mid); }//这是最主要的操作,其他都没什么。
感觉好像上代码也没咋理解……?不管啦,一会看完整的。
现在考虑怎么用线段树维护呢?我们观察后发现,能影响本次操作的,只有当前点,当前点所在行的最后一个点,和最后一列的最后一个点。所以我们直接建立n+1棵线段树,前n棵分别用于维护n行的前m-1个元素,最后一棵用于维护最后一列的n个元素。
首先考虑n=1的比较简单的情况。对于每一个人,当其在列队中,其位置所对应的值为1,否则位置所对应的值为0.这样的话,我们在询问某一行的第y位是谁的时候,我们只要找在线段树上前缀和为y的那个点所存储的节点编号是多少就可以了。
之后,当这个人出队的时候,我们将其位置所对应的值设为0,然后再把他重新插入到线段树的末尾。这样以后再次访问的时候,还是找前缀和为y的位置所对应的元素下表就可以了。(虽然会出现一些空间为空的状况,不过比起把他们全部前压可要容易得多)
这是n=1的时候,之后我们在考虑正常的情况应该怎么做,其实和n=1的地方,线段树上的操作都很相似,只是操作次数多了一些。(如果有大佬有更简洁的操作请告知)
大致是以下几步:
(1)找到要出队的孩子的位置和编号,并记录
(2)将出队的孩子的位置的属性全部清空
(3)找到最后一棵线段树(维护第m列)中与第x行(当前行)对应的孩子,即列队中第x行末尾的孩子的位置和编号,并记录
(4)把这个孩子压入这一行所对应的线段树中
(5)在最后一棵线段树上清空这个孩子的属性信息
(6)把刚刚出队的孩子压入最后一棵线段树的末尾
上面是,对于不是最后一列的元素的情况。否则的话更简单,因为只涉及到一棵线段树的操作。直接仿照n=1的情况就可以了。
注意一个是每次返回的节点编号有一些小小的改动。(代码里有注释)还有就是这题其实有可能节点编号爆int,开一下longlong吧。
这样不断地模拟,每次时间,空间复杂度都是log的,一波nlogn带走。
不过此题有很大的bug,就是你在动态开点的时候,到底应该怎么计算这个点他的权值到底是多少。
这棵线段树一开始应该是像这样的一个模型:111111111111111111111……000000000000000000
不过我们完全可以把后面的0都变为1,因为你并不会访问到那里。所以直接给新节点的权值赋为当前访问区间长度即可。不过这样的问题就是,你在把一个孩纸压入这一行所对应的线段树中的时候,你不能简单的使用m+i,而是要再开一个 变量记录一下当前加到哪里了。
这样就可以A了……(感谢肖大佬debug)
上代码看一下。
#include<cstdio> #include<cstring> #include<algorithm> #include<queue> #define rep(i,a,n) for(int i = a;i <= n;i++) #define per(i,n,a) for(int i = n;i >= a;i--) #define enter putchar('\n') using namespace std; typedef long long ll; const ll INF = 1e9; const int M = 300005; ll read() { ll ans = 0,op = 1; char ch = getchar(); while(ch < '0' || ch > '9') { if(ch == '-') op = -1; ch = getchar(); } while(ch >= '0' && ch <= '9') { ans *= 10; ans += ch - '0'; ch = getchar(); } return ans * op; } struct node { ll lch, rch; ll cnt, id; node(ll _cnt = 0) : lch(0), rch(0), cnt(_cnt), id(0) { } }pool[10000000]; ll pc, n, m, q, x, y,root[M],res,cur,used[M]; ll query(ll u, ll l, ll r, ll k, bool pd) { if (l == r) { if(pd) res = pool[u].id == 0 ? (x-1) * m + l : pool[u].id;//res记录出来的元素的编号 //如果 当前没有修改的话,直接返回其下标即可 else res = pool[u].id == 0 ? l * m : pool[u].id; return l; } ll mid = (l + r) >> 1; if(!pool[u].lch) { pool[u].lch = ++pc; pool[pc] = node(mid - l + 1);//注意这个地方!!! pool[u].rch = ++pc; pool[pc] = node(r - mid); } if (k <= pool[pool[u].lch].cnt) return query(pool[u].lch, l, mid, k, pd);//如果你要找的值小于等于其右子树的权值和,就在右子树中找 else return query(pool[u].rch, mid + 1, r, k - pool[pool[u].lch].cnt, pd);//否则在左子树中找,线段树二分的操作 } void modify(ll u, ll l, ll r, ll p, ll num, ll id) { if(l == r) { pool[u].cnt = num,pool[u].id = id;//把这个节点的权值和所对应的孩子编号修改 return; } ll mid = (l + r) >> 1; if(!pool[u].lch) { pool[u].lch = ++pc,pool[pc] = node(mid - l + 1); pool[u].rch = ++pc,pool[pc] = node(r - mid); } if(p <= mid) modify(pool[u].lch, l, mid, p, num, id); else modify(pool[u].rch, mid + 1, r, p, num, id); pool[u].cnt = pool[pool[u].lch].cnt + pool[pool[u].rch].cnt; }//常规的线段树修改(+动态开点) int main() { n = read(),m = read(),q = read(); rep(i,1,n) root[i] = ++pc,pool[root[i]] = node(m-1); root[n+1] = ++pc,pool[root[n+1]] = node(n);//先把每棵树的根节点都建出来,并且赋权值。 rep(i,1,q) { x = read(),y = read(); if(y <= m - 1)//如果没访问最后一列的元素 { ll p = query(root[x], 1, m + q, y, 1);//先找到出队的孩子的位置和编号 cur = res;//cur用于记录出队的孩子 modify(root[x], 1, m + q, p, 0, 0);//将出队的地方都设为0 p = query(root[n+1], 1, n + q, x, 0);//找到最后一列中与本行相对的孩子的编号 modify(root[x], 1, m + q, m + used[x]++, 1, res);//将这个孩子的编号压到本行线段树中 modify(root[n+1], 1, n + q, p, 0, 0);//把压到本行线段树中的孩子的地方都设为0 modify(root[n+1], 1, n + q, n + i, 1, cur);//最后再把出队的孩子压回来 printf("%lld\n", cur); } else//否则 { ll p = query(root[n+1], 1, n + q, x, 0);//找到出队孩子的编号 modify(root[n+1], 1, n + q, p, 0, 0); modify(root[n+1], 1, n + q, n + i, 1, res);//之后把这个孩子压回来 printf("%lld\n", res); } } return 0; }