Mit6.006-lecture01-Introduction notes

本文深入探讨了算法的概念,包括它们如何将输入转化为正确输出,以及如何证明算法的正确性和效率。算法的正确性通过归纳法进行证明,而效率则通过分析其运行时间和渐进复杂度来衡量。此外,文章还介绍了计算模型,如Word-RAM,以及数据结构在算法性能中的关键作用。重点讨论了静态数组作为基础数据结构的例子,以及如何使用它来实现生日匹配算法。最后,文章提出了渐进性练习,涉及组合数学、对数和指数函数的分析。
摘要由CSDN通过智能技术生成

一、问题

1、问题输入到正确输出的二元关系。
2、通常不需要对所有输入明确正确输出。
3、提供一个可验证的预测,正确输出必须满足。
4、6.006基于大量通用输入,研究问题。
5、非通用:小输入实例
样例:在此教室中,有一对学生有相同生日么?
6、通用:任意大量输入
样例:给定任意n个学生的集合,有一对学生有相同生日么?
如果生日是365天中的一个,对于n>365,答案总是正确的。
假设生日的分辨率超过n

二、算法

1、程序映射每个输入到单个输出。
2、如果算法对于每个问题输入,都返回一个正确输出,那么算法解决了问题。
3、例子:一个解决生日匹配的算法
维护一个名字与生日的记录(初始化为空)。
以某种顺序,采访每个学生。如果生日存在记录中,返回找到的这对。否则把姓名生日加到记录里。
如果采访的最后一名学生没有成功,返回None。

三、正确性

1、程序/算法有固定尺寸,如何证明其正确性?
2、对于少量输入,可以使用例子分析。
3、对于任意大量输入,算法必须是以某种方式,递归的或者循环的。
4、必须使用推断(为什么递归是一个计算机科学中如此关键的理念)
5、样例:生日匹配算法正确性的证明:
k:记录中学生的数量
假设:如果前k个包含匹配,在采访学生k+1之前返回匹配结果。
base case:k=0,前k个不包含匹配
归纳法假设k=k’成立,考虑k=k’+1
如果前k’包含一个匹配,已经通过推断返回了一个匹配
否则前k’不包含匹配,所以如果前k’+1有匹配,那么匹配包含k’+1
然后算法直接检测,是否学生k’+1的生日存于前k’

四、效率

1、算法多快产生一个正确的输出
可以评估时间,但想性能是独立于机器的。
返回算法操作(固定时间)的次数。
期望依赖输入的尺寸,更大的输入需要更多的时间。
输入的尺寸通常称作n,但不总是如此。
有效的,对于给定输入,返回多项式时间。
对于某个问题,有时没有有效的算法。
2、渐进理念:忽略常量因子和低阶项
上边界(O),下边界(Ω),上界和下界(Θ)。
时间估算,基于每个时钟周期一个操作,1GHz的单核机器。

inputconstantlogarithmiclinearlog-linearquadraticpolynomialexponential
n Θ ( 1 ) \Theta(1) Θ(1) Θ ( l o g n ) \Theta(logn) Θ(logn) Θ ( n ) \Theta(n) Θ(n) Θ ( n l o g n ) \Theta(nlogn) Θ(nlogn) Θ ( n 2 ) \Theta(n^2) Θ(n2) Θ ( n c ) \Theta(n^c) Θ(nc) 2 Θ ( n c ) 2^{\Theta(n^c)} 2Θ(nc)
10001 ≈ 10 \approx10 101000 ≈ 10 , 000 \approx10,000 10,0001,000,000 100 0 c 1000^c 1000c 2 1000 ≈ 1 0 301 2^{1000}\approx10^{301} 2100010301
Time1ns10ns1us10us1ms 1 0 3 c − 9 10^{3c-9} 103c9s 1 0 281 10^{281} 10281millenia

五、计算模型

1、声明,机器上的什么操作可在O(1)时间内执行。
2、这类模型成为Word-RAM。
3、Machine word:w位的块(w是w位Word-RAM的字尺寸)。
4、内存:可寻址的一系列machine words。
5、对于O(1)数量的字,处理器支持一些常量时间操作:

  • 整数运算:(+,-,*,/,%)
  • 逻辑操作符:(&&,||,!,==,<,>,<=,>=)
  • 位运算:(&,|,<<,>>,…)
  • 给定word a:可以读写地址a处的字

6、内存地址必须能够访问内存中的每一处:

  • 需要:w>=#位来代表最大的内存地址,例如: l o g 2 n log_2n log2n
  • 32位字,最大内存4GB;64位字,最大内存16EB

7、python是一个更复杂的计算模型,基于Word-RAM实现。

六、数据结构

1、数据结构是一个存储非常量数据方式,它支持一组的操作。
2、操作集合成为接口:
Sequence:项目外部有序(first、last、nth)
Set:项目内部有序(基于item的key查询)
3、数据结构可以用不同性能实现同样接口
4、例子:静态数组,固定槽位,固定长度,静态序列接口
StaticArray(n):分配尺寸n的静态数组,用Θ(n)时间初始化为0
StaticArray.get_at(i):返回存储在数组索引i处的word,用时Θ(1)
StaticArray.set_at(i, x):把word x写到数组索引i处,用时Θ(1)
5、存储的word可以持有更大对象的地址
6、与Python tuple,set_at(i, x)相似,python list是一个动态数组

七、recitation

算法(algorithms)

  算法研究是寻找有效解决问题的方法。本课的目的,不仅教你如何解决的问题,还教你告知别人:问题的解决方案是既正确、又高效的。
  问题是一个二元关系:将输入和正确输出关联起来。
  一个确定的算法,是一个将输入映射到一个输出上的程序。
  一个算法解决一个问题,对于每个问题的输入,它返回正确的输出。
  当问题输入有不止一个正确输出时,一个算法,应该仅对给定输入返回一个输出。举个例子,考虑这个问题:在复习课中,找到另外一个拥有同样生日的学生。
  问题:在复习课的学生中,要么返回拥有同样生日、年份的两个学生,要么说明没有这种情况存在。
这个问题,将一个输入(你的复习课),与一个或多个输出(由生日匹配的学生对组成、或-1)关联 .一个问题的输入有时称作问题的实例。解决这个问题的算法如下:
  算法:保留一个初始为空的学生姓名和生日的记录,遍历教室,询问每个学生的姓名和生日。采访完每个学生后,检查他们的生日是否已存在于记录中。如果已存在,返回两个学生的姓名。否则,将他们的姓名和生日添加到记录中。如果面试完所有学生后,没有满足的pair找到,返回没有满足的pair存在。
  当然,我们的算法解决更通用的问题,比起上面提到的。同样的算法,可以在任意集合的学生中找到生日匹配的pair,而不仅仅是复习课中学生。在本课中,我们尝试解决问题(输入可以是任意大)。生日匹配算法可以被应用到任意尺寸的复习课。但我们如何确定这个算法是正确的、有效的?

正确性(correctness)

  任何你写的计算机程序拥有有限尺寸,然而它作用的输入可以是任意大。因此本课中,我们讨论的每个算法,将需要重复算法中的命令,通过循环或递归。并且我们将能通过推断证明算法的正确性。让我们证明生日算法的正确性。
  证明:前k个学生面试完了。基本情形:k=0,没有匹配对,算法返回没有匹配对。或者,假设算法对于前k个学生返回正确。如果前k个学生包含一个匹配对,那么k+1个学生也是如此,并且算法已经返回了一个匹配对。否则,前k个学生没有包含匹配对,所以如果k+1个学生包含一个匹配,那么这个匹配包含学生k+1,并且算法检查是否学生k+1有同样的生日,如同已经处理过的一样。

效率(efficiency)

  什么让一个计算机程序有效?如果可以用更少的资源解决同样问题,那么一个程序比另外一个更有效。我们期望:更大输入可能花费更多时间解决,比起更小尺寸的输入。另外,程序使用的资源,例如存储空间、运行时间,将取决于使用的算法,以及实现的算法的机器。我们期望:更快机器上实现的算法,将比更慢机器上的相同算法,要更快,对于相同输入。我们想能够比较算法,无需关心机器多快。因此在本课中,我们比较算法,基于他们的渐进性能(与问题输入尺寸相关),为了忽略不同的硬件性能的常量因子。

渐进性理念(asymptotic notation)

  我们可以使用渐进性理念来忽略常量(不会随问题输入的尺寸改变)。O(f(n))代表函数(在自然数域满足以下属性)集合。

O理念:非负函数g(n)是O(f(n)),存在正实数c,和正整数n0,对于所有n>=n0,满足g(n)<=cf(n)。

此定义为足够大的n的函数的渐近增长上界。即使,我们以恒定数量放缩或移动我们的函数,增长的界限也是对的。按照惯例,对人们来说,称函数g(n)是O(f(n))或等于O(f(n))是更常见的,但它们真正的含义是子集,g(n)\inO(f(n))。因此,由于我们的问题大小是cn,我们可以忽略c,并将输入大小称为O(n)。一个相似的理念可被用作下边界。

Ω \Omega Ω理念:非负函数g(n)是 Ω ( f ( n ) ) \Omega(f(n)) Ω(f(n)),存在正实数c和正整数n0,对于n>=n0,让cf(n)<=g(n)。

当一个函数既上边界渐进、又下边界渐进另外一个函数,我们使用 Θ \Theta Θ理念。当g(n)=   Θ ( f ( n ) ) \ \Theta(f(n))  Θ(f(n)),我们称f(n)代表g(n)的严格上下限。

Θ \Theta Θ理念:如果 g ( n )   ∈ g(n)\ \in g(n)  O ( f ( n ) ) ∩ O(f(n))\cap O(f(n)) Ω ( f ( n ) ) \Omega(f(n)) Ω(f(n)),非负函数g(n)是 Θ ( f ( n ) ) \Theta(f(n)) Θ(f(n))

我们通常使用缩写来描述常见函数的渐进增长(例如:渐进复杂度),例如那些展示在下表中的。这里我们假设 c ∈ Θ ( 1 ) c\in\Theta(1) cΘ(1)

shorthandConstantLogarithmicLinearQuadraticPolynomialExponentia
Θ ( f ( n ) ) \Theta(f(n)) Θ(f(n)) Θ ( 1 ) \Theta(1) Θ(1) Θ ( l o g n ) \Theta(logn) Θ(logn) Θ ( n ) \Theta(n) Θ(n) Θ ( n c ) \Theta(n^c) Θ(nc) Θ ( n c ) \Theta(n^c) Θ(nc) 2 Θ ( n c ) 2^{\Theta(n^c)} 2Θ(nc)

对解决问题来说,线性时间通常是必要的,为了解决问题,整个输入必须被读入。然而,如果输入已经可从内存访问,一些问题能以sub-linear时间被解决。例如,在有序数组(已经加载进内存)中找一个值的问题,可以通过二分查找,以对数时间解决。本课中,我们聚焦于polynomial。在logarithmic、linear和exponential之间有很大不同。

如果n=1000, l o g n ≈ 10 logn\approx10 logn10,n ≈ 1 0 3 \approx10^3 103,然而 2 n ≈ 1 0 300 2^n\approx10^{300} 2n10300。为了比较,宇宙中原子的数量大约 1 0 80 10^{80} 1080。通常用变量n来代表参数(在问题输入尺寸中是线性的),尽管并非总是这种情况。例如,之后讨论的图算法,问题的输入将是一个图参数:点集合V和线集合E,因此自然数输入尺寸是: Θ ( ∣ V ∣ + ∣ E ∣ ) \Theta(|V|+|E|) Θ(V+E)。或者,当讨论矩阵算法时,通常n表示方阵的宽度,问题输入有尺寸 Θ ( n 2 ) \Theta(n^2) Θ(n2),表明n*n矩阵的每个元素。

计算模型(computation)

  为了精确地计算算法使用的资源,我们需要模型化 一个计算机,花费多久来执行基本操作。明确这样一组操作,提供一个计算模型,我们可以把我们的分析基于这个计算模型。在本课中,我们将使用w-bit 字RAM计算模型(将计算机模型化为一个随机访问的机器字数组,称为内存),以及一个执行内存操作的处理器。一个机器字是一串w位,代表一个整数{0,…, 2 w − 1 2^w-1 2w1}。一个Word-RAM处理器,可以在常量时间内,执行两个机器字的基本二维操作,包括加、减、乘、除、取模、位操作、二进制比较。另外,给定字a,处理器可以用常量时间,读、或写内存中位于地址a的字。如果一个机器字仅包含w位,处理器将只能读、写,至多2w个内存地址。因此当解决输入为存储在n个机器字上的问题时,我们将总是假设我们的Word-RAM至少有w> log ⁡ 2 n \log_2{n} log2n位,否则机器不能访问内存中的所有输入。为了让这个限制直观些,一个Word-RAM模型(可字节寻址的64位机器),允许输入尺寸高达 1 0 10 10^{10} 1010GB。

数据结构(data Structure)

  我们生日匹配算法的执行时间,取决于我们存了多少个名字与生日的记录。数据结构是一种存储非固定数量数据的一种方式,支持一组操作来操作数据。这组被数据结构支持的操作称为接口。一些数据结构可能支持相同操作,但可能对于每种操作有着不同的性能。一些问题可以通过用恰当的数据结构存储数据,来解决。对于我们的例子,我们将使用最原始的数据结构(源于Word-RAM):static-array。static-array是内存中简单的、连续的字序列,支持地序列接口:
  StaticArray(n):分配一个尺寸为n的新static array,初始化为0。用时 Θ ( n ) \Theta(n) Θ(n)
  StaticArray.get_at(i):返回存储在数组中,索引为i的元素。用时 Θ ( 1 ) \Theta(1) Θ(1)
  StaticArray.set_at(i, x):把字x写到数组索引为i的位置。用时 Θ ( 1 ) \Theta(1) Θ(1)
  get_at(i)和set_at(i, x)操作执行时间为常量,因为数组中的每个元素有同样的尺寸:一个机器字。为了在数组中存储更大的对象,我们可以将数组中的机器字,解释为一个内存地址(指向更大的一块内存)。Python tuple类似static array(除了set_at(I, x))。Python list实现dynamic array。

class StaticArray:
    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):
    n = len(students)
    record = StaticArray(n)
    for k in range(n):
        (name1, bday1) = students[k]
        for i in range(k):
            (name2, bday2) = record.get_at(i)
            if bday1 == bday2:
                return (name1, name2)
        record.set_at(k, (name1, bday1))
    return None

运行时间分析(running Time Analysis)

现在让我们分析我们生日匹配算法(基于包含n个学生的习题课)的运行时间。我们将假设每个名字与生日占用常量个机器字,以至于单个学生信息可以被收集,并以常量时间操作。我们逐行看下代码。除了第8、9、11行,所有行花费常量时间。第8行花费 Θ ( n ) \Theta(n) Θ(n)时间来初始化静态数组记录;第9行最多循环n次;第11行循环查看记录(record)中已存在的k个元素。因此这个算法的运行时间最多:

O ( n ) + ∑ k = 0 n − 1 ( O ( 1 ) + k ∗ O ( 1 ) ) = O ( n 2 ) O(n)+\sum_{k=0}^{n-1}(O(1)+k*O(1))=O(n^2) O(n)+k=0n1(O(1)+kO(1))=O(n2)

这是n的平方,这是多项式的。这是有效的?不,我们可以通过为我们的记录使用一个不同的数据结构来做得更好。我们将花费本课程的前半部分学习基本的数据结构,每个数据结构将被订制,用于有效地支持一系列不同操作。

渐进性练习(asymptotics Exercises)

1、 ( n 6006 ) \binom {n}{6006} (6006n)

分子: n ( n − 1 ) . . . ( n − 6005 ) n(n-1)...(n-6005) n(n1)...(n6005),n的6006次方的多项式

分母: 6006 ! 6006! 6006!,对于n来说是个常数

因此: ( n 6006 ) = Θ ( n 6006 ) \binom {n}{6006}=\Theta(n^{6006}) (6006n)=Θ(n6006)

2、 l o g 6006 ( ( l o g ( n n ) ) 2 ) log_{6006}{((log(n^{\sqrt{n}}))^2)} log6006((log(nn ))2)

公式: l o g a b = l o g a + l o g b logab=loga+logb logab=loga+logb l o g ( a b ) = b l o g a log(a^b)=bloga log(ab)=bloga l o g a b = l o g b / l o g a log_ab=logb/loga logab=logb/loga

l o g 6006 ( ( l o g ( n n ) ) 2 ) = 2 l o g 6006 l o g ( n l o g n ) = Θ ( l o g n 1 / 2 + l o g l o g n ) = Θ ( l o g n ) log_{6006}{((log(n^{\sqrt{n}}))^2)}=\dfrac{2}{log6006}log(\sqrt{n}logn)=\Theta(logn^{1/2}+loglogn)=\Theta(logn) log6006((log(nn ))2)=log60062log(n logn)=Θ(logn1/2+loglogn)=Θ(logn)

3、 2 n + 1 ∈ Θ ( 2 n ) , 2 2 n + 1 ∉ O ( 2 2 n ) 2^{n+1}\in\Theta(2^n),2^{2^{n+1}}\notin\mathcal{O}(2^{2n}) 2n+1Θ(2n)22n+1/O(22n)

2 n + 1 = 2 ∗ 2 n 2^{n+1}=2*2^n 2n+1=22n,常量乘 2 n 2^n 2n还是 2 n 2^n 2n

2 2 n + 1 = ( 2 2 n ) 2 2^{2^{n+1}}=(2^{2n})^2 22n+1=(22n)2 2 2 n 2^{2n} 22n并非一个常量

4、对于所有正数a、b, ( l o g n ) a = O ( n b ) (logn)^a=\mathcal{O}(n^b) (logn)a=O(nb)

lim ⁡ n → ∞ l o g ( n b ( l o g n ) a ) = lim ⁡ n → ∞ ( b l o g n − a l o g l o g n ) = lim ⁡ x → ∞ ( b x − a l o g x ) = ∞ \lim\limits_{n \to \infty}log(\dfrac{n^b}{(logn)^a})=\lim\limits_{n \to \infty}(blogn-aloglogn)=\lim\limits_{x \to \infty}(bx-alogx)=\infty nlimlog((logn)anb)=nlim(blognaloglogn)=xlim(bxalogx)=

5、 ( l o g n ) l o g n = Ω ( n ) (logn)^{logn}=\Omega(n) (logn)logn=Ω(n)

因为: m m = Ω ( 2 m ) m^m=\Omega(2^m) mm=Ω(2m),令 n = 2 m n=2^m n=2m,所以m=logn,得以证明

6、 ( 6 n ) ! ∉ Θ ( n ! ) (6n)!\notin\Theta(n!) (6n)!/Θ(n!),但 l o g ( ( 6 n ) ! ) ∈ Θ ( l o g ( n ! ) ) log((6n)!)\in\Theta(log(n!)) log((6n)!)Θ(log(n!))

Sterling’s近似法:

n ! = 2 π n ( n e ) n ( 1 + Θ ( 1 n ) ) n!=\sqrt{2\pi n}(\dfrac{n}{e})^n(1+\Theta(\dfrac{1}{n})) n!=2πn (en)n(1+Θ(n1))

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值