第十六章:贪心算法--活动选择问题html
前言:贪心算法也是用来解决最优化问题,将一个问题分红子问题,在如今子问题最优解的时,选择当前看起来是最优的解,指望经过所作的局部最优选择来产生一个全局最优解。书中先从活动选择问题来引入贪心算法,分别采用动态规划方法和贪心算法进行分析。本篇笔记给出活动选择问题的详细分析过程,并给出详细的实现代码进行测试验证。关于贪心算法的详细分析过程,下次在讨论。ios
一、活动选择问题描述算法
有一个须要使用每一个资源的n个活动组成的集合S= {a1,a2,···,an },资源每次只能由一个活动使用。每一个活动ai都有一个开始时间si和结束时间fi,且 0≤si<fi<∞ 。一旦被选择后,活动ai就占据半开时间区间[si,fi)。若是[si,fi]和[sj,fj]互不重叠,则称ai和aj两个活动是兼容的。该问题就是要找出一个由互相兼容的活动组成的最大子集。例以下图所示的活动集合S,其中各项活动按照结束时间单调递增排序。编程
从图中能够看出S中共有11个活动,最大的相互兼容的活动子集为:{a1,a4,a8,a11,}和{a2,a4,a9,a11}。数组
二、动态规划解决过程安全
(1)活动选择问题的最优子结构数据结构
定义子问题解空间Sij是S的子集,其中的每一个得到都是互相兼容的。即每一个活动都是在ai结束以后开始,且在aj开始以前结束。学习
为了方便讨论和后面的计算,添加两个虚构活动a0和an+1,其中f0=0,sn+1=∞。测试
结论:当i≥j时,Sij为空集。优化
若是活动按照结束时间单调递增排序,子问题空间被用来从Sij中选择最大兼容活动子集,其中0≤i<j≤n+1,因此其余的Sij都是空集。
最优子结构为:假设Sij的最优解Aij包含活动ak,则对Sik的解Aik和Skj的解Akj一定是最优的。
经过一个活动ak将问题分红两个子问题,下面的公式能够计算出Sij的解Aij。
(2)一个递归解
设c[i][j]为Sij中最大兼容子集中的活动数目,当Sij为空集时,c[i][j]=0;当Sij非空时,若ak在Sij的最大兼容子集中被使用,则则问题Sik和Skj的最大兼容子集也被使用,故可获得c[i][j] = c[i][k]+c[k][j]+1。
当i≥j时,Sij一定为空集,不然Sij则须要根据上面提供的公式进行计算,若是找到一个ak,则Sij非空(此时知足fi≤sk且fk≤sj),找不到这样的ak,则Sij为空集。
c[i][j]的完整计算公式以下所示:
(3)最优解计算过程
根据递归公式,采用自底向下的策略进行计算c[i][j],引入复杂数组ret[n][n]保存中间划分的k值。程序实现以下所示:
void dynamic_activity_selector(int *s,int *f,int c[N+1][N+1],int ret[N+1][N+1]) { int i,j,k; int temp; //当i>=j时候,子问题的解为空,即c[i][j]=0 for(j=1;j<=N;j++) for(i=j;i<=N;i++) c[i][j] = 0; //当i<j时,须要寻找子问题的最优解,找到一个k使得将问题分红两部分 for(j=2;j<=N;j++) for(i=1;i<j;i++) { //寻找k,将问题分红两个子问题c[i][k]、c[k][j] for(k=i+1;k<j;k++) if(s[k] >= f[i] && f[k] <= s[j]) //判断k活动是否知足兼容性 { temp = c[i][k]+c[k][j]+1; if(c[i][j] < temp) { c[i][j] =temp; ret[i][j] = k; } } } }
(4)构造一个最优解集合
根据第三保存的ret中的k值,递归调用输出得到集合。采用动态规划方法解决上面的例子,完整程序以下所示:
#include <stdio.h> #include <stdlib.h> #define N 11 void dynamic_activity_selector(int *s,int *f,int c[N+1][N+1],int ret[N+1][N+1]); void trace_route(int ret[N+1][N+1],int i,int j); int main() { int s[N+1] = {-1,1,3,0,5,3,5,6,8,8,2,12}; int f[N+1] = {-1,4,5,6,7,8,9,10,11,12,13,14}; int c[N+1][N+1]={0}; int ret[N+1][N+1]={0}; int i,j; dynamic_activity_selector(s,f,c,ret); printf("c[i][j]的值以下所示:\n"); for(i=1;i<=N;i++) { for(j=1;j<=N;j++) printf("%d ",c[i][j]); printf("\n"); } //包括第一个和最后一个元素 printf("最大子集的个数为: %d\n",c[1][N]+2); printf("ret[i][j]的值以下所示:\n"); for(i=1;i<=N;i++) { for(j=1;j<=N;j++) printf("%d ",ret[i][j]); printf("\n"); } printf("最大子集为:{ a1 "); trace_route(ret,1,N); printf("a%d}\n",N); system("pause"); return 0; } void dynamic_activity_selector(int *s,int *f,int c[N+1][N+1],int ret[N+1][N+1]) { int i,j,k; int temp; //当i>=j时候,子问题的解为空,即c[i][j]=0 for(j=1;j<=N;j++) for(i=j;i<=N;i++) c[i][j] = 0; //当i>j时,须要寻找子问题的最优解,找到一个k使得将问题分红两部分 for(j=2;j<=N;j++) for(i=1;i<j;i++) { //寻找k,将问题分红两个子问题c[i][k]、c[k][j] for(k=i+1;k<j;k++) if(s[k] >= f[i] && f[k] <= s[j]) //判断k活动是否知足兼容性 { temp = c[i][k]+c[k][j]+1; if(c[i][j] < temp) { c[i][j] =temp; ret[i][j] = k; } } } } void trace_route(int ret[N+1][N+1],int i,int j) { if(i<j) { trace_route(ret,i,ret[i][j]); if(ret[i][j] != 0 ) printf("a%d ", ret[i][j]); } }
程序测试结果以下所示:
三、贪心算法解决过程
针对活动选择问题,认真分析能够得出如下定理:对于任意非空子问题Sij,设am是Sij中具备最先结束时间的活动,那么:
(1)活动am在Sij中的某最大兼容活动子集中被使用。
(2)子问题Sim为空,因此选择am将使子问题Smj为惟一可能非空的子问题。
有这个定理,就简化了问题,使得最优解中只使用一个子问题,在解决子问题Sij时,在Sij中选择最先结束时间的那个活动。
贪心算法自顶向下地解决每一个问题,解决子问题Sij,先找到Sij中最先结束的活动am,而后将am添加到最优解活动集合中,再来解决子问题Smj。
基于这种思想能够采用递归和迭代进行实现。递归实现过程以下所示:
void recursive_activity_selector(int *s,int* f,int i,int n,int *ret) { int *ptmp = ret; int m = i+1; //在S i n中寻找第一个结束的活动 while(m<=n && s[m] < f[i]) m = m+1; if(m<=n) { *ptmp++ = m; //添加到结果中 recursive_activity_selector(s,f,m,n,ptmp); } }
迭代实现过程以下:
void greedy_activity_selector(int *s,int *f,int *ret) { int i,m; *ret++ = 1; i =1; for(m=2;m<=N;m++) if(s[m] >= f[i]) { *ret++ = m; i=m; } }
采用贪心算法实现上面的例子,完整代码以下所示:
#include <stdio.h> #include <stdlib.h> #define N 11 void recursive_activity_selector(int *s,int* f,int i,int n,int *ret); void greedy_activity_selector(int *s,int *f,int *ret); int main() { int s[N+1] = {-1,1,3,0,5,3,5,6,8,8,2,12}; int f[N+1] = {-1,4,5,6,7,8,9,10,11,12,13,14}; int c[N+1][N+1]={0}; int ret[N]={0}; int i,j; //recursive_activity_selector(s,f,0,N,ret); greedy_activity_selector(s,f,ret); printf("最大子集为:{ "); for(i=0;i<N;i++) { if(ret[i] != 0) printf("a%d ",ret[i]); } printf(" }\n"); system("pause"); return 0; } void recursive_activity_selector(int *s,int* f,int i,int n,int *ret) { int *ptmp = ret; int m = i+1; //在i和n中寻找第一个结束的活动 while(m<=n && s[m] < f[i]) m = m+1; if(m<=n) { *ptmp++ = m; //添加到结果中 recursive_activity_selector(s,f,m,n,ptmp); } } void greedy_activity_selector(int *s,int *f,int *ret) { int i,m; *ret++ = 1; i =1; for(m=2;m<=N;m++) if(s[m] >= f[i]) { *ret++ = m; i=m; } }
程序测试结果以下所示:
四、总结
活动选择问题分别采用动态规划和贪心算法进行分析并实现。动态规划的运行时间为O(n^3),贪心算法的运行时间为O(n)。动态规划解决问题时全局最优解中必定包含某个局部最优解,但不必定包含前一个局部最优解,所以须要记录以前的全部最优解。贪心算法的主要思想就是对问题求解时,老是作出在当前看来是最好的选择,产生一个局部最优解。
第十六章:背包问题:动态规划求解
一、前言
前段时间忙着搞毕业论文,看书效率不高,致使博客一个多月没有更新了。前段时间真是有些堕落啊,混日子的感受,不多不爽。今天开始继续看算法导论。今天继续学习动态规划和贪心算法。首先简单的介绍一下动态规划与贪心算法的各自特色及其区别。而后针对0-1背包问题进行讨论。最后给出一个简单的测试例子,联系动态规划实现0-1背包问题。
二、动态规划与贪心算法
关于动态规划的总结请参考http://www.cnblogs.com/Anker/archive/2013/03/15/2961725.html。这里重点介绍一下贪心算法的过程。贪心算法是经过一系列的选择来给出某一个问题的最优解,每次选择一个当前(看起来是)最佳的选择。贪心算法解决问题的步骤为:
(1)决定问题的最优子结构
(2)设计出一个递归解
(3)证实在递归的任一阶段,最优选择之一老是贪心选择。保证贪心选择老是安全的。
(4)证实经过贪心选择,全部子问题(除一个意外)都为空。
(5)设计出一个实现贪心策略的递归算法。
(6)将递归算法转换成迭代算法。
何时才能使用贪心算法的呢?书中给出了贪心算法的两个性质,只有最优化问题知足这些性质,就可采用贪心算法解决问题。
(1)贪心选择性质:一个全局最优解能够经过举办最优解(贪心)选择来达到。即:当考虑作选择时,只考虑对当前问题最佳的选择而不考虑子问题的结果。而在动态规划中,每一步都要作出选择,这些选择依赖于子问题的解。动态规划通常是自底向上,从小问题到大问题。贪心算法一般是自上而下,一个一个地作贪心选择,不断地将给定的问题实例规约为更小的子问题。
(2)最优子结构:问题的一个最优解包含了其子问题的最优解。
动态规划与贪心的区别:
贪心算法:
(1)贪心算法中,做出的每步贪心决策都没法改变,由于贪心策略是由上一步的最优解推导下一步的最优解,而上一部以前的最优解则不做保留;
(2)由(1)中的介绍,能够知道贪心法正确的条件是:每一步的最优解必定包含上一步的最优解。
动态规划算法:
(1)全局最优解中必定包含某个局部最优解,但不必定包含前一个局部最优解,所以须要记录以前的全部最优解 ;
(2)动态规划的关键是状态转移方程,即如何由以求出的局部最优解来推导全局最优解 ;
(3)边界条件:即最简单的,能够直接得出的局部最优解。
三、0-1背包问题描述
有一个窃贼在偷窃一家商店时发现有n件物品,第i件物品价值为vi元,重量为wi,假设vi和wi都为整数。他但愿带走的东西越值钱越好,但他的背包中之多只能装下W磅的东西,W为一整数。他应该带走哪几样东西?
0-1背包问题中:每件物品或被带走,或被留下,(须要作出0-1选择)。小偷不能只带走某个物品的一部分或带走两次以上同一个物品。
部分背包问题:小偷能够只带走某个物品的一部分,没必要作出0-1选择。
四、0-1背包问题解决方法
0-1背包问题是个典型举办子结构的问题,可是只能采用动态规划来解决,而不能采用贪心算法。由于在0-1背包问题中,在选择是否要把一个物品加到背包中,必须把该物品加进去的子问题的解与不取该物品的子问题的解进行比较。这种方式造成的问题致使了许多重叠子问题,知足动态规划的特征。动态规划解决0-1背包问题步骤以下:
0-1背包问题子结构:选择一个给定物品i,则须要比较选择i的造成的子问题的最优解与不选择i的子问题的最优解。分红两个子问题,进行选择比较,选择最优的。
0-1背包问题递归过程:设有n个物品,背包的重量为w,C[i][w]为最优解。即:
课后习题给出了伪代码:
五、编程实现
如今给定3个物品,背包的容量为50磅。物品1重10磅,价值为60,物品2重20磅,价值为100,物品3重30磅,价值为120。采用动态规划能够知道最优解为220,选择物品2和3。采用C++语言实现以下:
#include <iostream> using namespace std; //物品数据结构 typedef struct commodity { int value; //价值 int weight; //重量 }commodity; const int N = 3; //物品个数 const int W = 50; //背包的容量 //初始物品信息 commodity goods[N+1]={{0,0},{60,10},{100,20},{120,30}}; int select[N+1][W+1]; int max_value(); int main() { int maxvalue = max_value(); cout<<"The max value is: "; cout<<maxvalue<<endl; int remainspace = W; //输出所选择的物品列表: for(int i=N; i>=1; i--) { if (remainspace >= goods[i].weight) { if ((select[i][remainspace]-select[i-1][remainspace-goods[i].weight]==goods[i].value)) { cout << "item " << i << " is selected!" << endl; remainspace = remainspace - goods[i].weight;//若是第i个物品被选择,那么背包剩余容量将减去第i个物品的重量 ; } } } return 0; } int max_value() { //初始没有物品时候,背包的价值为0 for(int w=1;w<=W;++w) select[0][w] = 0; for(int i=1;i<=N;++i) { select[i][0] = 0; //背包容量为0时,最大价值为0 for(int w=1;w<=W;++w) { if(goods[i].weight <= w) //当前物品i的重量小于等于w,进行选择 { if( (goods[i].value + select[i-1][w-goods[i].weight]) > select[i-1][w]) select[i][w] = goods[i].value + select[i-1][w-goods[i].weight]; else select[i][w] = select[i-1][w]; } else //当前物品i的重量大于w,不选择 select[i][w] = select[i-1][w]; } } return select[N][W]; //最终求得最大值 }
程序测试结果以下: