贪心算法(又称贪婪算法)是指,在对问题求解时,总是做出在当前看来是最好的选择。也就是说,不从整体最优上加以考虑,他所做出的是在某种意义上的局部最优解。
贪心选择
贪心选择是指所求问题的整体最优解可以通过一系列局部最优的选择,即贪心选择来达到。这是贪心算法可行的第一个基本要素,也是贪心算法与动态规划算法的主要区别。贪心选择是采用从顶向下、以迭代的方法做出相继选择,每做一次贪心选择就将所求问题简化为一个规模更小的子问题。对于一个具体问题,要确定它是否具有贪心选择的性质,我们必须证明每一步所作的贪心选择最终能得到问题的最优解。通常可以首先证明问题的一个整体最优解,是从贪心选择开始的,而且作了贪心选择后,原问题简化为一个规模更小的类似子问题。然后,用数学归纳法证明,通过每一步贪心选择,最终可得到问题的一个整体最优解。
最优子结构
当一个问题的最优解包含其子问题的最优解时,称此问题具有最优子结构性质。运用贪心策略在每一次转化时都取得了最优解。问题的最优子结构性质是该问题可用贪心算法或动态规划算法求解的关键特征。贪心算法的每一次操作都对结果产生直接影响,而动态规划则不是。贪心算法对每个子问题的解决方案都做出选择,不能回退;动态规划则会根据以前的选择结果对当前进行选择,有回退功能。动态规划主要运用于二维或三维问题,而贪心一般是一维问题
基本思路
思想
贪心算法的基本思路是从问题的某一个初始解出发一步一步地进行,根据某个优化测度,每一步都要确保能获得局部最优解。每一步只考虑一个数据,他的选取应该满足局部优化的条件。若下一个数据和部分最优解连在一起不再是可行解时,就不把该数据添加到部分解中,直到把所有数据枚举完,或者不能再添加算法停止 [3] 。
过程
-
建立数学模型来描述问题;
-
把求解的问题分成若干个子问题;
-
对每一子问题求解,得到子问题的局部最优解;
-
把子问题的解局部最优解合成原来解问题的一个解。
算法特性
贪婪算法可解决的问题通常大部分都有如下的特性:
-
随着算法的进行,将积累起其它两个集合:一个包含已经被考虑过并被选出的候选对象,另一个包含已经被考虑过但被丢弃的候选对象。
-
有一个函数来检查一个候选对象的集合是否提供了问题的解答。该函数不考虑此时的解决方法是否最优。
-
还有一个函数检查是否一个候选对象的集合是可行的,也即是否可能往该集合上添加更多的候选对象以获得一个解。和上一个函数一样,此时不考虑解决方法的最优性。
-
选择函数可以指出哪一个剩余的候选对象最有希望构成问题的解。
-
最后,目标函数给出解的值。
-
为了解决问题,需要寻找一个构成解的候选对象集合,它可以优化目标函数,贪婪算法一步一步的进行。起初,算法选出的候选对象的集合为空。接下来的每一步中,根据选择函数,算法从剩余候选对象中选出最有希望构成解的对象。如果集合中加上该对象后不可行,那么该对象就被丢弃并不再考虑;否则就加到集合里。每一次都扩充集合,并检查该集合是否构成解。如果贪婪算法正确工作,那么找到的第一个解通常是最优的。
-
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;
}