#04贪心法

要点:

贪心法的基本思想、基本要素与求解步骤;

贪心法的应用。

难点:

贪心法的最优子结构性质与贪心选择性质。


贪心法的基本思想

每个阶段的决策一旦做出就不可更改。不允许回溯。

并不从整体最优考虑,所作出的选择只是在某种意义上的局部最优选择,并希望该局部最优选择可以导致一个全局最优解。

但对许多问题(如单源最短路经问题、最小生成树问题等)能产生整体最优解。

即使贪心算法不能得到整体最优解,其最终结果却是最优解的很好近似。

贪心法的条件判断

贪心选择性质【归纳法证明】 最优子结构性质【反证法证明】

 贪心法的解决步骤

分解: 将原问题分解为若干个相互独立的阶段。

解决: 对于每个阶段依据贪心策略进行贪心选择,求出局部的最优解。

合并: 将各个阶段的解合并为原问题的一个可行解。


会场安排问题

设有n个会议的集合C={1,2,…,n},其中每个会议都要求使用同一个资源(如会议室),而在同一时间内只能有一个会议使用该资源。

每个会议i都有要求使用该资源的起始时间bi和结束时间ei,且bi < ei 。如果选择了会议i使用会议室,则其在半开区间[bi, ei)内占用该资源。

贪心策略选择

选择最早开始时间且不与已安排会议重叠的会议: 但如果会议的使用时间无限长,如此选择策略就只能安排1个会议来使用资源。不可行。

选择使用时间最短且不与已安排会议重叠的会议: 但如果会议的开始时间最晚,如此选择策略也就只能安排1个会议来使用资源。不可行。

选择最早结束时间且不与已安排会议重叠的会议: 如果选择开始时间最早且使用时间最短的会议,较为理想。而此即结束时间最早的会议。可行。

求解步骤

①初始化,并按会议结束时间非减序排序【结束时间从早到晚】

开始时间存入数组B,结束时间存入数组F中; 按照结束时间的非减序排序,B需做相应调整; 集合A存储解。如果会议i在集合A中,当且仅当其被选中。

②根据贪心策略,做第一次贪心选择。

首令A[1] = 1;

③依次扫描每一个会议,直至所有会议检查完毕。

如果会议i的开始时间不小于最后一个选入A中的会议的结束时间,则将会议i加入A中; 否则,放弃并继续检查下一个会议与A中会议的相容性。

//基于贪心法求解会场安排问题的伪代码如下:
GREEDY-ACTIVITY-SELECTOR(B,E) //B,E为会以的开始时间和结束时间
  SORT-ASC-BY-F(B,E) //对E按减序排序并同时调整S
  n = B.length
  A = {1} //首先选择会议1
  k = 1 //已被选择会议集合中最晚结束的会议
  for m = 2 to n
    if B[m] >= E[k] //会议m的开始时间不小于会议k的结束时间
	    then A = A ∪ {m} //将会议m加入到集合A中
           k = m //此时集合A中最晚结束的会议为m
    return A

贪心选择性质【归纳法证明】 最优子结构性质【反证法证明】 略~

 (a) 按照会议结束时间非减序排序(如采用归并);

(b) 逐个检查剩下会议与已选择会议的相容性。

时间复杂性: (a):O(nlogn),(b):O(n)。->   O(nlogn)

空间复杂性: (a):O(n),(b):O(1)。->    O(n)


哈夫曼 (Huffman) 编码

怎样保证上述编码树所得到的编码总长度(亦即编码树的带权路径长度)最小?

以字符的使用频率作为结点的权构建哈夫曼树,核心思想是让权值较大的叶子离根更近(即频率高的字符编码短)。

贪心策略:

每次从树的集合(初始为n棵平凡树)中取出权值最小的两棵树作为左、右子树,构造一棵新树。新树的根结点的权值为其左右孩子结点权值之和,并将新树插入到树的集合中。

迭代地执行上述步骤,直至树的集合中只剩下一棵树为止,即得到哈夫曼编码树。

求解步骤:

①确定合适的数据结构;

②初始化。

构造n棵结点为n个字符的单结点树集合F={T1,T2,…, Tn},每棵树中只有一个带权的根结点,权值为该字符的使用频率;

③如F中只剩下一棵树,则哈夫曼树构造成功,转步骤6;否则,从集合F中取出权值最小的两棵树Ti和Tj,将它们合并成一棵新树Zk,新树以Ti为左儿子, Tj为右儿子(反之也可以)。新树Zk的根结点的权值为Ti与Tj的权值之和;

④从集合F中删去Ti、Tj,集合F加入Zk;

⑤重复步骤3和4;

⑥从叶子结点到根结点逆向求出每个字符的哈夫曼编码(约定左分支表示字符“0”,右分支表示字符“1”)。则从根结点到叶子结点路径上的分支字符组成的字符串即为叶子字符的哈夫曼编码。 算法结束。

基于贪心法求解哈夫曼编码问题的伪代码如下:
HUFFMAN(C) //C为具有n个待编码字符的集合
  n = C.length
  Q = C //Q为以freq为关键字的最小优先队列,存放已组合的二叉树
  for i = 1 to n-1
    z  allocate a new node //构建一个以z为根的一个新二叉树
    x = EXTRACT-MIN(Q) //从Q中出列一棵树x
	  z.left = x //将树x作为z的左子树
    y = EXTRACT-MIN(Q) //从Q中出列一棵树y
    z.right = y //将树y作为z的右子树
    z.freq = x.freq + y.freq //以x和y的freq作为z的freq
    INSERT(Q, z) //将构建的新树插入到Q中
  return EXTRACT-MIN(Q)

字符对应的哈夫曼编码:自叶子结点到根反向遍历

贪心选择性质【证明】 最优子结构性质【证明】 略~

时空复杂性分析 

未采用特殊数据结构时算法的复杂性:

时间复杂性:O(n2) 空间复杂性:O(n)(视排序是否为in-place而定)

采用最小二叉堆时算法的复杂性:

时间复杂性:O(nlogn) 空间复杂性:O(n)


单源最短路径 (SSSP)

两个结点u和v之间的最短带权路径长度(u, v):               

单源最短路径问题要求:计算源点s到其他各个结点v的最短路径(及其)长度δ(s, v)。

狄克斯特拉算法 Dijkstra

迪杰斯特拉算法主要特点是从起始点开始,采用贪心算法的策略,每次遍历到始点距离最近且未访问过的顶点的邻接节点,直到扩展到终点为止

贪心选择性质【证明】 最优子结构性质【证明】 略~

时空复杂性分析 

时间复杂性:

EXTRACT-MIN()的时间复杂性为O(logn);

二重循环的执行次数为(n-1)+(n-2)+…+1 = n(n-1)/2,即时间复杂性为O(n^2)。

所以,该算法的时间复杂性为O(n^2)

空间复杂性:

堆排序为in-place排序。辅助变量O(n);

所以,该算法的空间复杂性为O(n)。 

详细图解过程: 

图论:Dijkstra算法——最详细的分析,图文并茂,一次看懂!-CSDN博客


最小生成树 MST

设G=(V, E)是无向连通带权图。E中每条边(v, w)的权为c[v][w]。最小生成树:在G的所有生成树中,耗费最小的生成树。

应用:

1)在设计通信网络时,用图的顶点表示城市,用边(v,w)的权c[v][w]表示建立城市v和城市w之间的通信线路所需的费用,则最小生成树就给出了建立通信网络的最经济的方案

2) 城市之间的公路网等都是最小生成树的实际应用例子。

Prim算法

算法实现方法:

  1. 将连通网中的所有顶点分为两类(假设为 A 类和 B 类)。初始状态下,所有顶点位于 B 类;
  2. 选择任意一个顶点,将其从 B 类移动到 A 类;
  3. 从 B 类的所有顶点出发,找出一条连接着 A 类中的某个顶点且权值最小的边,将此边连接着的 A 类中的顶点移动到 B 类;
  4. 重复执行第 3  步,直至 B 类中的所有顶点全部移动到 A 类,恰好可以找到 N-1 条边。
// Prim算法的伪代码:
MST-PRIM(G, w, r) //r为根结点
  for each u∈G.V //初始化
    u.key = ∞ //u.key为连接u和树中结点所有边中最小边的权重
    u.p = NIL //u.p为结点u的父结点
  r.key = 0
  Q = G.V //极小优先队列(v.key)。后述的U=V-Q,V-U=Q
  while Q ≠ Φ
    u = EXTRACT-MIN(Q)
    for each v∈ (G.Adj[u] ∩ Q)
      if(w(u, v) < v.key
        v.p = u
        v.key = w(u, v) //DECREASE-KEY
时空复杂性 

时间复杂性:

使用数组,则为O(|V|^2),这里V为图中结点。

使用二叉堆,则O(|V|log|V|)。

空间复杂性:

O(|V|)。

​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​prim算法(普里姆算法)详解 (biancheng.net)


Kruskal算法

设G=(V,E)是具有n个结点的无向连通带权图,U是V。的一个非空子集。最小生成树的一个很重要的性质: 若(u, v)是一条具有最小权值的边,其中u∈U,v∈V-U,则必存在一棵包含边(u,v)的最小生成树。

Kruskal算法的关键是怎样判断加入某条边后图T会不会出现回路;

Kruskal算法的求解步骤:

① 初始化。将图G的边集E中的所有边按权从小到大排序,边集TE={},把每个顶点都初始化为一个孤立的分支,即一个顶点对应一个集合;

② 寻找权值最小的边。在E中寻找,得边(i,j);

③ 如不形成回路,则将边(i,j)加入TE。如结点i和j位于两个不同连通分支,则将边(i,j)加入边集TE,并执合并操作将两个连通分支进行合并;

④将新加入的边从E中删除。即E=E-{(i,j)};

⑤ 判断算法是否结束。如连通分支数目不为1,转步骤2;否则,算法结束,生成最小生成树T。 

//Kruskal算法的伪代码:
MST-KRUSKAL(G, w)
  A = Φ //最优解。边的集合
  for each v∈G.V
    MAKE-SET(v) //创建|V|棵仅含一个结点的树
  sort the edges of G.E into non-decreasing order by weight w
  for each edge(u, v)∈G.E //in nondecreasing order by weight w 
    if(FIND-SET(v) ≠ FIND-SET(u)) //v和u不属于同一棵树
      A = A ∪{(u, v)} //将边(u,v)加入到A中
      UNION(u, v) //基于边(u,v)合并两棵树
  return A

贪心选择性质【证明】 最优子结构性质【证明】 略~

Kruskal算法~问题1:选取权值最小的边的同时,要判断加入该条边后树中是否出现回路;

                     问题2:不同的连通分支如何进行合并。

时空复杂性

时间复杂性: O(|E|log|E|) 或 O(|E||V|) 。E为边,V为节点。

空间复杂性: O(|E|)。

【算法】最小生成树——Prim和Kruskal算法_kruskal最小生成树图解-CSDN博客
两种算法的比较

Kruskal适用于稀疏图,而Prim适用于稠密图;

从时间上讲,Prim算法的时间复杂度为O(V2)或O(ElogV),Kruskal算法的时间复杂度为O(ElogV)。 从空间上讲,显然在Prim算法中,只需要很小的空间就可以完成算法,因为每一次都是从个别点开始出发进行扫描的,而且每一次扫描也只扫描与当前顶点集对应的边。但在Kruskal算法中,因为时刻都得知道当前边集中权值最小的边在哪里,这就需要对所有的边进行排序,对于很大的图而言,Kruskal算法需要占用比Prim算法大得多的空间。

  • 19
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值