经典算法题12-贪心算法

一. 引入

在日常生活中,经常遇到找零钱的问题。假设1元、2元、5元、10元、20元、50元、100元的纸币分别有c0, c1, c2, c3, c4, c5, c6张。现在要用这些钱来支付K元,至少要用多少张纸币?

很显然,每一步尽可能用面值大的纸币即可。这就是日常生活中贪心算法思想的使用。

二. 概念

百度百科的定义是:

贪心算法(又称贪婪算法)是指,在对问题求解时,总是做出在当前看来是最好的选择。也就是说,不从整体最优上加以考虑,他所做出的是在某种意义上的局部最优解。

贪心是一种特殊的动态规划,动态规划的本质是独立的子问题,而贪心则是每次可以找到最优的独立子问题。后面我会细说两者的异同点。

三.最小生成树

图示

生成树,就是用边来把所有的顶点连通起来,前提条件是最后形成的连通图中不能存在回路,所以就形成这样一个无向图。
这里写图片描述

推理:假设图中的顶点有n个,则生成树的边有n-1条,多一条会存在回路,少一路则不能把所有顶点连通起来,如果非要在图中加上权重,则生成树中权重最小的叫做最小生成树。

Prim算法

算法简单描述

 1. 输入:一个加权连通图,其中顶点集合为V,边集合为E2. 初始化:Vnew = {x},其中x为集合V中的任一节点(起始点),Enew = {},为空;
 3. 重复下列操作,直到Vnew = V:
     a.在集合E中选取权值最小的边<u,v>,其中u为集合Vnew中的元素,而v不在Vnew集合当中,并且v∈V(如果存在有多条满足前述条件即具有相同权值的边,则可任意选取其中之一);
     b.将v加入集合Vnew中,将<u, v>边加入集合Enew中;
 4. 输出:使用集合Vnew和Enew来描述所得到的最小生成树。

四.Prim编码

图的存储有很多方式,邻接矩阵,邻接表,十字链表等等,当然都有自己的适合场景,下面分别用邻接矩阵和邻接表。
其中邻接矩阵需要采用两个数组,一个是保存顶点信息的一维数组,另一个是保存边信息的二维数组。

邻接矩阵:

/*
     * prim最小生成树
     *
     * 参数说明:
     *   start -- 从图中的第start个元素开始,生成最小树
     */
    public void prim(int start) {
        int num = mVexs.length;         // 顶点个数
        int index=0;                    // prim最小树的索引,即prims数组的索引
        char[] prims  = new char[num];  // prim最小树的结果数组
        int[] weights = new int[num];   // 顶点间边的权值

        // prim最小生成树中第一个数是"图中第start个顶点",因为是从start开始的。
        prims[index++] = mVexs[start];

        // 初始化"顶点的权值数组",
        // 将每个顶点的权值初始化为"第start个顶点"到"该顶点"的权值。
        for (int i = 0; i < num; i++ )
            weights[i] = mMatrix[start][i];
        // 将第start个顶点的权值初始化为0。
        // 可以理解为"第start个顶点到它自身的距离为0"。
        weights[start] = 0;

        for (int i = 0; i < num; i++) {
            // 由于从start开始的,因此不需要再对第start个顶点进行处理。
            if(start == i)
                continue;

            int j = 0;
            int k = 0;
            int min = INF;
            // 在未被加入到最小生成树的顶点中,找出权值最小的顶点。
            while (j < num) {
                // 若weights[j]=0,意味着"第j个节点已经被排序过"(或者说已经加入了最小生成树中)。
                if (weights[j] != 0 && weights[j] < min) {
                    min = weights[j];
                    k = j;
                }
                j++;
            }

            // 经过上面的处理后,在未被加入到最小生成树的顶点中,权值最小的顶点是第k个顶点。
            // 将第k个顶点加入到最小生成树的结果数组中
            prims[index++] = mVexs[k];
            // 将"第k个顶点的权值"标记为0,意味着第k个顶点已经排序过了(或者说已经加入了最小树结果中)。
            weights[k] = 0;
            // 当第k个顶点被加入到最小生成树的结果数组中之后,更新其它顶点的权值。
            for (j = 0 ; j < num; j++) {
                // 当第j个节点没有被处理,并且需要更新时才被更新。
                if (weights[j] != 0 && mMatrix[k][j] < weights[j])
                    weights[j] = mMatrix[k][j];
            }
        }

        // 计算最小生成树的权值
        int sum = 0;
        for (int i = 1; i < index; i++) {
            int min = INF;
            // 获取prims[i]在mMatrix中的位置
            int n = getPosition(prims[i]);
            // 在vexs[0...i]中,找出到j的权值最小的顶点。
            for (int j = 0; j < i; j++) {
                int m = getPosition(prims[j]);
                if (mMatrix[m][n]<min)
                    min = mMatrix[m][n];
            }
            sum += min;
        }
        // 打印最小生成树
        System.out.printf("PRIM(%c)=%d: ", mVexs[start], sum);
        for (int i = 0; i < index; i++)
            System.out.printf("%c ", prims[i]);
        System.out.printf("\n");
    }

具体完整工程代码见我的github
https://github.com/shibing624/BlogCode/blob/master/src/main/java/xm/math/mst/ListUDG.java

邻接表也是类似,不过要简单一些:

/*
     * 创建图(用已提供的矩阵)
     *
     * 参数说明:
     *     vexs  -- 顶点数组
     *     edges -- 边
     */
    public ListUDG(char[] vexs, EData[] edges) {

        // 初始化"顶点数"和"边数"
        int vlen = vexs.length;
        int elen = edges.length;

        // 初始化"顶点"
        mVexs = new VNode[vlen];
        for (int i = 0; i < mVexs.length; i++) {
            mVexs[i] = new VNode();
            mVexs[i].data = vexs[i];
            mVexs[i].firstEdge = null;
        }

        // 初始化"边"
        for (int i = 0; i < elen; i++) {
            // 读取边的起始顶点和结束顶点
            char c1 = edges[i].start;
            char c2 = edges[i].end;
            int weight = edges[i].weight;

            // 读取边的起始顶点和结束顶点
            int p1 = getPosition(c1);
            int p2 = getPosition(c2);
            // 初始化node1
            ENode node1 = new ENode();
            node1.ivex = p2;
            node1.weight = weight;
            // 将node1链接到"p1所在链表的末尾"
            if (mVexs[p1].firstEdge == null)
                mVexs[p1].firstEdge = node1;
            else
                linkLast(mVexs[p1].firstEdge, node1);
            // 初始化node2
            ENode node2 = new ENode();
            node2.ivex = p1;
            node2.weight = weight;
            // 将node2链接到"p2所在链表的末尾"
            if (mVexs[p2].firstEdge == null)
                mVexs[p2].firstEdge = node2;
            else
                linkLast(mVexs[p2].firstEdge, node2);
        }
    }

具体完整工程代码见我的github
https://github.com/shibing624/BlogCode/blob/master/src/main/java/xm/math/mst/ListUDG.java

五.贪心算法vs动态规划算法

异同点

:动态规划和贪心算法都是一种递推算法,均有局部最优解来推导全局最优解。

贪心算法:

  1. 贪心算法中,作出的每步贪心决策都无法改变,因为贪心策略是由上一步的最优解推导下一步的最优解,而上一步之前的最优解则不作保留;
  2. 由 1 可以知道贪心法正确的条件是:每一步的最优解一定包含上一步的最优解。

动态规划算法:

  1. 全局最优解中一定包含某个局部最优解,但不一定包含前一个局部最优解,因此需要记录之前的所有最优解;
  2. 动态规划的关键是状态转移方程,即如何由以求出的局部最优解来推导全局最优解;
  3. 边界条件:即最简单的,可以直接得出的局部最优解。

贪心算法的缺点

贪心法的缺点:

  1. 不能保证求得的最后解是最佳的;
  2. 不能用来求最大或最小解问题;
  3. 只能求满足某些约束条件的可行解的范围。

实现该算法的过程,从问题的某一初始解出发;

while   能朝给定总目标前进一步   do   
求出可行解的一个解元素;   
由所有解元素组合成问题的一个可行解 

举个栗子:
还是给钱问题:

比如中国的货币,只看元,有1元2元5元10元20、50、100   

如果我要16元,可以拿16个1元,8个2元,但是怎么最少呢?   
如果用贪心算,就是我每一次拿那张可能拿的最大的。   
比如16,我第一次拿20拿不起,拿10元,OK,剩下6元,再拿个5元,剩下1元,  
也就是3张:10、5、1。   

每次拿能拿的最大的,就是贪心。  

但是一定注意,贪心得到的并不是最优解,也就是说用贪心不一定是拿的最少的张数。贪心只能得到一个比较好的解,而且贪心算法很好想得到。

再注意,为什么我们的钱可以用贪心呢?因为我们国家的钱的大小设计,正好可以使得贪心算法算出来的是最优解(一般国家的钱币都应该这么设计)。

如果设计成别的样子情况就不同了,
比如:

某国的钱币分为:1元,3元,4元   
如果要拿6元钱,怎么拿?
贪心的话:先拿4元,再拿两个1元,一共3张钱。
实际最优呢?两张3元就够了。

给钱问题编码

问题:有最小面额为 11 5 1的三种人民币,用最少的张数找钱?
要求:比较 动态规划与贪心算法 解决问题
贪心算法解题

 /***************************贪心算法********************************
     *方法:
     *     Num_Value[i]表示 面额为VALUEi 的人民币用的张数
     *     能用大面额的人民币,就尽量用大面额
     */
    static int Greed(int money, int Num_Value[]) {
        //要找开 money元人民币,Num_Value[1~3]保存 三种面额人民币的张数
        int total = 0;                                            //总张数,返回值也即是总张数。
        Num_Value[1] = 0;
        Num_Value[2] = 0;
        Num_Value[3] = 0;
        for (int i = money; i >= 1; ) {
            if (i >= VALUE1) {
                Num_Value[1]++;
                i -= VALUE1;
                total++;
            } else if (i >= VALUE2) {
                Num_Value[2]++;
                i -= VALUE2;
                total++;
            } else if (i >= VALUE3) {
                Num_Value[3]++;
                i -= VALUE3;
                total++;
            } else {
            }
        }
        return total;
    }

动态规划算法解题

//-------------------------求最小值---------------------------------
    static int min(int a, int b, int c) {
        return a < b ? (a < c ? a : c) : (b < c ? b : c);
    }

    //-------------------------动态规划算法-------------------------------
    static int DP_Money(int money, int Num[]) {
        //获得要找开money元钱,需要的人民币总张数
        int i;
        for (i = 0; i <= VALUE2; i++) {                               //0~4 全用 1元
            Num[i] = i;
        }
        for (i = VALUE2; i <= money; i++) {                           //从5元开始 凑钱
            if (i - VALUE1 >= 0) {                                //如果比 11 元大,说明多了一种用11元面额人民币的可能
                //从用 11元、5元、1元中 选择一个张数小的
                Num[i] = min(Num[i - VALUE1] + 1, Num[i - VALUE2] + 1, Num[i - VALUE3] + 1);
            } else {                                            //从5元、1元中 选择一个张数小的
                Num[i] = Math.min(Num[i - VALUE2] + 1, Num[i - VALUE3] + 1);
            }
        }
        return Num[money];
    }

具体完整实现的工程代码见我的github
https://github.com/shibing624/BlogCode/blob/master/src/main/java/xm/math/mst/TanxinVSDongtaiGuihua.java

参考

  1. 百度百科:http://baike.baidu.com/link?url=NgaK-CjbKPvd88hpTibuIB5_kN0U0Q9pNZg5mcHp1SLVUDQL9r89orkShYE3B5Dw9jOersjJKqt9WF1vBwMfnyjX1TCzLgRXfKXBeMMwot48kS9cc_17m4V_yWewNcni
  2. csdn动态规划:http://blog.csdn.net/mingzai624/article/details/51543728
  3. prim算法:http://blog.csdn.net/yeruby/article/details/38615045
  4. 动态规划算法对比:http://blog.csdn.net/jarvischu/article/details/6056963
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值