前面简单的介绍了八大经典排序算法,此文将要介绍贪心算法,并介绍一些常见贪心算法题目。
所谓贪心算法是指,在对问题求解时,总是做出在当前看来是最好的选择。也就是说,不从整体最优上加以考虑,他所做出的仅是在某种意义上的局部最优解。
贪心算法没有固定的算法框架,算法设计的关键是贪心策略的选择。必须注意的是,贪心算法不是对所有问题都能得到整体最优解,选择的贪心策略必须具备无后效性,即某个状态以后的过程不会影响以前的状态,只与当前状态有关。
所以对所采用的贪心策略一定要仔细分析其是否满足无后效性。
贪心策略适用的前提是:局部最优策略能导致产生全局最优解。也就是当算法终止的时候,局部最优等于全局最优。
因为用贪心算法只能通过解局部最优解的策略来达到全局最优解,因此,一定要注意判断问题是否适合采用贪心算法策略,找到的解是否一定是问题的最优解。
如果确定可以使用贪心算法,那一定要选择合适的贪心策略;
问题: 有一个背包,背包容量是M=150。有7个物品,物品可以分割成任意大小。要求尽可能让装入背包中的物品总价值最大,但不能超过总容量。
物品 | A | B | C | D | E | F | G |
---|---|---|---|---|---|---|---|
重量 | 35 | 30 | 60 | 50 | 40 | 10 | 25 |
价值 | 10 | 40 | 30 | 50 | 35 | 40 | 30 |
分析:
目标函数: ∑pi最大
约束条件是装入的物品总重量不超过背包容量:∑wi<=M( M=150)
(1)根据贪心的策略,每次挑选价值最大的物品装入背包,得到的结果是否最优?
(2)每次挑选所占重量最小的物品装入是否能得到最优解?
(3)每次选取单位重量价值最大的物品,成为解本题的策略。
一般来说,贪心算法的证明围绕着:整个问题的最优解一定由在贪心策略中存在的子问题的最优解得来的。
对于例题中的3种贪心策略,都是无法成立(无法被证明)的,解释如下:
(1)贪心策略:选取价值最大者。
反例:
W=30
物品:A B C
重量:28 12 12
价值:30 20 20
根据策略,首先选取物品A,接下来就无法再选取了,可是,选取B、C则更好。
(2)贪心策略:选取重量最小。它的反例与第一种策略的反例差不多。
(3)贪心策略:选取单位重量价值最大的物品。反例:
W=30
物品:A B C
重量:28 20 10
价值:28 20 10
在 动态规划中将会学习三种最基本的背包问题:零一背包,部分背包,完全背包。上面已经证明,背包问题不能使用贪心算法。
不能解决为什么还要引用背包问题来讲解贪心算法呢?
为了加深对贪心算法的理解: 整个问题的最优解一定由在贪心策略中存在的子问题的最优解得来的。
这个问题在我们的日常生活中就更加普遍了。
用贪心算法的思想,很显然,每一步尽可能用面值大的纸币即可。
假设纸币金额为1元、5元、10元、20元、50元、100元,123元应该尽可能兑换少的纸币。
按尝试应该兑换1张100、1张20元和3张1元的。
算法思路很简单,只需要尽可能从最大的面值往下一直减即可。
static void splitChange(int money) {
int[] prices = {100, 50, 20, 10, 5, 1};
int[] notes = new int[prices.length];
int change = money;
if (money > 0) {
while (change > 0) {
for (int i = 0; i < prices.length; i++) {
int count = 0;
for (int k = 0; change - prices[i] >= 0; k++) {
if (change - prices[i] >= 0) {
change = change - prices[i];
count++;
} else break;
}
notes[i] = count;
}
}
}
System.out.println("找零:");
for (int num = 0; num < prices.length; num++) {
System.out.print(notes[num] + "张" + prices[num] + "元 ");
}
}
问题描述:
设有n个独立的作业{1, 2, …, n}, 由m台相同的机器进行加工处理. 作业i所需时间为ti。约定:任何作业可以在任何一台机器上加工处理, 但未完工前不允许中断处理,任何作业不能拆分成更小的子作业。要求给出一种作业调度方案,使所给的 n 个作业在尽可能短的时间内由 m 台机器加工处理完成。
多机调度问题是一个 NP 完全问题,到目前为止还没有完全有效的解法。对于这类问题,用贪心选择策略有时可以设计出一个比较好的近似算法。
贪心算法求解思路
采用最长处理时间作业优先的贪心策略:
当n≤m时, 只要将机器i的[0, ti]时间区间分配给作业i即可。
当n>m时, 将n个作业依其所需的处理时间从大到小排序,然后依次将作业分配给空闲的处理机。
/**
* @Description: 多机调度问题
* @Date: 15:49 2019/8/10
* @Param: [a, m]
* @return: int
*/
public static int greedy(int[] a, int m) {
//int n = a.length - 1;//a的下标从1开始,所以n(作业的数目)=a.length-1
int n = a.length;
int sum = 0;
if (n <= m) {
for (int i = 0; i < n; i++)
sum += a[i + 1];
System.out.println("为每个作业分别分配一台机器");
return sum;
}
List<JobNode> d = new ArrayList<>();//d保存所有的作业
for (int i = 0; i < n; i++) {//将所有的作业存入List中,每一项包含标号和时间
JobNode jb = new JobNode(i + 1, a[i]);
d.add(jb);
}
Collections.sort(d);//对作业的List进行排序
LinkedList<MachineNode> h = new LinkedList<>();//h保存所有的机器
for (int i = 0; i <m; i++) {//将所有的机器存入LinkedList中
MachineNode x = new MachineNode(i+1, 0);//初始时,每台机器的空闲时间(完成上一个作业的时间)都为0
h.add(x);
}
for (int i = 0; i < n; i++) {
Collections.sort(h);
MachineNode x = h.peek();
System.out.println("将机器" + x.id + "从" + x.avail + "到" + (x.avail + d.get(i).time) + "的时间段分配给作业" + d.get(i).id);
x.avail += d.get(i).time;
sum = x.avail;
}
return sum;
}
public static class JobNode implements Comparable {
int id;//作业的标号
int time;//作业时间
public JobNode(int id, int time) {
this.id = id;
this.time = time;
}
@Override
public int compareTo(Object x) {//按时间从大到小排列
int times = ((JobNode) x).time;
return Integer.compare(times, time);
}
}
public static class MachineNode implements Comparable {
int id;//机器的标号
int avail;//机器空闲的时间(即机器做完某一项工作的时间)
public MachineNode(int id, int avail) {
this.id = id;
this.avail = avail;
}
@Override
public int compareTo(Object o) {//升序排序,LinkedList的first为最小的
int xs = ((MachineNode) o).avail;
return Integer.compare(avail, xs);
}
}
问题描述:
有n门课,号码从1到n。每门课有对应的时长t以及截至日期d。选择了一门课,就得持续t天并且在d之前完成这门课。从第一天开始。
给定n门课程,以(t, d)对表示,你需要找出你可以参加的最多的课程数。
需要注意,你不能同时上两门课。
问题分析:
贪心法
先对数组以d排序,在遍历数组的过程中维护当前日期,若课程可选,那么增加当前日期,若不可选,从已经选过的课程中找出一个耗时最长的课程,若该课程耗时比当前课程长,那么替换。重复此过程
class Solution {
public int scheduleCourse(int[][] courses) {
Arrays.sort(courses, new Comparator<int[]>(){
@Override
public int compare(int[] a, int[] b){
return a[1] - b[1];
}
});
int count = 0, curtime = 0;
for(int i = 0;i < courses.length;i++){
//若可选,增加当前时间,并且将当前课程放入courses中
//否则,从courses中选一个耗时最长的课程,若这个耗时最长的课程比当前课程还长,则替换
if(curtime + courses[i][0] <= courses[i][1]){
courses[count++] = courses[i];
curtime += courses[i][0];
}else{
int max_i = i;
for(int j = count - 1;j >= 0;j--){
if(courses[j][0] > courses[max_i][0]) max_i = j;
}
if(courses[max_i][0] > courses[i][0]){
curtime += courses[i][0] - courses[max_i][0];
courses[max_i] = courses[i];
}
}
}
return count;
}
}
public class greedyProgramTest {
public static void main(String[] args) {
//找零问题
int money = 123;
greedyProgram.splitChange(money);
System.out.println();
System.out.println("-------");
//多机调度问题
int[] a = {5,4,2,14,16,6,5,3};
int m = 3;
System.out.println("总时间为:"+greedyProgram.greedy(a,m));
System.out.println("-------");
//课程表
int[][] course = {{2,5},{2,19},{1,8},{1,3}};
System.out.println(Solution.scheduleCourse(course));
}
}
运行结果:
找零:
1张100元 0张50元 1张20元 0张10元 0张5元 3张1元
-------
将机器1从0到16的时间段分配给作业5
将机器2从0到14的时间段分配给作业4
将机器3从0到6的时间段分配给作业6
将机器3从6到11的时间段分配给作业1
将机器3从11到16的时间段分配给作业7
将机器2从14到18的时间段分配给作业2
将机器3从16到19的时间段分配给作业8
将机器1从16到18的时间段分配给作业3
总时间为:18
-------
课程数:4
关于贪心算法的实例很多,这就不一一列举了,主要就是理解贪心算法的主要思想,通过局部最优解得到总体最优解的问题。贪心算法是很高效的算法之一,只要能简化出模型就能利用贪心算法来解决问题。