CF319E Ping-pong 题解

传送门

注意题目保证新加入的区间长度一定最大,想一想,这是保证了新区间不会被包含。

区间关系有三种:如果两个区间相交,则两个区间互相可达;如果是包含关系,小的能到大的;如果相离,都不能到。

显然当区间 \(a\)\(b\) 相互可达,\(b\)\(c\) 相互可达,则 \(a,b,c\) 两两可达。

所以我们定义一个 “区块”:若干个两两可达的区间的并,且无法加入一个新区间使得加入后依然两两可达。

显然这个并应该是连续的一个区间。

引理:区块们应当形成嵌套类型。也就是大区块包着若干个小区块,小区块又包着若干个小小区块……

证明:可以用归纳法。当新加入一个区间,把这个区间和所有与这个区间相交的区块都合并了,还是满足条件。

注意不能直接反证法,直接看两个相交的区块。因为这无法保证两个区块的区间是否有包含关系。

进一步地,只能从小区块跳到大区块,而不能从大区块跳到小区块。


结论:如果新区间能跳到旧区间,则新旧区间相互可达。

证明:简单。新区间旧区间有交集,且旧区间不包含新区间,显然能从旧区间跳到新区间。

我们建立一颗线段树,每个结点就正常代表一个区间,同时每个结点维护一个 set 或者一个 vector

每加入一个新区间,我们把这个区间在线段树上拆成若干个结点,然后在这些结点的 vector 上加入这个区间的编号。

同时,鉴于我们希望快速查询两个区间是否可达,而且可达关系为无向传递性,所以考虑使用并查集。

于是初始所有区间的并查集代表元素都是 \(i\)。每加入一个新区间,就把所有包含这个区间的左端点、右端点结点中记录的所有区间编号,和这个新区间都合并了。

因为如果一个结点包含了区间的左端点、右端点,则包含这个结点的旧区间一定与新区间有交且不被包含(因为被包含的旧区间不可能同时包含左右端点),而不可能旧区间包含新区间,所以新旧区间一定能相互到达。

这已经算是一个正确的算法了,但并不是一个高效的算法。我们观察发现,如果一个旧旧区间之前和一个旧区间合并了,那来了新区间的时候,其实没必要把新区间和旧区间、新区间和旧旧区间合并两次。

于是我们在新区间在和旧区间合并完了之后,就把当前节点上所有标记全删了,只留下新区间的标记

这样就快多了。一个区间至多被拆成 \(O(\log len)\) 个结点,一共最多打 \(O(n\log len)\) 个标记,每个标记只会被访问一次,被访问了就马上删掉,所以总复杂度是 \(O(n\log len)\) 的。


代码的细节。

  1. 离散化。其实只需要离散化 \(l[i],r[i]\) 即可,不用 \(l[i]+1,r[i]-1\)

  2. 用并查集维护可达情况。但是要注意可达有两种情况。一种是两区间属于同一个区块,一种是起点的区块被终点的区块包含。

  3. 其实不必真的写一颗线段树。我们只需要用 vector 数组即可。但是在写函数的时候要根据线段树的写法。

  4. 注意,打标记分两种情况:一种是把新区间拆成若干个结点打标记;一种是新区间和旧区间的标记合并了,也要留下一个新区间的标记作为代替。

#include <bits/stdc++.h>

using namespace std;
const int N = 4e5 + 100;

int n;
int L[N], R[N];
int op[N], l[N], r[N];
int cur = 0; //离散化后,cur就是总区间数量 
map<int, int> mp; //离散化 

int fa[N];
int fnd(int x) {
	if (fa[x] == x)
		return x;
	return fa[x] = fnd(fa[x]);
}
void unn(int x, int y) {
	fa[x] = y;
	L[y] = min(L[y], L[x]);
	R[y] = max(R[y], R[x]);
}

int sz;
vector<int> val[N];

void seg_init(int x) {
	for (sz = 1; sz < x; sz *= 2);
}

void upd(int x, int lx, int rx, int l, int r, int v) { //把新区间拆成若干个结点 
	if (l <= lx && rx <= r) {
		val[x].push_back(v);
		return;
	}
	if (l >= rx || lx >= r)
		return ;
	int m = (lx + rx) / 2;
	upd(x * 2, lx, m, l, r, v);
	upd(x * 2 + 1, m, rx, l, r, v);
}
int idx = 0; //动态记录到此时的区间数量 
void add(int x, int lx, int rx, int p) { //把包含点 p 的过去的区间合并 
	if (val[x].size()) { //如果当前结点没有需要合并的标记,当然就不用在这里留下新区间的标记 
		for (auto i: val[x]) {
			unn(fnd(i), idx); //如果新区间能到旧区间,旧区间也一定能到新区间,所以此时的可达性是双向的,可用并查集 
		}
		val[x].clear();
		val[x].push_back(idx); //用新区间的标记作为代替 
	}
	if (lx + 1 == rx)
		return ;
	int m = (lx + rx) / 2;
	if (p < m)
		add(x * 2, lx, m, p);
	else
		add(x * 2 + 1, m, rx, p);
}
void newrange(int l, int r) { //加入一个新区间(l,r) 
	L[++idx] = l;
	R[idx] = r; //新区间初始的所在区块就是 (l,r)
	add(1, 1, cur + 1, l); //cur是总区间个数 
	add(1, 1, cur + 1, r); //与包含左右端点的旧区间合并 
	upd(1, 1, cur + 1, l + 1, r, idx); //拆新区间 
}

int main() {
	cin >> n;
	for (int i = 1; i < N; i++)
		fa[i] = i;
	for (int i = 1; i <= n; i++) {
		cin >> op[i] >> l[i] >> r[i];
		if (op[i] == 1) {
			mp[l[i]] = 0;
			mp[r[i]] = 0;
		}
	}
	for (auto &i: mp)
		i.second = ++cur;
	for (int i = 1; i <= n; i++)
		if (op[i] == 1) {
			l[i] = mp[l[i]];
			r[i] = mp[r[i]];
		} //离散化 
	for (int i = 1; i <= n; i++) {
		if (op[i] == 1) {
			newrange(l[i], r[i]); //加入新区间 
		}
		else {
			int x = fnd(l[i]), y = fnd(r[i]); //查询所在区块
			if (x == y || (L[x] < R[y] && L[x] > L[y]) || (R[x] < R[y] && R[x] > L[y])) //同区块或者起点的区块被包含 
				puts("YES");
			else
				puts("NO");
		}
	} 
	return 0;
}
posted @ 2024-02-19 10:59  FLY_lai  阅读(13)  评论(0编辑  收藏  举报