在讲解活动选择问题之前,我们首先来介绍一动态规划
和贪心算法
的基础知识
动态规划是用来求解多阶段决策过程
最优化问题的一种方法。多阶段决策过程本意是指有这样一类活动,他们可以按照时间顺序分解为若干个互相联系的阶段,称为时段
。每一个时段都要做出一个决策,使得整个活动的总体效果最优。
由上述可知,动态规划方法与时间
关系很密切,随着时间过程的发展而决定各阶段的决策,产生一决策序列,这就是动态
的意思。
动态规划座右铭
各阶段有机联系,互相影响,最终影响全局,达到最优。
贪心算法不同于动态规划,贪心算法只考虑本阶段需要作出的最优选择,一般不用从整体最优上进行考虑,所做到的就是局部意义
上的最优解,再由局部最优解得到全局最优解。
因为每一步都只考虑对自己最优的情况,而忽略整体情况,故称之为贪心
。
贪心算法的座右铭
每一步都尽量做到最优,最终结果就算不是最优,那么也是次最优。
贪心算法与动态规划都是用来求解最优化问题的,他们之间有什么相似与相异的性质呢?
动态规划两大性质:
- 最优子结构
- 重叠子问题
贪心算法两大性质:
- 最优子结构
- 贪心选择
从上面的描述中知道,动态规划和贪心算法所能解决的是具有最优子结构性质的问题,这一点很重要,至于他们的差别,我们会在后续文章中继续讨论!
假设我们存在这样一个活动集合
比如下面的活动集合
我们假定在这个活动集合里面,都是按照
即:
在开始分析之前,我们首先定义几种写法
-
-
即如下图所示
我们假设在有活动集合
因此我们有
这里我们发现与之前讲过的动态规划有点类似,我们可以得到动态规划的递归式子:
上面的分析中,我们得到了两个对我们求解问题非常有帮助的东西:最优子结构
和递归式
。
下面我们将采用几种方法来设计算法,主要是
对于活动选择问题来说,什么是贪心选择呢?那就是选取一个活动,使得去掉这个活动以后,剩下来的资源最多。那么这里怎么选择才能使得剩下来的资源最多呢?我们这里共享的资源是什么?就是大家共有的哪一个时间段呀,我们首先想到肯定是占用时间最短的呀,即
因为我们给出的集合
证明:
令
Ak 是Sk 的一个最大兼容子集,aj 是Ak 里面最早结束的活动,于是我们将aj 从Ak 里面去掉得到Ak?1 ,Ak?1 也是一个兼容子集。我们假设ai 为Sk 里面最早结束的活动,那么有fi≤fj ,将活动ai 张贴到Ak?1 里面去,得到一个新的兼容兼容子集Ak1 ,我们知道|Ak|==|Ak1| ,于是Ak1 也是Sk 的一个最大兼容子集!
上面我们已经知道了贪心选择是什么,现在我们来看看怎么实现,我们首先选出最早结束的活动
递归方式
#include <iostream>
#include <utility>
#include <vector>
#include <string>
#include <cstdio>
#include <cstdlib>
#include <cstring>
using namespace std;
#define BufSize 20
// 用来存储解决方案
char buf[BufSize];
std::vector<string> solution;
size_t dealGreatActivitySelector(std::vector<pair<int , int> > & activities , int left , int right);
size_t greateActivitySelector(std::vector<pair<int , int> > & activities)
{
if(activities.size() == 0)
return 0;
return dealGreatActivitySelector(activities , 1 , activities.size()-1);
}
size_t dealGreatActivitySelector(std::vector<pair<int , 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<<endl;
}
int main(int argc, char const *argv[])
{
std::vector<pair<int , 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 : "<<greateActivitySelector(activities)<<endl;
printSolution();
return 0;
}
运行结果为:
The max selectors is 4
a1 a4 a8 a11
因为有大部分的代码是重复的,所以下面我们只贴出关键代码
/** many code */
size_t dealGreatActivitySelector(std::vector<pair<int , 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
上面两个算法很好理解的,就是首先找到一个开始活动加入到解集里面,然后再向后继续寻找后面与当前选出的活动兼容的活动加入到集合中,直到遍历一边所有活动!
于是最终选出活动
递归方式进行
因为大多数代码类似,所以只列出关键代码
/** many code 8*/
size_t dealGreatActivitySelector(std::vector<pair<int , 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
我们之前分析过,如果要设计一个动态规划的算法,那么首先就要经历这几步:
- 首先做出一个选择,在这里我们选择活动
- 假设活动
- 子问题产生,在选择
- 证明:如果
所以我们这里首先选出
#include <iostream>
#include <utility>
#include <vector>
using namespace std;
/** 最大活动的数目 */
#define MAX_ACTIVITY_NUM 20
size_t dealGreatActivitySelector(std::vector<pair<int , 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::vector<pair<int , 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::vector<pair<int , 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 "<<left<<" to "<<right<<" -----> "<<solution[left][right]<<endl;
return;
}
cout<<"from "<<left<<" to "<<right<<" -----> "<<solution[left][right]<<endl;
printSolution(left , border[left][right].first);
printSolution(border[left][right].second , right);
return;
}
int main(int argc, char const *argv[])
{
std::vector<pair<int , 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 : "<<greateActivitySelector(activities)<<endl;
printSolution(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::vector<pair<int , 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 的
通过上面的分析我们可知贪心算法也是要有最优子结构的,而且一旦决定了一种贪心选择,那么速度是远远快于动态规划的,难就难在怎么决定贪心选择,到底怎么选是贪心算法的难点
原文地址:http://blog.csdn.net/ii1245712564/article/details/45420061