贪心算法之活动选择问题
贪心算法之活动选择问题
我们在之前的文章里面已经提到过动态规划的方法来求解最优的问题,但是就是因为动态规划太过于强大,像一把瑞士军刀,在一些比较特殊的问题上再使用动态规划的话,就有点用脸盆刷牙的感觉了,而且动态规划运行时间也比较长。对于一些特殊的最优解问题不是很适合,于是我们就有了贪心算法的出现。
贪心算法的座右铭:每一步都尽量做到最优,最终结果就算不是最优,那么也是次最优
从上面的描述中知道,贪心算法不是每一个最优解问题都可以得到最优解的,但是最终的解也是趋近于最优解的!
下面我们就借用活动选择问题来理解一下贪心算法
问题描述
假设我们存在这样一个活动集合$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$
$$
\begin{array}{c|lcr}
i& \text{1} & \text{2} & \text{3} & \text{4} & \text{5} & \text{6} & \text{7} & \text{8} & \text{9} & \text{10} & \text{11} \
\hline
si & 1 & 3 & 0 & 5 & 3 & 5 & 6 & 8 & 8 & 2 & 12 \
fi & 4 & 5 & 6 & 7 & 9 & 9 & 10 & 11 & 12 & 14 & 16 \
\end{array}
$$
从上面可见,我们观察可得兼容子集有:$\lbrace a3,a9,a11 \rbrace $,但是这个并不是最大兼容子集,因为$\lbrace a1, a4 , a8 , a11\rbrace $也是这个活动的最大兼容子集,于是我们将活动选择问题描述为:给定一个集合$S=a1,a2,a3,...an$,在相同的资源下,求出最大兼容活动的个数。
活动选择问题的最优子结构
在开始分析之前,我们首先定义几种写法
- $Sij表明是在ai之后aj之前的活动集合$
- $Aij表明是在ai之后aj之前的最大兼容子集的集合$
我们假设在有活动集合$Sij$且其最大兼容子集为$Aij$,$Aij$之中包含活动$ak$,因为$ak$是在最大兼容子集里面,于是我们得到两个子问题集合$Sik$和$Skj$。令$Aik=Aij \cap Sik$和$Akj = Aij \cap Skj$,这样$Aik$就包含了$ak$之前的活动的最大兼容子集,$Akj$就包含了$ak$之后的最大活动兼容子集。
因此我们有$Aij=Aik \cup \lbrace ak \rbrace \cup Akj$
$Sij$里面的最大活动兼容子集个数为$|Aij|=|Aik|+|Akj|+1$
这里我们发现与之前讲过的动态规划有点类似,我们可以得到动态规划的递归式子:
$$
c[i,j]=c[i,k]+c[k,j]+1
$$
如果我们不知道$ak$的具体位置,那么我们需要便利$ai$到$aj$的所有位置来找到最大的兼容子集
$$
c[i,j]=
\begin{cases}
0 & i = j-1\
max\lbrace c[i,k]+c[k,j]+1\rbrace (i<=k<=j) & i>=j
\end{cases}
$$
这里我们首先分析一下动态规划的代价,我们这里子问题数量为$O(n)$,每一个子问题有$O(n)$种选择,于是动态规划的时间代价为$O(n^2)$。我们这里也采用了动态规划的方式进行了求解,并将代码附在本文的最后。大家可以对比两种算法的差别
贪心算法
假设我们无需考察所有的子问题就可将一个集合加入到最优解里面,将会怎样?,这将会使我们省去所有的递归考察过程。实际上,对于活动选择问题,我们只需考察一种选择:贪心选择
对于活动选择问题来说,什么是贪心选择呢?那就是选取一个活动,使得去掉这个活动以后,剩下来的资源最多。那么这里怎么选择才能使得剩下来的资源最多呢?我们这里共享的资源是什么?就是大家共有的哪一个时间段呀,我们首先想到肯定是占用时间最短的呀,即$fi-si$最小的哪一个。还有另外一种就是选择最早结束的活动,即$fi$最小的哪一个,其实这两种贪心选择的策略都是可行的,我们这里选择第二种来进行讲解,第一种我们只给出实现代码。
因为我们给出的集合$S$里面的活动都是按照$fi$进行升序排序的,这里我们就首先选出$ak$作为最先结束的活动,那么我们只需要考虑$ak$之后的集合即可。我们之前只是假设每次都选出子问题的最早结束的活动加入到最优解里面,但是这样做真的是正确的么?下面我们来证明一下:
证明:
令$Ak$是$Sk$的一个最大兼容子集,$aj$是$Ak$里面最早结束的活动,于是我们将$aj$从$Ak$里面去掉得到$Ak-1$,$Ak-1$也是一个兼容子集。我们假设$ai$为$Sk$里面最早结束的活动,那么有$fi<=sj$,将活动$ai$张贴到$Ak-1$里面去,得到一个新的兼容兼容子集$Ak1$,我们知道$|Ak|==|Ak1|$,于是$Ak1$也是$Sk$的一个最大兼容子集!
递归贪心算法
上面我们已经知道了贪心选择是什么,现在我们来看看怎么实现,我们首先选出最早结束的活动$ai$,那么之后最早结束活动一定是不和$ai$相交的,于是从$i$开始,一直找$si<fm$的那个活动,如果找到,就将活动加入到解里面,一次类推的寻找,下面我们采用递归和迭代的两种方式来实现代码:
选择最早结束活动的贪心选择代码实现
递归方式
/*************************************************
* @Filename: activitySelector_v5.cc
* @Author: qeesung
* @Email: qeesung@qq.com
* @DateTime: 2015-04-23 14:21:27
* @Version: 1.0
* @Description: 这里采用贪心算法的递归方式来解决最大兼容自问题
**************************************************/
#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;
}
迭代的方式进行
/*************************************************
* @Filename: activitySelector_v6.cc
* @Author: qeesung
* @Email: qeesung@qq.com
* @DateTime: 2015-04-23 14:35:43
* @Version: 1.0
* @Description: 迭代的贪心算法解决最大兼容子集
**************************************************/
#include <iostream>
#include <vector>
#include <utility>
#include <cstring>
#include <cstdlib>
#include <cstdio>
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;
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;
}
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;
}
选择最短时长的活动贪心算法实现
递归方式进行
/*************************************************
* @Filename: activitySelector_v7.cc
* @Author: qeesung
* @Email: qeesung@qq.com
* @DateTime: 2015-04-23 14:21:27
* @Version: 1.0
* @Description: 这里采用贪心算法的递归方式来解决最大兼容自问题
**************************************************/
#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;
//首先找到消耗最小的那个
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;
}
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;
}
采用动态规划方式实现
为了与动态规划有对比,我们这里采用两种动态规划的方式实现,一种自上而下,一种自下而上:
自上而下的实现
/*************************************************
* @Filename: activitySelector_v2.cc
* @Author: qeesung
* @Email: qeesung@qq.com
* @DateTime: 2015-04-23 12:58:05
* @Version: 1.0
* @Description: 带有解决方案的最大兼容子集-动态规划自上而下的算法
**************************************************/
#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;
}
自下而上的实现
/*************************************************
* @Filename: activitySelector_v4.cc
* @Author: qeesung
* @Email: qeesung@qq.com
* @DateTime: 2015-04-23 13:52:00
* @Version: 1.0
* @Description: 最大兼容子集 带有解决方案的自下而上的动态规划算法
**************************************************/
#include <iostream>
#include <utility>
#include <vector>
#include <string>
#include <stdlib.h>
#include <stdio.h>
using namespace std;
/** 最大活动的数目 */
#define MAX_ACTIVITY_NUM 20
void dealGreatActivitySelector(std::vector<pair<int , int> > & activities , int left , int right);
size_t great[MAX_ACTIVITY_NUM+1][MAX_ACTIVITY_NUM+1];//用来存储i到j的最大子集数目
size_t solution[MAX_ACTIVITY_NUM+1][MAX_ACTIVITY_NUM+1];//用来存储选择
pair<int , int> border[MAX_ACTIVITY_NUM+1][MAX_ACTIVITY_NUM+1];//用来存储边界值
/**
* 最大的兼容子集
* @param activities 活动的链表,已经按照结束时间的先后顺序拍好了
* @return 返回最大兼容的数量
*/
size_t greateActivitySelector(std::vector<pair<int , int> > & activities)
{
if(activities.size() == 0)
return 0;
dealGreatActivitySelector(activities , 1 , activities.size()-1);
return great[1][activities.size()-1];
}
/**
* 实际处理最大兼容子集的函数
* @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;
}
}
}
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>(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(1,activities.size()-1);
return 0;
}
结论
通过上面的分析我们可知贪心算法也是要有最优子结构的,而且一旦决定了一种贪心选择,那么速度是远远快于动态规划的,难就难在怎么决定贪心选择,到底怎么选是贪心算法的难点
浙公网安备 33010602011771号