|
|
#include <bits/stdc++.h>
|
|
|
using namespace std;
|
|
|
|
|
|
typedef long long LL;
|
|
|
const int N = 1e5 + 10; // 1e5个矩形
|
|
|
LL res; // 结果
|
|
|
int n; // n个矩形
|
|
|
int b[N << 2]; // 离散化数组,因为1e5个矩形,2e5条边,每条边两个y1,y2,所以,需要4e5
|
|
|
|
|
|
// 从左到右的扫描线
|
|
|
struct ScanLine {
|
|
|
// y1,y2:这条线段上面的那个点和下面的那个点的y坐标
|
|
|
// x:在这里不会用到,只是用来排序而已
|
|
|
// k:是入边还是出边
|
|
|
int y1, y2, x, mark;
|
|
|
const bool operator<(const ScanLine &t) {
|
|
|
return x < t.x; // 以x坐标进行排序
|
|
|
}
|
|
|
} line[N << 1]; // 2e5条边
|
|
|
|
|
|
struct Node {
|
|
|
int l, r;
|
|
|
int len; // 统计值:有效长度和
|
|
|
int cnt; // 属性:入边:+1,出边:-1,被多少个方格覆盖:cnt
|
|
|
} tr[N << 4]; // 开了16倍空间,矩形数量1e5,每个矩形2条竖边,就是2e5,每条边两个y1,y2,就是4e5个点,对于4e5需要开4倍空间,就是16e5
|
|
|
|
|
|
void pushup(int u) {
|
|
|
// pushup:利用左右儿子的信息,更新u节点的统计信息
|
|
|
// 扫描线题目中,统计信息就是有效区间长度,即len
|
|
|
// cnt不是统计信息,它可以视为某条线段树中某个区间的固有属性,它不需要进行向上推送更新
|
|
|
|
|
|
// 整个区间被覆盖过,有效len等于区间长度
|
|
|
// tr[u].l = L , tr[u].r + 1 = R => 坐标系中的L,R与线段树中[l,r]之间的映射关系
|
|
|
// 使用b[]离散数组,通过索引号找出此位置上的原始值,即y2,y1
|
|
|
if (tr[u].cnt)
|
|
|
tr[u].len = b[tr[u].r + 1] - b[tr[u].l];
|
|
|
else
|
|
|
tr[u].len = tr[u << 1].len + tr[u << 1 | 1].len;
|
|
|
// u节点所管辖的范围,并非完整命中,有效区间长度len=左节点len+右节点len
|
|
|
}
|
|
|
|
|
|
// 建树
|
|
|
void build(int u, int l, int r) {
|
|
|
tr[u].l = l, tr[u].r = r;
|
|
|
if (l == r) return;
|
|
|
int mid = (l + r) >> 1;
|
|
|
build(u << 1, l, mid);
|
|
|
build(u << 1 | 1, mid + 1, r);
|
|
|
}
|
|
|
|
|
|
void modify(int u, int l, int r, int v) { // v:入边+1,出边-1
|
|
|
if (l <= tr[u].l && r >= tr[u].r) { // 如果完整区间命中
|
|
|
tr[u].cnt += v; // 区间被覆盖次数+v
|
|
|
pushup(u); // 属性的更改,会造成统计信息的变更,所以需要pushup
|
|
|
return;
|
|
|
}
|
|
|
// 未完整命中时,分裂
|
|
|
int mid = (tr[u].l + tr[u].r) >> 1;
|
|
|
if (l <= mid) modify(u << 1, l, r, v);
|
|
|
if (r > mid) modify(u << 1 | 1, l, r, v);
|
|
|
// 更新统计信息
|
|
|
pushup(u);
|
|
|
}
|
|
|
|
|
|
int main() {
|
|
|
#ifndef ONLINE_JUDGE
|
|
|
freopen("P5490.in", "r", stdin);
|
|
|
#endif
|
|
|
// 加快读入
|
|
|
ios::sync_with_stdio(false), cin.tie(0);
|
|
|
|
|
|
cin >> n;
|
|
|
int x1, x2, y1, y2;
|
|
|
for (int i = 1; i <= n; i++) {
|
|
|
cin >> x1 >> y1 >> x2 >> y2;
|
|
|
b[i * 2 - 1] = y1;
|
|
|
b[i * 2] = y2;
|
|
|
// 将y1,y2打入b数组,使得1e9的范围离散化,扫描线从左向右
|
|
|
// 这里可以利用线段两个端点的特性,一个是2*i-1,另一个是2*i
|
|
|
// 当然也可以使用游标的办法:++bl,效果是一样的,只要是保证能塞进离散化数组b就行
|
|
|
|
|
|
// 将每个矩形的两条竖边记录扫描线数组
|
|
|
// 一个竖着的线段,三个属性:y1,y2,x
|
|
|
line[i * 2 - 1] = {y1, y2, x1, 1}; // 1:入边
|
|
|
line[i * 2] = {y1, y2, x2, -1}; // -1:出边
|
|
|
}
|
|
|
|
|
|
n *= 2; // 一个矩形两条线段,直接乘2,方便后的计算,n的新含义:扫描线条数
|
|
|
sort(line + 1, line + n + 1); // 按x坐标由小到大排序
|
|
|
|
|
|
sort(b + 1, b + n + 1); // 将y坐标离散化
|
|
|
int tot = unique(b + 1, b + n + 1) - b - 1; // 去重操作计算有多少个不同的y,也就是这个图象有几个y
|
|
|
|
|
|
build(1, 1, tot - 1); // 建树
|
|
|
/*
|
|
|
Q:为什么是tot-1呢?
|
|
|
答:因为[1,3][3,5]是坐标系中两条线段,但它们之间有交集3,采用的办法是左闭右开,即[1,3),[3,5)
|
|
|
也就是{1,2},{3,4}共四个子矩形,看明白了吧,这个矩形是格子,相当于最多到r-1。
|
|
|
最后一个右端点是tot,那么对应的线段数量就是tot-1
|
|
|
*/
|
|
|
|
|
|
for (int i = 1; i < n; i++) { // 开始扫描,面积并中扫描到n-1条线段即可,最后一条没用
|
|
|
int L = lower_bound(b + 1, b + tot + 1, line[i].y1) - b;
|
|
|
int R = lower_bound(b + 1, b + tot + 1, line[i].y2) - b;
|
|
|
// 在排好序并去重后的数组b中,通过二分+原始值y1,查找到y1在离散化后数组b的位置,位置编号就是y1在整体中的相当名次
|
|
|
// 上面构建build时,也是用tot-1可丁可卯的大小创建的线段树,所以这里也必须用find后的位置编号去指定位置上修改
|
|
|
|
|
|
modify(1, L, R - 1, line[i].mark);
|
|
|
// y1,y2:坐标系中的真实值
|
|
|
// L,R:离散化后的一一映射值
|
|
|
// 上面已经讨论过,坐标系中的线段区间 与 线段树中管控范围不是一一对应的,线段树区间是左闭右开的
|
|
|
// [L,R-1]-->(l,r)
|
|
|
|
|
|
res += (LL)tr[1].len * (line[i + 1].x - line[i].x);
|
|
|
// tr[1].len: 当前整体线段树中的可用长度和
|
|
|
// line[i + 1].x - line[i].x :下一条扫描线与本扫描线之间的宽度(扫描线从左向右,x上的变化视为宽度)
|
|
|
// res+ : 长度和*宽度
|
|
|
}
|
|
|
// 输出res
|
|
|
printf("%lld\n", res);
|
|
|
return 0;
|
|
|
} |