Skip to content

Lec 1 介绍

  • 算法的定义
  • 渐进表示法
  • 计算模型
  • 数据结构
  • 练习题

算法的定义

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

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

输入   输出
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) )内返回结果,则认为其是高效的。但有些问题可能不存在高效的算法(参见L20)。

渐进表示法

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

O表示法:当且仅当存在一个正实数c和一个正整数n0​,使得对于所有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))。因此,由于我们问题的输入规模是cn(某个常数c),我们可以忽略c,直接说输入规模是O(n)(即n的阶)。类似的表示法也可以用于下界。

Ω表示法:当且仅当存在一个正实数c和一个正整数n0,使得对于所有n ≥ n0,非负函数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))。

截屏2024-07-30 13.15.27

计算模型

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

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

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

数据结构

我们生日匹配算法的运行时间取决于我们如何存储名字和生日的记录。数据结构是一种存储非恒定数量数据的方法,支持一组与数据交互的操作。数据结构支持的操作集称为接口。许多数据结构可能支持相同的接口,但每个操作可能提供不同的性能。通过在适当的数据结构中存储数据,可以轻松解决许多问题。对于我们的例子,我们将使用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列表实现了一个动态数组。

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)的紧确界是什么

Solution: 由公式可以$ n(n − 1). . .(n − 6005) nn\binom{n}{6006} \approx \Theta(n^{6006})$

对于log6006(log(nn))2)的紧确界是什么?

log6006(log(nn))2)=2log6006log()nlogn=Θ(logn1/2+loglogn)=Θ(logn)

证明(logn)a=O(nb)a,b

Solution: 即证limnnb/(logn)a 趋近于正无穷,limnnb/(logn)a=limn(blognaloglogn)=limx(bxalogx)=

证明 (logn)logn=Ω(n)

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

证明 (6n)!Θ(n!),但是log((6n)!)Θ(log(n!))

有斯特林估算公式n!2πn(ne)n(1+Θ(1/n)),将6n带进去,发现至少是原来66n倍,但是如果用对数函数,会发现log(n!)=Θ(nlogn),并且代入6n,会发现只是常数倍的差距