算法基础(Java)--贪心算法

前言

前面简单的介绍了八大经典排序算法,此文将要介绍贪心算法,并介绍一些常见贪心算法题目。html

1. 贪心算法的概念

所谓贪心算法是指,在对问题求解时,老是作出在当前看来是最好的选择。也就是说,不从总体最优上加以考虑,他所作出的仅是在某种意义上的局部最优解。 贪心算法没有固定的算法框架,算法设计的关键是贪心策略的选择。必须注意的是,贪心算法不是对全部问题都能获得总体最优解,选择的贪心策略必须具有无后效性,即某个状态之后的过程不会影响之前的状态,只与当前状态有关。 因此对所采用的贪心策略必定要仔细分析其是否知足无后效性java

2. 基本思路

  1. 创建数学模型来描述问题。
  2. 把求解的问题分红若干个子问题。
  3. 对每一子问题求解,获得子问题的局部最优解。
  4. 把子问题的解局部最优解合成原来解问题的一个解。

3. 适用的问题

贪心策略适用的前提是:局部最优策略能致使产生全局最优解。也就是当算法终止的时候,局部最优等于全局最优。算法

由于用贪心算法只能经过解局部最优解的策略来达到全局最优解,所以,必定要注意判断问题是否适合采用贪心算法策略,找到的解是否必定是问题的最优解。 若是肯定可使用贪心算法,那必定要选择合适的贪心策略;数组

4. 实例讲解

4.1 背包问题

问题: 有一个背包,背包容量是M=150。有7个物品,物品能够分割成任意大小。要求尽量让装入背包中的物品总价值最大,但不能超过总容量。bash

物品 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种贪心策略,都是没法成立(没法被证实)的,解释以下ide

(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
复制代码

动态规划中将会学习三种最基本的背包问题:零一背包,部分背包,彻底背包。上面已经证实,背包问题不能使用贪心算法。函数

不能解决为何还要引用背包问题来说解贪心算法呢?学习

为了加深对贪心算法的理解: 整个问题的最优解必定由在贪心策略中存在的子问题的最优解得来的。

4.2 钱币找零问题

这个问题在咱们的平常生活中就更加广泛了。 用贪心算法的思想,很显然,每一步尽量用面值大的纸币便可。

假设纸币金额为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] + "元 ");
    }
}
复制代码

4.3 多机调度问题

问题描述: 设有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);
    }
}
复制代码

4.4 【leetcode】630-课程安排III

问题描述: 有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;
    }
}
复制代码

4.5 测试代码

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
复制代码

5. 小结&参考资料

小结

关于贪心算法的实例不少,这就不一一列举了,主要就是理解贪心算法的主要思想,经过局部最优解获得整体最优解的问题。贪心算法是很高效的算法之一,只要能简化出模型就能利用贪心算法来解决问题。

参考资料

相关文章
相关标签/搜索