【ACWing】247. 亚特兰蒂斯

这篇博客介绍了一种利用扫描线和线段树算法来计算多个矩形覆盖面积的方法。通过将问题转化为对每个矩形边界的处理,结合线段树的数据结构,实现了对每个矩形覆盖的面积进行累加,从而得到总面积。算法的时间复杂度为O(nlogn),空间复杂度为O(n)。这种方法适用于处理大型数据集,例如在处理包含大量不规则区域的地图面积计算时。
摘要由CSDN通过智能技术生成

题目地址:

https://www.acwing.com/problem/content/249/

有几个古希腊书籍中包含了对传说中的亚特兰蒂斯岛的描述。其中一些甚至包括岛屿部分地图。但不幸的是,这些地图描述了亚特兰蒂斯的不同区域。您的朋友Bill必须知道地图的总面积。你自告奋勇写了一个计算这个总面积的程序。

输入格式:
输入包含多组测试用例。对于每组测试用例,第一行包含整数 n n n,表示总的地图数量。接下来 n n n行,描绘了每张地图,每行包含四个数字 x 1 , y 1 , x 2 , y 2 x_1,y_1,x_2,y_2 x1,y1,x2,y2(不一定是整数), ( x 1 , y 1 ) (x_1,y_1) (x1,y1) ( x 2 , y 2 ) (x_2,y_2) (x2,y2)分别是地图的左上角位置和右下角位置。注意,坐标轴 x x x轴从上向下延伸, y y y轴从左向右延伸。当输入用例 n = 0 n=0 n=0时,表示输入终止,该用例无需处理。

输出格式:
每组测试用例输出两行。第一行输出Test case #k,其中 k k k是测试用例的编号,从 1 1 1开始。第二行输出Total explored area: a,其中a是总地图面积(即此测试用例中所有矩形的面积并,注意如果一片区域被多个地图包含,则在计算总面积时只计算一次),精确到小数点后两位数。在每个测试用例后输出一个空行。

数据范围:
1 ≤ n ≤ 10000 1≤n≤10000 1n10000
0 ≤ x 1 < x 2 ≤ 100000 0≤x_1<x_2≤100000 0x1<x2100000
0 ≤ y 1 < y 2 ≤ 100000 0≤y_1<y_2≤100000 0y1<y2100000
注意,本题 n n n的范围上限加强至 10000 10000 10000

思路是扫描线 + 线段树。首先对于 x x x y y y的方向,我们可以直接以数学里的直角坐标系的规定来看,这不影响答案的正确性。接着,可以用”积分“的思想,将整个图形划分为若干不相交的矩形。想象有条竖直的线,从左向右扫描,在扫描的过程中,扫过的部分一定能写为若干不相交矩形的并。所以我们只需要将每部分矩形的面积总和加起来就行了。如下图所示:
在这里插入图片描述
想象一个数轴(即图中的 y y y轴),对于这个 y y y轴要做两种操作,一个是将某个区间的权值整体加 1 1 1 − 1 -1 1(分别代表扫描线扫到了矩形左右边界),另一个是询问当前有多少长度的权值为正。可以用线段树来做。构造一个线段树,我们先将所有的 y y y坐标做离散化(主要原因是 y y y坐标不一定是整数,需要先离散化为整数才方便线段树进行操作),设离散化后一共 n n n个点,则有 n − 1 n-1 n1个区间,让这些区间被线段树所维护,线段树每个节点存的是维护的区间的两个端点区间的下标(这里的意思是,若该节点维护的区间是 [ a 1 , a k ] = [ a 1 , a 2 ] ∪ [ a 2 , a 3 ] ∪ . . . ∪ [ a k − 1 , a k ] [a_1,a_k]=[a_1,a_2]\cup [a_2,a_3]\cup...\cup [a_{k-1},a_k] [a1,ak]=[a1,a2][a2,a3]...[ak1,ak],那么该节点需要存储 [ a 1 , a 2 ] [a_1,a_2] [a1,a2] [ a k − 1 , a k ] [a_{k-1},a_k] [ak1,ak]的下标),以及该区间被覆盖的长度和覆盖次数(即权值)。修改操作只会修改覆盖次数。其两个操作以及pushup操作如下:
1、pushup操作:如果当前区间权值为正,则其覆盖长度等于其维护的区间的长度;如果当前区间权值为 0 0 0(当然不可能为负),则看一下是否是叶子节点(即是否维护的区间是单个”原子“区间),如果不是,则其覆盖长度就是其两个儿子覆盖长度之和;否则覆盖长度是 0 0 0
2、查询操作:只会查询树根的覆盖长度;
3、修改操作:如果当前节点的区间被完全覆盖,则直接增加或减少权值,否则递归修改左右儿子,最后pushup。

我们看到,由于查询操作只会对树根查询,所以不需要pushdown。对于修改操作,由于操作是成对出现的,并且成对的两次操作是对同样的区间进行了先增 1 1 1后减 1 1 1的操作,而且每次询问的答案只与每个区间被没被覆盖有关,而跟其覆盖了多少次无关(也就是说覆盖了 1 1 1次还是多次都不影响答案,答案只与次数是不是 0 0 0有关),所以这里是不需要pushdown的(具体可以用数学归纳法证明。每次修改操作可以视为是对线段树中的若干个节点的权值做了增 1 1 1或减 1 1 1的操作,可以归纳假设每次操作完,每个节点的权值是计算正确的,这里的计算正确的意思是,它本身的权值只和直接对它自己的操作有关,不考虑其父亲被操作的影响。那么每个节点维护的覆盖长度在pushup的作用下计算是正确的。接下来考虑下一次操作的影响,如果其是将权值加 1 1 1,则pushup完之后每个区间的覆盖长度仍然正确;如果是减 1 1 1,那么就是将之前加过的某若干个区间权值都减 1 1 1,且减完仍然非负,这样pushup完之后每个区间的覆盖长度也仍然正确)。代码如下:

#include <iostream>
#include <cstring>
#include <algorithm>
#include <vector>
using namespace std;

const int N = 100010;
int n;
struct Segment {
  double x, y1, y2;
  int k;
  bool operator<(const Segment &t) const { return x < t.x; }
} seg[N * 2];

struct Node {
  // l和r存的是当前节点维护的区间的两个端点区间在离散化情况下的下标
  int l, r;
  // cnt存当前节点维护的区间整体的权值
  int cnt;
  // len存当前节点维护的区间内被覆盖的长度
  double len;
} tr[N * 8];

vector<double> ys;

// 找到y离散化后的下标
int find(double y) {
  return lower_bound(ys.begin(), ys.end(), y) - ys.begin();
}

// pushup只为了更新len
void pushup(int u) {
  if (tr[u].cnt) tr[u].len = ys[tr[u].r + 1] - ys[tr[u].l];
  else {
    if (tr[u].l != tr[u].r) tr[u].len = tr[u << 1].len + tr[u << 1 | 1].len;
    else tr[u].len = 0;
  }
}

void build(int u, int l, int r) {
  tr[u] = {l, r};
  if (l == r) return;
  int mid = l + r >> 1;
  build(u << 1, l, mid), build(u << 1 | 1, mid + 1, r);
}

void modify(int u, int l, int r, int k) {
  if (tr[u].l >= l && tr[u].r <= r) {
    tr[u].cnt += k;
  } else {
    int mid = tr[u].l + tr[u].r >> 1;
    if (l <= mid) modify(u << 1, l, r, k);
    if (r > mid) modify(u << 1 | 1, l, r, k);
  }
    
  pushup(u);
}

int main() {
  int T = 1;
  while (cin >> n, n) {
    ys.clear();
    for (int i = 0, j = 0; i < n; i++) {
      double x1, y1, x2, y2;
      scanf("%lf%lf%lf%lf", &x1, &y1, &x2, &y2);
      seg[j++] = {x1, y1, y2, 1};
      seg[j++] = {x2, y1, y2, -1};
      ys.push_back(y1), ys.push_back(y2);
    }
		
	// 执行对y坐标的离散化,先排序,然后去重
    sort(ys.begin(), ys.end());
    ys.erase(unique(ys.begin(), ys.end()), ys.end());
    build(1, 0, ys.size() - 2);

    sort(seg, seg + n * 2);

    double res = 0;
    for (int i = 0; i < n * 2; i++) {
      if (i) res += tr[1].len * (seg[i].x - seg[i - 1].x);
      modify(1, find(seg[i].y1), find(seg[i].y2) - 1, seg[i].k);
    }

    printf("Test case #%d\n", T++);
    printf("Total explored area: %.2lf\n\n", res);
  }
}

每组数据时间复杂度 O ( n log ⁡ n ) O(n\log n) O(nlogn),空间 O ( n ) O(n) O(n)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值