贪心算法的座右铭:每一步都尽量做到最优,最终结果就算不是最优,那么也是次最优
活动选择问题的最优子结构
一、动归方法
比如下面的活动集合SS:
我们假定在这个活动集合里面,都是按照fifi进行升序排序的
即:0<=f1<=f2<=f3<=…<=fn0<=f1<=f2<=f3<=…<=fn
从上面可见,我们观察可得兼容子集有:{a3,a9,a11}{a3,a9,a11},但是这个并不是最大兼容子集,因为{a1,a4,a8,a11}{a1,a4,a8,a11}也是这个活动的最大兼容子集,于是我们将活动选择问题描述为:给定一个集合S=a1,a2,a3,…anS=a1,a2,a3,…an,在相同的资源下,求出最大兼容活动的个数。
在开始分析之前,我们首先定义几种写法
- Sij表明是在ai之后aj之前的活动集合
- Aij表明是在ai之后aj之前的最大兼容子集的集合
- 我们假设在有活动集合SijSij且其最大兼容子集为AijAij,AijAij之中包含活动akak,因为akak是在最大兼容子集里面,于是我们得到两个子问题集合SikSik和SkjSkj。令Aik=Aij∩SikAik=Aij∩Sik和Akj=Aij∩SkjAkj=Aij∩Skj,这样AikAik就包含了akak之前的活动的最大兼容子集,AkjAkj就包含了akak之后的最大活动兼容子集。
因此我们有Aij=Aik∪{ak}∪AkjAij=Aik∪{ak}∪Akj
SijSij里面的最大活动兼容子集个数为|Aij|=|Aik|+|Akj|+1|Aij|=|Aik|+|Akj|+1
这里我们发现与之前讲过的动态规划有点类似,我们可以得到动态规划的递归式子:
c[i,j]=c[i,k]+c[k,j]+1
c[i,j]=c[i,k]+c[k,j]+1
如果我们不知道akak的具体位置,那么我们需要便利aiai到ajaj的所有位置来找到最大的兼容子集
c[i,j]={0max{c[i,k]+c[k,j]+1}(i<=k<=j)i=j−1i>=j
c[i,j]={0i=j−1max{c[i,k]+c[k,j]+1}(i<=k<=j)i>=j
这里我们首先分析一下动态规划的代价,我们这里子问题数量为O(n)O(n),每一个子问题有O(n)O(n)种选择,于是动态规划的时间代价为O(n2)O(n2)。我们这里也采用了动态规划的方式进行了求解。
代码:
#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;
}
二、贪心算法
假设我们无需考察所有的子问题就可将一个集合加入到最优解里面,将会怎样?,这将会使我们省去所有的递归考察过程。实际上,对于活动选择问题,我们只需考察一种选择:贪心选择
对于活动选择问题来说,什么是贪心选择呢?那就是选取一个活动,使得去掉这个活动以后,剩下来的资源最多。那么这里怎么选择才能使得剩下来的资源最多呢?我们这里共享的资源是什么?就是大家共有的哪一个时间段呀,我们首先想到肯定是占用时间最短的呀,即fi−sifi−si最小的哪一个。还有另外一种就是选择最早结束的活动,即fifi最小的哪一个,其实这两种贪心选择的策略都是可行的,我们这里选择第二种来进行讲解,第一种我们只给出实现代码。
因为我们给出的集合SS里面的活动都是按照fifi进行升序排序的,这里我们就首先选出akak作为最先结束的活动,那么我们只需要考虑akak之后的集合即可。我们之前只是假设每次都选出子问题的最早结束的活动加入到最优解里面,但是这样做真的是正确的么?下面我们来证明一下:
证明:
令AkAk是SkSk的一个最大兼容子集,ajaj是AkAk里面最早结束的活动,于是我们将ajaj从AkAk里面去掉得到Ak−1Ak−1,Ak−1Ak−1也是一个兼容子集。我们假设aiai为SkSk里面最早结束的活动,那么有fi<=sjfi<=sj,将活动aiai张贴到Ak−1Ak−1里面去,得到一个新的兼容兼容子集Ak1Ak1,我们知道|Ak|==|Ak1||Ak|==|Ak1|,于是Ak1Ak1也是SkSk的一个最大兼容子集!
递归贪心算法
上面我们已经知道了贪心选择是什么,现在我们来看看怎么实现,我们首先选出最早结束的活动aiai,那么之后最早结束活动一定是不和aiai相交的,于是从ii开始,一直找si
#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;
}
迭代方式进行:
代码:
#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;
}