数学分析基础可视化

有限覆盖定理的证明

围绕有限覆盖定理的证明,观察有限覆盖定理、紧性、闭区间套定理之间的关系与推理路径。

打开原视频

finite_covering_theorem.py
1# -*- coding: utf-8 -*-2from manim import *3import numpy as np4 5class FiniteCoveringProof(Scene):6    def construct(self):7        # 创建坐标系8        axes = Axes(9            x_range=[-1, 6, 1],10            y_range=[-0.5, 3.5, 0.5],11            x_length=10,12            y_length=6,13            axis_config={"include_tip": True}14        )15        16        self.play(Create(axes), run_time=1.5)17        18        # 创建闭区间 [0,5]19        interval = Line(20            axes.c2p(0, 0),21            axes.c2p(5, 0),22            color=WHITE,23            stroke_width=424        )25        endpoints = VGroup(26            Dot(axes.c2p(0, 0), radius=0.07),27            Dot(axes.c2p(5, 0), radius=0.07)28        )29        interval_labels = VGroup(30            MathTex("a").next_to(axes.c2p(0, 0), DOWN),31            MathTex("b").next_to(axes.c2p(5, 0), DOWN)32        )33        34        self.play(35            Create(interval),36            Create(endpoints),37            Write(interval_labels),38            run_time=1.539        )40 41        # 标题和反证法说明42        title = Text("有限覆盖定理的证明", color=BLUE).scale(0.8)43        title.to_corner(UP)44        self.play(Write(title), run_time=1.5)45        46        # 添加反证法假设47        assumption = Text("反证法:假设不存在有限子覆盖", color=RED).scale(0.6)48        assumption.next_to(title, DOWN)49        self.play(Write(assumption), run_time=1.5)50        51        # 展示开覆盖 - 修改为包含更多开集并明确是无限覆盖52        centers = [0.4, 1.2, 2.0, 2.8, 3.6, 4.4, 5.2]  # 增加更多中心点53        radii = [0.8, 0.7, 0.8, 0.7, 0.8, 0.7, 0.8]    # 对应的半径54        colors = [BLUE, RED, GREEN, YELLOW, BLUE_B, RED_B, GREEN_B]  # 更多颜色55        y_positions = [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0]56        57        open_sets = VGroup()58        59        for i, (center, radius, color, y_pos) in enumerate(zip(centers, radii, colors, y_positions)):60            arc = Arc(61                radius=radius,62                angle=PI,63                color=color,64                stroke_width=3,65                fill_opacity=0.2,66                fill_color=color67            ).shift(axes.c2p(center, y_pos))68            69            # 只给前5个开集添加标签70            if i < 5:71                label = MathTex(f"U_{{{i+1}}}").set_color(color)72                label.next_to(arc, UP, buff=0.2)73                open_sets.add(VGroup(arc, label))74                75                self.play(76                    Create(arc),77                    Write(label),78                    run_time=0.8  # 放慢速度79                )80            else:81                # 最后几个开集快速创建,不添加标签82                open_sets.add(arc)83                self.play(Create(arc), run_time=0.6)  # 放慢速度84        85        # 添加省略号表示无限继续86        dots = MathTex(r"\ldots").scale(1.2).set_color(WHITE)87        dots.next_to(axes.c2p(5.5, 1.0), RIGHT, buff=0.1)88        self.play(Write(dots), run_time=1.0)89        90        # 添加无限开集的标签91        infinity_label = MathTex(r"U_n, n \in \mathbb{N}").set_color(WHITE)92        infinity_label.next_to(dots, UP, buff=0.2)93        self.play(Write(infinity_label), run_time=1.2)94 95        self.wait(1.5)96        97        # 淡化开覆盖,进入二分过程98        self.play(99            open_sets.animate.set_opacity(0.3),100            dots.animate.set_opacity(0.3),101            infinity_label.animate.set_opacity(0.3),102            run_time=1.5103        )104        105        # 初始划分106        mid_point = 2.5107        mid_dot = Dot(axes.c2p(mid_point, 0.5), color=YELLOW)108        left_interval = Line(109            axes.c2p(0, 0.5),110            axes.c2p(mid_point, 0.5),111            color=YELLOW112        )113        right_interval = Line(114            axes.c2p(mid_point, 0.5),115            axes.c2p(5, 0.5),116            color=YELLOW117        )118        119        # 添加区间端点标签120        a0_label = MathTex("a_0").scale(0.6).next_to(axes.c2p(0, 0.5), DOWN, buff=0.15)121        b0_label = MathTex("b_0").scale(0.6).next_to(axes.c2p(5, 0.5), DOWN, buff=0.15)122        123        # 添加简短说明124        split_text = Text("初始划分", color=YELLOW).scale(0.5)125        split_text.next_to(left_interval, UP, buff=0.2)126        127        self.play(128            Create(mid_dot),129            Create(left_interval),130            Create(right_interval),131            Write(split_text),132            Write(a0_label),133            Write(b0_label),134            run_time=1.8135        )136        137        # 选择左区间138        left_brace = Brace(left_interval, DOWN)139        left_text = Text("不能有限覆盖(下同)", color=YELLOW).scale(0.4)140        left_text.next_to(left_brace, DOWN, buff=0.1)141        142        self.play(143            Create(left_brace),144            Write(left_text),145            right_interval.animate.set_opacity(0.3),146            run_time=1.5147        )148        149        # 递归过程150        recursive_intervals = []151        current_left, current_right = 0, mid_point152        bisection_colors = [YELLOW_B, YELLOW_C, YELLOW_D]153        interval_endpoints = []  # 保存各区间端点154        endpoint_labels = VGroup()  # 端点标签组155        156        for i, color in enumerate(bisection_colors):157            y_level = 1.0 + (i+1) * 0.4158            new_mid = (current_left + current_right) / 2159            160            new_mid_dot = Dot(axes.c2p(new_mid, y_level), color=color)161            new_left = Line(162                axes.c2p(current_left, y_level),163                axes.c2p(new_mid, y_level),164                color=color165            )166            new_right = Line(167                axes.c2p(new_mid, y_level),168                axes.c2p(current_right, y_level),169                color=color170            )171            172            # 添加端点标签(不显示具体数值)173            a_label = MathTex(f"a_{{{i+1}}}").scale(0.5)174            a_label.next_to(axes.c2p(current_left, y_level), DOWN+LEFT, buff=0.1)175            mid_label = MathTex("").scale(0.5)  # 空标签,不显示中点值176            mid_label.next_to(axes.c2p(new_mid, y_level), DOWN, buff=0.1)177            b_label = MathTex(f"b_{{{i+1}}}").scale(0.5)178            b_label.next_to(axes.c2p(current_right, y_level), DOWN+RIGHT, buff=0.1)179            endpoint_labels.add(a_label, mid_label, b_label)180            181            recursive_intervals.append((current_left, current_right))182            183            self.play(184                Create(new_mid_dot),185                Create(new_left),186                Create(new_right),187                run_time=1.2188            )189            190            self.play(191                Write(a_label),192                Write(b_label),193                run_time=1.0194            )195            196            # 选择左区间或右区间(交替选择以展示递归过程)197            select_left = (i % 2 == 0)198            selected_line = new_left if select_left else new_right199            selected_brace = Brace(selected_line, DOWN)200            201            self.play(202                Create(selected_brace),203                (new_right if select_left else new_left).animate.set_opacity(0.3),204                run_time=1.2205            )206            207            # 保存区间端点208            if select_left:209                interval_endpoints.append((current_left, new_mid))210                current_right = new_mid211            else:212                interval_endpoints.append((new_mid, current_right))213                current_left = new_mid214        215        # 添加区间套说明216        interval_desc = Text("构造闭区间套", color=YELLOW_B).scale(0.5)217        interval_desc.to_corner(RIGHT).shift(LEFT * 2 + UP * 1)218        self.play(Write(interval_desc), run_time=1.2)219        220        # 计算最终区间位置和ξ的位置221        final_left, final_right = interval_endpoints[-1]222        # 确保ξ位于最终区间内223        xi_position = (final_left + final_right) / 2  # 选择区间中点作为ξ224        225        # 极限点226        limit_point = Dot(axes.c2p(xi_position, 0), color=WHITE, radius=0.1)227        limit_label = MathTex(r"\xi").scale(1.2).next_to(limit_point, UP, buff=0.2)228        229        # 分开处理中文和数学符号230        limit_text_ch = Text("存在唯一点", color=WHITE).scale(0.5)231        limit_math = MathTex(r"\xi \in [a_{n}, b_{n}]", color=WHITE).scale(0.7)232        limit_text_n = Text(",n=1,2,...", color=WHITE).scale(0.5)233        234        # 将这些元素组合成一组235        limit_group = VGroup(limit_text_ch, limit_math, limit_text_n)236        limit_group.arrange(RIGHT, buff=0.1)237        limit_group.next_to(interval_desc, DOWN, buff=0.3)238        239        self.play(240            FadeIn(limit_point, scale=1.5),241            Write(limit_label),242            Write(limit_group),243            run_time=1.8244        )245        246        # 强调ξ在最终区间内247        final_interval_with_brackets = VGroup(248            Line(axes.c2p(final_left, 0), axes.c2p(final_right, 0), color=YELLOW_D, stroke_width=5),249            Brace(Line(axes.c2p(final_left, 0), axes.c2p(final_right, 0)), DOWN, color=YELLOW_D)250        )251        252        final_interval_label = MathTex(r"[a_{n},b_{n}]").set_color(YELLOW_D).scale(0.8)253        final_interval_label.next_to(final_interval_with_brackets, DOWN, buff=0.2)254        255        self.play(256            Create(final_interval_with_brackets),257            Write(final_interval_label),258            run_time=1.5259        )260        261        # 创建epsilon说明文本 - 先显示这个262        epsilon_text_ch = Text("存在ε>0", color=WHITE).scale(0.5)263        epsilon_text_ch.next_to(limit_group, DOWN, buff=0.3)264        265        # 先显示文本说明266        self.play(Write(epsilon_text_ch), run_time=1.2)267        268        # 显示ξ的ε-邻域 - 更加明显,改为红色269        epsilon = (final_right - final_left) * 1.5  # 增大邻域范围,使其明显大于[a_n,b_n]270        epsilon_interval = Line(271            axes.c2p(xi_position-epsilon, 0),272            axes.c2p(xi_position+epsilon, 0),273            color=RED,  # 改为红色274            stroke_width=8275        )276        277        # 创建ε-邻域两端的点和标记 - 改为红色278        epsilon_left_dot = Dot(axes.c2p(xi_position-epsilon, 0), color=RED, radius=0.08)279        epsilon_right_dot = Dot(axes.c2p(xi_position+epsilon, 0), color=RED, radius=0.08)280        281        # 放在[a_n,b_n]标签下方 - 改为红色282        epsilon_brace = Brace(epsilon_interval, DOWN, color=RED)283        284        # 显示包含关系 - 将包含符号和标签放在同一行285        inclusion_symbol = MathTex(r"\subset", color=RED).scale(0.9)286        inclusion_symbol.next_to(final_interval_label, RIGHT, buff=0.2)287        288        epsilon_label = MathTex(r"({\xi}-\epsilon,{\xi}+\epsilon)").set_color(RED).scale(0.8)289        epsilon_label.next_to(inclusion_symbol, RIGHT, buff=0.2)  # 放在包含符号右侧290        291        # 然后再显示ε-邻域相关动画292        self.play(293            Create(epsilon_interval),294            FadeIn(epsilon_left_dot),295            FadeIn(epsilon_right_dot),296            Create(epsilon_brace),297            run_time=1.5298        )299        300        # 显示包含关系和标签在同一行301        self.play(302            Write(inclusion_symbol),303            Write(epsilon_label),304            run_time=1.5305        )306        307        # 当n足够大时的矛盾308        final_interval = Line(309            axes.c2p(final_left, 0),310            axes.c2p(final_right, 0),311            color=YELLOW_D,312            stroke_width=5313        )314        315        # 可视化包含关系 - 区间缩小的动画316        self.play(317            final_interval_with_brackets.animate.scale(0.9, about_point=axes.c2p(xi_position, 0)),318            rate_func=there_and_back_with_pause,319            run_time=2.5  # 加长这个动画320        )321        322        # 分开处理中文和数学符号,保持原来的位置323        n_large_ch = Text("当n足够大时,", color=YELLOW_D).scale(0.5)324        n_large_math = MathTex(r"[a_{n},b_{n}] \subset (\xi-\epsilon,\xi+\epsilon)", color=YELLOW_D).scale(0.7)325        n_large_group = VGroup(n_large_ch, n_large_math)326        n_large_group.arrange(RIGHT, buff=0.1)327        n_large_group.next_to(epsilon_text_ch, DOWN, buff=0.3)  # 保持相对位置引用328        329        self.play(330            Write(n_large_group),331            run_time=1.5332        )333        334        # 显示矛盾335        contradiction_ch = Text("矛盾!", color=RED).scale(0.6)336        contradiction_math = MathTex(r"[a_{n},b_{n}]", color=RED).scale(0.7)337        contradiction_ch2 = Text("可被单个开集覆盖!", color=RED).scale(0.6)338        contradiction_group = VGroup(contradiction_ch, contradiction_math, contradiction_ch2)339        contradiction_group.arrange(RIGHT, buff=0.1)340        contradiction_group.next_to(n_large_group, DOWN, buff=0.3)341        342        contradiction = Cross(scale_factor=0.5, stroke_width=6, color=RED)343        contradiction.move_to(final_interval)344        345        self.play(346            Create(contradiction),347            Write(contradiction_group),348            run_time=1.8349        )350        351        # 结论 - 向右移动一个单位并稍微下移352        conclusion = Text("结论:任意开覆盖存在有限子覆盖", color=GREEN).scale(0.6)353        conclusion.to_edge(DOWN, buff=0.3).shift(RIGHT)  # 减小buff值,使其下移一点354        self.play(Write(conclusion), run_time=1.5)355        356        self.wait(2.5)357 358def main():359    import os360    os.system("manim -pql finite_covering_theorem.py FiniteCoveringProof")361 362if __name__ == "__main__":363    main()

讲解

这个视频围绕「有限覆盖定理的证明」展开。画面把问题、图像和公式放在同一条理解路径中,让抽象关系先被看见。

开头先建立问题背景和主要对象,让观察从标题、坐标或第一组关系进入。

画面中出现的文字「有限覆盖定理的证明」给出视觉入口。随后画面通过对象创建、移动、淡入淡出和高亮,把抽象命题拆成可以跟随的步骤。

随后出现更具体的图像或公式提示,动画会把局部对象和整体结论联系起来。这里可以重点观察变量、曲线、区域或向量如何随镜头推进而变化。

核心公式可以写成:

\ldots

这类公式可以和画面中的符号一一对应。

观察路径

观察路径可以分三步:先锁定「有限覆盖定理的证明」中的核心对象,尤其留意有限覆盖定理、紧性、闭区间套定理之间的联系;再跟随画面中变量、图像或向量的变化;最后回到公式或结论,判断局部变化如何支撑整体关系。

本页可以从有限覆盖定理、紧性、闭区间套定理、反证法这些概念进入,继续沿相邻问题观察同一类数学结构。