T5 雨水收集系统

· · 题解

题目链接

本题是一道典型的计算几何题目。需要解题者掌握凸包和半平面交的求解方法。

题目给定的降雨云是一个凸多边形,当将其进行平移操作后,其路径所覆盖的区域也是一个凸多边形,这个凸多边形应该如何求呢?很简单,将平移后的各个凸包顶点求出来,与原来的凸包顶点一起,再求一次凸包即可。知道了降雨云覆盖的区域,将其与建筑顶面的矩形作半平面交,求出各个交的面积,然后累加即可。

主要注意实现的细节即可。

#include <bits/stdc++.h>

using namespace std;

const int MAXV = 1100;
const double EPSILON = 1E-7;

// 表示点的数据结构
struct point {
    double x, y;
    point (double x = 0, double y = 0): x(x), y(y) {}
    point operator + (point p) { return point(x + p.x, y + p.y); };
    point operator - (point p) { return point(x - p.x, y - p.y); };
    point operator * (double k) { return point(x * k, y * k); };
    point operator / (double k) { return point(x / k, y / k); };

    bool operator<(const point &p) const
    {
        if (fabs(y - p.y) > EPSILON) return y < p.y;
        return x < p.x;
    }

    bool operator==(const point &p) const
    {
        return fabs(x - p.x) <= EPSILON && fabs(y - p.y) <= EPSILON;
    }
};

// 定义多边形
typedef vector<point> polygon;

// 范数
double norm(point a)
{
    return a.x * a.x + a.y * a.y;
}

// 模
double abs(point a)
{
    return sqrt(norm(a));
}

// 内积(点积)
double dot(point a, point b)
{
    return a.x * b.x + a.y * b.y;
}

// 外积(叉积)
double cross(point a, point b)
{
    return a.x * b.y - a.y * b.x;
}

// 定义直线,使用两点和极角定义直线,加入极角是用于半平面交
struct line
{
    point a, b;
    double angle;
};

// 有向面积
double cp(point a, point b, point c)
{
    return cross(b - a, c - a);
}

// 利用有向面积判断顺时针旋转
bool cw(point a, point b, point c)
{
    return cp(a, b, c) < -EPSILON;
}

// 利用有向面积判断逆时针旋转
bool ccw(point &a, point &b, point &c)
{
    return cp(a, b, c) > EPSILON;
}

// 利用有向面积判断逆时针旋转或共线
bool ccwOrCollinear(point &a, point &b, point &c)
{
    double cp1 = cp(a, b, c);
    return cp1 > EPSILON || fabs(cp1) <= EPSILON;
}

// 利用 Andrew 合并法求凸包
polygon andrewConvexHull(polygon pg)
{
    polygon ch;

    sort(pg.begin(), pg.end());
    for (int i = 0; i < pg.size(); i++)
    {
        while (ch.size() >= 2 &&
            ccwOrCollinear(ch[ch.size() - 2], ch[ch.size() - 1], pg[i]))
            ch.pop_back();
        ch.push_back(pg[i]);
    }
    for (int i = pg.size() - 1, upper = ch.size() + 1; i >= 0; i--)
    {
        while (ch.size() >= upper &&
            ccwOrCollinear(ch[ch.size() - 2], ch[ch.size() - 1], pg[i]))
            ch.pop_back();
        ch.push_back(pg[i]);
    }
    ch.pop_back();

    return ch;
}

// 按极角对直线进行排序的比较函数
bool cmpLine(line p, line q)
{
    if (fabs(p.angle - q.angle) <= EPSILON) return cw(p.a, p.b, q.a);
    return p.angle < q.angle;
}

// 按极角对直线进行去重的比较函数
bool cmpAngle(line p, line q)
{
    return fabs(p.angle - q.angle) <= EPSILON;
}

// 判定两条直线是否平行
bool parallel(line p, line q)
{
    return fabs((p.a.x - p.b.x) * (q.a.y - q.b.y) -
        (q.a.x - q.b.x) * (p.a.y - p.b.y)) <= EPSILON;
}

// 确定两条直线的交点
point getIntersection(line p, line q)
{
    point p1 = p.a;
    double scale =
        ((p.a.x - q.a.x) * (q.a.y - q.b.y) - (p.a.y - q.a.y) * (q.a.x - q.b.x)) /
        ((p.a.x - p.b.x) * (q.a.y - q.b.y) - (p.a.y - p.b.y) * (q.a.x - q.b.x));
    p1.x += (p.b.x - p.a.x) * scale;
    p1.y += (p.b.y - p.a.y) * scale;
    return p1;
}

// 根据两点确定一条直线
line pointToLine(point a, point b)
{
    line lr;
    lr.a = a, lr.b = b, lr.angle = atan2(b.y - a.y, b.x - a.x);
    return lr;
}

// 使用朱泽园介绍的“排序增量法”求半平面交
polygon halfPlaneIntersection(line *sides, int nLine)
{
    polygon pg;
    line deq[MAXV];

    sort(sides, sides + nLine, cmpLine);
    nLine = unique(sides, sides + nLine, cmpAngle) - sides; 

    int btm = 0, top = 1;
    deq[0] = sides[0], deq[1] = sides[1];

    for (int i = 2; i < nLine; i++)
    {
        if (parallel(deq[top], deq[top - 1]) || parallel(deq[btm], deq[btm + 1]))
            return pg;

        while (btm < top &&
            cw(sides[i].a, sides[i].b, getIntersection(deq[top], deq[top - 1])))
            top--;
        while (btm < top &&
            cw(sides[i].a, sides[i].b, getIntersection(deq[btm], deq[btm + 1])))
            btm++;

        deq[++top] = sides[i];
    }

    while (btm < top &&
        cw(deq[btm].a, deq[btm].b, getIntersection(deq[top], deq[top - 1])))
        top--;
    while (btm < top &&
        cw(deq[top].a, deq[top].b, getIntersection(deq[btm], deq[btm + 1])))
        btm++;

    if (top <= (btm + 1)) return pg;

    for (int i = btm; i < top; i++)
        pg.push_back(getIntersection(deq[i], deq[i + 1]));
    if (btm < (top + 1))
        pg.push_back(getIntersection(deq[btm], deq[top]));

    return pg;
}

// 计算多边形的面积
double getArea(polygon pg)
{
    if (pg.size() < 3) return 0.0;
    double A = 0.0;
    int n = pg.size();
    for (int i = 0, j = (i + 1) % n; i < n; i++, j = (i + 1) % n)
        A += (pg[i].x * pg[j].y - pg[j].x * pg[i].y);
    return fabs(A / 2.0);
}

// 根据对角顶点坐标确定矩形
polygon getRectFromPoint(int x1, int y1, int x2, int y2)
{
    polygon rect;
    int minx = min(x1, x2), maxx = max(x1, x2);
    int miny = min(y1, y2), maxy = max(y1, y2);
    rect.push_back(point(minx, miny));
    rect.push_back(point(maxx, miny));
    rect.push_back(point(maxx, maxy));
    rect.push_back(point(minx, maxy));
    return rect;
}

// 解析时间
int getTime(string text)
{
    return stoi(text.substr(0, 2)) * 60 + stoi(text.substr(3));
}

int main(int argc, char *argv[])
{
    cin.tie(0), cout.tie(0), ios::sync_with_stdio(false);

    int cases, n, m, v;
    string time1, time2;

    cin >> cases;
    for (int cs = 1; cs <= cases; cs++)
    {
        cin >> n;

        // 读入每栋建筑的对角点坐标,由对角点坐标确定矩形,矩形的顶点按逆时针排列。
        vector<polygon> rects;
        int x1, y1, x2, y2;
        for (int i = 0; i < n; i++)
        {
            cin >> x1 >> y1 >> x2 >> y2;
            rects.push_back(getRectFromPoint(x1, y1, x2, y2));
        }

        // 读入降雨云。
        polygon cloud;
        cin >> m;
        for (int i = 0; i < m; i++)
        {
            cin >> x1 >> y1;
            cloud.push_back(point(x1, y1));
        }

        // 降雨云沿着直线方向做平移运动,其扫过的区域构成一个凸多边形。使用凸包算法求出凸多边形的顶点,顶点按照逆时针方向排序。
        point s, e;
        cin >> s.x >> s.y >> e.x >> e.y;
        cin >> v >> time1 >> time2;
        double d = v * (getTime(time2) - getTime(time1));
        int cnt = cloud.size();
        for (int i = 0; i < cnt; i++)
        {
            point moved = cloud[i] + (e - s) * d / abs(e - s);
            cloud.push_back(moved);
        }
        cloud = andrewConvexHull(cloud);
        reverse(cloud.begin(), cloud.end());

        // 使用半平面交确定降雨云与每栋建筑的顶层的交,计算面积并求和。
        double area = 0;
        line sides[MAXV];
        for (int i = 0; i < rects.size(); i++)
        {
            int nLine = 0;
            for (int j = 0; j < cloud.size(); j++)
                sides[nLine++] = pointToLine(cloud[j], cloud[(j + 1) % cloud.size()]);
            for (int j = 0; j < rects[i].size(); j++)
                sides[nLine++] = pointToLine(rects[i][j], rects[i][(j + 1) % rects[i].size()]);
            area += getArea(halfPlaneIntersection(sides, nLine));
        }
        cout << fixed << setprecision(1) << area << '\n';
    }

    return 0;
}