贪心算法的基本思想+任务安排问题、哈夫曼树、最小生成树算法(prim、kruskal)

什么是贪心算法

顾名思义,贪心算法是通过判断当前状态下看起来最好的结果,作为最好的结果。
一般来说,我们使用贪心算法的情况为需要一步步解决的问题,其中的每一个步骤都有一系列的选择,
比如01背包问题,我们有C容量的背包,上来就选择能装下的最大价值物品,然后对剩下容量继续上述操作。
(当然,如果知道的话,这种做法是错误的,我们将在后序给出讲解)

所以问题就来了,我们通过贪心算法一定能得到最优解吗?
还真不一定,我们需要进行证明,证明的关键点是优化子结构贪心选择性

优化子结构在dp问题中也是谈了很多次了,也就是整体最优是否能保证局部最优的问题,
这里我们要证明整体的最优解减去贪心的选择后剩下的部分仍为子问题的最优解。
(因为有子结构的存在,所以我们每一次的选择其实都可以通过第一步的证明类比出来。)

贪心选择性则需要强调一下,主要的落点还是选择,即我们选择的解是否一定在整体的最优解里面。

其实通过上面的例子和我们的分析,大约就能猜到dp和贪心之间有着千丝万缕的关系:

  • 首先吧,他们解决的问题都是存在子结构的,而且都需要保证子结构最优
  • dp说白了就是通过一个个的尝试,在最后给出一个最优解,
    而我们的贪心算法则是通过证明来给出一个选择方式,直接判断出哪一个选择是最优的,不需要比较,
    所以贪心是要比dp快很多的,但是需要一个证明。
  • 似乎看起来贪心能使用的情况下,dp都是成立的,其实很多问题确实如此,毕竟一次次比较也很难得不到最优解,但是对于最小生成树算法,比如prim和kruskal算法,这两个都是贪心算法,而dp的算法却很难给出。

任务安排问题

纸上谈兵终究很难抓住重点,我们先举一个例子,就是经典的任务安排问题。

定义

有一系列的任务,每一个任务都有开始和结束的时间,我们需要选择尽可能多但是不可以重叠的问题集。

既然是贪心算法,我们就需要有一个选择最优的判准,下面先给出几个

  1. 按照任务开始的时间,越早开始越好
  2. 按照任务结束的时间,越早结束越好
  3. 按照任务的时间长度,越短的越先开始。

这里我们直接给出答案,最早结束(或者最晚开始的活动也行)这一选择方式是可以证明的。

证明

贪心最重要的就是证明了,接下来我们看看应该怎么进行。

首先是优化子结构,我们还是采用反证法的方式。
我们贪心的选择是最早结束的活动a,最优解为S,那么X = S-a也一定为最优解。
(子结构为整体减去活动a)
如果不是最优解,那么我们就能找到另外一个X’,他比S‘的活动更多,当我们加上活动a时,很明显有:
X’+a > S’+a = S,而和S是最优解矛盾,所以子结构X一定最优,故得证。

然后是贪心选择性,也就是我的选择一定是在整体的最优解里面。
我们假设最优解为S,整个活动的最早结束活动为a,S中最早结束的活动为b
如果a == b,得证;
如果不等,因为a结束比b早,所以a和S-b一定没有交集,此时我们将b去掉,然后加上a,得到的S‘和S任务数相同,也为一个最优解,所以我们选择的a一定在一个最优解里面,得证。
(如果是最晚开始的活动也同理,这里不做证明)

在证明之后,我们就可以直接使用了,那就没什么可以讲的了。

哈夫曼树

之前也发过哈夫曼树的博客,但是没有讲到贪心的思想,这次补一下。

最开始没有接触贪心的时候只是感觉哈夫曼树属于一种很有意思的算法,这次受教了。

在这里我们就不再提定义了,直接看贪心的部分,这里的贪心是将两个最小的结点取出来,合并为一个大的结点(这里的大小指的是概率),这样能保证我们得到的编码树代价最小。

优化子结构

我们假设一棵树T是集合G最优的,其中我们贪心的选择是最小的两个结点m、n。
在去掉nm的树T‘中,nm的父节点x作为了新的叶子节点,其值为x = m+n。
如果T’不是G-m-n+x的最优解,那么我们可以找到更优解T‘’,当我们将T’‘加上m、n两个结点,一定是比T的代价还小,和题目不符,所以得证。

(多了一层,就相当于加了一份m+n的代价,对两棵G’构成的树是一样的,还是看剩余部分的代价)

贪心选择性

我们需要证明我们每一次选择的两个最小值一定是在最优解中的。
假设两个最小值为x、y,那么一定存在一棵最优哈夫曼树,其中xy为最后一层的两个相邻叶子结点(编码长度相同,只有最后一位不同。

取一棵最优哈夫曼树中最大深度的两个相邻叶子节点mn,然后我们交换x与n、y与m,那么我们得到了一棵新的哈夫曼树,因为我们将概率更小的元素放在了下层,而代价一定不比之前大(因为可能相同),所以我们得到了一棵最优哈夫曼树,得证。

实现

我们可以使用最小堆来实现,之前关于堆的博客,每一次弹出最小的两个元素,然后构造即可。

最小生成树问题

定义

在一个无向加权图中找出一棵生成树,保证生成树的边长和最小。
比如下面的图,我们需要的就是红色的边,其边之和最小。
贪心算法的基本思想+任务安排问题、哈夫曼树、最小生成树算法(prim、kruskal)_第1张图片
生成树的性质:

  • 生成树一定是包含了图的所有顶点,且没有形成环
  • 生成树的边数为顶点数减一,属于树的性质
  • 最小生成树中任意添加一条边,一定会形成一个环。(图论内容)

接下来我们看一下生成树的一个算法

kruskal

思想:
将所有的边排序,然后从最小的边开始,如果该边的两个顶点不属于一个,那么就划为一个(Union操作),直到所有的顶点都属于一个集合。(其实是边遍历结束,因为每一次都检测代价太大)
个人感觉其实谈不上合并,只需要最开始每一个顶点给一个编号,需要合并的时候修改一下编号就行了。
伪代码:

MST-Kruskal(G,W)
A=Φ;
For V v∈V[G]
	Make-Set(v);
sort(E[G]);//对边进行排序
For (u, v)∈E[G] (按W值的递增顺序)
	If Find-Set(u) != Find-Set(v)
		A=AU{(u, v)};  
       	Union(u, v);
 Return  A

其中V为任取,U为求并操作。

整体的代价就属排序最大,我们取O(mlogm),m为边数

证明

贪心选择性

因为我们每一次选择的是最小边,所以我们需要证明最小边一定在整个的最小生成树中。
我们假设有最小生成树T,其中没有最小边uv,此时我们将最小边加进去,一定会形成一个环,然后我们将其中的一条边删掉(这条边可以是顶点u或者v的另外一条边),就可以再次构成一棵树,此时的树代价是要比之前的小的,所以最小生成树中一定有uv。

优化子结构

先给一个操作,我们定义“-”表示将两个顶点合并成一个顶点的操作
首先假设T为最优的生成树,我们已经知道了T一定含有uv,那么设T’ = T-uv是G-uv的最小生成树。
(将最小生成树T中uv顶点合并,G中uv顶点合并,我们叫这个新结点为a)
如果不是,那么我们就能找到T’’,因为是G-uv的生成树,所以一定包含了a这个顶点,当我们将分隔为原来的u和v时,我们再将uv边加上,发现T’’+uv的生成树代价是要比T小的,和定义不符,得证。

prim

思想:每一次从当前已经遍历的顶点构成的集合(刚开始集合为空)中找连接出去的边的最小值。
比如这张图中我们已经遍历了AE,这个集合连接出去的边有ab、ad、be、de这些,我们取其中最小的ad,同时将取到的顶点划入集合中,直到集合为全集。
贪心算法的基本思想+任务安排问题、哈夫曼树、最小生成树算法(prim、kruskal)_第2张图片
伪代码:

MST-Prim(G,W,r)//输入连通图G,权值函数W,树根r 输出G的一棵以r为根的生成树
C={r};									//顶点集		
T=Φ;									//最小生成树的构成
建堆Q维护C与V-C之间的边
While C != V
	uv=Extract_Min(Q)					//横跨C和V-C
	C = CU{v};      					//顶点划入顶点集
	T = TU{uv}; 						//最小生成树的一条边
	for x∈Adj[v]						//新加入的顶点v的相邻边需要处理
		if x∈C 						//相邻边的两端都在C中,直接删了就行
			将vx从Q中删除
		Else							//如果是新的边,那就是连接C和V-C的边,需要加入Q中
			将vx插入Q
Return  T

解释一下:

  • V为总顶点集,而我们创建的顶点集为C,剩下的就是V-C了
  • Q为存储C和V-C之间的边的堆,其中我们取出的边uv,u在C中而v不在

证明

贪心选择性

我们每一次选择的是点集和点集之外的点构成的最短边uv,我们假设一棵最小生成树T不含有边uv,
当我们在T中添加uv,那么一定会形成环,此时我们将u的另外一条边断开(这条边也是属于连接的边,且一定是要比uv长的),我们新得到的生成树是要比之前的树还小,说明一定含有uv,得证。

优化子结构

我们每一次选择的是C中的顶点u和V-C中的顶点v之间的边(假设已经确定这条边最小)
最开始的时候,C为只有一个元素,所以uv也就是这个点的最小边。
我们还是采用T-uv和G-uv的方式来,和上面的基本相同,就不证了。

一些不能用贪心的例子

首先就是最开始提到的01背包问题,我们在选择了价值最大的物品之后不能证明符合贪心选择性。
但是如果问题改成存在0.1这样的选项,而非一定要都拿走或者不拿,那么贪心就可以解决问题。
(这个有一个叫法叫做拿金砖还是拿金砂的问题)

然后就是硬币找零问题,感觉这两个其实是很相近的问题,在dp中,01背包是有一个吐出来的过程,但是贪心是选好了就不能反悔,但我们不能保证选的一定是最好的。
这应该就是不能的主要原因了吧。

补充一个贪心小问题

这个应该是leecode上的问题:

题目描述:老师想给孩子们分发糖果,有 N 个孩子站成了一条直线,老师会根据每个孩子
的表现,预先给他们评分。
你需要按照以下要求,帮助老师给这些孩子分发糖果:
每个孩子至少分配到 1 个糖果。
相邻的孩子中,评分高的孩子必须获得更多的糖果。
那么这样下来,老师至少需要准备多少颗糖果呢?
示例 1:
输入: [1,0,2]
输出: 5
解释: 你可以分别给这三个孩子分发 2、1、2 颗糖果。
示例 2:
输入: [1,2,2]
输出: 4
解释: 你可以分别给这三个孩子分发 1、2、1 颗糖果。
第三个孩子只得到 1 颗糖果,这已满足上述两个条件。

这里我们的解决思路:
先每一个人发一颗糖。
因为两边都要看,我们就先看一边,从左到右,
如果后面的孩子比前面的高,那么就让后面的孩子糖果数为前面的孩子+1,否则不变;
然后从右到左重复一遍,也是后面的和前面的比较。
当两次遍历结束,我们得到了两组数据,对比两个数组,每一个位置取其中的较大值,最终得到的数组就是答案了。

这个问题说实话,证明起来不太好证,不过确实比较直观,很有贪心内味。

你可能感兴趣的:(算法初入,贪心算法,任务安排问题,哈夫曼树,prim,kruskal)