我生在四川省南边的城乡结合部,这里是一个没落的化工小镇。

窗外隔着大约一百几十米宽度的农田带,就是巍然耸立的发电设施和烟囱,它们守着我整整 18 年,我把它们视作我的父亲。

这个地方从三十年多前开始就一直有一些三轮车在跑,据说还有个什么“驾协”专门管理这些三轮车。听父辈说是下岗潮工人不想离家,所以在这里开三轮车营生。

我已经一学期加一个暑假没坐过那些三轮车了,我真的想念它们。

我的学校在一个标准的现代大都市里面,但大都市令人窒息,我从这种窒息里感觉到极度不自由——精神上的。大都市和大学里面自由的环境甚至加剧了我的囚禁感。虽然学校附近也有三轮车,但是那些三轮车真的好快!并且在匆匆忙忙的汽车和中间匆匆忙忙地见缝插针!

小镇的三轮车平均 30 km/h 不到,我喜欢那种亲切的速度。

三轮车不能载我去小镇以外的地方——它们只出现在小镇出口以内的路段,像是被保护起来了一样令人安心。

这里就是我的家乡。

我对此感到十分感激:假如我在未来遭遇了彻底的打击,我可以回到这个偏僻而静谧的地方,抒发我的一切不快。

遗憾的是,我的学校和可能工作的地方都离这里太远——

请别担心,我亲爱的小镇,等大象上的最后一只蝴蝶来临,我将长眠于您的怀抱。

“这件事本质上是...”,如果你经常和一些人讨论,当出现观点的分歧时,对方大概率会抛出这一句,他意图通过他认为的“本质”来(并以自己为胜者)结束话题。

对大多数人而言,“本质上”几乎是无意识地言说出来的。拉康说 "L'inconscient est le discours de l'Autre"(无意识是大他者的话语)

因为我们从小就教育所谓真理的神圣性,而本质与表象的二元对立又使得很多人如魔怔了一般去追求本质,用异轨自阿尔都塞的话讲,就是主体被大他者询唤,而这个大他者正是知识-权力的象征秩序。有一部分人虽然没那么魔怔,但是内心仍然持存着一份对知识的崇高敬畏,这些人就会实实在在地被那些掌握了知识的人用“本质上...”这种话语来欺负,并有可能留下精神创伤。

不要那么敬畏知识。对真理/知识的过于敬畏只会导致你:

  • 被“本质”话语的暴力所规训
  • 与知识本身天然产生一层隔阂
  • 敬畏权力

加粗是米歇尔·福柯指使的。

这学期学了什么

课程

这学期学校开了两门专业相关的课,一门是《计算机科学与工程导论》、一门是《C 语言程序设计》,然而只学它们会导致大脑被污染。学校学的东西落后无用(或者是晦涩难懂)=>自学已经是显而易见的规律。

《C 语言程序设计》似乎在之前的某篇文章里辣评了一下,这里不说了。但不得不承认的是,这门课发挥了它应有的价值:虽然学校自己的教授方法落后,作业考试大部分只是 intellectual masturbation,用的 IDE 是 Dev C++ 这种。为了让自己写得舒服点,我迫使自己掌握了 VS Code / VS + Git 的工作流,并且顺便熟悉了 Linux 操作系统等等。

所以,如果没有这么一套课程,我没有那么强烈的动力去做这些事情——这些事情其实就是 MIT 那套大名鼎鼎的 The Missing Semester of Your CS Education 课程会讲授的内容,但是我一直都没机会(或者说不敢/不想)上手做一遍。

《计算机科学与工程导论》这课上得也奇葩,老师就干讲 PPT,从数据结构讲到体系结构,整个学期就 2 次不痛不痒的作业。不过令人欣慰的是,有一次作业引导我们去思考递归算法和迭代算法对系统性能的影响,而不是完全聚焦于一些 intellectual masturbation 的功能实现,说明这门课其实并非一无是处。

LBD principle

Learn By Doing,这个谁都会讲,但是得自己做了才真的领略到。我不禁开始咒骂高中时期的自己,为什么那时就死活领会不到这个原则。不过也好,至少现在领会到了。

但是由于这个原则,我在相当长一段时间内又掉进了一个坑——就是看到什么都感觉想做一遍,并且经常 PUA 自己头脑里装的理论知识没有价值,觉得自己什么都不会做不如死了算了,搞得自己身心俱疲,并且学到的内容也很肤浅。知道自己想要什么再去做可能才是根本,我正尝试慢慢地接受这个事实。

重新学习

早在高中的时候就听说过 Lambda Calculus 的大名,但苦于认为自己没有一点系统的程序设计基础,于是对这些基础计算机科学敬而远之,后来才发现它们跟逻辑学、分析哲学其实是差不多的东西。

最近刷 QQ 空间看到有位朋友在一直推一些 Lambda 演算的科普视频,抱着好奇的心态点进去看,不到几分钟就立刻觉得自己以前学的完全不叫“计算机科学”(虽然这么说肯定是错的),于是想要好好了解一番这些东西。

所以自己又顺藤摸瓜找到了一些关于函数式编程的教程,不久前刚在 JavaScript 解释器上使用纯函数实现了单向链表的数据结构,第一次体验到以函数为 building block 的程序设计的灵韵——同时对计算的本质有了更加深入了理解;以及第一次知道程序还能这么学——就从单纯和解释器打交道开始,然后就直接是函数、抽象、递归······并且不受语言特性的打扰。最近书架上添置一本大名鼎鼎的程序设计教材,就是 SICP。希望以后能腾出足够的空闲时间好好领略。

不过这一定不是全部,好奇和求知的路没有尽头。

哇,2026 年的第一篇文章!

最近闲得无聊,又不想学 Unity 游戏制作(本来就不是很想学2333),居然买了本量子力学教材来消遣。虽然后面很难,但还是学到了很多东西,对自然界/微观世界的本质有了进一步的认识,同时也是我追星理论物理学家戈登·弗里曼的一大步(戈登的博士论文和量子力学有关)。

Schrödinger 方程简介

薛定谔(Schrödinger)方程是量子力学的基本方程,它与牛顿第二定律 在经典力学体系的地位相当。

这篇文章推导的薛定谔方程是定态的(time-independent),标准的含时薛定谔方程这次暂时不推,仅给出简单解释。

核心思想

能量守恒:总能量 = 动能 + 势能

这个其实就是标准薛定谔方程里哈密顿(Hamiltonian)量的核心。

Recap

波函数

在量子力学里,波函数用于描述一个量子系统的状态。

类比于声波的波幅,量子力学的波函数用记号 表示,可以看出它其实就是量子波幅,一个量子系统的状态由量子波幅刻画,强度由模方 表示。

测不准原理

其中 是约化普朗克常量。

由量子力学的另一位主要缔造者海森堡(Heisenberg)提出。

就是测量误差的意思,这个表达式想告诉我们的是:在任何情况下,我们都无法精确确定一个基本粒子的位置 和动量 总会存在 测量误差

所以,测量粒子在某个特定时间的状态是无意义的,我们只能通过描述它们的概率分布来寻求更为普遍的规律。这是量子力学与经典力学的根本区别。

能量守恒

本身是概率波的波幅,而概率波的强度 才能真正刻画概率分布,由于必须满足概率密度函数的非负性。直观地说, 越大的地方,在那里找到粒子的概率就越大。

接下来,将正式推导薛定谔方程:

一个粒子的总能量 等于动能 加上势能

而动能关系是我们非常熟悉的:

根据动量 ,动能可以写成:

得到能量守恒公式:

推导动量算子

德布罗意(De Broglie)关系告诉我们:粒子的动量 与波长 成反比:

其中 是约化普朗克常量, 是空间频率,也即在一米内有几个周期(波长)。

由能量守恒公式 知道,我们需要对 进行处理,由于量子力学下的动量遵循 ,它体现了物质粒子的波粒二象性。

先写一个最简单的波

在量子力学中,一个拥有确定动量 的自由粒子,其波函数通常用复指数形式表示(这比 处理起来更方便,因为 的导数还是它自己):

将波数 替换掉,波函数就变成了含有动量 的形式:

尝试提取动量

现在的目标是:寻找一个运算法则(算子),当它作用在 上时,结果等于 乘以

先试着写出对 的导数公式:

求导得到:

所以:

为了把 单独弄出来,于是把等式两边的常数移项:

分子分母同乘

我们得到了动量算子 ,它作用在波函数上就是上式的效果。写成表达式就是:

推导 对应的算子

算子的平方 意味着连续作用两次:

将我们刚才得到的定义代进去:

  • 常数部分:
  • 微分部分:(二阶导数)

于是:

最终,得到动量平方对应的算子:

推导方程

现在,我们将经典能量公式翻译成算子语言,作用在波函数 上:

还记方程的核心思想吗?那就是 总能量 = 动能 + 势能 ,我们刚刚又得到了动量算子,把它写成下面的形式:

注意现在整个表达式不是单纯的乘法,在数学上讲是一种变换。

代入刚才推导的动能算符,就得到了定态薛定谔方程:

方程含义

  • 动能项:

    • 描述了波函数的曲率(二阶导),当这个二阶导越大的时候(i.e. 波函数弯曲得越厉害的时候),动能越大。
  • 势能项:

    • 它是外部环境决定的,比如原子核对电子的吸引力,或者一个电场。它直接乘在 上,表示在位置 处的势能大小。
  • 总能量:

    • 这是一个常数。它告诉我们在这种波函数形态下,系统的总能量是多少。

假设一个电子被束缚在一个很小的盒子里,波函数必须弯得非常急才能使空间上的误差边界归零,这就导致动能极高:这解释了为什么原子核外的电子不会塌进原子核,因为如果不确定位置太小,动能就会大到把它弹开。

刚才的方程是不含时的,如果想知道波函数随时间 怎么演化,我们需要对时间求导,所以薛定谔方程的完整形态是:

其中 就是文章开头谈到的哈密顿量,它其实就是我们推导出来的动量算子和势能向相加:

既然薛定谔方程是个方程,那么就肯定要在实际情况下求解它。求解薛定谔方程其实就是在问:什么样的概率波形状 ,能让它的弯曲程度(动能)加上所在位置的势能,在各处加起来都等于同一个常数

凡是满足这个条件的波,就是电子可能存在的状态,比如原子里的 轨道、 轨道(死去的结构化学还在攻击我)

微分方程

微分方程里面有几个在学线性代数的时候有所耳闻的概念,例如特征方程、特征根、齐次线性方程等。

例如,下面就是个二阶齐次线性微分方程

它的特征方程 ,通过解这个二次方程便得到两个特征根。

然后还有个令人疑惑点就是设 ,不过书上说得对,它的导数与它本身只相差一个常数因子,这样方便进一步思考下去,但我还是感觉不妥当。

大一统

首先,需要建立一个统一的视角:算子(Operator)

高数第一章讲映射的时候就提到过,映射本身在不同的数学分支有不同的叫法,其中一个叫法就是算子

矩阵 维空间中的向量映射到 维空间中去,并且对加法和数乘运算满足线性,所以它是一个线性映射/线性算子

,它将函数空间中的某个函数映射到另一个函数空间,并且也对加法和数乘运算满足线性,它也是一个线性映射/线性算子

  • 线性代数研究的是矩阵 ,它作用于向量 (有限维向量空间)

  • 微分方程研究的是求导算子 ,它作用于函数 (无限维向量空间)

对应关系

为什么微分方程要“设”解为

在线性代数中,我们在寻找一组特殊的基底(特征向量),矩阵作用在它上面等效于直接乘以常数

  • 在线性代数里:

  • 在微分方程里: 我们在寻找一个函数,求导(算子 )之后还是它自己。只有一种函数满足“求导后形状不变,只变系数”,那就是指数函数

    • 就是微分算子 “特征向量”(准确地说叫特征函数

特征方程的由来

我们求解 。为了有非零解,必须要求行列式 ;

考虑一个微分方程 ,将它写成算子形式就是 如果我们把解限制在“特征函数” 的集合里,算子 就变成了代数变量 。于是算子多项式 就变成了代数多项式

杂交

对于二阶线性微分方程 ,引入变量 ,将其改写为矩阵形式:

以上可以优雅地简写为 ,此时可以发现矩阵方程与微分方程的特征方程完全一致

矩阵 的特征多项式是 。这与原微分方程的特征方程 完全一致!

并且,我们在这里首次揭示了导数算子 实际上可以用矩阵 来表示——求导和矩阵在本质上都是线性映射,它们在代数结构上存在一致。

它们本就一体

最后,来欣赏一下微分方程的通解和线性代数里特征向量的通解结构:

  • 线性代数:
  • 微分方程:

微分方程就是线性代数在无穷维向量空间下的情况进行考虑的。

哲学与科学

哲学与科学最大的区别不在于使用何种逻辑回路,而是把握的对象。不过这一分野在古希腊时期并不明显。包括以下区别大多数是属于文艺复兴之后的近现代哲学与科学的。

哲学与科学的知识具有根本性的不同。科学一定是倾向于体系化的,但哲学不一定,哲学是自由的

哲学不具备直接的实践可能,但科学有。

哲学要回答目前为止的科学不能回答的问题,一旦科学发展到可以回答那种问题的程度,哲学也随之发展。在这件事上,黑格尔和一众英美分析哲学家应该达成了一致。

哲学与实践

我不能立即回答哲学的实践是什么,但我能立即回答哲学的实践不是什么。

  • 哲学的实践不是文本的再生产:写出版物不是实践。
  • 哲学的实践不是拟像的再生产:做视频讨好文艺亚逼不是实践。同样地,在一个哲学圈子里混也不是实践。
  • 哲学的实践不是意识形态垃圾的再生产:放弃反思的政治不是实践。

实践

上一个部分一直在强调实践(Praxis),这在日常语境下并不是一个很令人震撼的词,但对哲学而言是。

哲学家们只是用不同的方式解释世界,但问题在改变世界。” 有人认为这句话终结了本体论(Ontology),我大概也这么认为。至少是对整个传统西方形而上学的有力反问。

说到传统西方形而上学,就不得不提海德格尔(Martin Heidegger, 1889-1976)这个人了。下次再聊吧。看他又是怎么在哲学的层面上颠覆传统形而上学的。

骂人

《C 语言程序设计》已经接近尾声了,但感觉自己除了一些语法和标准库之外什么都没学到。

教材是学校自己编的,编的简直是一坨翔,某一页指针的示意图搞忘把实际的值换成地址都算轻的,讲二维数组甚至出现了“三行四列的行列式”这种已经被当成梗了的错误。

其它的部分更难评。感觉从教材到课程,教授们根本就没打算让我们学好这门课。

其次,学校买的 OJ 简直是我用过最不舒服的东西,包括里面的某些题,只觉 mind-bending 却用处不大,我整个学期都没花什么精力在那上面。

考试

写这篇文章发牢骚当然也是因为考试。我并不擅长做题,考试基本上只能做出大部分。不过我确实得认,因为没刷过题,平时作业完全随性做,老师布置的一个 82 道题的习题集我只做了 1 道。

我之前因为发牢骚还被同届的某“大神”攻击过,好像以后做什么事都跟这些考试题关系很大一样。虽然我确实会被这种聪明的智障气到,但我也理解这些“大神”:他们花了大量时间精力在刷题,就为了得个好看的名次并意淫自己的能力压别人一头。我认为这是 incompetent 的表现。

诈骗

今天讲动态内存管理,你猜讲了多久?只讲了十分钟不到!从引入 stack, heap 概念到讲完书上包含 malloc() 例子的实现,全程只花了几分钟!虽然我觉得很无聊,但是对于以前从来没接触过这些概念的同学,他们真的理解了吗?即使记得了几个名词,知道了语法该怎么写,但是究竟发生了什么谁知道呢?先暂且不谈现代操作系统上指针存的是 virtual address 还是 physical address 的问题,就说大家在提交作业的时候有时也会遇到的“段错误”(Seg fault)是什么意思,课上也不教。

书的序言还好意思着重强调 C 语言对计算机底层和内存管理的优势,这下倒好,这俩在你们的课程里都跟完全没存在感的 NPC 一样。完全就是诈骗。

认真你就输了

你反驳我就是你对。有人急了跟我说这些都是在打基础,我当然承认学校的作业和考试会训练敲码的肌肉记忆。那就当我在扯淡吧。

6502 是什么

6502 由 MOS Technology 于 1975 年发布的一款 8 位微处理器,流行于上世纪七八十年代的大部分家用 PC。

苹果 I/II, Commodore 64, 任天堂 NES/FC 游戏主机(后来的 SNES/SFC 主机也只是 6502 的 16 位扩展)等现象级家用机平台都基于 6502 体系结构。

项目

easy6502 有助于快速上手 6502 体系结构的汇编语言,链接为 https://github.com/skilldrick/easy6502

TODO:汉化

递推公式

Fibonacci 数列的递推公式为:

其中

矩阵表示

上式可以用矩阵表示:

求解方法

通过寻找与矩阵 相似的对角矩阵 ,可以利用 来得到

通项

特征多项式 ,令 解得特征值

对应的特征向量分别为 ,得到过渡矩阵及其逆矩阵:

对角矩阵:

由相似变换 得到

所以:

所以 Fibonacci 数列的通项公式为:

测量 CPU 时间

上篇文章写了个简单的蒙特卡洛估计器,用于估计 的值,但是那个非常慢,可以通过 System.Diagnostics 提供的 Stopwatch() 方法算一下时间。

var watch = new Stopwatch();

stopwatch.Start();

for (ulong i = 0; i < N; i++)
{
    x = rnd.NextDouble();
    y = rnd.NextDouble();
    if (x * x + y * y <= 1)
    {
        circleCount++;
    }
}
double PI = 4 * (double)circleCount / N;

watch.Stop();

Console.WriteLine("Pi = {0}, time = {1}ms", PI, watch.ElapsedMilliseconds);

设置采样数为 5000000000,得到 Pi = 3.1415527664, time = 37026ms。为了计算这个稳定于 3.1415 以上的结果,花费了 37 秒。

多线程

现代 CPU 具有多个物理核心,可以把它们利用起来。用 ThreadLocal<Random>() 为各个线程单独生成随机数。

using System.Threading;
using System.Threading.Tasks;

void ParallelEst()
{
    // 全局计数器
    long totalCircleCount = 0;

    var watch = new Stopwatch();
    ThreadLocal<Random> threadRnd = new ThreadLocal<Random>(() => new Random(Guid.NewGuid().GetHashCode()));    //各线程通过唯一 GUID 生成哈希值,防止随机样本序列重复

    watch.Start();

    // 循环并行化
    Parallel.For(
        0,
        (long)N,
        () => 0UL,
        (i, loopState, localCount) =>   // i: 当前迭代,localCount: 当前线程的局部计数器
        {
            Random rnd = threadRnd.Value;
            double x = rnd.NextDouble();
            double y = rnd.NextDouble();
    
            if (x * x + y * y <= 1)
            {
                localCount++;
            }
            return localCount; 
        },
        localCount =>  // 累积局部计数器到全局计数器
        {
            Interlocked.Add(ref totalCircleCount, (long)localCount);
        }
    );

    threadRnd.Dispose();

    double PI = 4 * (double)totalCircleCount / N;

    watch.Stop();

    Console.WriteLine("Pi = {0}, time = {1}ms", PI, watch.ElapsedMilliseconds);
}

以下是运行时间:

1 线程 24 线程
36412 ms 6348 ms

多线程的确占用起来了:

taskmgr

踩坑

之前是这么写的,结果并行化反而慢了 10 倍:

void ParallelEst()
{
    ulong circleCount = 0;
    
    var watch = new Stopwatch();
    
    ThreadLocal<Random> threadRnd = new ThreadLocal<Random>(() => new Random(Guid.NewGuid().GetHashCode()));
    
    watch.Start();
    
    Parallel.For(0, (long)N, i =>
    {
        Random rnd = threadRnd.Value;
        double x = rnd.NextDouble();
        double y = rnd.NextDouble();

        if (x * x + y * y <= 1)
        {
            Interlocked.Increment(ref circleCount); // 锁在线程循环里面
        }
    });
    
    threadRnd.Dispose();
    
    double PI = 4 * (double)circleCount / N;
    
    watch.Stop();
    
    Console.WriteLine("Pi = {0}, time = {1}ms", PI, watch.ElapsedMilliseconds);
}

原因是每个线程每次累计一次 circleCount 都要调用一次 Interlocked 方法,导致线程之间产生锁竞争,大幅降低并发性能。

修复方案就是给单个线程分配一个局部的计数器 localCount,等各线程累计结束之后再调用原子操作。

0%