Archipelago

Lec 1算法与计算

讲义完成更新于 2026-09-16
承接

这是当前课程已收录的第一篇。

Lec 1 算法与计算

资料依据:课程视频与 transcript · Lecture notes

TL;DR

  • 算法是一个规模有限、定义清楚的计算过程;评价算法时必须同时说明正确性与效率。
  • 渐进记号描述输入规模增长时的运行时间,帮助我们忽略机器常数并比较增长率。
  • Word-RAM 把对一个机器字的基本操作视为常数时间;数据结构则把算法需要的操作接口与具体表示分离。

算法的定义

这门课的作用不仅教你如何解决计算问题,还教你如何与别人沟通,来说明的方案是正确的且高效的。

什么是问题? 什么是算法?

输入   输出
a      A
b      B
c      C 
d      D
输入
a: A, B ,C 
b: B, D
a: A

判断两数之和是否在同个数组里面?
[1, 3, 4, 6, 2],
1+3=4 (1,3)
2+4=6 (2,4)

问题是输入与正确输出之间存在的一种二元关系,通常不会为所有输入指定每一个正确输出(太多了!),而是提供一个可验证的谓词(或者说属性)来规定正确输出必须满足的条件。

例如

  • 非通用输入的问题: 在这间教室里,是否有一对学生有相同的生日?
  • 通用的输入的问题: 给定任意一组 n 个学生,是否有一对学生有相同的生日?

我们对通用的的问题更加感兴趣。这也是这个课程研究的对象。

(确定性的)算法则是用于解决这些问题的过程,过程将每个输入映射到单个输出(是确定的,也就是说正确的解可能有多个,但是算法的过程最终产生一个确定的输出)

如何说明你的算法是正确的呢?

必须通过归纳假设证明。

例子: 生日匹配算法的正确性证明

生日匹配步骤如下:

  1. 维护一个记录已采访过学生姓名和生日的集合。
  2. 遍历每个学生,询问他们的姓名和生日。
  3. 对于每个学生,检查他们的生日是否已经在记录中:
    • 如果是,返回这两个学生的姓名。
    • 如果否,将该学生的姓名和生日添加到记录中。
  4. 如果遍历所有学生后没有找到匹配对,返回没有匹配对。

正确性证明:

  • 对前 k 个被采访的学生进行归纳。
  • 基本情况: k=0 时,没有匹配对,算法返回没有匹配对。
  • 归纳假设:假设对于前 k‘ 个学生,算法返回的是正确的结果,并考虑 k = k' + 1 的情况
    • 如果前 k'个学生中包含匹配项,则根据归纳法已经返回匹配项。
    • 否则前 k'个学生中没有匹配项,所以如果前 k' + 1 个学生中有匹配项,那么匹配项包含第 k' + 1 个学生。
    • 然后算法直接检查学生 k' + 1 的生日是否存在于前 k‘个学生中
    • 所以对于前 k’+1 个学生的算法也能返回正确的结果。

**算法的执行速度有多快?**或者说算法效率怎么样?

  • 我们希望性能是与机器无关的,不要用时间来衡量。
  • 我们可以用操作计数:这个算法的动作的数量,并且我们期望性能(时间消耗)依赖于输入的大小,输入越大,所需时间越长。输入大小通常称为"n",但不总是如此,比如 n*n 的二维数组,我们说输入规模是 n*n
  • 如果算法在相对于输入的多项式时间(Θ(nc)Θ(n^c) )内返回结果,则认为其是高效的。但有些问题可能不存在高效的算法(参见 L20)。

渐进表示法

渐进表示法来忽略与问题输入规模无关的常数。O(f(n))表示一组定义在自然数域上的函数,这些函数满足以下性质。

O 表示法:当且仅当存在一个正实数 c 和一个正整数n0n_0,使得对于所有 n ≥ n0,非负函数 g(n)属于 O(f(n)),即 g(n) ≤ c · f(n)

该定义上界了一个函数的渐进增长,对于足够大的 n,即使我们将函数按常数量进行缩放或平移,该增长的上界仍然成立。按照惯例,人们更常说一个函数 g(n)是 O(f(n))或g(n)=O(f(n))g(n)=O(f(n)),但他们真正的意思是集合包含关系,即g(n)∈O(f(n))g(n) ∈ O(f(n))。因此,由于我们问题的输入规模是cncn(某个常数 c),我们可以忽略 c,直接说输入规模是 O(n)(即 n 的阶)。类似的表示法也可以用于下界。

Ω表示法:当且仅当存在一个正实数 c 和一个正整数n0n_0,使得对于所有 n ≥ n0n_0,非负函数 g(n)属于Ω(f(n)),即 c · f(n) ≤ g(n)。

当一个函数既有渐进上界又有渐进下界时,我们使用Θ表示法。当 g(n) = Θ(f(n))时,我们说 f(n)对 g(n)提供了一个紧密的界限。

Θ表示法:当且仅当 g(n) ∈ O(f(n))∩Ω(f(n)),非负函数 g(n)属于Θ(f(n))。

算法与计算图示 1

计算模型

为了精确计算算法使用的资源,我们需要模拟计算机执行基本操作所需的时间。指定这样的一组操作可以提供一个计算模型,基于此模型我们可以分析。 即哪些常数时间内(O(1))执行某些操作,我们需要定义一套明确的规范。这些规范帮助我们在分析和设计算法时能够统一考虑其效率。

Word-RAM 模型(RAM,随机访问内存), 是一种计算模型,用于描述在计算机上进行的基本操作。它假设:

  • 机器字是固定大小的位块。常见的大小有 32 位和 64 位。
  • 内存是由这些机器字组成的,可寻址的连续序列。
  • 处理器支持在常数时间内对这些字进行基本操作,包括整数运算、逻辑运算和位运算。
  • 给定一个机器字 a,处理器可以在常数时间内读取或写入位于地址 a 的内存中的机器字。如果一个机器字仅包含 w 位,那么处理器只能从最多2w2^w个内存地址中读取和写入

数据结构

我们生日匹配算法的运行时间取决于我们如何存储名字和生日的记录。数据结构是一种存储非恒定数量数据的方法,支持一组与数据交互的操作。数据结构支持的操作集称为接口。许多数据结构可能支持相同的接口,但每个操作可能提供不同的性能。通过在适当的数据结构中存储数据,可以轻松解决许多问题。对于我们的例子,我们将使用 Word-RAM 中最基本的数据结构:静态数组。静态数组只是内存中保留的一个连续的数据项序列,支持静态序列接口:

  • StaticArray(n): 分配一个大小为 n 的新的静态数组,并在Θ(n)时间内初始化为 0
  • StaticArray.get_at(i): 在Θ(1)时间内返回存储在数组索引 i 处的单词
  • StaticArray.set_at(i, x): 在Θ(1)时间内将单词 x 写入数组索引 i 处

get_at(i)和 set_at(i, x)操作在常数时间内运行,因为数组中的每个数据项大小相同:一个机器字。要在数组索引处存储更大的对象,我们可以将该索引处的机器字解释为指向更大内存块的内存地址。Python 元组类似于没有 set_at(i, x)的静态数组。Python 列表实现了一个动态数组。

class Static_Array:
    def __init__(self, n):
        self.data = [None] * n

    def get_at(self, i):
        if not (0 <= i < len(self.data)):
            raise IndexError
        return self.data[i]

    def set_at(self, i, x):
        if not (0 <= i < len(self.data)):
            raise IndexError
        self.data[i] = x

def birthday_match(students):
    '''
    Find a pair of students with the same birthday
    Input: tuple of student (name, bday) tuples
    Output: tuple of student names or None
    '''
    n = len(students)  # O(1)
    record = Static_Array(n)  # O(n)
    for k in range(n):  # n
        (name1, bday1) = students[k]  # O(1)
        for i in range(k):  # k Check if in record
            (name2, bday2) = record.get_at(i)  # O(1)
            if bday1 == bday2:  # O(1)
                return (name1, name2)  # O(1)
        record.set_at(k, (name1, bday1))  # O(1)
    return None  # O(1)

练习题

对于(n6006)\binom{n}{6006}的紧确界是什么

Solution: 由公式可以n(n−1)...(n−6005)n(n − 1). . .(n − 6005),当 n 较大时,每个项都接近与 n,因此由(n6006)≈Θ(n6006)\binom{n}{6006} \approx \Theta(n^{6006})

对于log⁡6006(log⁡(nn))2)\log_{6006}{(\log{(n^{\sqrt{n})})^2})}的紧确界是什么?

log⁡6006(log⁡(nn))2)=2log⁡6006log⁡()nlog⁡n=Θ(log⁡n1/2+log⁡log⁡n)=Θ(log⁡n)\log_{6006}{(\log{(n^{\sqrt{n})})^2})} = {2\over{\log{6006}}}\log{()\sqrt{n}\log{n}} = \Theta(\log n^{1/2} + \log{\log{n}})= \Theta(\log{n})

证明(log⁡n)a=O(nb)对于所有的常数a,b成立(\log{n})^a = O(n^b) 对于所有的常数a, b成立

Solution: 即证lim⁡n→∞nb/(log⁡n)a\lim_{n\to \infty} n^b/(\log{n})^a 趋近于正无穷,lim⁡n→∞nb/(log⁡n)a=lim⁡n→∞(blog⁡n−alog⁡log⁡n)=lim⁡x→∞(bx−alog⁡x)=∞\lim_{n\to \infty} n^b/(\log{n})^a = \lim_{n\to \infty} (b\log{n} - a\log{\log{n}}) = \lim_{x \to \infty}(bx-a\log{x} ) = \infty

证明 (log⁡n)log⁡n=Ω(n)(\log{n})^{\log{n}} = \Omega(n)

我们知道mm=Ω(2m)m^m = \Omega(2^m), 用n=2mn = 2^m带进去即可证明

证明 (6n)!∉Θ(n!)(6n)! \notin \Theta(n!),但是log⁡((6n)!)∈Θ(log⁡(n!))\log((6n)!) \in \Theta(\log(n!))

有斯特林估算公式n!≈2πn(ne)n(1+Θ(1/n))n! \approx \sqrt{2 \pi n} \left(\frac{n}{e}\right)^n(1+\Theta(1/n)),将 6n 带进去,发现至少是原来66n6^{6n}倍,但是如果用对数函数,会发现log⁡(n!)=Θ(nlog⁡n)\log(n!) = \Theta(n\log{n}),并且代入 6n,会发现只是常数倍的差距

我的理解

我的理解

“解决问题”和“证明解法”是同一项工作的两面:没有明确的计算模型,效率声明就没有可比性;没有正确性论证,更快的程序也不能称为算法解。