题目描述
有几个古希腊书籍中包含了对传说中的亚特兰蒂斯岛的描述。
其中一些甚至包括岛屿部分地图。
但不幸的是,这些地图描述了亚特兰蒂斯的不同区域。
您的朋友Bill必须知道地图的总面积。
你自告奋勇写了一个计算这个总面积的程序。
输入格式
输入包含多组测试用例。
对于每组测试用例,第一行包含整数n,表示总的地图数量。
接下来n行,描绘了每张地图,每行包含四个数字$x1,y1,x2,y2$(不一定是整数),$(x1,y1)$和$(x2,y2)$分别是地图的左上角位置和右下角位置。
当输入用例$n=0$时,表示输入终止,该用例无需处理。
输出格式
每组测试用例输出两行。
第一行输出”Test case #k”,其中k是测试用例的编号,从1开始。
第二行输出“Total explored area:a”,其中a是总地图面积(即此测试用例中所有矩形的面积并,注意如果一片区域被多个地图包含,则在计算总面积时只计算一次),精确到小数点后两位数。
在每个测试用例后输出一个空行。
数据范围
$1≤n≤100$
$0≤x_1<x_2≤100000$
$0≤y_1<y_2≤100000$
样例
输入样例:
2
10 10 20 20
15 15 25 25.5
0
输出样例:
Test case #1
Total explored area: 180.00
线段树+扫描线+二分查找+离散化
这道题目是真的有毒还是我太菜了,各种算法大杂烩,最后还是看了五篇优秀的blog东凑西凑才理解的.为什么我要自告奋勇写这道题目,可见出题人的内心有多么友好啊......
离散化:首先坐标太大了,我们需要离散化.
二分:既然离散化了,自然就要二分找坐标了.
扫描线:题目告诉我们,需要将二维转化成为一维,所以我们需要扫描线这种变态级别神器来解答这道题目.具体来说,我们就是要取出N个矩阵的左右边界,然后从下往上或者从左往右的方向,不断地扫描,然后我们就会求出每一条扫描线在矩阵上覆盖的长度L,然后这一段面积就是L*宽度.
我觉得自己讲得不好,主要是思路也有些混乱,所以给出几位网上大佬的题解,希望能让各位不浪费点击我这条博客的流量,虽然图片很多.手动滑稽
现在假设我们有一根线,从下往上开始扫描
如图所示,我们可以把整个矩形分成如图各个颜色不同的小矩形,那么这个小矩形的高就是我们扫过的距离,那么剩下了一个变量,那就是矩形的长一直在变化。
我们的线段树就是为了维护矩形的长,我们给每一个矩形的上下边进行标记,下面的边标记为1,上面的边标记为-1,每遇到一个矩形时,我们知道了标记为1的边,我们就加进来这一条矩形的长,等到扫描到-1时,证明这一条边需要删除,就删去,利用1和-1可以轻松的到这种状态。
还要注意这里的线段树指的并不是线段的一个端点,而指的是一个区间,所以我们要计算的时候r+1和r-1
再提一下离散化,离散化就是把一段很大的区间映射到一个小区间内,这样会节省大量空间,要进行离散化,我们先对端点进行排序,然后去重,然后二分找值就可以了
以下为另外一位大佬的blog
这道题我一直在纠结,怎么求当前有扫描线上有的线段总长?怎么lazy下放?我一直想的是每个点维护的都是它维护的这个区间内的总的cnt等等。
后来我发现换个思路,一切都很简单!
我的每个节点t[x].l~t[x].r维护的其实是线段t[x].l~(t[x].r+1),也就是若干条线段,因为点分成左右孩子的时候会有问题(比如[3,3]维护的到底是什么?)。
我们要把每个节点看成是一条线段。
对于每个节点维护两个值:
cnt:这个点所代表的线段被覆盖了多少次。
len:以这个点为根的子树中被覆盖的区间一共有多长。
当一条线段进来的时候,在代表它的那若干个节点上cnt++,其它节点cnt不用加。
然后len维护的就是这个区间内那些cnt>0的节点所覆盖的区间总长。
其实线段树上每个节点都可以有它的实际意义
C++ 代码
//这道题目码风有些诡异,虽然我已经尽量变成我这种通俗易懂的码风了.
#include <bits/stdc++.h>
using namespace std;
const int N=1100;
double dis[N];
struct node
{
double x1,x2,h;
int f;
void init(double l2,double r2,double h2,int key)
{
x1=l2;
x2=r2;
h=h2;
f=key;
}
} line[N];
struct line_tree
{
int l,r,f;
double len;
} t[N<<2];
int cmp(node a,node b)
{
return a.h<b.h;
}
void build(int p,int l,int r)
{
t[p].l=l;
t[p].r=r;
t[p].f=0;
t[p].len=0;
if (l==r)
return ;
int mid=(l+r)>>1;
build(p<<1,l,mid);
build((p<<1)+1,mid+1,r);
}
void get_len(int p)
{
if (t[p].f>=1)
t[p].len=dis[t[p].r+1]-dis[t[p].l];//这里记得+1
else
t[p].len=t[p<<1].len+t[(p<<1)|1].len;
}
void change(int p,int l,int r,int v)
{
if (t[p].l==l && r==t[p].r)
{
t[p].f+=v;
get_len(p);
return ;
}
int mid=(t[p].l+t[p].r)>>1;
if (r<=mid)
change(p<<1,l,r,v);
else if (l>mid)
change((p<<1)+1,l,r,v);
else
{
change(p<<1,l,mid,v);
change((p<<1)+1,mid+1,r,v);
}
get_len(p);
}
int find(double p,int l,int r)//诡异版二分
{
while(l<=r)
{
int mid=(l+r)>>1;
if (dis[mid]==p)
return mid;
if (dis[mid]<p)
l=mid+1;
else
r=mid-1;
}
}
int main()
{
int ts=0,n;
while(scanf("%d",&n)!=EOF)
{
if (n==0)
break;
printf("Test case #%d\n",(++ts));
int num=0;
for(int i=0;i<n;i++)
{
double x1,x2,y1,y2;
scanf("%lf%lf%lf%lf",&x1,&y1,&x2,&y2);
line[num].init(x1,x2,y1,1);
dis[num++]=x1;
line[num].init(x1,x2,y2,-1);
dis[num++]=x2;
}
sort(line,line+num,cmp);
sort(dis,dis+num);
int dis_num=unique(dis,dis+num)-dis;
build(1,0,dis_num-1);
double ans=0;
for(int i=0;i<num-1;i++)
{
int line_l=find(line[i].x1,0,dis_num);
int line_r=find(line[i].x2,0,dis_num)-1;
change(1,line_l,line_r,line[i].f);
ans=ans+t[1].len*(line[i+1].h-line[i].h);
}
printf("Total explored area: %.2lf\n\n",ans);
}
return 0;
}
图 全部 炸了。/kel
图挂了
图挂了
图炸了
我的每个节点t[x].l~t[x].r维护的其实是线段t[x].l~(t[x].r+1),也就是若干条线段,因为点分成左右孩子的时候会有问题(比如[3,3]维护的到底是什么?)。 这是为什么???
图挂了
图炸了
想请教:每个节点t[x].l~t[x].r维护的其实是线段t[x].l~(t[x].r+1)
为啥是t[x].r+1啊
图挂了+1
图挂了
图逝了
想请教一下为什么要离散化呀?感觉数据范围100000不是特别大的
因为坐标是可以是小数,不都是整数的
噢噢,懂了,谢谢
图没了
这个题正常的 lazy标记 是不是不行啊
什么是正常的$\text{Lazy}$标记啊
就是pushdown的那个,这题不是用性质没pushdown吗?
可以呀,但是根本写不出来0.0
我还是不能理解这到题目,已经耗了一个下午了,紫书上的做法越看越怀疑。
为什么每篇题解讲到无lazy优化就打止了?
为什么lyd的无lazy优化是$O(n)$的?
不会吧。Lazy标记式线段树区间修改的必备操作,否则极有可能超时。
Lyd大佬的代码是$O(n)$,我觉得emm,可能这就是大佬吧。
其实我也不知道,我也没有看到代码。
我和同学讨论了一个小时,晓得为什么了,但是书上还存在一个疑问,这到题目抽象出来显然就是给出一个序列,支持区间加和操作,询问一段区间中大于等于k的元素个数,这到题目我思维太死了,一定要求线段树存储一个固定的信息,而这里只有标记,但是书上的做法是正确的,但是解释的太令人不懂了,而且它的代码显然是错的。另外它提及了它的做法是因为本题的特殊性质,就是只查询根节点和修改的对应,它还提及了,它的做法不必要延迟标记,言外之意就是存在延迟标记的做法,但是这个做法应该能够解决我抽象出来的这个更加一般的题目,但是书上就打止了,太吊人胃口了。
我想知道这个延迟标记的做法。
做题应该寻找通解。
就是开一个Lazy数组标记。
然后加一个Push_down操作。
评论框,我根本就不好解释懒惰标记,也就是延迟标记啊。
这个东西要在分享里面,讲义里面才能讲解清晰啊。
毕竟是一个小算法。。。
延迟标记,其实就是一句话可以理解的意思。
就是有修改操作,但是我不做处理。
直到这个点要被查询了,我再去修改。
你的就是书上的做法,没有特殊性质就是错的啊
已找到通解,同学提供的。
延迟标记,从来不需要特殊性质,至少我刷了这么多道题目,只要是区间修改,就用延迟标记。
他就是通解。
不信你自己对拍一下,把这些特殊性质去掉
你的做法是不是带个标记和答案,如果标记大于0被覆盖长度为该区间,否则为两个子节点的和?如果搬到那个序列模型,加入你有一个大区间,你打上一个标记,显然你是不会下放的,但是如果你此时想修改大区间的小区间,你怎么办。
还有的是线段树为什么这样写,去拆分询问区间?
另外,你没有特判叶子节点,所以空间要开8倍。
不需要啊,线段树有人证明,最多空间开四倍。
区间划分是线段树的套路。
你可以看看我的线段树讲义,应该可以给你一个有力的援助。
显然会下放。每次查询的时候,如果大区间有标记,会迅速下传到儿子节点,而且根据包含判断,我们会迅速剪掉不需要的部分。我之前写的代码,都不是我的风格,我现在的线段树模板,已经更新了,是当前最为流行&通用的模板。
我估计你可能是被我之前的模板给弄模糊了。。。。
说声抱歉,然后建议看看我的新模板。
你没有下放的操作,而且对于一个区间被标记了,也就是你的$f\geq 1$,那么你直接得到该区间被覆盖的长度,否则为两个子节点的和,这是你的思想对吧,那么对于叶子节点你进行同样的操作,是不是访问不存在的节点?而且因为数据的问题,你这个错误没有暴露出来。
我这里似乎木有Push_down操作,看来我当时学这个数据结构的时候,太菜了。
还有数据没有问题吧。。。
这个不就是官方数据包的东西吗。。。。
HDU千人检测的题目,应该不存在锅。。。
话说您能不能出一组Hack数据呢。。。。
我已经把这道题目标记了一下,日后估计会更新这道题目的代码吧。。。。
最近追星ing & 自闭ing.
所以这种扫描线的题目,我估计很难回答啊。。。。
沉迷状态压缩DP不可自拔,该死的SHOI2013的day2T3,怎么就这么难呢。。。。
您太fake了,我想说的不是数据有问题,而是数据太”强”了。
hack数据只要保证你访问的叶子节点未被初始化就行了,但是该题的线段树信息太稀疏了,不好保证你正好撞到,而且询问的时候正好被用,而且考场上你也不会想到开8倍空间,因此你会re。
这到题目我花了一个中午加一个下午加一个晚上,几乎一天呢
线段树还有什么模板流行?这是为了方便标记永久化和可持久化才这样写的,而且标记永久化的使用范围比较窄。
zkw线段树比较流行了.
这种模板算是目前流行好用的呢.如果二进制水平高,可以用一下zkw线段树
深陷sam无法自拔,现在都还没有看懂。
后缀自动机大佬,您是要AK省选吧....
您太fake了,我机房最菜,别人都已经学完了。
大佬这题可以不需要pushDown,书上没有说错。因为区间修改是成对存在的,不存在子节点需要继承父节点信息的情况。只存在父节点需要子节点更新的情况,所以只用pushUp即可。
为什么N改205就SE了呢?
数据范围开小了.
n≤100 按理说不超过200个点吧?ಥ_ಥ
线段树空间复杂度至少*4
线段树是完全2叉树,应该开4倍就一定够用了吧。我的意思是把您上面题解的const int 的N变
为205,那线段树就是205*4,应该是够用的吧?
明白您的意思了.四倍的确够用的.
这题是多组数据.num一直在累加.
我的代码没有特殊初始化.
什么是“特殊初始化”呢?您题解中明明每次都初始化num了啊
哦,初始化了,我自己没有看到,写了太久忘得差不多了
QwQ,我去看数据去了,我感觉可能是数据的锅,
大佬您还在嘛,我也有这个疑问QAQ
线段树开N * 8才能过,为什么呢
感觉应该是离散化的锅,离散化的总元素(y坐标数量)个数是 2*N,线段树本身要求 4 倍,因此就是 8 倍了。
还要注意这里的线段树指的并不是线段的一个端点,而指的是一个区间,所以我们要计算的时候r+1和r-1
这个什么意思,看了半天没看懂
稍等yxc老师课程上完.
因为我们这个是扫描线,所以肯定不是线段的一个端点
而一个区间,肯定就是由左端点+右端点构成的
r+1,r-1是我打错了.QwQ
r+1和r-1是在不同的地方.
好的,谢谢