【算法设计与分析】贪心算法学习笔记
Greedy Approach
贪心算法:一种”短视“的,只考虑局部最优的方法(不像DP会考虑子问题的计算结果)
因此贪心法往往时间效率更高,但论证贪心算法的正确性也较为困难。
正确性证明常见方法:1.归纳法(对算法执行步数) 2.交换论证法(反证)
1. 活动选择问题
活动时间两两不能重叠,问能进行的最大活动数。
该问题等效于线段覆盖问题中的 最多不相交线段覆盖。
算法:按照右端点(活动结束时间)升序排序,依次添加线段。
正确性证明思路:对算法执行步数进行归纳。归纳基础:先证明存在一个最优解包含右端点最小的区间(否则可以进行替换)。归纳步骤:对选出的活动个数k进行归纳,证明最优解去掉前k个的活动,是剩下集合的最优解,否则矛盾,必然存在对剩下的集合的最优解包含第一个活动,归纳结论成立。
2. 任务调度问题
某工厂需要为n个客户提供任务,给定n个客户的任务的完成耗时T=<t1,…,tn>T= <t_1,\dots,t_n>T=<t1,…,tn>,以及n个客户对完成时间的要求 D=<d1,…,dn>D=<d_1,\dots,d_n>D=<d1,…,dn> 。求调度方案 fff,使得所有客户延迟时间的最大值最小。
数学建模:minfmaxi{f(i)+ti−di}\min\limits_f \max\limits_i \{f(i)+t_i -d_i\}fminimax{f(i)+ti−di} (且f(i)f(i)f(i)保证维修时间互不重叠)
算法:根据截止时间 dnd_ndn 从小到大排序即可(即不存在逆序)
正确性证明:可通过交换论证法,将最优解变换成无逆序的情形(相邻的两两交换,总交换次数必然有限),最大延迟不会变差。
贪心算法得不到最优解的处理方法
- 输入参数分析
e.g. 找零钱问题:有n种硬币(数量不限),给出其重量和价值。需要付款的总价格为Y,求如何选择面值使得总重量最低。
多重背包问题:可使用DP法,时间复杂度为O(nY2)O(nY^2)O(nY2)
考虑贪心算法:对单位价值重量按最大到小排序,优先选相对轻的(编号大)
记使用前k种零钱,总钱数为y时,利用贪心法所获得重量为Gk(y)G_k (y)Gk(y),可得
Gk(y)=wk⌊yvk⌋+Gk−1 (y mod vk)
G_k(y) = w_k \lfloor\dfrac y {v_k} \rfloor+G_{k-1}\ (y \ mod \ v_k) \\
Gk(y)=wk⌊vky⌋+Gk−1 (y mod vk)
观察:当n等于1,2时,贪心法是最优解。
当n=2时,对DP中的递推式Fk(y)=min{Fk−1(y−xkvk)+wkxk}F_k(y) = \min\limits_{} \{F_{k-1}(y-x_kv_k)+w_k x_k \}Fk(y)=min{Fk−1(y−xkvk)+wkxk}。可证明当x2x_2x2取得最大值时得到最优解(和贪心法得到的解一样)
可证明定理:若对前k种硬币使用贪心法都能得到最优解,则对前k+1种硬币使用也得到最优解的充要条件为∀y∈N,Gk+1(y)≤Gk(y)\forall y \in N, G_{k+1}(y) \le G_k(y)∀y∈N,Gk+1(y)≤Gk(y)
-
但需要对所有非负整数y进行验证,这在实际运算中不可行。需要其他条件。
判别条件定理:(检验贪心法是否产生最优解)
Gk(y)=Fk(y),k∈Nvk+1=pvk−δ
G_k(y) = F_k(y),k \in N \\
v_{k+1} = pv_k - \delta
Gk(y)=Fk(y),k∈Nvk+1=pvk−δ
则下列命题等价:
条件(1): Gk+1(y)=Fk+1(y),∀y∈NG_{k+1}(y) = F_{k+1}(y),\forall y \in NGk+1(y)=Fk+1(y),∀y∈N
条件(2): Gk+1(pvk)=Fk+1(pvk)G_{k+1}(pv_k) = F_{k+1}(pv_k)Gk+1(pvk)=Fk+1(pvk)
条件(3): ωk+1+Gk(δ)≤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(kn)O(kn)O(kn)的时间。
条件2则帮助找反例,“一点定理”
-
近似误差分析
典例:集合覆盖问题(Set Cover Problem)
属于NP-难问题;可以用贪心法进行近似,近似的因子为logn数量级。
3. Huffman编码
二元前缀码:prefix-free(从而保证解码唯一性)
前缀码的二叉树表示:叶子结点对应编码
平均传输位数:B=∑i=1nf(xi)diB = \sum\limits_{i=1}^n f(x_i)d_iB=i=1∑nf(xi)di
Huffman树算法:
归纳证明:将两个最小的子结点合并成新结点,得到的新二叉树也是一个最优二叉编码树(否则可推出矛盾)
引理:对频率最小的两个字符,存在一种最优的编码方式使其是二叉树中的兄弟。
应用:文件二分归并
等效于建立二叉树,归并代价最小时恰好对应huffman树
4. 最小生成树
回顾图论中关于生成树的性质:
1)T是n阶连通图G的生成树当且仅当T有n-1条边且无圈
2)对G的生成树T,若e∉Te \notin Te∈/T,那么添加e后包含一个圈
3)剔除2)中圈C的任意一条边,可以得到G的生成树
生成树性质的应用:
约束:不构成回路 截止:边数达到n-1
改进生成树:用回路中权值更小的边来替代
Prim算法
算法步骤:随意选择一个点加入已选集合S1S_1S1,然后寻找S1S1S1到剩余点集S2S_2S2中权值最小的一条边,将对应的S2S_2S2 中的点加入S1S_1S1,直到所有点都加入为止
算法正确性证明:
主要利用性质3)和反证法(假设任意最小生成树都不包含某条边)。如果存在权值更小的边且未被加入生成树,可以连接该条边,再从圈中选择一条边删去(必然存在连接S1S_1S1和S2S_2S2的边,否则不可能是连通树)得到的还是生成树。
证明方法:添加权值最小的边,再从圈中删除。
时间复杂度分析
需要添加n个结点,每次寻找最短边的时间复杂度为O(n)O(n)O(n) 。实现方式:用距离数组d[i]记录S2S_2S2中的点到S1S_1S1的最短距离,只需要在每次添加结点时进行更新即可。更新的方法:只需要对V2中的点,检查到新添加到S1中的点的距离是否小于原来的值,若则进行更新,因此总的找最小和更新值的时间复杂度均为O(n)O(n)O(n),因此时间复杂度为O(n2)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(mlogm)=O(mlogn)O(m\log m) = O(m\log n)O(mlogm)=O(mlogn)
Kruskal算法
算法步骤:等价类(连通分支)
算法思想:考察当前最短边 eee,若它与正在构建的最小生成树 TTT 不构成回路,则将该条边加入T中,否则不加入,继续检查下一条边。直到选择了n-1条边为止。
理解:构成回路 —— 相当于该条边连接的两个端点已经位于一个连通分支内(或者属于同一个等价类)
算法正确性证明
归纳证明:利用短接(边的收缩;类似于将huffman二叉树两个最小顶点合并)
先短接最短的边,对结点个数减少的情况利用归纳假设,然后恢复这条边。反证:前提是首先需证明存在一棵包含这条最短边(显然)
算法复杂度分析
预处理:按照边的权值大小进行排序,时间复杂度为O(mlogm)O(m\log m)O(mlogm)
关键步骤:如果判定两个顶点属于一个连通分支?
-
使用并查集维护和路径压缩算法
使用UNION函数和FIND函数。
时间复杂度:对于每条边,查找两个顶点是否已经连通,需要O(mα(n))O(m\alpha(n))O(mα(n)) 的时间。其中α\alphaα可认为是增长很缓慢、趋近于常数的因子;总时间复杂度瓶颈为排序,需要O(mlogm)=O(mlogn)O(m\log m) = O(m\log n)O(mlogm)=O(mlogn)的时间。
-
也可采用标记数组FIND:用编号来标记当前顶点所在的分支,因此每次更新需要进行分支的合并(更改个数较少的分支编号。
可以证明更新FIND数组的总时间复杂度不超过O(nlogn)O(n\log n)O(nlogn)。因为每次合并集合之后,集合中的顶点个数至少增加一倍,因此合并次数不超过logn\log nlogn次。
由上述分析可知,总时间复杂度的瓶颈依然为排序过程。
比较:Prim算法和Kruskal算法
可以理解为prim算法围绕顶点出发,kruskal算法则是处理边的。
复杂度比较:考虑正常情形下的prim算法,时间复杂度O(n2)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(n2)O(n^2)O(n2)
比较:Floyd算法
-
动态规划,可以用来解决多源最短路径问题;可以解决负权边的问题(但不能解决负权回路)
拓展:Bellman-Ford算法
-
可以用来求解负权边问题
-
但注意如果图中存在负权环则无解
6. 覆盖问题
区间完全覆盖问题
问题:给定一个长度为m的区间([start, end]),再给出n条线段的起点s[i]和终点e[i](闭区间),求最少使用多少条线段可以将整个区间完全覆盖?
预处理:对区间左端点进行排序。
局部最优:从左端点不大于目标区间左端点的所有区间内,选择右端点最大的(尽可能多覆盖)
区间选点问题
问题描述:给定n个闭区间的起点s[i]和终点e[i](均为整数),以及n个正整数c1,c2…,cnc_1,c_2…,c_nc1,c2…,cn。求元素最少的整数集Z,使得第i个区间中至少有cic_ici个的整数点落在Z内。(保证 cic_ici <= e[i]-s[i]+1)
预处理:对区间右端点进行排序。(why ?)
局部最优:对当前右端点最小的区间而言,若还需选择t个点进行覆盖,则在扫描该区间时必须全部选择(否则更靠后就无法覆盖了)
算法正确性证明
1.归纳基础:证明存在一种最优解包含区间1的最右边c1c_1c1个点
2.归纳步骤:对算法执行的步数k进行归纳(仿造活动选择问题)
集合覆盖问题
问题描述:假设存在下面需要付费的广播台,以及广播台信号可以覆盖的地区。 如何选择最少的广播台,让所有的地区都可以接收到信号?
更一般化的表述:
- 属于NP-难问题,但可以使用贪心算法求近似解。
贪心算法的直接思想:每次选择未被覆盖元素最多(可理解为平均代价最小)的集合
拓展:估计贪心算法的解的误差
可证明贪心算法所得解 g 和真正最优解 c 满足:
g=O(clogn),n=∣X∣
g = O(c\log n), n = |X|
g=O(clogn),n=∣X∣
贪心算法总结
适用条件:求解优化问题
思路:自顶向下,只顾眼前的”局部最优“(需要对算法正确性进行
时间复杂度:算法本身往往较简单,主要时间复杂度取决于预处理操作(如排序算法)
更多推荐
所有评论(0)