您的位置:首页 > 其它

贪心算法之活动选择问题

2015-04-25 14:03 459 查看

贪心算法之活动选择问题

我们在之前的文章里面已经提到过动态规划的方法来求解最优的问题,但是就是因为动态规划太过于强大,像一把瑞士军刀,在一些比较特殊的问题上再使用动态规划的话,就有点用脸盆刷牙的感觉了,而且动态规划运行时间也比较长。对于一些特殊的最优解问题不是很适合,于是我们就有了贪心算法的出现。

贪心算法的座右铭:每一步都尽量做到最优,最终结果就算不是最优,那么也是次最优

从上面的描述中知道,贪心算法不是每一个最优解问题都可以得到最优解的,但是最终的解也是趋近于最优解的!

下面我们就借用活动选择问题来理解一下贪心算法

问题描述

假设我们存在这样一个活动集合S=a1,a2,a3,a4,...,anS={a1,a2,a3,a4,...,an},其中每一个活动aiai都有一个开始时间sisi和结束时间fi保证(0<=si<fi)fi保证(0<=si,活动aiai进行时,那么它占用的时间为[si,fi)[si , fi).现在这些活动占用一个共同的资源,就是这些活动会在某一时间段里面进行安排,如果两个活动aiai和ajaj的占用时间[si,fi),[sj,fj)[si,fi),[sj,fj)不重叠,那么就说明这两个活动是兼容的,也就是说当si<=fjsi<=fj或者sj<=fisj<=fi,那么活动ai,ajai,aj是兼容的。

比如下面的活动集合SS:

我们假定在这个活动集合里面,都是按照fifi进行升序排序的

即:0<=f1<=f2<=f3<=...<=fn0<=f1<=f2<=f3<=...<=fn

isifi11423530645753965976108811981210214111216
\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}


从上面可见,我们观察可得兼容子集有:{a3,a9,a11}\lbrace a3,a9,a11 \rbrace ,但是这个并不是最大兼容子集,因为{a1,a4,a8,a11}\lbrace a1, a4 , a8 , a11\rbrace 也是这个活动的最大兼容子集,于是我们将活动选择问题描述为:给定一个集合S=a1,a2,a3,...anS=a1,a2,a3,...an,在相同的资源下,求出最大兼容活动的个数。

活动选择问题的最优子结构

在开始分析之前,我们首先定义几种写法

- Sij表明是在ai之后aj之前的活动集合Sij表明是在ai之后aj之前的活动集合

- Aij表明是在ai之后aj之前的最大兼容子集的集合Aij表明是在ai之后aj之前的最大兼容子集的集合

我们假设在有活动集合SijSij且其最大兼容子集为AijAij,AijAij之中包含活动akak,因为akak是在最大兼容子集里面,于是我们得到两个子问题集合SikSik和SkjSkj。令Aik=Aij∩SikAik=Aij \cap Sik和Akj=Aij∩SkjAkj = Aij \cap Skj,这样AikAik就包含了akak之前的活动的最大兼容子集,AkjAkj就包含了akak之后的最大活动兼容子集。

因此我们有Aij=Aik∪{ak}∪AkjAij=Aik \cup \lbrace ak \rbrace \cup 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]=
\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)O(n)种选择,于是动态规划的时间代价为O(n2)O(n^2)。我们这里也采用了动态规划的方式进行了求解,并将代码附在本文的最后。大家可以对比两种算法的差别

贪心算法

假设我们无需考察所有的子问题就可将一个集合加入到最优解里面,将会怎样?,这将会使我们省去所有的递归考察过程。实际上,对于活动选择问题,我们只需考察一种选择:贪心选择

对于活动选择问题来说,什么是贪心选择呢?那就是选取一个活动,使得去掉这个活动以后,剩下来的资源最多。那么这里怎么选择才能使得剩下来的资源最多呢?我们这里共享的资源是什么?就是大家共有的哪一个时间段呀,我们首先想到肯定是占用时间最短的呀,即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<fmsi的那个活动,如果找到,就将活动加入到解里面,一次类推的寻找,下面我们采用递归和迭代的两种方式来实现代码:

选择最早结束活动的贪心选择代码实现

递归方式

/*************************************************
* @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;
}


结论

通过上面的分析我们可知贪心算法也是要有最优子结构的,而且一旦决定了一种贪心选择,那么速度是远远快于动态规划的,难就难在怎么决定贪心选择,到底怎么选是贪心算法的难点
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: