算法复习
第一章 算法引论
算法:
-
算法的特性:输入(0个或多个)、输出(至少一个)、确定性(无歧义)、有限性
-
描述方式:自然语言、图形、程序设计语言、伪代码
-
三要素:操作、控制结构、数据结构
算法设计的一般过程:
- 理解问题
- 精确解或近似解选择数据结构算法设计策略
- 设计算法
- 证明正确性
- 分析算法
- 设计程序
时间复杂度?
时间复杂度的比较
第二章 递归与分治
分治法能够解决的问题常见的有:二分查找、循环日程赛、快速排序、线性时间选择、棋盘覆盖等。
分治法的基本思想、基本要素与求解步骤
分治法求解n个元素排序(也用到了递归,在递归中不断进行分解,在分解完成后返回在不断的进行合并)
思想:递归地把待排序序列分解为若干子序列并进行排序(治),再把已排序的子序列合并为整体有序序列,最终实现全序列的有序:
- 分解:将待排序元素分成大小大致相同的两个子序列
- 求解子问题:用合并排序法分别对两个子序列递归进行排序
- 合并:将排好序的有序子序列进行合并
对此时间复杂度进行进行分析:
-
当n=1时,T(n)=O(1).
-
当n>1时,将时间T如下分解:
- 分解:这一步需要需要常量时间O(1)。
- 解决子问题:递归求解两个规模为n/2的子问题,所需时间为2T(n/2)。
- 合并:Merge算法可在O(n)时间内完成。
-
有此可以得到合并排序算法运行时间T(n)的递归形式
T ( n ) = { O ( 1 ) n = 1 2 T ( n / 2 ) + O ( n ) n > 1 T(n)=\left\{ \begin{array}{lr} O(1) & n=1 \\ 2T(n/2)+O(n) & n>1 \end{array} \right. T(n)={O(1)2T(n/2)+O(n)n=1n>1
空间复杂度为:O(n)
分治策略的基本思想*:
将一个难以直接解决的大问题,分割成一些规模较小的相同问题,以便各个击破,分而治之。
可以采用分治法求解的问题的基本特征:
- 问题的规模缩小到一定程度就可以容易解决。
- 问题可以分解为若干个规模较小的相同子问题。
- 问题所分解出的各个子问题是相互独立的。
- 问题分解出的子问题的解可以合并为原问题的解。
分治法的基本步骤:
用伪代码解释:
DIVIDE-AND-CONQUER(P)
if(|P|<= n0) //问题规模足够小,n0为规模阈值
then SOLVE(P); //解决小问题
subs = DIVIDE(P) //分解为子问题,subs为子问题集
for i = 1 to subs.length()
r[i]=DIVIDE-AND-CONQUER(subs[i]); //递归求解子问题
return COMBINE(r); //将各子问题的解合并为原问题的解
最好是使得划分后的子问题的规模大致相同
二分查找
查找的方法有:
- 静态查找:顺序查找(O(n)),有序查找、二分查找(或折半查找)、索引查找等;
- 动态查找:(平衡)二叉树、B树或B+树、键树等;
- 哈希查找。
二分查找的基本思想
- 输入为有序的序列
- 取中间元素与待查找元素x进行比较,如果x等于中间元素,则算法终止;
- 如x小于中间元素,则在序列的左半部继续查找,否则,在序列的右半部继续查找。
重复利用了元素间的次序关系
二分查找的求解步骤
- 确定合适的数据结构。设置数组s[n]来存放n个已排好序的元素;变量low和high表示查找范围在数组中下界和上界;middle表示查找范围的中间位置;x为 特定元素;
- 初始化。令low = 0;high=n-1;
- 求中间位置。即middle=(low+high)/2;
- 判定算法是否结束。判定low小于等于high是否成立,如果成立,转步骤5.否则,算法结束;
- 判断待查找元素与中间元素 是否相等。如x==s[mid],算法结束。如x>s[mid],令low=mid+1。否则令high=mid-1,转步骤3。
二分查找的伪代码(递归)
BINARY-SEARCH-REC(A,low,high,x)
if low > high then
return -1;
mid = (low + high )/2
if x== A[mid] then
return mid
else if x > A[mid]
return BINARY-SARCH-REC(A,mid+1,high,x)
else
return BINARY-SEARCH-REC(A,low,mid-1,x)
二分查找的伪代码(非递归)
BINARY-SEARCH-NOREC(A,n,x)
while low <= high
mid =(low+high)/2
if x== A[mid]
then return mid
else if x > A[mid]
then low=mid +1
else
then high = mid-1
return -1
证明算法正确性的方法:
- 结构归纳是数学归纳法的一般化
用于证明某种递归结构x(list or tree )满足命题P(x),证明方法类似于数学归纳法。首先提出一个命题P(x),证明最小结构和子结构均满足命题P(x)。
算法内部计算使用的方式,一般有循环和递归,分别可以使用循环不变量和结构归纳来证明它的正确性。
所谓的循环不变量是指一种在整个循环过程中保持不变的性质,它必须在一下3种情况下均保持不变,且该性质在循环终止后能证明算法的正确性。
- 初始化:循环初始化后,循环条件测试前。
- 保持:迭代(第n次迭代后,第n+1次迭代前)
- 终止:循环终止即循环条件判断为false时。
二分查找算法的正确性证明-循环不变式
-
循环不变式:如x存在于原始数组[1…n],则一定在[low…high]中。
-
初始化:第一轮循环开始之前,处理的数组即原始数组,显然成立。
-
保持:
- 每次循环前,x存在于A[low…high]中。
- 对于A[mid]<x,A[low…mid]均小于x,x只可能存在于A[mid+1…high]中;
- 对于A[mid]>x, A[mid…high]均大于x,x只可能存在于A[low,mid-1]中;对于A[mid]==x,直接返回x对应下标。
-
终止:结束时low>high,待处理数组为空,表示x不在A中。每步的部分数组中也不可能有x,因此x不存在于原数组,返回。
时间复杂度:
非递归:循环次数最大为logn,因此T(n)=O(logn)。
递归:
T
(
n
)
=
{
O
(
1
)
n
=
1
T
(
n
/
2
)
+
O
(
1
)
n
>
1
T(n)=\left\{ \begin{array}{lr} O(1) & n = 1\\ T(n/2)+O(1) & n > 1 \end{array} \right.
T(n)={O(1)T(n/2)+O(1)n=1n>1
基于后向导入或递归树可知:T(n)=O(logn)
空间复杂度
非递归:O(1).
递归:O(logn).
递归法的优缺点
优点:结果清晰,可读性强,而且容易用数字归纳来证明算法的正确性。
缺点:运行效率低。
分治法的优缺点
优点:规模如果很小则很容易解决。
缺点:如果子问题不独立,需要重复求公共子问题。
循环日程赛
问题描述:设有n=2^k个运动员要进行羽毛球循环赛,现要设计一个满足以下要求的比赛日程赛:
- 每个选手必须与其他n-1个选手各赛一次;
- 每个选手一天只能比赛一次;
- 循环赛一共需要进行n-1天。
需要注意的是:
因为n=2^k ,所以n为偶数。
采用分治策略求解的分析:
- 将所有的选手分为两半,n个选手的比赛日程表就可通过为n/2个选手设计的比赛日程表来决定。
- 递归进行分割,直到只剩下2个选手时,比赛日程表的制定就变得简单。
ROUND-ROBIN-CALENDAR-REC(A,n)
if n == 1
then A[1][1] = 1
return
ROUND-ROBIN-CALENDAR-REC(A,n/2)
COPY(n,A)//求解另个规模为n/2的问题
COPY(n,A)
m = n/2;
for i = 1 to m
for j = 1 to m
do A[i][j+m] = A[i][j] + m; //小块的数值抄到右上角
A[i+m][j] = A[i][j+m]; //右上抄到左下
A[i+m][j+m] = A[i][j]; //左上抄到右下
时间复杂度:
递归:
T
(
n
)
=
{
O
(
1
)
n
=
1
T
(
n
/
2
)
+
c
(
n
/
2
)
2
n
>
1
T(n)=\left\{ \begin{array}{lr} O(1) & n = 1\\ T(n/2)+c(n/2)^2 & n > 1 \end{array} \right.
T(n)={O(1)T(n/2)+c(n/2)2n=1n>1
基于后向导入或递归树可知:T(n)=O(n^2).
空间复杂度:
递归:O(logn).
正确性证明
采用循环不变式证明合并
循环不变式-数组A[n] [n]分别沿两条对角线对称。
快速排序
问题描述:
对n个元素进行排序。
算法思想:
-
通过一趟扫描将待排序的元素分割成独立的三个部分:
-
第一个部分中所有元素均不大于基准元素
-
第二个是基准元素
-
第三个部分中所有元素均不小于基准元素
-
-
再按此方法对第一个序列和第三个序列分别进行排序。
-
整个排序过程可以递归进行。
基准元素的选取:
- 取第一个元素;
- 取最后一个元素;
- 取位于中间位置的元素;
- “三者取中的规则”;
- 取位于low和high之间的随机数,用R[k]作为基准元素。
分解
在A[low:high]中选定一共元素作为基准元素,将待排序序列划分为两个子序列并使序列A[low:pivotpos-1]中所有元素的值均小于等于A[pivotpos],序列A[pivotpos+1:high] 中所有元素的值均大于等于A[pivotpos] 。
求解子问题:
对两个子序列,分别通过递归调用快速排序算法来进行排序。
合并
就地排序
快速排序算法的伪代码如下:
QUICK-SORT(A,low,high)
if low < high
then pivotpos = PARTITION(A,low,high)//划分序列
QUICK-SORT(A,low,pivotpos-1)//对左区间递归排序
QUICK-SORT(A,pivotpos+1,high)//对右区间递归排序
快速排序中序列划分的求解步骤:
-
假设待排序序列为R[low:high],该划分过程以第一个元素作为基准元素。
-
步骤1:设置两个参数i和j,i=low,j=high;
-
步骤2:选取R[low]作为基准元素,并将该值赋给变量pivot;
-
步骤3:令j自j位置开始向左扫描,如果j位置所对应的元素值大于等于pivot,则j前移一个位置(即j–)。重复该过程,直至找到第1个小于pivot的元素R[j],将**R[j]与R[i]**进行交换,i++。
-
步骤4:令i自i位置开始向右扫描,如果i位置所对应的元素的值小于等于pivot,则i后移一个位置(即i++)。重复该过程,直至找到第1个大于pivot的元素R[i],将R[j]与R[i]进行交换,j–。
-
步骤5:重复步骤3、4,交替改变扫描方向,从两端各自往中间靠拢直至i=j。
此时i和j指向同一个位置,即基准元素pivot的最终位置。
正确性证明:
可以尝试采用循环不变式自行证明;
线性时间选择
第三章 动态规划
动态规划能够解决的问题常见的有:矩阵连乘、最长公共子序列、0-1背包问题、最优二叉查找树、凸多边形最优三角剖发等。
动态规划方法与分治法的异同;
动态规划方法的基本要素与求解步骤;
动态规划方法的应用。
如何根据问题的最优子结构性质构造动态规划方法中的递归公式或动态规划方程。
动态规划(DP)是一种多阶段决策优化方法,在数学、计算机科学和经济学中使用。
动态规划背后的基本思想非常简单,类似于分治法。但鉴于通常许多子问题可能是相同的,为此动态规划法视图仅仅对每个子问题只求解一次,从而减少计算量。一旦某个给定子问题的解已经算出,则以填表方式将其存储,下次需要时可直接查表使用。
两个基本要素*
-
最优子结构
-
重叠子问题
动态规划的基本思想
动态规划方法的实质是分治思想和解决冗余
- 分治思想:将原解问题分解为更小、更易求解的子问题,然后对子问题进行求解,并最终产生原问题的解。
- 解决冗余:求解过程中,所有子问题只求解一次并以表的方式保持,对应相同子问题并不重复求解而通过查表的方式获得。
动态规划于分治法的异同:
-
相同点:都基于分治思想;
-
不同点:分治法中各个子问题是独立的,而动态规划方法中允许子问题之间存在重叠。
动态规划解决问题的步骤
- 分析最优解的结构
- 建立最优值的递归式
- 计算最优值
- 构造最优解
动态规划的基本要素
-
最优子结构性质:问题最优解包含其子问题的最优解。为动态规划的基础。基于最优子结构性质到处递归公式或动态规划基本方程是解决一切动态规划问题的基本方法。反证法。
-
子问题重叠性质:求解过程中有些子问题出现多次而存在重叠。第一次遇到就加以解决并保存,若再次遇到时无需重复计算而直接查表得到,从而提高求解效率。该性质不是必要条件,但无该性质该方法就没有了优势。
-
自底向上的求解方法:鉴于子问题重叠性质,采用自底向上的方法。先填停止条件,求解每一级子问题并保存,直至得到原问题的解。
动态规划的求解步骤:
- 分析最优解性质(考察该问题是否具备最优子结构性质)
- 递归定义最优值(建立递归公式或动态规划方程)
- 计算最优值(自底向上的方式,并记录相关信息,充分利用子问题重叠性质)
- 构造最优解
动态规划的伪代码
RECURSIVE-MATRIX-CHAIN(p,i,j)
if i == j
then return 0
m[i,j] = max//无限
for k = i to j-1
q = RECURSIVE-MATRIX-CHAIN(p,i,k)
+ RECURSIVE-MATRIX-CHAIN(p,k+1,j)
+ pi-1Pkpj
if q < m[i,j]
m[i,j] = q
s[i,j] = k
return m and s
T ( n ) = { O ( 1 ) n = 1 ∑ k = 1 n − 1 ( T ( k ) + T ( n − k ) + 1 ) n > 1 T(n)=\left\{ \begin{array}{lr} O(1) & n = 1\\ \sum_{k=1}^{n-1}(T(k)+T(n-k)+1) & n > 1 \end{array} \right. T(n)={O(1)∑k=1n−1(T(k)+T(n−k)+1)n=1n>1
T
(
n
)
≥
∑
k
=
1
n
−
1
T
(
k
)
+
∑
k
=
1
n
−
1
T
(
n
−
k
)
+
n
T(n) \geq \sum_{k=1}^{n-1}T(k)+\sum_{k=1}^{n-1}T(n-k)+n
T(n)≥k=1∑n−1T(k)+k=1∑n−1T(n−k)+n
可以证明:对于n>1,
T
(
n
)
≥
2
n
−
1
T(n)\geq 2^{n-1}
T(n)≥2n−1
由此可见,利用递归求解,花费的时间代价至少是指数函数。
备忘录方法为动态规划方法的变形:
相同点:采用表格保持已解决的子问题的值,在下次需要时直接查表而无需重新计算。
不同点:
- 备忘录方法采用自顶向下的递归方式,而动态规划方法采用自底向上的递归方法。
- 其控制结构与直接递归的控制结构相同,区别在于备忘录方法为每个已解的子问题建立备忘录以备需要时看,避免了相同子问题的重复求解。
最长公共子序列
序列:一个具有严格次序的对象
BCBA
从右下角看,从最大的数开始,4往上,4结束到3,优先往上看,如果上面的数字小于就往左边看,当左边的数也小于就往左上看,就可以得出最长公共子序列。
0-1背包
贪心法
会场安排问题、单源最短路径、最小生成树
贪心法的基本思想、基本要素与求解步骤
基本思想:在每一个阶段都根据贪心策略来做出当前最优的决策,逐步降低问题的规模(难度),逐渐逼近目标。
基本要素:
- 每次面临选择时,都采用对眼前最有利的选择
- 选择一旦做出不可更改
- 根据贪心策略来逐步构造问题的解
贪心法基本思想的推论
- 总是做出在当前看来最好的选择
- 并不从整体最优考虑,所做出的选择只是在某种意义上的局部最优选择,并希望该局部最优解选择可以导致一个全局最优解。
- 虽不能对所以问题都得到整体最优解,但对于许多的问题它确实能产生整体最优解。如单源最短路径问题,最小生成树问题。
- 在一些情况下,即使贪心算法不能得到整体最优解,其最终结果也会和最优解相似。
- 每个阶段的决策一旦做出就不可更改。不允许回溯
- 贪心法根据贪心策略来逐步构造问题的解。
如何判断是否可以使用贪心法*
- 问题是否具有贪心选择性质
- 问题是否具有最优子结构性质
贪心选择性质:
所求问题的整体最优解可通过一系列局部最优的选择获得,即通过一系列的逐步局部最优选择使得最终的选择方案是全局最优的。
- 所求问题的整体最优解可通过一系列局部最优的选择获得,这是贪心算法可行的第一个要素,也是与动态规划的区别。
- 动态规划每步所做出的选择依赖相关子问题的解。贪心算法仅考虑当前状态下做出最好的选择,局部最优。
- 贪心选择可以依赖于以往的选择,但不依赖于将来所作出的选择,也不依赖于子问题的解。
- 动态规划采用自底向上的方式解决各子问题,而贪心算法采用自顶向下的方式,迭代方式做出相继的贪心选择,每一次选择就将所求问题简化为规模更小的子问题。
- 对于一个具体问题,要确定它是否具有贪心选择性质,必须证明每一步所做出的贪心选择最终导致问题的整体最优解。
- 首先考察一个整体最优解,每做一步贪心选择后,原问题变为规模更小的类似子问题,然后用数学归纳法证明,通过每一步做贪心选择,最终可以得到整体最优解。
最优子结构性质:
- 一个问题的最优解包含其子问题的最优解。
- 是动态规划和贪心法求解的关键特征
- 可以采用反证法证明
贪心法解题步骤
单源最短路径问题
迪杰斯特拉
普利姆算法
回溯法
搜索法:举搜索、深度优先搜索、回溯法
解空间树:子集树、排列数、满m叉树
0-1背包、作业调度、n皇后、图的m着色
搜索法
回溯法
分支限界法
宽度优先搜索、布线问题、0-1背包问题、旅行售货员
概率算法
随机数、数值概率算法、舍伍德、拉斯维加斯、蒙特卡罗
舍伍德一定能得到一个解是正确的但不是最优;拉斯维加斯不一定能得到解,得到解的话就是正确的。
数值概率算法
数值算法所得到的解往往是近似解,且近似解的精度随计算时间的增加不断提高。
舍伍德算法
舍伍德一定能得到一个解,是正确的但不一定是最优解。
拉斯维加斯算法
拉斯维加斯算法不一定能得到解,得到的解的话一定是正确解。并且找到解的概率随着所用的时间的增加而增加。简单的讲就是一直求就越能求到解。
蒙特卡罗
蒙特卡罗算法用于求问题的准确解,但这个解未必是正确的,且求的正确解的概率依赖于执行算法所用的时间。一般情况下,无法有效判定所得到的解是否肯定正确。解决的办法就是多猜几次。
对于同一个实例,蒙特卡罗算法不会给出两个不同的正确解,则称该算法是一致的。
串与序列算法
子串搜索算法、后缀数组与最长公共子串、序列比较算法
后缀数组的构造
首先我们要知道后缀数组的基本概念:
- *后缀数组是将一个字符串的所有后缀按照字典序排序的字符串数组
假设一个文本串为:t[0…n-1] = AACAAAAC;t的全部后缀如下表
要构建下表,重要的在于先构建最右边的数组序列,是根据字典大小的顺序(从小到大)来进行排序的,也就是其中最小的就是AAAAC,到次大的AAAC,再到AAC;由此规律可以先得出整个序列数组。
得到这个序列数组后我们再对Sa[]数组进行赋值,附上对应的该序列数组在所有的序列数组中的大小排名,比如Sa[0]对应着AAAAC,是整个序列数组中排第四的,但是因为我们这里有0,所以对应的应该是3,所以Sa[0]=3;而Sa[6]对应着的是C,是整个序列数组中最短的,所以这里Sa[6]=7
S0 | AACAAAAC | Sa[0]=3 | AAAAC |
---|---|---|---|
S1 | ACAAAAC | Sa[1]=4 | AAAC |
S2 | CAAAAC | Sa[2]=5 | AAC |
S3 | AAAAC | Sa[3]=0 | AACAAAAC |
S4 | AAAC | Sa[4]=6 | AC |
S5 | AAC | Sa[5]=1 | ACAAAAC |
S6 | AC | Sa[6]=7 | C |
S7 | C | Sa[7]=2 | CAAAAC |
得出这个全部后缀的表后,我们可以得到构造的后缀数组为:
Sa=[3,4,5,0,6,1,7,2]
后缀数组Sa[i]:表示所有后缀在排序完成后,排名为i的后缀在原串中的位置。
排名数组rank[i]:表示所有后缀在排序完成后,原字符串中第i个字符为开始的排名。
Suffix(str){
n = str.length
suffixes[n],sa[n]
for(i=0;i<n;i++)
suffixes[i]=str.substring(i)
Arrays.sort(suffixes)
for(i=0;i<n;i++)
sa[i]=n-suffixes[i].length
}
全部的后缀长度之和O(n^2)
简答:最小生成树,哈夫曼编码、贪心算法与动态规划算法的差异
回溯法适合什么情况,分支限界法适合什么情况
概率算法的特性,能解决什么问题、舍伍德算法
NP的概念(2分)
0-1背包的三种解法
0-1背包问题不具备贪心选择性质,所以不能用贪心法解决
动态规划解法
回溯法
分支限界法
优先队列