回顾
算法是指解题方案的准确而完整的描述,是一系列解决问题的清晰指令。
算法是解决问题的一种特殊方法,不是问题本身的答案,而是经过准确定义的、以获得问题解的过程。
算法是问题的程序化解决方案,是一系列解决问题的清晰指令,对于符合规范的输入,能够在有限的时间内获得所需要的输出。
算法分析是对计算机性能和资源利用的研究
有穷性,确切性,输入项,输出项,可行性
表达方式
- 流程图
- 伪代码
- PAD图
伪代码中选择了高级编程语言,如Python,Java和C++中共有的编程语言构件
- 表达式:标准数学符号和布尔表达式; ←作为赋值运算;=作为相等关系
- 方法声明:算法 name(param1, param2,…)
- 决策结构:if then else then
- 循环结构:while 条件 do 操作;repeat 操作 until 条件;for 变量-增量-定义 do 操作
- 数组索引:A[i]表示数组A的第i个元素,i从0到n-1
- 方法调用:object.method(args),在可理解的情况下object可以省略
- 方法返回:return 值
具体步骤:
- 理解问题:使用小规模例子手工尝试
- 决定计算方式:顺序、并行?
- 决定精确还是近似解法:数值计算、优化问题
- 使用的数据结构:算法+数据结构=程序
- 算法的设计策略:“授人以鱼,不如授人以渔”以应对新问题;分类以形成体系
- 设计并描述算法
- 算法正确性证明
- 算法分析
- 算法的代码实现
图的操作
- 查询:点、边、权重、路径、入度、出度等
- 遍历
- 增、删、改
- 最短路径、最大连通子图等
图的表示方法
- 邻接矩阵,权重矩阵
- 邻接列表
有序树、堆、优先队列
每个结点的孩子结点之间定义了一种线性顺序,则称为有序树
例如:书本中的内容,每一个章节内部的各个部分有先后顺序
二叉树、二叉查找树、多路查找树
例如:算术表达式的树结构表示
集合与字典
互不相同的项的无序组合
检查成员是否存在、并集、交集
多重集、包
字典:一种基于集合的抽象数据类型
算法效率
时间效率:多快
空间效率:额外空间
输入的规模:一般越大,则时间、空间效率越低
分析步骤
- 输入规模度量:表示要处理的单元个数
- 时间度量的单位:基本操作的次数
- 增长情况:当输入规模增长时,执行时间的变化情况
- 最优、最差、平均效率
- 效率相关的度量符号
Ο上界,Ω下界,Θ - 比较两个算法的效率
平均效率比较、增长次数比
用递归的思维理解问题、分析问题
用递归给出基本的解决方案:递归的开销大?
尽力用循环+设计的数据结构改造原方案
判定问题
- 多项式问题——P问题,
- 难解问题——不确定是否存在多项式类算法解决的问题 NP
- 无解问题——NP Hard
判定问题难解问题(不能确定是否存在多项式级别的解)
- 哈密顿回路:所有点一次
- 旅行商问题:N个点一次最短距离(最短哈密顿回路)
- 背包问题:将多个物品放入一个背包,最多放多少个
- 划分问题:N个正整数划分成两个子集,和相等
- 装箱问题:将一批物体放入固定大小的箱子,最少要多少箱子
- 图着色问题:最少多少颜色使相邻颜色不同
- 整数线性规划问题等:线性函数在约束条件下的最大值或最小值
算法设计思想
-
蛮力法:是一种简单直接地解决问题的方法,直接基于问题的描述和涉及的概念定义
- 选择排序:每次遍历未排序的整个列表,选择一个最小的与第一个没有被交换的进行交换,下一次从被交换的下一个元素开始遍历。
- 冒泡排序:从第一个元素开始遍历列表直到不确定位置的最后一个元素,如果相邻元素是逆序则交换位置;每一次遍历后,有一个元素被放到对应的位置上。
-
分治法:将问题实例划分为同一个问题的几个较小的实例(最好拥有相同的规模);对这些较小的实例求解;最后合并这些较小问题实例的解得到原问题的解。对顺序执行的算法没有非常理想的优化效果;但是对可并行执行的算法优化效果明显
- 合并排序(归并排序):将一个需要排序的数组A[0…n-1]一分为两个子数组A[0…⎿n/2⏌-1]和A[⎿n/2⏌…n-1],分别对两个子数组排序,最后将两个子数组合并。
MergeSort(A[0…n-1]) //递归调用mergesort函数对数组合并排序 //输入:一个可排序的数组 //输出:升序排列的数组 if n > 1 copy A[0.. ⎿n/2⏌-1] to B[0.. ⎿n/2⏌-1] copy A[⎿n/2⏌..n-1] to C[0.. ⎾n/2⏋-1] MergeSort(B[0.. ⎿n/2⏌-1] ) MergeSort(C[0.. ⎾n/2⏋-1] ) Merge(B, C, A) Merge(B[0.. p-1] , C[0.. q-1],A[0..p+q-1]) //将两个有序数组合并为一个有序数组 i ← 0,; j ← 0; k← 0 while i < p and j < q do if B[i] ≤ C[j] then A[k] ← B[i]; i ← i+1 else A[k] ← C[j]; j ← j+1 k ← k+1 if i = p then copy C[j..q-1] to A[k..p+q-1] else copy B[i..p-1] to A[k..p+q-1]
-
快速排序:对所有元素进行分区,使得在下标s前的元素的值都小于等于下标为s的元素值,之后的则大于s位置的元素值
建立这样的分区后,下标为s的元素所在的位置与目标升序数组中的位置相同;接下来就是对前后两个分区的数据使用同样的方式进行分区;直到每一个分区都只有一个元素
QuickSort(A[l…r]) //用快速排序方法对子数组进行排序 //输入:一个可排序的子数组 //输出:非降序排列的子数组 if l < r then s ← Partition(A[l…r]) //s是分裂的位置 QuickSort(A[l…s-1]) QuickSort(A[s+1…r]) Partition(A[l…r]) //以第一个元素为中轴,将子数组分区 //输入:原数组的一个子数组,通过下标定义区间 //输出:原数组中的分裂点 p ← A[l]; i ← l; j ← r + 1 repeat repeat i ← i + 1 until A[i] ≥ p //找到大于或等于中轴的准备交换 repeat j ← j -1 until A[j] ≤ p //找到小于或等于中轴的准备交换 swap(A[i], A[j]) until i ≥ j swap(A[i], A[j]) swap(A[l], A[j]) return j
-
减治法:利用一个问题给定实例的解和同样问题较小实例的解之间的某种关系,将一个大规模的问题逐步化简为一个小规模的问题;建立与小规模问题之间的联系=>本质上是一种递推关系。
有3种主要的缩小问题规模的方式
- 减去一个常量,通常是1
- 减去一个常量因子,通常为2
- 减去一个可变的规模
和分治法之间的区别和联系?
-
分治:是多个小问题,小问题之间的联系
-
减治:还是一个小问题,小问题与原问题之间的联系
-
插入排序:假设前n-1个元素已经排序好了,则如何将最后一个元素插入到其它元素中去–》递推关系
三种方式插入最后一个元素
- 从左到右扫描,遇到第一个大于或等于的元素,将其插入前面
- 从右向左扫描,遇到第一个小于或等于的元素,将其插入后面
- 折半查找
InsertionSort(A[0…n-1]) //输入:长度为n的可排序数组 //输出:非降序排列的数组 for i ← 0 to n - 1 do v ← A[i]; j ← i - 1 while j ≥ 0 and A[j] > v do A[j+1] ← A[i] j ← j -1 A[j+1] ← v
-
- 插入排序
-
变治法:通过转换问题使得原问题更容易求解
-
实例化简
- 还是原来的问题,只是进行了一些中间操作,使得问题求解变得容易
-
改变表现
- 主要是改变使用的数据结构
-
问题化简
- 将给定的问题变换为一个新的问题,对新的问题求解
-
-
- 预排序:一种变治的思想,先对输入列表进行排序,再求解原问题
-
时空权衡:空间换时间:对问题的部分或者全部的输入作预处理,然后对获得的额外信息进行存储,以加速后面问题的求解
- 计数排序:多次扫描列表,利用额外空间记录比每一个元素小的元素的个数,最后把原列表的元素复制到新数组对应下标地方
蛮力法
蛮力法回顾
-
枚举法、穷举法、暴力解法
在不考虑时间、空间效率的情况下,寻求问题的解决方案
-
体现蛮力法的一些解决问题的方案
搜索所有解空间
找约束条件、找枚举范围:在搜索前尽可能减小搜索空间
搜索所有路径
直接计算
模拟和仿真
选择排序
遍历列表,找到最大或最小的与第一个没有排序好的交换
时间复杂度Θ(n^2)
插入排序
遍历列表,每次都把与当前元素与下一个是逆序的元素交换,把最大或最小的元素排到最后
时间复杂度Θ(n^2)
顺序查找
遍历列表直到找到给定的元素
需要判断列表是否结束
技巧:将给定元素放到列表结尾,最后判断一次是否是列表的结尾
蛮力字符串匹配
给定一个n个字符组成的串,称为文本,一个m(m<=n)个字符的串,称为模式,从文本中寻找匹配模式的子串
蛮力解法
- 以文本中的每一个字符为开始字符,用模式串去匹配,直到文本结束
- 最糟糕的情况下算法效率属于Θ(nm)
- 可以认为大多数移动的情况都是发生在很少的比较次数之后,因此算法的平均效率比最坏情况要好的多
- 随机查找文本的时候,显示出线性效率Θ(n+m)=Θ(n)
最近对问题
找出一个包含n个点的集合中距离最近的两个点
- 求出每两个点之间的距离,选出最小的那个
- 不重复计算两个点之间距离:i=0; j=i+1
- 不计算具体距离,使用平方和代表距离
复杂度为Θ(n^2)
凸包问题
凸集合
- 对于平面上的一个点集合(有限的或者无限的),如果以集合中任意两点P和Q为端点的线段都属于该集合,我们说这个集合是凸的
- 凸集合例子:直线、三角形和任意凸多边形等
- 非凸集合例子:五角星、月牙形状等
凸包
- 一个点集合S的凸包是包含S的最小凸集合(“最小”是意指,S的凸包一定是所有包含S的凸集合的子集)
- 例子
- 如果S是凸的,它的凸包是它本身
- 如果S只有两个点、如果S只有三个共线的点、如果S只有三个不共线的点
凸包定理
- 任意包含多于两个不共线点的集合S的凸包肯定是以S中某些点为顶点的凸多边形
凸包问题
- 为一个包含n个点的集合构造凸包的问题
极点:凸包的顶点
找到极点、找到极点连接的顺序
蛮力解法
如果两个点构成的线段是凸包的边界,则其它点都在这条线段所在直线的一边
复杂度Θ(n^3)
线性规划问题转换为求凸包极点的问题
旅行商问题
旅行商问题(Traveling salesman problem, TSP)
- 又名“货郎担问题”或“旅行推销员问题”
- 有一个旅行商从城市1出发,需要到城市2,3,…,n去推销货物,最后返回城市1,若任意两个城市之间的距离已知,则该旅行商应如何选择其最佳行走路线?
- TSP在图论意义下又被称为最小哈密顿回路问题:对图的每一个点都经过一次最后回到原点的路径问题。
瓶颈TSP——简称BTSP
- 经过的最长距离最短
- 最小化瓶颈距离
- 仍然是NP难问题
最小比率TSP——简称MRTSP
- 除了行程还会产生收益
- 优化目标:总行程与总收益比最小
- 与单纯的总行程最短比,更具有实际意义
多人TSP——简称MTSP
- 多个推销员同时出发,走不同路线,使得所有城市至少被访问一次
- 所有人的总路程最小
背包问题
给定n个,重量为w1, w2, …, wn、价值为v1,…,vn的物品和一个承重为W的背包,求这些物品中一个最有价值的子集,并且要能够装到背包中。
完全背包:每一件物品不计件数
多重背包:每一个物品对应有一个确定的件数
分治法
分治法回顾
具体方案流程
-
将问题实例划分为同一个问题的几个较小实例,最好拥有相同规模
-
对每一个较小规模的实例进行求解
-
如果需要则以某种方式合并这些小问题的解得到原问题的解
折半查找
对于有序列表的卓越查找方案
通过对比查找键值与列表中间元素的大小,确定下一次查找的位置
BinarySearch(A[l,…,r],K)
//折半查找的递归方案
m ← ⎿(l+r)/2⏌
if K == A[m]
return m
else if K < A[m]
return BinarySearch(A[l,…,m-1],K)
else return BinarySearch(A[m+1,…,r],K)
二叉树遍历及其相关特性
定义:要么为空,要么由一个根和两棵称为T_L和T_R的不相交二叉树构成
根,左子树,右子树
扩展
- 完全二叉树:如果有h层,1~h-1层 布满节点,h层节点从左向右分布
- 满二叉树:除了叶子节点其它节点都有左右子节点,且叶子节点都在最底层
- 二叉查找树:排序好二叉树,如果左子树不空,则左子树值小于根节点值;如果右子树不空,则右子树值 都大于根节点;左右子树分别是二叉查找树。
- 平衡二叉树:是一棵左右子树高度差绝对值不大于1的二叉查找树
大整数乘法和Strassen矩阵乘法
分治解最近对问题
分治思想:
- 使用一个坐标维度将点分为两个大小接近的集合:需要先排序
- 分别计算两个集合中的最近对
- 然后合并
- 对小集合中的点递归使用分治思想
分治解凸包问题
- 找到某种方式将点集合分为两个部分,且两个部分可以使用相同的方法求解
- 某一个维度上的最大值与最小值一定是顶点
- 顶点的连线
- 两个顶点的连线将点分为两个部分
- 两个部分可以用同样的方法处理
- 找到距离直线距离最大的点,一定是凸包顶点,其它顶点一定位于有向线段的左侧
减治法
减治法回顾
利用给定实例下问题的解和较小规模实例下相同问题的解之间关系
¡建立原问题与较小规模问题解之间的关系
减去一个常量:插入排序、深度优先、广度优先遍历
减去一个常因子:折半查找、三份查找
减去的规模是可变的:欧几里得最大公约数、选择问题、插值查找
深度优先和广度优先查找
深度优先遍历的应用
检查图的连通性
计算图的连通分量
检查图的无环性
广度优先遍历的应用
检查连通性
检查无环性
给出两个顶点间边最少的路径
拓扑排序
对一个有向无环图,获得一种顶点的序列,使得图中所有边的开始顶点都在结束顶点之前
DFS
解法一:深度优先遍历的时候
出栈是因为节点是端点
所有还在栈内的点必定是其前面的点
其之前的点不在栈内的肯定还没有压栈
出栈的点必定在其之后
出栈的逆序就是一个拓扑排序的解
解法二:减一法
规模变为|V|-1个顶点
关键问题:减去哪一个顶点?使得问题变得简单
找到一个序列的问题——即,找到肯定在其它顶点之前的点
减一问题
减去肯定在其它点之前的那个点,问题变为在余下点中找肯定出现在其它点之前的点
问题没有变,规模变小
生成组合对象
生成排列
生成n-1个数的排列
将第n个数依次插入n-1个数的每一个排列中
缺点
记录所有中间结果,耗费存储空间
构造第一个排列1,2,3,…,n,每一个数的方向初始化为向左
当当前排列中存在活动整数时
找到最大活动整数m
改变m与其指向相邻元素的位置
改变所有满足p>m的整数p的方向
获得一个新的排列
结束
生成子集
减一法
生成集合{a1,…,an-1}的所有子集
在每一个上述子集中加入an,得到包含an的所有子集
包含an的子集和集合{a1,…,an-1}的子集合并为{a1,…,an}的子集
变治法
变治法回顾
变治的三种方式
一个更简单的实例——实例化简——预排序,将问题变为排序好的列表的问题
一个实例的不同表现——改变表现——平衡查找树、堆——改变数据结构
变为另一个问题的实例——问题化简——对NP难问题和NP完全问题的定义
预排序
查找问题——折半查找、插值查找
检验元素唯一性
模式计算
计算最近元素
分治法解凸包问题
高斯消去法
- 通过初等变换——不改变方程组的解
- 交换方程组中两个方程的位置
- 把一个方程替换为它的非零倍
- 把一个方程替换为它和另一个方程倍数之间的和或差
- 用第一个方程的一个倍数和第二个方程求差,将第二个方程中x1系数变为0;同样与其它方程求差,将所有x1系数变为0;
- 再用第二个方程与其它方程作同样操作,将所有第二个方程后的所有x2系数变为0;
- 最终得到下三角为0的系数矩阵
平衡查找树
两种避免二叉树退化到最差情况的方案
实例化简:将不平衡二叉树转变为平衡的状态,使得问题变得简单:AVL树,红黑树、分裂树
改变表现:允许单个节点中不止包含一个元素:2-3树、3-4树、B树
2-3树
改变了树的表现形式
允许两种节点:一种有两个子树一个键值的2节点,一种有三个子树两个键值的3节点
所有叶子节点都在同一层:必须是一个绝对平衡的树,每个非叶子节点的平衡因子都是0;每个节点最多可以有2个键值
高为h的2-3树包含的节点数大于等于高度为h的满二叉树的节点数,即至少有2^h-1个节点
插入
- 一定插入叶子节点;
- 如果插入后有两个元素,则完成;
- 如果插入后有三个则分裂,最小的和最大的放到两个叶子节点,中间的提到父节点里;
- 直到没有包含三个键值的节点。
优点:相对与平衡二叉树可能降低了树的高度,提高操作效率
堆和堆排序
堆是一棵完全二叉树(只有最后一层最右边可能为空),且每一个节点的值都大于或等于它的子女的键,为最大堆。
霍纳法则和二进制幂
霍纳法则
改变计算方式,减少计算次数
问题化简
把一个要解决的问题化简为一个我们知道如何求解的问题
线性规划问题
多变量线性函数的最优化问题,变量满足的约束是以线性等式或者不等式出现的
单纯形法,Karmarkar算法
将问题转化为线性规划问题:背包问题,给定承重为W的背包和n个重量为w1,w2,…,wn价值为v1, v2,…vn的物品,求物品中价值最大的一个子集,且要能够放到背包中
游戏类题目转化为图问题时,节点表示可能的状态,边则表示状态之间可能的转变:状态空间图
时空权衡
算法设计中
存储空间换取运行时间
运行时间换取存储空间——少:资源真的有限的情况下
时空权衡思想
输入增强:对问题的部分或全部输入做预处理,然后对获得的额外信息进行存储,以加速后面问题的求解
预构造:使用额外空间实现更快和(或)更方便的数据存取
祖先问题:要求在一棵给定的N个顶点二叉树中,确定一个顶点u是否是顶点v的祖先。请设计一个属于O(N)的输入增强算法,使得可以在常量时间内获得每一对顶点足够的信息来求解该问题。
字符串匹配要求在一个较长的n个字符的串(称为文本)中,寻找一个给定的m个字符的串(称为模式)
输入增强:对模式进行预处理以得到它的一些信息,把这些信息存储在表中,然后在给定文本中实际查找模式的时候使用这些信息——Knuth-Morris_Pratt(KMP)算法和Boyer-Moore(BM)算法
- BM在比较过程中从右向左
- 开始的时候把模式和文本的开头字符对齐。如果第一次尝试失败了,把模式向右移。
- 只是每次尝试过程中的比较是从右向左的,即从模式的最后一个字符开始。
时空权衡:预先算出每次移动的距离并把它们存在表中,将距离填入表中的单元格中
散列
开散列(分离链):键被存储在附着于散列表单元格上的链表中,散列地址相同的记录存放于同一单链表中。查找时:首先根据键值求出散列地址,然后在该地址所在的单链表中搜索;
闭散列(开式寻址):所有的键值都存储在散列表本身中,而没有使用链表。表的长度m至少必须和键的数量一样大。
B树是一种最重要的索引结构
对2-3树的扩展(允许查找树的一个节点中包含多个键)
所有的数据记录(或者键)都按照键的升序存储在叶子中;它们的父母节点作为索引
每个父母节点包含n-1个有序的键K1<…<Kn-1,称为n节点
这些键之间有n个指向子女的指针,使得子树T0中的所有键都小于K1,子树T1中的大于等于K1小于K2,以此类推
一棵次数为m≥2的B树必须满足下面这些特性:
它的根要么是一个叶子,要么具有2到m个子女
除了根和叶子以外的每个节点,具有⌈m/2⌉到m个子女
这棵树是(完美)平衡的,也就是说,它的所有叶子都是在同一层上
B树插入算法:插入到叶子节点中,递归地调整父亲节点,直到满足B树条件。
动态规划
动态规划 Dynamic Programming
一种“使多阶段决策过程最优”的通用方法
一种算法设计技术:时空权衡思想
应用场景
如果问题由交叠的子问题组成,并且能够给出子问题的解与给定问题的解之间的递推关系,将子问题逐步分解为更小的子问题,就可以使用动态规划方法
递推关系 => 递归?
区别
-
递归是保存求解过程中每一个步骤的计算空间,达到停止条件后,逐步退回——自顶向下
-
动态规划是想直接从停止条件开始往要求解的结果计算,保存的只有中间结果——自底向上
例子:求解斐波那契数列F(n)——递归调用树、重复计算、备忘录
共同点:找到递推关系
动态规划算法常用来求解最优化问题,设计一个动态规划算法通常有以下四个步骤
-
找出最优解的结构
最优子结构性质:最优解包含着其子问题的最优解
如何确定子问题-》如何表示原问题-》变量是哪些?
-
建立递推关系
递归地定义最优值:用变量表达原问题解和子问题之间的关系
变量找到了一般就容易确定了
-
以自底向上的方式(从最简单问题开始入手)计算出最优解的值
底在哪里?——确定变量的边界值
-
根据计算最优解的值的信息,构造一个最优解
最优二叉查找树
构造一个平均查找次数最低的二叉查找树
给定一个排好序的键值序列,与每一个键值可能被查找的概率,构建一个整体平均查找次数最小的二叉查找树
给定序列 K = <k1, k2, …, kn >,其中n个关键字互不相同,且都已排好序 (k1 < k2 ··· < kn),并且有 n + 1 个“虚拟”的关键字 d0, d1, d2, …, dn 我们希望从这些关键字中建立一棵二叉搜索树。
对于每个关键字 ki, 搜索的概率为 pi 。
对于每个 di, 相应的搜索概率为 qi 。
最优子结构性质
如果最优二叉搜索树 T 有一棵子树 T′ 包含关键字 ki, …, kj, 那么子树 T′必须也是最优的
如果不是最优,则比有另一棵子树T′′的预期成本低于子树 T′,那么我们可以从T中将T′换为 T′′,从而新的二叉搜索树的预期成本小于T, 这与 T是最优的矛盾
所以最优结构的子树必定是最优的
找出最优 BST包含ki,…,kj, 其中i ≥ 1, j ≤ n, j ≥ i-1. 当 j = i-1, 树只含有 di-1.
定义 e[i, j ] =对于 ki,…,kj 和虚拟节点 di-1, …, dj,最优 BST 的期望搜索成本
If j = i-1, then e[i, j ] = qi-1.
If j ≥ i,
选出树根 kr, 对于某个r, i ≤ r ≤ j .
递归地构造一棵最优 BSTs
对ki,…,kr-1 构造左子树
对kr+1,…,kj 构造右子树
当最优的子树成为一个结点的子树时:
每个原来在最优子树中结点的深度加1.
期望搜索费用增加
如果 kr 是一棵由ki,…,kj 组成的最优BST的根 :
e[i, j ] = pr + (e[i, r-1] + w(i, r-1))+(e[r+1, j] + w(r+1, j))= e[i, r-1] + e[r+1, j] + w(i, j).
背包问题
破解为子问题(假设所有的物品w<W,都是可拿可不拿的)
可变的是物品的个数,如果减少一个物品,问题是什么?前n-1个物品的选择方式,加上最后一个物品是否加入:
已经放不下了,就从n-1个里面选了;
可能替换一下能有更大的价值。
用V[i,j]表示前i个物品中能够放入承重为j的背包的物品总价值,物品重量为w1,w2,…,wi,价值为v1,v2,…,vi。
递推关系:V[i,j] = max{ V[i-1,j], V[i-1,j-wi] + vi}
V[i,j]分为两种情况
不包含第i个物品的最大价值是V[i-1,j]
包含第i个物品的最大价值是vi + V[i-1, j-wi] (这是下一个子问题的最优解)
贪婪技术—贪心算法
通过一系列步骤构造问题的解,每一步对目前构造的部分解作一个扩展,直到获得问题的完整解。
不只是用于最优解,当作一种通用的设计技术
核心:每一步选择都必须满足以下条件
可行性:必须满足问题的约束——从给定的面值中选择组成比剩余金额小的数值
局部最优:当前已经有的部分解情况下,在当前步骤的所有可行选择中选择最佳——局部最佳
不可取消:选择一旦作出,进入下一个步骤后,之前的选择不可取消
有些问题,贪婪能够得到最优解
有些问题,只能得到接近最优解的解——可能省时省力
Matrix-Tree定理:
G的所有不同的生成树的个数等于其Kirchhoff矩阵C[G]任何一个n-1阶主子式的行列式的绝对值;
所谓n-1阶主子式,就是对于r(1≤r≤n),将C[G]的第r行,第r列同时去掉后得到的新矩阵,用Cr[G]表示;
Prim算法
思路:
通过一系列不断扩张子树来构造一棵最小生成树
从图的顶点集合中任意选择一个单顶点,作为初始子树——所有顶点都在结果中
每一次迭代,以一种贪婪的方式扩张当前生成树:把不在树中的最近顶点和对应边加入树中
当所有顶点都在树中,则停止
Kruskal算法
另一种贪婪的想法
Prim是从一个点开始找到最近的点,逐渐长成完整的树
先对所有的边按照权值排序,不停的加入权值最小的边,如果加入的边导致了回路,则跳过,直到所有顶点都加入
过程中生成的可能是多个不连通的子树,即森林;但最终会连接为一个连通子树。
加入的边如果形成回路,则该边的顶点一定在同一个连通子树中。
Dijkstra算法
完全最短路径——从每一个顶点到其它顶点的最短路径,Floyd算法——动态规划
单源最短路径:给定一个起点,求其到其它每一个顶点的最短路径
一组路径
一条经过其它所有顶点的最短路径:类似旅行商问题
贪心的Dijkstra算法
思想:每次只求出1个到起点第k近的点及其路径;第1次求第1近的,第k次求第k近的
迭代i次:第i-1次迭代后计算过的点共i-1个,第i次迭代中,计算剩下点经过前i-1个点到起点最近的那个点
与前i-1个点或起点相邻的称为“边缘顶点”,也可以是所有剩下的都作为“边缘顶点”
加入的点构成一棵树,与起点一起称为树的顶点
对每一个点记录其到起点的最近距离;为了便于构造结果,同时记录其前一个经过的点。
哈夫曼树
字符编码问题
用n个不同的一串串比特位表示n个不同的字符
定长编码:所有字符都使用相同长度的比特串
标准的ASCII码的原理
变长编码:将较长代码分配给不常用的字符,较短代码给常用字符
编码容易,解码难
要求所有字符的编码都不是另一个字符编码的前缀:自由前缀码
哈夫曼算法
-
初始化n个单节点的树,并标上字母表中的字符。把每个字符的概率记在根中,用来指出树的权重
-
一般而言,树的权重等于所有叶子节点的概率和
-
重复执行:
找到两棵权重最小的树,把他们合并成新树的两个子树,并更新新树的权重记录在根节点
-
直到只剩下一棵单独的树
与最优二叉查找树的区别
- 数据都在叶子节点 vs. 数据在所有节点
- 数据无序 vs. 数据有序
归并排序、生成组合、背包问题、最优二叉查找树、线性规划和流量网络问题