「POJ 2528」Mayor's posters
题意
先给定一个\(T\),表示共有\(T\)组输入。
对于每组输入,给定一个\(n\),表示下面有\(n\)对\([l, r]\);
每对\([l,r]\)的含义为:在给线段\([l, r]\)染上一种新的颜色(会覆盖掉原来的颜色)。问最后线段上一共有几种不同的颜色?
数据范围:\(1 \leq n \leq 10^5, 1 \leq l, r \leq 10^7\)。(注意:原题给定的\(1 \leq n \leq 10000\)的数据范围是错的)
思路
本题有好多种解法。关于线段树+离散化的解法有很多题解,这里就不写了(其实是我不会QwQ);
这里介绍一下柯朵莉树和线段树动态开点的做法。
柯朵莉树
这题完全就是一柯朵莉树的板题。
详细的柯朵莉树的介绍请参考oi-wiki,这里只讲与本题有关的内容。
简单介绍一下柯朵莉树(又称老司机树,ODT):
柯朵莉树是借助C++的set
来实现大部分\(O(\log n)\)操作的。其原理就是,用一个struct
存储线段的信息,其中struct
长下面这样:
struct Node {
ll l, r;
mutable ll v;
Node(ll u, ll v = -1, ll w = 0): l(u), r(v), v(w) { }
bool operator < (const Node &rhs) const { // 按照l排序
return this -> l < rhs.l;
}
};
具体原理就是,用\(l,r\)表示区间的左端和右端,然后用\(v\)存区间的值。因为区间是依据\(l\)排序的,所以可以放心将\(v\)声明成mutable
(与const
相对,表示恒可修改)。
用set
维护的柯朵莉树满足这样的性质:set
中存储的点,其一定满足任意两个不同的点表示的区间不重叠。这样就会有一个问题,就是我们先给\([1, 4]\)赋值为\(2\),然后又想给\([3,5]\)赋值为\(3\)应该怎么办?
所以就要用到split
操作。我们先将\([1,4]\)split
成\([1,2],[3,5]\)两个区间,然后再进行操作。split
函数如下:
// 表示从pos这里分开成[l, pos - 1]和[pos, r],并返回[pos, r]的iterator
set<Node>::iterator split(ll pos) {
set<Node>::iterator it = s.lower_bound(pos);
if (it != s.end() && it -> l == pos) return it;
--it;
ll L = it -> l, R = it -> r, V = it -> v;
s.erase(it);
s.insert(Node(L, pos - 1, V));
return s.insert(Node(pos, R, V)).first;
}
(应该能直接看懂,就不加注释了)
简单粗暴。
最后,区间赋值。
有了上面的介绍,就应该很简单了:
void Assign(ll l, ll r, ll val = 0) {
set<Node>::iterator itr = split(r + 1);
set<Node>::iterator itl = split(l);
s.erase(itl, itr);
s.insert(Node(l, r, val));
}
注意必须先split(r + 1)
,然后再split(l)
,否则split(r + 1)
后可能会导致itl
变成野指针。
最后,别忘了初始化。完整代码如下:
// 875ms
#include <cstdio>
#include <algorithm>
#include <iostream>
#include <cstring>
#include <set>
#include <vector>
#include <map>
typedef long long ll;
using namespace std;
struct Node {
ll l, r;
mutable ll v;
Node(ll u, ll v = -1, ll w = 0): l(u), r(v), v(w) { }
bool operator < (const Node &rhs) const {
return this -> l < rhs.l;
}
};
set<Node> s;
map<ll, ll> vis;
ll T, n, x, y;
set<Node>::iterator split(ll pos) {
set<Node>::iterator it = s.lower_bound(pos);
if (it != s.end() && it -> l == pos) return it;
--it;
ll L = it -> l, R = it -> r, V = it -> v;
s.erase(it);
s.insert(Node(L, pos - 1, V));
return s.insert(Node(pos, R, V)).first;
}
void Assign(ll l, ll r, ll val = 0) {
set<Node>::iterator itr = split(r + 1);
set<Node>::iterator itl = split(l);
s.erase(itl, itr);
s.insert(Node(l, r, val));
}
inline void init() {
s.clear();
vis.clear();
s.insert(Node(-0x3f3f3f3f3f3f3f3f, -1, 0));
s.insert(Node(0, 0x3f3f3f3f3f3f3f3f, 0));
}
int main() {
scanf("%lld", &T);
while (T--) {
init();
scanf("%lld", &n);
for (ll i = 1; i <= n; i++) {
scanf("%lld%lld", &x, &y);
if (x > y) swap(x, y);
Assign(x, y, i);
}
for (set<Node>::iterator it = s.begin(); it != s.end(); it++) {
vis[it -> v] = 1;
}
printf("%lld\n", (ll)vis.size() - 1);
}
return 0;
}
线段树+动态开点
传统的线段树我们查询rt
左右子结点的时候,就直接rt << 1
和rt << 1 | 1
了,是因为我们直接把数组当树状结构用了。但是本题的数据范围太大,而且会发现很多叶子结点根本用不到,所以可以考虑动态开点。
动态开点要把原来直接用int
存的线段树结点换成struct
,因为我们还需要存左右子节点的位置,因为我们只有在用到这个结点的时候,才会去创建它。我们用专门的一个函数来表示创建新节点:
int create() {
cnt++;
SegTree[cnt].l = 0;
SegTree[cnt].r = 0;
SegTree[cnt].val = 0;
return (cnt);
}
这里赋值为\(0\)是表示初始化,因为有多组输入。
其他与传统线段树不同的地方,基本就只有在访问子节点的时候需要先判断一下有没有子节点,没有的话需要先创建子节点。
所以就直接上完整代码了:
// 407ms
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <set>
typedef long long ll;
using namespace std;
const int maxn = 2e5 + 5;
int cnt, T, n;
// 因为set会自动将重复的点合并,所以就直接用set存了
set<int> res;
struct Node {
int val;
ll l, r;
};
Node SegTree[maxn << 2];
int create() {
cnt++;
SegTree[cnt].l = 0;
SegTree[cnt].r = 0;
SegTree[cnt].val = 0;
return (cnt);
}
// 本题并没有用到lazy,所以pushdown其实大概相当于pushup
// 只不过因为题目特点,这个是将点的值向下放
void pushdown(int rt) {
if (SegTree[rt].val) {
if (!SegTree[rt].l) SegTree[rt].l = create();
SegTree[SegTree[rt].l].val = SegTree[rt].val;
if (!SegTree[rt].r) SegTree[rt].r = create();
SegTree[SegTree[rt].r].val = SegTree[rt].val;
}
SegTree[rt].val = 0;
}
void modify(int rt, ll l, ll r, ll L, ll R, int val) {
if (L <= l && r <= R) {
SegTree[rt].val = val;
return ;
}
ll mid = (l + r) >> 1;
pushdown(rt);
if (L <= mid) {
if (!SegTree[rt].l) SegTree[rt].l = create();
modify(SegTree[rt].l, l, mid, L, R, val);
}
if (R > mid) {
if (!SegTree[rt].r) SegTree[rt].r = create();
modify(SegTree[rt].r, mid + 1, r, L, R, val);
}
}
void query(int rt) {
if (SegTree[rt].val) {
res.insert(SegTree[rt].val);
return ;
}
if (SegTree[rt].l) query(SegTree[rt].l);
if (SegTree[rt].r) query(SegTree[rt].r);
}
void init() {
cnt = 0;
res.clear();
SegTree[0].l = 0; SegTree[0].r = 0; SegTree[0].val = 0;
}
int main() {
scanf("%d", &T);
while (T--) {
init();
scanf("%d", &n);
for (int i = 1; i <= n; i++) {
ll x, y;
scanf("%lld%lld", &x, &y);
modify(0, 1, 10000000, x, y, i);
}
query(0);
printf("%lld\n", (long long)res.size());
}
return 0;
}
虽然都是\(O(n \log n)\)的复杂度,但是实测动态开点速度基本上是柯朵莉树的两倍。可能是因为本来STL的常数就大,而且柯朵莉树的操作还比较多。数据离散化因为本蒟蒻不会写,所以并不知道具体速度QAQ