1.2 基本算法
在进行算法分析的时候,要着重考虑算法的时间复杂度,选择最优算法
1.2.1 枚举算法
枚举算法核心思想:在很多时候,无法立刻得出某个问题的可行解或者最优解,但是可以用一种比较“笨”的方法通过列举所有情况然后逐一判断得到的结果。
缺点:浪费时间、有可能漏掉某些情况
作用:枚举是一种理想的辅助算法,在毫无头绪的情况下,枚举为我们打开一个缺口,让其他算法得以成功。
练习
离散函数——不会
有一个离散函数,定义在集合{1,2,3…,N},取值在-232…232。请找出函数图像上两个点,使得函数在这两点之间的点都在两点连线的下方,且此连线的斜率尽量大。N≤10000.(注:时间复杂度为O(n2)的算法容易想到,那么O(n)算法呢?)
1.2.2 贪心法
每次选择当前最优策略
概述:每次选择一个局部最优策略进行实施,而不去考虑对今后的影响。一般来说,它的时间复杂度比较低,算法实现页比较容易,但是很多题目贪心法并不能得到最优解。即使可以,也比较难证明解的最优性。
例子
- 最优装载问题
单一元素 - 部分背包问题
多种元素单一限制 - 乘船问题
多种元素多种限制 - 区间覆盖问题
两个调度问题
流水作业调度
有n个作业要在两台机器M1和M2组成的流水线上完成加工。每个作业i都必须先花时间ai在M1上加工,然后花时间bi在M2上加工。确定n个作业的加工顺序,使得从作业1在机器M1上加工开始到作业n在机器M2上加工为止总时间最少。
Johnson算法
设N1为a < b的作业集合,N2 为a ≥ b的作业集合,将N1中的作业按照a非减序排序,N2中的作业按照b非增序排序,则N1作业接N2作业构成最优顺序。
显然算法的核心只是一个排序过程,时间复杂度为O(nlogn),关键在于它的正确性证明。
带限期和罚款的单位时间任务调度
有n个单位时间任务,每个任务需要单位长度时间。第i个任务的限期是di,超时罚款为fi,给n个任务排序使得所有未按时完成的任务罚款总和尽量小。
矩阵胚(matroid)
给定非空有限集S,I是S的一类具有遗传性质的独立子集族(即空集属于I,且对于I里的任意元素B,B的任意子集也属于I)。我们把I中的元素称为独立子集。如果I满足交换性质(即对于I的任意两个元素A和B,若|A| < |B|,一定可以找到一个元素x ∈ B − A,使得A ∪ {x}仍然是独立集),称有序对(S,I)为矩阵胚,或称拟阵。
对于独立集A,如果存在x使得A∪{x}仍是独立集,称x为A的可扩展元素。没有可扩展元素的独立集A称为极大独立集。
定理1:矩阵胚的所有极大独立集具有相同的基数。
最大权独立子集问题 给S的每个元素x赋予正权W(x) > 0,独立集的权被定义为其中所有元素的权和。求加权矩阵胚中权最大的独立子集。
贪心算法
从空集开始(权为0),每次选择权最大的可扩展元素加入独立集中,直到不存在可扩展元素为止,此时得到的独立集一定是权最大独立子集。
应用
应用一:Huffman编码
练习
喷水装置——不懂
有一块草坪,长为l,宽为w,在它的中心线上不同位置处装有n(n≤10000)个点状的喷水装置。每个喷水装置i的喷水效果会让以它为中心半径为ri的圆被润湿。请选择尽量少的喷水装置,把整个草坪全部润湿。
1.2.3 递归与分治
分治是递归的典型应用
递归法概述:递归法把问题转化为规模更小的子问题解决。递归法思路清晰,编程简单,但有时候难以想到。如果确定了用递归法解题,思考的重点应该放到建立原问题和子问题之间的联系。有的问题有很明显的递归结构,但是需要仔细思考,才能正确的转化为结构相同的子问题。
分治法使用递归的思考方式,遵守以下方法:
划分(divide):把问题划分为若干子问题
求解(conquer):递归求解子问题
合并(combine):把子问题的解合并成原问题的解
其中第二个步骤需要递归调用,而划分子问题和合并子问题的解需要仔细设计算法。
注意:尽量减少递归的调用,尤其不要进行重复的计算
典型事例
- 棋盘覆盖问题
- 循环日程表问题
- 巨人与鬼
三个经典的分治算法
- 最大值与最小值
- 有序表查找问题
- 快速幂
1.2.4 递推法
有一类试题,每相邻两项数之间的变化有一定的规律性,我们可将这种规律归纳成如下简捷的递推关系式:
F
n
=
g
(
F
n
−
1
)
F_n=g(F_{n-1})
Fn=g(Fn−1)
所谓倒推法,就是在不知初始值的情况下,经某种递推关系而获知问题的解或目标,再倒过来,推知它的初始条件,因为这类问题的运算过程是一一映射的,故可分析得其递推公式。然后再从这个解或目标出发,采用倒推手段,一步步地倒推到这个问题的初始陈述。
一般的分析思路:
if 求解初始条件 F~1~
then begin{ 倒推 }
由题意(或递推关系)确定最终结果F~n~;
求出倒推关系式F~i-1~=g'(F~i~);
i=n;{从最终结果F~n~出发进行倒推}
while 当前结果F~i~非初始值F~1~ do由F~i-1~=g(F~i~)倒推前项;
输出倒推结果F~1~,和倒推过程;
end{then}
else begin{顺推}
由题意(或递推关系)确定初始值F~1~(边界条件);
求出顺推关系式F~i~=g(F~i-1~);
i=1;{由边界条件F~1~,出发进行顺推}
whie 当前结果F~i~,非最终结果F~n~ do由F~i~=g(F~i-1~)顺推后项;
输出顺推结果F~n~,和顺推过程;
end;{else}
1.2.5 动态规划 dynamic programming
动态规划的思想源于递归,是一种问题转化策略。
Fibonacci sequence(合并状态)
基本思想:建立子问题的描述,建立状态间的转移关系,使用递推或记忆化搜索法来实。
动态规划要点:
1、状态定义:用问题的某些特征参数描述一个子问题
2、状态转移方程:即状态值之间的递推关系。这个方程通常需要考虑两个部分:一是递推的顺序;而是递推的边界(也是递推的起点)。
注意:**重叠子问题(overlapping subproblem)**是动态规划展示威力的关键。
将问题转化为一个个子问题,根据总结出的固有表达式,实现子问题(subproblem)的解决思路相同。
前提:问题解决的思路,总结出表达式
方法一:递归计算
缺点:时间效率太低,原因为重复计算
方法二:递推计算
即从最小的问题开始计算,往前递推,直到顶部。
优点:减少重复计算
方法三:记忆化搜索
使用一个visited数组作为当前结点是否被访问的记录,被访问后直接调用结果。
根据题目情况设置数组初始化的值