关于贪心法的基本思想,最优度量标准:
先来看看问题的一般特征:对于我们遇到的一个问题(比如01背包问题),问题的解是由n个输入的、满足某些事先给定的条件的子集组成。
解决问题的一般方法是:我们会根据题意,选取一种度量标准,然后按照这种度量标准对n个输入排序,排好序后按照这种顺序,一次输入一个量。
如果这个输入和当前已构成在这种度量意义下的部分最优解加在一起不能产生一个可行解(比如说背包问题中,加入当前这个输入后背包超重了),则不把此输入加到这部分解中。否则,将当前输入合并到部分解中从而得到包含当前输入的新的部分解。
这种能够得到某种量度意义下的最优解的分级处理方法称为贪心方法。
需要注意的是,得到的贪心解不一定是最优解,度量标准有很多,我们可能只是选择其中一种作为度量标准。
因此,使用贪心法求解问题的关键就变成了选取能够得到问题最优解的那个度量标准。
接下来让我们看看使用贪心法求解几个问题的思路和对应的时间复杂度,以此来加深对贪心法的理解。
1.贪心法解决部分背包问题:
问题描述:已知n种物品具有重量(W1,W2,...Wn)和效益值(P1,P2,......Pn),以及一个可容纳M重量的背包;设当物品i全部或部分xi放入背包将得到pixi的效益值。这里0=<xi<=1,pi>0。
问题:采用怎样的方法才能使得装入背包的物品的总效益最大?
上面说过贪心法的关键是找到能够使得问题得到最优解的度量标准。在这个问题里的度量标准是每次装入要使得累计效益值与所用容量的比值有最多的增加(首次装入)和最小的减小(越往后装这个比值肯定是越来越小的)。
所以,我们按照物品的单位效益值:pi/wi(效益值比重量)比值的非增次序来放入背包。
下面用伪代码来描述其思想,我觉得伪代码看起来也是比较晦涩的,但是稍微试着努力看一下,还是可以看明白的。
procedure GREEDY——KNAPSACK(P,W,M,X,n) //procedure(程序) GREEDY(贪心法) KNAPSACK(背包问题) (P,W,M,X,n)为参数。
real P(1:n),W(1:n),X(1:n),M,cu; //P(1:n),W(1:n)是n件物品的效益值和重量这两个数组。M是背包的容量大小,而x(1:n)是解向量
real P(1:n),W(1:n),X(1:n),M,cu;//real代表实数
integer I,n
X←0//将解向量初始化为空
cu←M//cu是背包的剩余容量
for i←1 to n do
if W(i)>cu then exit endif
X(i)←1
cu←cu-W(i)
repeat//for和repeat之间的是循环操作要做的内容
if i<=n,thenX(i)←cu/W(i) endif//这个是处理放到最后一整个物品放不下,选择该物品的一部分放
end GREEDY -KNAPSACK
2.带有限期的作业排序
问题描述:假定在一台机器上处理n个作业,每个作业均可在单位时间内完成(也就是每个作业的完成时间都是1);同时每个作业i都有一个截至期限di>0,当且仅当作业i在其截止期限以前被完成时,则获得pi>0的效益。
现在的问题是:求这n个作业的一个子集J,其中的所有作业都可在其截止期限内完成。从这里就可以看到J其实是问题的一个可行解。
可行解J中的所有作业的效益和,具有最大效益值的可行解是该问题的最优解。
目标函数:
约束条件:可行解中的所有作业都应该在其期限之前完成。
来到了最关键的部分:度量标准的选择。这里,我们以目标函数作为度量。
度量标准:下一个要计入的作业将是使得在满足所生产的J是一个可行解的限制条件下让
得到最大增加的作业。
处理规则:按Pi的非增次序(也就是这些作业按效益值从大到小,大的优先考虑)处理这些作业。
作业排序算法的概略描述
procedure GREEDY-JOB(D,J,n)//作业已经默认是按照p1>p2>p3的效益值从大到小的次序输入,D(i)是它们的期限值。J是在它们的截止期限完成的作业的集合。
J←{1}
fori←2 to n do
if J∪{i}的所有作业能在它们的截止期限前完成
then J←J∪{i}//作业i加入进J
endif
repeat
end GREEDY-JOB
带有限期和效益的单位时间的作业排序贪心算法详细描述
procedure JS(D,J,n,k)
//D(i)是期限值.J(i)是最优解中的第i个作业,终止时,D(J(i)<=D(J(i+1))。
integer D(0:n),J(0:n),i,k,n,r
D(0)←J(0)←0//初始化
k←1;J(1)←1//计入作业1
for i←2 to n do //按p的非增次序考虑作业。找i的位置并检查插入的可行性
r←k//这里为什么要把k赋给r呢,是因为用r来记录当前已经加入进解向量中的数量有多少,然后下一步还得让r从后往前跟当前选择的作业的截止期限进行比较。
while D(J(r))>D(i) and D(J(r))不等于r do //r可以后移,找到最前插入位置
r←r-1
repeat//while循环运行到这里
if D(J(r))=<D(i) and D(i)>r then //把i插到J中
for i←k to r+1 by -1 do//倒着来,k是解向量中的最后一个,从后往前一直到作业i要插入的位置,把这些作业依此向后移动一位
J(i+1)←J(i)//将插入点的作业后移一位
repeat
J(r+1)←I;k←k+1//插入当前元素,同时解向量的个数增加一个
end if
repeat//对应第一个for循环
end JS
计算时间分析
for i←2 to n do //这个将循环n-1次
r←k
while D(J(r))>D(i) and D(J(r))不等于r //至多循环k次,k时当前计入J中的作业数
r←r-1
repeat
If D(J(r))=<D(i) and D(i) >r then
for i←k to r+1 by -1 do 循环k-r次,r是插入点的位置。
J(i+1)←J(i)
repeat
J(r+1)←I;k←k+1
endif
repeat
在这里设S是最终计入J中的作业数,则算法JS所需总时间是O(sn)。s=<n,故
最坏情况:Tjs=O(n^2),特例情况:pi=di=n-i+1,1=<i=<n(我的理解是效益值大的截止时间在后面)
最好情况 Tjs=O(n),特例情况:pi=di=i,1=<i=<n(效益值大的截止时间小)
使用UNION和FIND算法的作业排序问题
贪心法解决最小生成树问题
首先还是问题描述:
生成树:设G=(V,E)是一个无向连通图。如果G的生成子图T=(V,E')是一棵树,则称T是G的一棵生成树(spanning tree)。
贪心策略:选择能使迄今为止所计入的边的成本和有最小增加的那条边。
有两种算法,prim和kruskal算法。
先来看prim:
策略:使得每次选择一条边加入集合A(集合A用来存放每次选择的边)后能构成一棵树,对将要加入到A中的下一条边(u,v),应该是不在A中且使得A∪{(u,v)}也是一棵树的最小成本边。简单来说,就是每次找成本最小的边,如果这条边加入后不能构成树,就找下一条相对小的边。
算法伪代码描述:
procedure PRIM(E,COST,n,T,mincost)//E是G的边,COST(n,n)是n结点图G的成本邻接矩阵,矩阵元素COST(i,j)是一个正实数。如果不存在边(i,j),则为+∞。计算一颗最小生成树并把它作为一个集合存放到数组T(1:n-1,2)中(T(i,1),T(i,2))是最小生成树的一条边。最小生成树的总成本最后赋给mincost。
NEAR(j)是树中使得COST(j,NEAR(j))是对NEAR(j)所有选择中的最小值
real COST(n,n),mincost
integer NEAR(n),n,i,j,k,I,T(1:n-1,2)
(k,I)←具有最小成本的边
mincost←COST(k,I)
(T(I,1),T(I,2))←(k,I)
for i←1 to n do//将NEAR置初值
if COST(i,I)<COST(i,k) then NEAR(i)←I
else NEAR(i)←k
endif
repeat//对应于for i←1 to n do(结束将NEAR置初值)
NEAR(k)←NEAR(I)←0
for i← 2 to n-1 do//找T的其余n-2条边
设j是NEAR(j)不等于0且COST(j,NEARR(j))最小的下标
(T(i,1),T(i,2))←(j,NEAR(j))
prim的伪代码还需整理。。。
Kruskal算法
(连通)图的边按成本的非降次序排列,下一条计入生成树T中的边是还没有计入的边中的具有最小成本、且和T中现有的边不会构成环路的边。简单说就是从小往大挑不会构成环路的边。
单源最短路径
1.问题描述:
已知一个n结点的有向图G=(V,E)和边的权函数c(e),求由G中某指定结点V0到其他各个结点的最短路径。假定边的权值为正。
2.贪心策略求解:
1.度量标准
量度的选择:迄今已经生成的所有路径长度之和———为使之达到最小,其中任意一条路径都应该具有最小长度:
假定已经构造了i条最短路径,则下一条要构造的路径应是下一条最短的路径。
处理规则:按照路径长度的非降次序依此生成从结点v0到其他各个结点的最短路径。
也就是说单源最短路径问题是求一个结点到其余所有结点的最短路径。比如一个图中有5个结点,那么就会求出一个点分别到其他4个点的总共4条最短路径。
2.贪心算法
※设S是已经对其生成了最短路径的结点集合(包括出发点v0)。
※对于当前不在S中的结点w,记DIST(w)是从v0开始,只经过s中的结点而在w结束的那条最短路径的长度。(DIST数组里面存距离)
1.如果下一条最短路径是到结点u,则这条路径是从结点v0出发在u处终止,且只经过那些在S中的结点,,也就是说从v0到u的这条最短路径上的所有中间结点都是S中的结点。
设w是这条路径上的任意中间结点,则从v0到u的路径页包含了一条从v0到w的路径,且其长度小于从v0到u的路径长度。
根据生成规则:最短路径是按照路径长度的非降次序(为什么会非降呢?因为每个被确定的最短路径结点都是从之前结点的基础上延伸)生成的,因此从v0到w的最短路径应该已经生成。从而w也应该在S中。所以不存在不在S中的中间节点。
2.所生成的下一条路径的终点u必定是所有不在S内的结点且具有最小距离DIST(u)的结点。
3.如果选出了这样的结点u并生成了从v0到u的最短路径,结点u将成为S中的一个成员。此时,那些从v0出发,只经过S中的结点并且在S外的结点w处结束的最短路径可能会减少——DIST(w)的值变小。
如果这样的路径的长度发生了改变,则这些路径必定是一条从v0开始,经过u然后到w的更短的路所致。
※根据DIST(w属于的定义,它所表示的v0到w的最短路径上的所有中间节点都在S中;
※只考虑<u,w>属于E和<u,w>不属于E的情况。
※u是从v0到w的最短路径上所经过的结点。则有:DIST(w)=DIST(u)+c(u,w)
伪代码有点繁琐,先写到这里