【算法设计与分析】贪心算法学习笔记

本文探讨了贪心算法在活动选择、任务调度、找零钱、Huffman编码、最小生成树等场景的应用,以及正确性证明方法和优化处理。通过归纳法和交换论证法,揭示了贪心算法的优势与挑战,以及如何通过预处理和特定条件判断保证局部最优解的全局有效性。
摘要由CSDN通过智能技术生成

Greedy Approach

贪心算法:一种”短视“的,只考虑局部最优的方法(不像DP会考虑子问题的计算结果)

因此贪心法往往时间效率更高,但论证贪心算法的正确性也较为困难。

正确性证明常见方法:1.归纳法(对算法执行步数) 2.交换论证法(反证)


1. 活动选择问题

活动时间两两不能重叠,问能进行的最大活动数。

该问题等效于线段覆盖问题中的 最多不相交线段覆盖。

算法:按照右端点(活动结束时间)升序排序,依次添加线段。

正确性证明思路:对算法执行步数进行归纳。归纳基础:先证明存在一个最优解包含右端点最小的区间(否则可以进行替换)。归纳步骤:对选出的活动个数k进行归纳,证明最优解去掉前k个的活动,是剩下集合的最优解,否则矛盾,必然存在对剩下的集合的最优解包含第一个活动,归纳结论成立。


2. 任务调度问题

某工厂需要为n个客户提供任务,给定n个客户的任务的完成耗时 T = < t 1 , … , t n > T= <t_1,\dots,t_n> T=<t1,,tn>,以及n个客户对完成时间的要求 D = < d 1 , … , d n > D=<d_1,\dots,d_n> D=<d1,,dn> 。求调度方案 f f f,使得所有客户延迟时间的最大值最小。


数学建模: min ⁡ f max ⁡ i { f ( i ) + t i − d i } \min\limits_f \max\limits_i \{f(i)+t_i -d_i\} fminimax{f(i)+tidi} (且 f ( i ) f(i) f(i)保证维修时间互不重叠)

算法:根据截止时间 d n d_n dn 从小到大排序即可(即不存在逆序)

正确性证明:可通过交换论证法,将最优解变换成无逆序的情形(相邻的两两交换,总交换次数必然有限),最大延迟不会变差。


贪心算法得不到最优解的处理方法

  • 输入参数分析

e.g. 找零钱问题:有n种硬币(数量不限),给出其重量和价值。需要付款的总价格为Y,求如何选择面值使得总重量最低。

多重背包问题:可使用DP法,时间复杂度为 O ( n Y 2 ) O(nY^2) O(nY2)


考虑贪心算法:对单位价值重量按最大到小排序,优先选相对轻的(编号大)

记使用前k种零钱,总钱数为y时,利用贪心法所获得重量为 G k ( y ) G_k (y) Gk(y),可得
G k ( y ) = w k ⌊ y v k ⌋ + G k − 1   ( y   m o d   v k ) G_k(y) = w_k \lfloor\dfrac y {v_k} \rfloor+G_{k-1}\ (y \ mod \ v_k) \\ Gk(y)=wkvky+Gk1 (y mod vk)
观察:当n等于1,2时,贪心法是最优解。

当n=2时,对DP中的递推式 F k ( y ) = min ⁡ { F k − 1 ( y − x k v k ) + w k x k } F_k(y) = \min\limits_{} \{F_{k-1}(y-x_kv_k)+w_k x_k \} Fk(y)=min{Fk1(yxkvk)+wkxk}。可证明当 x 2 x_2 x2取得最大值时得到最优解(和贪心法得到的解一样)

可证明定理:若对前k种硬币使用贪心法都能得到最优解,则对前k+1种硬币使用也得到最优解的充要条件为 ∀ y ∈ N , G k + 1 ( y ) ≤ G k ( y ) \forall y \in N, G_{k+1}(y) \le G_k(y) yN,Gk+1(y)Gk(y)

  • 但需要对所有非负整数y进行验证,这在实际运算中不可行。需要其他条件。


判别条件定理:(检验贪心法是否产生最优解)
G k ( y ) = F k ( y ) , k ∈ N v k + 1 = p v k − δ G_k(y) = F_k(y),k \in N \\ v_{k+1} = pv_k - \delta Gk(y)=Fk(y),kNvk+1=pvkδ
则下列命题等价:

条件(1): G k + 1 ( y ) = F k + 1 ( y ) , ∀ y ∈ N G_{k+1}(y) = F_{k+1}(y),\forall y \in N Gk+1(y)=Fk+1(y),yN

条件(2): G k + 1 ( p v k ) = F k + 1 ( p v k ) G_{k+1}(pv_k) = F_{k+1}(pv_k) Gk+1(pvk)=Fk+1(pvk)

条件(3): ω k + 1 + G k ( δ ) ≤ p ω k \omega_{k+1}+G_k(\delta) \le p \omega_k ωk+1+Gk(δ)pωk

条件3验证一次只需要 O ( k ) O(k) O(k)时间。一共需要验证 O ( k n ) O(kn) O(kn)的时间。

条件2则帮助找反例,“一点定理”


  • 近似误差分析

    典例:集合覆盖问题(Set Cover Problem)

    属于NP-难问题;可以用贪心法进行近似,近似的因子为logn数量级。


3. Huffman编码

二元前缀码:prefix-free(从而保证解码唯一性)

前缀码的二叉树表示:叶子结点对应编码

平均传输位数: B = ∑ i = 1 n f ( x i ) d i B = \sum\limits_{i=1}^n f(x_i)d_i B=i=1nf(xi)di


Huffman树算法:

归纳证明:将两个最小的子结点合并成新结点,得到的新二叉树也是一个最优二叉编码树(否则可推出矛盾)

引理:对频率最小的两个字符,存在一种最优的编码方式使其是二叉树中的兄弟。

应用:文件二分归并

等效于建立二叉树,归并代价最小时恰好对应huffman树


4. 最小生成树

回顾图论中关于生成树的性质:

1)T是n阶连通图G的生成树当且仅当T有n-1条边且无圈

2)对G的生成树T,若 e ∉ T e \notin T e/T,那么添加e后包含一个圈

3)剔除2)中圈C的任意一条边,可以得到G的生成树


生成树性质的应用:

约束:不构成回路 截止:边数达到n-1

改进生成树:用回路中权值更小的边来替代


Prim算法

算法步骤:随意选择一个点加入已选集合 S 1 S_1 S1,然后寻找 S 1 S1 S1到剩余点集 S 2 S_2 S2中权值最小的一条边,将对应的 S 2 S_2 S2 中的点加入 S 1 S_1 S1,直到所有点都加入为止


算法正确性证明

主要利用性质3)和反证法(假设任意最小生成树都不包含某条边)。如果存在权值更小的边且未被加入生成树,可以连接该条边,再从圈中选择一条边删去(必然存在连接 S 1 S_1 S1 S 2 S_2 S2的边,否则不可能是连通树)得到的还是生成树。

证明方法:添加权值最小的边,再从圈中删除。


时间复杂度分析

需要添加n个结点,每次寻找最短边的时间复杂度为 O ( n ) O(n) O(n) 。实现方式:用距离数组d[i]记录 S 2 S_2 S2中的点到 S 1 S_1 S1的最短距离,只需要在每次添加结点时进行更新即可。更新的方法:只需要对V2中的点,检查到新添加到S1中的点的距离是否小于原来的值,若则进行更新,因此总的找最小和更新值的时间复杂度均为 O ( n ) O(n) O(n),因此时间复杂度为 O ( n 2 ) O(n^2) O(n2)


优化:维护一个最小堆

先对m条边进行建堆,则时间复杂度为 O ( m ) O(m) O(m),然后按从小到大的顺序遍历所有边,直到找到第一条连接S和V-S的边。每次寻找最小边是 O ( 1 ) O(1) O(1),但删除元素(优先队列首元素出队)的时间复杂度是 O ( m ) O(m) O(m)的,因此总时间复杂度为 O ( m log ⁡ m ) = O ( m log ⁡ n ) O(m\log m) = O(m\log n) O(mlogm)=O(mlogn)


Kruskal算法

算法步骤:等价类(连通分支)

算法思想:考察当前最短边 e e e,若它与正在构建的最小生成树 T T T 不构成回路,则将该条边加入T中,否则不加入,继续检查下一条边。直到选择了n-1条边为止。

理解:构成回路 —— 相当于该条边连接的两个端点已经位于一个连通分支内(或者属于同一个等价类)


算法正确性证明

归纳证明:利用短接(边的收缩;类似于将huffman二叉树两个最小顶点合并)

先短接最短的边,对结点个数减少的情况利用归纳假设,然后恢复这条边。反证:前提是首先需证明存在一棵包含这条最短边(显然)


算法复杂度分析

预处理:按照边的权值大小进行排序,时间复杂度为 O ( m log ⁡ m ) O(m\log m) O(mlogm)

关键步骤:如果判定两个顶点属于一个连通分支?

  • 使用并查集维护和路径压缩算法

    使用UNION函数和FIND函数。

    时间复杂度:对于每条边,查找两个顶点是否已经连通,需要 O ( m α ( n ) ) O(m\alpha(n)) O(mα(n)) 的时间。其中 α \alpha α可认为是增长很缓慢、趋近于常数的因子;总时间复杂度瓶颈为排序,需要 O ( m log ⁡ m ) = O ( m log ⁡ n ) O(m\log m) = O(m\log n) O(mlogm)=O(mlogn)的时间。

  • 也可采用标记数组FIND:用编号来标记当前顶点所在的分支,因此每次更新需要进行分支的合并(更改个数较少的分支编号。

    可以证明更新FIND数组的总时间复杂度不超过 O ( n log ⁡ n ) O(n\log n) O(nlogn)。因为每次合并集合之后,集合中的顶点个数至少增加一倍,因此合并次数不超过 log ⁡ n \log n logn次。

    由上述分析可知,总时间复杂度的瓶颈依然为排序过程。


比较:Prim算法和Kruskal算法

可以理解为prim算法围绕顶点出发,kruskal算法则是处理边的。

复杂度比较:考虑正常情形下的prim算法,时间复杂度 O ( n 2 ) O(n^2) O(n2);此时选择何种算法取决于图中边的疏密程度。若为稀疏图则选择Kruskal,反之选择Prim。


5. Dijkstra算法

目标问题:单源最短路径问题

使用条件:不存在负权边的情况

算法实现:贪心策略(每次选择V-S中距离起点最近(路径只能途径S中的点)的点,加入集合S中,并更新距离数组的值。


算法正确性证明

对集合S中的顶点个数做归纳法。

归纳基础:首先n=1时显然成立。

归纳步骤:证明对选入S的k个点,此时dist数组中存储的已经是起点到对应点的最短路径。采用反证法,若存在一条其他路径L更短,设该路径第一次离开S的边为<x,y>,其中x属于集合S。由于算法在这一步选择了顶点v,就说明dist[v] <= dist[y],可推出矛盾(从顶点y到v的顶点非负,此处利用了边的权值一定是非负的


时间复杂度分析

每次遍历dist数组选择最小值,时间复杂度为 O ( n ) O(n) O(n);然后添加新的顶点后,只需遍历所有顶点,更新所有和新加入顶点相邻的顶点的距离值,时间复杂度也为 O ( n ) O(n) O(n)。因此总时间复杂度为 O ( n 2 ) O(n^2) O(n2)


比较:Floyd算法

  • 动态规划,可以用来解决多源最短路径问题;可以解决负权边的问题(但不能解决负权回路)


拓展:Bellman-Ford算法

  • 可以用来求解负权边问题

  • 但注意如果图中存在负权环则无解


6. 覆盖问题

区间完全覆盖问题

问题:给定一个长度为m的区间([start, end]),再给出n条线段的起点s[i]和终点e[i](闭区间),求最少使用多少条线段可以将整个区间完全覆盖?

image-20210427154535354

预处理:对区间左端点进行排序。

局部最优:从左端点不大于目标区间左端点的所有区间内,选择右端点最大的(尽可能多覆盖)


区间选点问题

问题描述:给定n个闭区间的起点s[i]和终点e[i](均为整数),以及n个正整数 c 1 , c 2 … , c n c_1,c_2…,c_n c1,c2,cn。求元素最少的整数集Z,使得第i个区间中至少有 c i c_i ci个的整数点落在Z内。(保证 c i c_i ci <= e[i]-s[i]+1)


预处理:对区间右端点进行排序。(why ?)

局部最优:对当前右端点最小的区间而言,若还需选择t个点进行覆盖,则在扫描该区间时必须全部选择(否则更靠后就无法覆盖了)


算法正确性证明

1.归纳基础:证明存在一种最优解包含区间1的最右边 c 1 c_1 c1个点

2.归纳步骤:对算法执行的步数k进行归纳(仿造活动选择问题)


集合覆盖问题

问题描述:假设存在下面需要付费的广播台,以及广播台信号可以覆盖的地区。 如何选择最少的广播台,让所有的地区都可以接收到信号?

image-20210427154808172

更一般化的表述:

image-20210427154827726
  • 属于NP-难问题,但可以使用贪心算法求近似解。

贪心算法的直接思想:每次选择未被覆盖元素最多(可理解为平均代价最小)的集合

拓展:估计贪心算法的解的误差

可证明贪心算法所得解 g 和真正最优解 c 满足:
g = O ( c log ⁡ n ) , n = ∣ X ∣ g = O(c\log n), n = |X| g=O(clogn),n=X


贪心算法总结

适用条件:求解优化问题

思路:自顶向下,只顾眼前的”局部最优“(需要对算法正确性进行

时间复杂度:算法本身往往较简单,主要时间复杂度取决于预处理操作(如排序算法)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值