目录
贪心算法(又称贪婪算法)是指,在对问题求解时,总是做出在当前看来是最好的选择。也就是说,不从整体最优上加以考虑,算法得到的是在某种意义上的局部最优解。 虽然贪心算法不能对所有问题都得到整体最优解,但是对许多问题能产生整体最优解。
贪心算法不是对所有问题都能得到整体最优解,关键是贪心策略的选择。(局部最优解)。
一、贪心算法的基本要素
用贪心算法求解的问题一般具有2个重要的性质:贪心算法性质和最优子结构性质。
1、贪心算法性质:
(1)局部最优
指所求问题的整体最优解通过一系列局部最优选择来实现。这是贪心算法与动态规划算法的主要区别。
(2)自顶向下的方式解各子问题
动态规划算法通常以自底向上的方式解各子问题。 而贪心算法通常使用自顶向下的方式进行。以迭代的方式做出相继的贪心选择。每做一次贪心选择就将所求问题简化为规模更小的子问题。
2、最优子结构性质:
当一个问题的最优解包含其子问题的最优解时,称此问题具有最优子结构性质。问题的最优子结构性质是该问题可用动态规划算法或贪心算法求解的关键特征。
二、贪心算法与动态规划算法的差异:
贪心算法和动态规划算法都要求问题具有最优子结构性质,这是两类算法的一个共同特点。
解决问题的方法:
贪心算法:在每一步选择中都采取在当前状态下最好或最优(即最有利)的选择,从而希望导致结果是全局最好或最优的算法。贪心算法更关注局部最优解,并不保证能得到全局最优解。
动态规划:通过将原问题分解为相对简单的子问题的方式求解,这些子问题的解被保存并重复使用,从而避免重复计算。动态规划算法能够保证得到全局最优解。
记忆化:
贪心算法:通常不需要记忆化,因为它们只关注当前的最优选择。
动态规划:通常需要记忆化(通过表格或递归中的备忘录),以存储子问题的解,避免重复计算。
确定性:
贪心算法:在某些情况下,贪心算法的解可能是唯一的,但在其他情况下,可能存在多个贪心解。
动态规划:动态规划算法通常会找到一个唯一的全局最优解。
算法设计:
贪心算法:算法设计相对简单,通常基于直观的贪心策略。
动态规划:算法设计更复杂,需要识别子问题,定义状态和状态转移方程。
以0-1背包和背包问题为例:
0-1背包问题描述:
背包问题描述:
它们的最优子结构描述:
贪心算法不能解决0-1背包问题,但是可以解决背包问题。贪心算法解决背包问题的基本步骤:
贪心算法解决背包问题算法实现:
对于0-1背包问题,贪心算法之所以不能够得到最优解是因为在这种情况下,它无法保证最终能将背包装满,部分闲置的背包空间使每斤背包空间的价值降低了。
例题1:活动安排问题
问题描述
解题思路:
1.输入活动及其完成时间的非减序排列(即非严格递增排序)。
2.每次选择具有最早完成时间的相容活动加入集合a中。即选择 s [ i+1 ] >= f [ i ] 的活动。否则判断 s [ i+2 ] 与 f [ i ] 的大小,直至所有的活动都判断完毕。数组a是用来判断第 i 个活动是否被选择,如果选择则数组 a[ i ]=1,否则为0。
算法复杂度分析:
当输入的活动已按结束时间的非减序排序,算法只需O(n)的时间安排n个活动,使最多的活动够相容地使用公共资源。 如果所给活动没有进行排序,可以用O(nlogn)的时间重排。
伪代码
类 Main:
方法 greedySelector(s, f, a):
设置 m 为 s数组的长度减去 1
设置 a[1] 为 true
设置 ans 为 1
设置 j 为 1
对于 i 从 2 到 m:
如果 s[i] 大于等于 f[j]:
设置 a[i] 为 true
ans = ans + 1
j = i
否则:
设置 a[i] 为 false
返回 ans
方法 main:
创建 Scanner对象 scanner 用于读取输入
读取整数 n
创建整数数组 s,长度为 n+1
创建整数数组 f,长度为 n+1
创建布尔数组 a,长度为 n+1
// 输入
对于 i 从 1 到 n:
读取 s[i]
读取 f[i]
// 对 f 进行排序
对于 i 从 1 到 n:
对于 j 从 i+1 到 n:
如果 f[i] 大于等于 f[j]:
交换 s[i] 和 s[j]
交换 f[i] 和 f[j]
计算 count 为调用 greedySelector(s, f, a) 的返回值
打印 count
java实现代码
import java.util.Scanner;
public class Main{
public static int greedySelector(int[] s,int[] f,boolean []a){
int m=s.length-1;
a[1]=true;//如果a[i]为true,则说明第i个活动已经被选择
int ans=1;
int j=1;
for(int i=2;i<=m;i++){
if(s[i]>=f[j]){
a[i]=true;
ans++;
j=i;
}
else{
a[i]=false;
}
}
return ans;
}
public static void main(String[] args){
Scanner scanner=new Scanner(System.in);
int n=scanner.nextInt();
int[] s=new int [n+1];
int[] f=new int [n+1];
boolean[] a=new boolean [n+1];
//输入
for(int i=1;i<=n;i++){
s[i]=scanner.nextInt();
f[i]=scanner.nextInt();
}
//对f进行排序
for(int i=1;i<=n;i++){
for(int j=i+1;j<=n;j++){
if(f[i]>=f[j]){
int temp1=s[i];
int temp2=f[i];
s[i]=s[j];
f[i]=f[j];
s[j]=temp1;
f[j]=temp2;
}
}
}
int count=greedySelector(s,f,a);
System.out.println(count);
}
}
例题2:最优装载
问题描述
解题思路:
(1)采用重量最轻者先装的贪心选择策略,可产生最优装载问题的最优解。
(2)该算法的贪心选择的意义:直观上,按照这种选择方法为未安排的集装箱留下了尽可能多的装载量。也就数说使剩余装载重量极大化,以便安排尽可能多的集装箱
算法复杂度分析:
主要计算量在于将集装箱以其重量从大到小排序,故算法所需的计算时间为O(nlogn)。
伪代码
类 test:
属性 w: 浮点数 // 物品重量
属性 i: 整数 // 物品索引
构造函数 test(ww, ii):
w = ww
i = ii
方法 mergeSort(array):
如果 array的长度 > 1:
创建 left数组,长度为 array长度的一半
创建 right数组,长度为 array长度减去 left长度
将 array的前半部分复制到 left
将 array的后半部分复制到 right
递归调用 mergeSort(left)
递归调用 mergeSort(right)
调用 merge(array, left, right)
方法 merge(array, left, right):
初始化索引 i, j, k 为 0
当 i < left长度 且 j < right长度:
如果 left[i] ≤ right[j]:
将 left[i] 赋值给 array[k]
i = i + 1
k = k + 1
否则:
将 right[j] 赋值给 array[k]
j = j + 1
k = k + 1
当 i < left长度:
将 left[i] 赋值给 array[k]
i = i + 1
k = k + 1
当 j < right长度:
将 right[j] 赋值给 array[k]
j = j + 1
k = k + 1
方法 loading(c, w, x):
n = w的长度
创建 w1数组,长度为 n
创建 d数组,类型为 test,长度为 n
对于 i 从 0 到 n-1:
d[i] = 新 test(w[i], i)
w1[i] = d[i].w
调用 mergeSort(w1)
对于 i 从 0 到 n-1:
d[i].w = w1[i]
opt = 0
对于 i 从 0 到 n-1:
x[i] = 0
对于 i 从 0 到 n-1:
如果 i < n 且 d[i].w ≤ c:
x[d[i].i] = 1
opt = opt + d[i].w
c = c - d[i].w
返回 opt
方法 main:
c = 150
ww = [30, 10, 60, 40, 50, 20]
xx = 新整数数组,长度为 ww的长度
opt1 = 调用 loading(c, ww, xx)
打印 "最大装载重量为:" + opt1
打印 "排序后的最优解为:"
对于 i 从 0 到 ww长度-1:
打印 xx[i] + " "
打印换行
java实现代码
public class test {
float w; // 物品重量
int i; // 物品索引
public test(float ww, int ii) {
w = ww;
i = ii;
}
// 归并排序的实现
public static void mergeSort(float[] array) {
if (array.length > 1) {
float[] left = new float[array.length / 2];
float[] right = new float[array.length - left.length];
System.arraycopy(array, 0, left, 0, left.length);
System.arraycopy(array, left.length, right, 0, right.length);
mergeSort(left);
mergeSort(right);
merge(array, left, right);
}
}
// 合并两个已排序数组
private static void merge(float[] array, float[] left, float[] right) {
int i = 0, j = 0, k = 0;
while (i < left.length && j < right.length) {
if (left[i] <= right[j]) {
array[k++] = left[i++];
} else {
array[k++] = right[j++];
}
}
while (i < left.length) {
array[k++] = left[i++];
}
while (j < right.length) {
array[k++] = right[j++];
}
}
public static float loading(float c, float[] w, int[] x) {
int n = w.length;
float[] w1 = new float[n];
test[] d = new test[n];
for (int i = 0; i < n; i++) {
d[i] = new test(w[i], i);
w1[i] = d[i].w;
}
// 调用mergeSort方法对w1数组进行非递减排序
mergeSort(w1);
for (int i = 0; i < n; i++) {
d[i].w = w1[i];
}
float opt = 0;
for (int i = 0; i < n; i++) x[i] = 0;
for (int i = 0; i < n && d[i].w <= c; i++) {
x[d[i].i] = 1;
opt += d[i].w;
c -= d[i].w;
}
return opt;
}
public static void main(String[] args) {
float c = 150;
float[] ww = {30, 10, 60, 40, 50, 20};
int[] xx = new int[ww.length];
float opt1 = loading(c, ww, xx);
System.out.println("最大装载重量为:" + opt1);
System.out.println("排序后的最优解为:");
for (int i = 0; i < ww.length; i++) {
System.out.print(xx[i] + " ");
}
System.out.println();
}
}
例题3:单源最短路径
问题描述
算法思路:
迪杰斯特拉是解决单源最短路径的贪心算法。其基本思想是:设置顶点集合S并不断地做贪心选择来扩充这个集合。
1.设S是已经对v生成了最短路径的节点集合(包括源v),初始时,S中仅含有源v。
2.记dist(j)是从v开始,只经过S中的顶点而在 j 结束的那条最短特殊路径的长度。
3. 每次从v-s中取出最具有最短特殊路径长度的顶点 j ,将 j 添加到 s 中,同时修改dist[j]为最短长度。一旦S包含了所有V中顶点,dist就记录了从源到所有其他顶点之间的最短路径长度。
算法复杂度分析:
迪杰斯特拉算法的主循环体:顶点u加入集合s中之后需要更新 v 到各顶点的路径长度,需要O(n)时间。
外循环:把除源点v以外的n-1个顶点逐个加入集合s中,需要执行n-1次。
T(n)=O()
伪代码
类 test:
方法 dijkstra(v, a, dist, prev):
n = dist数组的长度 - 1
如果 v不在1和n之间:
返回
创建布尔数组 s,长度为 n + 1
对于 i 从 1 到 n:
// 初始化
dist[i] = a[v][i]
s[i] = false
如果 dist[i] 等于 Float.MAX_VALUE:
prev[i] = -1
否则:
prev[i] = v
dist[v] = 0
s[v] = true
对于 i 从 1 到 n - 1:
temp = Float.MAX_VALUE
u = v
对于 j 从 1 到 n:
如果 (!s[j] 且 dist[j] < temp):
u = j
temp = dist[j]
s[u] = true
对于 j 从 1 到 n:
如果 (!s[j] 且 a[u][j] 不等于 Float.MAX_VALUE):
newdist = dist[u] + a[u][j]
如果 newdist < dist[j]:
dist[j] = newdist
prev[j] = u
方法 printPath(v, prev, dist):
打印 "节点" + v + ":到源节点最短路径为"
调用 printPathHelper(v, prev)
打印 ",长度为" + dist[v]
方法 printPathHelper(v, prev):
如果 v 等于 1:
打印 1
否则如果 prev[v] 不等于 -1:
递归调用 printPathHelper(prev[v], prev)
打印 "->" + v
方法 main:
m = 5 // 顶点个数5个
MAX = Float.MAX_VALUE
创建 float 类型二维数组 a,大小为 m + 1
对于 i 从 1 到 m:
对于 j 从 1 到 m:
a[i][j] = MAX // 初始化所有距离为无穷大
// 初始化数组,设置边的权重
a[1][2] = 10; a[1][4] = 30; a[1][5] = 100;
a[2][3] = 50; a[3][5] = 10; a[4][3] = 20; a[4][5] = 60;
创建 float 数组 dist,长度为 m + 1
创建 int 数组 prev,长度为 m + 1
调用 dijkstra(1, a, dist, prev) // 从顶点1开始计算最短路径
打印 "节点1:该节点为源节点!"
对于 i 从 2 到 m:
调用 printPath(i, prev, dist)
java实现代码
public class test {
public static void dijkstra(int v, float[][] a, float[] dist, int[] prev) {
int n = dist.length - 1;
if (v < 1 || v > n) return;
boolean[] s = new boolean[n + 1];
for (int i = 1; i <= n; i++) {
// 初始化
dist[i] = a[v][i];
s[i] = false;
if (dist[i] == Float.MAX_VALUE) prev[i] = -1; // 没有路径时,前驱设置为-1
else prev[i] = v;
}
dist[v] = 0;
s[v] = true;
for (int i = 1; i < n; i++) {
float temp = Float.MAX_VALUE;
int u = v;
for (int j = 1; j <= n; j++) {
if (!s[j] && dist[j] < temp) {
u = j;
temp = dist[j];
}
}
s[u] = true;
for (int j = 1; j <= n; j++) {
if (!s[j] && a[u][j] != Float.MAX_VALUE) {
float newdist = dist[u] + a[u][j];
if (newdist < dist[j]) {
dist[j] = newdist;
prev[j] = u;
}
}
}
}
}
public static void printPath(int v, int[] prev,float[] dist) {
System.out.print("节点" + v + ":到源节点最短路径为");
printPathHelper(v, prev);
System.out.println(",长度为" + dist[v]);
}
public static void printPathHelper(int v, int[] prev) {
if (v == 1) {
System.out.print("1");
} else if (prev[v] != -1) {
printPathHelper(prev[v], prev);
System.out.print("->" + v);
}
}
public static void main(String[] args) {
int m = 5; // 顶点个数5个
float MAX = Float.MAX_VALUE;
float[][] a = new float[m + 1][m + 1]; // 构造从i到j的长度数组
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= m; j++) {
a[i][j] = MAX; // 先让所有的点之间的距离为无穷
}
}
// 初始化数组
a[1][2] = 10; a[1][4] = 30; a[1][5] = 100;
a[2][3] = 50; a[3][5] = 10; a[4][3] = 20; a[4][5] = 60;
float[] dist = new float[m + 1];
int[] prev = new int[m + 1];
dijkstra(1, a, dist, prev); // 从顶点1开始计算最短路径
System.out.println("节点1:该节点为源节点!");
for (int i = 2; i <= m; i++) {
printPath(i, prev,dist);
}
}
}