写在最前面:本文以及接下来的几篇都是我读《算法导论》时的笔记,边学边写边提交。我为了锻炼英语,看的原版,用英文写标题这种假洋鬼子的情况会经常有,还请见谅
文章目录
The Role Of Algorithms in Computing
算法问题的两个特征:
- 绝大多数问题解决方法有备选项,找出其中最优的需要花费大量时间
- 有实际应用
本部分内容以插入排序为例,来演示如何解决一个排序问题
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(n−1)+c4(n−1)+c5
Worst-case and average-case analysis
关注最坏情况的3个理由:
- 最坏的运行情况给了运行时间一个上界。
- 对于某些算法,最坏的情况时常出现。 比如检索某条数据,可是没有该数据。
- 平均情况往往与最坏情况大致一样差。
在判断计算效率时,我们常常忽略低阶项和最重要的项的常系数,只剩下最重要的项中的因子,如我们记插入排序具有最坏情况运行时间 θ ( n 2 ) \theta(n^2) θ(n2)
Designing algorithms
The divide-and-conquer approach
许多算法在结构上是递归的
分治法在每层递归时有三个步骤:
- Divide 分解原问题为若干子问题,这些子问题是原问题的规模较小的实例
- Conquer递归地解决这些子问题,若这些问题足够小,则直接这些子问题
- 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}
T(n)={θ(1)aT(n/b)+D(n)+C(n)若n≤c其他
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}
T(n)={θ(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}
T(n)={c2T(n/2)+cn若n=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,c2和n0,使得对所有n≥n0,有0≤c1g(n)≤f(n)≤c2g(n)}
换句话说,对所有 n ≥ n 0 n\geq n_0 n≥n0,函数 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):存在正常量c和n0,使得对所有n≥n0,有0≤f(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>0存在常量n0>0,,使得对所有n≥n0,有0≤f(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>0存在常量n0>0,,使得对所有n≥n0,有0≤cg(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))类似于a≤bf(n)=Ω(g(n))类似于a≥bf(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 x−1<⌊x⌋≤x≤⌈x⌉<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
x≥0和整数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 − n⌊a/n⌋
结果有
0
≤
a
m
o
d
n
<
n
0\leq a\ mod\ n < n
0≤a 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),当子问题足够小时,我们进入基本情况。
求递归式的三种方法
-
代入法
我们猜测一个界,然后用数学归纳法来证明这个界是正确的
-
递归树法
将递归式转换为一棵树,其结点表示不同层次的递归调用产生的代价,然后采用边界和技术来求解递归式
-
主方法
可求解形如下面公式的递归式的界:
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>1,f(n) 是 一 个 给 定 的 函 数 。 这 种 形 式 的 递 归 式 十 分 常 见 , 它 刻 画 了 这 样 一 个 分 治 算 法 : 生 成 a 个 子 问 题 , 每 个 子 问 题 的 规 模 是 原 问 题 规 模 的 1 / b , 分 解 和 合 并 步 骤 总 共 花 费 时 间 为 是一个给定的函数。这种形式的递归式十分常见,它刻画了这样一个分治算法:生成a个子问题,每个子问题的规模是原问题规模的1/b,分解和合并步骤总共花费时间为 是一个给定的函数。这种形式的递归式十分常见,它刻画了这样一个分治算法:生成a个子问题,每个子问题的规模是原问题规模的1/b,分解和合并步骤总共花费时间为f(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=1若n>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 low≤i≤j≤mid
- 完全位于子数组A[mid+1,high]中,因此 m i d < i ≤ j ≤ h i g h mid< i\leq j \leq high mid<i≤j≤high
- 跨越了中点,因此 l o w ≤ i ≤ m i d < j ≤ h i g h low\leq i\leq mid<j\leq high low≤i≤mid<j≤high
我们可以递归地求解 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=1若n>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次矩阵加法,每个矩阵含
,同时,还需要4次矩阵加法,每个矩阵含n2/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算法
这个算法讲的也不太清楚,挺不容易懂的,建议自己再查查
-
将输入矩阵A,B和输出矩阵C分解为n/2 × \times ×n/2的子矩阵,采用下标计算方法,将此步骤花费 θ ( 1 ) \theta(1) θ(1)时间,与SQUARE-MATRIX–MULTIPLY-RECURSIVE相同。
-
创建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)。
-
用步骤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的。
-
通过 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=1若n>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 A⋅B,其中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
代入法求解递归式主要分两步:
- 猜测解的形式
- 用数学归纳法求出解的常数,并证明解是对的
对于要讨论的大多数递归式选择合适的 n 0 , n ≥ n 0 n_0,n\ge n_0 n0,n≥n0和足够大的常数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).
最后求整个树的代价之和:
还可以进一步回退
这里总结一下,目前见到的为了求出递归式做出的妥协
-
假设都是x的幂(在无限的情况下对数字做出某些要求)
-
n趋于无穷变真无穷
-
回退操作
用递归树找出一个差不多的式子来,使用代入法,这个中间过程我们可以忍受一些不精确
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
a≥1,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
a≥1,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)有如下渐进界:;
- 若对某个常数 ε > 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)。
- 若 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)
- 若对某个常数 ε > 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)={10如果A发生如果A不发生
我是这么理解的:
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:上述过程可以计算出一个均匀随机排列
第一章内容,堂堂完结。后续的课后题笔记应该会进一步更新。