「POJ 2528」Mayor's posters

题目链接 POJ 2528

题意

先给定一个\(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 << 1rt << 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

posted @ 2020-10-05 23:09  icysky  阅读(122)  评论(0编辑  收藏  举报