@loj - 2461@ 「2018 集训队互测 Day 1」完美的队列


@description@

小 D 有 n 个 std::queue,他把它们编号为 1 到 n。

小 D 对每个队列有不同的喜爱程度,如果有他不怎么喜欢的队列占用了太大的内存,小 D 就会不开心。

具体地说,如果第 i 个队列的 size() 大于 ai,小 D 就会对这个队列一直执行 pop() 直到其 size() 小等于 ai。

现在这些队列都是空的,小 D 觉得太单调了,于是他决定做一些操作。

每次操作都可以用 l r x 来描述,表示对编号在 [l,r] 内的所有队列执行 push(x) 操作。当然,每次操作结束后,小 D 都会用之前提到的方法来避免这些队列占用太大的内存。

小 D 的队列很神奇,所以他能用 O(1) 的时间执行每次操作。

相信大家的队列都能做到,于是小 D 把这道题出给大家送分。

为了证明你确实执行了这些操作,你需要在每次操作后输出目前还在队列内的权值种数。

输入格式
第一行两个正整数 n,m,分别表示队列个数和操作个数。

第二行 n 个正整数,第 i 个表示 ai。

接下来 m 行,每行三个正整数 l r x,其中第 i 行表示第 i 次操作。

输出格式
输出共 m 行,每行一个非负整数,表示第 i 次操作结束后所有队列中的权值种数。

样例
样例输入
3 3
1 2 3
1 2 1
2 3 2
1 3 3
样例输出
1
2
2

样例解释
第一次操作后,队列变成 {1}{1}{},还在队列内的权值有 1,共 1 种。

第二次操作后,队列变成 {1}{1,2}{2},还在队列内的权值 1,2,共 2 种。

第三次操作后,队列变成 {3}{2,3}{2,3},还在队列内的权值有 2,3,共 2 种。

数据范围与提示
对于所有数据,n,m,ai,x<=10^5,l<=r。

@solution@

@part - 0@

先讲一些非正解的思路。
(如果是想直接知道正解的可以直接跳到 part - 1)

当 ai = 1 时,就相当于区间涂色,统计总颜色种类
注意到每次涂色,最多会把一个连续颜色段分割成两个。也就是连续颜色段的个数每次最多增加 1。
因此我们用平衡树维护一下这些颜色段即可,每次将消失的颜色段暴力删除,将颜色段暴力裂开

当 ai ≠ 1 时,可以维护 max{ai} 棵平衡树,每次从上一棵平衡树中取出操作区间(用 splay 或非旋 treap 即可),并把这个区间覆盖到本层的这棵平衡树上。
如果到达了这个区间内所有队列的最小容量(即 min{a[l...r]}),就暴力弹,分成两个区间继续操作。

可以发现这个思路到这里就停了,因为这个思路始终与 max{ai} 相关。

遇事不决先离线。我们考虑离线是否能简化问题。
采用线段树分治类似的套路:我们尝试求出每次操作能够影响到的时间段(注意不是每种颜色的时间)。
然后发现如果我们求出了这个值,剩下的只需要按时间从前往后扫一遍即可。

求每次操作能够影响的时间段,其实就是求这个操作的颜色全部弹出的最早时间
这个时间是具有单调性的:一旦在时刻 t 所有颜色均被弹出,则 t 秒之后依然全部颜色均被弹出。
可以二分。对于操作 i,二分最早时间 t。

至于怎么判断合法。我们维护一个线段树,每个位置的值维护为 “队列容量 - 队列的元素总数”。
然后我们把 [i, t] 这些操作全部塞进线段树里面,查询 i 对应的区间 [li, ri] 的最大值是否为负。
注意到如果知道 [l, r] 的线段树,我们可以 O(log) 求出 [l-1, r] 与 [l, r+1] 的线段树(类似于莫队的性质)。

但是单纯的二分是过不了。虽然可以 分块 + 整体二分 乱搞,不过最好的方法是类似于大步小步的方法:
我们取 \(b_i = i*\sqrt{n}\),则一共会有 \(\sqrt{n}\) 个 b。然后我们对于每一个 i,求出 \([i, b_{1...\sqrt{n}}]\) 的线段树。
然后对于每个 i,可以先判定 i 对应的最早时间 t 在哪两个 b 之间,然后在这两个 b 之间暴力枚举。
这个算法的复杂度为 \(O(n\sqrt{n}\log n)\)

@part - 1@

在看完以上那些乱搞后,会觉得以上那些做法看起来非常正确。
所以极有可能就不小心跑偏了。。。

一样地,我们采用离线
采用线段树分治类似的套路:我们尝试求出每次操作能够影响到的时间段(注意不是每种颜色的时间)。
然后发现如果我们求出了这个值,剩下的只需要按时间从前往后扫一遍即可。
求每次操作能够影响的时间段,其实就是求这个操作的颜色全部弹出的最早时间
(对我就是直接复制 part - 0 中的话)

注意到有一个性质:假如两个操作 i, j 的操作区间相同,且 i 在 j 之前,则 i 永远比 j 先消失。
即:操作区间相同时,删除顺序具有单调性

为了充分利用单调性,我们对序列分块,将一个操作区间拆成若干整块操作与最多 2 个散块操作。
然后考虑每一个块,怎么处理涉及到这个块的整块操作与散块操作的最早删除时间。

先考虑整块操作的删除时间。我们维护每个队列的 “队列容量 - 队列的元素总数”,再维护这个块内该值的最大值 smx。
用个 two-points。当 smx < 0 时,左端点对应元素全部被弹出,记录此时右端点为左端点的最早时间,然后将左端点右移;否则,将右端点右移。
至于右移的影响,整块直接改 smx,散块暴力重算(因为散块总个数为 \(O(n)\))。

然后考虑散块。我们去找散块中的每一个元素算删除时间(一样地,因为散块总个数为 \(O(n)\))。
两个散块操作之间的整块操作是没有区别的。于是我们处理出相邻两个散块操作之间有多少个整块操作,然后一样地一遍 two-points 搞定(这里的 two-points 只遍历散块操作)。

这样时间复杂度为 \(O(n\sqrt{n})\),非常优秀。

@accepted code@

#include<cstdio>
#include<vector>
#include<algorithm>
using namespace std;
const int MAXN = 100000;
const int BLOCK = 320;
int le[MAXN + 5], ri[MAXN + 5], id[MAXN + 5], bcnt;
void init(int n) {
	for(int i=1;i<=n;i++) {
		if( (i-1) % BLOCK == 0 )
			le[++bcnt] = i;
		ri[bcnt] = i, id[i] = bcnt;
	}
}
int tg[MAXN + 5], nxt[MAXN + 5];
int arr[MAXN + 5], type[MAXN + 5], cnt;
int l[MAXN + 5], r[MAXN + 5], x[MAXN + 5];
int a[MAXN + 5], ed[MAXN + 5], n, m;
vector<int>v1[MAXN + 5], v2[MAXN + 5];
int siz[MAXN + 5];
void solve() {
	for(int i=1;i<=m;i++)
		v1[i].push_back(x[i]), v2[ed[i]].push_back(x[i]);
	int ans = 0;
	for(int i=1;i<=m;i++) {
		for(int j=0;j<v1[i].size();j++) {
			if( siz[v1[i][j]] == 0 ) ans++;
			siz[v1[i][j]]++;
		}
		for(int j=0;j<v2[i].size();j++) {
			siz[v2[i][j]]--;
			if( siz[v2[i][j]] == 0 ) ans--;
		}
		printf("%d\n", ans);
	}
}
int main() {
	scanf("%d%d", &n, &m), init(n);
	for(int i=1;i<=n;i++)
		scanf("%d", &a[i]);
	for(int i=1;i<=m;i++)
		scanf("%d%d%d", &l[i], &r[i], &x[i]);
	for(int i=1;i<=bcnt;i++) {
		cnt = 0;
		for(int j=1;j<=m;j++)
			if( l[j] > ri[i] || r[j] < le[i] )
				continue;
			else if( l[j] <= le[i] && ri[i] <= r[j] )
				cnt++, arr[cnt] = j, type[cnt] = 0;
			else
				cnt++, arr[cnt] = j, type[cnt] = 1;
		int stg = 0, smx = 0, right = 0;
		for(int j=le[i];j<=ri[i];j++)
			tg[j] = a[j], smx = max(smx, tg[j]);
		arr[cnt + 1] = m + 1;
		for(int j=1;j<=cnt;j++) {
			while( right <= cnt && smx - stg >= 0 ) {
				right++;
				if( right > cnt ) break;
				if( type[right] == 0 )
					stg++;
				else {
					smx = 0;
					for(int k=le[i];k<=ri[i];k++) {
						if( l[arr[right]] <= k && k <= r[arr[right]] )
							tg[k]--;
						smx = max(smx, tg[k]);
					}
				}
			}
			if( type[j] == 0 ) {
				ed[arr[j]] = max(ed[arr[j]], arr[right]);
				stg--;
			}
			else {
				smx = 0;
				for(int k=le[i];k<=ri[i];k++) {
					if( l[arr[j]] <= k && k <= r[arr[j]] )
						tg[k]++;
					smx = max(smx, tg[k]);
				}
			}
		}
		int tmp = cnt + 1;
		for(int j=cnt;j>=1;j--)
			if( type[j] == 1 )
				nxt[j] = tmp, tmp = j;
		for(int j=le[i];j<=ri[i];j++) {
			int right = 0, tot = 0;
			for(int k=tmp;k<=cnt;k=nxt[k]) {
				if( right < k ) {
					right = k;
					if( l[arr[k]] <= j && j <= r[arr[k]] )
						tot++;
				}
				while( right <= cnt && tot + nxt[right] - right - 1 <= a[j] ) {
					tot += nxt[right] - right - 1;
					right = nxt[right];
					if( right > cnt ) break;
					if( l[arr[right]] <= j && j <= r[arr[right]] )
						tot++;
				}
				if( l[arr[k]] <= j && j <= r[arr[k]] ) {
					if( right > cnt ) ed[arr[k]] = max(ed[arr[k]], arr[right]);
					else ed[arr[k]] = max(ed[arr[k]], arr[right + a[j] - tot + 1]);
				}
				if( l[arr[k]] <= j && j <= r[arr[k]] )
					tot--;
				if( right > k )
					tot -= nxt[k] - k - 1;
				else tot = 0;
			}
		}
	}
	solve();
}

@details@

把 max 打成 min,然后 WA 到怀疑人生。。。
(质疑自己的脑袋是否完整.jpg)

posted @ 2019-10-11 09:58  Tiw_Air_OAO  阅读(251)  评论(0编辑  收藏  举报