1 问题引入:
有一堆钞票,你可以拿走十张钞票,如果你想达到最带的金额,你要怎么拿?
每次拿最大面值的钞票,十次之后,你拿走的就是最大数额的钱。
这里"有便宜就占"用的就是贪心算法的思想,即每一次拿最大的就是局部优先,最后拿走的最大数额的钱就是全局最优。
2 概述:
贪心算法(greedy algorithm,又称贪婪算法)是指在对问题求解时,总是做出在当前看来是最好的选择。也就说不从整体最优上加以考虑,算法得到的是在某种意义上的局部最优解。
简单地说:贪心算法的本质就是从局部最优到全局最优。
贪心算法的基本思想是:将问题的求解过程看作是一系列选择,每次选择一个输入,每次选择都是当前状态下的最好选择(局部最优解),每做一个选择后,所求问题会简化为一个规模更小的问题。从而通过每一步的最优解达到整体的最优解。
贪心算法的适用问题:具有贪心选择1和最优子结构性质2的最优化问题。
贪心算法的优点是:简单,高效,省去了为了找最优解可能需要穷举操作,通常作为其它算法的辅助算法来使用。
贪心算法也有很多的缺点:
贪心算法的解题步骤:
-
建立数学模型来描述问题
-
将问题分解为若干个子问题
-
找出适合的贪心策略
-
求解每一个子问题的最优解
-
将局部最优解 堆叠成 全局最优解
3 案例:
贪心算法有很多经典的应用,比如霍夫曼(Huffman)编码、普利姆(Prim)算法、克鲁斯卡尔(Kruskal)算法最小生成树算法还有迪杰斯特拉(Dijkstra)单源最短路径算法等。
选择排序
十大经典算法中的选择排序采用的就是贪心算法。选择排序是一种简单的排序算法,时间复杂度是$ O(n²) $。
选择排序的基本思想:是每一趟从待排序的数据元素中选出最小(或最大)的一个元素,顺序放在已排好序的数列的最后,直到全部待排序的数据元素排完。
选择排序的算法步骤是:
-
首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置。
-
再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。
-
重复第二步,直到所有元素均排序完毕。
例如给定一组数据,[23, 4, 6, 8, 10] 其排序过程如以下所示:
第一次:[23, 4, 6, 8, 10]
第二次:[4, 6, 23, 8, 10]
第三次:[4, 6, 8, 23, 10]
第四次:[4, 6, 8, 10, 23]
程序结束,得出[4, 6, 8, 10, 23]
代码实现:
class SelectionSort {
public static int[] sort(int[] sourceArray) throws Exception {
int[] arr = Arrays.copyOf(sourceArray, sourceArray.length);
// 总共要经过 N-1 轮比较
for (int i = 0; i < arr.length - 1; i++) {
int max = i;
// 每轮需要比较的次数 N-i
for (int j = i + 1; j < arr.length; j++) {
if (arr[j] > arr[max]) {
// 记录目前能找到的最大值元素的下标
max = j;
}
}
// 将找到的最大值和i位置所在的值进行交换
if (i != max) {
int tmp = arr[i];
arr[i] = arr[max];
arr[max] = tmp;
}
}
return arr;
}
public static void main(String[] args) throws Exception {
int[] arr = new int[]{23, 4, 6, 8, 10};
System.out.println("原数组:" + Arrays.toString(arr));
System.out.println("选择排序:" + Arrays.toString(sort(arr)));
}
}
部分背包问题
有一个背包,容量是c,有若干个物品,价值各不相同,重量也不同。请你选择一部分物品装入背包,要保证不超过背包容量的前提下,背包中的物品的总价值最大。
注:在选择物品时候可以选择物品的一部分:比如值选择0.2 份物品1,0.5份物品2
假设背包的容量是10,有5个商品可以提供选择:
选择策略:每一个物品都有两个属性,不可以只靠价值5和重量6的单一属性来选择物品。我们应该先算出每一件物品的性价比,即单位重量下的价值,然后选择性价比最高的物品放到背包。
计算一下每一个物品的性价比,其结果为:
此时,性价比最高的的物品4,性价比是3,我们就把物品4放到背包中,此时背包容量就剩8。
之后我们从剩余的物品中选择性价比最高的物品1放入背包中,这时候背包的容量还剩下4。
剩余的物品中性价比最高的是物品5,但是物品5的重量是5,但是背包的容量是4,题目的条件允许选择物品的一部分,那就能选择0.8份的物品5,刚好占满整个背包。
此时的背包的总价值是: 9 + 6 + 5 ∗ 0.9 = 19 9 + 6 + 5 * 0.9 = 19 9+6+5∗0.9=19为背包的最大总价值。
代码实现:
public static void main(String[] args) {
int capacity = 10;
int[] weights = {4, 6, 3, 2, 5};
int[] values = {9, 3, 1, 6, 5};
for (int i = 0; i < weights.length; i++) {
System.out.println(String.format("物品%d,价值:%d,重量:%d,性价比:%.2f",
(i + 1), values[i], weights[i], (double) values[i] / weights[i]));
}
System.out.println("背包最大价值:" + getHighestValue(capacity, weights, values));
}
public static double getHighestValue(int capacity, int[] weights, int[] values) {
//创建物品列表并按照性价比倒序
List<Item> itemList = new ArrayList<>();
for (int i = 0; i < weights.length; i++) {
itemList.add(new Item(weights[i], values[i]));
}
itemList = itemList.stream().sorted(Comparator.comparing(Item::getRatio).reversed()).collect(Collectors.toList());
//背包剩余容量
int restCapacity = capacity;
//当前背包物品的最大价值
double highestValue = 0;
//按照性价比从高到低选择物品
for (Item item : itemList) {
if (item.weight <= restCapacity) {
highestValue += item.value;
restCapacity -= item.weight;
} else {
//背包装不下完整物品时,选择该件物品的一部分
highestValue += (double) restCapacity / (double) item.weight * item.value;
break;
}
}
return highestValue;
}
static class Item {
private final int weight;
private final int value;
//物品的性价比
private final double ratio;
public Item(int weight, int value) {
this.weight = weight;
this.value = value;
this.ratio = (double) value / (double) weight;
}
public double getRatio() {
return ratio;
}
}
这时候,有一个贪心算法的缺点:贪心算法具有一定的局限性。因为有些时候局部的最优解不一定是全局的最优解。
**01背包问题:**如果有一个容量为10的背包,有3个物品可以选择,但是我们只允许选择整个物品,不能选择一个物品的一部分。
按照贪心算法的思想,我们首先选择的是性价比最高的物品1,那我们背包的剩余容量就剩下4,再也装不了其他的物品了,而此时的总价值是6:
但这样的选择,并不一定是最优解,我们可以发现,我们不用贪心算法的思想,不选择物品1,而是选择物品2和物品3,他们的重量正好是10,总价值是7。
此时,7 > 6 ,所以依靠贪心算法算出的结果,未必是全局最优解。但是也是比较接近最优解。
对于01背包问题,不适合用贪心算法,而是应该用动态规划算法求解。
4 贪心和动态规划:
动态规划算法和贪心算法的思想都是通过分治的思想,求一个问题的最优解。主要过程是:将一个大问题分解成许多的小问题,分别求每个子问题的局部最优解,分而治之,最终合成大问题的最优解。
动态规划的思想是“反复回头”,也就是,我们分解小问题存在联系性,在一个子问题上得到的最优解,也许在下一个问题上被证伪,那么算法就进行回溯处理。在这种思想上,获取的将是“全局的绝对最优解”。
贪心算法的思想就是“有便宜就占,绝不回头”,也就是在每个子问题上取最优解,直到所有子问题被处理完,再合成大问题的最优解,过程中不会回头修改任何计算过的子问题的解。在这种思想上,获得将是“局部的相对最优解”。
简单地说,有两个人甲和乙都有一个正向的大目标,两个人都将大的目标分成一个一个的小目标,然后去完成小目标。对于甲来说,在完成小目标之后,会去反复回头检查,如果出错就去调整自己的目标。但是对于乙来说,绝不会回头,一直完成定好的小目标就行。到最后,甲会完成大目标,而乙可能不会完成大目标,但是离大目标也比较接近。
5 总结:
贪心算法的总体思想就是:局部优先从而达到全局优先。
贪心算法是局部的相对优先,如果想要达到全局的绝对优先,那还是得选择动态规划。
我们主要介绍了贪心算法的典型应用:背包问题。
贪心算法题目类型多样,很多情况下要具体情况具体分析。