贪心算法是一个一条路走到底无论如何都要达到目的的一种算法思想,很多的应用都是通过贪心算法实现的,如Huffman Coding,Prim,Kruskal和最小生成树算法。
我们如何才能理解贪心算法???
下面先来看一下经典的背包问题。。。
假设我们有一个可以容纳100kg物品的背包,可以装各种物品。有以下五种豆子,每种豆子总量和总价值各不相同。为了使背包中所装物品的总价值最大,如何选择装哪些豆子?每种豆子该装多少呢???
既然是使价值最大,那么需要算一下相对价值,显而易见黑豆的价值最高。。。
现在使用贪心算法思路来说明一下问题的解决步骤:
- 首先要联想到贪心算法: 针对一组数据,定义了限制值和期望值,希望在满足限制值的情况下,达到最大期望。
- 思考是否可以使用贪心算法: 计算出在限制值相同的情况下能达到最大期望值的单位。
- 将思路带入题目,看看是否能达到最优: 就像刚才的题目,看看会不会有比选用单位价值最高的豆子放进背包更好的策略
以上是贪心算法的一般步骤,但是也不是总能给出最优解。。。
举一个有向无环图的例子,求最短路径,从S开始到T,选出最短路径,贪心算法是每次都选取相邻边权值最小的边去组,显然是不行的,这就是特例,因此要考虑一下这种情况:
这种情况下,贪心算法不起作用是因为后面的选择会被前面影响,而不是之前的背包问题,相互不影响,换句话说走的每一步路都直接影响到了最终的总长度,于是就无缘最优解了。
贪心算法实战
<1>、分糖果
现在有m个糖果n个孩子,现在要把糖果分给这些孩子,但是糖果少(m<n),所以只能分给一部分的孩子。糖果的大小不同,所以每个孩子对糖果的期望不同,当糖果大小满足孩子需求,才能得到满足。
糖果大小分别是a1、a2、a3。。。
孩子需求分别是b1、b2、b3。。。
问题是如何满足更多的孩子????
答案是:按照上面说到的步骤,将问题按照步骤解决:
- 首先联想到贪心算法,贪心算法需要一个限制值和一个期望值: 糖果数量就是限制值,满足孩子的个数就是期望值。
- 思考是否可以使用贪心算法: 既然期望值是孩子个数,那么肯定要尽量满足需求量少的孩子,因为这样可以省下大糖果了。我们将孩子的期望值排序,然后每次都找出最低期望的孩子去满足,就可以完成贪心算法了。
- 思考是否有特例: 每一个孩子都是独立的,如果我们按照顺序去安排满足孩子的顺序,就不会对后面产生影响,所以是没有特例的。
<2>、钱币找零
假设现在我们有1、2、5、10、20、50、100元的纸币,分别有c1、c2、c5、c10、c20、c50、c100张,我们需要支付X元,最少需要多少张纸币。
还是按照刚才的思路:
- 首先联想到贪心算法,贪心算法需要一个限制值和一个期望值: 这里100块钱就是限制值,期望值就是最少使用的纸币数量。
- 思考是否可以使用贪心算法: 既然要的是最少纸币数量,那么只要先按照顺序将100块钱全部拿出去,再依次按照面额支付即可,然后到小的数额再去用1块钱或者两块钱去补齐即可。
- 思考是否有特例: 其实还确实有特例的,但是还需要去验证。
<3>、区间覆盖
假设现在有n个区间,区间的起始端点和结束端点为[l1,r1],[l2,r2],[l3,r3]…[ln,rn],要从这n个区间中选出一部分区间,这部分区间满足两两不相交,最多可以选出多少区间。
再次使用刚才的思路:
- 首先联想到贪心算法,贪心算法需要一个限制值和一个期望值: 限制值就是区间的最左到最右,假设为[min,max],期望值就是选出的区间
- 思考是否可以使用贪心算法: 如果要完成这个题目,应该尽量让选出的区间覆盖程度更大化,然后保留那些左端点不会和前一个区间右端点不重合,右端点还尽量小的区间。
<4>、Huffman Coding的实现原理
首先先来了解一下什么是哈夫曼编码:
依据字符出现概率来构造异字头的平均长度最短的码字就是哈夫曼编码
假设有一个1000字符的文件,每个字符占用1 byte = 8 bits,如何更加节省空间,已知文件只有abcdef六种字符,而三个二进制位就可以表示八个不同的字符,那就是3000bits,有没有更好的策略呢?
a(000) b(001) c(010) d(011) e(100) f(101)
哈夫曼编码不仅仅会看文本中有多少字符,还会考察每个字符出现的频率,然后根据频率不同,选择不同长度的编码,进一步增加压缩的效率。根据贪心算法,可以将频率高的字符,用短编码,对频率少的字符,用长编码。
刚才转化成的二进制编码,是等长表示字符, 但是哈夫曼是不等长的,为了避免解压缩的歧义,所以哈夫曼编码要求不让某个编码是另一个编码的前缀。
假设这六种字符的出现频率从大到小是abcdef,因此可以这样设计:
重新计算一下,占用字节数又会少了很多。
但是哈夫曼树的难点不在于本身的思路,而是编码的方式。。
接下来说一下编码方式:
将每一个字符看做是一个节点,取出队列中频率最小的两个节点AB,新建一个节点C,然后让C作为父节点,AB作为子节点,将C放到优先队列。具体如下图:
接下来给边加上权值,左0右1:
总结
最后附上一个使用贪心算法的代码案例:
package sufang;
import java.util.Arrays;
/**
* 贪心算法
*/
public class GreedyPackage {
private int MAX_WEIGHT = 150;
private int[] weights = {35,30,60,50,40,10,25};
private int[] values = {10,40,30,50,35,40,30};
private void packageGreedy(int capacity,int[] weights,int[] values){
int n = weights.length;
double[] r = new double[n]; //性价比数组
int[] index = new int[n]; //按性价比排序物品的下标
for (int i=0;i<n;i++){
r[i] = (double)values[i]/weights[i];
index[i] = i;//默认排序
}
double temp = 0; //对性价比进行排序
for (int i=0;i<n-1;i++){
for (int j = i+1;j<n;j++){
if (r[i]<r[j]){
temp = r[i];
r[i] = r[j];
r[j] = temp;
int x = index[i];
index[i] = index[j];
index[j] = x;
}
}
}
//排序好的重量和价值分别存到数组
int[] w1 = new int[n];
int[] v1 = new int[n];
for (int i=0;i<n;i++){
w1[i] = weights[index[i]];
v1[i] = values[index[i]];
}
int[] x = new int[n];
int maxValue = 0;
for (int i=0;i<n;i++){
if (w1[i]<capacity){
x[i] = 1;//该物品被装进包里
maxValue = v1[i];
System.out.println("物品"+w1[i]+"被放进包中");
capacity = capacity - w1[i];
}
}
System.out.println("总共放下的物品数量为:"+ Arrays.toString(x));
System.out.println("最大价值为:"+maxValue);
}
public static void main(String[] args) {
GreedyPackage greedyPackage = new GreedyPackage();
greedyPackage.packageGreedy(greedyPackage.MAX_WEIGHT,greedyPackage.weights,greedyPackage.values);
}
}