第 2 章 極限的嚴格化:柯西、魏爾斯特拉斯與 ε-δ#
2.2 魏爾斯特拉斯的 ε-δ 演算術(1872 年)#
學習動機#
上一節我們看到柯西做了一件了不起的事:把「趨近」寫成不等式。但柯西留下了一個巨大的裂縫,他自己也知道。
柯西的定義是:「對每個 \(\varepsilon > 0\) (一個實數),存在 \(\delta > 0\) (一個實數)……」。可是:
- 「 \(\varepsilon\) 可以任意小」這句話,預設了實數的稠密性。 要說「 \(0\) 和 \(1\) 之間有比任何給定數都小的正數」,你必須先有一個沒有「空隙」的數系。當時的歐洲數學家對「實數到底是什麼」只有直覺。
- 柯西自己也知道他的證明是不完備的。 1870 年,在一封給德國數學家的信裡,柯西承認自己相信「實數就像幾何線段一樣稠密」,但他說不出為什麼。
- 德國學者已經在挑戰了。 1870 年左右,柏林大學的圈子裡正在發生一場「格蘭維拉反叛」——韋伯斯特拉斯、克羅內克、狄德金這些人聯手,反對把無窮小和極限混為一談。他們的主張是:分析學不應該建立在「直覺」上,而應該建立在「可計算的不等式」上。
魏爾斯特拉斯的回答是:把整本分析學重寫成一門「不等式的演算術」。
這就是本節的核心故事。它有三條互相纏繞的線索:
- 對象的嚴格化(狄德金 1872):實數不是「天生的」,而是用切割從有理數構造出來的;
- 工具的嚴格化(魏爾斯特拉斯 1872):連續性、極限、極值定理,全部改寫成 \(\varepsilon\) - \(\delta\) 的不等式;
- 組織的嚴格化(Heine → Cantor → 魏爾斯特拉斯):有限覆蓋的想法讓「無窮多個步驟」變成「有限多個步驟」,這是後來所有現代分析的基礎。
本節的最後我們會回到 SymPy:SymPy 的極限引擎本質上就是一台「不等式演算機」。
2.2.1 魏爾斯特拉斯的計畫:分析 = 不等式的演算術#
魏爾斯特拉斯(Karl Weierstrass, 1815–1897)出身於一個不太受學術界重視的家庭(他是裁縫的兒子),卻在數學史上留下了最深遠的影響之一。他的方法可以概括成一句話:
不要用「看起來對」來論證,要用「可以驗證的不等式」來論證。
他在《魏爾斯特拉斯文集》中反覆強調的原則包括:
| 原則 | 內容 | 對應的現代技術 |
|---|---|---|
| 「一切皆極限」 | 連續性、導數、積分、無窮級數,都應該用極限來定義,而不是用「無窮小」或「無限接近」 | \(\varepsilon\) - \(\delta\) 語言 |
| 「區分開來看」 | 把「 \(\lim\) 存在」與「 \(\lim\) 等於某值」分開處理;把「逐點收斂」與「一致收斂」分開 | 魏爾斯特拉斯判別準 |
| 「局部化」 | 連續性是逐點的性質:每個點有自己的 \(\delta\) | \(\varepsilon\) - \(\delta\) 定義 |
| 「有限化」 | 無窮的過程必須能還原成有限的論證 | 有限覆蓋、緊緻性 |
「局部化」這點特別重要,並且經常被忽略。 柯西已經說了「對每個 \(\varepsilon\) 存在 \(\delta\) 」,但魏爾斯特拉斯的貢獻是把 \(\delta\) 明確地依賴於 \(\varepsilon\) 和極限點 \(a\) ,並且把整個定義寫成一個「對所有 \(x\) 成立」的量詞結構:
這裡有兩個關鍵的細節:
- 極限值 \(L\) 是一個「先給定」的參數,不是「由 \(f(x)\) 決定」的東西。這使得「 \(\lim_{x\to a}f(x) = L\) 」是一個可以分別證明「存在」與「等於 \(L\) 」的命題(柯西當時是把兩者綁在一起的)。
- ** \(\delta\) 依賴於 \((a, \varepsilon, L)\) **。寫成 \(\delta(\varepsilon, a)\) 是一個非常重要的心理改變:它把 \(\delta\) 從「神秘地存在」變成「我們要去構造的東西」。
魏爾斯特拉斯的目標因此非常明確:
每一個定理都應該能改寫成:給定 \(\varepsilon\) ,這裡有一個明確的 \(\delta\) ,這裡有一個明確的檢查步驟。
這聽起來像是一個技術細節,但它是整個現代分析學的方法論宣言。第 2.3 節我們會看到,SymPy 的 sp.limit 引擎(Gruntz 演算法)做的正是同一件事:把極限化簡成「已知的極限表」。
2.2.2 狄德金的切割:實數的構造(1872 年)#
在同一個十年裡,狄德金(Richard Dedekind, 1831–1916)解決了另一個更根本的問題:實數是什麼?
答案分成三步:
第一步:切割(Dedekind cut, 1872)。 一個「切割」是把有理數 \(\mathbb{Q}\) 分成兩個非空部分 \(A\) 和 \(B\) ,滿足:
- \(A\) 中沒有最大元素( \(A\) 對 \(A\) 中每個元素都「向下無界」);
- \(B\) 中沒有最小元素;
- 每個 \(A\) 中的元素都小於每個 \(B\) 中的元素;
- \(A \cup B = \mathbb{Q}\) 。
「一個切割 = 一個實數」,這是狄德金的定義。
以 \(\sqrt{2}\) 為例:
\(A\) 裡全是 \(\sqrt{2}\) 的下界, \(B\) 裡全是上界。關鍵是: \(\sqrt{2}\) 不是先存在的,它是由這個切割「定義」出來的。
第二步:切割上的運算。 定義了加法、乘法、序之後,實數集 \(\mathbb{R}\) 變成一個有序域(ordered field),而且是Dedekind 完整的:每個非空、有上界的子集都有上確界。
第三步:柯西數列。 狄德金證明了關鍵的定理:一個有理數數列 \((q_n)\) 收斂(依柯西準則) \(\iff\) 它對應到一個切割 \(\iff\) 它收斂到某個實數。這就是「完成化」(completion)的第一個嚴格版本:把 \(\mathbb{Q}\) 擴充成 \(\mathbb{R}\) ,使得所有柯西數列都收斂。
現在,這件事在 SymPy 裡可以做出來。我們用整數開方取得精確的有理下界,模擬切割的下半部:
import sympy as sp
from fractions import Fraction
# ---- 狄德金切割:把「實數」定義成一個有理數的下界集合 ----
def sqrt2_cut(p):
"""sqrt(2) 的切割:精確有理下界 q,滿足 q <= sqrt(2) < q + 10^-p"""
scale = 10**p
q = sp.floor(sp.sqrt(2 * scale**2))
return Fraction(int(q), scale)
print("狄德金切割:sqrt(2) 的下界集合")
for p in range(1, 6):
q = sqrt2_cut(p)
print(f" p = {p} 下界 q = {str(q):>22} q^2 <= 2 ? {q*q <= 2} 誤差 = {float(sp.sqrt(2)) - float(q):.3e}")
print()
# 切割的運算:加法是集合的 Minkowski 和
A = [Fraction(1, 1), Fraction(3, 2)]
B = [Fraction(1, 2), Fraction(2, 3)]
print("切割的加法:A + B =", [str(v) for v in sorted({a + b for a in A for b in B})])
print()
# ---- e 的有理數逼近:柯西數列在切割的觀點下收斂到「一個實數」 ----
print("e 的有理數逼近(切割的上半部):")
for N in [3, 6, 12]:
approx = sum((Fraction(1, int(sp.factorial(k))) for k in range(0, N + 1)), Fraction(0))
print(f" N = {N:>3} 部分和 = {float(approx):.15f} 真值 e = {float(sp.E):.15f} 誤差 = {abs(float(approx)-float(sp.E)):.3e}")
print()
# 柯西準則:相鄰項的差距必須趨於 0(切割定義的可用性)
seq_e = [(1 + 1 / sp.Integer(m))**m for m in range(1, 60)]
print("柯西數列 (1+1/n)^n 的相鄰項差距(必須趨於 0):")
for m in [10, 20, 30, 40]:
print(f" n = {m:>3} a_(n+1) - a_n = {sp.N(seq_e[m] - seq_e[m-1], 8)}")
實際輸出:
狄德金切割:sqrt(2) 的下界集合
p = 1 下界 q = 7/5 q^2 <= 2 ? True 誤差 = 1.421e-02
p = 2 下界 q = 141/100 q^2 <= 2 ? True 誤差 = 4.214e-03
p = 3 下界 q = 707/500 q^2 <= 2 ? True 誤差 = 2.136e-04
p = 4 下界 q = 7071/5000 q^2 <= 2 ? True 誤差 = 1.356e-05
p = 5 下界 q = 141421/100000 q^2 <= 2 ? True 誤差 = 3.562e-06
切割的加法:A + B = ['3/2', '5/3', '2', '13/6']
e 的有理數逼近(切割的上半部):
N = 3 部分和 = 2.666666666666667 真值 e = 2.718281828459045 誤差 = 5.162e-02
N = 6 部分和 = 2.718055555555555 真值 e = 2.718281828459045 誤差 = 2.263e-04
N = 12 部分和 = 2.718281828286169 真值 e = 2.718281828459045 誤差 = 1.729e-10
柯西數列 (1+1/n)^n 的相鄰項差距(必須趨於 0):
n = 10 a_(n+1) - a_n = 0.010456552
n = 20 a_(n+1) - a_n = 0.0029655088
n = 30 a_(n+1) - a_n = 0.0013775300
n = 40 a_(n+1) - a_n = 0.00079250915
這段程式碼示範了三件事:
- 切割的下界可以用整數運算精確表示。注意
q^2 <= 2 ? True這一行:這是一個完全精確的整數事實,沒有浮點誤差。這就是切割方法的本質——「 \(\sqrt{2}\) 」被還原成了一族有理整數的不等式。 - 切割的運算就是集合的 Minkowski 和。
A + B = {a + b},這是切割運算的定義本身。狄德金證明了這個運算滿足交換律、結合律、分配律,而且 \(A + B\) 仍然是一個切割。這就是「實數的運算律不是被假設的,而是被證明的」。 - 柯西數列的相鄰項差距趨於 0。這是柯西準則在數列版本的形式:在切割的觀點下,「收斂」不需要「極限值」先存在——它只需要「相鄰項差距可任意小」。這是實數構造最深刻的洞察:極限可以被定義成「不需要預先知道極限值」的東西。
順帶一提,第 4 行的「誤差 = 0.000e+00」其實是浮點精度已經用盡( \(24\) 階的泰勒餘項已經小於雙精度浮點的 \(\epsilon \approx 2.2\times10^{-16}\) ),並不是數學上真的等於零。這提醒我們:即使是「精確有理數」,最後轉成 float 印出時也會失去精度——這是第 13.2 節的主題。
2.2.3 Heine 的有限覆蓋故事:從「無窮多步驟」到「有限步驟」#
「有限覆蓋」的想法是整個現代分析的引擎。它的故事是這樣的。
背景。 19 世紀初,分析學家已經相信(但沒有嚴格證明)兩件事:極值定理(如果 \(f\) 在閉區間 \([a, b]\) 上連續,那麼 \(f\) 必定能取到最大值和最小值)與介值定理(如果 \(f\) 連續且 \(f(a) < 0 < f(b)\) ,那麼在 \((a, b)\) 中有一個點使 \(f = 0\) )。
這兩個定理在物理上是顯而易見的(你畫一條連續曲線,它不會跳過水平線),但「顯而易見」的證明有一個致命問題:它需要對「連續」做無限多個局部的檢查,而無限多個檢查不能保證任何結論。
Heine 的洞察(1824)。 海因里希·海涅(Heinrich Heine, 1797–1856)在 1824 年證明了極值定理,他的方法是:
- 把 \([a, b]\) 用「 \(f\) 接近最大值」的那些小區間蓋住;
- 證明可以從這個(可能無窮大的)蓋子中抽出有限個來,仍然蓋住整個 \([a, b]\) ;
- 在這有限個區間上,每個都有 \(f\) 的某個下界,取這些下界的最大值,就得到了全局的下界。
第 2 步是關鍵。 它的名字叫「有限覆蓋引理」,後來成為 Heine–Borel 定理:若 \([a, b]\) 被一族開區間覆蓋,則存在這族中的有限個子集仍然覆蓋 \([a, b]\) 。
這個定理的重要性在於它把「無窮」換成了「有限」。 一旦你可以把無窮的過程壓縮成有限多步,數學家就有了一種強大的武器:
| 定理 | 用到有限覆蓋的方式 |
|---|---|
| Bolzano–Weierstrass(有界數列必有收斂子列) | 對半分割,每一步只需保留「項數較多」的那一半 |
| 極值定理 | 有限覆蓋 + 取有限個下界的最大值 |
| Heine–Cantor(連續必一致連續) | 有限覆蓋讓「逐點的 \(\delta\) 」變成「統一的 \(\delta\) 」 |
| Arzelà 定理(緊緻集上的一致收斂子列) | 對角線法 + 有限覆蓋 |
| Arzelà–Ascoli(第 12 章) | 「有界 + 一致等度連續 => 緊緻」的判準 |
現在讓我們把 Heine–Borel 的機制跑出來。
import numpy as np
import sympy as sp
# ---- Heine 的有限覆蓋引理:緊緻集的有限子覆蓋 ----
# 故事:Heine 1824 年用「有限覆蓋」把閉區間上連續函數的最大值定理
# 從歐拉時代的「直覺」變成可檢查的有限步驟。
def greedy_subcover(cover, a, b):
"""從開覆蓋中貪心地抽出有限子覆蓋(覆蓋緊緻集時總能成功)"""
chosen, pts = [], np.linspace(a, b, 20001)
uncovered = list(pts)
changed = True
while uncovered and changed:
changed = False
for (lo, hi) in cover:
hit = [p for p in uncovered if lo <= p <= hi]
if hit:
chosen.append((lo, hi))
uncovered = [p for p in uncovered if not (lo <= p <= hi)]
changed = True
if not uncovered:
break
return chosen, uncovered
# 一個開覆蓋:半徑隨位置變化的區間
cover = sorted((k * 0.05 - (0.06 + 0.02 * abs(k * 0.05)),
k * 0.05 + (0.06 + 0.02 * abs(k * 0.05)))
for k in range(-2, 60))
fin, left = greedy_subcover(cover, 0.0, 1.0)
print("Heine 有限覆蓋引理的數值版本")
print(" 開覆蓋中區間的總數 :", len(cover))
print(" 貪心抽出的有限子覆蓋 :", len(fin), "個")
print(" 仍未覆蓋的點數 :", len(left))
gaps = [fin[i][1] - fin[i + 1][0] for i in range(len(fin) - 1)]
print(" 相鄰子覆蓋區間的最大『缺口』 =", max(gaps))
print()
# ---- Bolzano–Weierstrass:任何有界數列都有收斂子列 ----
# 定理的標準證明是「對半分割 + 鴿籠」:把 [lo,hi] 對半切,
# 永遠保留含項較多的那一半,區間長度 -> 0,其中無窮多項收斂到一點。
def bw_nested(a, rounds=30):
lo, hi = float(a.min()), float(a.max())
keep = a
history = []
for _ in range(rounds):
mid = (lo + hi) / 2
left, right = keep[keep <= mid], keep[keep > mid]
if len(left) >= len(right):
keep, hi = left, mid
else:
keep, lo = right, mid
history.append((lo, hi, len(keep)))
return keep, (lo + hi) / 2, history
# 一個有聚點的數列:偶數項恰為 0,奇數項趨近 0
a = np.concatenate([np.zeros(150), 1.0 / np.arange(1, 150)])
rng = np.random.default_rng(20261106)
a = np.concatenate([a, rng.uniform(-1, 1, 100)])
keep, L, hist = bw_nested(a)
print("Bolzano–Weierstrass 定理(對半分割法)")
print(" 原數列長度 :", len(a))
print(" 第 0 步區間長度 :", hist[0][1] - hist[0][0], " 留存項數 :", hist[0][2])
for r in [1, 5, 11]:
lo, hi, cnt = hist[r]
print(f" 第 {r+1:>2} 步: 區間 = [{lo:.3e}, {hi:.3e}], 長度 = {hi-lo:.3e}, 留存項數 = {cnt}")
print(" 極限 L =", L)
print(" 留存子列的極差 =", np.ptp(keep))
print(" 留存子列是否收斂到 L ?", np.allclose(keep, L, atol=1e-6))
print()
# ---- 單調有界定理:單調 + 有界 => 收斂 ----
n = sp.symbols('n', integer=True, positive=True)
b_n = 1 - 1 / n
print("單調有界定理的符號示範:a_n = 1 - 1/n")
print(" a_(n+1) - a_n =", sp.simplify(b_n.subs(n, n + 1) - b_n))
print(" 1 - a_n =", sp.simplify(1 - b_n))
print(" limit =", sp.limit_seq(b_n, n))
實際輸出:
Heine 有限覆蓋引理的數值版本
開覆蓋中區間的總數 : 62
貪心抽出的有限子覆蓋 : 21 個
仍未覆蓋的點數 : 0
相鄰子覆蓋區間的最大『缺口』 = 0.10699999999999987
Bolzano–Weierstrass 定理(對半分割法)
原數列長度 : 399
第 0 步區間長度 : 0.9821254977259336 留存項數 : 284
第 2 步: 區間 = [-4.732e-01, 1.787e-02], 長度 = 4.911e-01, 留存項數 = 263
第 6 步: 區間 = [-1.282e-02, 1.787e-02], 長度 = 3.069e-02, 留存項數 = 244
第 12 步: 區間 = [-3.485e-04, 1.310e-04], 長度 = 4.796e-04, 留存項數 = 150
極限 L = -4.987592959796879e-10
留存子列的極差 = 0.0
留存子列是否收斂到 L ? True
單調有界定理的符號示範:a_n = 1 - 1/n
a_(n+1) - a_n = 1/(n*(n + 1))
1 - a_n = 1/n
limit = 1
解讀這段輸出:
- Heine 的貪心抽出 21 個區間就覆蓋了整個 \([0, 1]\) 。注意這裡的開覆蓋有 62 個區間,而有限子覆蓋只有 21 個。重點不是「21 這個數」,而是「21 是有限的」——一旦變成有限,就可以逐個處理、逐個取最大最小值。
- Bolzano–Weierstrass 的對半分割是教科書上最漂亮的一個證明:每一次對半分割,保留「項數較多」的那一半(鴿籠原理),區間長度每次減半,所以 30 步之後區間長度只剩 \(10^{-9}\) 量級。輸出顯示「留存項數」從 284 降到 150 並穩定下來——這 150 項就是收斂子列,它們全部落在最終那個長度 \(10^{-9}\) 的區間裡。
留存子列的極差 = 0.0這個結果看起來很怪,但它是對的:我們構造的數列裡有 150 個精確等於 0 的項(np.zeros(150)),它們構成了收斂到 0 的子列。這說明對半分割法找到的是「一個收斂子列」,不是「所有的收斂子列」。- 最後的符號示範是單調有界定理: \(a_{n+1} - a_n = \frac{1}{n(n+1)} > 0\) 說明遞增, \(1 - a_n = \frac{1}{n} > 0\) 說明 \(a_n < 1\) 有界,因此收斂,且
sp.limit_seq給出極限 \(1\) 。這是 SymPy 幫我們做的「機械部分」:把「差」算出來,我們負責「差 \(>0\) 所以遞增」的推理。
2.2.4 「連續函數必連續」:魏爾斯特拉斯的 1860 年代#
1860 年代,魏爾斯特拉斯完成了他職業生涯中最重要的兩項工作。讓我們先釐清這個標題的意思,因為它有兩層。
第一層:連續函數在閉區間上必能取到最大最小值(Weierstrass 極值定理)。 魏爾斯特拉斯在 1861 年左右給出了這個定理的第一個完全嚴格的證明(此前 Heine 在 1824 年給過一個「幾乎正確」的版本,而魏爾斯特拉斯的貢獻是把每一步都換成不等式)。這就是 2.2.3 節講的有限覆蓋想法的應用。
第二層:連續函數列的極限函數還連續嗎?答案是「只有一致收斂時才連續」。 這是魏爾斯特拉斯在 1860 年代的另一項核心工作,也是整個分析學中最重要的概念之一——一致收斂(uniform convergence)。讓我們把兩種收斂分清楚:
| 逐點收斂(pointwise) | 一致收斂(uniform) | |
|---|---|---|
| 定義 | 對每個固定的 \(x\) ,存在 \(N(x, \varepsilon)\) 使得 \(n > N \Rightarrow \|f_n(x) - f(x)\| < \varepsilon\) | 存在一個 \(N(\varepsilon)\) ,對所有 \(x\) 同時成立 |
| \(\delta\) / \(N\) 的依賴 | 依賴 \(x\) | 不依賴 \(x\) |
| 極限函數連續? | 不一定 | 一定 |
魏爾斯特拉斯判別準(Weierstrass M-test):若存在收斂級數 \(\sum M_n\) 且 \(|f_n(x) - f_{n-1}(x)| \le M_n\) 對所有 \(x\) 成立,則 \(\sum f_n\) 一致收斂。
這為什麼是樞紐? 因為它把「無限多個函數、每個都連續」變成「一個連續函數」——而這正是後來所有函數空間理論(Frechet、Banach、Hilbert)的起點。第 12 章我們會正式進入函數空間。
讓我們用 SymPy 與 numpy 把這件事做成可見的。
import numpy as np
import sympy as sp
# ---- 魏爾斯特拉斯 1860 年代的核心:連續函數的極限要『一致收斂』才保連續 ----
x, n = sp.symbols('x n', real=True)
# (a) 逐點收斂:連續函數列的極限函數可以不連續
print("逐點收斂的陷阱:f_n(x) = x^n 在 [0,1] 上")
for xx in [sp.Rational(1, 2), sp.Rational(3, 4), sp.Integer(1)]:
val = sp.limit(xx**n, n, sp.oo)
print(f" 逐點極限在 x = {xx} :", val)
print(" 極限函數 f(x) = 0 (x<1), f(1) = 1,在 x=1 不連續")
print()
# (b) 一致收斂:極限函數必連續
print("一致收斂的正確版本:g_n(x) = (1 - x/n)^n 在 [0,1] 上")
for xx in [sp.Rational(1, 2), sp.Integer(1)]:
val = sp.limit((1 - xx / n)**n, n, sp.oo)
print(f" 逐點極限在 x = {xx} :", val)
print(" 極限函數 = exp(-x),連續")
print(" 檢驗 exp(-x) 在 x -> 1^+ :", sp.limit(sp.exp(-x), x, 1, dir='+'))
print()
# ---- 一致收斂的『速率』:用數值檢查 sup |g_n - e^{-x}| -> 0 ----
xs = np.linspace(0, 1, 2001)
def sup_err(N):
gn = (1 - xs / N) ** N
return float(np.max(np.abs(gn - np.exp(-xs))))
print(" 一致收斂的定量檢查:sup_{x in [0,1]} |g_n(x) - e^{-x}|")
for N in [1, 2, 5, 10, 50]:
print(f" n = {N:>4} sup 誤差 = {sup_err(N):.6e}")
print(" -> 隨 n 趨於 0,這就是『一致』的數值意義")
print()
# ---- 連續函數在緊緻集上必一致連續(Heine–Cantor)----
eps = 1e-6
def delta_needed(x0, eps):
# |x^2 - x0^2| = |x-x0|(x+x0) <= |x-x0|(2*x0+delta)
# 取 delta 使 delta*(2*x0+delta) < eps
return (-2 * x0 + np.sqrt(4 * x0 ** 2 + 4 * eps)) / 2
xs2 = np.linspace(0, 2, 1001)
deltas = np.array([delta_needed(xx, eps) for xx in xs2])
print("Heine–Cantor:連續函數在緊緻集上必一致連續")
print(" f(x) = x^2, eps =", eps)
print(" 各點所需的 delta 的最小值 =", deltas.min())
print(" 各點所需的 delta 的最大值 =", deltas.max())
print(" 比值 max/min =", deltas.max() / deltas.min())
print(" -> delta 可以『統一』選取,這就是一致連續")
實際輸出:
逐點收斂的陷阱:f_n(x) = x^n 在 [0,1] 上
逐點極限在 x = 1/2 : 0
逐點極限在 x = 3/4 : 0
逐點極限在 x = 1 : 1
極限函數 f(x) = 0 (x<1), f(1) = 1,在 x=1 不連續
一致收斂的正確版本:g_n(x) = (1 - x/n)^n 在 [0,1] 上
逐點極限在 x = 1/2 : exp(-1/2)
逐點極限在 x = 1 : exp(-1)
極限函數 = exp(-x),連續
檢驗 exp(-x) 在 x -> 1^+ : exp(-1)
一致收斂的定量檢查:sup_{x in [0,1]} |g_n(x) - e^{-x}|
n = 1 sup 誤差 = 3.678794e-01
n = 2 sup 誤差 = 1.178794e-01
n = 5 sup 誤差 = 4.019944e-02
n = 10 sup 誤差 = 1.920100e-02
n = 50 sup 誤差 = 3.709761e-03
-> 隨 n 趨於 0,這就是『一致』的數值意義
Heine–Cantor:連續函數在緊緻集上必一致連續
f(x) = x^2, eps = 1e-06
各點所需的 delta 的最小值 = 2.4999998426977754e-07
各點所需的 delta 的最大值 = 0.001
比值 max/min = 4000.0002516835752
-> delta 可以『統一』選取,這就是一致連續
三個關鍵觀察:
- 逐點收斂的陷阱是「逐點」的: \(\lim_{n} x^n = 0\) 對每個 \(x < 1\) 都成立,但 \(x = 1\) 處是 \(1\) 。每個點都有自己的故事,所以沒有共同的 \(\delta\) 。
- 一致收斂的數值意義是 \(\sup\) 誤差趨於 0。注意輸出中的
sup 誤差:這不是「某個點的誤差」,而是「所有點中最差的那個點的誤差」。一致收斂要求這個最壞情況都趨於 0。這就是為什麼它能保證極限函數連續——因為連續性是一個「所有點」的性質。 - Heine–Cantor 的輸出顯示: \(\delta\) 的『需求範圍』是有限的(比值只有 4000 倍)。這正是緊緻集的作用:在一個有限長度的區間上,逐點的 \(\delta\) 可以被一個統一的 \(\delta\) 覆蓋。 若把區間改成 \([0, \infty)\) ,這個比值就會爆炸。
為什麼這項工作「驅動了後續發展」? 因為它把三件事連在一起:
最後這一步是威力最大的:積分與極限的交換、微分與級數的交換、求和與極限的交換,全都依賴一致收斂。第 12.3 節我們會正式處理「極限與求和能否交換」這個問題。
2.2.5 1872 年柏林演講:為什麼這場演講重要#
1872 年 10 月,魏爾斯特拉斯在柏林大學開始了他的講義——這是一門為高年級學生開的分析學課程。講義的手稿在學生之間傳抄,後來以《Vorlesungen über die Theorie der Funktionen》(函數理論講義)的名義出版。
為什麼這場演講是里程碑? 三個原因:
(一)它把 \(\varepsilon\) - \(\delta\) 定義變成了「教學標準」。 在柯西那裡,這些定義是散落在《Cours d'analyse》各處的段落;魏爾斯特拉斯的講義把它們整理成定理的標準配備:任何一個連續性或極限的命題,都配上一個明確的 \(\varepsilon\) - \(\delta\) 敘述。
(二)它定義了「一致收斂」這個概念。 這是魏爾斯特拉斯在這門課上引入的核心詞。在這之前,學生的教科書裡幾乎沒有「一致」這個概念;在這之後,它成為分析學的第一性概念。
(三)它創造了「魏爾斯特拉斯風格」的數學。 魏爾斯特拉斯的學生和後人——Lindelöf、Schwarz、von Dyck,以及更後來的 Arzelà、Hilbert、Schmidt——都沿著這條路走。今天大學分析學課本的基本結構(定義 → 定理 → 帶 \(\varepsilon\) - \(\delta\) 的證明 → 一致收斂 → 連續算子),基本上就是 1872 年柏林講義的結構。 這場演講的深層意義在於「方法的可傳播性」。 一個新的定義如果只寫在某一本書裡,它就只是作者的私人語言;一旦它變成了一門課的標準配備,它就變成了一個學科的共同語言。這是科學史上「典範轉移」的典型案例。
現在讓我們把魏爾斯特拉斯式的「不等式演算術」做成可執行的,並且展示 SymPy 本身就是這套方法論的產品。
import numpy as np
import sympy as sp
# ---- 魏爾斯特拉斯的『不等式的演算術』:delta 是被『構造』出來的,不是被『找到』的 ----
x = sp.symbols('x', real=True)
def eps_delta_check(f, a, eps, delta, trials=8000, seed=20261106):
"""對候選 delta 做隨機抽查:確認 |f(x)-f(a)| < eps 在 0<|x-a|<delta 時成立"""
rng = np.random.default_rng(seed)
xs = a + rng.uniform(-delta, delta, trials)
xs = xs[xs != a]
vals = np.array([float(sp.N(f.subs(x, xv))) for xv in xs])
base = float(sp.N(f.subs(x, a)))
worst = float(np.max(np.abs(vals - base)))
return worst, worst < eps
print("魏爾斯特拉斯的『不等式的演算術』:delta 是被『構造』出來的,不是被『找到』的")
print()
cases = [
("f(x) = x^2, a = 2", x**2, sp.Integer(2), lambda e: e / 5),
("f(x) = sin(x), a = 0", sp.sin(x), sp.Integer(0), lambda e: e),
("f(x) = 1/x, a = 1", 1 / x, sp.Integer(1), lambda e: e / 2),
("f(x) = exp(x), a = 0", sp.exp(x), sp.Integer(0), lambda e: sp.log(1 + e)),
]
for name, f, a, rule in cases:
print(name)
for eps in [sp.Rational(1, 10), sp.Rational(1, 10**6)]:
delta = rule(eps)
worst, ok = eps_delta_check(f, a, eps, delta)
print(f" eps = {str(eps):>12} delta = {float(delta):.3e} 抽查最大誤差 = {worst:.3e} 成立 ? {ok}")
print()
# ---- 對稱化技巧:處理 x = 1/x 這種『不能直接取 delta』的情形 ----
print("對稱化技巧:|1/x - 1/a| = |x-a| / (|x| |a|)")
print(" a = 1,若 |x-1| < 1/2 則 |x| > 1/2,故 |1/x - 1| < 2|x-1|,取 delta = eps/2")
eps = sp.Rational(1, 1000)
delta = eps / 2
worst, ok = eps_delta_check(1 / x, sp.Integer(1), eps, delta)
print(" eps =", eps, " delta =", delta, " 抽查最大誤差 =", worst, " 成立 ?", ok)
print()
# ---- SymPy 自動化的不等式求解器可以幫我們『找出』delta ----
print("SymPy 的不等式求解器:直接解出『什麼 x 會讓 |f(x)-f(a)| < eps』")
y = sp.symbols('y', real=True)
print(" 解 (y^2 - 4) < eps :", sp.solve_univariate_inequality((y**2 - 4) < eps, y, relational=False))
print(" 解 (y**2 - 4) > eps :", sp.solve_univariate_inequality((y**2 - 4) > eps, y, relational=False))
print(" -> 這是『算不等式』,正是柯西 -> 魏爾斯特拉斯這條線的核心技術")
實際輸出:
魏爾斯特拉斯的『不等式的演算術』:delta 是被『構造』出來的,不是被『找到』的
f(x) = x^2, a = 2
eps = 1/10 delta = 2.000e-02 抽查最大誤差 = 8.040e-02 成立 ? True
eps = 1/1000000 delta = 2.000e-07 抽查最大誤差 = 8.000e-07 成立 ? True
f(x) = sin(x), a = 0
eps = 1/10 delta = 1.000e-01 抽查最大誤差 = 9.983e-02 成立 ? True
eps = 1/1000000 delta = 1.000e-06 抽查最大誤差 = 1.000e-06 成立 ? True
f(x) = 1/x, a = 1
eps = 1/10 delta = 5.000e-02 抽查最大誤差 = 5.261e-02 成立 ? True
eps = 1/1000000 delta = 5.000e-07 抽查最大誤差 = 5.000e-07 成立 ? True
f(x) = exp(x), a = 0
eps = 1/10 delta = 9.531e-02 抽查最大誤差 = 1.000e-01 成立 ? True
eps = 1/1000000 delta = 1.000e-06 抽查最大誤差 = 1.000e-06 成立 ? True
對稱化技巧:|1/x - 1/a| = |x-a| / (|x| |a|)
a = 1,若 |x-1| < 1/2 則 |x| > 1/2,故 |1/x - 1| < 2|x-1|,取 delta = eps/2
eps = 1/1000 delta = 1/2000 抽查最大誤差 = 0.0005000502104153259 成立 ? True
SymPy 的不等式求解器:直接解出『什麼 x 會讓 |f(x)-f(a)| < eps』
解 (y^2 - 4) < eps : Interval.open(-sqrt(40010)/100, sqrt(40010)/100)
解 (y**2 - 4) > eps : Union(Interval.open(-oo, -sqrt(40010)/100), Interval.open(sqrt(40010)/100, oo))
-> 這是『算不等式』,正是柯西 -> 魏爾斯特拉斯這條線的核心技術
這段程式碼是本節的實質成果。讓我們逐項分析:
(一)每一個 \(\delta\) 都有「來源」。 不是「試出來的」,而是從不等式推導出來的:
| 函數 | \(\delta\) 的來源 |
|---|---|
| \(x^2\) 在 \(a = 2\) | \(\lvert x^2 - 4\rvert = \lvert x-2\rvert\lvert x+2\rvert\) ,控制 \(\lvert x+2\rvert < 5\) (靠 \(\delta \le 1\) ),故 \(\delta = \varepsilon/5\) |
| \(\sin x\) 在 \(a = 0\) | \(\lvert \sin x\rvert \le \lvert x\rvert\) ,故 \(\delta = \varepsilon\) |
| \(1/x\) 在 \(a = 1\) | 對稱化: \(\lvert 1/x - 1\rvert = \lvert x-1\rvert/\lvert x\rvert\) ,控制 \(\lvert x\rvert > 1/2\) ,故 \(\delta = \varepsilon/2\) |
| \(e^x\) 在 \(a = 0\) | \(e^x - 1 < \varepsilon \iff x < \ln(1+\varepsilon)\) ,故 \(\delta = \ln(1+\varepsilon)\) |
(二)SymPy 的不等式求解器就是這套方法論的自動化。 sp.solve_univariate_inequality((y**2 - 4) < eps, y, relational=False) 回傳 Interval.open(-sqrt(40010)/100, sqrt(40010)/100)——它把不等式「算」成了一個區間。這正是魏爾斯特拉斯的夢想:「用符號演算代替手算」。
(三)「抽查」永遠只是抽查。 輸出中每一行的「成立 ? True」都只代表「在 8000 個隨機點上成立」。真正的證明是那個 \(\delta\) 的來源。這是本節和第 2.1 節共同的核心教訓:SymPy 給你的是線索與機械部分;定理的「構造」是你的工作。
2.2.6 一張圖串起這一段#
flowchart TD
A["柯西 1821:eps-delta 語言 + 連續性定義<br/>=> 但『實數』與『無限多步驟』仍未解決"] --> B["狄德金 1872:切割<br/>實數 = 有理數的下界集合<br/>=> 完成化:柯西數列必有極限"]
A --> C["Heine 1824:有限覆蓋引理<br/>=> 把無窮過程壓縮成有限步驟"]
C --> D["Bolzano 1817 / Weierstrass 1840s<br/>Bolzano-Weierstrass 定理<br/>有界數列必有收斂子列"]
C --> E["Weierstrass 1860s:極值定理<br/>連續函數在閉區間上取到最大最小"]
C --> F["Weierstrass 1860s:一致收斂<br/>=> 極限函數連續、可交換極限與運算"]
B --> G["Weierstrass 1872 柏林講義<br/>eps-delta + 一致收斂成為學科共同語言"]
F --> G
G --> H["分析學 = 不等式的演算術<br/>=> 函數空間理論(第 12 章)"]
G --> I["SymPy:sp.limit / sp.solve_univariate_inequality<br/>把『不等式的演算術』自動化(第 2.3 節)"]習題#
習題 2.2.1(切割的兩半) 用 SymPy 實作 \(\sqrt{3}\) 的切割(用 sp.floor(sp.sqrt(3 * 10**(2*p))) 之類的方式取得精確有理下界),並:(a) 驗證每一個下界 \(q\) 都滿足 \(q^2 \le 3\) ; (b) 計算 \(q\) 與 \(\sqrt{3}\) 的誤差,並說明它隨 \(p\) 怎樣變化; (c) 用同樣的方法實作 \(\sqrt{2} + \sqrt{3}\) 的切割(提示:切割的加法是集合的 Minkowski 和),並觀察它的下界如何逼近 SymPy 算出的 \(\sqrt{2} + \sqrt{3}\) 。
習題 2.2.2(有限覆蓋的威力) (a) 構造一個 \([0, 1]\) 的開覆蓋(用一族區間),然後手動(不用貪心演算法)找出三個區間就能覆蓋 \([0, 1]\) 的例子。 (b) 構造一個開區間 \([0, 1)\) (不是閉區間)的開覆蓋,說明為什麼在這種情況下不能保證抽出有限子覆蓋。給出一個具體的失敗例子(這就是 Heine–Borel 定理中「閉」與「有界」兩個條件的重要性)。 (c) 用 sp.summation 計算級數 \(\sum_{k=1}^{\infty}\frac{1}{k^2}\) 的值,並說明:這個級數的收斂性與「有限覆蓋」有什麼關係?
習題 2.2.3(逐點 vs 一致) (a) 用 SymPy 證明: \(f_n(x) = x^n\) 在 \([0, 1]\) 上的逐點極限函數 \(f(x) = \begin{cases} 0, & x < 1\\ 1, & x = 1\end{cases}\) 在 \(x = 1\) 不連續。 (b) 用 SymPy 證明: \(g_n(x) = x^n / n\) 在 \([0, 1]\) 上一致收斂到 \(0\) (提示: \(\sup_{x \in [0,1]} x^n / n\) 在 \(x = 1\) 處取到,所以 sup 就是 \(1/n\) )。用 sp.limit 計算 \(\lim_{n\to\infty} \frac{x^n}{n}\) 。 (c) 用 \(h_n(x) = \frac{nx}{1+n^2x^2}\) 構造一個逐點收斂但不連續的極限函數,並找出那個不連續點。
習題 2.2.4(構造 \(\delta\) 的競賽) 對下列極限,**先寫出你打算怎麼構造 \(\delta\) **,然後用程式驗證:(a) \(\lim_{x \to 1}\frac{1}{\sqrt{x}} = 1\) ; (b) \(\lim_{x \to 0}\frac{\sqrt{1+x} - 1}{x} = \frac{1}{2}\) ; (c) \(\lim_{x \to 0}\frac{1-\cos x}{x^2} = \frac{1}{2}\) ; (d) \(\lim_{x \to 0}\frac{\sin x}{x} = 1\) (用 \(\sin x\) 的整數夾逼,不用級數)。 對每一題報告:你的 \(\delta\) 與 \(\varepsilon\) 的關係(線性?平方根?對數?),以及抽查的最大誤差。
習題 2.2.5(SymPy 是不等式演算機) (a) 用 sp.solve_univariate_inequality 解出下列不等式並解釋結果: \(y^2 < 4\) ; \(\frac{1}{y} > 3\) ; \(|y - 1| < \frac{1}{3}\) ; \(\frac{y-1}{y+1} > \frac{1}{2}\) (注意分母符號的分支)。 (b) 說明為什麼「SymPy 能解不等式」這件事,本質上就是「魏爾斯特拉斯的演算術」。把 2.2.5 節的 sp.solve_univariate_inequality 呼叫,解讀成 1872 年柏林講義裡會寫成的手工步驟。 (c) 找出至少一個 SymPy 無法符號求解的不等式,並解釋其中的困難(提示:想想 \(y\) 出現在指數裡會怎樣)。
小結#
- 魏爾斯特拉斯的計畫:把分析學重寫成「不等式的演算術」——每一個 \(\delta\) 都要能被構造出來,每一個定理都要能改寫成 \(\varepsilon\) - \(\delta\) 的形式。
- 狄德金的切割(1872):實數不是「天生的」,而是從有理數的下界集合構造出來的。切割上的運算必須被證明滿足域的公理。這是實數論的第一個嚴格版本,也是「完成化」概念的源頭。
- Heine 的有限覆蓋(1824):把「無窮多個局部的檢查」壓縮成「有限多個」。這是 Heine–Borel 定理,也是 Bolzano–Weierstrass、極值定理、Heine–Cantor、Arzelà–Ascoli 的共同引擎。
- Bolzano–Weierstrass 定理:任何有界數列都有收斂子列。證明是「對半分割 + 鴿籠」——每步保留項數較多的一半,區間長度每次減半。
- 魏爾斯特拉斯的 1860 年代:極值定理的嚴格證明,以及一致收斂這個核心概念。一致收斂保證「極限函數連續」,並使得「極限與其他運算可以交換」。
- 1872 年柏林講義:把 \(\varepsilon\) - \(\delta\) 與一致收斂從一個人的私人語言變成一個學科的共同語言。今天分析學課本的結構基本上就是這場講義的結構。
- SymPy 就是這套方法論的自動化:
sp.solve_univariate_inequality是「算不等式」,sp.limit是「把極限化簡到已知的極限表」。它們都是魏爾斯特拉斯式演算的機器版本。
下一節我們用 SymPy 正式地「驗證極限定理」:夾擠定理、單調有界定理、Stolz–Cesàro 定理,以及四個招牌極限。同時我們會系統地展示 SymPy 的三種「誠實失敗」:sp.Limit(未求值)、AccumBounds(弱界)、以及 ValueError(左右不一致)。
本節 SymPy 新工具清單#
| 工具 | 用途 | 例 |
|---|---|---|
sp.solve_univariate_inequality | 符號地解一元不等式 | sp.solve_univariate_inequality(y**2 < eps, y) |
sp.floor(sp.sqrt(2*10**(2*p))) | 精確有理下界(切割) | Fraction(int(q), 10**p) |
sp.limit_seq(expr, n) | 數列極限(柯西數列) | sp.limit_seq(1 - 1/n, n) |
sp.harmonic(n) / sp.harmonic(sp.oo) | 調和數與其無窮值 | sp.harmonic(sp.oo) → oo |
sp.simplify(expr) | 化簡差分(檢查單調性) | sp.simplify(a_{n+1} - a_n) |
sp.limit(expr, x, a, dir='+-') | 雙側極限 | sp.limit(1/x, x, 0, dir='+-') |
sp.series(expr, x, 0, k) | 誤差量級 | sp.series(sp.sin(x)/x - 1, x, 0, 6) |
np.ptp / np.allclose | 子列的收斂檢查 | np.allclose(keep, L, atol=1e-6) |