贪心算法,顾名思义就是贪得当前情况下的最优解(局部最优解),在某些情况下,每次的选择如果都依赖于前面的依次选择而不受后续操作的影响,局部最优解组成全局最优解(最优子结构),就可以用贪心法求解。
贪心法的基本概念很简单,但是可以有很多经典的应用:
目录
背包问题
最优装载问题
最优装载问题是最简单、最容易理解的贪心背包问题。给出n个物体,第i个物体的重量为wi,不可分割。选出尽量多的物体,使总质量不超过C。
由于只关心数量,所以装轻的性价比要比装重的高——先把所有物体按照重量排序,再从小的开始放进背包,直到装不下为止。
部分背包问题
给出n个物体,第i个物体的重量为wi,价值为vi。在总质量不超过C的情况下让总价值尽量高。每一个物体可以只取一部分。
和上一道题不同,这里质量和价值都是需要考虑的因素。“总质量限定的情况下价值尽量高”,也就是说价值和质量的“价值密度”要尽量高。所以将所有物体的价值密度(double价值/质量)从大到小排序后放入背包,直到装满为止。
一般情况下除了最后一个物体,其他放进背包的物体都不分割。
乘船问题
n个人,其中第i个人重量为wi。每艘船最大载重量为C,且最多只能做俩人。要求运送所有人的最少的船只数。
这类题的考虑思路是,对于最轻的人,安排最重的人和他坐一条船,这样“浪费”才会最少。如果最重的人和最轻的人一条船都无法承载,那么最重的人只能单独乘一条船,最轻的人再尝试和第二轻的人共渡。
代码实现方面,可以用两个下标i和j表示当前数组的首端(最轻的人所在位置)和末端(最重的人所在位置)。在循环中,如果最轻的人和最重的人可以共渡,则i++并j--。如果不行,则只需要j--。直到全部上船为止。
该程序的复杂度仅为O(n),显然是最优算法。
区间问题
选择不相交区间
数轴上有n个开区间(ai,bi),要求尽量选择多个区间使其不相交。
不相交区间问题的解决首先将这些开区间按照右端点bi从小到大排序,并取排序后的第一段(这一段一定要取),接着按照bi从小到大遍历,判断如果遍历到的一段的ai在前一段的bi-1之后,则可以取这一段。
关于为什么要取第一段,可以分两种情况考虑:
第一种情况是b1<b2且a1>a1,也就是第一段处在被第二段“包围”的状态。这种情况下选小区间更划算吗,因为不仅本次取的数目不会减小,还给后续区间留下了位置。
第二种情况是b1<b2且a1<a2,这种情况也很好判断,由于第一段在前面“伸出来”的一段不会对任何后续产生影响,后面部分还短小(不会对后续造成影响),所以果断选第一段。
区间选点问题
数轴上有n个闭区间[ai,bi]。取尽量少的点,使得每个区间内都至少有一个点(不同区间内含的点可以是同一个)。
首先一个基本思想是,如果一个区间被另一个大区间“包裹”,那么在小区间内取点则大区间一定也满足。所以在区间包含情况下,大区间不需要考虑。
同理把所以区间按b从小到大排序(b相同时a从大到小排序->区间包含的情况,小区间在前面)。同样要取第一个区间,但是取的是第一个区间的最后一个点,那样可以尽量多满足后续区间。接着在当前b后找到第一个a,取该a所对应区间的末端点,同理可以满足多段。
区间覆盖问题
数轴上有n个闭区间[ai,bi],选择尽量少的区间覆盖一条指定线段[s,t]。
本题思路仍然是区间包含和排序扫描,与上面不同的是需要进行一次预处理——将所有区间在起点s之前的所有部分都切掉,选择此时起点为s的最长区间,并将“新起点”设置为该最长区间的最右端点,重新开始一遍操作。递归或循环都可以解决。
Huffman编码
还记得之前提到的文件压缩吗?那就是Huffman编码的典型应用。可以将所有字符出现的频率列成一张表P并进行排列,然后创建一个新节点队列Q,每次合并两个节点后把新节点放到队列Q中。合并后的频率一定比合并前频率和大,所以Q内部一定是有序的。
类似于有序表的合并过程,每次检查P和Q的首元素即可找到频率最小的元素,时间复杂度为O(n)。排序后总程序的复杂度为O(nlogn)。