《算法导论》读书笔记

AlanTu AlanTu     2022-10-20     121

关键词:

《算法导论》读书笔记之第16章 贪心算法—活动选择问题

  前言:贪心算法也是用来解决最优化问题,将一个问题分成子问题,在现在子问题最优解的时,选择当前看起来是最优的解,期望通过所做的局部最优选择来产生一个全局最优解。书中先从活动选择问题来引入贪心算法,分别采用动态规划方法和贪心算法进行分析。本篇笔记给出活动选择问题的详细分析过程,并给出详细的实现代码进行测试验证。关于贪心算法的详细分析过程,下次在讨论。

1、活动选择问题描述

    有一个需要使用每个资源的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}。

2、动态规划解决过程

(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值。程序实现如下所示:

复制代码
 1 void dynamic_activity_selector(int *s,int *f,int c[N+1][N+1],int ret[N+1][N+1])
 2 {
 3     int i,j,k;
 4     int temp;
 5     //当i>=j时候,子问题的解为空,即c[i][j]=0
 6     for(j=1;j<=N;j++)
 7       for(i=j;i<=N;i++)
 8          c[i][j] = 0;
 9     //当i<j时,需要寻找子问题的最优解,找到一个k使得将问题分成两部分
10     for(j=2;j<=N;j++)
11      for(i=1;i<j;i++)
12       {
13          //寻找k,将问题分成两个子问题c[i][k]、c[k][j] 
14          for(k=i+1;k<j;k++)
15             if(s[k] >= f[i] && f[k] <= s[j])   //判断k活动是否满足兼容性 
16              {
17                temp = c[i][k]+c[k][j]+1;
18                if(c[i][j] < temp)
19                 {
20                   c[i][j] =temp;
21                   ret[i][j] = k;
22                 }
23             }
24       }
25 }
复制代码

 (4)构造一个最优解集合

  根据第三保存的ret中的k值,递归调用输出获得集合。采用动态规划方法解决上面的例子,完整程序如下所示:

 

 1 #include <stdio.h>
 2 #include <stdlib.h>
 3 
 4 #define N 11
 5 
 6 void dynamic_activity_selector(int *s,int *f,int c[N+1][N+1],int ret[N+1][N+1]);
 7 void trace_route(int ret[N+1][N+1],int i,int j);
 8 
 9 int main()
10 {
11     int s[N+1] = {-1,1,3,0,5,3,5,6,8,8,2,12};
12     int f[N+1] = {-1,4,5,6,7,8,9,10,11,12,13,14};
13     int c[N+1][N+1]={0};
14     int ret[N+1][N+1]={0};
15     int i,j;
16     dynamic_activity_selector(s,f,c,ret);
17     printf("c[i][j]的值如下所示:\n");
18     for(i=1;i<=N;i++)
19     {
20         for(j=1;j<=N;j++)
21             printf("%d ",c[i][j]);
22         printf("\n");
23     }
24     //包括第一个和最后一个元素 
25     printf("最大子集的个数为: %d\n",c[1][N]+2); 
26     printf("ret[i][j]的值如下所示:\n");
27     for(i=1;i<=N;i++)
28     {
29         for(j=1;j<=N;j++)
30             printf("%d ",ret[i][j]);
31         printf("\n");
32     }
33     printf("最大子集为:{ a1 ");
34     trace_route(ret,1,N);
35     printf("a%d}\n",N);
36     system("pause");
37     return 0;
38 }
39 
40 void dynamic_activity_selector(int *s,int *f,int c[N+1][N+1],int ret[N+1][N+1])
41 {
42     int i,j,k;
43     int temp;
44     //当i>=j时候,子问题的解为空,即c[i][j]=0
45     for(j=1;j<=N;j++)
46       for(i=j;i<=N;i++)
47          c[i][j] = 0;
48     //当i>j时,需要寻找子问题的最优解,找到一个k使得将问题分成两部分
49     for(j=2;j<=N;j++)
50      for(i=1;i<j;i++)
51      {
52          //寻找k,将问题分成两个子问题c[i][k]、c[k][j] 
53          for(k=i+1;k<j;k++)
54             if(s[k] >= f[i] && f[k] <= s[j])   //判断k活动是否满足兼容性 
55             {
56                temp = c[i][k]+c[k][j]+1;
57                if(c[i][j] < temp)
58                {
59                   c[i][j] =temp;
60                   ret[i][j] = k;
61                }
62             }
63      }
64 }
65 
66 void trace_route(int ret[N+1][N+1],int i,int j)
67 {
68      if(i<j)
69      {
70          trace_route(ret,i,ret[i][j]);
71          if(ret[i][j] != 0 )  
72             printf("a%d ", ret[i][j]);
73      }
74 } 

 

程序测试结果如下所示:

3、贪心算法解决过程

针对活动选择问题,认真分析可以得出以下定理:对于任意非空子问题Sij,设am是Sij中具有最早结束时间的活动,那么:

(1)活动am在Sij中的某最大兼容活动子集中被使用。

(2)子问题Sim为空,所以选择am将使子问题Smj为唯一可能非空的子问题。

有这个定理,就简化了问题,使得最优解中只使用一个子问题,在解决子问题Sij时,在Sij中选择最早结束时间的那个活动。

贪心算法自顶向下地解决每个问题,解决子问题Sij,先找到Sij中最早结束的活动am,然后将am添加到最优解活动集合中,再来解决子问题Smj

基于这种思想可以采用递归和迭代进行实现。递归实现过程如下所示:

复制代码
 1 void recursive_activity_selector(int *s,int* f,int i,int n,int *ret)
 2 {
 3      int *ptmp = ret;
 4      int m = i+1;
 5      //在Sin中寻找第一个结束的活动 
 6      while(m<=n && s[m] < f[i])
 7         m = m+1;
 8      if(m<=n)
 9      {
10         *ptmp++ = m;  //添加到结果中 
11         recursive_activity_selector(s,f,m,n,ptmp);
12      }
13 }
复制代码

迭代实现过程如下:

复制代码
 1 void greedy_activity_selector(int *s,int *f,int *ret)
 2 {
 3   int i,m;
 4   *ret++ = 1;
 5   i =1;
 6   for(m=2;m<=N;m++)
 7     if(s[m] >= f[i])
 8     {
 9        *ret++ = m;
10        i=m;
11     }
12 }
复制代码

采用贪心算法实现上面的例子,完整代码如下所示:

 1 #include <stdio.h>
 2 #include <stdlib.h>
 3 
 4 #define N 11
 5 
 6 void recursive_activity_selector(int *s,int* f,int i,int n,int *ret);
 7 
 8 void greedy_activity_selector(int *s,int *f,int *ret); 
 9 
10 int main()
11 {
12     int s[N+1] = {-1,1,3,0,5,3,5,6,8,8,2,12};
13     int f[N+1] = {-1,4,5,6,7,8,9,10,11,12,13,14};
14     int c[N+1][N+1]={0};
15     int ret[N]={0};
16     int i,j;
17     //recursive_activity_selector(s,f,0,N,ret);
18     greedy_activity_selector(s,f,ret);
19     printf("最大子集为:{ ");
20     for(i=0;i<N;i++)
21     {
22        if(ret[i] != 0)
23          printf("a%d ",ret[i]);
24     }
25     printf(" }\n");
26     system("pause");
27     return 0;
28 }
29 
30 void recursive_activity_selector(int *s,int* f,int i,int n,int *ret)
31 {
32      int *ptmp = ret;
33      int m = i+1;
34      //在i和n中寻找第一个结束的活动 
35      while(m<=n && s[m] < f[i])
36         m = m+1;
37      if(m<=n)
38      {
39         *ptmp++ = m;  //添加到结果中 
40         recursive_activity_selector(s,f,m,n,ptmp);
41      }
42 }
43 
44 void greedy_activity_selector(int *s,int *f,int *ret)
45 {
46   int i,m;
47   *ret++ = 1;
48   i =1;
49   for(m=2;m<=N;m++)
50     if(s[m] >= f[i])
51     {
52        *ret++ = m;
53        i=m;
54     }
55 }

程序测试结果如下所示:

 4、总结

  活动选择问题分别采用动态规划和贪心算法进行分析并实现。动态规划的运行时间为O(n^3),贪心算法的运行时间为O(n)。动态规划解决问题时全局最优解中一定包含某个局部最优解,但不一定包含前一个局部最优解,因此需要记录之前的所有最优解。贪心算法的主要思想就是对问题求解时,总是做出在当前看来是最好的选择,产生一个局部最优解。

 

 

 

《算法导论》读书笔记之第16章 0-1背包问题—动态规划求解

1、前言

  前段时间忙着搞毕业论文,看书效率不高,导致博客一个多月没有更新了。前段时间真是有些堕落啊,混日子的感觉,很少不爽。今天开始继续看算法导论。今天继续学习动态规划和贪心算法。首先简单的介绍一下动态规划与贪心算法的各自特点及其区别。然后针对0-1背包问题进行讨论。最后给出一个简单的测试例子,联系动态规划实现0-1背包问题。

2、动态规划与贪心算法

  关于动态规划的总结请参考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)边界条件:即最简单的,可以直接得出的局部最优解。

3、0-1背包问题描述

  有一个窃贼在偷窃一家商店时发现有n件物品,第i件物品价值为vi元,重量为wi,假设vi和wi都为整数。他希望带走的东西越值钱越好,但他的背包中之多只能装下W磅的东西,W为一整数。他应该带走哪几样东西?

0-1背包问题中:每件物品或被带走,或被留下,(需要做出0-1选择)。小偷不能只带走某个物品的一部分或带走两次以上同一个物品。

部分背包问题:小偷可以只带走某个物品的一部分,不必做出0-1选择。

4、0-1背包问题解决方法

  0-1背包问题是个典型举办子结构的问题,但是只能采用动态规划来解决,而不能采用贪心算法。因为在0-1背包问题中,在选择是否要把一个物品加到背包中,必须把该物品加进去的子问题的解与不取该物品的子问题的解进行比较。这种方式形成的问题导致了许多重叠子问题,满足动态规划的特征。动态规划解决0-1背包问题步骤如下:

0-1背包问题子结构:选择一个给定物品i,则需要比较选择i的形成的子问题的最优解与不选择i的子问题的最优解。分成两个子问题,进行选择比较,选择最优的。

0-1背包问题递归过程:设有n个物品,背包的重量为w,C[i][w]为最优解。即:

课后习题给出了伪代码:

5、编程实现

  现在给定3个物品,背包的容量为50磅。物品1重10磅,价值为60,物品2重20磅,价值为100,物品3重30磅,价值为120。采用动态规划可以知道最优解为220,选择物品2和3。采用C++语言实现如下:

复制代码
 1 #include <iostream>
 2 using namespace std;
 3 
 4 //物品数据结构
 5 typedef struct commodity
 6 {
 7     int value;  //价值
 8     int weight; //重量
 9 }commodity;
10 
11 const int N = 3;  //物品个数
12 const int W = 50; //背包的容量
13 
14 //初始物品信息
15 commodity goods[N+1]={{0,0},{60,10},{100,20},{120,30}};
16 int select[N+1][W+1];
17 
18 int max_value();
19 
20 int main()
21 {
22     int maxvalue = max_value();
23     cout<<"The max value is: ";
24     cout<<maxvalue<<endl;
25     int remainspace = W;
26     //输出所选择的物品列表:
27     for(int i=N; i>=1; i--)
28     {
29         if (remainspace >= goods[i].weight)
30         {
31              if ((select[i][remainspace]-select[i-1][remainspace-goods[i].weight]==goods[i].value))
32              {
33                  cout << "item " << i << " is selected!" << endl;
34                  remainspace = remainspace - goods[i].weight;//如果第i个物品被选择,那么背包剩余容量将减去第i个物品的重量 ;
35              }
36         }
37     }
38     return 0;
39 }
40 int max_value()
41 {
42     //初始没有物品时候,背包的价值为0
43     for(int w=1;w<=W;++w)
44         select[0][w] = 0;
45     for(int i=1;i<=N;++i)
46     {
47         select[i][0] = 0;  //背包容量为0时,最大价值为0
48            for(int w=1;w<=W;++w)
49            {
50                if(goods[i].weight <= w)  //当前物品i的重量小于等于w,进行选择
51                {
52                    if( (goods[i].value + select[i-1][w-goods[i].weight]) > select[i-1][w])
53                     select[i][w] = goods[i].value + select[i-1][w-goods[i].weight];
54                    else
55                     select[i][w] = select[i-1][w];
56                }
57                else //当前物品i的重量大于w,不选择
58                  select[i][w] = select[i-1][w];
59            }
60     }
61     return select[N][W];  //最终求得最大值
62 }
复制代码

程序测试结果如下:

《算法导论》读书笔记

  前言:贪心算法也是用来解决最优化问题,将一个问题分成子问题,在现在子问题最优解的时,选择当前看起来是最优的解,期望通过所做的局部最优选择来产生一个全局最优解。书中先从活动选择问题来引入贪心算法,分... 查看详情

《算法导论》读书笔记

  本章介绍了快速排序及其算法分析,快速排序采用的是分治算法思想,对包含n个数的输入数组,最坏情况下运行时间为θ(n^2),但是平均性能相当好,期望的运行时间为θ(nlgn)。另外快速排序能够就地排序(我理解是不需要... 查看详情

《算法导论》读书笔记

摘要  本章介绍了几种基本的数据结构,包括栈、队列、链表以及有根树,讨论了使用指针的简单数据结构来表示动态集合。本章的内容对于学过数据结构的人来说,没有什么难处,简单的总结一下。1、栈和队列  栈和队... 查看详情

《算法导论》读书笔记

摘要:  本章介绍了二叉查找树的概念及操作。主要内容包括二叉查找树的性质,如何在二叉查找树中查找最大值、最小值和给定的值,如何找出某一个元素的前驱和后继,如何在二叉查找树中进行插入和删除操作。在二叉查... 查看详情

算法导论读书笔记-第十三章-红黑树

算法导论第13章红黑树红黑树(red-blacktree)是许多平衡搜索树中的一种,可以保证在最坏情况下基本动态集合操作的时间复杂度为O(lgn).13.1红黑树的性质红黑树(red-blacktree):满足下面性质的二叉搜索树:每个结点是红色的或者黑色的.根... 查看详情

读书笔记--算法导论(序言+第一部分)

什么是基础呢? 就是要把我们大学所学的离散数学,算法与数据结构,操作系统,计算机体系结构,编译原理等课程学好。对计算机的体系,CPU本身,操作系统内核,系统平台,面向对象编程,程序的性能等要有深层次的掌... 查看详情

《算法导论》读书笔记--第12章课后题(转)

第一章 转自http://www.cnblogs.com/batteryhp/p/4654860.html思考题1-1(运行时间的比较)确定时间t内求解的问题的最大规模。上面是网上提供的答案。注意点:1、最左边一列的是关于n的增长情况描述,值得记住的是这些增长的排列顺... 查看详情

算法导论读书笔记-第十四章-数据结构的扩张

算法导论第14章数据结构的扩张一些工程应用需要的只是标准数据结构,但也有许多其他的应用需要对现有数据结构进行少许的创新和改造,但是只在很少情况下需要创造出全新类型的数据结构,更经常的是通过存储额外信息的方法... 查看详情

读书笔记--算法导论(第二部分排序和顺序统计学)

...为卫星数据,即它们通常以key为中心传送。在一个排序的算法中,当交换关键字时,卫星数据也必须交换。如果记录都很大,我们可以交换一组指向各个记录的指针而不是记录本身,以求将数据移动量减少到最小。在一定意义上... 查看详情

读书笔记:博弈论导论-06-混合的策略

读书笔记:博弈论导论-06-混合的策略混合的策略本文是GameTheoryAnIntroduction(byStevenTadelis)的学习笔记。策略,信念和期望收益混合策略玩家i的有限纯策略集合(S_i={s_{i1},s_{i2},cdots,s_{im}})。将(DeltaS_i)定义为(S_i)的单纯形,是在(S_i)上所... 查看详情

读书笔记:博弈论导论-14-不完整信息的静态博弈机制设计

读书笔记:博弈论导论-14-不完整信息的静态博弈机制设计机制设计(MechanismDesign)本文是GameTheoryAnIntroduction(byStevenTadelis)的学习笔记。机制设计的概念机制设计的目标是设计一个可以达到期望收益的博弈。由于这是根据博弈结果来推... 查看详情

读书笔记:博弈论导论-12-不完整信息的静态博弈贝叶斯博弈

读书笔记:博弈论导论-12-不完整信息的静态博弈贝叶斯博弈贝叶斯博弈(BayesianGames)本文是GameTheoryAnIntroduction(byStevenTadelis)的学习笔记。不完整信息的静态博弈(Incompleteinformationstaticgames)不完整信息博弈意味着玩家之间缺乏共识(common... 查看详情

读书笔记:博弈论导论-07-完美信息的动态博弈预备知识

读书笔记:博弈论导论-07-完美信息的动态博弈预备知识完美信息的动态博弈预备知识本文是GameTheoryAnIntroduction(byStevenTadelis)的学习笔记。动态博弈(DynamicGames)静态博弈是每个玩家同时(并且在不知道其他玩家选择的情况下)做出选择... 查看详情

《大数据日知录:架构与算法》读书笔记(多图)

第二次读这本书,这次是精读,画了思维导图。书很好,完整的知识结构和由浅入深的介绍,非常全面以至于知识点都梳理了三天。作为导论式的总览,对大数据领域有了个总体的认识,接下来可以更针对性地加强和实践。总体... 查看详情

读书笔记:博弈论导论-17-不完整信息的动态博弈建立信誉

读书笔记:博弈论导论-17-不完整信息的动态博弈建立信誉建立信誉(BuildingaReputation)本文是GameTheoryAnIntroduction(byStevenTadelis)的学习笔记。为什么我们要建立良好的信誉?为什么我们更愿意和有信誉的人交往?本章从囚徒困境这个问... 查看详情

读书笔记:博弈论导论-16-不完整信息的动态博弈信号传递博弈

读书笔记:博弈论导论-16-不完整信息的动态博弈信号传递博弈信号传递博弈(SignalingGames)本文是GameTheoryAnIntroduction(byStevenTadelis)的学习笔记。信号传递博弈的核心在于玩家2如何判断玩家1的类型。可以想象玩家2是一个面试官,试图... 查看详情

《数据挖掘导论》-读书笔记-分类:基本概念决策树与模型评估[2016-8-21]

第4章  分类:基本概念、决策树与模型评估  分类任务就是确定对象属于哪个预定义的目标类。分类问题是一个普遍存在的问题,有许多不同的应用。例如:根据电子邮件的标题和内容检查出垃圾邮件,根据核磁共振扫描的... 查看详情

mit公开课:算法导论笔记

...链接:http://open.163.com/special/opencourse/algorithms.html第一课:算法分析基础1.介绍插入排序与归并排序,计算并比较最坏运行时间2.算法分析重点与渐近分析方法以下为个人笔记,根据字幕整理 第一课算法分析总结解决问题的方... 查看详情