动态规划方法求解最优化问题时,每个步骤都面临多种选择。对于许多最优化问题,使用动态规划算法来求解最优解有些杀鸡用牛刀了,可以使用更简单更高效的贪心算法。贪心算法在每一步的选择中,都选择当时最佳的情况。即局部最优的选择。贪心算法并不能保证总能得到最优解,但是很多问题确实可以求得最优解。
一:活动选择问题
多个活动调度竞争共享资源,目标是选出一个最大的互相兼容的活动集合。假设有一个n个活动组成的集合S = { }。这些活动使用同一个资源(如教室)。该资源一次只能被一个活动占用。每个活动 都有一个开始时间 和结束时间 。其中0 < ∞。如果被选中后,活动 就占据半开时间区间[ )。如果两个活动 满足[ )和[ )不重叠,则他们是兼容的。在活动选择问题中,希望选出一个最大兼容活动集。假定活动已经按照结束时间进行了排序:。
比如下面的活动集合S:
子集{ }中的活动相互兼容,子集{ }和子集{ }都是最大相容子集。
1:最优子结构
可以证明该问题具有最优子结构性质,考虑S的子集 ,表示在 开始前的所有活动的集合。希望求得 的最大兼容子集。如果 就是这样的一个子集。假设 在 中,可以证明 中也包含两个子问题 的最优解。
所以,可以使用动态规划的方法求解活动选择问题,用c[i,j]表示集合 的最优解的大小,所以有:c[i,j] = c[i,k] + c[k, j] + 1。这是在已经知道最优解包含k的情况下,如果k未知,则面临多个选择,因而有递归式:
。
所以,普通的递归算法如下,数组s表示开始时间结合,f表示结束时间的集合:
normal_actsel(i, j)
q = 0;
for(k = i+1; k < j; k++)
{
if(s[k]>= f[i] && f[k] <= s[j])
{
temp= normal_actsel(i,k) + normal_actsel(k,j) + 1;
q= max(q, temp);
}
}
return q;
2:贪心选择
如果在求解问题时,不考虑所有的选择,而只考虑贪心选择,贪心选择就是选出它后剩下的资源应该能被尽可能多的活动所用,所以可以选择集合S中最早结束或者最晚开始的活动。这里只讨论最早结束的活动。
令 = { S: }。下面证明任意非空集合,如果为中结束时间最早的活动,则在的某个最大兼容活动子集中。
证明:假设 是 的一个最大兼容活动子集,并且 为 中,结束时间最早的活动。
如果 = ,则已得证;
如果 ,则令集合 = ( ) { }。也就是将 中的元素 替换为 。因为 为 中结束时间最早的活动,所以 中的活动也是相互兼容的,又因为| | = | |,所以 也是 的最大兼容活动子集,所以得证。
3:递归贪心算法
贪心算法通常采用自顶向下的设计,因为不需要做出过多的选择而求解所有的子问题。
在该问题中,通常假定活动已经按照结束时间排好序,如果未排序,则可以在O(n lgn)时间内排序。
递归贪心算法如下,k表示求解活动 结束后的最大兼容子集,初始调用为0;
void recurgreedy_actsel(int k)
{
static int i = 0;
int m;
m = k+1;
while(m<= N && s[m] < f[k])
{
m =m+1;
}
if(m <=N)
{
A[i++]= m;
recurgreedy_actsel(m);
}
}
4:迭代贪心算法
将递归算法改变为循环之后,得到算法如下:
void loopgreedy_actsel()
{
int i = 0;
int k = 0;
int m;
k= 0;
m = k+1;
while(m <= N)
{
if(s[m]>= f[k])
{
A[i++]= m;
k= m;
m= k+1;
}
else
{
m++;
}
}
}
递归算法和循环算法的时间复杂度为O(n),而动态规划的时间复杂度为O( )
二:贪心算法原理
一般情况下,可以按照下面的步骤设计贪心算法:
a:将最优化问题转化为这样的形式:对其作出一次选择之后,只剩下一个子问题需要求解;
b:证明作出贪心选择后,原问题总是存在最优解,也就是贪心选择总是安全的;
c:证明作出贪心选择后,剩余的子问题满足性质:其最优解与贪心选择组合即可得到原问题的最优解。
每个贪心算法之下,几乎总有一个更繁琐的动态规划算法。
某个问题是否适合贪心算法,贪心选择性质和最优子结构性质是两个关键要素。
1:贪心选择性质
通过局部最优选择来构造最优解。也就是说,当考虑做何选择时,我们只考虑对当前问题最佳的选择而不考虑子问题的结果。
这一点是贪心算法不同于动态规划之处。在动态规划中,每一步都要做出选择,但是这些选择依赖于子问题的解(比如钢条切割的第一切割点)。因此,解动态规划问题一般是自底向上,从小子问题处理至大子问题。在贪心算法中,我们所做的总是当前看似最佳的选择、然后再解决选择之后所出现的子问题。贪心算法不依赖于将来的选择或者子问题的解。
因此,不像动态规划方法那样自底向上地解决子间题,贪心策略通常是自顶向下地做的,一个一个地做出贪心选择,不断地将给定的问题实例归约为更小的问题。
当然,必须证明每个步骤作出贪心选择能生成局部最优解。
2:最优子结构
最优子结构也是应用于贪心算法的关键要素。在贪心算法中使用最优子结构时,通常是用更直接的方式:如前所述,假设在原问题中做了一个贪心选择而得到了一个子问题。真正要做的是证明将此子问题的最优解与所做的贪心选择合并后,的确可以得到原问题的一个最优解。这个方案意味着要对子问题采用归纳法,来证明每个步骤中所做的贪心选择最终会产生出一个最优解。
3:背包问题
贪心算法和动态规划不容易区分,背包问题例子如下:
01背包问题:有一个贼在偷窃一家商店时发现有n个物品,第i件物品价值为 ,重量为 。此处 和 都是整数。他希望带走的东西越值钱越好,但他的背包中至多只能装下W磅的东西,W为一整数。应该带走哪几样东西?(这个问题之所以称为01背包问题,是因为每件物品或被带走,或被留下。小愉不能只带走某个物品的一部分或带走两次以上的同一物品。)
分数背包问题:场景等与上面问题一样,但是窃贼可以带走物品的一部分,而不必做出01的二分选择。可以把01背包问题的一件物品想像成一个金锭,而部分背包问题中的一件物品则更像金粉。
两种背包问题都具有最优子结构性质,但分数背包问题可用贪心策路来解决,而01背包问题却不行。
为解决分数背包问题,先对每件物品计算其每磅的价值 / 。按照一种贪心策略,窃贼开始时对具有最大每磅价值的物品尽量多拿一些。如果他拿完了该物品而仍可以取一些其他物品时,他就再取具有次大的每磅价值的物品,一直继续下去,直到不能再取为止,这样,通过按每磅价值来对所有物品排序,贪心算法就可以O(n lgn)时间内运行。关于贪心选择的证明类似于上一节活动选择的证明。
对于01背包问题,当我们在考虑是否要把一件物品加到背包中时,必须对把该物品加进去的子问题的解与不取该物品的子问题的解进行比较。由这种方式形成的问题导致了许多重叠子问题(这是动态规划的一个特点)。所以,我们可以用动态规划来解决。递归方程如下:
f(n,m)=max{f(n-1,m), f(n-1,m-w[n])+P(n)}
f(n,m)表示前n个物品,放入总承重m的包中的最大价值。对于第n个物品,考虑放入和不放入的最大值。代码如下:
for(i=0;i<n;i++)
for(j=0;j<m;j++)
c[i][j]=0;
for(i=1;i<n+1;i++)
for(j=1;j<m+1;j++)
{
if(w[i]<=j)
{
if( p[i]+c[i-1][j-w[i]] > c[i-1][j] )
c[i][j]=p[i]+c[i-1][j-w[i]];
else
c[i][j]=c[i-1][j];
}
else
c[i][j]=c[i-1][j];
}
return(c[n][m]);
三:赫夫曼编码
赫夫曼编码可以有效的压缩数据,通常可以节省20%~30%的空间。根据字符的出现频率,赫夫曼贪心算法构造字符的最优二进制表示。
赫夫曼树是一颗满二叉树,但不是二叉搜索树。赫夫曼树的过程中需要用到最小优先队列Q,Q可以用最小二叉堆实现。
更多的赫夫曼编码参照 ”./10/06哈夫曼树及哈夫曼编码”。