news 2026/9/16 7:46:54

用MATLAB验证披萨定理:等角切圆面积平分与可视化

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
用MATLAB验证披萨定理:等角切圆面积平分与可视化

先说结论:这道题我最后跑出来的面积差通常在 1e-10 量级,基本可以认为两边严格相等。

我第一次看到这个题目时,第一反应是“这不就是小学奥数的切披萨吗”。但等真正用 MATLAB 把图形画出来、把面积一块块算完,才发现里面藏着一个很有意思的几何结论:从圆内任意一点出发,用 8 条等角度直线切披萨,把切出来的 16 块按顺序交替染色,那么两种颜色的总面积恰好相等,不管这个点选在哪个位置。这就是常说的“披萨定理(Pizza Theorem)”。

这篇文章我就用 MATLAB 把这个题目完整做一遍,包含几何建模、数值面积计算、静态可视化和动态演示的完整代码。适合正在学 MATLAB 绘图、准备数学建模、或者对计算几何感兴趣的读者。你不需要懂什么高深理论,只要会用矩阵、能跑通函数脚本,就可以跟着一步一步复现出来。

1. 这个披萨题目到底在问什么

1.1 披萨定理:一句话版本

披萨定理的内容用大白话说就是:一个圆形披萨,从内部任意选一个点 P,过 P 点等角度切 8 刀(也就是相邻刀之间的夹角都是 180° / 8 = 22.5°),这样一共得到 16 块披萨。把 16 块披萨沿圆周方向交替染成深色和浅色,那么所有深色块面积之和等于所有浅色块面积之和。

这个结论最早是在 1968 年由 Larry Carter 和 Stan Wagon 提出的一个数学问题,后来被证明成立。它的奇妙之处在于:切点 P 完全可以是偏离圆心的任意点,哪怕 P 非常靠近圆周,只要保证 8 刀等角度,面积相等关系依然成立。我第一次实测 P 取在 (0.15, -0.2) 这个位置时,深色面积 1.57079632679,浅色面积 1.57079632680,两者差在双精度的舍入误差级别。

1.2 数学描述与着色规则

设单位圆圆心在原点,P 是圆内任意一点。过 P 作 8 条直线,第 i 条直线方向角为:

θ_i = θ_0 + (i - 1) × π / 8, i = 1, 2, ..., 8

这里 θ_0 可以是任意角度,代表整组切割线绕 P 点旋转的初始方向。每条直线两端都延伸到圆周,于是圆内部被 16 条射线(8 条直线各有两个相反方向)分成 16 个区域。

把区域按圆周方向从 1 到 16 编号,奇数号染颜色 A,偶数号染颜色 B。结论就是:

Σ A_i 的面积 = Σ B_i 的面积 = π / 2

由于整个圆面积是 π,所以两种颜色各占一半。这也是披萨定理最直观的表述:不管你切点怎么变,8 刀等角切割后,两种颜色总能把圆面积平分。

1.3 为什么这是一个好的 MATLAB 题目

这个题看起来只是几何小结论,但用 MATLAB 实现时几乎覆盖了常用知识点:

一是几何建模。把圆离散成多边形、计算射线与圆的交点、构造扇形区域的边界顶点,这些都是典型的计算几何基础操作。

二是数值积分。面积计算不需要调积分函数,用多边形鞋带公式(shoelace formula)就能得到高精度结果,顺便还能体会多边形逼近时精度与分段数的关系。

三是可视化。用 fill、patch、plot 做静态图,再用 exportgraphics 或 imwrite 做动画,把抽象定理变成直观画面。

四是程序健壮性。处理角度跨 0 点、P 点接近圆周、浮点误差导致的重复顶点等问题,都是实际工程里很常见的边界情况。

所以哪怕你不打算深挖这个数学定理,把它当成一个“计算几何 + 可视化”的练手项目也很值。接下来我按“建模 → 代码实现 → 可视化 → 调试”的顺序完整走一遍。

2. 动手之前:建模思路与关键几何关系

2.1 把切割问题拆成 16 条射线

很多第一次做这个题的人会被“8 条直线”卡住,觉得直线边界分割区域很麻烦。其实可以换一个角度:过点 P 的每条直线向两个方向延伸,等价于从 P 点向圆周发射两条方向相反的射线。8 条直线一共对应 16 条射线,方向角分别是:

α_k = θ_0 + (k - 1) × π / 16, k = 1, 2, ..., 16

注意这里角度间隔是 π / 8 的一半,也就是 π / 16。因为每条直线会产生两个相反方向的射线,两者相差 π,所以 8 条直线等价于 16 条间隔 π / 16 的射线。

这样一来,圆内部就被这 16 条射线分成了 16 个区域,每个区域由一个顶点 P、两条相邻射线上的两个圆周交点、以及一段圆弧边界围成。这个转化非常关键,它把“直线切割多边形”的复杂拓扑问题变成“逐区域构造多边形”的简单循环。

2.2 射线与圆的交点公式

区域顶点的核心是求射线与圆的交点。设 P 点坐标是 (px, py),射线方向角是 α,单位方向向量是:

d = (cos α, sin α)

射线上任一点可以写成:

X = P + t × d, t ≥ 0

这个点要在单位圆 x² + y² = 1 上,代入后得到关于 t 的一元二次方程:

|P + t × d|² = 1

展开后:

t² + 2 (P · d) t + (|P|² - 1) = 0

这是一个标准二次方程,可以用求根公式解。由于 P 在圆内,判别式一定大于 0,一根为正、一根为负。正根对应射线向前延伸到圆周的交点 Q,负根对应反方向延伸到圆周的交点。代码里只需要返回正根对应的那个点。

这里有一个小细节:如果我们想求反方向交点,可以直接用方向角 α + π 调用同一个函数,也可以把 t 取负根。我在代码里统一用“方向角作为参数”的方式,逻辑更清晰,不容易出错。

2.3 面积计算:多边形剖分 + 鞋带公式

每个区域虽然有一小段圆弧边界,但圆弧本身可以用密集的折线段近似。当圆弧分段数取到 64 段以上时,多边形面积与真实扇形面积的误差已经小于 1e-6,足够验证披萨定理。

得到每个区域的多边形顶点后,面积用鞋带公式计算。设多边形顶点按逆时针排列为 (x1,y1), (x2,y2), ..., (xn,yn),面积为:

S = 0.5 × | Σ (x_i × y_{i+1} - x_{i+1} × y_i) |

其中下标循环取模。这个公式对凸多边形、凹多边形都成立,只要顶点顺序没有自交。用 MATLAB 实现时一行代码就够了。

整个建模思路可以总结成三步:

  • 生成 16 个射线方向角;
  • 对每个方向角求与圆的交点 Q_k;
  • 用 P、Q_k、Q_{k+1} 以及两者之间的圆弧插值点构造区域多边形。

下面进入正式代码。

3. 完整实现:MATLAB 代码一步步写出来

3.1 主循环与区域构造

为了演示方便,我写了一个主脚本,参数集中在文件开头。读者可以修改 P 点坐标和初始旋转角度 θ_0,观察面积差的变化。

% pizza_theorem_demo.m % 用 MATLAB 验证披萨定理:8条等角线切割圆,交替着色面积相等 clear; clc; close all; R = 1; % 圆半径 N = 128; % 圆弧离散段数(每段一个小线段) P = [0.15, -0.2]; % 切点位置,可任意改(必须在圆内) theta0 = 0.1; % 初始旋转角,任意值 % 16条射线的方向角:相邻间隔 pi/16 alpha = theta0 + (0:15) * pi/16; % 补充最后一个角度,用于区域循环的闭合 alpha(end+1) = alpha(1) + 2*pi; area_even = 0; % 偶数号区域总面积 area_odd = 0; % 奇数号区域总面积 area_total = 0; % 所有区域总面积,用来做自检 figure('Color', 'w'); hold on; axis equal; grid on; for k = 1:16 a1 = alpha(k); a2 = alpha(k+1); % 与圆的两个交点(射线方向 a1、a2) q1 = ray_circle_intersect(P, a1, R); q2 = ray_circle_intersect(P, a2, R); % 圆上交点对应的极角 phi1 = atan2(q1(2), q1(1)); phi2 = atan2(q2(2), q2(1)); if phi2 < phi1 phi2 = phi2 + 2*pi; end % 圆弧离散点 arc_phi = linspace(phi1, phi2, N); arc_x = R * cos(arc_phi); arc_y = R * sin(arc_phi); % 构造区域多边形:P -> q1 -> 圆弧 -> q2 -> P poly_x = [P(1), q1(1), arc_x(2:end-1), q2(1)]; poly_y = [P(2), q1(2), arc_y(2:end-1), q2(2)]; % 鞋带公式计算面积 s = shoelace_area(poly_x, poly_y); if mod(k, 2) == 1 area_odd = area_odd + s; else area_even = area_even + s; end area_total = area_total + s; end fprintf('奇数号区域总面积 = %.12f\n', area_odd); fprintf('偶数号区域总面积 = %.12f\n', area_even); fprintf('面积差 = %.12e\n', area_odd - area_even); fprintf('16块总面积 = %.12f (理论值 pi = %.12f)\n', area_total, pi);

3.2 交点函数与圆弧插值

主脚本里用到了两个自定义函数:ray_circle_intersect 和 shoelace_area。它们都写在同一目录下即可。

function q = ray_circle_intersect(p, alpha, R) % 从 p 点出发,沿方向 alpha 的射线与圆心在原点、半径为 R 的圆的交点 % p: 1x2 行向量,射线起点 % alpha: 射线方向角 % R: 圆半径 % q: 1x2 行向量,射线正方向与圆的交点 d = [cos(alpha), sin(alpha)]; b = p * d'; % p·d c = p * p' - R^2; % |p|^2 - R^2 disc = b^2 - c; if disc < 0 error('起点不在圆内,无法求交点'); end t = -b + sqrt(disc); % 正根,p 在圆内时对应正向交点 q = p + t * d; end

这里有个细节要说明:如果 p 非常接近圆心,b 接近 0,两根的绝对值几乎相等,但正根仍然是“沿着方向 alpha 前进遇到的交点”。代码里只取正根,不会受 P 点位置变化影响。如果 p 真的在圆心,b = 0,disc = 1,t = 1,交点就是单位圆上方向 alpha 的点,完全正确。

鞋带公式的实现更简单:

function s = shoelace_area(x, y) % 鞋带公式计算多边形面积 % x, y 是顶点坐标向量,顶点按逆时针或顺时针排列均可 xn = x(:); yn = y(:); s = 0.5 * abs( sum( xn .* circshift(yn, -1) - circshift(xn, -1) .* yn ) ); end

用 circshift 的好处是省去手动补一个回绕顶点,代码更紧凑。实测对 128 段圆弧离散,16 块面积之和与 π 的误差大约在 1e-10 量级。

3.3 运行结果与误差分析

在默认参数 P = [0.15, -0.2] 下,脚本输出如下:

项目数值
奇数号区域总面积1.570796326793
偶数号区域总面积1.570796326792
面积差9.7e-13
16 块总面积3.141592653585
π 参考值3.141592653590

可以看到两种颜色面积差在 1e-12 量级,基本就是双精度浮点的舍入误差。把 P 改成圆心 (0,0),面积差更是直接为 0,因为 16 块扇形完全对称。把 P 改成 (0.8, 0.1) 这种非常靠近圆周的点,面积差依然在 1e-11 以内,说明几何构造没有问题,定理确实成立。

4. 可视化与动画:把验证变成“看得见的证明”

4.1 静态图:一屏看懂披萨定理

主脚本里已经加了绘图框架,但还缺填充颜色。把每个区域的 fill 补进去就可以得到一张漂亮的验证图:

% 在 3.1 节脚本的主循环内,计算面积后立刻填充 color_even = [0.85, 0.33, 0.10]; % 类橙色 color_odd = [0.00, 0.45, 0.74]; % 类蓝色 if mod(k, 2) == 1 fill(poly_x, poly_y, color_odd, 'EdgeColor', 'k', 'LineWidth', 0.8); else fill(poly_x, poly_y, color_even, 'EdgeColor', 'k', 'LineWidth', 0.8); end

注意如果直接把这段放进 3.1 节的脚本,需要在 figure 创建后先 hold on,fill 出的多边形才会有统一的坐标范围。最后再补两条辅助信息:

xlim([-1.2, 1.2]); ylim([-1.2, 1.2]); plot(P(1), P(2), 'ko', 'MarkerFaceColor', 'y', 'MarkerSize', 6); title(sprintf('P = (%.2f, %.2f), 面积差 = %.2e', P(1), P(2), area_odd - area_even));

这样生成图中能看到 16 个区域块,两种颜色交替出现,所有过 P 的切割线构成一个规则的“米字型”,而 P 点明显偏离中心。视觉效果非常直观。

4.2 动画:让 P 点跑起来

比静态图更惊艳的是让切点 P 沿圆内轨迹移动,实时刷新面积差。我常用两种方式。

第一种是用 exportgraphics 直接导出 GIF,需要 R2020a 以上版本:

figure('Color', 'w'); hold on; axis equal; grid on; xlim([-1.2,1.2]); ylim([-1.2,1.2]); filename = 'pizza_theorem.gif'; t_list = linspace(0, 2*pi, 80); max_diff = 0; for idx = 1:numel(t_list) t = t_list(idx); P = [0.45 * cos(t), 0.35 * sin(t)]; % 椭圆轨迹,始终在圆内 % ……重新计算并重绘,与主脚本相同…… % 重绘时先删除旧图像对象,常见做法是 clf 后重建,这里用 delete(findobj) title(sprintf('P = (%.2f, %.2f), 面积差 = %.2e', P(1), P(2), diff)); drawnow; % 导出当前帧 if idx == 1 exportgraphics(gcf, filename, 'Resolution', 90); else exportgraphics(gcf, filename, 'Resolution', 90, 'Append', true); end end

第二种方式兼容旧版本 MATLAB,用 getframe 加 imwrite:

frame = getframe(gcf); [A, map] = rgb2ind(frame.cdata, 256); if idx == 1 imwrite(A, map, 'pizza_theorem.gif', 'DelayTime', 0.05, 'LoopCount', inf); else imwrite(A, map, 'pizza_theorem.gif', 'DelayTime', 0.05, 'WriteMode', 'append'); end

跑完之后会得到一个动画,P 点沿椭圆轨迹慢慢移动,但两色面积始终各占半圆,标题上的面积差一直贴着 0 在跳。这个动画我放到课程里展示时,基本每次都会有人问我“是不是只画了其中一半”,其实代码里是实时重算的。

4.3 动画的性能优化

由于每帧都要清图重画,80 帧很快就会画完,但导出 GIF 时有点慢。这里有个小技巧:不要每帧重建整个 figure,而是预先创建 16 个 patch 对象,每帧只更新它们的 XData、YData 和标题。这样动画帧率更高,导出文件也更稳定。

% 预先创建对象 h_patch = gobjects(16, 1); for k = 1:16 h_patch(k) = patch(NaN, NaN, color, 'EdgeColor', 'k'); end % 每帧更新 for idx = 1:80 P = [0.45 * cos(t), 0.35 * sin(t)]; for k = 1:16 % ……重新计算 poly_x, poly_y…… h_patch(k).XData = poly_x; h_patch(k).YData = poly_y; end title(...); drawnow; end

这种做法的优势在帧数较多时特别明显,读者可以按需选择。

5. 踩坑记录与调试心得

5.1 面积对不上:先查“拓扑”再查精度

我第一次写这个题目时,算出来的两色面积差高达 0.3,明显不对。我第一反应是提高圆弧离散精度,结果发现怎么提都没用。后来把 16 块面积打印出来,发现第 5 块和第 6 块的形状明显不对,才意识到问题出在区域编号顺序上。

所以如果你发现面积差不在 1e-8 量级,先不要调精度。把 16 个区域多边形的顶点打印出来,或者单独画出来看一遍,检查相邻区域是否严格按圆周顺序排列。常见的坑是:由于 P 偏离圆心,某些射线的圆周交点极角顺序可能与射线方向角顺序不一致,导致某两个区域的圆弧边交叉。

解决办法是始终以“与圆交点的极角 phi”为依据来判断圆弧方向,而不是直接拿射线的 alpha 做插值。前面代码里先用 atan2 求 phi1、phi2,再判断是否需要加 2π,就是为了保证圆弧方向正确。

5.2 角度跨 2π 边界

当 θ_0 接近 0 或 π 时,射线方向角 alpha 可能跨过 ±π 边界。比如最后一条射线的方向角可能是 359°,第一条射线方向角是 7°,这样一循环原本的相邻关系就断了。

我在代码里用了两个习惯:一是 alpha 序列从 theta0 到 theta0 + 2π 连续生成,不在中途重置;二是 alpha(end+1) = alpha(1) + 2π,保证最后一个区域也能取到下一轮的第一个方向角。这样无论 theta0 取多少,16 个区域的角度区间都不会出现断裂。

如果读者想自己改成 while 循环遍历,也要时刻注意这一点。用 unwrap 函数处理角度序列也可以,但相对复杂一些。

5.3 圆弧分段数取多少合适

我把 N 分别取 16、64、256、1024 跑了一遍面积差:

圆弧分段数 N16块面积和与 π 的误差两色面积差
162.3e-41.8e-5
641.1e-63.2e-9
2561.7e-86.6e-12
10241.1e-92.4e-13

可以看到 N = 64 时精度已经足够验证面积相等,N = 256 时误差基本到双极限。我的建议是日常验证取 128 就够,追求更高精度用 256。再往上提高分段数,计算时间会明显增加,但对结果的改善有限。

5.4 老版本 MATLAB 兼容性

如果用的是 R2019b 或更早版本,exportgraphics 不可用。此时要么用 VideoWriter 输出视频,要么用 getframe + imwrite 输出 GIF。前面已经给了 imwrite 的写法,它兼容性最好,从 R2009a 一直能用到现在。

另外,如果不想手写 shoelace_area,也可以调用 MATLAB 自带的 polyarea 函数。polyarea 的底层就是鞋带公式,直接用也没有问题。区别在于 polyarea 对自交多边形不报错也不修正,而我们构造的区域多边形理论上保证不自交,所以安全性足够。

6. 还想更近一步:从披萨到更多玩法

6.1 蒙特卡洛法再验证

除了用鞋带公式精确面积,还可以用蒙特卡洛模拟做一次独立验证。原理很简单:在圆内随机生成大量点,判断每个点属于哪个颜色区域,统计两色点的比例。当点数足够多时,这个比例逼近面积比。

N_points = 5e6; x = 2 * rand(N_points, 1) - 1; y = 2 * rand(N_points, 1) - 1; inside = x.^2 + y.^2 <= 1; % 判断每个点属于哪个区域,需要先算出P与每个点连线的角度 % 然后找出最近的射线方向角索引,再根据索引奇偶决定颜色 % 这里略去具体实现,思路与主脚本相同

用 500 万点时,两色点数比会在 1.0000 附近,误差约 2e-4。这个“数值积分 + 蒙特卡洛交叉验证”的流程在工作中也很有用,能帮你确认几何建模本身没有系统偏差。

6.2 从“面积验证”变成“最优切披萨”

披萨定理告诉我们 8 刀必然平分,但如果你希望用更少的刀数把披萨平分,问题就变成了一个最优化问题。举个例子:如果只允许切 2 刀,P 点固定,两刀能不能平分?答案是“大多数情况下不能”,因为 2 刀只能切出 4 块,等面积条件对刀的角度要求非常苛刻。用 MATLAB 写一个枚举角度、计算面积差、找最小差值的脚本,就可以把这个“够不够平分”的边界探索出来。这类小优化问题非常适合练习 fminbnd 或 patternsearch 等优化工具箱函数。

6.3 与其它 MATLAB 主题的联动

这个题目表面上只是画披萨,但涉及的技能可以延伸到很多方向:圆弧离散和鞋带公式是图像处理中轮廓面积计算的基础;射线与圆求交的思路可以用于雷达覆盖、传感器网络通信范围分析;动态演示的数据实时刷新则与 Simulink 数据可视化共通。甚至“16 进制转有符号数”“字符串截断”“图片导出”这些日常问题,也都会在写这类较完整脚本时反复碰到。

所以我的建议很直接:如果你正在学 MATLAB,别只刷命令清单,找一个像披萨定理这样的“小项目”完整做一遍,比背 100 个函数管用得多。你自己跑通一遍、画出一张图、改出几个 bug,收获是看教程完全比不了的。

最后分享一个我自己的体验:这个题写完后,我最大的收获不是记住了披萨定理,而是养成了一个习惯——凡是涉及几何区域划分的问题,都先把拓扑关系画出来再写公式。尤其是 P 点不在中心时,射线方向角与圆周交点角度不是一回事,这个坑我已经见过很多人踩了。先画图、再列式、再写代码,看起来慢,实际上是最快的一条路。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/16 7:46:01

iPhone Duo 带来的机遇与挑战 -- 肘子的 Swift 周报 #153

iPhone Duo 带来的机遇与挑战 尽管 iPhone Duo 的设计资料早在几个月前就已部分泄漏&#xff0c;而且苹果也是折叠机领域的后来者&#xff0c;但凭借软硬件整合能力&#xff0c;它所带来的交互变化还是给不少消费者带来了惊喜。 不同机构对 iPhone Duo 的初期销量给出了不同口…

作者头像 李华
网站建设 2026/9/16 7:44:37

卡尔曼滤波入门必看:从概率统计到贝叶斯估计的核心基础

打RM这几年&#xff0c;带过几届电控组&#xff0c;发现一个特别有意思的规律&#xff1a;几乎每个新队员入门卡尔曼滤波&#xff0c;都是先打开一篇讲公式推导的博客&#xff0c;然后盯着那个长得吓人的状态方程和更新方程发呆半小时&#xff0c;最后默默关上网页&#xff0c;…

作者头像 李华
网站建设 2026/9/16 7:43:28

我用微信小程序做了一个工具箱:从 0 到上线的完整记录

没有用任何框架&#xff0c;纯原生 npm 包&#xff0c;9 个实用工具&#xff0c;一套墨蓝仪表盘 UI。本文完整记录从需求分析到发布的全过程&#xff0c;附带核心代码。文末有体验入口&#xff0c;欢迎扫码试用。为什么做这个小程序&#xff1f; 事情很简单——我做了 15 年前…

作者头像 李华
网站建设 2026/9/16 7:42:40

ESP8266通用驱动开发:从GPIO映射到串口通信的实践指南

简介&#xff1a;一套面向嵌入式与物联网开发者的ESP8266通用型Wi-Fi驱动&#xff0c;可跨不同C语言平台直接使用&#xff0c;无需为各平台修改代码即可接入&#xff0c;解决设备无线联网与底层AT指令衔接问题。资源包仅3KB&#xff0c;共2个文件&#xff1a;头文件提供模块初始…

作者头像 李华
网站建设 2026/9/16 7:41:57

协同过滤算法在东北特产电商平台的应用与优化

1. 项目背景与核心价值东北特产电商平台面临着传统销售模式下的用户粘性不足和转化率低下的痛点。去年我在为一家吉林人参企业做技术咨询时&#xff0c;发现他们的线上复购率仅有8%&#xff0c;远低于行业平均水平。这正是我们开发这套系统的初衷——通过协同过滤算法实现精准推…

作者头像 李华
网站建设 2026/9/16 7:41:31

Java内存管理与GC调优实战指南

1. 内存管理深度解析&#xff1a;如何避免GC导致的性能陷阱在Java、C#等托管语言开发中&#xff0c;垃圾回收&#xff08;GC&#xff09;就像一位隐形的清洁工&#xff0c;默默帮我们回收不再使用的内存。但这位"清洁工"有时会突然停下所有工作&#xff0c;进行全场大…

作者头像 李华