算法基础
问题求解的关键
建模:
对输入参数和解给出形式化或半形式化的描述
设计算法:
采用什么算法设计技术
正确性--是否对所有的实例都得到正确的解
分析算法--效率
算法+数据结构=程序
好的算法
提高求解问题的效率
节省存储空间
算法的研究目标
问题->建模并寻找算法 算法设计技术
算法->算法的评价 算法分析方法
算法类-> 问题复杂度估计 问题复杂度分析
问题类-> 能够求解的边界 计算复杂性理论
算法
有限条指令的序列
这个指令序列确定了解决某个问题的一系列运算或操作
算法A解问题P
- 把问题P的任何势力作为算法A的输入
- 每步计算是确定性的
- A能够在有限步停机输出该实例的正确的解
算法时间复杂度:针对指定基本运算,计数算法所做运算次数
基本运算:比较,加法,乘法,置指针,交换。。。。
输入规模:输入串编码的长度。 比如:数组元素多少,调度问题的任务个数
算法基本运算次数可表为输入规模的函数
给定问题和基本运算就决定了一个算法类
算法的两种时间复杂度
- 最坏情况下的时间复杂度W(n)
- 平均情况下的时间复杂度A(n)
分治策略的基本思想
分治策略
将原始问题划分或者归结为规模较小的子问题
递归或迭代求解每个子问题
将子问题的解综合得到原问题的解
注意:
子问题与原始问题性质完全一样
子问题之间可彼此独立地求解
递归停止时子问题可直接求解
分治算法的特点:
将元问题归约为规模小的子问题,子问题与原问题具有相同的性质;
子问题规模足够小时可直接求解;
算法可以递归也可以迭代实现;
算法的分析方法:递推方程
分治算法设计要点
原问题可以划分或者归约为规模较小的子问题
子问题与原问题具有相同的性质;
子问题的求解彼此独立
划分时子问题的规模尽可能均衡
子问题规模足够小时可直接求解
子问题的解综合得到原问题的解
算法实现:递归或迭代
分治算法之快速排序
基本思想
用首元素x作划分标准,将输入数组A划分成不超过x的元素构成的数组AL,大于x的元素构成的数组AR,其中AL,AR从左到右存放在数组A的位置。
递归地对子问题AL和AR进行排序,直到子问题规模为1时停止
伪码
算法 Quicksort(A,p,r)
输入: 数组A[P..r]
输出: 排好序的数组A
1. if p<r
2. then q <- Partition(A,p,r)
3. A[p] <-> A[q]
4. Quicksort(A,p,q-1)
5. Quicksort(A,q+1,r)
划分过程:
Partition(A,p,r)
1. x ← A[p]
2. i ← p
3. j ← r+1
4. while true do
5. repeat j ← j-1
6. until A[j] <=x //不超过首元素
7. repeat i ← i+1
8. until A[i] > x //比首元素大的
9. if i<j
10. then A[i] ←→ A[j]
11. else return j
时间复杂度
- 最坏情况:
W(n)=W(n-1)+n-1
W(1)=0
W(n)=n(n-1)/2 - 最好划分
T(n)=2T(n/2)+n-1
T(1)=0
T(n)=θ(nlogn)
小结
快速排序算法
- 分治策略
- 子问题划分时由首元素决定
- 最坏情况下时间O(n的2次方)
- 平均情况下时间为O(nlogn)
幂乘算法及应用
幂乘问题
输入: a为给定实数,n为自然数
输出:a的n次方
传统算法:顺序相乘
a的n次方=(….(((a a)a)a)..)a
乘法次数:θ(n)
分治算法——划分
分治算法分析
以乘法作为基本运算
- 子问题规模,不超过n/2
- 两个规模近似n/2的子问题完全一样,只要计算1次
W(n)=W(n/2)+θ(1)
W(n)=θ(log n)
幂乘算法的应用
Fibonacci数列:1,1,2,3,5,8,13,21,….
增加F0=0,得到数列:0,1,1,2,3,5,8,13,21….
问题:已知F0=0,F1=1,给定n,计算Fn
通常算法:从F0,F1,….开始,根据递推公式:F(n)=F(n-1)+F(n-2)
陆续相加可得Fn,时间复杂度为θ(n)
Fibonacci数的性质
算法:
令矩阵 ,用乘幂算法计算
时间复杂度:
- 矩阵乘法次数 T(n)=θ(log n)
- 每次矩阵乘法需要8次元素相乘
- 总计元素相乘次数为θ(log n)
改进分治算法的途径1:减少子问题数
减少子问题个数的依据
分治算法的时间复杂度方程
W(n)=a*W(n/b)+d(n)
a:子问题数,n/b:子问题规模,
d(n):划分与综合工作量
当a较大,b较小,d(n)不大时,方程的解:
减少a是降低函数W(n)的阶的途径
利用子问题的依赖关系,使某些子问题的解通过组合其他字问题的解而得到
例子:矩阵相乘的问题
矩阵乘法的研究及应用
矩阵乘法问题的难度:
- Coppersmith-Winograd 算法:
目前为止最好的上界
- 目前最好的下界:
应用 - 科学计算、图像处理、数据挖掘等
- 回归、聚类、主成分分析、决策树等挖掘算法常涉及大规模矩阵运算
改进途径小结
- 适用于:子问题个数多,划分和综合工作量不太大,时间复杂度函数
- 利用自问题依赖关系,用某些子问题解的代数表达式表示另一些子问题的解,减少独立计算子问题个数。
- 综合解的工作量不影响W(n)的阶。
改进分治算法的途径2:增加预处理
例子:平面点对问题
输入:平面点集P中有n个点,n>1
输出:P中的两个点,其距离最小
蛮力算法:
C(n,2)个点对,计算最小距离,O(n的2次方)
分治策略:P划分为大小相等的PL和PR
算法伪码:
算法分析:
增加预处理:
原算法:
在每次划分时对子问题数组重新排序
改进算法:
- 在递归前对X,Y排序,作为预处理
划分时对排序的数组X,Y进行拆分,得到针对子问题PL的数组XL,YL及针对子问题PR的数组XR,YR
原问题规模为n,拆分的时间为O(n)
改进算法时间复杂度
改进分治算法的途径:小结
依据
W(n)=a*W(n/b)+f(n)
提高算法效率的方法:
- 减少子问题个数a:
- 增加预处理,减少f(n)
分治算法典型应用:
选第二大
输入:n个数的数组L
输出:第二大的数 second
通常算法:顺序比较
- 顺序比较找到最大max
从剩下n-1个数中找最大,就是第二大second
时间复杂度:W(n)=n-1+n-2=2n-3
提高效率的途径
- 成为第二大数的条件:仅在与最大数的比较中被淘汰
- 要确定第二大数,必须找到最大数
- 在确定最大数的过程中记录下被最大数直接淘汰的元素
- 在上述范围(被最大数直接淘汰的数)内的最大数就是第二大数
- 设计思想:用空间换时间
锦标赛算法
- 两两分组比较,大者进入下一轮,知道剩下1个元素max为止。
- 在每次比较中淘汰较小元素,将被淘汰元素记录在淘汰它的元素的链表上
- 检查max的链表,从中知道最大元素,即second
伪码
算法 FindSecond
输入:n个数的数组L,输出:second
- k <- n //参与淘汰的元素数
- 将k个元素两两1组,分成[k/2]组
- 每组的2个数比较,找到较大数
- 将被淘汰数记入较大数的链表
=======一轮淘汰结束============== - if k 为奇数 then k <- [k/2]+1
- else k <- [k/2]
- if k>1 then geto 2 //继续分组淘汰
- max <- 最大数
second <- max 的链表中的最大
实例
时间复杂度分析
第一阶段元素数:n
比较次数:n-1
淘汰了n-1个元素
第二阶段:元素数[log n]
比较次数:[log n]-1
淘汰元素数为[log n]-1
时间复杂度是
W(n)=n-1+[log n]-1=n+[log n]-2
小结
小结:分治算法设计
将元问题归约为子问题:
- 直接划分注意尽量均衡
- 通过计算归约为特殊的子问题
- 子问题与原问题具有相同的性质
- 子问题之间独立计算
算法实现:
- 递归或迭代实现
- 注意递归执行的边界
小结:分治算法的分析及改进
- 时间复杂度分析:
- 给出关于时间复杂度函数的递推方程和初始值
- 求解方程
- 提高效率的途径:
- 减少子问题个数
- 预处理
小结: 重要的分治算法
- 检索算法:二分检索
- 排序算法:快速排序、二分归并排序
- 选择算法: 选最大与选最小、选第二大
- 快速傅里叶变换FFT算法
- 平面点集的凸包
动态规划
动态规划(Dynamic Programming)
- 求解过程是多阶段决策过程,每步处理一个字问题,可用于求解组合优化问题
- 适用条件:问题要满足优化原则或最优子结构性质,即:一个最优决策序列的任何子序列本身一定是相对于子序列的初始和结束状态的最优决策序列
实例:最短路径问题
问题:
输入:
起点集合{S1,S2,…,Sn}
终点集合{T1,T2,…,Tn}
中间结点集,
边集E,对于任意边e有长度
输出:一条从起点到终点的最短路径
实例:
算法设计:
蛮力算法: 考察每一条从某个起点到某个终点的路径,计算长度,从其中找出最短路径。
上述实例中,如果网络层数为k,那么路径条数将接近2的k次方。
动态规划算法: 多阶段决策过程。
每步求解的问题是后面阶段求解问题的子问题。每步决策将依赖于以前步骤的决策结果。
动态规划求解:
优化原则:最优子结构性质
优化函数的特点:任何最短路的子路径相对于子问题始、终点最短
优化原则:一个最优决策序列的任何子序列本身一定是相对于子序列的初始和结束状态的最优决策序列
动态规划算法设计
动态规划设计要素
- 问题建模,优化的目标函数是什么?约束条件是什么?
- 如何划分子问题(边界)?
- 问题的优化函数值与子问题的优化函数值存在着什么依赖关系?(递推方程)
- 是否满足优化原则
- 最小子问题怎样界定?其优化函数值,即初始值等于什么?
动态规划算法设计要素小结
- 多阶段决策过程,每步处理一个字问题,界定子问题的边界
- 列出优化函数的递推过程及初值
- 问题要满足优化原则或最优子结构性质,即:一个最优决策序列的任何子序列本身一定是相对于子序列的初始和结束状态的最优决策序列
动态规划算法的递归实现
小结
- 与蛮力算法相比较,动态规划算法利用了子问题优化函数间的依赖关系,时间复杂度有所降低
- 动态规划算法的递归实现效率不高,原因在于同一子问题多次重复出现,每次出现都需要重新计算一遍。
- 采用空间换时间策略,记录每个子问题首次计算结果,后面再用时就直接取值,每个字问题只算一次。
动态规划算法的迭代实现
迭代计算的关键
- 每个子问题只计算一次
迭代过程
- 从最小的子问题算起
- 考虑计算顺序,以保证后面用的值前面已经计算好
- 存储结构保存计算结果–备忘录
解的追踪
- 设计标记函数标记每步的决策
- 考虑根据标记函数追踪解的算法
递归和迭代这两种实现的比较
递归实现:时间复杂性高,空间较小
迭代实现:时间复杂性低,空间消耗多
原因:递归实现子问题多次重复计算,子问题计算次数呈指数增长,迭代实现每个子问题只计算一次。
动态规划时间复杂度:
备忘录各项计算量之和+追踪解工作量
通常追踪工作量不超过计算工作量,是问题规模的多项式函数
动态规划算法的要素
- 划分子问题,确定子问题边界,将问题求解转变成多步判断的过程。
- 定义优化函数,以该函数极大(或极小)值作为依据,确定是否满足优化原则。
- 列优化函数的递推方程和边界条件
- 自底向上计算,设计备忘录(表格)
- 考虑是否需要设立标记函数
总结:动态规划算法设计要点
- 引入参数来界定子问题的边界。注意子问题的重叠程度(动态规划之所以比蛮力算法好,效率高,是在于它的每一个子问题子算一次,如果动态规划算法里面,它的子问题出现次数非常少,前面存放着,后面几乎不再引用,这时相当于跟蛮力算法就没有太大区别了。所以只有字问题呗多次重叠出现,后面计算的时候多次要调用这个值,这个时候动态规划算法才能体现出它的时间效率)。
- 给出带边界参数的优化幻术定义与优化函数的递推关系,找到递推关系的初值。
- 判断该优化问题是否满足优化原则。
- 考虑是否需要标记函数。
- 采用自底向上的实现技术,从最小的子问题开始迭代计算,计算中用备忘录保留优化函数和标记函数的值。
- 动态规划算法的时间复杂度是对所有字问题(备忘录)的计算工作量求和(可能需要追踪解的工作量)
- 动态规划一般使用较多的存储空间,这往往成为限制动态规划算法使用的瓶颈因素。(空间换时间)
贪心算法
贪心算法的特点
设计要素:
- 贪心法适用于组合优化问题。
- 求解过程是多不判断过程,最终的判断序列对应于问题的最优解。
- 依据某种“短视的”贪心选择性质判断,性质好坏决定算法的成败。
- 贪心法必须进行正确性证明。
- 证明贪心法不正确的技巧:举反例。
贪心法的优势:算法简单,时间和空间复杂性低
最优装载问题
问题:
n个集装箱1,2,3,…..,n装上轮船,集装箱i的重量wi,轮船装载重量限制为C,无体积限制。问如何装是的上船的集装箱最多?不妨设每个箱子的重量Wi<=C.
该问题是0-1背包问题的子问题,集装箱相当于物品,物品重量是Wi,价值Vi都等于1,轮船载重限制C相当于背包重量限制b。
建模:
设 < x1,x2,x3,x4….,xn >表示解向量,Xi=0,1,Xi=0当且仅当第i个集装箱装上船
算法设计
- 贪心策略:轻者优先
- 算法设计:
将集装箱排序,使得
W1<=W2<=…<=Wn
按照标号从小到大装箱,直到装入下一个箱子将使得集装箱 总重超过轮船装载重量限制,则停止。
正确性证明思路
- 命题:对装载问题任何规模为n的输入实例,算法得到最优解。
- 设集装箱从轻到中记为1,2,3…,n
- 归纳基础:证明对任何只含1个箱子的输入实例,贪心法得到最优解。显然正确
- 归纳步骤: 证明:假设对于任何n个箱子的输入实例贪心法都能得到最优解,那么对任何n+1个箱子的输入实例贪心法也得到最优解。
小结
- 装载问题是0-1背包问题的子问题(每件物品重量为1),NP难的问题存在多项式时间可解的子问题
得不到最优解的问题的处理方法
找零钱问题
最小生成树
Prim算法
Kruskal算法
单源最短路径问题及算法
Dijkstra算法
贪心法小结
- 贪心法适用于组合优化问题
- 求解过程是多步判断过程,最终的判断序列对应于问题的最优解
- 判断依据某种“短视的”贪心选择性质,性质的好坏决定了算法的正确性。贪心性质的选择往往依赖于直觉或经验。
- 贪心法正确性证明方法:
1).直接结算优化函数,贪心法的解恰好取得最优值
2).数学归纳法(对算法步数或者问题规模归纳)
3). 交换论证 - 证明贪心策略不对:举反例
- 对于某些不能保证对所有的实例都得到最优解的贪心算法(近似算法),可做参数化分析或者误差分析
- 贪心法的优势:算法简单,时间和空间复杂性低
- 几个著名的贪心算法
最小生成树的Prim算法
最小生成树的Kruskal算法
单源最短路的Dijkstra算法
回溯算法
回溯算法例子 :小结
- 回溯算法的例子:n后问题,0-1背包问题,货郎问题
- 解:向量
- 搜索空间:树,可能是n叉树、子集树、排列树等等,树的结点对应于部分向量,可行解在叶结点
- 搜索方法:深度优先,宽度优先,….跳跃式遍历搜索树,找到解
深度与宽度优先搜索
回溯算法的基本思想
- 适用:求解搜索问题和优化问题
- 搜索空间:树,结点对应部分解向量,可行解在树叶上
- 搜索过程:采用系统的方法隐含遍历搜索树
- 搜索策略:深度优先,宽度优先,函数优先,宽深结合等
结点分支判定条件:
满足约束条件–分支扩张解向量
不满足约束条件,回溯到该节点的父节点- 结点状态:动态生成
白节点(尚未访问)
灰节点(正在访问该节点为根的子树)
黑节点(该节点为根的子树遍历完成)
- 存储:当前路径
回溯算法适用条件:多米诺性质
回溯算法的设计步骤
- 定义解向量和每个分量的取值范围
解向量 为 < X1,X2,X3,….,Xn >
确定Xi的取值集合为Xi,i=1,2,3…,n - 在< X1,X2,X,….X(k-1)>确定如何计算Xk取值集合Sk,
- 确定节点儿子的排列规则
- 判断是否满足多米诺性质
- 确定每个结点分支的约束条件
- 确定搜索策略:深度优先,宽度优先等
- 确定存储搜索路径的数据结构
回溯算法的实现
回溯算法的递归实现
回溯算法的实现:递归实现、迭代实现
迭代实现
装载问题
图的着色
着色问题的应用
会场分配问题:
搜索树结点数估计
Monte Carlo 方法
目的:估计搜索树真正访问结点数
步骤:
1).随机抽样,选择一条路径,用这条路径代替其他路径,逐层累加树的节点数
2).多次选择,取节点数的平均值
组合优化问题
最大团问题
应用
编码,故障诊断,计算机视觉,聚类分析,经济学,移动通信,VLSI电路设计,…..
货郎问题
圆排列问题
连续邮资问题
算法设计与分析总结
知识框架
函数的阶
序列求和
基本求和公式:
等比数列
等差数列
调和数级数估计和式的阶:
放大,然后估计上界
用积分估计上下界
递推方程求解
主要的求解方法:
迭代+进行序列求和
递归树+求和
主定理:注意条件验证一些常见的递推方程的解:
算法设计技术
- 设计技术:
分治策略
动态规划
贪心法
回溯和分支限界 - 关注问题:
使用条件
主要设计步骤
时间复杂度分析方法
改进途径
典型例子
分治策略
- 适用条件:归约为独立求解子问题
- 设计步骤:归约方法,初始子问题的计算,子问题解的综合方法,注意子问题划分,类型相同
- 递归算法分析:求解递推方程
- 改进途径:减少子问题数,预处理
- 典型问题:二分检索,归并排序,芯片测试,幂乘,矩阵乘法,最临近点对,多项式求值
动态规划
- 适用条件: 优化问题,多步判断求解,满足优化原则,子问题重叠
- 设计步骤: 确定子问题边界,列关于目标函数的递推方程及初值;自底向上,备忘录存储;标记函数及解的追踪方法
- 复杂度分析: 备忘录,递推方程
- 典型问题: 矩阵链相乘,投资,背包,最长公共子序列,图像压缩,最大子段和,最优二分检索树,生物信息学应用。
贪心法
- 适用条件: 组合优化问题,多步判断求解,有贪心选择性质
- 设计步骤: 局部优化策略的确定及算法正确性证明(直接证明,数学归纳法,交换论证)
- * 复杂度分析:*
- 典型问题: 活动选择,装载问题,最小延迟调度,最优前缀码,最小生成树,单源最短路
回溯和分支限界
- 适用条件: 搜索或优化问题,多步判断求解,满足多米诺性质
- 设计步骤: 确定解向量,搜索树结构,搜索顺序,节点分支搜索的约束条件与代价函数,路径存储
- 搜索树节点数分析:
- 复杂度分析:
- 典型问题: n后问题,背包问题,货郎问题,装载问题,最大团问题,圆排序问题,连续邮资问题
算法设计
- 设计思想:尽管选复杂度低的算法
- 算法实现依赖于数据结构,选择合适的数据结构
- 实际问题中的综合考虑:时空权衡,实现成本的权衡,…..