poj 1177 && hdu 1828

来源:互联网 发布:linux双系统升级win10 编辑:程序博客网 时间:2024/06/05 12:41

题目概述

给定N个矩形左下角和右上角顶点坐标,求其组合所得图形的总周长
矩形的边一定平行或垂直与坐标轴

时限

2000ms/6000ms

输入

第一行正整数N,其后N行,每行4个整数,描述两个顶点坐标,输入到EOF为止

限制

0<=N<=5000;-10000<=坐标值<=10000

输出

每行一个数,为所求图形周长

样例输入

7
-15 0 5 10
-5 8 20 25
15 -4 24 14
0 -6 16 4
2 15 10 22
30 10 36 20
34 0 40 16
3
0 1 2 2
2 1 4 2
1 0 3 1

样例输出

228
12

讨论

计算几何,扫描线+离散化+线段树,下面的讨论中部分思想/名词是基于poj 1151的,建议先看一下那个题的讨论部分
从基本思想上看,扫描线和以前一样是平行与x轴的,那么应如何获得平行于x轴部分的边长呢?额的想法和之前做那两个求面积的(poj 1151, poj 1389)差不多,也是用到了覆盖层的概念,也用到了有效宽度的概念,唯一不同的是,每一次得到新的有效宽度,和上一次的做差取绝对值

在这张图中,红线是从下向上移动的扫描线,其会在处理完黄色矩形上底边后计算周长,此时有效长度1,上次有效长度3,因而周长加上abs(1-3)=2
为什么这是正确的?不难考虑,如果两条边不重叠,两次有效宽度的差的绝对值应当是两条边宽度和,而如果重叠,差就成了此次新增/删的宽度值,是不包含互相覆盖的那部分宽度的,而增添会导致有效宽度上升(确切说是不下降),删除会导致有效宽度下降(确切是不上升),那么只要取绝对值,最后得到的便是周长,如果不取绝对值呢?自然是0
解决了水平方向边的计量,对于竖直方向的处理就有了两个方向,其一,将扫描线旋转90度,再从左到右扫描一次,其二,找一种方法在处理水平边时顺带把竖直边处理,法1固然通俗易懂,但需要把同样的算法写两遍,跑两遍,太麻烦,这里容易想到进行扫描线处理后,纵坐标被分成了若干段,为何不能直接把前后纵坐标的差乘2(左右各计一次)直接加上?实际上能想到乘2就不难想到乘4乘8或乘更多,见下面这张图

现在需要乘以几?显然简单的乘以2无法应对,稍微拓展一番思维,在相邻的两条水平线中间,竖直的线将图形划分为若干有色(实心)部分,那竖直的线和有色区域的数量是什么关系?2倍,每个有色区域的左右各有一条竖线,因而有多少有色区域,乘以2,再乘以纵坐标差就是这一段竖直方向贡献的周长
到这里,基本思路基本清晰,下面就是实现问题了
主要的操作和求面积都差不多,也不再赘述,水平部分的周长直接取前后有效宽度的差的绝对值,上面也说的很明白,简单说一下处理竖直方向,虽然作为人类很容易看出来相邻的有色区域应该视为一块,但线段树只认识1+1=2,这显然不对,因而需要一点额外信息让程序也知道应该少数一块,很简单,只要记录下线段树每个节点的覆盖层是否与边界接触了就可以,比如一个节点表示范围1到4,如果范围3到4的计数器非0,那这就是右接触,如果范围4到7又碰巧左接触,那么其父节点(范围1到7)在计算有色区域时就需要-1,因为这是同一块有色区域,而父节点的接触信息是取自左子树的左接触情况和右子树的右接触情况,记录这个信息并不需要太多内存,两个布尔型,甚至两个位就够
另外有一个以前就想过但是没多考虑的问题,就是离散变量的重复问题,额做前面两个题的时候对于离散变量都没有去重,也都过了,本以为这回也一样,结果真的碰到不能过的数据了(第二组样例),为了发现他也是费了一些周折,最后加了一条unique函数成功解决
代码通过后习惯性看了看讨论版,发现国家队99年陈宏的论文正是以此为例,没有仔细看,不过好像是讲的数据结构,额如果不会做这个题,恐怕看了他论文只会更头晕吧

题解状态

408K,16MS,C++,1957B

题解代码

#include<cstdio>#include<cstring>#include<algorithm>#include<cmath>using namespace std;#define INF 0x3f3f3f3f#define MAXN 5002#define memset0(a) memset(a,0,sizeof(a))#define EPS 1e-8struct Ln//line 水平线的结构{    int l, r, y, f;//left right 线段左右端点横坐标 y 纵坐标 flag 上底边是1 下底边是-1 配合覆盖层使用    bool operator<(Ln &b)const//方便排序的    {        return y < b.y;    }}lns[MAXN * 2];//5000个矩形 10000条水平边int N;//矩形总数int hashx[MAXN * 2], L[MAXN * 10], R[MAXN * 10], c[MAXN * 10], len[MAXN * 10], len2[MAXN * 10];//hashx 名为哈希 实为将横坐标离散化 后面的数组是原本线段树的 L,R 节点指代范围(离散下标) cover 覆盖层 只有非0的时候才能记录有效宽度 也会影响竖直边的记录 length 有效宽度 应对水平方向的边用 length2 有色区域数 应对竖直方向的边用char blank[MAXN * 10];//记录左右接触情况 其实只用到其中2个位void build(int i, int l, int r)//构造树 i是递归用的内部参数 l,r是节点范围(离散下标){    L[i] = l, R[i] = r;//只有这两个信息需要初始化 其他的都会自动清零    if (l + 1 != r) {//构造对应一个点的节点没有任何意义        build(i * 2, l, (l + r) / 2);        build(i * 2 + 1, (l + r) / 2, r);    }}void modify(int i, Ln &a)//更新树 i仍然是内部参数{    if (a.l == hashx[L[i]] && a.r == hashx[R[i]])//线段与节点范围等宽        c[i] += a.f;//修改覆盖层    else if (a.l >= hashx[L[i * 2 + 1]])//线段完全在节点右子树范围内        modify(i * 2 + 1, a);    else if (a.r <= hashx[R[i * 2]])//线段完全在节点左子树范围内        modify(i * 2, a);    else {//线段横跨左右子树        Ln b = a;        b.r = hashx[R[i * 2]];//从左右子树分界点切割开        modify(i * 2, b);//一半更新左子树        b = a;        b.l = hashx[L[i * 2 + 1]];        modify(i * 2 + 1, b);//一半更新右子树 如此保证了线段永远不会超出节点指代范围    }    if (c[i]) {//计算两个len 如果被完全覆盖        len[i] = hashx[R[i]] - hashx[L[i]];//有效宽度就是其指代的宽度        blank[i] = 3;//二进制是11 左右都接触        len2[i] = 1;//计1个有色区域    }    else {//没有覆盖或不完全覆盖        len[i] = len[i * 2] + len[i * 2 + 1];//直接取左右子树有效宽度和 对于叶节点左右子树有效宽度都是0 这点不受既往数据影响        len2[i] = len2[i * 2] + len2[i * 2 + 1] - ((blank[i * 2] & 1) && (blank[i * 2 + 1] & 2));//两部分有色区域数相加 如果左子树右接触同时右子树左接触 则额外-1        blank[i] = (blank[i * 2] & 2) + (blank[i * 2 + 1] & 1);//然后更新该节点的接触情况 取左子树左接触情况和右子树右接触情况    }}int fun(){    for (int p = 1; p <= N; p++) {        int x1, y1, x2, y2;        scanf("%d%d%d%d", &x1, &y1, &x2, &y2);//input        lns[p].l = x1, lns[p].r = x2, lns[p].y = y1, lns[p].f = 1;        lns[N + p].l = x1, lns[N + p].r = x2, lns[N + p].y = y2, lns[N + p].f = -1;        hashx[p] = x1, hashx[N + p] = x2;    }    sort(lns + 1, lns + 2 * N + 1);    sort(hashx + 1, hashx + 2 * N + 1);//先对离散数组排序    build(1, 1, unique(hashx + 1, hashx + 2 * N + 1) - hashx - 1);//建树之前通过unique函数去重并确定实际建树范围    int peri = 0, lastlen = 0;//perimeter 周长 为什么不是C lastlen 上次的有效宽度    for (int p = 1; p <= 2 * N; p++) {//虽然离散数组去重了 但是水平线段还是那么多        modify(1, lns[p]);//先更新树        peri += abs(len[1] - lastlen) + len2[1] * 2 * (lns[p + 1].y - lns[p].y);//然后计算能加到周长的宽度和高度        lastlen = len[1];    }    return peri;}int main(void){    //freopen("vs_cin.txt", "r", stdin);    //freopen("vs_cout.txt", "w", stdout);    while (~scanf("%d", &N)) {//input        printf("%d\n", fun());//output    }}

EOF

0 0