算法导论笔记-Ⅰ.Foundations

写在最前面:本文以及接下来的几篇都是我读《算法导论》时的笔记,边学边写边提交。我为了锻炼英语,看的原版,用英文写标题这种假洋鬼子的情况会经常有,还请见谅

The Role Of Algorithms in Computing

算法问题的两个特征:

  1. 绝大多数问题解决方法有备选项,找出其中最优的需要花费大量时间
  2. 有实际应用

本部分内容以插入排序为例,来演示如何解决一个排序问题

Insertion sort

input:

n个数的一个序列< a 1 , a 2 , . . . a n a_1,a_2,...a_n a1,a2,...an>

output:

输入序列的一个排序< a 1 ′ , a 2 ′ , . . . a n ′ a_1',a_2',...a_n' a1,a2,...an>,递增序列

我们所希望解决的数字称为关键词(keys),尽管我们在解决一个顺序问题,但是输入应该以含有n个元素的向量的形式

本书中,我们将通过伪代码的方式描写算法

Analyzing algorithms

分析算法主要是为了预测算法所需要的相应资源,常见的比如内存,带宽,而我们最希望计算的是算法所消耗的时间

RAM

分析一个算法前,必须有一个要使用的实现技术的模型

一种通用的单处理器计算模型–random-access machine,RAM(随机访问机),在该模型中,指令一条接一条地执行,没有并发操作。

RAM的常见指令

算数指令(arithmetic) 比如加法,减法,乘法,除法,取余,向上,下取整

数据移动指令(data movement)比如装入,存储和复制

控制指令(control)比如条件与无条件转移,子程序调用和返回

以上指令都会花费一部分时间

RAM中的数据类型只有整形和浮点型

Analysis of insertion sort

时间取决于输入,一取决于他们的规模,而且取决于他们初始的排布情况

总体来说,更为重要的还是输入的规模,通常将一个程序的“运行时间”描述成其“输入规模”的函数

输入规模(input size) 其最佳理解为输入量的项数

运行时间(the running time) 每个执行的原始操作或者“步”的数量,我们这么理解:执行每行伪代码需要常量的时间、

每行时间不一定相等,我们规定第i行的每次操作执行时间 c i c_i ci

说一千,道一万,正经八百地从插入排序开始吧

我们将给出每个语句的执行时间和执行次数,**当一个for 或while 循环按通常的方式结束时,执行测试的次数比执行循环体的次数多1(由于循环头的测试)

在这里插入图片描述

T ( n ) = c 1 n + c 2 ( n − 1 ) + c 4 ( n − 1 ) + c 5 T(n)=c_1n+c_2(n-1)+c_4(n-1)+c_5 T(n)=c1n+c2(n1)+c4(n1)+c5

Worst-case and average-case analysis

关注最坏情况的3个理由:

  1. 最坏的运行情况给了运行时间一个上界。
  2. 对于某些算法,最坏的情况时常出现。 比如检索某条数据,可是没有该数据。
  3. 平均情况往往与最坏情况大致一样差。

在判断计算效率时,我们常常忽略低阶项和最重要的项的常系数,只剩下最重要的项中的因子,如我们记插入排序具有最坏情况运行时间 θ ( n 2 ) \theta(n^2) θ(n2)

Designing algorithms

The divide-and-conquer approach

许多算法在结构上是递归的

分治法在每层递归时有三个步骤:

  1. Divide 分解原问题为若干子问题,这些子问题是原问题的规模较小的实例
  2. Conquer递归地解决这些子问题,若这些问题足够小,则直接这些子问题
  3. Combine合并这些子问题的解成原问题的解

归并排序地关键是将两个排序好的序列合并的步骤

sentinel(哨兵)

在这里插入图片描述

Analyzing divide-and -conquer algorithms

当一个算法包含一个对其自身的递归调用时,我们经常用递归方程(recurrence equation)或者递归式(recurrence)来描述它的运行时间

分治算法运行时间的递归式来自于基本模式的三个步骤。我们假设 T ( n ) T(n) T(n)是规模为n的一个问题的运行时间;若问题的规模足够小,如对某个常量 c c c,n ≤ c \leq c c ,则直接求解需要常量时间,记作 θ ( 1 ) \theta(1) θ(1),假设把原问题分解成a个子问题,每个子问题的规模是原问题的1/b,又为了求解一个规模为n/b的子问题,需要 T ( n / b ) T(n/b) T(n/b)的时间,共需要 a T ( n / b ) a T(n/b) aT(n/b)的时间来解决a个子问题,如果分解问题成子问题需要时间 D ( n ) D(n) D(n),合并子问题的解成原问题需要时间 C ( n ) C(n) C(n),那么就有递归式:
T ( n ) = { θ ( 1 ) 若 n ≤ c a T ( n / b ) + D ( n ) + C ( n ) 其 他 T(n)=\begin{cases} \theta(1) & 若n\leq c \\ a T(n/b)+D(n)+C(n)&其他 \end{cases} Tn={θ(1)aT(n/b)+D(n)+C(n)nc

Analysis of merge sort

以归并排序为例,下面我们分析建立归并排序的递归式

分解:分解步骤仅仅计算子数组的中间位置,需要常量时间,因此 D ( n ) = θ ( 1 ) D(n)=\theta(1) D(n)=θ(1)

解决:我们递归地求解两个规模均为n/2的子问题,将贡献2T(n/2)的运行时间

合并:我们已经注意到在一个具有n个元素的子数组上过程需要 θ ( n ) \theta(n) θ(n)的时间,所以C(n)= θ ( n ) \theta(n) θ(n)

有以上给出归并排序的最坏情况运行时间T(n)的递归式:
T ( n ) = { θ ( 1 ) 若 n = 1 2 T ( n / 2 ) + θ ( n ) n > 1 T(n)=\begin{cases} \theta(1) & 若n=1 \\ 2T(n/2)+\theta(n) &n>1 \end{cases} Tn={θ(1)2T(n/2)+θ(n)n=1n>1
上式的求解为 T ( n ) = θ ( n l o g n ) T(n)=\theta(nlogn) T(n)=θ(nlogn) ,为了便于理解,我们可以将上式写成如下形式:
T ( n ) = { c 若 n = 1 2 T ( n / 2 ) + c n n > 1 T(n)=\begin{cases} c & 若n=1 \\ 2T(n/2)+cn &n>1 \end{cases} Tn={c2T(n/2)+cnn=1n>1

Growth of Functions

Asymptotic notation

θ \theta θ记号

θ ( g ( n ) ) = { f ( n ) : 存 在 正 常 量 c 1 , c 2 和 n 0 , 使 得 对 所 有 n ≥ n 0 , 有 0 ≤ c 1 g ( n ) ≤ f ( n ) ≤ c 2 g ( n ) } \theta(g(n))=\{f(n):存在正常量c_1,c_2和n_0,使得对所有n\geq n_0,有0\leq c_1 g(n)\leq f(n)\leq c_2 g(n)\} θ(g(n))={f(n):c1,c2n0,使nn0,0c1g(n)f(n)c2g(n)}

换句话说,对所有 n ≥ n 0 n\geq n_0 nn0,函数 f ( n ) f(n) f(n)在一个常量因子内等于 g ( n ) g(n) g(n)。我们称 g ( n ) g(n) g(n) f ( n ) f(n) f(n)的一个渐进紧确界(asymptotiaclly tight bound)

在这里插入图片描述

θ ( g ( n ) ) \theta(g(n)) θ(g(n))的定义要求所有的 f ( n ) ∈ θ ( g ( n ) ) f(n)\in \theta(g(n)) f(n)θ(g(n))是渐进非负的(asymptotically nonnegative)

O O O记号

O ( g ( n ) ) = { f ( n ) : 存 在 正 常 量 c 和 n 0 , 使 得 对 所 有 n ≥ n 0 , 有 0 ≤ f ( n ) ≤ c g ( n ) } O(g(n))=\{f(n):存在正常量c和n_0,使得对所有n\geq n_0,有0\leq f(n)\leq c g(n)\} O(g(n))={f(n):cn0,使nn0,0f(n)cg(n)}

以此来形容一个渐进上界

注意到 f ( n ) = θ ( g ( n ) ) f(n)=\theta(g(n)) f(n)=θ(g(n))蕴含着 f ( n ) = O ( g ( n ) ) f(n)=O(g(n)) f(n)=O(g(n)) ,集合论中前者包含于后者

当我们说运行时间为 O ( n 2 ) O(n^2) O(n2)时,意思是存在一个 O ( n 2 ) O(n^2) O(n2)的函数 f ( n ) f(n) f(n),使得对于n的任意值,不管选择什么特定的规模为n的输入,其运行时间的上界都是 f ( n ) f(n) f(n)。也就是说最坏情况运行时间为 O ( n 2 ) O(n^2) O(n2)

Ω \Omega Ω记号

渐进下界,定义同理于 O O O记号

定理3.1:对任意两个函数 f ( n ) f(n) f(n) g ( n ) g(n) g(n),我们有 f ( n ) = θ ( g ( n ) ) f(n)=\theta(g(n)) f(n)=θ(g(n)),当且仅当 f ( n ) = O ( g ( n ) ) f(n)=O(g(n)) f(n)=O(g(n)) f ( n ) = θ ( g ( n ) ) f(n)=\theta(g(n)) f(n)=θ(g(n))

举个例子,插入排序的运行时间是介于 Ω ( n ) \Omega(n) Ω(n) O ( n 2 ) O(n^2) O(n2)

等式和不等式中的渐进记号

当渐进记号独立于等式(或不等式)的右边(即不在一个更大的公式内)时,如在n= O ( n 2 ) O(n^2) O(n2)中,我们已经定义等号意指集合的成员关系:n ∈ O ( n 2 ) \in O(n^2) O(n2),然而,一般来说,当渐进记号出现在某个公式中时,我们将其解释为代表某个我们不关注名称的匿名函数。例如,公式 2 n 2 + 3 n + 1 = 2 n 2 + θ ( n ) 2n^2+3n+1=2n^2+\theta(n) 2n2+3n+1=2n2+θ(n),其中 f ( n ) f(n) f(n)是集合 θ ( n ) \theta(n) θ(n)中的某个函数。本例中,假设 f ( n ) = 3 n + 1 f(n)=3n+1 f(n)=3n+1,该函数确实在 θ ( n ) \theta(n) θ(n)

同时使用这种方式可以消除一个等式中无关紧要的细节与混乱,比如我们可以把归并排序的最坏情况运行时间表示为递归式
T ( n ) = 2 T ( n / 2 ) + θ ( n ) T(n)=2T(n/2)+\theta(n) T(n)=2T(n/2)+θ(n)
一个表达式中匿名函数 θ ( ) \theta() θ()的数目可以理解为等于渐近记号出现的次数。例如,在表达式 ∑ O ( i ) \sum O(i) O(i)中,只有一个匿名函数(一个i的函数)。

我们用以下规则来解释等式:无论怎样选择等号左边的匿名函数,总有一种办法来选择等号右边的匿名函数使等式使等式成立

比如:
2 n 2 + θ ( n ) = θ ( n 2 ) 2n^2+\theta(n)=\theta(n^2) 2n2+θ(n)=θ(n2)
对任意的函数 g ( n ) ∈ θ ( n ) g(n)\in\theta(n) g(n)θ(n),存在某个函数 h ( n ) ∈ θ ( n 2 ) h(n)\in \theta(n^2) h(n)θ(n2) ,使得对于所有的n有 2 n 2 + θ ( n ) = θ ( n 2 ) 2n^2+\theta(n)=\theta(n^2) 2n2+θ(n)=θ(n2)

o o o记号

我们用 o o o记号来表示一个非渐进紧确的上界,定义如下:
o ( g ( n ) ) = { f ( n ) : 对 任 意 正 常 量 c > 0 存 在 常 量 n 0 > 0 , , 使 得 对 所 有 n ≥ n 0 , 有 0 ≤ f ( n ) ≤ c g ( n ) } o(g(n))=\{f(n):对任意正常量c>0存在常量n_0>0,,使得对所有n\geq n_0,有0\leq f(n)\leq c g(n)\} o(g(n))={f(n):c>0n0>0,使nn0,0f(n)cg(n)}
例如 2 n = o ( n 2 ) 2n=o(n^2) 2n=o(n2),但是 2 n 2 ! = O ( n 2 ) 2n^2!=O(n^2) 2n2!=O(n2)

直观上当n趋于无穷时, f ( n ) f(n) f(n)对于 g ( n ) g(n) g(n)微不足道

ω \omega ω记号

定义的一种方式如下:
f ( n ) ∈ ω ( g ( n ) ) 当 且 仅 当 g ( n ) ∈ o ( f ( n ) ) f(n)\in\omega(g(n))当且仅当g(n)\in o(f(n)) f(n)ω(g(n))g(n)o(f(n))

ω ( g ( n ) ) = { f ( n ) : 对 任 意 正 常 量 c > 0 存 在 常 量 n 0 > 0 , , 使 得 对 所 有 n ≥ n 0 , 有 0 ≤ c g ( n ) ≤ f ( n ) } \omega(g(n))=\{f(n):对任意正常量c>0存在常量n_0>0,,使得对所有n\geq n_0,有0\leq c g(n)\leq f(n) \} ω(g(n))={f(n):c>0n0>0,使nn0,0cg(n)f(n)}

同理类似于上面(或者说对偶于上面)当n趋近于无穷时, f ( n ) f(n) f(n)对于 g ( n ) g(n) g(n)来说变得任意大

比较各种函数

可以做如下类比
f ( n ) = O ( g ( n ) ) 类 似 于 a ≤ b f ( n ) = Ω ( g ( n ) ) 类 似 于 a ≥ b f ( n ) = θ ( g ( n ) ) 类 似 于 a = b f ( n ) = o ( g ( n ) ) 类 似 于 a < b f ( n ) = ω ( g ( n ) ) 类 似 于 a > b f(n)=O(g(n))类似于a\leq b\\ f(n)=\Omega(g(n)) 类似于a\geq b\\ f(n)=\theta(g(n)) 类似于a=b\\ f(n)=o(g(n))类似于a<b\\ f(n)=\omega (g(n)) 类似于a>b f(n)=O(g(n))abf(n)=Ω(g(n))abf(n)=θ(g(n))a=bf(n)=o(g(n))a<bf(n)=ω(g(n))a>b
对于任意两个实数都可以比较,但不是所有函数都可以渐进比较,意思是,对两个函数 f ( n ) f(n) f(n) g ( n ) g(n) g(n),也许 f ( n ) = O ( g ( n ) ) f(n)=O(g(n)) f(n)=O(g(n)) f ( n ) = Ω ( g ( n ) ) f(n)=\Omega(g(n)) f(n)=Ω(g(n))都不成立。例如,我们不能使用渐进记号来比较函数n和 n 1 + s i n n n^{1+sinn} n1+sinn

Standard notations and common functions

单调性,指数,对数啥的都会就不提了

向下和向上取整(Flooes and ceilings)

x − 1 < ⌊ x ⌋ ≤ x ≤ ⌈ x ⌉ < x + 1 x-1<\lfloor x \rfloor\leq x\leq \lceil x \rceil<x+1 x1<xxx<x+1

对任意整数n
⌊ n / 2 ⌋ + ⌈ n / 2 ⌉ = n \lfloor n/2 \rfloor+\lceil n/2 \rceil=n n/2+n/2=n
对任意整数 x ≥ 0 x\geq0 x0和整数a,b>0

在这里插入图片描述

横运算

对任意整数a和任意正整数n,a mod n的值就是商a/n的余数
a   m o d   n = a   −   n ⌊ a / n ⌋ a\ mod\ n=a\ -\ n\lfloor a/n\rfloor a mod n=a  na/n
结果有
0 ≤ a   m o d   n < n 0\leq a\ mod\ n < n 0a mod n<n

Divide-and-Conquer

让我们来回顾之前提到的归并排序

Divide the problem into a number of subproblems that are smaller instances of the same problem.

Conquer the subproblems by solving them recursively .If the subproblem sizes are small enough,however,just solve the subproblems in a straightforward manner.

Combine the solutions to the subproblems into the solution for the original problem.

当子问题足够大,需要递归求解时,我们称之为递归情况(recursive case),当子问题足够小时,我们进入基本情况。

求递归式的三种方法
  1. 代入法

    我们猜测一个界,然后用数学归纳法来证明这个界是正确的

  2. 递归树法

    将递归式转换为一棵树,其结点表示不同层次的递归调用产生的代价,然后采用边界和技术来求解递归式

  3. 主方法

    可求解形如下面公式的递归式的界:
    T ( n ) = a T ( n / b ) + f ( n ) T(n)=aT(n/b)+f(n) T(n)=aT(n/b)+f(n)

其中a$\geq 1 , b > 1 , 1,b>1, 1,b>1f(n) 是 一 个 给 定 的 函 数 。 这 种 形 式 的 递 归 式 十 分 常 见 , 它 刻 画 了 这 样 一 个 分 治 算 法 : 生 成 a 个 子 问 题 , 每 个 子 问 题 的 规 模 是 原 问 题 规 模 的 1 / b , 分 解 和 合 并 步 骤 总 共 花 费 时 间 为 是一个给定的函数。这种形式的递归式十分常见,它刻画了这样一个分治算法:生成a个子问题,每个子问题的规模是原问题规模的1/b,分解和合并步骤总共花费时间为 a1/bf(n)$.

常见细节分析

我们常常忽略一些细节,比如对n个元素调用归并排序,当n为奇数,则两个子问题的规模分别为 ⌊ n / 2 ⌋ \lfloor n/2\rfloor n/2 ⌈ n / 2 ⌉ \lceil n/2\rceil n/2,由此,我们写出归并排序最坏运行时间的准确的递归式为:
T ( n ) = { θ ( 1 ) 若 n = 1 T ( ⌈ n / 2 ⌉ ) + T ( ⌊ n / 2 ⌋ ) + θ ( n ) 若 n > 1 T(n)=\begin{cases} \theta(1) & 若n=1 \\ T(\lceil n/2 \rceil)+T(\lfloor n/2 \rfloor)+\theta(n) & 若n>1 \end{cases} T(n)={θ(1)T(n/2)+T(n/2)+θ(n)n=1n>1
边界条件是一个值得重视的细节

最大子数组问题

比如买股票追求低买高卖,如果有n天的股价我们抽一天买再抽一天卖,暴力求解不用多说,那么n天中共有 ( n 2 ) \tbinom{n}{2} (2n)种组合,即复杂度为 θ ( n 2 ) \theta(n^2) θ(n2)而处理每对日期所花费的时间至少也是常量,因此,这种方法的运行时间为 Ω ( n 2 ) \Omega(n^2) Ω(n2)

为找到时间复杂度为 o ( n 2 ) o(n^2) o(n2)的算法,我们可以从每日的价格变化出发,将每天的价格较前一天的变化,单列一个数组,并找到这个数组的最大子数组(maximum subarray)

顺便一提,显然是数组里有负数接下来的研究才有意义,不然的话,等到最后一天就好了

分治求解

我们将在[low,high]中寻找最大子数组,将其划分为两个子数组[low,mid]和[mid+1,high]

则任意连续子数组必然是以下三种情况之一:

  • 完全位于子数组A[low,mid]中,因此 l o w ≤ i ≤ j ≤ m i d low\leq i\leq j \leq mid lowijmid
  • 完全位于子数组A[mid+1,high]中,因此 m i d < i ≤ j ≤ h i g h mid< i\leq j \leq high mid<ijhigh
  • 跨越了中点,因此 l o w ≤ i ≤ m i d < j ≤ h i g h low\leq i\leq mid<j\leq high lowimid<jhigh

我们可以递归地求解 A [ l o w , m i d ] A[low,mid] A[low,mid] A [ m i d + 1 , h i g h ] A[mid+1,high] A[mid+1,high]的最大子数组,因为这两个子数组仍然是最大子数组问题,只是规模更小。

因此,剩下的工作就只有寻找跨越中点的最大数组,然后在三种情况中选取最大者

找出跨越中点的最大子数组并不难,因为跨越重点数组[i,j]可以分成两个子数组A[i,mid]和A[mid+1,j]组成,之后再将其合并即可

因此伪代码如下:

在这里插入图片描述

这玩意挺简单就不废话了,注意花费的时间位 θ ( n ) \theta(n) θ(n),毕竟遍历整个数组

那么总体就是

在这里插入图片描述

分治算法的分析

当n=1时,最基本的情况下,1,2行花费常量时间
T ( 1 ) = θ ( 1 ) T(1)=\theta(1) T(1)=θ(1)
当n>1时,即为递归情况1和3行花费常量时间,4,5行均为 T ( n / 2 ) T(n/2) T(n/2),第六行则为 θ ( n ) \theta(n) θ(n),7到11行花费时间为 θ ( 1 ) \theta(1) θ(1)

有以上,我们得出
T ( n ) = { θ ( 1 ) 若 n = 1 2 T ( n / 2 ) + θ ( n ) 若 n > 1 T(n)=\begin{cases} \theta(1) & 若n=1 \\ 2T( n/2)+\theta(n) & 若n>1 \end{cases} T(n)={θ(1)2T(n/2)+θ(n)n=1n>1
得到解为 T ( n ) = θ ( n l o g n ) T(n)=\theta(nlogn) T(n)=θ(nlogn)

Strassen’s algorithm for matrix multiplication

正如矩阵乘法要遍历 n 2 n^2 n2个矩阵元素,每个元素是n个值的和

在这里插入图片描述

由于三重循环for ,每重循环都会花费n次,那么矩阵乘法总共花费 θ ( n 3 ) \theta(n^3) θ(n3),而本节有个更为快速的算法。

A simple divide-and-conquer algorithm

我们假设所有的n × \times ×n矩阵中,n都是2的幂,因为在每个分解步骤中,n × \times ×n矩阵可以被划分为4个n/2 × \times ×n/2的子矩阵

在这里插入图片描述

因此可以将公式C=A ⋅ \cdot B改写为:

在这里插入图片描述

那么就将等价于

在这里插入图片描述

,根据以上计算,我们可以得到一个直接的递归分治算法:

在这里插入图片描述

继续递归分析,当n=1时,我们只需进行一次标量乘法,因此
T ( 1 ) = θ ( 1 ) T(1)=\theta(1) T(1)=θ(1)
当n>1时是递归情况,在第5行使用下标分解矩阵花费 θ ( 1 ) \theta(1) θ(1)时间。第6~9行,我们共8次递归调用,花费时间为8$ T(n/2) , 同 时 , 还 需 要 4 次 矩 阵 加 法 , 每 个 矩 阵 含 ,同时,还需要4次矩阵加法,每个矩阵含 4n2/4$个元素,因此矩阵加法花费$\theta(n2)$的时间。

由以上,递归情况的总时间为分解时间,递归调用时间及矩阵加法时间之和:
T ( n ) = θ ( 1 ) + 8 T ( n / 2 ) + θ ( n 2 ) = 8 T ( n / 2 ) + θ ( n 2 ) T(n)=\theta(1)+8T(n/2)+\theta(n^2)=8T(n/2)+\theta(n^2) T(n)=θ(1)+8T(n/2)+θ(n2)=8T(n/2)+θ(n2)
由以上,得到解为 T ( n ) = θ ( n 3 ) T(n)=\theta(n^3) T(n)=θ(n3)

如果矩阵的n不是2的幂怎么办?

Strassen算法

这个算法讲的也不太清楚,挺不容易懂的,建议自己再查查

  1. 将输入矩阵A,B和输出矩阵C分解为n/2 × \times ×n/2的子矩阵,采用下标计算方法,将此步骤花费 θ ( 1 ) \theta(1) θ(1)时间,与SQUARE-MATRIX–MULTIPLY-RECURSIVE相同。

  2. 创建10个n/2 × \times ×n/2的矩阵 S 1 , S 2 , . . . , S 10 S_1,S_2,...,S_{10} S1,S2,...,S10,每个矩阵保存步骤1中创建的两个子矩阵的和或差。花费时间为 θ ( n 2 ) \theta(n^2) θ(n2)

  3. 用步骤1中创建的子矩阵和步骤2中创建的10个矩阵,递归地计算7个矩阵积 P 1 , P 2 , . . . , P 7 P_1,P_2,...,P_7 P1,P2,...,P7。每个矩阵 P i P_i Pi都是n/2 × \times ×n/2的。

  4. 通过 P i P_i Pi矩阵的不同组合进行加减运算,计算出结果矩阵C的子矩阵 C 11 , C 12 , C 21 , C 22 C_{11},C_{12},C_{21},C_{22} C11,C12,C21,C22.花费时间 θ ( n 2 ) \theta(n^2) θ(n2)

    用了该算法的运行时间T(n)的递归式:
    T ( n ) = { θ ( 1 ) 若 n = 1 7 T ( n / 2 ) + θ ( n 2 ) 若 n > 1 T(n)=\begin{cases} \theta(1) & 若n=1 \\ 7T(n/2 )+\theta(n^2) & 若n>1 \end{cases} T(n)={θ(1)7T(n/2)+θ(n2)n=1n>1

接下来,我们来介绍这种算法的细节:

在这里插入图片描述

由于必须进行10此n/2 × \times ×n/2矩阵的加减法,因此,该步骤花费时间 θ ( n 2 ) \theta(n^2) θ(n2)时间。

在步骤3中,递归地进行7次n/2 × \times ×n/2矩阵的乘法:

在这里插入图片描述

注意只有中间一例需要计算。

最后

在这里插入图片描述

strassen算法详解

既然没看明白是怎么算的,索性上知乎找了全套解析,如下:

首先,最普通的矩阵乘法,对矩阵 A ⋅ B A\cdot B AB,其中A为 m × p m\times p m×p矩阵,B为 p × n p\times n p×n矩阵,若 m = n = p = N m=n=p=N m=n=p=N则算法的复杂度为 θ ( n 3 ) \theta(n^3) θ(n3)

按照上面的将矩阵都分解为 n / 2 × n / 2 n/2 \times n/2 n/2×n/2矩阵,八次乘法和所有元素对应加和的加法,有
T ( n ) = θ ( 1 ) + 8 T ( n / 2 ) + θ ( n 2 ) = 8 T ( n / 2 ) + θ ( n 2 ) T(n)=\theta(1)+8T(n/2)+\theta(n^2)=8T(n/2)+\theta(n^2) T(n)=θ(1)+8T(n/2)+θ(n2)=8T(n/2)+θ(n2)
其实说白了就是看所有的计算过程,普通的进行了8次乘法,strassen进行了7次乘法,而对应结果矩阵有多少元素就有最少多少次加法,这些导致了结果的不同

strassen算法的不足之处

这也并不是绝对好的,显然strssen需要创立更多的动态数组,会消耗大量内存,因此当数组计算量足够大时,不如直接计算的方法

The substitution method for solving recurrences

代入法求解递归式主要分两步:

  1. 猜测解的形式
  2. 用数学归纳法求出解的常数,并证明解是对的

对于要讨论的大多数递归式选择合适的 n 0 , n ≥ n 0 n_0,n\ge n_0 n0,nn0和足够大的常数C,扩展边界条件使归纳假设对较小的n成立,是一种简单的方法

微妙的细节:

归纳证明失败有可能是因为做出的归纳假设不够强,无法证出准确的界。当遇到这种障碍时,如果修改猜测,将它减去一个低阶的项,数学证明常常能顺利进行。

在这里插入图片描述

避免陷阱

在这里插入图片描述

改变常量

有时,一个小的代数运算可以将一个未知的递归式变成熟悉的形式:
T ( n ) = 2 T ( ⌊ n ⌋ ) + l g n T(n)=2T(\lfloor\sqrt n\rfloor)+lgn T(n)=2T(n )+lgn
看起来很困难但是我们令 S ( m ) = T ( 2 m ) S(m)=T(2^m) S(m)=T(2m)得到新的递归式:
S ( m ) = 2 S ( m / 2 ) + m S(m)=2S(m/2)+m S(m)=2S(m/2)+m
因此
S ( m ) = O ( m l g m ) S(m)=O(mlgm) S(m)=O(mlgm)
得到
T ( n ) = T ( 2 m ) = S ( m ) = O ( m l g m ) = O ( l g n l g l g n ) T(n)=T(2^m)=S(m)=O(mlgm)=O(lgnlglgn) T(n)=T(2m)=S(m)=O(mlgm)=O(lgnlglgn)

The recursion-tree method for solving recurrences

在递归树中,每个结点表示一个单一子问题的代价,子问题对应某次递归函数调用。我们将树中每层中的代价求和,得到每层代价,然后将所有层的代价求和,得到所有层次的递归调用的总代价

我们以递归式 T ( n ) = 3 T ( ⌊ n / 4 ⌋ ) + θ ( n 2 ) T(n)=3T(\lfloor n/4\rfloor)+\theta(n^2) T(n)=3T(n/4)+θ(n2)为例,我们知道舍入对求解递归式通常没有影响,因此可以为递归式 T ( n ) = 3 T ( ⌊ n / 4 ⌋ ) + c n 2 T(n)=3T(\lfloor n/4\rfloor)+cn^2 T(n)=3T(n/4)+cn2创建一棵递归树,其中已将渐进符号改写为隐含的常数系数c>0

接下来我们构造递归树,为方便起见,我们假定n是4的幂,这样所有子问题的规模均为正数,构建如下图所示的递归树。根节点中的 c n 2 cn^2 cn2项表示递归调用顶层的代价,根的三棵子树表示规模为n/4的子问题所产生的代价,如下图:

在这里插入图片描述

上述递归树,其高度为 l o g 4 n log_4n log4n(有 l o g 4 n + 1 log_4n+1 log4n+1层)

因为深度为i的结点对应规模为 n / 4 i n/4^i n/4i的子问题。因此,当 n / 4 i = 1 n/4^i=1 n/4i=1,求得 i = l o g 4 n i=log_4n i=log4n,因此有 l o g 4 n + 1 log_4n+1 log4n+1

然后是每一层的代价,每层的节点数是上一层的3倍,因此深度为i的结点的节点数为 3 i 3^i 3i。因为每一层子问题的规模都是上一层的1/4因此对深度为i的每个结点的代价为 c ( n / 4 i ) 2 c(n/4^i)^2 c(n/4i)2,因此对深度为i的所有结点的总代价为 3 i c ( n / 4 i ) 2 = ( 3 / 16 ) i c n 2 3^ic(n/4^i)^2=(3/16)^icn^2 3ic(n/4i)2=(3/16)icn2,树的最底层深度为 l o g 4 n log_4n log4n可求得节点数,而每个结点的代价为T(1),总代价为 n l o g 4 3 T ( 1 ) n^{log_43}T(1) nlog43T(1),即 θ ( n l o g 4 3 ) \theta(n^{log_43}) θ(nlog43).

最后求整个树的代价之和:

在这里插入图片描述

还可以进一步回退

在这里插入图片描述

这里总结一下,目前见到的为了求出递归式做出的妥协

  1. 假设都是x的幂(在无限的情况下对数字做出某些要求)

  2. n趋于无穷变真无穷

  3. 回退操作

    在这里插入图片描述

用递归树找出一个差不多的式子来,使用代入法,这个中间过程我们可以忍受一些不精确

The master method for solving recurrences

主方法为下列问题提供了一个菜谱式的算法
T ( n ) = a T ( n / b ) + f ( n ) T(n)=aT(n/b)+f(n) T(n)=aT(n/b)+f(n)
其中 a ≥ 1 , b > 1 a\geq1,b>1 a1,b>1,且 a , b a,b a,b为常数,而 f ( n ) f(n) f(n)是渐进正函数

主函数需要的记住3种例子

其实上式描写了这样一种算法的运行时间:

将一个规模为n的算法分解为a个子问题,每个子问题的规模为 n / b n/b n/b,a个子问题由都由递归求解,每个的时间都是 T ( n / b ) T(n/b) T(n/b),函数 f ( n ) f(n) f(n)包含了分解问题和合并子问题结果的总花费。

从技术方面的正确性来说,这个递归式并不是很好,因为 n / b n/b n/b不一定是整数。但将a项 T ( n / b ) T(n/b) T(n/b)都替换为 T ( ⌊ n / b ⌋ ) T(\lfloor n/b \rfloor) T(n/b) T ( ⌈ n / b ⌉ ) T(\lceil n/b \rceil) T(n/b)并不会影响递归式的渐进性质。因此,我们会经常性的忽略舍入,这很方便。

主定理

a ≥ 1 , b > 1 a\geq1,b>1 a1,b>1,且 a , b a,b a,b为常数, f ( n ) f(n) f(n)是一个函数, T ( n ) T(n) T(n)是定义在非负整数上的递归式:
T ( n ) = a T ( n / b ) + f ( n ) T(n)=aT(n/b)+f(n) T(n)=aT(n/b)+f(n)
其中我们将 n / b n/b n/b解释为 ⌊ n / b ⌋ \lfloor n/b \rfloor n/b ⌈ n / b ⌉ \lceil n/b \rceil n/b。那么 T ( n ) T(n) T(n)有如下渐进界:;

  1. 若对某个常数 ε > 0 \varepsilon>0 ε>0 f ( n ) = O ( n l o g b a − ε ) f(n)=O(n^{log_b{a-\varepsilon}}) f(n)=O(nlogbaε),则 T ( n ) = θ ( n l o g b a ) T(n)=\theta(n^{log_ba}) T(n)=θ(nlogba)
  2. f ( n ) = θ ( n l o g b a ) f(n)=\theta(n^{log_ba}) f(n)=θ(nlogba),则 T ( n ) = θ ( n l o g b a l g n ) T(n)=\theta(n^{log_ba}lgn) T(n)=θ(nlogbalgn)
  3. 若对某个常数 ε > 0 \varepsilon>0 ε>0 f ( n ) = Ω ( n l o g b a + ε ) f(n)=\Omega(n^{log_ba+\varepsilon}) f(n)=Ω(nlogba+ε),且对某个常数 c < 1 c<1 c<1和所有足够大的 n n n a f ( n / b ) ≤ c f ( n ) af(n/b)\leq cf(n) af(n/b)cf(n),则 T ( n ) = θ ( f ( n ) ) T(n)=\theta(f(n)) T(n)=θ(f(n))

我们来看一下这几个式子,其实本质上是将函数 f ( n ) f(n) f(n) n l o g b a n^{log_ba} nlogba进行比较,这两个函数谁更大,决定了递归式的解,若 f ( n ) f(n) f(n)更大,则为情况3,若后者更大,则为情况1.若两个函数大小相当,则乘上一个对数因子,解为 T ( n ) = θ ( n l o g b a l g n ) = θ ( f ( n ) l g n ) T(n)=\theta(n^{log_ba}lgn)=\theta(f(n)lgn) T(n)=θ(nlogbalgn)=θ(f(n)lgn)

其中还有一些细节需要我们注意,第一种情况中并非只要求 f ( n ) f(n) f(n)小于 n l o g b a n^{log_ba} nlogba就行,必须是多项式意义上的小于,意即渐进小于,要相差一个因子 n ε n^{\varepsilon} nε,其中 ε \varepsilon ε是大于0的常数;第三种情况同样,多项式意义上的大于,同时要满足正则条件 a f ( n / b ) ≤ c f ( n ) af(n/b)\leq cf(n) af(n/b)cf(n)

请注意,这三种情况并未覆盖 f ( n ) f(n) f(n)的所有可能性若落入情况1和2,2和3的间隙中,或情况3却不满足正则条件,则不能使用主方法。

Probabilistic Analysis and Randomized Algorithms

如果一个算法的行为不仅由输入决定,而且也由随机数生成器产生的数值决定,则称这个算法是随机的。我们将假设有一个随机数生成器RANDOM,调用RANDOM(a,b)将返回一个介于a和b之间的整数,并且每个整数出现的情况等可能。

我们将一个随机算法的运行时间称为期望运行时间

Indicator random variables

假设我们有样本空间 S S S和事件 A A A,那么事件A对应的指示器随机变量I{A}定义为:
I ( A ) = { 1 如 果 A 发 生 0 如 果 A 不 发 生 I(A)=\begin{cases} 1 & 如果A发生 \\ 0 & 如果A不发生 \\ \end{cases} I(A)={10AA
我是这么理解的:

I ( A ) I(A) I(A)就是对事件A的一种赋值方式或者对A的解释,其对应变量 X A X_A XA的值, I ( A ) I(A) I(A)等价于 X H X_H XH X H X_H XH也认作是对于事件的解释

引理5.1:给定一个样本空间S和S中的一个事件A,设

X A = X_A= XA= I I I{ A A A},那么 E [ X A ] = P r { A } E[X_A]=Pr \{A\} E[XA]=Pr{A}

引理5.2:假设应聘者以随机次序出现,算法HIRE-ASSISTANT总的雇佣费用平均情况下为 O ( c b l n n ) O(c_blnn) O(cblnn)

这里重点说一下为什么是lnn,这里我是这么理解的:在每一次挑选的过程中所有人被选中的结果是等可能的,但不同次挑选时,所得的概率是不同的,我们没有站在宏观上挑选所有人,第一次一定雇佣,第二次的人,可能比之前好也可能坏,概率是1/2,第三次是1/3,注意到每个概率取决于那一次挑选,因此总和为lnn.

随机算法

在上面,我们通过对m的分布分析出平均情况,但是在很多时候,我们是无法得知输入分布信息的。但我们也许可以设计一个随机算法。

针对上面的雇用问题,我们可以在算法运行前先随机地排列应聘者,以加强所有的排列都是等可能出现的。

在聘用助理问题中,我们曾经断言雇佣一个新的办公助理的期望次数大约是 l n n lnn lnn,而雇佣一个新办公助理的次数将因为输入的不同而不同,而且依赖于各个应聘者的排名

在这里插入图片描述

在这里插入图片描述

第二个算法相较于第一个只做了一个简单的改进,即添加了一个随机算法,讨论雇佣费用时,对1是平均雇佣费用(因为每次输入的顺序是特定的),对2是期望雇佣费用

随机排列数组

在这里插入图片描述

第四行选取一个在1~ n 2 n^2 n2之间的随机数,是为了让P中所有优先级尽可能唯一。

要证明一个排列是均匀随机排列,只要证明对每个元素A[i],它排在位置j的概率是1/n,其实不然,这是个弱条件

产生随机排列的一个更好方法是原址排列给定数组,该过程将在 O ( n ) O(n) O(n)事件内完成,在进行第i次迭代时,元素A[i]是从元素A[i]到A[n]中随机选取的,第i次迭代后,A[i]不再改变

在这里插入图片描述

我们将证明上式能产生一个均匀随机排列。一个具有n个元素的k排列是包含这n个元素中的k个元素的序列,并且不重复。一共有n!/(n-k)!种可能的k排列。

引理5.5:上述过程可以计算出一个均匀随机排列

在这里插入图片描述
第一章内容,堂堂完结。后续的课后题笔记应该会进一步更新。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值