【学习笔记】线段树—扫描线补充 (IC_QQQ)
【学习笔记】线段树—扫描线补充 (IC_QQQ)
(感谢 \(IC\)_\(QQQ\) 大佬授以本内容的著作权。此人超然于世外,仅有 \(Luogu\) 账号 尚可膜拜)
上题:
给出N个矩形,求最后所得的图形的面积(周长)。
比如这个图形,我们去掉多余的线条,看看它最后的图形:
看到这个花里胡哨的图形,它的面积就是抛开红色的其他部分面积和。
周长呢?就是下面这个图形的红色边长度。
问题是怎么求,这就需要用到扫描线~了。
先解决简单一些的面积:
我们假象有一条垂直于水平面的直线,从左到右依次扫过这个图形,那直线被覆盖的长度(L)只有在每个矩形的左右边界才会变化。
继续上图:
这个图形被2N(8)条边分成了2N-1(7)个部分。
最后的答案就是这2N-1(7)个部分面积的和。
每个部分的面积又怎么求呢?显而易见 \(S=L*该段的宽\) 。
该怎么实现呢?
我们首先能想到的是,这2N条边至少有3个属性: \(x\),\(y_1\) , \(y_2\)。
如果一个矩形扫描完了呢,肯定要删除吧。
第四个属性就呼之欲出了,记录这是左边界还是右边界。
如果是左边界就记为+1,表示加入了扫描线;
如果是右边界就记为 -1,表示退出了扫描线。
好,想到这里,我们就可以把这2N条边取出来,记做四元组\((x,y_1,y_2,k)\)。
左边界\(k=1\),右边界\(k=-1\)。
然后我们按\(x\)排序,就得到了上面这个图。
可以看到,这条扫描线被纵坐标\(y\)分成了很多段,我们用数组\(c[i]\)记录扫描线第\(i\)段被覆盖的次数。
下一步:
接下来我们就可以模拟扫描线从左到右扫一遍啦!
我们来走一遍流程:
- 这条扫描线到达了边\(x\),修改扫描线。
- 扫描线的状态固定了,影响一直持续到下一条边\(x’\)。
- 我们找到扫描线被覆盖的部分长度和\(H\)。
- 那这一部分(\(x\)→\(x’\))的面积就找到了:\(H*(x’-x)\),\(ans\)加上这一部分。
\(emmmmm\),可以看到,这个流程并不好走,我们遇到了一抹多的问题。
怎么修改扫描线:
我们取出这条边的纵坐标\(y_1,y_2\),那它们对应着扫描线的哪几段呢?
先把所有的纵坐标放到一个数组\(row\)中,从小到大排序,\(y\)的下标\(val(y)\)就是第几段。
找到了对应的位置,把这些位置加上\(k\)。
H怎么找
至于\(H\)怎么找,我们可以朴素的扫描一遍\(c\)数组,只要\(c[i]>=1\),我们就加上这一段的长度。
又有一个问题:扫描线第\(i\)段的长度怎么找?
这个问题和前面怎么修改扫描线那个问题是相辅相成的。
第\(i\)段的长度就是\(row[i+1]-row[i]\)。
离散化
把所有的\(y\)放进数组\(row\)中,从小到大排序,去重,\(i\)就作为\(row[i]\)的代表数,然后\(val(y)\)(\(y\)的代表数)可以用一个函数查询。下面给出代码:
void pre_work_row(){
sort(row+1,row+1+2*n);//排序
tot=unique(row+1,row+1+2*n)-(row+1);//去重
return;
}
int query(int val){//查询代表数
return lower_bound(row+1,row+1+tot,val)-row;
}
怎么和线段树联系呢:
可以看到,数组\(c\)支持区间修改和查询,这可以用线段树维护,但是不同的题,数组\(c\)存放的值不一样,线段树维护的内容和方式也不一样,我们结合具体的例题讲解。
【题目链接】
好,又领悟了一门新的功法:扫描线~ 求解面积。
接下来,我们就去应用一下这门功法。AKIOI变强指日可待。
窗口的星星 \(Stars\) \(in\) \(Your\) \(Window\) \([P1502]\) \([POJ2482]\)
【标签】线段树/离散化/扫描线
【题解】\(IC\)_\(QQQ\)
【标签】线段树/扫描线
矩形周长 \(Picture\) \([USACO5.5]\) \([P1856]\) \([POJ1177]\)
【标签】线段树/扫描线
【题解】Atlantis
描述
有几个古希腊文本包含传说中的亚特兰蒂斯岛的描述,其中一些文本甚至包括岛屿部分地图。但不幸的是,这些地图描述了亚特兰蒂斯的不同区域。您的朋友Bill必须知道地图的总面积。 你(不明智地)自告奋勇写了一个计算这个数量的程序。
输入
输入包含几个测试用例。每个测试用例以整数\(n\)(\(1 \leqslant n \leqslant 100\))的开始。以下\(n\)行描述了每个地图。这些行中的每一行包含四个数字\(x1; y1; x2; y2\)(\(0 \leqslant x1 <x2 \leqslant 100000; 0 \leqslant y1 <y2 \leqslant 100000\)),不一定是整数。值(\(x1; y1\))和(\(x2; y2\))是左下角的坐标。 映射区域的右上角。输入文件由\(0\)终止,不处理它。
输出
略
样例
样例输入:
2
10 10 20 20
15 15 25 25.5
0
样例输出:
Test case #1
Total explored area: 180.00
分析
这是一道模(mú)板题
这道题呢,数组\(c\) 存的是覆盖次数,我们想想怎么用线段树优化。
这道题中,我们只关心扫描线被覆盖的总长度,除了左右端点 \((l,r)\),至少还要维护区间长度 \(len\),而区间长度受被覆盖次数的影响,肯定还要维护区间的覆盖次数 \(cnt\)。
所以,线段树的每个节点就出来了: \(l,r,len,cnt\)。
如果当前节点 \(cnt \geqslant 1\),\(len=row(r+1)-row(l)\)。
否则,当前节点 \(len=(\)左儿子\()len+(\)右儿子\()len\)。
这道题的特殊之处在于:不需要下传\(cnt\)。
我们想想为什么。
因为四元组 \((x,y_1,y_2,k)\) 是成对出现的,而且\(cnt\)是记录区间被覆盖次数,只要\(cnt>=1\),无论\(cnt\)是多少, \(len=row(r+1)-row(l)\) 是不变的,因此我们没有必要下传 \(cnt\)。
我们可以模拟一下,不下传\(cnt\)对结果有没有影响。
代码实现:
好,接下来,我们就可以来看看这道题怎么写了:
代码看似很长,其实真的很长很亲切的
#include<cstdio>//一定在VJ上选C++
#include<cstring>
#include<iostream>
#include<algorithm>
using namespace std;
const int N=205;
int n,nn,tot,Case;//n:矩形个数,nn:边的数量(2*n)
//tot:离散化后,代表数的个数
double ans,row[N];//答案,离散化数组
struct node{
int k;double x,u,v;//横坐标,纵坐标,v>u
}da[N];//四元组
struct aaa{int l,r,cnt;double len;}tree[4*N];//数组大小是重点
bool cmp(node a,node b){return a.x<b.x;}
void pre_reset(){
nn=2*n;ans=0;tot=0;
memset(da,0,sizeof(da));
memset(row,0,sizeof(row));
memset(tree,0,sizeof(tree));
}
void scan_work(){
double a,b,c,d;
for(int i=1;i<=n;i++){
scanf("%lf%lf%lf%lf",&a,&b,&c,&d);
row[i]=b;row[i+n]=d;
da[i].x=a;da[i+n].x=c;
da[i].u=b;da[i+n].u=b;
da[i].v=d;da[i+n].v=d;
da[i].k=1;da[i+n].k=-1;
}
sort(da+1,da+1+nn,cmp);
return;
}
void pre_row_work(){
sort(row+1,row+1+nn);
tot=unique(row+1,row+1+nn)-(row+1);
return;
}
int query(double val){
return lower_bound(row+1,row+1+tot,val)-row;
}
void built_work(int pos,int l,int r){
tree[pos].l=l;tree[pos].r=r;
if(l==r) return;
int mid=(l+r)>>1;
built_work(pos*2,l,mid);
built_work(pos*2+1,mid+1,r);
return;
}
void push_up(int pos){
int ls=pos*2,rs=pos*2+1;
if(tree[pos].cnt)//如果当前区间被覆盖
tree[pos].len=row[tree[pos].r+1]-row[tree[pos].l];
else{//当前区间没有被覆盖
if(tree[pos].l==tree[pos].r) tree[pos].len=0;//只有一段
//也可以不加这个特判,但tree要再开大一倍,防止越界
else tree[pos].len=tree[ls].len+tree[rs].len;
}
return;
}
void change(int pos,int l,int r,int w){
if(l<=tree[pos].l&&r>=tree[pos].r){
tree[pos].cnt+=w;
push_up(pos);//传递信息
return;
}
int mid=(tree[pos].l+tree[pos].r)>>1;
if(l<=mid) change(pos*2,l,r,w);
if(r>mid) change(pos*2+1,l,r,w);
push_up(pos);//传递信息
return;
}
int main(){
while(1){
scanf("%d",&n);
if(n==0) break;
pre_reset();//清零
scan_work();//读入
pre_row_work();//离散化
built_work(1,1,tot-1);//建树
for(int i=1;i<nn;i++){
int u=query(da[i].u);
int v=query(da[i].v)-1;
change(1,u,v,da[i].k);//修改扫描线
double H=tree[1].len;
ans+=(da[i+1].x-da[i].x)*H;
}
printf("Test case #%d\n",++Case);
printf("Total explored area: %.2lf\n\n",ans);
}
}
【题解】矩形周长 Picture
描述
略
输入
输入文件的第一行是一个整数\(N(0 \leqslant N \leqslant 5000)\),表示有多少个矩形。接下来N行给出了每一个矩形左下角坐标和右上角坐标(所有坐标的数值范围都在\(-10000\)到\(10000\)之间)。所有矩形顶点的坐标都是整数。
输出
输出文件只有一个正整数,表示最终图形的周长。
举例说明
这是给出的图形:
这是最终的图形:
尝试解决:
老规矩,取出\(2N\)条边,记为四元组:\((x,y_1,y_2,k)\)。左边界\(k=1\),右边界\(k=-1\)。
以\(x\)为关键元从小到大排序这些四元组。
对所有的\(y\)离散化,得到\(tot\)个代表数。
扫描线被分成了\(tot-1\)段。
然后呢?然后怎么办:
我们模拟扫一遍,列出扫描线的所有状态,大家想想怎么解决。
通过上面的思索,我们发现,\(c[i]\) 还是表示第\(i\)段被覆盖的次数。
修改和\(Atlantis\)也一模一样:
- 修改扫描线。
- 查询扫描线被覆盖长度\(len\)。
唯一有所不同的是,如何累加\(ans\)。
经过我们的探究,高就是\(len_i-len_{i-1}\)。
长呢?这需要在扫描\(c[i]\)求\(len\)时多做一步,求出扫描线被覆盖的段数\(num\)。
并且前面我们探究过了,长就是\(x_{i+1}-x_i*num*2\)。
\(ans\)累加上高和长就好了。
用线段树优化:
对四元组排序时,\(x\)相等怎么办?
根据前面的分析,很容易想到,需要维护区间被覆盖次数\(cnt\),区间长度\(len\),区间的被覆盖段数\(num\)。
前面两个大家都会写吧,重点是新多出来的这个\(num\)。
我们再来模拟修改。
如果当前节点\(cnt\geqslant 1\),\(num=1\)。
否则,\(num=(\)左儿子\()num+(\)右儿子\()num\)。
但是呢,事情没那没简单,如果遇到这种情况怎么办?
下面来看一张图:
咳咳,懒得画图了。
总之,我们线段树还需要维护两个变量\(lg,rg\),记录该区间的左右端点是否被覆盖。
如果当前节点\(cnt\geqslant1\),\(lg=1,rg=1\)。
否则,\(lg=(\)左儿子\()lg,rg=(\)右儿子\()rg\)。
统计\(num\)时,如果碰到上述情况(\(cnt<1\),左儿子\(rg=1\),右儿子\(g=1\)),\(num--\)。
代码实现:
#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
using namespace std;
const int N=1e4+5;
int n,nn,ans;
int tot,last;//last:上次扫描线被覆盖长度
int row[N];
struct node{int x,u,v,k;}da[N];
struct aaa{
int cnt,num,len;
bool lg,rg;
}tree[4*N];
bool cmp(node a,node b){
if(a.x!=b.x) return a.x<b.x;
return a.k>b.k;
}
void pre_work_row(){
sort(row+1,row+1+nn);
tot=unique(row+1,row+1+nn)-(row+1);
return;
}
int query(int x){
return lower_bound(row+1,row+1+tot,x)-row;
}
void Pushup(int pos,int pl,int pr){
int ls=pos*2,rs=pos*2+1;
if(tree[pos].cnt){
tree[pos].num=1;
tree[pos].len=row[pr+1]-row[pl];
tree[pos].lg=tree[pos].rg=1;
}
else{
tree[pos].num=tree[ls].num+tree[rs].num;
if(tree[ls].rg&&tree[rs].lg) tree[pos].num--;
tree[pos].len=tree[ls].len+tree[rs].len;
tree[pos].lg=tree[ls].lg;
tree[pos].rg=tree[rs].rg;
}
return;
}
void change(int pos,int pl,int pr,int l,int r,int w){
if(l<=pl&&r>=pr){
tree[pos].cnt+=w;
Pushup(pos,pl,pr);
return;
}
int mid=(pl+pr)>>1;
if(l<=mid) change(pos*2,pl,mid,l,r,w);
if(r>mid) change(pos*2+1,mid+1,pr,l,r,w);
Pushup(pos,pl,pr);
return;
}
int main(){
scanf("%d",&n);
int a,b,c,d;nn=2*n;
for(int i=1;i<=n;i++){
scanf("%d%d%d%d",&a,&b,&c,&d);
row[i]=b;row[i+n]=d;
da[i].x=a;da[i+n].x=c;
da[i].u=b;da[i+n].u=b;
da[i].v=d;da[i+n].v=d;
da[i].k=1;da[i+n].k=-1;
}
pre_work_row();
sort(da+1,da+1+nn,cmp);
for(int i=1;i<=nn;i++){
last=tree[1].len;
change(1,1,tot-1,query(da[i].u),query(da[i].v)-1,da[i].k);
ans+=abs(tree[1].len-last);
ans+=(da[i+1].x-da[i].x)*tree[1].num*2;
}
printf("%d",ans);
return 0;
}
【学习笔记】线段树—扫描线补充 (IC_QQQ)的更多相关文章
- hdu 5091(线段树+扫描线)
上海邀请赛的一道题目,看比赛时很多队伍水过去了,当时还想了好久却没有发现这题有什么水题的性质,原来是道成题. 最近学习了下线段树扫描线才发现确实是挺水的一道题. hdu5091 #include &l ...
- 线段树 扫描线 L - Atlantis HDU - 1542 M - City Horizon POJ - 3277 N - Paint the Wall HDU - 1543
学习博客推荐——线段树+扫描线(有关扫描线的理解) 我觉得要注意的几点 1 我的模板线段树的叶子节点存的都是 x[L]~x[L+1] 2 如果没有必要这个lazy 标志是可以不下传的 也就省了一个pu ...
- 【Codeforces720D】Slalom 线段树 + 扫描线 (优化DP)
D. Slalom time limit per test:2 seconds memory limit per test:256 megabytes input:standard input out ...
- Codeforces VK CUP 2015 D. Closest Equals(线段树+扫描线)
题目链接:http://codeforces.com/contest/522/problem/D 题目大意: 给你一个长度为n的序列,然后有m次查询,每次查询输入一个区间[li,lj],对于每一个查 ...
- 【POJ-2482】Stars in your window 线段树 + 扫描线
Stars in Your Window Time Limit: 1000MS Memory Limit: 65536K Total Submissions: 11706 Accepted: ...
- HDU 4419 Colourful Rectangle --离散化+线段树扫描线
题意: 有三种颜色的矩形n个,不同颜色的矩形重叠会生成不同的颜色,总共有R,G,B,RG,RB,GB,RGB 7种颜色,问7种颜色每种颜色的面积. 解法: 很容易想到线段树扫描线求矩形面积并,但是如何 ...
- BZOJ-3228 棋盘控制 线段树+扫描线+鬼畜毒瘤
3228: [Sdoi2008]棋盘控制 Time Limit: 10 Sec Memory Limit: 128 MB Submit: 23 Solved: 9 [Submit][Status][D ...
- BZOJ-3225 立方体覆盖 线段树+扫描线+乱搞
看数据范围像是个暴力,而且理论复杂度似乎可行,然后被卡了两个点...然后来了个乱搞的线段树+扫描线.. 3225: [Sdoi2008]立方体覆盖 Time Limit: 2 Sec Memory L ...
- POJ1151+线段树+扫描线
/* 线段树+扫描线+离散化 求多个矩形的面积 */ #include<stdio.h> #include<string.h> #include<stdlib.h> ...
随机推荐
- nginx4win10 文件下载服务器
默认root是Nginx下目录html. 我们在其目录下新建download目录,然后在该目录下copy几个供下载的文件. 在浏览器输入http://localhost:9001/download/y ...
- POJ 1026 置换群的k次幂问题
题目大意: 给定了一组对应关系,经过k次幂后,得到新的对应关系b[i],然后将给定的字符串上的第i位字符放置到b[i]的位置上, 如果字符串长度不足n就用空格补足,这里的是空格,也就是str[i] = ...
- HDU 1561 树形DP背包问题
这是自己第一道背包上树形结构问题,不是很理解这个概念的可以先看看背包九讲 自己第一次做,看了一下别人的思路,结合着对简单背包问题的求解方式自己一次AC了还是有点小激动的 题目大意是: 攻克m个城市,每 ...
- Regular Number 字符串匹配算法 Shift_and
Using regular expression to define a numeric string is a very common thing. Generally, use the shape ...
- C#: 根据指定压缩比率压缩图片
直接上代码: /// <summary> /// 根据指定压缩比率压缩图片 /// </summary> /// <param name="original&q ...
- jsp,velocity,freemark页面引擎的比較
在java领域.表现层技术主要有三种:jsp.freemarker.velocity. jsp是大家最熟悉的技术 长处: 1.功能强大,能够写java代码 2.支持jsp标签(jsp tag) 3.支 ...
- Unity3D 怎样在安卓手机上播放视频
曾经仅仅会在电脑上通过Unity3D播放视频,研究了下发现通过Unity3D在手机上播放视频也很easy.现介绍例如以下. void OnGUI() { if (GUI.But ...
- POJ 1861 Network (Kruskal求MST模板题)
Network Time Limit: 1000MS Memory Limit: 30000K Total Submissions: 14103 Accepted: 5528 Specia ...
- willRotateToInterfaceOrientation 屏幕旋转
/* 1.屏幕发生旋转后, 这个旋转事件会先传递给window的rootViewController(窗口的根控制器) 2.rootViewController又会将这个旋转事件传递给它的子控制器 * ...
- myeclipse中Servlet出错
在myeclipse中Servlet总是出错,执行的时候一直提示找不到出现404错误, 然后把代码拷贝到eclipse ee中就没有问题,一直不理解怎么回事. 然后发了好长时间试了好些方法,结果把my ...