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)\) 的。
代码的细节。
-
离散化。其实只需要离散化 \(l[i],r[i]\) 即可,不用 \(l[i]+1,r[i]-1\)。
-
用并查集维护可达情况。但是要注意可达有两种情况。一种是两区间属于同一个区块,一种是起点的区块被终点的区块包含。
-
其实不必真的写一颗线段树。我们只需要用
vector
数组即可。但是在写函数的时候要根据线段树的写法。 -
注意,打标记分两种情况:一种是把新区间拆成若干个结点打标记;一种是新区间和旧区间的标记合并了,也要留下一个新区间的标记作为代替。
#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;
}