Powered by:NEFU AB-IN
Link
X星球的一批考古机器人正在一片废墟上考古。
该区域的地面坚硬如石、平整如镜。
管理人员为方便,建立了标准的直角坐标系。
每个机器人都各有特长、身怀绝技。
它们感兴趣的内容也不相同。
经过各种测量,每个机器人都会报告一个或多个矩形区域,作为优先考古的区域。
矩形的表示格式为 (x1,y1,x2,y2),代表矩形的两个对角点坐标。
为了醒目,总部要求对所有机器人选中的矩形区域涂黄色油漆。
小明并不需要当油漆工,只是他需要计算一下,一共要耗费多少油漆
其实这也不难,只要算出所有矩形覆盖的区域一共有多大面积就可以了。
注意,各个矩形间可能重叠。
离散线段树 完成扫描线算法
之前写过这个题的题解,用的是y总讲的扫描线的模板,但每次都记不住…
这次是用的董老师的模板,直接套用离散线段树,不用二分find,直接在build的时候就放入y坐标值了,非常好用!!这是视频链接 link
概念解释
扫描线: 就是一根线,从左往右扫,以每个矩形的左右边为界,可以把n个矩形组成的区域分割成2n-1个区块,每个区块的宽就是扫过的距离,每个区块的有效高度一直在变化。
有效高度: 就是区块内矩形的高度的并,每当扫到一个矩形的左边时,就加入该矩形高的贡献;而当扫到该矩形的右边时,就减去该矩形高的贡献。
线段树的作用:有效高度不断变化,可以通过线段树维护区间的修改。但是,所给矩形的Y坐标“值域大(109)数量少(105)".需要离散化。可以使用离散线段树直接维护Y坐标。
算法流程
下面是董老师视频中的截图,对比了普通线段树和离散线段树的区别
下面我会放不带注释和带注释的代码,建议要真正理解思想
不带注释
/*
* @Author: NEFU AB-IN
* @Date: 2023-03-28 20:04:18
* @FilePath: \Acwing\1228\1228.cpp
* @LastEditTime: 2023-03-28 20:50:10
*/
#include
using namespace std;
#define int long long
#define SZ(X) ((int)(X).size())
#define ALL(X) (X).begin(), (X).end()
#define IOS \
ios::sync_with_stdio(false); \
cin.tie(nullptr); \
cout.tie(nullptr)
#define DEBUG(X) cout << #X << ": " << X << '\n'
typedef pair<int, int> PII;
#define ls p << 1
#define rs p << 1 | 1
const int N = 1e5 + 10, INF = 0x3f3f3f3f;
struct Tree
{
int l, r, len, cnt;
} tr[N << 3];
struct Line
{
int x, y1, y2, flag;
bool operator<(Line &a) const
{
return x < a.x;
}
} seg[N];
int n, xs[N];
void pushup(int p)
{
if (tr[p].cnt)
tr[p].len = tr[p].r - tr[p].l;
else
tr[p].len = tr[ls].len + tr[rs].len;
}
void build(int p, int l, int r)
{
tr[p] = {xs[l], xs[r], 0, 0};
if (r == l + 1)
return;
int mid = l + r >> 1;
build(ls, l, mid);
build(rs, mid, r);
}
void update(int p, int L, int R, int v)
{
if (tr[p].l >= R || tr[p].r <= L)
return;
if (tr[p].l >= L && tr[p].r <= R)
{
tr[p].cnt += v;
pushup(p);
return;
}
update(ls, L, R, v);
update(rs, L, R, v);
pushup(p);
}
signed main()
{
IOS;
int n;
cin >> n;
for (int i = 1; i <= n; ++i)
{
int x1, x2, y1, y2;
cin >> x1 >> y1 >> x2 >> y2;
seg[i] = {x1, y1, y2, 1};
seg[i + n] = {x2, y1, y2, -1};
xs[i] = y1;
xs[i + n] = y2;
}
n *= 2;
sort(seg + 1, seg + 1 + n);
sort(xs + 1, xs + 1 + n);
build(1, 1, n);
int ans = 0;
for (int i = 1; i < n; ++i)
{
update(1, seg[i].y1, seg[i].y2, seg[i].flag);
ans += (seg[i + 1].x - seg[i].x) * tr[1].len;
}
cout << ans << '\n';
return 0;
}
带注释
/*
* @Author: NEFU AB-IN
* @Date: 2023-03-28 20:04:18
* @FilePath: \Acwing\1228\1228.cpp
* @LastEditTime: 2023-03-28 20:50:10
*/
#include
using namespace std;
#define int long long
#define SZ(X) ((int)(X).size())
#define ALL(X) (X).begin(), (X).end()
#define IOS \
ios::sync_with_stdio(false); \
cin.tie(nullptr); \
cout.tie(nullptr)
#define DEBUG(X) cout << #X << ": " << X << '\n'
typedef pair<int, int> PII;
#define ls p << 1
#define rs p << 1 | 1
const int N = 1e5 + 10, INF = 0x3f3f3f3f;
// 题目给的n是1e4, 如果这么开的话会RE,所以要多开十倍!!!比较保险
struct Tree
{
int l, r, len, cnt; // len: 有效高度,cnt: 覆盖次数
} tr[N << 3]; // 要开8倍空间(这是建立在n是正常开的基础上的,但我们这里n多开了十倍,所以这里也可以开4倍)
struct Line
{
int x, y1, y2, flag; // flag 就代表 矩阵左边的线为1,右边的线为-1
bool operator<(Line &a) const // 其实这里这么写也可以 bool operator<(Line a)
{
return x < a.x;
} // 按x坐标排序
} seg[N];
int n, xs[N]; // 离散化数组
void pushup(int p)
// 个人理解:和线段树的pushup一样,都叫pushup,而且都是往上传
// 普通线段树的pushup是一个情况,而这个是分为两种情况,所以两个地方(if中和函数最后)都得pushup
// 本质上是 len属性的两种情况更新
// 两种情况:一种是有标记,那么就自己处理len;另一种是没标记,那么就要等于子节点的len和
{
if (tr[p].cnt)
tr[p].len = tr[p].r - tr[p].l;
else
tr[p].len = tr[ls].len + tr[rs].len;
}
void build(int p, int l, int r)
{
// 这就是我之前说的,在build的时候就已经离散化了,也就是这里的l,r已经不是普通线段树的l,r了,已经下标对应到真实的y坐标了,具体可根据上图理解
tr[p] = {xs[l], xs[r], 0, 0};
if (r == l + 1) // 这里也是不同的地方,因为叶子宽度为2
return;
int mid = l + r >> 1;
build(ls, l, mid);
build(rs, mid, r); // 一定要注意这里是mid,不是mid+1,这里的下标是允许重合的
}
void update(int p, int L, int R, int v)
{
if (tr[p].l >= R || tr[p].r <= L) // 越界 return,这里也是不同点,也就是我们递归到的区间不在[L, R]中,那就return
return;
if (tr[p].l >= L && tr[p].r <= R)
{
tr[p].cnt += v; // 打上标记,v的取值是1或-1,cnt的值就不止1了
pushup(p); // 这里需要pushup!!! 其实类似于普通线段树的区间修改,这里也要更新结点的属性,但是len没更新,我们是把它的更新放到了pushup中
return;
}
update(ls, L, R, v); // 这里也是不同点,由于设置了越界,每个分支都得进,判断交给越界即可
update(rs, L, R, v);
pushup(p); // 最后正常pushup
}
signed main()
{
IOS;
int n;
cin >> n;
for (int i = 1; i <= n; ++i)
{
int x1, x2, y1, y2;
cin >> x1 >> y1 >> x2 >> y2;
seg[i] = {x1, y1, y2, 1};
seg[i + n] = {x2, y1, y2, -1};
xs[i] = y1;
xs[i + n] = y2;
}
n *= 2; // n的范围扩大一倍,因为两条边界
//有序!
sort(seg + 1, seg + 1 + n);
sort(xs + 1, xs + 1 + n);
build(1, 1, n);
int ans = 0;
for (int i = 1; i < n; ++i) // 只需到n-1即可,因为每当遍历一条边时,它右边的面积就算上了,所以最后的就不用了 (当然写成n也可以)
{
// 先更新 再加和
update(1, seg[i].y1, seg[i].y2, seg[i].flag);
ans += (seg[i + 1].x - seg[i].x) * tr[1].len;
// tr[1].len 即当前的有效高度
}
cout << ans << '\n';
return 0;
}