本文介绍如何利用定积分
近似计算圆周率 Pi。文章先解释该积分为什么等于 Pi,然后介绍梯形法则和中点法则的数学原理,并给出 Python 与 C++ 的单线程、多线程实现。最后进一步说明如何将积分区间划分到五个分布式计算节点上,并比较不同实现方式的精度、性能、并行能力和适用场景。
圆周率 Pi,通常写作
,广泛出现在数学、物理、工程、统计学和计算机科学中。虽然大多数编程语言都已经提供了内置的 Pi 常量,但亲自计算 Pi 仍然是学习微积分、数值积分、多线程和分布式计算的一个很好的学习的例子。
本文将通过下面这个定积分计算 Pi:

本文将介绍:
- 为什么这个积分等于 Pi
- 数值积分如何近似计算定积分
- 梯形法则
- 中点法则
- 单线程 Python 实现
- 多线程 Python 实现
- 单线程 C++ 实现
- 多线程 C++ 实现
- 如何将同一个算法分配到五个计算节点上
为什么这个积分等于 Pi?
考虑下面的定积分:

关键在于,反正切函数的导数是:

因此:

将积分上下限零和一代入:

也就是:

我们知道:

并且:

因此:

所以:

这就为我们提供了一种通过数值积分计算 Pi 的方法。
数值积分是如何工作的?
计算机不一定要通过符号运算直接求出积分的解析解。它可以把积分区间分成许多很小的部分,然后近似计算曲线下面的面积。
假设我们需要计算:

将区间
分成
个长度相等的小区间。
每个小区间的宽度为:

划分区间的各个点为:

其中:

近似计算每一个小区域的面积有多种方法。两种常见的方法是:
- 梯形法则
- 中点法则
梯形法则
你可能记得的那个包含
、
和除以二的公式,就是梯形法则。
对于从
到
的一个小区间,我们使用一条直线连接函数在两个端点上的值。
这样形成的区域是一个梯形。
它的面积近似为:

将所有梯形的面积相加,可以得到:

一个等价并且通常更容易编程实现的公式是:

两个端点只计算一半的权重:

所有内部点使用完整权重:

对于我们的 Pi 积分:

并且:

因此:

梯形法则对 Pi 的近似公式为:

因为:

并且:

所以还可以写成:

一个简单的梯形法则示例
假设我们把积分区间分成四份:

每个小区间的宽度为:

五个边界点为:

函数在这些点上的值大约为:





应用梯形法则:

因此:

Pi 的真实值大约为:

即使只划分为四个小区间,我们也已经得到了一个比较合理的近似值。增大
可以进一步提高结果的精度。
中点法则
本文后面的主要代码使用的是中点法则。
梯形法则在每个小区间的边界上计算函数值,而中点法则在每个小区间的中心位置计算函数值。
第
个区间的中点为:

该小区间的面积可以近似看成一个矩形:

将所有矩形面积相加:

对于 Pi 的积分,
、
,并且
。
因此:

并且:

代入
:

越大,计算结果通常越接近 Pi 的真实值。
梯形法则与中点法则的比较
| 方法 | 函数取值位置 | 公式 |
|---|---|---|
| 梯形法则 | 每个区间的边界 | ![]() |
| 中点法则 | 每个区间的中心 | ![]() |
对于足够平滑的函数,这两种方法的误差通常都与下面的量成正比:

不过,在使用相同数量小区间的情况下,中点法则通常比梯形法则更加精确。
使用中点法则的单线程 Python 实现
下面的 Python 程序将积分区间划分成 2500 万个小区间。
import math
import time
def calculate_pi(total_steps: int) -> float:
if total_steps <= 0:
raise ValueError("total_steps 必须为正数")
step_width = 1.0 / total_steps
total = 0.0
for step in range(total_steps):
midpoint = (step + 0.5) * step_width
total += 4.0 / (1.0 + midpoint * midpoint)
return total * step_width
def main() -> None:
total_steps = 25_000_000
started_at = time.perf_counter()
pi_estimate = calculate_pi(total_steps)
elapsed_seconds = time.perf_counter() - started_at
absolute_error = abs(pi_estimate - math.pi)
print(f"Pi 估算值: {pi_estimate:.15f}")
print(f"Pi 参考值: {math.pi:.15f}")
print(f"绝对误差: {absolute_error:.3e}")
print(f"运行时间: {elapsed_seconds:.3f} 秒")
if __name__ == "__main__":
main()
核心计算代码是:
midpoint = (step + 0.5) * step_width
total += 4.0 / (1.0 + midpoint * midpoint)
第一行计算当前小区间的中点:

第二行计算函数值:

在累加所有函数值之后,再乘以每个小区间的宽度:
return total * step_width
这在数学上对应:

使用梯形法则的单线程 Python 实现
梯形法则版本在各个小区间的边界位置计算函数值,而不是在中点计算。
import math
import time
def calculate_pi_trapezoidal(total_steps: int) -> float:
if total_steps <= 0:
raise ValueError("total_steps 必须为正数")
h = 1.0 / total_steps
def f(x: float) -> float:
return 4.0 / (1.0 + x * x)
total = 0.5 * (f(0.0) + f(1.0))
for step in range(1, total_steps):
x = step * h
total += f(x)
return total * h
def main() -> None:
total_steps = 25_000_000
started_at = time.perf_counter()
pi_estimate = calculate_pi_trapezoidal(total_steps)
elapsed_seconds = time.perf_counter() - started_at
absolute_error = abs(pi_estimate - math.pi)
print(f"Pi 估算值: {pi_estimate:.15f}")
print(f"Pi 参考值: {math.pi:.15f}")
print(f"绝对误差: {absolute_error:.3e}")
print(f"运行时间: {elapsed_seconds:.3f} 秒")
if __name__ == "__main__":
main()
两个端点的贡献通过下面的代码计算:
total = 0.5 * (f(0.0) + f(1.0))
这对应:

循环计算所有内部点的函数值之和:
for step in range(1, total_steps):
x = step * h
total += f(x)
这对应:

最后再乘以
:
return total * h
使用中点法则的多线程 Python 实现
我们可以把 2500 万个小区间分配给五个工作线程。
对于总共
个工作线程中的第
个工作线程,它的起始位置为:

结束位置为:

对于五个工作线程和 2500 万个小区间:
| 工作线程 | 起始步骤 | 结束步骤 | 步骤数量 |
|---|---|---|---|
| 0 | 0 | 5,000,000 | 5,000,000 |
| 1 | 5,000,000 | 10,000,000 | 5,000,000 |
| 2 | 10,000,000 | 15,000,000 | 5,000,000 |
| 3 | 15,000,000 | 20,000,000 | 5,000,000 |
| 4 | 20,000,000 | 25,000,000 | 5,000,000 |
下面使用 ThreadPoolExecutor 实现:
import math
import time
from concurrent.futures import ThreadPoolExecutor
def calculate_partial_sum(
start_step: int,
end_step: int,
step_width: float,
) -> float:
partial_sum = 0.0
for step in range(start_step, end_step):
midpoint = (step + 0.5) * step_width
partial_sum += 4.0 / (1.0 + midpoint * midpoint)
return partial_sum
def calculate_pi_multithreaded(
total_steps: int,
worker_count: int,
) -> float:
if total_steps <= 0:
raise ValueError("total_steps 必须为正数")
if worker_count <= 0:
raise ValueError("worker_count 必须为正数")
step_width = 1.0 / total_steps
ranges = []
for worker_id in range(worker_count):
start_step = total_steps * worker_id // worker_count
end_step = total_steps * (worker_id + 1) // worker_count
ranges.append((start_step, end_step))
with ThreadPoolExecutor(max_workers=worker_count) as executor:
futures = [
executor.submit(
calculate_partial_sum,
start_step,
end_step,
step_width,
)
for start_step, end_step in ranges
]
partial_sums = [
future.result()
for future in futures
]
return sum(partial_sums) * step_width
def main() -> None:
total_steps = 25_000_000
worker_count = 5
started_at = time.perf_counter()
pi_estimate = calculate_pi_multithreaded(
total_steps,
worker_count,
)
elapsed_seconds = time.perf_counter() - started_at
absolute_error = abs(pi_estimate - math.pi)
print(f"工作线程数: {worker_count}")
print(f"Pi 估算值: {pi_estimate:.15f}")
print(f"Pi 参考值: {math.pi:.15f}")
print(f"绝对误差: {absolute_error:.3e}")
print(f"运行时间: {elapsed_seconds:.3f} 秒")
if __name__ == "__main__":
main()
每个工作线程计算一个部分和:

主线程再把这些结果合并起来:

Python 多线程的一个重要限制
这个多线程 Python 版本展示了如何把计算划分为多个独立区间,但它不一定比单线程版本运行得更快。
标准 CPython 存在全局解释器锁,也就是通常所说的 GIL。
对于 CPU 密集型 Python 代码,在同一时刻通常只有一个线程能够执行 Python 字节码。
因此,五个 Python 线程并不意味着能够有效地同时使用五个 CPU 核心。
这个多线程版本可能会出现以下情况:
- 运行速度与单线程版本基本相同
- 由于线程管理开销,运行速度反而稍慢
- 适合演示如何分配计算任务
- 不适合在普通 CPython 中实现真正的 CPU 并行计算
如果需要在 Python 中实现真正的 CPU 并行,可以考虑:
ProcessPoolExecutormultiprocessing模块- NumPy
- 本地编译扩展
- MPI
- 独立的分布式进程
使用中点法则的单线程 C++ 实现
C++ 非常适合执行这种紧密的数值循环,因为它可以被编译为本地机器代码/Native Code。
#include <chrono>
#include <cmath>
#include <cstdint>
#include <iomanip>
#include <iostream>
#include <stdexcept>
double calculate_pi(std::uint64_t total_steps) {
if (total_steps == 0) {
throw std::invalid_argument(
"total_steps 必须为正数"
);
}
const double step_width =
1.0 / static_cast<double>(total_steps);
double total = 0.0;
for (
std::uint64_t step = 0;
step < total_steps;
++step
) {
const double midpoint =
(static_cast<double>(step) + 0.5)
* step_width;
total += 4.0 / (1.0 + midpoint * midpoint);
}
return total * step_width;
}
int main() {
constexpr std::uint64_t total_steps = 25'000'000;
const auto started_at =
std::chrono::steady_clock::now();
const double pi_estimate =
calculate_pi(total_steps);
const auto finished_at =
std::chrono::steady_clock::now();
const double elapsed_seconds =
std::chrono::duration<double>(
finished_at - started_at
).count();
const double reference_pi = std::acos(-1.0);
const double absolute_error =
std::abs(pi_estimate - reference_pi);
std::cout
<< std::fixed
<< std::setprecision(15);
std::cout
<< "Pi 估算值: "
<< pi_estimate
<< '\n';
std::cout
<< "Pi 参考值: "
<< reference_pi
<< '\n';
std::cout
<< std::scientific
<< std::setprecision(3);
std::cout
<< "绝对误差: "
<< absolute_error
<< '\n';
std::cout
<< std::fixed
<< std::setprecision(3);
std::cout
<< "运行时间: "
<< elapsed_seconds
<< " 秒\n";
return 0;
}
使用优化选项进行编译:
g++ -O3 -std=c++17 pi_single.cpp -o pi_single
运行程序:
./pi_single
-O3 选项会启用较为激进的编译器优化。如果不启用优化,程序的运行速度可能会慢很多。
使用梯形法则的单线程 C++ 实现
下面的 C++ 程序实现了梯形法则:

#include <chrono>
#include <cmath>
#include <cstdint>
#include <iomanip>
#include <iostream>
#include <stdexcept>
double f(double x) {
return 4.0 / (1.0 + x * x);
}
double calculate_pi_trapezoidal(
std::uint64_t total_steps
) {
if (total_steps == 0) {
throw std::invalid_argument(
"total_steps 必须为正数"
);
}
const double h =
1.0 / static_cast<double>(total_steps);
double total =
0.5 * (f(0.0) + f(1.0));
for (
std::uint64_t step = 1;
step < total_steps;
++step
) {
const double x =
static_cast<double>(step) * h;
total += f(x);
}
return total * h;
}
int main() {
constexpr std::uint64_t total_steps = 25'000'000;
const auto started_at =
std::chrono::steady_clock::now();
const double pi_estimate =
calculate_pi_trapezoidal(total_steps);
const auto finished_at =
std::chrono::steady_clock::now();
const double elapsed_seconds =
std::chrono::duration<double>(
finished_at - started_at
).count();
const double reference_pi = std::acos(-1.0);
const double absolute_error =
std::abs(pi_estimate - reference_pi);
std::cout
<< std::fixed
<< std::setprecision(15);
std::cout
<< "Pi 估算值: "
<< pi_estimate
<< '\n';
std::cout
<< "Pi 参考值: "
<< reference_pi
<< '\n';
std::cout
<< std::scientific
<< std::setprecision(3);
std::cout
<< "绝对误差: "
<< absolute_error
<< '\n';
std::cout
<< std::fixed
<< std::setprecision(3);
std::cout
<< "运行时间: "
<< elapsed_seconds
<< " 秒\n";
return 0;
}
使用下面的命令编译:
g++ -O3 -std=c++17 pi_trapezoidal.cpp -o pi_trapezoidal
使用中点法则的多线程 C++ 实现
与普通 CPython 线程不同,C++ 线程可以在多个 CPU 核心上同时执行 CPU 密集型计算。
下面的实现将计算任务分配给五个线程。
#include <chrono>
#include <cmath>
#include <cstdint>
#include <iomanip>
#include <iostream>
#include <numeric>
#include <stdexcept>
#include <thread>
#include <vector>
double calculate_partial_sum(
std::uint64_t start_step,
std::uint64_t end_step,
double step_width
) {
double partial_sum = 0.0;
for (
std::uint64_t step = start_step;
step < end_step;
++step
) {
const double midpoint =
(static_cast<double>(step) + 0.5)
* step_width;
partial_sum +=
4.0 / (1.0 + midpoint * midpoint);
}
return partial_sum;
}
double calculate_pi_multithreaded(
std::uint64_t total_steps,
std::size_t worker_count
) {
if (total_steps == 0) {
throw std::invalid_argument(
"total_steps 必须为正数"
);
}
if (worker_count == 0) {
throw std::invalid_argument(
"worker_count 必须为正数"
);
}
const double step_width =
1.0 / static_cast<double>(total_steps);
std::vector<std::thread> workers;
std::vector<double> partial_sums(
worker_count,
0.0
);
workers.reserve(worker_count);
for (
std::size_t worker_id = 0;
worker_id < worker_count;
++worker_id
) {
const std::uint64_t start_step =
total_steps
* worker_id
/ worker_count;
const std::uint64_t end_step =
total_steps
* (worker_id + 1)
/ worker_count;
workers.emplace_back(
[
&,
worker_id,
start_step,
end_step
]() {
partial_sums[worker_id] =
calculate_partial_sum(
start_step,
end_step,
step_width
);
}
);
}
for (std::thread& worker : workers) {
worker.join();
}
const double combined_sum =
std::accumulate(
partial_sums.begin(),
partial_sums.end(),
0.0
);
return combined_sum * step_width;
}
int main() {
constexpr std::uint64_t total_steps = 25'000'000;
constexpr std::size_t worker_count = 5;
const auto started_at =
std::chrono::steady_clock::now();
const double pi_estimate =
calculate_pi_multithreaded(
total_steps,
worker_count
);
const auto finished_at =
std::chrono::steady_clock::now();
const double elapsed_seconds =
std::chrono::duration<double>(
finished_at - started_at
).count();
const double reference_pi = std::acos(-1.0);
const double absolute_error =
std::abs(pi_estimate - reference_pi);
std::cout
<< "工作线程数: "
<< worker_count
<< '\n';
std::cout
<< std::fixed
<< std::setprecision(15);
std::cout
<< "Pi 估算值: "
<< pi_estimate
<< '\n';
std::cout
<< "Pi 参考值: "
<< reference_pi
<< '\n';
std::cout
<< std::scientific
<< std::setprecision(3);
std::cout
<< "绝对误差: "
<< absolute_error
<< '\n';
std::cout
<< std::fixed
<< std::setprecision(3);
std::cout
<< "运行时间: "
<< elapsed_seconds
<< " 秒\n";
return 0;
}
使用线程支持和优化选项进行编译:
g++ -O3 -std=c++17 -pthread pi_multithreaded.cpp -o pi_multithreaded
运行程序:
./pi_multithreaded
为什么每个线程都使用自己的局部求和变量?
每个线程内部首先使用一个局部变量进行计算:
double partial_sum = 0.0;
线程不会在每一次循环中都修改同一个共享的全局变量。
如果所有线程不断更新同一个共享变量,程序就需要使用互斥锁或者原子操作。
这会引入同步开销和线程竞争,可能抵消多线程带来的性能优势。
因此,程序采用了类似 MapReduce 的模式:
- 为每个工作线程分配一段积分区间。
- 让每个工作线程独立完成计算。
- 每个线程保存一个部分结果。
- 等待所有线程结束。
- 将所有部分结果相加。
在数学上:

最终近似值为:

将算法扩展到五个分布式计算节点
多线程和分布式计算使用相同的数学划分方法,但它们是两种不同的执行模型。
线程通常具有以下特点:
- 运行在同一台机器上
- 共享相同的内存空间
- 属于同一个进程
- 通过共享变量进行通信
分布式工作进程通常具有以下特点:
- 作为独立进程运行
- 可能运行在不同的物理机器上
- 不共享普通的进程内存
- 通过文件、Socket、MPI、RPC 或其他分布式运行时进行通信
在一个五节点任务中,每个进程都会获得一个 Rank:

进程总数为:

每个 Rank 使用下面的公式计算自己的任务起始位置:

结束位置为:

在 Python 中,可以这样分配计算范围:
import os
rank = int(os.environ["RANK"])
world_size = int(os.environ["WORLD_SIZE"])
start_step = total_steps * rank // world_size
end_step = total_steps * (rank + 1) // world_size
每个 Rank 只计算自己负责的区间:
partial_sum = 0.0
for step in range(start_step, end_step):
midpoint = (step + 0.5) * step_width
partial_sum += 4.0 / (
1.0 + midpoint * midpoint
)
对于五个 Rank,最终结果为:

在前面的 Singularity 示例中,每个 Rank 都会写入一个结果文件:
rank-0.json
rank-1.json
rank-2.json
rank-3.json
rank-4.json
Rank 0 等待全部五个文件生成,然后读取每个文件中的部分和,将它们相加,最终得到 Pi 的估算值。
这个过程实现了三种分布式操作:
- 屏障同步(Barrier):等待所有工作节点完成计算
- 收集(Gather):收集所有节点的部分结果
- 归约(Reduce):将所有部分和相加
基于文件的实现比较简单,也很容易理解,但它假设所有节点都可以访问同一个共享输出目录。
在正式的生产级分布式程序中,通常会使用 MPI 或其他支持集合通信的框架,以获得更加可靠和高效的通信机制。
数值积分方法的精度
对于足够平滑的函数,梯形法则和中点法则的误差通常都与下面的量成正比:

这意味着,当小区间数量加倍时,数值积分误差可能大约缩小为原来的四分之一。
不过,无限增大
并不意味着可以获得无限精度。
计算机使用有限精度的浮点数存储数据。当程序累加几百万甚至几十亿个数值时,舍入误差也可能逐渐累积。
并行版本和单线程版本在最后几位数字上也可能略有不同,因为浮点数加法并不严格满足结合律:

这些表达式在数学上是等价的,但是在有限精度浮点计算中,改变加法顺序可能会改变最终的舍入结果。
性能方面的注意事项
在以下条件下,多线程 C++ 实现通常会比单线程版本更快:
- 计算机拥有多个 CPU 核心
- 计算任务足够大
- 工作线程数量设置合理
- 启用了编译器优化
使用五个线程并不保证一定能够获得五倍性能提升。
程序性能还会受到以下因素影响:
- CPU 核心数量
- 处理器运行频率
- CPU 温度以及降频
- 操作系统的线程调度
- CPU 缓存行为
- 线程创建和销毁开销
- 计算机上正在运行的其他任务
对于 Python,GIL 是主要限制。增加 Python 线程数量通常无法加速纯 Python 的 CPU 密集型循环。
对于分布式计算,节点分配、进程启动、共享存储访问以及结果合并也都会产生额外开销。
使用 2500 万个区间计算 Pi 是一个很好的教学示例,但在真实生产环境中,这个计算任务过于简单,不值得为此分配多个昂贵的 GPU 计算节点。
不同实现方式的比较
| 实现方式 | 执行模型 | 真正的 CPU 并行 | 预期性能 |
|---|---|---|---|
| Python 单线程 | 一个 Python 线程 | 否 | 实现简单,但速度相对较慢 |
| Python 多线程 | 多个 Python 线程 | 通常不能,因为受到 GIL 限制 | 通常不会比单线程更快 |
| C++ 单线程 | 一个本地线程 | 否 | 通常比纯 Python 快很多 |
| C++ 多线程 | 多个本地线程 | 是 | 通常是本地计算中最快的版本 |
| 五节点分布式计算 | 多个独立工作进程 | 是 | 可以扩展,但存在启动和通信开销 |
总结
下面这个恒等式:

为我们提供了一个简单的例子,展示如何把微积分问题转换成计算机可以执行的数值计算问题。
梯形法则使用相邻边界点之间的直线近似曲线:

中点法则使用每个小区间的中心位置计算函数值:

同一个数学计算可以通过多种方式执行:
- 在一个 CPU 核心上运行单个循环
- 在同一台机器上使用多个线程
- 使用多个进程和多个 CPU 核心
- 使用运行在不同计算节点上的分布式工作进程
无论使用哪一种执行模型,背后的数学算法基本不变。
真正发生变化的是:如何划分积分区间、在哪里计算部分和,以及如何将所有部分结果重新合并起来。
这也是很多并行算法背后的核心模式:
任务分割 —> 计算部分结果 —> 合并计算最终结果

虽然计算 Pi 只是一个简单的示例,但同样的 MapReduce 思想广泛应用于科学模拟、机器学习、数据处理、图形渲染、金融建模和大规模分布式系统中。
数学
- 使用 Python 和 C++ 数值积分计算圆周率 Pi
- 数学之美: Sigma 函数的推导公式与 Python 实现
- 教孩子编程: 证明根号2是个无理数的两种方法(反正法/几何无限下降法)
- 机器学习(最优化)根本数学公式: arg_max_{x∈X} F(x)
- ChatGPT (3.5和4) 解2023年高考数学题
- [答案] 数学 × 程式编写比赛 (第八回) - 4位数的电子显示器
- 数学证明 0.9 ... 无限循环等于1
英文:Calculating Pi with Numerical Integration in Python and C
- 2003年高考数学: 一张试卷改变了多少人的命运
2003年高考数学,因为试卷被盗临时启用备用卷,难度陡增,成为许多考生难以忘记的一场考试。作者回忆当年走出考场后的崩溃与无助,也感慨一张试卷如何改变了无数人的命运。多年以后再回头看,那场考试留下的不只是分数,还有对人生无常的深刻记忆。 2003 年福建高考数学使用的是全国卷。那一年因为四川南充南部县发生高考试卷被盗案,外界普遍说法是临时启用了备用卷,所以数学难度异常高。后来福建从 2004 年开始语文、数学、英语自行命题,直到 2016 年才再次全部科目回归全国卷。 公开报道提到,福建“上一次全部科目使用全国卷是 2003 年”,2004 年起语数英开始自行命题。 发生在四川南充南部县,作案人杨博盗走了语文、英语、文科数学、理科数学、综合等试卷,后来被判刑。 2003全国高考数学卷难度地狱 我是2003年参加高考的。 如果没记错,那时候的考试安排是:第一天上午考语文,下午考数学;第二天上午考英语,下午考理综,也就是物理、化学和生物。 我们当时并不是在本校考试,而是要去另一所学校参加高考。那种感觉现在想起来还很清楚:陌生的考场,紧张的气氛,所有人都绷着一根弦。 但真正让我至今难忘的,是第一天下午的数学。 数学一考完,整个考场外几乎是一片“狼嚎”。很多同学出来以后都崩溃了,听说还有不少人当场哭了。那种难,不是平时考试最后几道大题做不出来的难,而是从选择题开始就让人怀疑人生。 我自己当时也完全懵了。 平时数学正常发挥的话,基本上是120分起跳。可那一次,我在考场里从一开始就觉得不对劲:怎么选择题都这么吃力?怎么大题几乎无从下手?我当时第一反应不是“题太难”,而是怀疑自己是不是今天脑子坏了。… - 今年IMC英国数学竞赛很难-我娃搞了个金奖
我娃就是一普娃。他和我说在之前公校都学不到东西,言下之意就是太简单了。我说那是因为难度还没上去。我在你这年纪(初一)第一次期中考 班级第一,年段第二。黑板上写着前几名的名字表扬。还有当时家长会在操场上坐着,念到我名字表扬上台领了一奖状 还有20元红包。98年20元相当于现在10英镑? 这可能是我人生到现在唯一一次高光时刻吧。当时我妈在台下应该很自豪。 我娃说那你当时应该很用功。我说当时我没咋学。因为太简单了。你现在觉得简单是应为遗传了我的智商,而难度上去就不够了。97努力+3%的天赋。我娃一脸震惊🤯 希望他们多受些挫折 数学这一门学科,会就是会,不会就是不会,所以经常用来看一个娃是不是聪明,是不是有天赋。 这个据说今年特别难,金奖绝对是有实力的。这种水平感觉在perse 也是牛娃了。 关键是娃的爹是MSRC顶尖计算机科学家 你俩的郎才女貌全被遗传走了啊 小时候那几块钱的奖励其实很有意思的,还有小本子发,要是没有那个奖励我估计我还找不到读书的动力 今年的IMC难度大,普遍得分低,他7年级拿Gold非常优秀了。我老大9年级也才拿90来分。 主要是私校这么贵的学费 也没见砸出个什么水花来 不er,你这有点凡尔赛了啊。 去年差2分进入下一轮Olympic。听说Kangroo的分数线很低。 本文一共 492 个汉字,… - 组合数学入门(2): 卡特兰数的简介及应用
组合数学入门(2):卡特兰数 卡特兰数是组合数学中最重要的数列之一。它们出现在许多表面上看起来完全不同的计数问题中,但实际上这些问题都具有相同的内在结构:平衡性、递归性以及“不交叉”约束。 在本文中,我们将介绍卡特兰数/Catalan,展示几个重要公式,并解释一些经典应用场景,特别是路径不能越过对角线的网格行走问题。 什么是卡特兰数? 卡特兰数列如下: 第 n 个卡特兰数的一般公式为: 一个等价形式为: 这两个公式完全等价,在组合数学中都经常出现。 为什么卡特兰数很重要 卡特兰数用于计数许多具有递归结构或平衡结构的问题。它们通常出现在以下情形中: 对象必须以平衡方式构造, 路径必须保持在某个边界之内, 配对之间不能交叉, 结构可以被拆分为独立的左右部分。 因此,它们广泛出现在括号、树、网格、多边形以及栈操作等问题中。 一些重要的卡特兰公式 闭式公式 差分等价形式… - 教娃(比教媳妇)编程/数学更有成就感
从2020年11月22日开始第一课,那时我还在亚马逊 AWS S3 团队,孩子们还很小。最初教娃是每天一课,后来调整为每周三课,再到两课,最后稳定在每周一课(中间还停过大约半年)。 我媳妇说得挺对的——我确实挺喜欢教别人,从中能获得一种满足感和成就感。当然,这只是原因之一。当初教孩子,还有一个小心思:有两个固定“听众”,可以让我更自然地练习表达能力和英语,同时也能顺便刷题,再把知识教给孩子,一举多得。好在两个孩子也挺配合,而且都是理科型,不知道是不是受了我的影响。 其实内容并不局限于编程(数据结构与算法),有时也会穿插一些数据库、数学和逻辑等内容,整体以一种自由探索式的学习为主。 到今天为止,一共给他们上了739节编程课(五年半)。从去年开始,我每天带着弟弟刷 LeetCode,让他多动手实践——他敲代码,我在旁边指导(到今天已经坚持了400天),最近哥哥也加入进来了。说实话,这种陪伴式的成长过程,真的很有成就感。 我以前也试过教我妻子编程,但很快就发现这并不是她感兴趣或擅长的事情。时间一长,她基本也就忘得差不多了。这其实很正常——人往往很难记住那些既不感兴趣、也不常用的知识。这件事也让我意识到:自己擅长,并不代表就一定能教好别人,尤其是在对方缺乏兴趣或动力的情况下。 有人质疑,现在刷题已经没用了。确实,在 AI 时代,单纯为了面试而刷题的意义在下降。但对我来说,教孩子刷题这件事的价值,从来就不只是“做题”本身,而在于它背后的能力培养:比如智力训练、逻辑思维的建立、专注力的提升,以及延迟满足的能力。同时,这也是一种高质量的亲子陪伴方式,在一起解决问题的过程中,关系会变得更紧密。 至于教媳妇,其实意义就完全不一样了。更多是一种尝试去理解彼此思维方式的过程,也是在探索“沟通”和“教学”的边界——你会发现,有些事情不是努力就一定有结果,有些人也不需要被“改变”。与其强行去教,不如尊重差异,找到各自更舒服的相处方式。换句话说,教媳妇最大的收获,反而是让我学会了不再执着于“教会”,而是学会“放手”和“理解”。 RING摄像头有30天(付费)云记录,有时候我会保存一下,以后有空整理一下重温一下,等娃大了,给他们看。 来两张媳妇前几年学编程一脸生无可恋,哈哈。 英文:Programming is not my wife’s… - 数学之美: Sigma 函数的推导公式与 Python 实现
理解 Sigma 函数:因子、乘法性与公式推导 一文看懂 Sigma 函数:因子分解的终极威力! σ(n) 完全解析:为什么求和函数能“自动”变成乘积? 数学之美:Sigma 函数的推导、公式与 Python 实现 从几何级数到质因数:Sigma 函数的魔法公式大揭秘 搞懂 σ(n) 的那一天,我看到了数学的秩序 为什么 σ(n) =…
上一篇: 月光奏鸣曲: 从学校礼堂弹到根特街头
下一篇: 一个人的生命力, 藏在他的欲望里


