线段树扫描线

Posted mcggvc

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了线段树扫描线相关的知识,希望对你有一定的参考价值。

扫描线一般运用在图形上面,它和它的字面意思十分相似,就是一条线在整个图上扫来扫去,它一般被用来解决图形面积,周长等问题。     -------OI Wiki

扫描线求面积并

P5490 【模板】扫描线
技术图片
如图,假设三个矩形,求其并集的面积。考虑将每个矩形处理成两条平行于 y 轴的线段,左边的线段标记为 +1 ,右边的线段标记为 -1 ,用一个四元组((x, y_1, y_2, k))存下来,于是我们得到了这样一个图形:
技术图片
按照四元组中 x 坐标由小到大排序,我们就可以分成一段一段地处理整个图形:
技术图片
不同颜色表示一次处理的一段,此图形共可分成5段。
考虑每次处理的矩形,沿 x 轴的长度很容易知道,即当前四元组 x 减去上一个四元组 x 。我们要处理沿 y 轴的长度,维护一个序列:
1.若当前读到一个标记为 +1 的线段 ,就在序列上将这段区间 +1 ,反之 - 1。
2.每次统计时求序列上标记大于 0 的长度即可。
具体处理:
1.由于题目中数据较大,需要离散化,用一个数组(num[])映射到原坐标值。
2.用线段树维护序列(序列中第 i 项对应([num[i], num[i + 1] ])这个区间),线段树中每个节点维护两个值:s 和 cnt ,cnt 表示此段被标记的值,s 表示这段上 cnt > 0 的长度。update时,若当前结点 cnt > 0,那么 s 值为整段的长度,即(num[r + 1] - num[l]),否则为两个子结点长度之和,因为每次查询我们只关心整段序列,即线段树根结点,所以这个区间修改不需要标记下传,。
3.读入一个四元组((x, y_1, y_2, k))时,先统计答案,再将序列上([y_1, y_2 - 1])这段的标记加上 k ,。

#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <vector>
using namespace std;
typedef long long lld;
const int N = 100005;
const int M = 1000005;
int n, tot = 0, num[M], maxn = 0;
lld ans = 0;
struct Tree {
    int cnt, s;
} e[M << 2];
struct node {
    int x, l, r, k;
} p[N << 1];
void update(int i, int l, int r) {
    if(e[i].cnt > 0) e[i].s = num[r + 1] - num[l];// l, r表示num[l]到num[r + 1]这一段
    else e[i].s = e[i << 1].s + e[i << 1 | 1].s;
}
void add(int i, int l, int r, int nl, int nr, int k) {
    if(l >= nl && r <= nr) {
        e[i].cnt += k; update(i, l, r);
        return ;
    }
    int mid = (l + r) >> 1;
    if(nl <= mid) add(i << 1, l, mid, nl, nr, k);
    if(nr > mid) add(i << 1 | 1, mid + 1, r, nl, nr, k);
    update(i, l, r);
}
int get() {
    return e[1].s;
}
void add1(int x1, int x2, int y1, int y2) {
    p[++tot].x = x1; p[tot].l = y1; p[tot].r = y2; p[tot].k = 1;
    p[++tot].x = x2; p[tot].l = y1; p[tot].r = y2; p[tot].k = -1;
    maxn = max(y1, max(y2, maxn));
}
int a[N], b[N], c[N], d[N];
vector <int> tmp;
bool cmp(node a, node b) {
    return a.x < b.x;
}
int main() {
//  freopen("data.in", "r", stdin);
    scanf("%d", &n);
    for(int i = 1, x1, x2, y1, y2; i <= n; i++) {
        scanf("%d%d%d%d", &x1, &y1, &x2, &y2);
        a[i] = x1, b[i] = x2, c[i] = y1, d[i] = y2;
        tmp.push_back(y1); tmp.push_back(y2);
    }
    sort(tmp.begin(), tmp.end());//离散化
    for(int i = 1; i <= n; i++) {
        int save1 = c[i], save2 = d[i];
        c[i] = lower_bound(tmp.begin(), tmp.end(), c[i]) - tmp.begin() + 1;
        d[i] = lower_bound(tmp.begin(), tmp.end(), d[i]) - tmp.begin() + 1;
        num[c[i]] = save1, num[d[i]] = save2;//映射原值
        add1(a[i], b[i], c[i], d[i]);//添加四元组
    }
    sort(p + 1, p + 1 + tot, cmp);//按照x排序
    add(1, 1, maxn, p[1].l, p[1].r - 1, p[1].k);//添加第一条线段
    for(int i = 2; i <= tot; i++) {
        ans = ans + (1ll * get() * (p[i].x - p[i - 1].x));//统计答案,长乘宽
        add(1, 1, maxn, p[i].l, p[i].r - 1, p[i].k);//修改序列
    }
    printf("%lld", ans);
    return 0;
}

扫描线求矩形周长

P1856 [USACO5.5]矩形周长Picture

方法类似求面积,离散化处理,线段树区间修改和查询方法同上。
统计答案: 读入一个四元组时,先记录当前的查询的值,进行修改后再查询一次,答案增加的量为两次查询的差的绝对值。 证明方法显然,这里不多bb。
细节:
1.这样每次只能统计平行于 y 轴的线段的周长和,我们需要将每个点的 x,y 坐标交换后再重复上述操作。
2.四元组排序时若 x 坐标相同,则 k = 1 的排在前面(先加上再减去对统计对答案没有影响,若是先减去在加上可能会导致答案偏大)

#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <vector>
using namespace std;
const int N = 5005;
const int M = 20015;
const int F = 10001;
int a[N], b[N], c[N], d[N], n, tot = 0;
int num[M << 2];
long long ans = 0;
struct node {
    int x, l, r, k;
} p[N << 1];
void add(int x1, int y1, int x2, int y2) {
    if(y1 > y2) swap(y1, y2);
    p[++tot].x = x1, p[tot].l = y1, p[tot].r = y2, p[tot].k = 1;
    p[++tot].x = x2, p[tot].l = y1, p[tot].r = y2, p[tot].k = -1;
}
struct tree {
    int s, cnt;
} e[M << 2];
vector <int> tmp, opt;
void update(int i, int l, int r) {
    if(e[i].cnt > 0) e[i].s = num[r + 1] - num[l];
    else e[i].s = e[i << 1].s + e[i << 1 | 1].s;
}
void add(int i, int l, int r, int nl, int nr, int k) {
    if(l >= nl && r <= nr) {
        e[i].cnt += k; update(i, l, r);
        return ;
    }
    int mid = (l + r) >> 1;
    if(nl <= mid) add(i << 1, l, mid, nl, nr, k);
    if(nr > mid) add(i << 1 | 1, mid + 1, r, nl, nr, k);
    update(i, l, r);
}
int get() {
    return e[1].s;
}
bool cmp(node a, node b) {
    if(a.x == b.x) return a.k > b.k; //k = 1排在前面
    return a.x < b.x;
}
void solve() {
    sort(p + 1, p + 1 + tot, cmp);
    for(int i = 1; i <= tot; i++) {
        long long per = get();
        add(1, 1, tot, p[i].l, p[i].r - 1, p[i].k);
        ans = (ans + abs(per - get()));
    }
}
int main() {
//  freopen("data.in", "r", stdin);
    scanf("%d", &n);
    for(int i = 1, x1, x2, y1, y2; i <= n; i++) {
        scanf("%d%d%d%d", &x1, &y1, &x2, &y2);
        a[i] = x1, b[i] = y1, c[i] = x2, d[i] = y2;
        tmp.push_back(y1); tmp.push_back(y2);
        opt.push_back(x1), opt.push_back(x2);
    }
    sort(tmp.begin(), tmp.end());
    for(int i = 1; i <= n; i++) {
        int save1 = b[i], save2 = d[i];
        b[i] = lower_bound(tmp.begin(), tmp.end(), b[i]) - tmp.begin() + 1;
        d[i] = lower_bound(tmp.begin(), tmp.end(), d[i]) - tmp.begin() + 1;
        num[b[i]] = save1, num[d[i]] = save2;
        add(a[i], b[i], c[i], d[i]);
    }
    solve();
    for(int i = 0; i < (M << 2); i++) e[i].s = e[i].cnt = 0;
    memset(num, 0, sizeof(num));
    tot = 0;
    sort(opt.begin(), opt.end());
    for(int i = 1; i <= n; i++) {
        int save1 = a[i], save2 = c[i];
        a[i] = lower_bound(opt.begin(), opt.end(), a[i]) - opt.begin() + 1;
        c[i] = lower_bound(opt.begin(), opt.end(), c[i]) - opt.begin() + 1;
        num[a[i]] = save1, num[c[i]] = save2;
        add(b[i], a[i], d[i], c[i]);
    }
    solve();
    printf("%lld", ans);
    return 0;
}

以上是关于线段树扫描线的主要内容,如果未能解决你的问题,请参考以下文章

hdu1542线段树(扫描线+离散化)

hdu 1542 线段树之扫描线之面积并

优先队列 + 并查集 + 字典树 + 树状数组 + 线段树 + 线段树点更新 + KMP +AC自动机 + 扫描线

扫描线——~~原理分析与代码块(指针线段树)~~——从入门到放弃

线段树+扫描线求矩形面积的并

HDU 3265 Posters ——(线段树+扫描线)