在讲解活动选择问题之前,我们首先来介绍一动态规划
和贪心算法
的基础知识
动态规划是用来求解多阶段决策过程
最优化问题的一种方法。多阶段决策过程本意是指有这样一类活动,他们可以按照时间顺序分解为若干个互相联系的阶段,称为时段
。每一个时段都要做出一个决策,使得整个活动的总体效果最优。
由上述可知,动态规划方法与时间
关系很密切,随着时间过程的发展而决定各阶段的决策,产生一决策序列,这就是动态
的意思。
动态规划座右铭
各阶段有机联系,互相影响,最终影响全局,达到最优。
贪心算法不同于动态规划,贪心算法只考虑本阶段需要作出的最优选择,一般不用从整体最优上进行考虑,所做到的就是局部意义
上的最优解,再由局部最优解得到全局最优解。
因为每一步都只考虑对自己最优的情况,而忽略整体情况,故称之为贪心
。
贪心算法的座右铭
每一步都尽量做到最优,最终结果就算不是最优,那么也是次最优。
贪心算法与动态规划都是用来求解最优化问题的,他们之间有什么相似与相异的性质呢?
动态规划两大性质:
- 最优子结构
- 重叠子问题
贪心算法两大性质:
- 最优子结构
- 贪心选择
从上面的描述中知道,动态规划和贪心算法所能解决的是具有最优子结构性质的问题,这一点很重要,至于他们的差别,我们会在后续文章中继续讨论!
假设我们存在这样一个活动集合 S={a1,a2,a3,a4,...,an} ,其中每一个活动 ai 都有一个开始时间 si 和结束时间 fi保证(0≤si<fi) ,活动 ai 进行时,那么它占用的时间为 [si,fi) .现在这些活动占用一个共同的资源,就是这些活动会在某一时间段里面进行安排,如果两个活动 ai 和 aj 的占用时间 [si,fi),[sj,fj) 不重叠,那么就说明这两个活动是兼容的,也就是说当 si<=fj 或者 sj<=fi ,那么活动 ai,aj 是兼容的。
比如下面的活动集合 S :
我们假定在这个活动集合里面,都是按照 fi 进行升序排序的
即: 0≤f1≤f2≤f3≤...≤fn
在开始分析之前,我们首先定义几种写法
- Sij 表明是在 ai 之后 aj 之前的活动集合
- Aij 表明是在 ai 之后 aj 之前的最大兼容子集的集合
即如下图所示
我们假设在有活动集合 Sij 且其最大兼容子集为 Aij , Aij 之中包含活动 ak ,因为 ak 是在最大兼容子集里面,于是我们得到两个子问题集合 Sik 和 Skj 。令 Aik=Aij∩Sik 和 Akj=Aij∩Skj ,这样 Aik 就包含了 ak 之前的活动的最大兼容子集, Akj 就包含了 ak 之后的最大活动兼容子集。
因此我们有 Aij=Aik∪{ak}∪Akj
Sij 里面的最大活动兼容子集个数为 |Aij|=|Aik|+|Akj|+1
这里我们发现与之前讲过的动态规划有点类似,我们可以得到动态规划的递归式子:
上面的分析中,我们得到了两个对我们求解问题非常有帮助的东西:最优子结构
和递归式
。
下面我们将采用几种方法来设计算法,主要是
对于活动选择问题来说,什么是贪心选择呢?那就是选取一个活动,使得去掉这个活动以后,剩下来的资源最多。那么这里怎么选择才能使得剩下来的资源最多呢?我们这里共享的资源是什么?就是大家共有的哪一个时间段呀,我们首先想到肯定是占用时间最短的呀,即 fi−si 最小的哪一个。还有另外一种就是选择最早结束的活动,即 fi 最小的哪一个,其实这两种贪心选择的策略都是可行的,我们这里选择第二种来进行讲解,第一种我们只给出实现代码。
因为我们给出的集合 S 里面的活动都是按照 fi 进行升序排序的,这里我们就首先选出 ak 作为最先结束的活动,那么我们只需要考虑 ak 之后的集合即可。我们之前只是假设每次都选出子问题的最早结束的活动加入到最优解里面,但是这样做真的是正确的么?下面我们来证明一下:
证明:
令 Ak 是 Sk 的一个最大兼容子集, aj 是 Ak 里面最早结束的活动,于是我们将 aj 从 Ak 里面去掉得到 Ak−1 , Ak−1 也是一个兼容子集。我们假设 ai 为 Sk 里面最早结束的活动,那么有 fi≤fj ,将活动 ai 张贴到 Ak−1 里面去,得到一个新的兼容兼容子集 Ak1 ,我们知道 |Ak|==|Ak1| ,于是 Ak1 也是 Sk 的一个最大兼容子集!
上面我们已经知道了贪心选择是什么,现在我们来看看怎么实现,我们首先选出最早结束的活动 ai ,那么之后最早结束活动一定是不和 ai 相交的,于是从 i 开始,一直找 si<fm 的那个活动,如果找到,就将活动加入到解里面,以此类推的寻找,下面我们采用递归和迭代的两种方式来实现代码:
递归方式
#include
#include
#include
#include
#include
#include
#include
using namespace std;
#define BufSize 20
// 用来存储解决方案
char buf[BufSize];
std::vector<string> solution;
size_t dealGreatActivitySelector(std::vectorint , int> > & activities , int left , int right);
size_t greateActivitySelector(std::vectorint , int> > & activities)
{
if(activities.size() == 0)
return 0;
return dealGreatActivitySelector(activities , 1 , activities.size()-1);
}
size_t dealGreatActivitySelector(std::vectorint , int> > & activities , int left , int right)
{
if(left > right)
return 0;
// 找到第一个边界,使得与activies[left]兼容
int newLeft = left;
while(newLeft <= right && activities[left].second > activities[newLeft].first)
newLeft++;
snprintf(buf , BufSize , "a%d" , left);
solution.push_back(string(buf , buf+BufSize));
memset(buf , BufSize , 0);
return dealGreatActivitySelector(activities , newLeft , right)+1;
}
void printSolution()
{
for (std::vector<string>::iterator i = solution.begin(); i != solution.end(); ++i)
{
cout<<*i<<"\t";
}
cout<int main(int argc, char const *argv[])
{
std::vectorint , int> > activities;
activities.push_back(pair<int , int>(0,0));
activities.push_back(pair<int , int>(1,4));
activities.push_back(pair<int , int>(3,5));
activities.push_back(pair<int , int>(0,6));
activities.push_back(pair<int , int>(5,7));
activities.push_back(pair<int , int>(3,9));
activities.push_back(pair<int , int>(5,9));
activities.push_back(pair<int , int>(6,10));
activities.push_back(pair<int , int>(8,11));
activities.push_back(pair<int , int>(8,12));
activities.push_back(pair<int , int>(2,14));
activities.push_back(pair<int , int>(12,16));
cout<<"The max selectors is : "<return 0;
}
运行结果为:
The max selectors is 4
a1 a4 a8 a11
因为有大部分的代码是重复的,所以下面我们只贴出关键代码
/** many code */
size_t dealGreatActivitySelector(std::vectorint , int> > & activities , int left , int right)
{
if(left > right)
return 0;
int count = 1;
snprintf(buf , BufSize , "a%d" , left);
solution.push_back(string(buf , buf+BufSize));
memset(buf , BufSize , 0);
int lastPos=left;
for (int i = left+1; i <= right; ++i)
{
// 不断的寻找边界
while(i<= right && activities[i].first < activities[lastPos].second)
++i;
if(i > right)
break;
//找到就加入到solution里面
snprintf(buf , BufSize , "a%d" , i);
solution.push_back(string(buf , buf+BufSize));
memset(buf , BufSize , 0);
lastPos = i;
count++;
}
return count;
}
/** many code */
运行结果为:
The max selectors is 4
a1 a4 a8 a11
上面两个算法很好理解的,就是首先找到一个开始活动加入到解集里面,然后再向后继续寻找后面与当前选出的活动兼容的活动加入到集合中,直到遍历一边所有活动!
于是最终选出活动 a1,a4,a8,a11
递归方式进行
因为大多数代码类似,所以只列出关键代码
/** many code 8*/
size_t dealGreatActivitySelector(std::vectorint , int> > & activities , int left , int right)
{
if(left > right)
return 0;
//首先找到消耗最小的那个
int minPos = left;
int min = 100;
for(int i = left ; i < right ; ++i)
{
if((activities[i].second-activities[i].first) < min)
{
min = activities[i].second-activities[i].first;
minPos = i;
}
}
snprintf(buf , BufSize , "a%d" , left);
solution.push_back(string(buf , buf+BufSize));
memset(buf , BufSize , 0);
int leftTemp = minPos;
int rightTemp = minPos;
/** 找到左边界 */
while(leftTemp >= left && activities[leftTemp].second > activities[minPos].first )
leftTemp--;
/** 找到右边界 */
while(rightTemp <= right && activities[rightTemp].first < activities[minPos].second)
rightTemp++;
return dealGreatActivitySelector(activities , left , leftTemp)+\
dealGreatActivitySelector(activities , rightTemp, right)+1;
}
运行结果:
The max selectors is 4
a1 a4 a8 a11
我们之前分析过,如果要设计一个动态规划的算法,那么首先就要经历这几步:
- 首先做出一个选择,在这里我们选择活动 ak
- 假设活动 ak 是最优解的一个选择
- 子问题产生,在选择 ak 以后会产生两个子问题 Sij1 和 Si1j ,这两个子问题的最优解加上 ak 构成原问题的最优解
- 证明:如果 Sik−1 或者 Sk+1j 子问题的解不是最优解,那么将最优解替换进去将会得到一个比原问题最优解更优的解,矛盾!
所以我们这里首先选出 ak ,于是有这样的子结构
#include
#include
#include
using namespace std;
/** 最大活动的数目 */
#define MAX_ACTIVITY_NUM 20
size_t dealGreatActivitySelector(std::vectorint , int> > & activities , int left , int right);
size_t great[MAX_ACTIVITY_NUM][MAX_ACTIVITY_NUM];//用来存储i到j的最大子集数目
size_t solution[MAX_ACTIVITY_NUM][MAX_ACTIVITY_NUM];//用来存储选择
pair<int , int> border[MAX_ACTIVITY_NUM][MAX_ACTIVITY_NUM];//用来存储边界值
/**
* 最大的兼容子集
* @param activities 活动的链表,已经按照结束时间的先后顺序拍好了
* @return 返回最大兼容的数量
*/
size_t greateActivitySelector(std::vectorint , int> > & activities)
{
if(activities.size() == 0)
return 0;
dealGreatActivitySelector(activities , 0 , activities.size()-1);
return great[0][activities.size()-1];
}
/**
* 实际处理最大兼容子集的函数
* @param activities 活动
* @param left 左边界
* @param right 右边界
* @return left到right的最大兼容子集数
*/
size_t dealGreatActivitySelector(std::vectorint , int> > & activities , int left , int right)
{
if(left > right)
return 0;
// 只有一个活动
if(left == right)
{
great[left][right] = 1;
solution[left][right] = left;
return 1;
}
if(great[left][right] != 0)
return great[left][right];// 之前已经算过
//求解过程
int max = 0;
int pos = left;
pair<int , int> borderTemp;
for (int i = left; i <= right ; ++i)
{
//以i为基准,向两边找到不与i活动相交的集合 //
int leftTemp = i;
int rightTemp = i;
/** 找到左边界 */
while(leftTemp >= left && activities[leftTemp].second > activities[i].first )
leftTemp--;
/** 找到右边界 */
while(rightTemp <= right && activities[rightTemp].first < activities[i].second)
rightTemp++;
int temp = dealGreatActivitySelector(activities , left , leftTemp)+\
dealGreatActivitySelector(activities , rightTemp , right)+1;
if(temp > max)
{
max = temp;
pos = i ;
borderTemp = pair<int , int>(leftTemp , rightTemp);
}
}
solution[left][right] = pos;
border[left][right] = borderTemp;
great[left][right] = max;
return max;
}
void printSolution(int left , int right)
{
if(left > right)
return;
if(left == right)
{
cout<<"from "<" to "<" -----> "<return;
}
cout<<"from "<" to "<" -----> "<return;
}
int main(int argc, char const *argv[])
{
std::vectorint , int> > activities;
activities.push_back(pair<int , int>(1,4));
activities.push_back(pair<int , int>(3,5));
activities.push_back(pair<int , int>(0,6));
activities.push_back(pair<int , int>(5,7));
activities.push_back(pair<int , int>(3,9));
activities.push_back(pair<int , int>(5,9));
activities.push_back(pair<int , int>(6,10));
activities.push_back(pair<int , int>(8,11));
activities.push_back(pair<int , int>(8,12));
activities.push_back(pair<int , int>(2,14));
activities.push_back(pair<int , int>(12,16));
cout<<"The max selectors is : "<0 , activities.size()-1);
return 0;
}
运行结果为:
The max selectors is : 4
from 0 to 10 —–> 0
from 3 to 10 —–> 3
from 7 to 10 —–> 7
from 10 to 10 —–> 10
也就是a1,a4,a8,a11
因为大部分代码相同,所以这里只展示部分代码
/** many code */
/**
* 实际处理最大兼容子集的函数
* @param activities 活动
* @param left 左边界
* @param right 右边界
* @return left到right的最大兼容子集数
*/
void dealGreatActivitySelector(std::vectorint , int> > & activities , int left , int right)
{
// 只有一个活动,初始化
for (int i = left; i < right; ++i)
{
great[i][i-1] = 0;
}
for(int k = 0 ; k <= right-left ; ++k)
{
for (int i = left; i <=right ; ++i)
{
int max = 0;
int pos = i;
int leftBorder=i;
int rightBorder=i;
for(int j = i ; j <= i+k ; ++j)
{
// 首先需要计算左右边界
int leftTemp = j;
int rightTemp = j;
/** 找到左边界 */
while(leftTemp >= i && activities[leftTemp].second > activities[j].first )
leftTemp--;
/** 找到右边界 */
while(rightTemp <= i+k && activities[rightTemp].first < activities[j].second)
rightTemp++;
int temp = great[i][leftTemp]+great[rightTemp][i+k]+1;
if(max < temp)
{
max = temp;
pos = j;
leftBorder = leftTemp;
rightBorder = rightTemp;
}
}
solution[i][i+k] = pos;
border[i][i+k] = pair<int , int>(leftBorder , rightBorder);
great[i][i+k] = max;
}
}
}
运行结果为:
The max selectors is : 4
from 0 to 10 —–> 0
from 3 to 10 —–> 3
from 7 to 10 —–> 7
from 10 to 10 —–> 10注意:
上面的有两段代码需要特别注意
/** 找到左边界 */ while(leftTemp >= left && activities[leftTemp].second > activities[i].first ) leftTemp--; /** 找到右边界 */ while(rightTemp <= right && activities[rightTemp].first < activities[i].second) rightTemp++;
这段代码就是为了找到 i1 与 j1 的
通过上面的分析我们可知贪心算法也是要有最优子结构的,而且一旦决定了一种贪心选择,那么速度是远远快于动态规划的,难就难在怎么决定贪心选择,到底怎么选是贪心算法的难点