【⭐建议收藏】
本文特色:1.优化各类算法的考试答题模板。2.概率算法通俗讲解了4类算法。
第1章:算法概述
算法:具有输入、输出、确定性(清晰无歧义)、有限性。
程序(算法+数据结构=程序):具有输入、输出、确定性
注意:算法是有限的,程序不一定是有限的,如操作系统是程序,但是在无限循环中执行的程序
衡量算法好坏的方法:正确性(有限时间正确结果)、简明性、效率(时间、空间)、最优性。
— 渐进意义下的符号
O:表示算法的渐近上界。
如果算法的时间复杂度为O(g(n)),使得对于任何输入规模n,算法的运行时间都不会超过c * g(n),即算法的时间复杂度不会超过g(n)的某个常数倍,此谓上界。
注意:比较时间复杂度的题,一定要带上上界符号O,阶乘n!比指数复杂度更高。
Ω:表示算法的渐近下界。
如果算法的时间复杂度为Ω(g(n)),使得对于任何输入规模n,算法的运行时间都不会低于c * g(n),即算法的时间复杂度不会低于g(n)的某个常数倍,此谓下界。
Θ:表示算法的渐近紧确界(同阶)。
如果算法的时间复杂度为Θ(g(n)),使得对于任何输入规模n,算法的运行时间都被夹在c1 * g(n)和c2 * g(n)之间,即算法的时间复杂度大约是g(n)的某个常数倍,此谓同阶。
第2章:递归与分治(无设计)
— 分治法
所谓分治就是“分而治之”。将规模为n的问题分解为k个规模较小的子问题,子问题间互相独立且与原问题相似。通过递归求解子问题,最后将解合并,可实现对原问题的求解。
分治法的核心思想是:分解和递归。
所谓分解:就是将大问题分解为子问题。
所谓递归:是因为子问题性质与原问题相似,因此可以使用相同的递归式进行求解。
注意子问题间的求解必须是独立的,否则会做很多不必要的重复工作,【如果子问题间不完全独立的话,归属于动态规划范畴】
分治法求解过程:分解 -> 递归求解 -> 合并。
分治算法典型问题:二分搜索、合并排序、快速排序、大数乘法、矩阵乘法、棋盘覆盖、最近点对问题、线性时间选择、循环赛日程。
排序方法 | 最好时间 | 平均时间 | 最坏时间 | 辅助空间 |
插入排序 | ||||
堆排序 | ||||
冒泡排序 | ||||
快速排序 | ||||
归并排序 |
— 分治答题策略
第1步:简单描述所设计的算法,要重点说明是如何将问题划分为子问题(分解策略,通常是进行二分)
第2步:说明如何进行递归,给出递归方程
第3步:合并过程如果有的话就一句话带过即可
第4步:注意要在最后写上时间复杂度和空间复杂度。
计算时间复杂度方法如下:
带有T(n)=T(n/2)的时间复杂度一般是O(nlogn)
第3章:动态规划
— 动态规划
将规模为n的问题分解为k个规模较小的子问题,子问题间彼此关联(存在冗余)且与原问题相似。通过将子问题的解记录下来,以避免重复计算。
动态规划常用于解决具有重叠子问题和最优子结构特性的问题,通常使用表格法或记忆化搜索来实现,最终得到原问题的最优解。
重叠子问题:在递归执行的过程中,同一个子问题会被多次遇到和解决,通过存储已解决子问题的答案,动态规划可以避免对相同子问题的重复计算。(典型的是斐波那契数列的计算)
最优子结构:一个问题的最优解包含其子问题的最优解。
动态规划与分治法不同的一点在于:动态规划的子问题间不是完全独立的,换句话说子问题间具有关联或者说是依赖的关系,因此可以用表来记录已解决的子问题答案,后续操作用到时只需要查表即可,不用再重新计算,可以节约开销。
动态规划的时间效率比分治法更高,用空间来换取时间,提高解题效率。
动态规划算法典型问题:最长公共子序列、最大子段和问题、凸多边形最优剖分、多边形游戏、图象压缩、电路布线、流水作业调度、0-1背包问题。
— 动态规划答题策略
第1步:简单描述所设计的算法。重点分析最优解结构(满足最优子结构,一般使用反证法,要结合递归方程进行证明),简单说明满足重叠子问题性质(结合题目)。
第2步:建立递归关系,给出递归方程。
第3步:计算最优值,即简单描述求解过程。
第4步:给出时间复杂度,给出空间复杂度(中间信息记录,表复杂度)。
第4章:贪心算法
贪心算法:根据当前状态,做出最好选择。每一步都做出最优选择,最终得到整体最优的结果。
— 贪心选择性质
所求问题的整体最优解可以通过一系列局部最优的选择来达到。
证明满足贪心选择性质:证明每一步所做出的贪心选择最终将获得问题的整体最优解。(首先考察问题的一个整体最优解,并证明可修改这个最优解,使其以贪心选择开始。在做出贪心选择后,原问题简化为规模更小的类似子问题。用数学归纳法证明,通过每一步做贪心选择,最终可得到问题的全局最优解)
— 最优子结构性质
一个问题的最优解包含其子问题的最优解。
贪心算法典型问题:装船问题、哈夫曼编码问题、单源最短路径问题、最小生成树问题、多机调度问题。
— 贪心算法答题策略
第1步:先描述所设计的算法(贪心策略)。
第2步:证明算法的正确性,包括贪心选择性质(第一个满足条件的解包含在最优解里)和最优子结构性质(反证,完整证明,集合策略,通项证明)。
第3步:给出时间复杂度。
(如有谬误望请指正,持续更新中)
拟阵的概念
第5章:回溯法
— 回溯法
在确定完解空间的组织结构后,从根结点出发,以深度优先的方式搜索整个解空间。
搜索是向纵深方向进行,如果当前方向不能继续搜索,当前结点及其子树全部变成死结点,开始回溯(回退),移动到父结点处。
将该结点作为扩展结点,判断是否能继续往其它纵深方向搜索,如果所有方向已被探索完,继续回退,直至能往其它活结点继续搜索。
最终当解空间中无活结点终止搜索。
— 回溯法答题策略
第1步:定义解空间(包含问题正确解,更小解空间剪枝)。
包括解空间的规模,多少阶多少岔等信息,简单描述解空间的形态。
解空间可以被定义为子集树或排列树。
子集树:假如有n个元素,叶节点数量是。比如背包问题,假如有n件物品,每件物品可以有放入/不放入背包这两种情况,因此总共有种情况。
排列树:假如有n个元素,叶节点的数量是n!。比如有4个地点ABCD需要前往,第1次可以从4个地点里选择1个地点前往,比如选择地点A;第2次剩下3个地点可选,第3次剩下2个地点可选...所以总共有4x3x2x1种情况。
子集树定义:当所给的问题是从n个元素的集合S中找出S满足某种性质的子集时,相应的解空间树被称为子集树。
排列数定义:当所给问题是确定n个元素满足某种性质的排列时,相应的解空间树被称为排列树。
子集树和排列树有点像我们初中学的排列组合问题,子集树和元素顺序无关,排列树和元素顺序有关。
第2步:描述剪枝函数的设计。
剪枝函数包括:约束函数和限界函数。
约束函数:剪去不符合约束的子树——也就是不符合条件的。比如规定物品重量不超过20,现有物品重量19,如果下一件物品重15,19+15>20不符合条件,直接剪枝,后面不必再计算。
限界函数:剪去得不到最优解的子树。比如当前最优的重量是15,现在某一分支重量为14,新物品重量为2,因为14+2>15,不可能成为最优解(尽管满足14+2<20的约束条件),但仍进行剪枝。
第3步:简单描述回溯的过程(如何利用剪枝函数剪枝)。
1.考虑当前节点是叶子结点的情况,i==n。
— 记录最优解。
2.考虑当前节点是非叶子结点的情况,i<n。
— 结点可行的情况,扩展活结点
— 结点不可行的情况(不符合约束条件,不可能成为最优解),进行剪枝,将当前结点和子树中的结点全部设为死结点,回溯到父结点。
第4步:给出回溯终止的条件(局部遍历:找到了可行解,全局遍历:找到最优解,或全部可行解)。
—根节点子树中所有结点设置为死结点。
第6章:分支限界法
— 分支限界法
求解目标是找出解空间中满足约束条件的一个解,以广度优先的方式搜索解空间树。一个活结点只有一次机会成为扩展结点,一旦成为扩展结点,就一次性产生所有儿子结点,并舍弃不可行解或非最优解的子结点。其余儿子结点加入活结点队列。此后,从活结点队列中取出下一结点成为当前扩展结点,并重复以上过程。
在扩展结点处,先产生其所有的子节点(分支),然后根据约束函数和限界函数,确定哪些子节点将导致不可行解或非最优解,将这些子节点剔除,用剩下的子节点构造当前的活结点队列,然后从该队列中取一个结点作为当前扩展结点,并重复上述过程。
分支限界法分类:1.队列式FIFO分支限界法。2.优先队列式分支限界法。
二者的区别:1.FIFO分支限界法是根据先进先出的原则选取结点作为扩展结点。2.优先队列式分支限界法会将结点按优先级进行排列,每次最先取出的扩展结点是优先级最高的结点。
— 分支限界法答题策略
第1步:先说明是使用了哪种(FIFO/优先队列)分支限界法,如果是优先队列式要说明以什么作为优先级的判断标准。
第2步:限界函数和约束函数设计。
限界函数思路:一般以当前求得值最小的结点(当前最优解)作为基准,去控制值可能更大的结点,剪去不可能成为最优解的结点的子树。要注意更新当前最优解。
另一种思路:加入“预测”,更快剪枝。通过"当前结点值+剩余结点值"是否大于最优解来提前预判是否要进行剪枝,这种方式效率更高。
限界函数:目的是为了判断某个节点是否有可能包含优于当前已知最优解的解(关键是更优)。如果当前结点不能产生比已知最优解更优的解,则直接抛弃。
约束函数:用于在分支过程中检查生成的子问题是否符合原问题的约束,从而确保分支过程只会产生可行解。(关键是可行)
第3步:简单定义解空间。
第一种:用子集树存储解。越往树的叶子结点走,结点内的元素逐渐增加。比如第1层[A],第2层[A,B],第3层[A,B,D]。
第二种:普通n叉树。每个结点都只有一个元素,从叶子结点回溯走过的路径得到最终解。比如第1层A,第2层B,第3层D,A->B->D,从叶子结点回溯得到[A,B,D]。
第4步:简单描述搜索策略,包括:1.起始条件(队列为空),2.扩展节点选取的依据(优先级),3.入队出队的过程描述,4.终止条件(找到解,找到最优解)。
第7章:概率算法(无设计)
引入随机过程,使得算法具有不确定性(计算精度随时间的增加而增加)。
学习本章需要结合具体的例子进行理解和学习。
— 数值概率算法
优点:用于数值问题求解,解精度随计算时间增加而提高。
缺点:得到的往往是近似解。
举例:比如通过随机投点法来计算π的值:
因为圆半径为r,所以圆的面积为:。矩形的边长为2r,所以矩形面积为:。二者的比值为:,这个是精确解,化简后为。
现在假设k为落入圆内的点数,n是落入矩形中的点数点数。近似解可以通过:“落入圆中的点数量/落入矩形中的点数量”,即: 来近似表示。
上面两种计算方式等效,结果可以表示为:,由此可以求出π的值。
通过这种数值概率算法计算的前提是:当点的数量很大时,才可以较为准确表示π的值(解精度随计算时间增加而提高)。但这种方式不可求得精确解(所以得到的往往是近似解)。
— 舍伍德算法
优点:总能求得问题解,且解一定是正确的。
适用场景:在最坏情况下的计算复杂度远高于平均情况下的计算复杂度,可以引入随机性,让计算复杂度尽可能逼近平均复杂度,目的是降低计算复杂度。
舍伍德算法可以消除最坏情形行为与特定实例间的关联性。
相关问题:线性时间选择,搜索有序表,跳跃表。
举例:接下来我通过分析线性时间选择算法来给大家解释一下舍伍德算法。
通俗的说,线性时间选择,就是在O(n)的时间内,找到一个数组内的一个元素,比如是第k大或第k小的元素。
一般的解法是通过分治法来解题。
首先找到当前数组的中位数,然后逐个元素与中位数进行比较,小的放到左边的数组A里,大的放到右边的数组B里,确保数组A中的数全部小于数组B中的数。
假设数组A中元素个数为x,数组B中元素个数为y,基准元素所在的位置为i,现在我们要找的是第k大元素。
如果i==k,表示基准元素就是我们要找的第k大元素。
如果i<k,代表第k大元素在右边更大的数组B里。
如果i>k,代表第k大元素在左边更小的数组A里。
然后通过分治法,对第k大元素所在的数组继续进行划分,直至找到第k大元素。
有人可能会问,为什么不直接通过数组的下标访问第k大或第k小元素?那是因为如果那样的话需要对数组进行排序,会用到O(nlogn)的时间,那样就不符合线性时间的要求。
这样的算法有一些弊端,比如计算中位数需要时间开销(这个时间开销还是蛮大的);假如不通过中位数,最坏的情况是每次取到的都是当前数组中的最大或最小值,那样要进行平方次计算,时间开销是O()。
现在我们想到的改进方法是引入随机性,每次任取数组中的一个元素作为基准元素,然后将数组中逐个元素与该数比较,继续按之前的方法,比它小的放左边,比它大的放右边,然后对第k大数所在的数组继续随机选取基准元素...直至找到第k大元素。
这样做的好处是,通过引入随机性,让最坏情况下的计算复杂度尽可能逼近平均复杂度,起到降低复杂度的作用。
所以舍伍德算法主要是在最坏情况下的计算复杂度远高于平均情况下的计算复杂度的情况下适用。
— 拉斯维加斯算法
优点:求得的一定是正确解,找到解的概率随算法计算时间的增加而提高
缺点:有时找不到问题的解
相关问题:N皇后问题、整数因子分解
举例:接下来我通过分析N皇后问题来给大家解释一下拉斯维加斯算法。
首先简单说一下N皇后问题的要求:在棋盘上摆放皇后,要求每行、每列、每个对角线上不能出现两个皇后(即每行、每列、每对角线上只能有1个皇后,如下图,不然皇后会打架)。
一般的解法是通过回溯法,在每行的每个位置上尝试摆放皇后,如果出现打架的情况,就回退,继续尝试在下一个位置上摆放皇后。
回溯法虽然能遍历棋盘中的每一种皇后摆放情况,但耗费的时间是很大的(指数级别),如果棋盘的行数n很大,那么很可能需要消耗几万年才能算出结果。
现在我们用拉斯维加斯算法来进行改进:采用随机放置策略+回溯法。
比如先在棋盘的前几行放置若干皇后,保证互不攻击,然后在剩余的行通过回溯法来放置皇后,
如果找到了一种皇后的正确摆法就返回true,如果返回true时这种摆法一定是正确的,能满足每行、每列、每对角线上只能有1个皇后(求得的一定是正确解)。
并且随着计算时间增加,尝试更多初始随机摆放的方式,有更大的几率找到解(找到解的概率随算法计算时间的增加而提高)。
但如果运气很背,假设摆了1000次也没摆对前几行随机皇后的位置,会导致后续的回溯都找不到摆放的正确解(有时找不到问题的解)、
— 蒙特卡罗算法
优点:可以求出问题的精确解。求得正确解的概率依赖于计算时间。
缺点:但无法判定求得的解是否正确。
相关问题:主元素问题,素数测试。
首先明确几个概念:
1.如果一个蒙特卡罗算法对问题的任意实例得到正确解的概率不小于p,则称该蒙特卡罗算法是p正确的。
2.如果对于同一实例,蒙特卡罗算法不会给出两个不同的正确解答,则称该蒙特卡罗算法是一致的。
对于一个一致的p正确的蒙特卡罗算法,要提高获得正确解的概率,只需要执行该算法若干次,并选择出现频次最高的解即可。
偏真蒙特卡罗算法,简单来说就是当结果返回true时,解总是正确的;当返回false时,解有可能是错误的。所以当多次调用一个偏真蒙特卡罗算法时,只要有一次调用返回结果为true,则可以判定解为true。
举例:接下来我通过分析主元素问题来给大家解释一下蒙特卡罗算法。
相信大家对主元素问题都不陌生:就是找到n个数中出现次数最多的那个数,且该数出现次数要大于 。可以简单理解为就是找众数的问题。
这道题如果用蒙特卡罗算法来解,很关键的一个点就是,如果随机选取一个数,且该数刚好为主元素的话,那么它出现的概率是大于的(这个概率是很重要的)。
因此假如你抽取了10个数,有某一个数出现的次数大于5次,那么你可以初步猜测这个数是主元素。但也有可能这个数并不是主元素,但它刚好出现了大于5次(因此无法判定求得的解是否正确的)。但如果你抽取了更多数,假如又重复抽取了10000次,有一个数出现的次数很多,可能是5000次,那么你此时几乎可以确定该数就是主元素(可以求出问题的精确解,但求得正确解的概率依赖于计算时间)。
一般来说蒙特卡罗算法是最爱考的。