j•jueansuyA LITTLE SPACE TO BUILD 回到实践笔记归档
PRACTICE NOTE / 009

蒙特卡洛:
几何概率估计 π、平方比较与边界等于陷阱

复盘 CSP 202509 认证第一题《蒙特卡洛》(OJ 满分 100 分)。作为 CSP 考试中非常考查“数学直觉与工程严谨性”的几何模拟题,题目虽然只有不到 20 行核心逻辑,却潜藏着三大经典考场陷阱:为什么不能直接调用 sqrt()?为什么边界点必须用 <= 而不是 <?C 语言中的整除截断又是如何无声吞掉精度的?

01 / GEOMETRIC PROBABILITY

几何概率模型:为什么撒点能算出 π?

题目描述:在第一象限以原点 $(0, 0)$ 为左下角、边长为 $a$ 的正方形区域内,随机撒下 $n$ 个点 $(x, y)$。圆心在原点、半径为 $a$ 的圆与该正方形的交集恰好是四分之一圆。统计落在圆内的点数 $cnt$,通过面积比估计圆周率 $\pi$:

大数定律与面积比推导:
  • 正方形区域面积:$S_{\text{square}} = a \times a = a^2$;
  • 四分之一圆区域面积:$S_{\text{quarter}} = \frac{1}{4} \pi a^2$;
  • 几何概率比例:当撒点足够密集且均匀分布时,落入四分之一圆内的概率等于面积之比: $$\frac{cnt}{n} \approx \frac{S_{\text{quarter}}}{S_{\text{square}}} = \frac{\frac{1}{4} \pi a^2}{a^2} = \frac{\pi}{4}$$
  • 圆周率反解公式:两边同乘 4,即可得出估算值:$$\pi \approx 4 \times \frac{cnt}{n}$$

这个思想就是著名的蒙特卡洛方法(Monte Carlo Method)——用计算机海量伪随机抽样来逼近确定性数学解。在现代计算机图形学渲染(光线追踪)、物理粒子碰撞模拟与金融期权定价中,蒙特卡洛都是不可或缺的底层基石。

02 / INTERACTIVE SIMULATION

蒙特卡洛采样与收敛动态演练

下方的交互画布实时模拟第一象限 $[0, a] \times [0, a]$ 的随机撒点过程。绿色为圆内点,红色为圆外点,青色弧线为边界 $x^2 + y^2 = a^2$。你可以切换不同样本量,或者手动批量撒点,亲眼见证估算值如何随着样本量增加一步步收敛到 $3.1416$:

MONTE CARLO SIMULATOR · FIRST QUADRANT 纯浏览器本地 Canvas 渲染 · 零依赖
03 / THREE TRAPS IN EXAM

考场三大致命陷阱:平方比较、边界等于与整除截断

这道题在考场上看似简单,但通过率却远没有想象中高。以下是复盘中总结出的三大极易丢分的工程陷阱:

陷阱 01 / 性能与精度

为什么绝不能用 sqrt() 开方求欧氏距离?

很多考生的第一直觉是计算点到原点的距离:double dist = sqrt(x * x + y * y); if (dist <= a) ...。这在工程上有两大隐患:

  • 指令开销巨大:浮点数开方 sqrt() 是 CPU 中计算开销极大的操作,需要数十甚至上百个时钟周期。当 $n$ 达到 $10^5$ 时,频繁调用 sqrt() 很容易超时(TLE);
  • 浮点精度漂移:浮点开方会引入精度舍入误差。对于原本恰好在圆上的边界点,开方可能因为微小扰动算出 5.000000000000001,导致误判!

标准解法:两边平方,直接比较:x * x + y * y <= (double)a * a。不仅只用加法和乘法(极快),而且完全避开了开方精度损失!

陷阱 02 / 边界严格等于

勾股数 (3, 4, 5) 边界点为什么必须用 <=?

题目明确说明:“圆内包含边界”。如果考场上顺手写成了 < a * a:

当半径 $a = 5$,测试数据中出现经典勾股数 $(3, 4)$ 时,$3^2 + 4^2 = 25 = 5^2$。如果代码是 <,这个点将被判为圆外,导致计数 $cnt$ 少算 1,整个测试点全错!

无论是几何几何题还是数组二分题,“等号是否包含”永远是决定 0 分还是 100 分的生死线。

陷阱 03 / C 语言整除截断

4 * cnt / n 为什么算出了 0 或者 3?

在 C 语言中,如果把计数器 cnt 声明为普通整型 int cnt = 0;,那么表达式 4 * cnt / n 是纯整数运算!

例如采样 100 个点,圆内 78 个点:4 * 78 / 100 = 312 / 100 = 3,小数部分被强制截断,精度全失;如果先算 cnt / n 甚至会直接得到 0!

避坑策略:直接将 cnt 声明为 double cnt = 0;,或者在乘法中引入浮点字面量 4.0 * cnt / n,强制整个运算链提升为双精度浮点计算。

04 / C CODE IMPLEMENTATION

CSP 认证 100 分满分 C 语言代码节选

C (C99 / GCC)
#include <stdio.h>

int main(void) {
    int n, a;
    // 读取采样点数 n 和正方形边长/半径 a
    if (scanf("%d %d", &n, &a) != 2) {
        return 0;
    }

    double x, y;
    // cnt 声明为 double,直接规避后续整除截断陷阱
    double cnt = 0;

    for (int i = 0; i < n; i++) {
        scanf("%lf %lf", &x, &y);
        // 核心:直接比较平方,避开 sqrt 浮点指令开销与精度漂移
        // 核心:必须使用 <= 包含圆弧边界点(如 3-4-5 勾股点)
        if (x * x + y * y <= (double)a * a) {
            cnt++;
        }
    }

    // 4.0 保证全精度双精度浮点运算:π ≈ 4 * (cnt / n)
    double p = 4.0 * cnt / n;
    printf("%lf\n", p);

    return 0;
}
05 / COMPLEXITY & CONVERGENCE

复杂度与大数定律收敛速率

时间复杂度 O(N)

N 为采样点数。单重循环流式处理输入点,无数组存储开销,每个点仅做 2 次乘法、1 次加法与 1 次常数比较,计算量极小,轻松在 100ms 内跑完十万级采样。

空间复杂度与收敛率 O(1) · 误差 O(1 / √N)

只使用标量变量,空间占用 $O(1)$。根据中心极限定理,蒙特卡洛采样的标准差为 $O(1 / \sqrt{N})$,样本量每扩大 100 倍,估计值有效数字增加 1 位。