扫描线

简介

扫描线一般运用在图形上面,顾名思义,就是一条线在整个图上扫来扫去,它一般被用来解决图形面积,周长等问题。

扫描线的主要思想是虚拟出一条平行与 x 或 y 轴的无限长的线一路扫过去,如果发现触碰到矩形的边(被当前扫描线完全覆盖)就停下进行相关操作。

一、面积并

题型:在二维坐标系上,给出多个矩形的左下以及右上坐标,求出所有矩形构成的图形的面积。

考虑一个这样的例子

假设一个自下而上的扫描线,每次会在碰到横边的时候停下来

为了快速计算出截线段长度,可以将横边赋上不同的权值,具体为:对于一个矩形,其下边权值为 1,上边权值为 −1

然后把所有的横边按照 y 坐标升序排序。这样,对于每个矩形,扫描线总是会先碰到下边,然后再碰到上边。那么就能保证扫描线所截的长度永远非负

这样操作以后,就可以和线段树扯上关系。先把所有端点在 x 轴上离散化(其实就是把所有点的横坐标存到 X 数组里,然后排个序并且去重

在这个例子中,4 个端点的纵投影总共把 x 轴切割成了 5 部分。取中间的 3 部分线段,建立一棵线段树,其每个端点维护一条线段(也就是一个区间)的信息,之后整个问题就转化为了一个区间查询问题,即每次将当前扫描线扫到的边对应的信息按照之前赋上的权值更新,然后再查询线段树根节点的信息,最后得到当前扫描线扫过的面积

一些具体实现

对于线段树,我们大部分情况维护一个 sum 为当前区间被几个矩形覆盖,以及一个 len 表示当前区间被覆盖的区间长度

建树

void build(int i, int l, int r) {
  tree[i].l = l; tree[i].r = r;
  tree[i].len = tree[i].sum = 0;
  if (l == r) return;
  int mid = (l + r) >> 1;
  build(i << 1, l, mid);
  build(i << 1 | 1, mid + 1, r);
  return;
} 

pushup

我们扫到一条线,就对这个区间的 sum 进行修改,再由 cnt 对 len 修改,并且把 len 向上更新

inline void pushup(int i) {
  int l = tree[i].l, r = tree[i].r;
  if (tree[i].sum) tree[i].len = x[r + 1] - x[l]; //被覆盖过则更新长度
  else tree[i].len = tree[i << 1].len + tree[i << 1 | 1].len; //合并儿子信息
}

在 pushup 函数中我们对于被完全覆盖的区间,是用了其区间右端点 +1 进行计算。这是因为在线段树的叶节点上,线段树上的区间是 [l, l] 但是这样的区间在几何中长度为 0 了,所以我们令区间 [l, r] 表示的区间实际为 [x[l], x[r+1]] (x 是离散化数组) 就可以解决这个问题

例题

P5490 【模板】扫描线

#include <bits/stdc++.h>
using namespace std;
const int maxn = 1e6 + 10;
int n;
long long x1, yy1, x2, y2, ans;
long long x[maxn << 1];
struct scanline{
  long long l, r, h;
  int flag;
  bool operator < (const scanline &num) const {
    return h < num.h;
  }
}line[maxn << 1];
struct node{
  int l, r, sum;
  long long len;
}tree[maxn << 2];
void build(int i, int l, int r) {
  tree[i].l = l; tree[i].r = r;
  tree[i].len = tree[i].sum = 0;
  if (l == r) return;
  int mid = (l + r) >> 1;
  build(i << 1, l, mid);
  build(i << 1 | 1, mid + 1, r);
  return;
} 
inline void pushup(int i) {
  int l = tree[i].l, r = tree[i].r;
  if (tree[i].sum) tree[i].len = x[r + 1] - x[l];
  else tree[i].len = tree[i << 1].len + tree[i << 1 | 1].len;
}
void update(int i, long long L, long long R, int num) {
  int l = tree[i].l, r = tree[i].r;
  //注意: l、r和L、R的意义完全不同。l、r表示这个节点管辖的下标范围,而L、R则表示需要修改的真实区间
  if (x[r + 1] <= L || x[l] >= R) return;
  if (L <= x[l] && x[r + 1] <= R) {
    tree[i].sum += num;
    pushup(i);
    return;
  }
  update(i << 1, L, R, num);
  update(i << 1 | 1, L, R, num);
  pushup(i);
}
signed main() {
  cin >> n;
  for (int i = 1; i <= n; i++) {
    scanf("%lld%lld%lld%lld", &x1, &yy1, &x2, &y2);
    x[2 * i - 1] = x1; x[2 * i] = x2;
    line[2 * i - 1] = (scanline){x1, x2, yy1, 1};
    line[2 * i] = (scanline){x1, x2, y2, -1};
  }
  n <<= 1;
  sort(x + 1, x + 1 + n);
  sort(line + 1, line + 1 + n);
  int tot = unique(x + 1, x + 1 + n) - (x + 1);
  build(1, 1, tot - 1);
  //[1, tot - 1]描述的就是[X[1], X[tot]]
  for (int i = 1; i < n; i++) {
    update(1, line[i].l, line[i].r, line[i].flag);
    ans += tree[1].len * (line[i + 1].h - line[i].h);
  }
  printf("%lld\n", ans);
  return 0;
}

POJ-1151 Atlantis

#include <iostream>
#include <cmath>
#include <cstring>
#include <cstdio>
#include <algorithm> 
using namespace std;
const int maxn = 1e6 + 10;
int n, cnt;
double x1, yy1, x2, y2, ans;
double x[maxn << 1];
struct scanline{
  double l, r, h;
  int flag;
  bool operator < (const scanline &num) const {
    return h < num.h;
  }
}line[maxn << 1];
struct node{
  int l, r, sum;
  double len;
}tree[maxn << 2];
inline void pushup(int i) {
  int l = tree[i].l, r = tree[i].r;
  if (tree[i].sum) tree[i].len = x[r + 1] - x[l];
  else tree[i].len = tree[i << 1].len + tree[i << 1 | 1].len;
}
void build(int i, int l, int r) {
  tree[i].l = l; tree[i].r= r;
  tree[i].len = tree[i].sum = 0;
  if (l == r) return;
  int mid = (l + r) >> 1;
  build(i << 1, l, mid);
  build(i << 1 | 1, mid + 1, r);
  pushup(i);
  return;
}
void update(int i, double L, double R, int num) {
  int l = tree[i].l, r = tree[i].r;
  if (x[r + 1] <= L || x[l] >= R) return;
  if (L <= x[l] && x[r + 1] <= R) {
    tree[i].sum += num;
    pushup(i);
    return;
  }
  update(i << 1, L, R, num);
  update(i << 1 | 1, L, R, num);
  pushup(i);
}
signed main() {
  while (cin >> n && n) {
    ans = 0; cnt++;
    for (int i = 1; i <= n; i++) {
      cin >> x1 >> yy1 >> x2 >> y2;
      x[2 * i - 1] = x1; x[2 * i] = x2;
      line[2 * i - 1] = (scanline){x1, x2, yy1, 1};
      line[2 * i] = (scanline){x1, x2, y2, -1};
    }
    n <<= 1;
    sort(x + 1, x + 1 + n);
    sort(line + 1, line + 1 + n);
    int tot = unique(x + 1, x + 1 + n) - (x + 1);
    build(1, 1, tot - 1);
    for (int i = 1; i < n; i++) {
      update(1, line[i].l, line[i].r, line[i].flag);
      ans += tree[1].len * (line[i + 1].h - line[i].h); 
    }
    printf("Test case #%d\n", cnt);
    printf("Total explored area: %.2f\n\n", ans);
  }
  return 0;
}

二、周长并

P1856 [IOI1998] [USACO5.5] 矩形周长Picture 为例,假使有如下几条扫描线,考虑所截得的竖边长

发现:纵边总长度 = ∑ (2 × 被截得的线段条数 × 扫过的高度)

发现:横边总长度 = ∑∣上次截得的总长 − 现在截得的总长∣

所以和面积并比起来,周长并中的线段树还要维护线段的条数。另外,横边和纵边需分别计算

具体表现为:

#include <bits/stdc++.h>
using namespace std;
const int maxn = 5e3 + 10;
int n, x1, yy1, x2, y2, ans, pre;
int x[maxn << 1];
struct scanline{
  int l, r, h, flag;
  bool operator < (const scanline &num) const {
    if (h == num.h) return flag > num.flag;
    return h < num.h;
  }
}line[maxn << 1];
struct node{
  int l, r, sum, len, cnt;
  //cnt表示区间线段条数
  bool lc, rc;
}tree[maxn << 2];
void build(int i, int l, int r) {
  tree[i].l = l; tree[i].r = r;
  tree[i].lc = tree[i].rc = 0;
  tree[i].len = tree[i].sum = tree[i].cnt = 0;
  if (l == r) return;
  int mid = (l + r) >> 1;
  build(i << 1, l, mid);
  build(i << 1 | 1, mid + 1, r);
  return;
} 
inline void pushup(int i) {
  int l = tree[i].l, r = tree[i].r;
  if (tree[i].sum) {
    tree[i].len = x[r + 1] - x[l];
    tree[i].lc = tree[i].rc = true;
    tree[i].cnt = 1; //做好相应的标记
  }
  else {
    tree[i].len = tree[i << 1].len + tree[i << 1 | 1].len;
    tree[i].lc = tree[i << 1].lc;
    tree[i].rc = tree[i << 1 | 1].rc;
    tree[i].cnt = tree[i << 1].cnt + tree[i << 1 | 1].cnt;
    //如果左儿子右端点和右儿子左端点都被覆盖,那么中间就是连续的一段,所以要-1
    if (tree[i << 1].rc && tree[i << 1 | 1].lc) tree[i].cnt--;
  }
}
void update(int i, long long L, long long R, int num) {
  int l = tree[i].l, r = tree[i].r;
  if (x[r + 1] <= L || x[l] >= R) return;
  if (L <= x[l] && x[r + 1] <= R) {
    tree[i].sum += num;
    pushup(i);
    return;
  }
  update(i << 1, L, R, num);
  update(i << 1 | 1, L, R, num);
  pushup(i);
}
signed main() {
  cin >> n;
  for (int i = 1; i <= n; i++) {
    scanf("%d%d%d%d", &x1, &yy1, &x2, &y2);
    x[2 * i - 1] = x1; x[2 * i] = x2;
    line[2 * i - 1] = (scanline){x1, x2, yy1, 1};
    line[2 * i] = (scanline){x1, x2, y2, -1};
  }
  n <<= 1;
  sort(x + 1, x + 1 + n);
  sort(line + 1, line + 1 + n);
  int tot = unique(x + 1, x + 1 + n) - (x + 1);
  build(1, 1, tot - 1);
  for (int i = 1; i < n; i++) {
    update(1, line[i].l, line[i].r, line[i].flag);
    ans += abs(pre - tree[1].len);
    pre = tree[1].len;
    ans += 2 * tree[1].cnt * (line[i + 1].h - line[i].h);
  }
  ans += line[n].r - line[n].l; //特判一下枚举不到的最后一条扫描线
  printf("%d\n", ans);
  return 0;
}

参考:
【学习笔记】扫描线
cbdsopa-扫描线

posted @ 2022-04-05 18:49  Moominn  阅读(821)  评论(0编辑  收藏  举报