贪心算法

来源:互联网 发布:在上海工作的感受 知乎 编辑:程序博客网 时间:2024/06/09 22:17
贪心算法(又称贪婪算法)是指,在对问题求解时,总是做出在当前看来是最好的选择。也就是说,不从整体最优上加以考虑,他所做出的是在某种意义上的局部最优解。
贪心算法不是对所有问题都能得到整体最优解,关键是贪心策略的选择,选择的贪心策略必须具备无后效性,即某个状态以前的过程不会影响以后的状态,只与当前状态有关。[1] 
中文名
贪心算法
外文名
greedy algorithm
别    称
贪婪算法
性    质
一种改进了的分级处理方法
核    心
根据题意选取一种量度标准

目录

  1. 1 基本要素
  2.  贪心选择
  3.  最优子结构
  4. 2 基本思路
  5.  思想
  1.  过程
  2. 3 算法特性
  3. 4 例题分析
  4.  0-1背包问题
  5.  马踏棋盘
  1.  均分纸牌
  2. 5 备注
  3. 6 应用

基本要素

编辑

贪心选择

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

最优子结构

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

基本思路

编辑

思想

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

过程

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

算法特性

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

例题分析

编辑

0-1背包问题

有一个背包,背包容量是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问题,用贪心法并不一定可以求得最优解,以后了解了动态规划算法后本题就有了新的解法。

马踏棋盘

在8×8方格的棋盘上,从任意指定方格出发,为马寻找一条走遍棋盘每一格并且只经过一次的一条路径。
【初步设计】
首先这是一个搜索问题,运用深度优先搜索进行求解。算法如下:
⒈ 输入初始位置坐标x,y;
⒉ 步骤 c:
如果c> 64输出一个解,返回上一步骤c--
(x,y) ← c
计算(x,y)的八个方位的子结点,选出那些可行的子结点
循环遍历所有可行子结点,步骤c++重复2
显然⑵是一个递归调用的过程,大致如下:
C++程序:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#define N 8
void dfs(int x,int y,int count)
{
    int i,tx,ty;
    if(count>N*N)
    {
        output_solution();//输出一个解
        return;
    }
    for(i=0; i<8; i++)
    {
        tx=hn[i].x;//hn[]保存八个方位子结点
        ty=hn[i].y;
        s[tx][ty]=count;
        dfs(tx,ty,count+1);//递归调用
        s[tx][ty]=0;
    }
}
Pascal程序:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
ProgramYS;
ConstFXx:array[1..8]of-2..2=(1,2,2,1,-1,-2,-2,-1);
FXy:array[1..8]of-2..2=(2,1,-1,-2,-2,-1,1,2);
Var
Road:array[1..10,1..10]ofinteger;
x,y,x1,y1,total:integer;
ProcedureFind(x,y:integer);
varNx,Ny,i:integer;
Begin
Fori:=1to8do
begin{8个方向}
If(x+FXx[i]in[1..8])and(y+FXy[i]in[1..8])Then{确定新坐标是否越界}
IfRoad[x+Fxx[i],y+Fxy[i]]=0Then
begin{判断是否走过}
Nx:=x+FXx[i];Ny:=y+FXy[i];Road[Nx,Ny]:=1;{建立新坐标}
If(Nx=x1)and(Ny=y1)
Theninc(total)
elseFind(Nx,Ny);{递归}
Road[Nx,Ny]:=0{回朔}
end
end
End;
BEGIN{Main}
Total:=0;
FillChar(Road,sizeof(road),0);
Readln(x,y);{读入开始坐标}
Readln(x1,y1);{读入结束坐标}
If(x>10)or(y>10)or(x1>10)or(y1>10)Thenwriteln('Error'){判断是否越界}
ElseFind(x,y);
Writeln('Total:',total){打出总数}
END.
这样做是完全可行的,它输入的是全部解,但是马遍历当8×8时解是非常之多的,用天文数字形容也不为过,这样一来求解的过程就非常慢,并且出一个解也非常慢。
怎么才能快速地得到部分解呢?
【贪心算法】
其实马踏棋盘的问题很早就有人提出,且早在1823年,J.C.Warnsdorff就提出了一个有名的算法。在每个结点对其子结点进行选取时,优先选择‘出口’最小的进行搜索,‘出口’的意思是在这些子结点中它们的可行子结点的个数,也就是‘孙子’结点越少的越优先跳,为什么要这样选取,这是一种局部调整最优的做法,如果优先选择出口多的子结点,那出口少的子结点就会越来越多,很可能出现‘死’结点(顾名思义就是没有出口又没有跳过的结点),这样对下面的搜索纯粹是徒劳,这样会浪费很多无用的时间,反过来如果每次都优先选择出口少的结点跳,那出口少的结点就会越来越少,这样跳成功的机会就更大一些。这种算法称为为贪心算法,也叫贪婪算法或启发式算法,它对整个求解过程的局部做最优调整,它只适用于求较优解或者部分解,而不能求最优解。这样的调整方法叫贪心策略,至于什么问题需要什么样的贪心策略是不确定的,具体问题具体分析。实验可以证明马遍历问题在运用到了上面的贪心策略之后求解速率有非常明显的提高,如果只要求出一个解甚至不用回溯就可以完成,因为在这个算法提出的时候世界上还没有计算机,这种方法完全可以用手工求出解来,其效率可想而知。

均分纸牌

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#include<cstdio>
#include<iostream>
#include<cstdlib>
int a[1000];
using namespace std;
int f(int n)
{
    int ave=0;
    int f=0;
    for (int i=1;i<=n;i++)
    {
        f=f+a[i];
    }
    return f/n;
}
int main()
{
    int n;
    int ans=0;
    int ave;
    scanf ("%d",&n);
    for (int i=1;i<=n;i++)
    {
        scanf ("%d",&a[i]);
    }
    ave=f(n);
    for (int i=1;i<=n;i++)
    {
       if (a[i]==ave)
       {
             continue;
       }
       if (a[i]!=ave)
       {
           a[i+1]+=a[i]-ave;
           ans++;
       }
    }
    printf ("%d",ans);
    return 0;
}

备注

编辑
贪心算法当然也有正确的时候。求最小生成树的Prim算法和Kruskal算法都是漂亮的贪心算法。
贪心法的应用算法有Dijkstra的单源最短路径和Chvatal的贪心集合覆盖启发式
所以需要说明的是,贪心算法可以与随机化算法一起使用,具体的例子就不再多举了。其实很多的智能算法(也叫启发式算法),本质上就是贪心算法和随机化算法结合——这样的算法结果虽然也是局部最优解,但是比单纯的贪心算法更靠近了最优解。例如遗传算法,模拟退火算法。

应用

编辑
如把3/7和13/23分别化为三个单位分数的和
【贪心算法】
设a、b为互质正整数,a<b 分数a/b 可用以下的步骤分解成若干个单位分数之和:
步骤一: 用b 除以a,得商数q1 及余数r1。(r1=b - a*q1)
步骤二:把a/b 记作:a/b=1/(q1+1)+(a-r1)/b(q1+1)
步骤三:重复步骤2,直到分解完毕
3/7=1/3+2/21=1/3+1/11+1/231
13/23=1/2+3/46=1/2+1/16+1/368
以上其实是数学家斐波那契提出的一种求解埃及分数的贪心算法,准确的算法表述应该是这样的:
设某个真分数的分子为a,分母为b;
把b除以a的商部分加1后的值作为埃及分数的某一个分母c;
将a乘以c再减去b,作为新的a;
将b乘以c,得到新的b;
如果a大于1且能整除b,则最后一个分母为b/a;算法结束;
或者,如果a等于1,则,最后一个分母为b;算法结束;
否则重复上面的步骤。
备注:事实上,后面判断a是否大于1和a是否等于1的两个判断可以合在一起,及判断b%a是否等于0,最后一个分母为b/a,显然是正确的。
PHP代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
class tanxin{
  public $weight;
  public $price;
  public function __construct($weight=0,$price=0){
    $this->weight=$weight;
    $this->price=$price;
  }
}
//生成数据
$n=10;
for($i=1;$i<=$n;$i++){
  $weight=rand(1,20);
  $price=rand(1,10);
  $x[$i]=new tanxin($weight,$price);
}
//输出结果
function display($x){
  $len=count($x);
  foreach($x as $val){
    echo $val->weight,' ',$val->price;
    echo '<br>';
  }
}
//按照价格和重量比排序
function tsort(&$x){
  $len=count($x);
  for($i=1;$i<=$len;$i++)
  {
    for($j=1;$j<=$len-$i;$j++)
    
      $temp=$x[$j];
      $res=$x[$j+1]->price/$x[$j+1]->weight;
      $temres=$temp->price/$temp->weight;
      if($res>$temres){
        $x[$j]=$x[$j+1];
        $x[$j+1]=$temp;
      }
    }
  
}
//贪心算法
function tanxin($x,$totalweight=50){
  $len=count($x);
  $allprice=0;
  for($i=1;$i<=$len;$i++){
    if($x[$i]->weight>$totalweightbreak;
    else{
      $allprice+=$x[$i]->price;
      $totalweight=$totalweight-$x[$i]->weight;
    }
  }
  if($i<$len$allprice+=$x[$i]->price*($totalweight/$x[$i]->weight);
  return $allprice;
}
tsort($x);//按非递增次序排序
display($x);//显示
echo '0-1背包最优解为:';
echo tanxin($x);
Java源代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
package main;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Random;
 
public class Main {
 
    /**
    * 测试
    */
    public static void main(String[] args) {
        // 1.随机构造一批任务
        List<Pair<Integer>> inputList = new ArrayList<Pair<Integer>>();
        Random rand = new Random();
        for (int n = 0; n < 20; ++n) {
            Integer left = rand.nextInt(100);
            Integer right = left + rand.nextInt(100) + 1;
            Pair<Integer> pair = new Pair<Integer>(left, right);
            inputList.add(pair);
        }
        // 将任务列表按结束时间排序(也就是根据right字段进行排序)
        sortByRight(inputList);
        printPairList(inputList);
        // 执行算法
        List<Pair<Integer>> outputList = algorithm(inputList);
        System.out.println();
        printPairList(outputList);
    }
 
    /**
    * 贪心算法
    
    * @paraminputList
    * @return使数量最多的任务方案
    */
    public static <T extends Comparable<T>> List<Pair<T>> algorithm(
            List<Pair<T>> inputList) {
        if (null == inputList || inputList.size() == 0 || inputList.size() == 1) {
            return inputList;
        }
        sortByRight(inputList);
        List<Pair<T>> outputList = new ArrayList<Pair<T>>();
        int last = 0;
        outputList.add(inputList.get(last));
        int intputSize = inputList.size();
        for (int m = 1; m < intputSize; ++m) {
            Pair<T> nextPair = inputList.get(m);
            T nextLeft = nextPair.getLeft();
            Pair<T> lastOutPair = inputList.get(last);
            T lastRight = lastOutPair.getRight();
            int flag = nextLeft.compareTo(lastRight);
            if (flag >= 0) {
                outputList.add(nextPair);
                last = m;
            }
        }
 
        return outputList;
    }
 
    /**
    * 对传入的List<Pair<T>>对象进行排序,使Pair根据right从小到大排序。
    
    * @paraminputList
    */
    private static <T extends Comparable<T>> void sortByRight(
            List<Pair<T>> inputList) {
        CompareByRight<T> comparator = new CompareByRight<T>();
        Collections.sort(inputList, comparator);
    }
 
    /**
    * 打印一个List<Pair<T>>对象。
    
    * @paraminputList
    */
    private static <T extends Comparable<T>> void printPairList(
            List<Pair<T>> inputList) {
        for (Pair<T> pair : inputList) {
            System.out.println(pair.toString());
        }
    }
}
 
/**
 * 根据Pair.right比较两个Pair。用于Conlections.sort()方法。
 
 * @param<T>
 */
class CompareByRight<T extends Comparable<T>> implements Comparator<Pair<T>> {
    /* @ Override */
    public int compare(Pair<T> o1, Pair<T> o2) {
        T r1 = o1.getRight();
        T r2 = o2.getRight();
        int flag = r1.compareTo(r2);
        return flag;
    }
}
 
/**
 * 代表一个任务对象。有点装逼用模板来写了。left表示开始时间,right表示结束时间。
 
 * @param<T>
 */
class Pair<T extends Comparable<T>> {
    private T left;
 
    private T right;
 
    public Pair(T left, T right) {
        this.left = left;
        this.right = right;
    }
 
    @Override
    public String toString() {
        return "[left=" + left.toString() + ',' "right=" + right.toString()
                ']';
    }
 
    public T getLeft() {
        return left;
    }
 
    public void setLeft(T left) {
        this.left = left;
    }
 
    public T getRight() {
        return right;
    }
 
    public void setRight(T right) {
        this.right = right;
    }
}
参考资料
  • 1.  五大常用算法之三:贪心算法  .博客园.2010-05-22[引用日期2015-01-23]
  • 2.  浅析贪心算法  .中国知网[引用日期2016-12-21]
  • 3.  贪心算法的探讨与研究  .中国知网[引用日期2016-12-21]
原创粉丝点击