线段树系列之——扫描法

例题

不管是一个技术还是一个算法,最好的学习场景就是一次实践、一道题目。这次要根据线段树中的扩展,看一题经典的线段树扫描法。题目链接

题目大意:
给出n个矩形的左下角坐标和右上角坐标。然后求这些矩形面积的总和。如下图:

给出了三个矩形

我们把这幅图处理一下,然后下图中的红色部分的面积就是我们需要求得面积总和(可能有点丑)

红色部分的面积就是需要求得的面积总和

条件:0<=x<=100000、0<=y<=100000、n<=100

解析(先看懂解决方法,再进行实践扩展)

首先我们把这个“畸形”分成若干个矩形,如下图


畸形分割

如上五块面积之和就是需要求的面积大小。

我们拿出其中一块的面积,矩形面积=x轴对应长度(下面称为宽度)×y轴对应长度(下面称为高度)

在接收坐标的时候,我们把这几条黄线都存下来,用如下结构体(y_starty_end的位置如上图)

struct Line {
    double y_end,y_start,x;  //y_start是y轴下顶点坐标,y_end是y轴的上顶点坐标,x是x轴的坐标
    int flag;  //标记,矩形左边是1,右边是-1
}line[2*maxn];

然后我们模拟一个扫描的行为——其实就是黄线从左到右的for循环
在扫描的过程中,很容易求得,line[i+1].x-line[i].x就是这个矩形的宽度。那高度怎么求呢?

然后线段树就来了


线段树维护的y轴区间

这次我们把这个“畸形”按y轴分割成5个部分。这五个部分投影到y轴也就是5个线段——位置从下标0到下标5

我们发现上面我们要求的高度,无非就是这5个线段的组合而已。比如,第①个面积的高度是线段1-4,第②个面积的高度是线段0-4,第③个面积的高度是线段0-3,第④个面积的高度是线段0-5,第⑤个面积的高度是线段2-5当然有一个大前提,这个线段没有单位,每一段都是一个抽象的概念,比如线段0-1的实际长度可能是12,线段1-2的实际长度可能是29

那怎么知道现在扫描到的这个矩形的高度是哪几个线段的组合呢??

我们对这5段建立线段树(当然测试用例肯定不止5段线段)

struct node {
    int l,r,flag;  //l、r表示线段的起点和终点,flag表示是否需要把这一段加入到面积的计算中
    double len;  //长度
}tree[100*maxn];

void build(int i,int l,int r){   //建立线段树
    tree[i].l = l;
    tree[i].r = r;
    tree[i].flag = tree[i].len = 0;   //长度和标记默认为0
    if(l+1 == r) return;
    int mid = (l+r)/2;
    build(2*i,l,mid);   //递归建树,直到叶节点位置,直到每一段都是最小单位
    build(2*i+1,mid,r);
}

岔开一下,到这里,说明一下为什么要用线段树,因为前面存在一个标记,矩形的左边是1,右边是-1,标记为1表示下面这部分y轴的相对距离(这部分线段)都是要加入到面积的计算的。如果矩形的面积为0,那这部分线段就不需要加入面积的计算。所以它需要在扫描的过程中不断的更新每一个线段的状态。所以用线段树来实现

代码不难,可能更新的过程需要解释一下

void calcul(int i) {
    if(tree[i].flag) tree[i].len = y_axis[tree[i].r] - y_axis[tree[i].l];   //如果标记为>=1,则长度等于这个线段的高度
    else if(tree[i].l+1 == tree[i].r) tree[i].len = 0;    //如果为叶子阶段,且标记为0,则置1
    else tree[i].len = tree[2*i].len + tree[2*i+1].len;   //如果不是叶子节点,那么此节点的长度就是子节点的长度之和
}
void updata(int i,int l,int r,int flag) {
    if(tree[i].l>r || tree[i].r<l)   return;   //不符合条件
    if(tree[i].l>=l && tree[i].r<=r) {   //这个段在指定下标所在段之间
        tree[i].flag += flag;    //累加标记,1-1=0可以抵消标记
        calcul(i);   //计算这个一段的有效长度(能加入面积计算的长度)
        return;
    }
    updata(2*i,l,r,flag);  //如果这个段不在指定下标所在的段之间,就遍历子节点
    updata(2*i+1,l,r,flag);
    calcul(i);
}
…………
for(int i=0;i<t-1;i++){    //扫描这几条线
    startY = lower_bound(y_axis, y_axis+t, line[i].y_start) - y_axis;   //找到这条线的下顶点所表示的线段下标
    endY = lower_bound(y_axis, y_axis+t, line[i].y_end) - y_axis;    //找到这条线的上顶点所表示的线段下标
    updata(1,startY,endY,line[i].flag);  //更新这个两个下标所对应的线段的状态,line[I].flag传入的是1或者-1
    area += tree[1].len * (line[i+1].x - line[i].x);   //面积累加
}
…………

在边有重叠的情况下,y轴有重叠需要去重,因为建树叶子节点需要唯一。而x轴不需要去重,可以思考一下为啥~


重叠边的情况

去重过程

 for(int i=1;i<t;i++)
    if(y_axis[i-1] != y_axis[I])
         y_axis[len++] = y_axis[I];
build(1,0,--len);   //--len表示去重后的长度,也就是建树需要的长度
去重后只需要维护四个线段

总结

写了很长的解析部分,总结一下思路把

对于这个畸形,作与y轴平行的线,将这个“畸形”分成5个部分,通过从左往右扫描这几条线,取得相应的面积。

每到达一条线时,(前面已经维护了x=a这条边的y轴起点和终点),将对应的边映射到线段树中。然后更新线段树。如果这是左边,那么flag=1,在更新时,主要是把这条边加入到面积的计算中。如果这是右边,那么flag=0,在更新时,主要是把这条边从面积的计算中移除。

那是不是每次扫描到右边都没有面积,因为都是在处理移除操作。

并不是。比如,扫描到第一条线是左边,将线段0-2加入面积计算。扫描到第二条线是左边,将线段3-4加入到面积的计算中,总线段是(0-2) + (3-4)。扫描到第三条边是右边,将线段0-2移除面积计算。但是在移除的同时我们发现第二次扫描的线段3-4还在面积的计算中,所以第三次扫描计算面积时,总线段是3-4

扫描到最后,取得的就是总的面积

AC代码

#include <iostream>
#include <string>
#include <algorithm>
#define maxn 110
using namespace std;
struct node {
    int l,r,flag;  //l、r表示线段的起点和终点,flag表示是否需要把这一段加入到面积的计算中
    double len;  //长度
}tree[100*maxn];
struct Line {
    double y_end,y_start,x;  //y_start是y轴下顶点坐标,y_end是y轴的上顶点坐标,x是x轴的坐标
    int flag;  //标记,矩形左边是1,右边是-1
}line[2*maxn];
double y_axis[2*maxn];
int cmp(Line a,Line b){
    return a.x<b.x;
}
void build(int i,int l,int r){   //建立线段树
    tree[i].l = l;
    tree[i].r = r;
    tree[i].flag = tree[i].len = 0;   //长度和标记默认为0
    if(l+1 == r) return;
    int mid = (l+r)/2;
    build(2*i,l,mid);   //递归建树,直到叶节点位置,直到每一段都是最小单位
    build(2*i+1,mid,r);
}
void calcul(int i) {
    if(tree[i].flag) tree[i].len = y_axis[tree[i].r] - y_axis[tree[i].l];   //如果标记为>=1,则长度等于这个线段的高度
    else if(tree[i].l+1 == tree[i].r) tree[i].len = 0;    //如果为叶子阶段,且标记为0,则置1
    else tree[i].len = tree[2*i].len + tree[2*i+1].len;   //如果不是叶子节点,那么此节点的长度就是子节点的长度之和
}
void updata(int i,int l,int r,int flag) {
    if(tree[i].l>r || tree[i].r<l)   return;   //不符合条件
    if(tree[i].l>=l && tree[i].r<=r) {   //这个段在指定下标所在段之间
        tree[i].flag += flag;    //累加标记,1-1=0可以抵消标记
        calcul(i);   //计算这个一段的有效长度(能加入面积计算的长度)
        return;
    }
    updata(2*i,l,r,flag);  //如果这个段不在指定下标所在的段之间,就遍历子节点
    updata(2*i+1,l,r,flag);
    calcul(i);
}
int main(){
    double x1,y1,x2,y2,area;
    int t,startY,endY,cas=1,n,len;;
    while(scanf("%d",&n)==1 && n) {
        t=0;len=1;area=0;
        for(int i=0;i<n;i++) {
            scanf("%lf %lf %lf %lf",&x1,&y1,&x2,&y2);
            y_axis[t] = y1;
            y_axis[t+1] = y2;
            line[t].flag = 1;
            line[t+1].flag = -1;
            line[t].x = x1;
            line[t+1].x = x2;
            line[t].y_start = line[t+1].y_start = y1;
            line[t].y_end = line[t+1].y_end = y2;
            t += 2;
        }
        sort(y_axis,y_axis+t);
        sort(line,line+t,cmp);
        for(int i=1;i<t;i++)   //y轴去重,x轴不需要去重,可以思考一下为啥
            if(y_axis[i-1] != y_axis[I])
                y_axis[len++] = y_axis[I];
        build(1,0,--len);  //--len表示去重后的长度,也就是建树需要的长度
        printf("Test case #%d\n",cas++);
        for(int i=0;i<t-1;i++){    //扫描这几条线
            startY = lower_bound(y_axis, y_axis+t, line[i].y_start) - y_axis;   //找到这条线的下顶点所表示的线段下标
            endY = lower_bound(y_axis, y_axis+t, line[i].y_end) - y_axis;    //找到这条线的上顶点所表示的线段下标
            updata(1,startY,endY,line[i].flag);  //更新这个两个下标所对应的线段的状态,line[I].flag传入的是1或者-1
            area += tree[1].len * (line[i+1].x - line[i].x);   //面积累加
        }
        printf("Total explored area: %0.2lf\n\n",area);
    }
    return 0;
}


扩展

题目链接

题意

给出n个矩形的左下角坐标和右上角坐标(和上面的一样)。每个矩形编号从1~n。现在要进行若干次查询,每次查询的格式如3 1 4 5,第一个3表示后面跟了3个编号,需要求的是这3个编号的矩形的面积总和

所以这题和上面这题的区别是,上面是求所有矩形面积的总和。而这题需要求其中几个编号的矩形的面积总和。

解析

在上面的代码的基础上,增加一个结构体,用来存储所有的节点

struct rec {
    int x1,y1,x2,y2;
}cache[2*maxn];

然后每次查询的时候重新建树即可

AC代码
#include <iostream>
#include <string>
#include <algorithm>
#define maxn 110
using namespace std;
struct node {
    int l,r,flag,len;  //l、r表示线段的起点和终点,flag表示是否需要把这一段加入到面积的计算中
}tree[100*maxn];
struct Line {
    int y_end,y_start,x;  //y_start是y轴下顶点坐标,y_end是y轴的上顶点坐标,x是x轴的坐标
    int flag;  //标记,矩形左边是1,右边是-1
}line[2*maxn];
struct rec {
    int x1,y1,x2,y2;
}cache[2*maxn];
int y_axis[2*maxn];
int cmp(Line a,Line b){
    return a.x<b.x;
}
void build(int i,int l,int r){   //建立线段树
    tree[i].l = l;
    tree[i].r = r;
    tree[i].flag = tree[i].len = 0;   //长度和标记默认为0
    if(l+1 == r) return;
    int mid = (l+r)/2;
    build(2*i,l,mid);   //递归建树,直到叶节点位置,直到每一段都是最小单位
    build(2*i+1,mid,r);
}
void calcul(int i) {
    if(tree[i].flag) tree[i].len = y_axis[tree[i].r] - y_axis[tree[i].l];   //如果标记为>=1,则长度等于这个线段的高度
    else if(tree[i].l+1 == tree[i].r) tree[i].len = 0;    //如果为叶子阶段,且标记为0,则置1
    else tree[i].len = tree[2*i].len + tree[2*i+1].len;   //如果不是叶子节点,那么此节点的长度就是子节点的长度之和
}
void updata(int i,int l,int r,int flag) {
    if(tree[i].l>r || tree[i].r<l)   return;   //不符合条件
    if(tree[i].l>=l && tree[i].r<=r) {   //这个段在指定下标所在段之间
        tree[i].flag += flag;    //累加标记,1-1=0可以抵消标记
        calcul(i);   //计算这个一段的有效长度(能加入面积计算的长度)
        return;
    }
    updata(2*i,l,r,flag);  //如果这个段不在指定下标所在的段之间,就遍历子节点
    updata(2*i+1,l,r,flag);
    calcul(i);
}
int main(){
    int t,startY,endY,cas=1,n,m,r,k,len,area;
    while (cin>>n>>m){
        if (n==0 && m==0) break;
        for (int i=1; i<=n; i++)
            scanf("%d %d %d %d",&cache[i].x1,&cache[i].y1,&cache[i].x2,&cache[i].y2);
        printf("Case %d:\n",cas++);
        for (int i=1; i<=m; i++) {
            scanf("%d",&r);
            t=0;len=1;area=0;
            for (int j=0; j<r; j++) {
                scanf("%d",&k);
                y_axis[t] = cache[k].y1;
                y_axis[t+1] = cache[k].y2;
                line[t].flag = 1;
                line[t+1].flag = -1;
                line[t].x = cache[k].x1;
                line[t+1].x = cache[k].x2;
                line[t].y_start = line[t+1].y_start = cache[k].y1;
                line[t].y_end = line[t+1].y_end = cache[k].y2;
                t += 2;
            }
            sort(y_axis,y_axis+t);
            sort(line,line+t,cmp);
            for(int i=1;i<t;i++)   //y轴去重,x轴不需要去重,可以思考一下为啥
                if(y_axis[i-1] != y_axis[i])
                    y_axis[len++] = y_axis[i];
            build(1,0,--len);  //--len表示去重后的长度,也就是建树需要的长度
            for(int i=0;i<t-1;i++){    //扫描这几条线
                startY = lower_bound(y_axis, y_axis+t, line[i].y_start) - y_axis;   //找到这条线的下顶点所表示的线段下标
                endY = lower_bound(y_axis, y_axis+t, line[i].y_end) - y_axis;    //找到这条线的上顶点所表示的线段下标
                updata(1,startY,endY,line[i].flag);  //更新这个两个下标所对应的线段的状态,line[I].flag传入的是1或者-1
                area += tree[1].len * (line[i+1].x - line[i].x);   //面积累加
            }
            printf("Query %d: %d\n",i,area);
        }
        printf("\n");
    }
    return 0;
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,905评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,140评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,791评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,483评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,476评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,516评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,905评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,560评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,778评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,557评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,635评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,338评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,925评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,898评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,142评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,818评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,347评论 2 342