题目描述
在二维平面上有一个无限的网格图形,初始状态下,所有的格子都是空白的。
现在有n个操作,每个操作是选择一行或一列,并在这行或这列上选择两个端点网格,把以这两个网格为端点的区间内的所有网格染黑(包含这两个端点)。
问经过n次操作以后,共有多少个格子被染黑,重复染色同一格子只计数一次。
样例
输入
3
1 2 3 2
2 5 2 3
1 4 3 4
输出
8
题解
似乎评论区都挂的是$O(n^2)$的代码。这题数据量是$1e4$,感觉会超。实际上拿$O(n^2)$跑下来是1447ms。
这里提供$O(nlogn)$的解法:离散化后先区间合并,之后把合并后的所有线段长累加进$ans$中。对于竖着的线段,在起点和终点分别打上tag。维护一个BIT,从下到上扫描,每次询问横着的区间内有多少根竖着的线段,和为交点个数。每次减掉即可。
AC代码
代码写得很丑。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int MAXN = 3e5 + 10;
void compress(vector <int> &a)
{
sort(a.begin(), a.end());
a.erase(unique(a.begin(), a.end()), a.end());
}
int getIndex(vector <int> &a, int t)
{
return lower_bound(a.begin(), a.end(), t) - a.begin() + 1;
}
struct seg
{
int l, r, pos;
};
struct node
{
int posX, posY, tag;
};
bool cmpSeg(seg a, seg b)
{
if (a.pos == b.pos) return a.l < b.l;
else return a.pos < b.pos;
}
bool cmpNode(node a, node b)
{
if (a.posY == b.posY) return a.tag > b.tag;
else return a.posY < b.posY;
}
void combine(vector <seg> &a, vector <seg> &cbna)
{
int l = a[0].l, r = a[0].r, prevPos = a[0].pos;
for (int i = 1; i < a.size(); ++i)
{
if (a[i].pos != prevPos)
{
cbna.push_back({l, r, prevPos});
l = a[i].l, r = a[i].r, prevPos = a[i].pos;
}
else
{
if (a[i].l <= r) r = max(r, a[i].r);
else
{
cbna.push_back({l, r, prevPos});
l = a[i].l, r = a[i].r;
}
}
}
cbna.push_back({l, r, prevPos});
}
int BIT[MAXN] = {0};
void modify(int x, int c, int upper)
{
while (x <= upper) BIT[x] += c, x += x & -x;
}
int query(int x)
{
int s = 0;
while (x > 0) s += BIT[x], x -= x & -x;
return s;
}
int main()
{
ios::sync_with_stdio(false);
int n; cin >> n;
vector <seg> segX, segY;
vector <int> posX, posY;
vector <node> tags;
for (int i = 1; i <= n; ++i)
{
int x1, y1, x2, y2; cin >> x1 >> y1 >> x2 >> y2;
if (x1 == x2) segY.push_back({min(y1, y2), max(y1, y2), x1});
else segX.push_back({min(x1, x2), max(x1, x2), y1});
posX.push_back(x1), posX.push_back(x2);
posY.push_back(y1), posY.push_back(y2);
}
compress(posX), compress(posY);
sort(segX.begin(), segX.end(), cmpSeg), sort(segY.begin(), segY.end(), cmpSeg);
vector <seg> cbnSegX, cbnSegY;
combine(segX, cbnSegX), combine(segY, cbnSegY);
ll ans = 0;
for (auto &i : cbnSegX) ans += i.r - i.l + 1;
for (auto &i : cbnSegY)
{
ans += i.r - i.l + 1;
tags.push_back({i.pos, i.l, 1}), tags.push_back({i.pos, i.r, -1});
}
sort(tags.begin(), tags.end(), cmpNode);
int i = 0, j = 0;
for (int p = 0; p < posY.size(); ++p)
{
while (j < tags.size() && tags[j].posY == posY[p] && tags[j].tag == 1)
modify(getIndex(posX, tags[j].posX), 1, posX.size()), ++j;
while (i < cbnSegX.size() && cbnSegX[i].pos == posY[p])
ans -= query(getIndex(posX, cbnSegX[i].r)) - query(getIndex(posX, cbnSegX[i].l) - 1), ++i;
while (j < tags.size() && tags[j].posY == posY[p] && tags[j].tag == -1)
modify(getIndex(posX, tags[j].posX), -1, posX.size()), ++j;
}
cout << ans << endl;
return 0;
}
大佬的代码写得很好,学到了很多东西,但由于水平有限,看了几个小时才明白,所以我在大佬的代码上加上了一些自己的理解,希望能帮到和我一样刚刚开始学习的同学。
有几个可能思维会有点难转换的点:
1、离散化之后posX和posY中只有输入的x和y坐标
例如
posX
中只有1 2 3
,posY
中只有2 3 4 5
,所以posX
和posY
中的x和y都是会被用到的2、合并区间后,
cbnSegX
和cbnSegY
中的区间都是两两不相交的(如果相交会被合并为一个区间),这样一个范围[y1, y2]内的一个横区间和纵区间只会有一个交点。3、在主函数的
for
循环中,对于posY
中的每一个y坐标,由于在第1点的基础上,所以只有两种情况(可能同时出现):①y是某个横区间
segY
的头(在BIT的segY.pos
处加1)或者尾(在BIT的segY.pos
处减1)②y是某个纵区间的pos(可能不止一个纵区间)
对于第一种情况,第一个和第三个whlie循环可以解决,而如果是第二种情况,那么不会进入第一个和第三个循环,而是直接进入第二个while循环求前缀和,由于在第一个循环中在BIT的
segY.pos
处加上了1,那么对于某个纵区间,求[l, r]的前缀和时,若该纵区间与目前的横区间有交点,那么前缀和肯定会为1,否则为0,这样在第二个while循环中就会减去交点的个数。而由于已经对区间和结点排过序了,所以i
和j
是不会回退的,当目前的y为某个横区间的尾时,求完这一列的交点后,下一个y肯定是大于目前这个y的,所以当前y之前的所有列上的纵区间肯定全部遍历过了,所以令BIT中当前segY.pos
位置的值减1,通俗的讲就是取消这一行的标记,下一个y如果是下一个横区间的头,那么标记下一个横区间,直到所有的posY
中的y坐标都被遍历过。(注意segY.pos
指的是一个x坐标,即某一行)这是我个人的理解,如果有错误希望各位大佬能指出来,希望能对初学的同学有一些启发
modify
和query
两个函数不是太懂,可以再详细解释一下吗~可以搜一下Binary Indexed Tree,树状数组,也就是题解里的BIT。
谢谢大佬,现在会了
nlogn交上去大概就100ms+,快的不是一点点。牛
有没有兄弟能告诉我一下离散化的目的是什么呢,实在啃不动这位老哥的代码,太强了
坐标范围是$10^9$级别的,直接开数组也开不下,但考虑到$n=10^4$,用到的坐标数量最多也不超过$4\times 10^4$个,离散化之后把坐标范围映射到$10^4$级别,方便维护
大佬的文章竟然没人看.挺遗憾的
支持一下,不管怎样也是nlogn算法~ 虽然n^2也能ac。精益求精更好!!~~