题解:CF600D Area of Two Circles' Intersection

· · 题解

记一些卡精度技巧,这下再也不怕 ymg 讲邪恶计算几何了。

题面很短。有点像 CSP - S 2021 阅读程序第一题(求两球的体积交)。

记圆心距为 d,两圆半径分别为 r_1,r_2。

好日子先过,先把平凡解判掉:

对于相交的情况,先画图。

相交面积 S 可以如此拆解:

\begin{align*} S &= (S_{\text{扇形}O_1AB} - S_{\triangle O_1AB}) + (S_{\text{扇形}O_2AB} - S_{\triangle O_2AB}) \\ &= S_{\text{扇形}O_1AB} + S_{\text{扇形}O_2AB} - S_{\text{筝形}O_1AO_2B} \\ &= S_{\text{扇形}O_1AB} + S_{\text{扇形}O_2AB} - 2S_{\triangle O_1O_2A} \end{align*}

注意到 \triangle O_1O_2A 的三边都是已知的,说明它是一个可解三角形,按照最直观的想法,我们会对由两圆心和一个交点构成的 \triangle O_1O_2A 使用余弦定理:

\cos\angle AO_1O_2 = \frac{d^2 + r_1^2 - r_2^2}{2dr_1}

从而可以用 \arccos 算出一扇形半圆心角 \alpha=\angle AO_1O_2,对称地得另一扇形半圆心角,从而求得扇形面积。注意到我们求出的 \alpha 是半圆心角。完整的扇形圆心角为 2\alpha,代入扇形面积公式 \frac1 2 r^2 (2\alpha),恰好抵消,即面积为 r^2 \alpha。

对于三角形的面积,考虑使用海伦公式。但这题卡精度,为了避免除以 2 的浮点运算,我们将其代数展开:

16 S_{\triangle}^2 = (d+r_1+r_2)(d+r_1-r_2)(d-r_1+r_2)(r_1+r_2-d)

记 Y=\sqrt{\text{RHS}}=\sqrt{(d+r_1+r_2)(d+r_1-r_2)(d-r_1+r_2)(r_1+r_2-d)},则 S_{\triangle O_1O_2A}=\frac Y 4。

依据以上式子计算即可。

\text{Ans} = r_1^2 \arccos \left( \frac{d^2 + r_1^2 - r_2^2}{2dr_1} \right) + r_2^2 \arccos \left( \frac{d^2 + r_2^2 - r_1^2}{2dr_2} \right) - \frac{1}{2} Y

……?

WA on test 28。

精度爆炸的离谱(差出了 \approx 1000)。

我们来分析一下是哪里出了问题。这篇题解 声称,“精度问题来自海伦公式”。AI 则声称,这是错误的。精度问题实际上来自于 std::acos。将其换成 <cmath> 中的 std::atan2(其中 \operatorname{atan2}(Y,X) 在数学中可以理解为 \arctan(\frac Y X))就不会爆精度。

:::info[为什么 \arccos 会炸掉?为什么 \arctan 不会?]

相反,$\arctan(x)$ 的导数是 $\frac 1 {1+x^2}$。这个导数是有界的,不会趋近于无穷大。 ::: :::info[为什么其他题解的 $\arccos$ 没有炸掉?] 记扇形圆心角为 $\theta$。现在的 [高赞题解](https://www.luogu.com.cn/article/if90wuuf) 是这样算 $S_1=S_{\text{扇形}O_1AB} - S_{\triangle O_1AB}$ 的: $$ S_1 = \frac{1}{2} r^2 \theta - \frac{1}{2} r^2 \sin \theta = \frac{1}{2} r^2 (\theta - \sin \theta) $$ 我们定义函数 $f(x) = x - \sin x$。 代码里传入的不是真实的 $\theta$,而是带有误差的 $\theta + \Delta$。用泰勒展开估算误差:$f(\theta + \Delta) \approx f(\theta) + f'(\theta) \cdot \Delta, f'(x) = 1 - \cos x$。当 $\theta \to 0$ 时,$1 - \cos \theta \approx 0$。这意味着,尽管 `std::acos` 制造了一个极其庞大的角度误差 $\Delta$(比如导致面积本应偏离 $10^6$),但这个误差被乘以了 $1 - \cos \theta$(一个接近 $10^{-8}$ 的极小数),导致看上去没有误差。 ::: 于是尝试凑一凑对边比邻边。设两圆交点构成的公共弦 $AB$ 与连心线 $O_1O_2$ 垂直交于点 $H$。在 $\text{Rt}\triangle O_1HA$ 中,顶角即为半圆心角 $\alpha$,斜边为 $r_1$,对边为半弦长 $\frac1 2|AB|$,记邻边为 $d_1=O_1H$,记 $d_2=d-d_1=O_2H$。 在 $\text{Rt}\triangle O_1HA$ 和 $\text{Rt}\triangle O_2HA$ 中, $$ d_1^2 - d_2^2 = r_1^2 - r_2^2 $$ 解得 $d_1 = \frac{d^2 + r_1^2 - r_2^2}{2d}$。 所以 $$ \alpha = \operatorname{atan2}(\frac1 2|AB|,\frac{d^2 + r_1^2 - r_2^2}{2d})=\operatorname{atan2}(d|AB|,d^2+r_1^2-r_2^2) $$ 注意到 $d|AB|$ 恰好是筝形的两条对角线之积,联想到筝形的另一个面积公式。 $$ \frac 1 2 d|AB|=\frac 1 2 |O_1O_2| \times |AB|=S_{\text{筝形}O_1AO_2B}=\frac 1 2Y \implies d|AB|=Y $$ 好妙!这里和前文的 $Y$ 联系上了。所以 $\alpha=\operatorname{atan2}(Y,d^2+r_1^2-r_2^2)$。 因此答案如果用 $\operatorname{atan2}$ 表述,就是 $$ \text{Ans} = r_1^2 \operatorname{atan2}( Y,d^2+r_1^2-r_2^2) + r_2^2 \operatorname{atan2}( Y,d^2+r_2^2-r_1^2) - \frac{1}{2} Y $$ 就可以过了。 不过好像还有一个坑没有填。 :::info[为什么要使用 `std::atan2` 而不是 `std::atan`?] 很多人误以为 `std::atan2(Y, X)` 只是 `std::atan(Y / X)` 的语法糖,但这个想法在计算几何中是致命的。 首先,上面我们讲的做法看似完美无瑕。 ……但你做了几百道题的直觉提醒你:你有没有遍历整个实数空间?你有没有忽略边界情况? 一个我们可能忽略的情况。 ![](https://cdn.luogu.com.cn/upload/image_hosting/anetyx53.webp) 当一个圆的圆心(不妨设为 $O_1$)在 $\odot O_2$ 内部时,四边形 $O_1AO_2B$ 可能会变为一个燕尾形,而不是一个筝形。 不过好在,在数学考试里,只要我们肯耐下心再分类讨论一遍,你就可以发现你**得到了完全相同的公式**。 可是在信息竞赛里面你要考虑得就多了。 这种情况中,$\alpha = \angle AO_1O_2 > \frac \pi 2$。可是无论是在数学里的 $\arctan(x)$ 还是信息里的 `std::atan` 的值域都是 $(-\frac \pi 2,\frac \pi 2)$ 啊?! 所以此时如果你使用 `std::atan` 是无法算出真正的 $\alpha$ 的,此时需要特判。 但是 `std::atan2` 就可以产生在 $(-\pi,\pi]$ 内的所有角度,因为它可以通过传入的 `Y` 和 `X` 的符号来判断角度的象限。所以我们为了避免额外特判,使用 `std::atan2`。 ::: :::success[Code] 使用了 AI 优化代码风格。 ```cpp #include <iostream> #include <cmath> #include <iomanip> #include <algorithm> using namespace std; typedef long double ld; typedef long long ll; inline ll sqr(ll x) { return x * x; } int main() { ios::sync_with_stdio(false); cin.tie(nullptr); ll x1, y1, r1, x2, y2, r2; cin >> x1 >> y1 >> r1 >> x2 >> y2 >> r2; ll d_2 = sqr(x1 - x2) + sqr(y1 - y2); ld ans = 0; if (d_2 >= sqr(r1 + r2)) { ans = 0; } else if (d_2 <= sqr(r1 - r2)) { ans = sqr(min(r1, r2)) * acosl(-1.0L); } else { ll r1_2 = sqr(r1), r2_2 = sqr(r2); ld Y = sqrtl((ld)(sqr(r1 + r2) - d_2) * (d_2 - sqr(r1 - r2))); ans = r1_2 * atan2l(Y, d_2 + r1_2 - r2_2) + r2_2 * atan2l(Y, d_2 + r2_2 - r1_2) - 0.5 * Y; } cout << fixed << setprecision(10) << ans << "\n"; return 0; } ``` ::: --- ![](https://cdn.luogu.com.cn/upload/image_hosting/lqaiicws.webp) 关于数学公式的问题,我找不到其他的方式来表示 $S_{\text{扇形}}$ 等等了。我专门翻了下人教版九年级上册数学书,他用的也是这个形式。求管理员通过……或者可以提供细致的建议。