贪心算法

从零开始 2020-04-30

介绍:

贪心算法(又称贪婪算法)是指,在对问题求解时,总是做出在当前看来是最好的选择。也就是说,不从整体最优上加以考虑,他所做出的是在某种意义上的局部最优解
贪心算法不是对所有问题都能得到整体最优解,关键是贪心策略的选择,选择的贪心策略必须具备无后效性,即某个状态以前的过程不会影响以后的状态,只与当前状态有关。

基本要素

贪心选择

贪心选择是指所求问题的整体最优解可以通过一系列局部最优的选择,即贪心选择来达到。这是贪心算法可行的第一个基本要素,也是贪心算法与动态规划算法的主要区别。贪心选择是采用从顶向下、以迭代的方法做出相继选择,每做一次贪心选择就将所求问题简化为一个规模更小的子问题。对于一个具体问题,要确定它是否具有贪心选择的性质,我们必须证明每一步所作的贪心选择最终能得到问题的最优解。通常可以首先证明问题的一个整体最优解,是从贪心选择开始的,而且作了贪心选择后,原问题简化为一个规模更小的类似子问题。然后,用数学归纳法证明,通过每一步贪心选择,最终可得到问题的一个整体最优解。
 

最优子结构

当一个问题的最优解包含其子问题的最优解时,称此问题具有最优子结构性质。运用贪心策略在每一次转化时都取得了最优解。问题的最优子结构性质是该问题可用贪心算法或动态规划算法求解的关键特征。贪心算法的每一次操作都对结果产生直接影响,而动态规划则不是。贪心算法对每个子问题的解决方案都做出选择,不能回退;动态规划则会根据以前的选择结果对当前进行选择,有回退功能。动态规划主要运用于二维或三维问题,而贪心一般是一维问题。
贪心算法

基本思路

思想

贪心算法的基本思路是从问题的某一个初始解出发一步一步地进行,根据某个优化测度,每一步都要确保能获得局部最优解。每一步只考虑一个数据,他的选取应该满足局部优化的条件。若下一个数据和部分最优解连在一起不再是可行解时,就不把该数据添加到部分解中,直到把所有数据枚举完,或者不能再添加算法停止 [3]  。

过程

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

算法特性

贪婪算法可解决的问题通常大部分都有如下的特性:
  • 随着算法的进行,将积累起其它两个集合:一个包含已经被考虑过并被选出的候选对象,另一个包含已经被考虑过但被丢弃的候选对象。
  • 有一个函数来检查一个候选对象的集合是否提供了问题的解答。该函数不考虑此时的解决方法是否最优。
  • 还有一个函数检查是否一个候选对象的集合是可行的,也即是否可能往该集合上添加更多的候选对象以获得一个解。和上一个函数一样,此时不考虑解决方法的最优性。
  • 选择函数可以指出哪一个剩余的候选对象最有希望构成问题的解。
  • 最后,目标函数给出解的值。
  • 为了解决问题,需要寻找一个构成解的候选对象集合,它可以优化目标函数,贪婪算法一步一步的进行。起初,算法选出的候选对象的集合为空。接下来的每一步中,根据选择函数,算法从剩余候选对象中选出最有希望构成解的对象。如果集合中加上该对象后不可行,那么该对象就被丢弃并不再考虑;否则就加到集合里。每一次都扩充集合,并检查该集合是否构成解。如果贪婪算法正确工作,那么找到的第一个解通常是最优的。

背包问题

有一个背包,背包容量是M=150kg。有7个物品,物品不可以分割成任意大小。要求尽可能让装入背包中的物品总价值最大,但不能超过总容量。
物品 A B C D E F G
重量 35kg 30kg 6kg 50kg 40kg 10kg 25kg
价值 10$ 40$ 30$ 50$ 35$ 40$ 30$
分析:
目标函数:∑pi最大
约束条件是装入的物品总重量不超过背包容量:∑wi<=M(M=150)
⑴根据贪心的策略,每次挑选价值最大的物品装入背包,得到的结果是否最优?
⑵每次挑选所占重量最小的物品装入是否能得到最优解?
⑶每次选取单位重量价值最大的物品,成为解本题的策略。
值得注意的是,贪心算法并不是完全不可以使用,贪心策略一旦经过证明成立后,它就是一种高效的算法。
贪心算法还是很常见的算法之一,这是由于它简单易行,构造贪心策略不是很困难。
可惜的是,它需要证明后才能真正运用到题目的算法中。
一般来说,贪心算法的证明围绕着:整个问题的最优解一定由在贪心策略中存在的子问题的最优解得来的。
对于例题中的3种贪心策略,都是无法成立(无法被证明)的,解释如下:
⑴贪心策略:选取价值最大者。
W=30
物品:A B C
重量:28 12 12
价值:30 20 20
根据策略,首先选取物品A,接下来就无法再选取了,可是,选取B、C则更好。
⑵贪心策略:选取重量最小。它的反例与第一种策略的反例差不多。
⑶贪心策略:选取单位重量价值最大的物品。
反例:
W=30
物品:A B C
重量:28 20 10
价值:28 20 10
根据策略,三种物品单位重量价值一样,程序无法依据现有策略作出判断,如果选择A,则答案错误。
【注意:如果物品可以分割为任意大小,那么策略3可得最优解】
对于选取单位重量价值最大的物品这个策略,可以再加一条优化的规则:对于单位重量价值一样的,则优先选择重量小的!这样,上面的反例就解决了。
但是,如果题目是如下所示,这个策略就也不行了。
W=40
物品:A B C
重量:25 20 15
价值:25 20 15
附:本题是个DP问题,用贪心法并不一定可以求得最优解,以后了解了动态规划算法后本题就有了新的解法。
public class Greed {
    /*贪心算法:在对问题求解时, 没一部的选择都是最优的选择*/
    public static void main(String[] args) {
        System.out.println("121212");
        //创建广播电台
        Map<String, HashSet<String>> broadcasts = new HashMap<String, HashSet<String>>();
      HashSet<String> hashSet1 = new HashSet<>();
      hashSet1.add("北京");
      hashSet1.add("上海");
      hashSet1.add("天津");
        HashSet<String> hashSet2 = new HashSet<>();
        hashSet2.add("广州");
        hashSet2.add("北京");
        hashSet2.add("深圳");
        HashSet<String> hashSet3 = new HashSet<>();
        hashSet3.add("成都");
        hashSet3.add("上海");
        hashSet3.add("杭州");
        HashSet<String> hashSet4 = new HashSet<>();
        hashSet4.add("上海");
        hashSet4.add("天津");
        HashSet<String> hashSet5 = new HashSet<>();
        hashSet5.add("杭州");
        hashSet5.add("大连");
        broadcasts.put("K1",hashSet1);
        broadcasts.put("K2",hashSet2);
        broadcasts.put("K3",hashSet3);
        broadcasts.put("K4",hashSet4);
        broadcasts.put("K5",hashSet5);

        /*allAreas存放所有的地区*/
        HashSet<String> allAreas = new HashSet<>();
        allAreas.add("北京");
        allAreas.add("上海");
        allAreas.add("天津");
        allAreas.add("广州");
        allAreas.add("深圳");
        allAreas.add("成都");
        allAreas.add("杭州");
        allAreas.add("大连");
        //存放选择的电台集合
        ArrayList<String> selects = new ArrayList<>();

        HashSet<String> tempSet = new HashSet<>();//交集
        //最大覆盖电台数
        String maxKey = null;
        while (allAreas.size()!=0){
            maxKey=null;//置空
            for (String key : broadcasts.keySet()){
                tempSet.clear();
                //取出key 对应的地区
                HashSet<String> areas = broadcasts.get(key);
                tempSet.addAll(areas);
                //取交集
                tempSet.retainAll(allAreas);
                if (tempSet.size()>0 &&
                        (maxKey ==null||tempSet.size()>broadcasts.get(maxKey).size())){
                        maxKey =key;
                }
            }
            if (maxKey!=null){
                selects.add(maxKey);
                //将广播电台成all清除;
                allAreas.removeAll(broadcasts.get(maxKey));
            }
        }
        System.out.println("得到的选择结果 "+selects);
    }

}

马踏棋盘

在8×8方格的棋盘上,从任意指定方格出发,为马寻找一条走遍棋盘每一格并且只经过一次的一条路径。
【初步设计】
首先这是一个搜索问题,运用深度优先搜索进行求解。算法如下:
⒈ 输入初始位置坐标x,y;
⒉ 步骤 c:
如果c> 64输出一个解,返回上一步骤c--
(x,y) ← c
计算(x,y)的八个方位的子结点,选出那些可行的子结点
循环遍历所有可行子结点



public class ma {

    //马在一个点时,能走的方向有8个;
    static final int[] fx = {-1,-2,-2,-1,1,2,2,1};
    static final int[] fy={2,1,-1,-2,-2,-1,1,2};
    static final int N =8;              //棋盘的大小;
    static int[][] board = new int[N][N];//马走的棋盘;

    class manode{
        int x;          //x坐标
        int y;          //y坐标
        int way;        //该点有多少种走法; 
    }                   //定义一个内部类,马所在点的信息;

    int wayout(int x,int y){
        int tx,ty;
        int count = 0;
        if(x<0||y<0||x>=N||y>=N||board[x][y]>0){
            return -1;
        }               //当点超出边界或者所在的点的值不为0时,返回-1;
        for(int i = 0;i<N;i++){
            tx = x+fx[i];
            ty = y+fy[i];
            if(tx<0||ty<0||tx>=N||ty>=N){
                continue;//如果点的另一个点超出边界时,就continue;
            }
            if(board[tx][ty] == 0){
                count++;//否则计数器计数;
            }
        }
        return count;
    }//计算该点的走法有多少种;

    void sort(manode[] hn,int n){
        int i,j,k;
        manode temp;    //临时对象,用来排序交换;
        for(i=0;i<n;i++){
            for(k=i,j=i+1;j<n;j++){
                if(hn[j].way<hn[k].way)
                    k=j;
            }
            if(k>i){
                temp = hn[i];
                hn[i] = hn[k];
                hn[k] = temp;
            }
        }
    }//将走法的数量进行排序,将最少的走法放在数组的头;

    void dfs(int x,int y,int count){
        int i,tx,ty;
        manode[] t = new manode[N];
        if(count >N*N){
            output();
            return;
        }   //当count计数器超过64时,打印输出;

        for(i=0;i<N;i++){
            tx = x+fx[i];
            ty = y+fy[i];
            manode h = new manode();
            t[i]=h;
            t[i].x = tx;
            t[i].y = ty;
            t[i].way = wayout(tx,ty);
        }
        sort(t,N);

        for(i = 0;t[i].way<0;i++)
            ;
        for(;i<N;i++){
            tx = t[i].x;
            ty = t[i].y;
            board[tx][ty] = count;
            dfs(tx,ty,count+1);
            board[tx][ty] = 0;//遇到死路时,回退回去,并将其变成0;
        }
    }//深度搜索与回溯算法;

    public static void main(String[] args) {
        int x,y;
        Scanner input = new Scanner(System.in);
        System.out.println("please input x,y:");
        x = input.nextInt();
        y = input.nextInt();
        ma test = new ma();
        board[x][y]=1;
        test.dfs(x, y, 2);
    }

    void output(){
        for(int i = N-1;i>=0;i--){
            for(int j = 0;j<N;j++){
                System.out.printf("%d\t",board[i][j]);
            }
            System.out.printf("\n\n\n");
        }
        System.exit(0);
    }       //打印输出函数;
}

publicclass ma {//马在一个点时,能走的方向有8个;staticfinalint[] fx = {-1,-2,-2,-1,1,2,2,1}; staticfinalint[] fy={2,1,-1,-2,-2,-1,1,2}; staticfinalint N =8; //棋盘的大小;staticint[][] board = newint[N][N];//马走的棋盘;class manode{int x; //x坐标int y; //y坐标int way; //该点有多少种走法; } //定义一个内部类,马所在点的信息;int wayout(int x,int y){ int tx,ty; intcount = 0; if(x<0||y<0||x>=N||y>=N||board[x][y]>0){ return -1; } //当点超出边界或者所在的点的值不为0时,返回-1;for(int i = 0;i<N;i++){ tx = x+fx[i]; ty = y+fy[i]; if(tx<0||ty<0||tx>=N||ty>=N){ continue;//如果点的另一个点超出边界时,就continue; } if(board[tx][ty] == 0){ count++;//否则计数器计数; } } returncount; }//计算该点的走法有多少种;void sort(manode[] hn,int n){ int i,j,k; manode temp; //临时对象,用来排序交换;for(i=0;i<n;i++){ for(k=i,j=i+1;j<n;j++){ if(hn[j].way<hn[k].way) k=j; } if(k>i){ temp = hn[i]; hn[i] = hn[k]; hn[k] = temp; } } }//将走法的数量进行排序,将最少的走法放在数组的头;void dfs(int x,int y,intcount){ int i,tx,ty; manode[] t = new manode[N]; if(count >N*N){ output(); return; } //当count计数器超过64时,打印输出;for(i=0;i<N;i++){ tx = x+fx[i]; ty = y+fy[i]; manode h = new manode(); t[i]=h; t[i].x = tx; t[i].y = ty; t[i].way = wayout(tx,ty); } sort(t,N); for(i = 0;t[i].way<0;i++) ; for(;i<N;i++){ tx = t[i].x; ty = t[i].y; board[tx][ty] = count; dfs(tx,ty,count+1); board[tx][ty] = 0;//遇到死路时,回退回去,并将其变成0; } }//深度搜索与回溯算法;publicstaticvoid main(String[] args) { int x,y; Scanner input = new Scanner(System.in); System.out.println("please input x,y:"); x = input.nextInt(); y = input.nextInt(); ma test = new ma(); board[x][y]=1; test.dfs(x, y, 2); } void output(){ for(int i = N-1;i>=0;i--){ for(int j = 0;j<N;j++){ System.out.printf("%d\t",board[i][j]); } System.out.printf("\n\n\n"); } System.exit(0); } //打印输出函数;}

相关推荐