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,使得所有客户延迟时间的最大值最小。


数学建模:min⁡fmax⁡i{f(i)+ti−di}\min\limits_f \max\limits_i \{f(i)+t_i -d_i\}fminimax{f(i)+tidi} (且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)=wkvky+Gk1 (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{Fk1(yxkvk)+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)yN,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),kNvk+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),yN

条件(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=1nf(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_1S1S2S_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(mlog⁡m)=O(mlog⁡n)O(m\log m) = O(m\log n)O(mlogm)=O(mlogn)


Kruskal算法

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

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

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


算法正确性证明

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

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


算法复杂度分析

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

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

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

    使用UNION函数和FIND函数。

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

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

    可以证明更新FIND数组的总时间复杂度不超过O(nlog⁡n)O(n\log n)O(nlogn)。因为每次合并集合之后,集合中的顶点个数至少增加一倍,因此合并次数不超过log⁡n\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](闭区间),求最少使用多少条线段可以将整个区间完全覆盖?

image-20210427154535354

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

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


区间选点问题

问题描述:给定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进行归纳(仿造活动选择问题)


集合覆盖问题

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

image-20210427154808172

更一般化的表述:

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

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

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

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


贪心算法总结

适用条件:求解优化问题

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

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

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐