P4196 [CQOI2006]凸多边形 /【模板】半平面交 题解

· · 题解

半平面交

0.前言

关于我写代码的一些习惯与声明:

typedef pair<double, double> PDD;

#define x first
#define y second

struct Line {
    PDD st, ed;
}line[N];

PDD operator - (PDD a,  PDD b) {
    return {a.x - b.x, a.y - b.y};
}

1.前置知识

1.1 高中平面向量相关知识

1.2 向量叉积 (cross)

1.2.1 几何意义:由两个向量围成平行四边形的有向面积。

1.2.2 规定:第二个向量在第一个向量的逆时针方向为正,顺时针方向为负。

double cross(PDD a, PDD b) {
    return a.x * b.y - a.y * b.x;
}

double area(PDD a, PDD b, PDD c) {
    return cross(b - a, c - a);
}

1.3 求直线交点 (get line intersection)

设交点为 p + t \times \vec{v}

\vec{u} = p - q

根据三角形相似,不难得出 t = \frac{\vec{w} \times \vec{u}}{\vec{v} \times \vec{w}}

PDD gli(PDD p, PDD v, PDD q, PDD w) {
    auto u = p - q;
    double t = cross(w, u) / cross(v, w);
    return {p.x + v.x * t, p.y + v.y * t};
}

PDD gli(Line a, Line b) {
    return gli(a.st, a.ed - a.st, b.st, b.ed - b.st);
}

1.4 判断两直线交点是否在另一直线右侧

设两直线交点为 o

判断这条直线上的任一条线段和这个交点围成的三角形面积符号即可。

bool on_right(Line a, Line b, Line c) {
    auto o = gli(b, c);
    return sign(area(a.st, a.ed, o)) <= 0;
}

1.5 求某条直线与水平方向的夹角

设这条直线上的某个点为 (x, y),与水平方向的夹角为 \alpha

$\alpha = \arctan(\frac{y}{x})$。 使用 ```cmath``` 库中的 ```atan2``` 函数,注意,这个函数在调用时先传纵坐标,再传横坐标。 ``` double get_angle(Line a) { return atan2(a.ed.y - a.st.y, a.ed.x - a.st.x); } ``` ### 2.半平面 半平面交 **2.1 半平面** 一个向量把平面分成了左、右两部分,我们将左(右)部分的集合称作半平面。 这里的左是指以向量的方向为向前的左边。 **2.2 半平面交** 许多向量的半平面的交集称作半平面交。 我们把题目给的凸多边形按逆时针标定方向,所求即为所有向量的半平面交,一个简单的例子如下。 比如我们要求一个给定三边的三角形面积。 ![](https://cdn.luogu.com.cn/upload/image_hosting/1vvzzcvm.png?x-oss-process=image/resize,m_lfit,h_200,w_200) ![](https://cdn.luogu.com.cn/upload/image_hosting/plxx0ur6.png?x-oss-process=image/resize,m_lfit,h_200,w_200) ![](https://cdn.luogu.com.cn/upload/image_hosting/ljo3wpet.png?x-oss-process=image/resize,m_lfit,h_200,w_200) 维护三条直线,求他们的半平面交即可。 本题样例如下。 ![](https://cdn.luogu.com.cn/upload/image_hosting/bkjdv01o.png?x-oss-process=image/resize,m_lfit,h_200,w_200) ### 3.算法流程 首先,我们将给定的所有直线按照与水平面的夹角排序,然后用一个双端队列维护所有的直线。 如何维护?或者说,当满足什么条件的时候,我们就不再需要队列中的某条直线了? 首先,如果有平行的直线,我们只需要它们之中靠左的那条。 其他的更新方式,看下面的例子。我们以黑色表示队列中原有的直线,蓝色表示枚举到的用来更新的直线。 ![](https://cdn.luogu.com.cn/upload/image_hosting/vv2t6q3j.png?x-oss-process=image/resize,m_lfit,h_170,w_225) 这个情况下,我们就不再需要队尾的直线。 ![](https://cdn.luogu.com.cn/upload/image_hosting/h71behqa.png?x-oss-process=image/resize,m_lfit,h_200,w_225) 而这种情况下,我们则需要保留队尾的直线。 稍加总结,不难得到:看队内两条直线的交点在新的直线的左侧还是右侧即可。若在右侧,则我们不再需要队尾直线,可以直接弹出。 更新队头方式同理。最后还要用队头更新一下队尾。 注意,当三线交于一点时应当如何取舍,应当根据题干而定。本题中这种情况对最后的答案没有影响,所以可以直接弹出。 ``` double calc() { sort(line, line + cnt, cmp); int hh = 0, tt = -1; rep(i, 0, cnt - 1) { if(i && !dcmp(get_angle(line[i]), get_angle(line[i - 1]))) continue; while(hh + 1 <= tt && on_right(line[i], line[q[tt - 1]], line[q[tt]])) -- tt; while(hh + 1 <= tt && on_right(line[i], line[q[hh]], line[q[hh + 1]])) ++ hh; q[ ++ tt] = i; } while(hh + 1 <= tt && on_right(line[q[hh]], line[q[tt - 1]], line[q[tt]])) -- tt; q[ ++ tt] = q[hh]; int k = 0; rep(i, hh, tt - 1) ans[k ++ ] = gli(line[q[i]], line[q[i + 1]]); double res = 0; rep(i, 1, k - 2) res += area(ans[0], ans[i], ans[i + 1]); return res / 2; } ``` ### 4.代码 操作核心代码在上面已经给出。[完整代码。](https://www.luogu.com.cn/paste/ctn717jz)