算法——贪心算法

一、贪心算法

什么情况下我们要想到用贪心算法:

1、当我们看到这类问题的时候,首先要联想到贪心算法:针对一组数据,我们定义了限制值和期望值,希望从中选出几个数据,在满足限制值的情况下,期望值最大;
2、我们尝试看下这个问题是否可以用贪心算法解决:每次选择当前情况下,在对限制值同等贡献量的情况下,对期望值贡献最大的数据 。
3、我们举几个例子看下贪心算法产生的结果是否是最优的。大部分情况下,举几个例子验证一下就可以
了。
注意:不要刻意去记忆贪心算法的原理,要多练习;不要严谨的证明算法,多举几个例子验证一下!
我们用几个案例来理解贪心算法。
1、有一个可以装100kg物品的箱子,我们有5种物品(大米100kg,总价值100元; 玉米30kg,总价值90元;小麦60kg,总价值120元;芝麻20kg,总价值80元;大豆50kg,总价值75元),为了让箱子中所装物品的总价值最大,在箱子中要选择哪些物品,每种物品的要装多少?
这个问题我们可以直接算出来,在箱子装20kg芝麻、30kg玉米、50kg小麦。
代码实现如下:

  /**
     * 箱子价值最大化
     *
     * @param w 物品重量
     * @param v 物品价值
     * @param n 物品数量
     * @param m 背包承受重量
     */
    public void boxLoading(int[] w, int[] v, int n, int m) {
        // 计算每个物品的单价
        float[] p = new float[n];
        // 记录物品排序后的下标
        int[] idxs = new int[n];
        for (int i = 0; i < n; i++) {
            p[i] = v[i] / (float) w[i];
            idxs[i] = i;
        }

        // 将单价按降序排序
        for (int i = 0; i < n; i++) {
            // 排序完成标识
            boolean flag = false;
            for (int j = 0; j < n - i - 1; j++) {
                if (p[j] < p[j + 1]) {
                    float temp = p[j];
                    p[j] = p[j + 1];
                    p[j + 1] = temp;
                    flag = true;
                    int index = idxs[j];
                    idxs[j] = idxs[j + 1];
                    idxs[j + 1] = index;
                }
            }
            if (!flag) {
                break;
            }
        }

        // 将重量和总价值降序排序
        int[] w1 = new int[n];
        int[] v1 = new int[n];
        // 每个物品的放到箱子的重量
        int[] res = new int[n];
        for (int i = 0; i < n; i++) {
            w1[i] = w[idxs[i]];
            v1[i] = v[idxs[i]];
            res[i] = 0;
        }

        // 箱子剩余可放入重量
        int cu = m;
        // 计算每个物品要放入重量
        for (int i = 0; i < n; i++) {
            // 物品重量大于等于可放入的重量
            if (w1[i] >= cu) {
                // 还有剩余空间
                if (cu > 0) {
                    res[i] = cu;
                }
                break;
            } else {
                // 否则,把当前物品全部装入箱子中
                cu = cu - w1[i];
                res[i] = w1[i];
            }
        }

        // 输出装入箱子的物品的重量
        for (int i = 0; i < n; i++) {
            int idx = idxs[i] + 1;
            System.out.println("第" + idx + "个物品,重量为:" + res[i]);
        }
    }

2、分糖果
我们有m个糖果和n个孩子(m<n)。每个糖果的大小不等,每个小孩对糖果的需求也是不一样的,如何分配糖,能尽可能满足最多数量的孩子。

我们可以从n个孩子中,抽取一部分分配糖果,让满足的孩子的个数(期望值)是最大的。限制值就是糖果个数m。

我们每次从剩下的孩子中,找出对糖果大小需求最小的,然后发给他剩下的糖果中能满足他的最小的糖果,这样得到的分配方案,也就是满足孩子个数最多的方案。代码实现如下:

/**
     * 分糖果
     * @param candies 糖果大小
     * @param reqs  孩子需求量
     * @return 孩子数量
     */
    public int divideCandy(int[] candies, int[] reqs) {
        int m = candies.length;
        int n = reqs.length;

        // 对糖果和孩子的需求排序
        insertSort(candies);
        insertSort(reqs);
        // 假设每个小孩只能获得一棵糖果
        int[] children = new int[n];
        // 记录孩子个数
        int count = 0;

        int i = 0;
        int j = 0;
        while (i < m) {
            // 糖果的大小要大于等于当前需求才满足
            if (candies[i] >= reqs[j]) {
                children[j] = reqs[j];
                count++;
                j++;
            }
            i++;
        }
        return count;
    }

    // 插入排序
    private void insertSort(int[] arr) {
        for (int i = 1; i < arr.length; i++) {
            int value = arr[i];
            int j = i - 1;
            for (; j >= 0; j--) {
                if (arr[j] > value) {
                    arr[j + 1] = arr[j];
                } else {
                    break;
                }
            }
            arr[j + 1] = value;
        }
    }

3、钱币找零
假设我们有1元、2元、5元、10元、20元、50元,100元这些面额的纸币,它们的张数分别是c1、c2、c5、c20、c50、c100。我们现在要用这些钱来支付K元,最少要用多少张纸币呢?

在贡献相同期望值(纸币数目)的情况下,我们希望多贡献点金额,这样就可以让纸币数量更少,这是一种贪心算法的解决思路。代码如下:

  /**
     * 零钱找零
     *
     * @param moneys  钱面额
     * @param amounts 每张面额数量
     * @param k       要支付多少钱
     * @return 要找多少张钱
     */
    public int coinChange(int[] moneys, int[] amounts, int k) {
        int n = moneys.length;
        // 记录金额
        int cur = 0;
        // 记录钱币的数量
        int count = 0;
        // 记录需要哪些面额
        int[] m1 = new int[n];
        // 记录需要面额的张数
        int[] a1 = new int[n];
        for (int i = n - 1; i >= 0; i--) {
            // 从最大的钱币开始判断是否满足
            int money = moneys[i];
            m1[i] = money;
            // 如果钱币的面积大于k,直接到下一个
            if (money > k) {
                continue;
            }
            int amount = amounts[i];
            int tmep = 0;
            // 累计的钱加上当前面额小于等于k,说明这张钱可以找出去
            while (cur + money <= k && amount >= 0) {
                cur += money;
                amount--;
                count++;
                tmep++;
            }
            a1[i] = tmep;
            // 零钱已经找好了
            if (cur == k) {
                break;
            }
        }
        return count;
    }

4、区间覆盖
假设我们有n个区间,区间的起始端点和结束端点分别是【l1, r1],[l2, r2],[l3, r3],...,[ln, rn]。从这n个区间中选出一部分区间,这部分区间满足两两不相交(端点相交的情况不算相交),最多能选出多少个区间呢?

比如,区间:[6, 8], [2, 4], [3, 5], [1,5], [5, 9], [8, 10]; 不相交的区别有:[2, 4], [6, 8], [8, 10]

这个处理思想在很多贪心算法问题中都有用到,像任务调度、教师排课。

解决思路:假设这n个区间中最左端点是lmin,最右端点是rmax。我们选择几个不相交的区别,从左到右[lmin, rmax]覆盖上(如下图)。我们按照起始端点从小到大顺序对这几个区间排序。从图可以看出,我们每次选择的时候,左端点跟前面的已经覆盖的区间不重合,右端点又尽量小的,这样可以让剩下的未覆盖区间覆盖尽可能的大,就可以放置更多的区间。



代码实现如下:

 /**
     * 区间覆盖
     *
     * @param arr 区间数组
     * @return 满足两两不相交区间
     */
    public int[][] intervalCoverage(int[][] arr) {
        int n = arr.length;
        // 对数组按左端点进行排序
        selectSort(arr);
        int[][] res = new int[n][2];
        int left = arr[0][0];
        int right = arr[0][1];
        // 记录右端点最小的区间
        int index = 0;
        for (int i = 1; i < n; i++) {
            // 判断是否是相交的区间,左端点在区间中,说明相交
            if (arr[i][0] >= left && arr[i][0] < right) {
                // 判断是右端点小于等于right
                if (arr[i][1] <= right) {
                    right = arr[i][1];
                    index = i;
                }
            } else {
                // 如果不相交,再以这个区间与下个区间进行比较
                left = arr[i][0];
                right = arr[i][1];
                // 将上一次相交的区间的右端点加到结果中
                res[index] = arr[index];
                // 更新index
                index = i;
            }
        }
        // 如果最后区间就是最小右端点
        if (index == n - 1) {
            res[index] = arr[index];
        }
        return res;
    }

    // 选择排序
    private void selectSort(int[][] arr) {
        for (int i = 0; i < arr.length; i++) {
            int min = i;
            for (int j = i; j < arr.length; j++) {
                if (arr[min][0] > arr[j][0]) {
                    min = j;
                }
            }
            int[] temp = arr[i];
            arr[i] = arr[min];
            arr[min] = temp;
        }
    }

5、霍夫曼编码
假设有一个包含1000个字符的文件,每个字符占1个byte(1byte=8bits),存储这1000个字符一共需要8000bits,那有没有节省空间的存储方式呢?

假设通过统计分析发现,这1000个字符只包含6个不同的字符,假设它们分别是a, b, c, d, e, f。而3个二进制位就可以表示8个不同的字符,所以为了尽量减少存储空间,每个字符我们用3个二进制来表示。那存储1000个字符只需要3000bits就可以了。
a(000)、b(001)、c(010)、d(011)、e(100)、f(101)

二进制的存储方式,确实节省了很多空间。不过还更节省存储方式:霍夫曼编码,它是一种十分有效的编码方法,广泛用于数据压缩中,其压缩率通常在20%~90%之间。

霍夫曼编码是不等长的,每次应该读取1位还是2位、3位等等来解压缩呢?这个问题会导致霍夫曼编码解压缩的时候比较复杂。为了避免解压缩过程中的歧义,霍夫曼编码要求各个字符的编码之间,不会出现某个编码是另一个编码前缀的情况。

我们把字符编码下面这个样子,任何一个字符的编码都不是另一个前缀,在解压缩的时候,每次读取尽可能长的可解压的二进制串,所以在解压缩的时候也不会歧义。经过这种编码压缩之后,这1000个字符只需要2100bits就可以了。


怎么根据字符出现频率的不同,给不同的字符进行不同长度的编码呢?

我们把每个字符看作一个节点,并且附带着频率放到优先队列中。我们从队列中取出频率最小的两个节点A、B,然后新建一个节点C,把频率设置为两个节点的频率之后,并把这个新节点作为A、B节点的父节点。最后再把C节点放入到优先级队列中。重复这个过程,直到队列中没有数据。



现在我们给每一边边上画上一个权值,指向左子节点的边我们统统标记为0,指向右子节点的边,统统标记为1,从根节点到时叶节点的路径就是叶节点对应字符的霍夫曼编码。


©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容