贪心算法方面
贪心算法是什么?
在求最优解问题的过程中,依据某种贪心标准,从问题的初始状态出发,直接去求每一步的最优解,通过若干次的贪心选择,最终得出整个问题的最优解,这种求解方法就是贪心算法。
从贪心算法的定义可以看出,贪心法并不是从整体上考虑问题,它所做出的选择只是在某种意义上的局部最优解,而由问题自身的特性决定了该题运用贪心算法可以得到最优解。
贪心算法详解
贪心算法思想:
顾名思义,贪心算法总是作出在当前看来最好的选择。也就是说贪心算法并不从整体最优考虑,它所作出的选择只是在某种意义上的局部最优选择。当然,希 望贪心算法得到的最终结果也是整体最优的。虽然贪心算法不能对所有问题都得到整体最优解,但对许多问题它能产生整体最优解。如单源最短路经问题,最小生成 树问题等。在一些情况下,即使贪心算法不能得到整体最优解,其最终结果却是最优解的很好近似。
贪心算法的基本要素:
1.贪心选择性质。所谓贪心选择性质是指所求问题的整体最优解可以通过一系列局部最优的选择,即贪心选择来达到。这是贪心算法可行的第一个基本要素,也是贪心算法与动态规划算法的主要区别。
动态规划算法通常以自底向上的方式解各子问题,而贪心算法则通常以自顶向下的方式进行,以迭代的方式作出相继的贪心选择,每作一次贪心选择就将所求问题简化为规模更小的子问题。
对于一个具体问题,要确定它是否具有贪心选择性质,必须证明每一步所作的贪心选择最终导致问题的整体最优解。
2.当一个问题的最优解包含其子问题的最优解时,称此问题具有最优子结构性质。问题的最优子结构性质是该问题可用动态规划算法或贪心算法求解的关键特征。
贪心算法的基本思路:
从问题的某一个初始解出发逐步逼近给定的目标,以尽可能快的地求得更好的解。当达到算法中的某一步不能再继续前进时,算法停止。
该算法存在问题:
1.不能保证求得的最后解是最佳的;
2.不能用来求最大或最小解问题;
3.只能求满足某些约束条件的可行解的范围。
实现该算法的过程:
从问题的某一初始解出发;
while 能朝给定总目标前进一步 do
求出可行解的一个解元素;
由所有解元素组合成问题的一个可行解;
用背包问题来介绍贪心算法:
背包问题:有一个背包,背包容量是M=150。有7个物品,物品可以分割成任意大小。要求尽可能让装入背包中的物品总价值最大,但不能超过总容量。
物品 A B C D E F G
重量 35 30 60 50 40 10 25
价值 10 40 30 50 35 40 30
分析如下
目标函数: ∑pi最大
约束条件是装入的物品总重量不超过背包容量:∑wi<=M( M=150)。
(1)根据贪心的策略,每次挑选价值最大的物品装入背包,得到的结果是否最优?
(2)每次挑选所占重量最小的物品装入是否能得到最优解?
(3)每次选取单位重量价值最大的物品,成为解本题的策略。
值得注意的是,贪心算法并不是完全不可以使用,贪心策略一旦经过证明成立后,它就是一种高效的算法。
贪心算法还是很常见的算法之一,这是由于它简单易行,构造贪心策略不是很困难。
可惜的是,它需要证明后才能真正运用到题目的算法中。
一般来说,贪心算法的证明围绕着:整个问题的最优解一定由在贪心策略中存在的子问题的最优解得来的。
贪心策略的特点与应用
一、贪心策略的定义
【定义1】 贪心策略是指从问题的初始状态出发,通过若干次的贪心选择而得出最优值(或较优解)的一种解题方法。
其实,从"贪心策略"一词我们便可以看出,贪心策略总是做出在当前看来是最优的选择,也就是说贪心策略并不是从整体上加以考虑,它所做出的选择只是在某种意义上的局部最优解,而许多问题自身的特性决定了该题运用贪心策略可以得到最优解或较优解。
二、贪心算法的特点
通过上文的介绍,可能有人会问:贪心算法有什么样的特点呢?我认为,适用于贪心算法解决的问题应具有以下2个特点:
1、贪心选择性质:
所谓贪心选择性质是指应用同一规则f,将原问题变为一个相似的、但规模更小的子问题、而后的每一步都是当前看似最佳的选择。这种选择依赖于已做出的选择,但不依赖于未做出的选择。从全局来看,运用贪心策略解决的问题在程序的运行过程中无回溯过程。关于贪心选择性质,读者可在后文给出的贪心策略状态空间图中得到深刻地体会。
2、局部最优解:
我们通过特点2向大家介绍了贪心策略的数学描述。由于运用贪心策略解题在每一次都取得了最优解,但能够保证局部最优解得不一定是贪心算法。如大家所熟悉得动态规划算法就可以满足局部最优解,在广度优先搜索(Breadth First Search)中的解题过程亦可以满足局部最优解。
在遇到具体问题时,往往分不清哪些题该用贪心策略求解,哪些题该用动态规划法求解。在此,我们对两种解题策略进行比较。
三、贪心策略的理论基础--矩阵胚
正如前文所说的那样,贪心策略是最接近人类认知思维的一种解题策略。但是,越是显而易见的方法往往越难以证明。下面我们就来介绍贪心策略的理论--矩阵胚。
"矩阵胚"理论是一种能够确定贪心策略何时能够产生最优解的理论,虽然这套理论还很不完善,但在求解最优化问题时发挥着越来越重要的作用。
【定义[B]3】矩阵胚是一个序对M=[S,I] ,其中S是一个有序非空集合,I是S的一个非空子集,成为S的一个独立子集。
如果M是一个N×M的矩阵的话,即:
若M是无向图G的矩阵胚的话,则S为图的边集,I是所有构成森林的一组边的子集。
如果对S的每一个元素X(X∈S)赋予一个正的权值W(X),则称矩阵胚M=(S,I)为一个加权矩阵胚。
适宜于用贪心策略来求解的许多问题都可以归结为在加权矩阵胚中找一个具有最大权值的独立子集的问题,即给定一个加权矩阵胚,M=(S,I),若能找出一个独立且具有最大可能权值的子集A,且A不被M中比它更大的独立子集所包含,那么A为最优子集,也是一个最大的独立子集。
矩阵胚理论对于我们判断贪心策略是否适用于某一复杂问题是十分有效的。
四、几种典型的贪心算法
贪心策略在图论中有着极其重要的应用。诸如Kruskal、 Prim、 Dijkstra等体现“贪心”思想的图形算法更是广泛地应用于树与图的处理。下面就分别来介绍Kruskal算法、Prim算法和Dijkstra算法。
Ⅰ、库鲁斯卡尔(Kruskal)算法
【定义4】 设图G=(V,E)是一简单连通图,|V| =n,|E|=m,每条边ei都给以权W ,W 假定是边e 的长度(其他的也可以),i=1,2,3,……,m。求图G的总长度最短的树,这就是最短树问题。
kruskal算法的基本思想是:首先将赋权图G的边按权的升序排列,不失一般性为:e ,e ,……,e 。其中W ≤W ,然后在不构成回路的条件下择优取进权最小的边。
其流程如下:
(1) 对属于E的边进行排序得e ≤e ≤……≤e 。
(2) 初始化操作 w←0,T←ф ,k←0,t←0;
(3) 若t=n-1,则转(6),否则转(4)
(4) 若T∪{e }构成一回路,则作
【k←k+1,转(4)】
(5) T←T∪{ e },w←w+ w ,t←t+1,k←k+1,转(3)
(6) 输出T,w,停止。
下面我们对这个算法的合理性进行证明。
设在最短树中,有边<v ,v >,连接两顶点v ,v ,边<v ,v >的权为wp,若<v ,v >加入到树中不能保证树的总长度最短,那么一定有另一条边<v ,v >或另两条边<v ,v >、<v ,v >,且w<vi,vj><wp或w<vi,vk>+w<vk,vj><wp,因为<v ,v >、<v ,v >不在最短树中,可知当<v ,v >、<v ,v >加入到树中时已构成回路,此时程序终止。因为<v ,v >∈ T,<v ,v >∈T且w<vI,vk>+w<vk,vj><w p,与程序流程矛盾。
下面给出C语言描述的kruskal算法:
#define MAXE <最多的边数>
typedef struct {
int u;// 边的起始顶点
int v;// 边的终止顶点
int w;// 边的权值
} Edge;
void kruskal(Edge E[],int n,int e)//边的权值从小到大排列
{
int i,j,m1,m2,sn1,sn2,k;
int vset[MAXV];
for(i=0;i<n;i++)
vset[i]=i;
k=1;j=0;
while(k<n)
{
m1=E[j].u;
m2=E[j].v;
sn1=vset[m1];
sn2=vset[m2];
if(sn1!=sn2)//两顶点属于不同的集合,是最小生成树的一条边
{
输出这条边;
k++;
for(i=0;i<n;i++)
if(vset[i]==sn2)
vset[i]=sn1;
}
j++;
}
}
kruskal算法对边的稀疏图比较合适,时间复杂度为o(elog2e),e是边数,与顶点无关.
Ⅱ、普林(Prim)算法:
Kruskal算法采取在不构成回路的条件下,优先选择长度最短的边作为最短树的边,而Prim则是采取了另一种贪心策略。
已知图G=(V,E),V={v ,v ,v ,..., v },D=(d ) 是图G的矩阵,若<v ,v >∈E,则令dij=∞,并假定dij=∞
Prim算法的基本思想是:从某一顶点(设为v )开始,令S←{v },求V/S中点与S中点v 距离最短的点,即从矩阵D的第一行元素中找到最小的元素,设为d,则令S←S∪{v},继续求V/S中点与S的距离最短的点,设为v ,则令S←S∪{v},继续以上的步骤,直到n个顶点用n-1条边连接起来为止。
流程如下:
(1) 初始化操作:T←ф,q(1)←-1,I从2到N作
【p(I)←1,q(I)←dI1】,k←1
(2) 若k≥n,则作【输出T,结束】
否则作【min←∞,j从2到N作
【若0<q(i)<min则作
【min←q(i) h←j】
】
】
(3) T←T∪{h,p(h)},q(h)←-1
(4) j从2到N作
【若d <q(j)则作【q(j)←d ,p(j)←h】】
(5) k←k+1,转(2)
算法中数组p(i)是用以记录和v 点最接近的属于S的点,q(i)则是记录了v 点和S中点的最短距离,q(i)=-1用以表示v 点已进入集合S。算法中第四步:v 点进入S后,对不属于S中的点vj的p(j)和q(j)进行适当调整,使之分别记录了所有属于S且和S距离最短的点和最短的距离,点v , v ,…,v 分别用1,2,…,n表示。
下面给出C语言描述的prim算法:
void prim(int cost[][MAXV],int n,int v)//v是起始顶点
{
int lowcost[MAXV],min;
int closest[MAXV],i,j,k;
/*closest[i]表示U中的一个顶点,该顶点和V-U中的一个顶点构成的边(i,closese[i])具有最小的权 */
//lowcost[i]表示边(i,closet[i])的权值
for(i=0;i<n;i++)
{
lowcost[i]=cost[v][i];
closest[i]=v;
}
for(i=1;i<n;i++)
{
min=INF;
for(j=0;j<n;j++)//在(V-U)中找出离U最近的顶点K.
if(lowcost[j]!=0&&lowcost[j]<min)
{
min=lowcost[j];
k=j;
}
输出边: closet[k]-->k;
lowcost[k]=0;//标记k已经加入U;
for(j=0;j<n;j++)//修改数组lowcost和closest
if(cost[k][j]!=0&&cost[k][j]<lowcost[j])
{
lowcost[j]=cost[k][j];
closest[j]=k;
}
}
}
Prim算法的时间复杂度为O(n^2),与边无关,适用于边稠密的图
Ⅲ、戴克斯德拉(Dijkstra)算法:
给定一个(无向)图G,及G中的两点s、t,确定一条从s到t的最短路径。
a[i][j]记边(i,j)的权,数组dist[u]记从源v到顶点u所对应的最短特殊路径长度
算法描述如下:
S1:初始化,S, T,对每个yS,{dist[y]=a[v][y],prev[y]=nil}
S2:若S=V,则输出dist,prev,结束
S3:uV/S中有最小dist值的点,SS{u}
S4:对u的每个相邻顶点x,调整dist[x]:即
若dist[x]>dist[u]+a[u][x],则{dist[x]=dist[u]+a[u][x],prev[x]=u},转S2
对于具有n个顶点和e条边的带权有向图,如果用带权邻接矩阵表示这个图,那么Dijkstra算法的主循环体需要O(n)时间。这个循环需要执行n-1次,所以完成循环需要O(n2)时间。算法的其余部分所需要时间不超过O(n2)。
五、贪心策略在P类问题求解中的应用
在现实世界中,我们可以将问题分为两大类。其中一类被称为P类问题,它存在有效算法,可求得最优解;另一类问题被称为NPC类问题,这类问题到目前为止人们尚未找到求得最优解的有效算法,这就需要每一位程序设计人员根据自己对题目的理解设计出求较优解的方法。下面我们着重分析贪心策略在求解P类问题中的应用。
在现实生活中,P类问题是十分有限的,而NPC类问题则是普遍的、广泛的。

浙公网安备 33010602011771号