经典贪心算法(哈夫曼算法,Dijstra单源最短路径算法,最小费用最大流)
哈夫曼编码与哈夫曼算法
哈弗曼编码的目的是,如何用更短的bit来编码数据。
通过变长编码压缩编码长度。我们知道普通的编码都是定长的,比如常用的ASCII编码,每个字符都是8个bit。但在很多情况下,数据文件中的字符出现的概率是不均匀的,比如在一篇英语文章中,字母“E”出现的频率最高,“Z”最低,这时我们可以使用不定长的bit编码,频率高的字母用比较短的编码表示,频率低的字母用长的编码表示。
但这就要求编码要符合“前缀编码”的要求,即较短的编码不能是任何较长的编码的前缀,这样解析的时候才不会混淆。要生成这种编码,最方便的就是用二叉树,把要编码的字符放在二叉树的叶子上,所有的左节点是0,右节点是1,从根浏览到叶子上,因为字符只能出现在树叶上,任何一个字符的路径都不会是另一字符路径的前缀路径,符合前缀原则编码就可以得到。
现在我们可以开始考虑压缩的问题,如果有一篇只包含这五个字符的文章,而这几个字符的出现的次数如下:
A:
6次
B
: 15次
C:
2次
D
: 9次
E:
1次
用过用定长的编码,每个字符3bit,这篇文章总长度为:3*6 + 3*15 + 3*2 + 3*9 + 3*1 = 99
而用上面用二叉树生成的编码,总长度为: 2*6 + 3*15 + 2*2 + 2*9 + 2*1 = 80
字符 |
编码 |
A |
00 |
B |
010 |
C |
011 |
D |
10 |
E |
11 |
显然,这颗树还可以进一步优化,使得编码更短,比如下面的编码
生成的数据长度为:3*6 + 1*15 + 4*2 + 2*9 + 4*1 = 63
下面我们先来介绍哈弗曼算法是如何构造最优二叉树的。
哈夫曼算法的步骤是这样的:
- 从各个节点中找出最小的两个节点,给它们建一个父节点,值为这两个节点之和。
- 然后从节点序列中去除这两个节点,加入它们的父节点到序列中。
- 重复上面两个步骤,直到节点序列中只剩下唯一一个节点。这时一棵最优二叉树就已经建成了,它的根就是剩下的这个节点。
比如上面的例子,哈弗曼树建立的过程如下:
1) 列出原始的节点数据:
2)
将最小的两个节点C和E结合起来:
3)
再将新的节点和A组合起来
4)
再将D节点加入
5)
如此循环,最终得到一个最优二叉树
生成的数据文件长度为: 3*6 + 1*15 + 4*2 + 2*9 + 4*1 = 63
下面我们用逆推法来证明对于各种不同的节点序列,用哈弗曼算法建立起来的树总是一棵最优二叉树:
- 当这个过程中的节点序列只有两个节点时(比如前例中的15和18),肯定是一棵最优二叉树,一个编码为0,另一个编码为1,无法再进一步优化。
- 然后往前步进,节点序列中不断地减少一个节点,增加两个节点,在步进过程中将始终保持是一棵最优二叉树,这是因为:
- 按照哈弗曼树的建立过程,新增的两个节点是当前节点序列中最小的两个,其他的任何两个节点的父节点都大于(或等于)这两个节点的父节点,只要前一步是最优 二叉树,其他的任何两个节点的父节点就一定都处在它们的父节点的上层或同层,所以这两个节点一定处在当前二叉树的最低一层。
- 这两个新增的节点是最小的,所以无法和其他上层节点对换。符合我们前面说的最优二叉树的第一个条件。
- 只要前一步是最优二叉树,由于这两个新增的节点是最小的,即使同层有其他节点,也无法和同层其他节点重新结合,产生比它们的父节点更小的上层节点来和同层 的其他节点对换。它们的父节点小于其他节点的父节点,它们又小于其他所有节点,只要前一步符合最优二叉树的第二个条件,到这一步仍将符合。
这样一步步逆推下去,在这个过程中哈弗曼树每一步都始终保持着是一棵最优二叉树。
Dijstra单源最短路径算法
单元最短路径:给定顶点到其它任一顶点的最短路径
Dijstra单源最短路径算法的基本思想是,设置顶点集合S并不断地作贪心选择来扩充这个集合。一个顶点属于集合S当且仅当从源到该顶点的最短路径长度已知。
初始时,S中仅含有源。设u是G的某一个顶点,把从源到u且中间只经过S中顶点的路称为从源到u的特殊路径,并用数组dist记录当前每个顶点所对应的最短特殊路径长度。Dijkstra算法每次从V-S中取出具有最短特殊路长度的顶点u,将u添加到S中,同时对数组dist作必要的修改。一旦S包含了所有V中顶点,dist就记录了从源到所有其它顶点之间的最短路径长度。
例如,对下图中的有向图,应用Dijkstra算法计算从源顶点1到其它顶点间最短路径的过程列在下表中。
Dijkstra算法的迭代过程:
迭代 |
S |
u |
dist[2] |
dist[3] |
dist[4] |
dist[5] |
初始 |
{1} |
- |
10 |
maxint |
30 |
100 |
1 |
{1,2} |
2 |
10 |
60 |
30 |
100 |
2 |
{1,2,4} |
4 |
10 |
50 |
30 |
90 |
3 |
{1,2,4,3} |
3 |
10 |
50 |
30 |
60 |
4 |
{1,2,4,3,5} |
5 |
10 |
50 |
30 |
60 |
***Floyd算法求两个顶点间的最短距离***
Floyd算法的基本思想如下:从任意节点A到任意节点B的最短路径不外乎2种可能,1是直接从A到B,2是从A经过若干个节点X到B。所以,我们假设Dis(AB)为节点A到节点B的最短路径的距离,对于每一个节点X,我们检查Dis(AX) + Dis(XB) < Dis(AB)是否成立,如果成立,证明从A到X再到B的路径比A直接到B的路径短,我们便设置Dis(AB) = Dis(AX) + Dis(XB),这样一来,当我们遍历完所有节点X,Dis(AB)中记录的便是A到B的最短路径的距离。
Floyd算法由三个for循环构成,所以时间复杂度为O(n3)
最小费用最大流
对于流f,每次选择最小费用增广链进行改进,直到不存在增广链为止。这样的得到的最大流必然是费用最小的。
增广链,指某可行流上,沿着从始点到终点的某条链上每条弧的前向弧都大于零。也称为“可改进流”
假如有这么一条路,这条路从源点(Source点)开始一直一段一段的连到了汇点(Sink点),并且,这条路上的每一段都满足流量 < 容量。那么,我们一定能找到这条路上的每一段的(容量-流量)的值当中的最小值delta。我们把这条路上每一段的流量都加上这个delta,一定可以保证这个流依然是可行流。这样我们就得到了一个更大的流,他的流量是之前的流量+delta,而这条路就叫做增广路径。
最大流最小割定理(max flow/min cut theory): 任意一个流网络的最大流量等于该网络的最小割。
什么是割?对于一个图中的两个节点来说,如果把图中的一些边去掉(流量减为0),刚好让他们之间无法连通的话,这些被去掉的边组成的集合就叫做割了,最小割就是指所有割中权重之和最小(流量减少最小)的一个割。
最小费用最大流
若流f满足:
(a) 流量V(f)最大。
(b) 满足a的前提下,流的费用Cost(f) = 最小。
就称f是网络流图G的最小费用最大流。