活动选择问题(贪心算法vs动态规划)

  • 活动选择问题贪心算法vs动态规划

    • 基础知识

      • 1-1动态规划
      • 1-2贪心算法
      • 1-3贪心算法vs动态规划
    • 活动选择问题描述
    • 活动选择问题最优子结构
    • 活动选择问题算法设计
      • 4-1贪心算法之选择最早结束活动

        • 4-1-1递归贪心算法
        • 4-1-2迭代的方式进行
      • 4-2贪心算法之选择最短时长活动
      • 4-3动态规划方法实现
        • 4-3-1自上而下的实现
        • 4-3-2自下而上的实现
    • 结论

活动选择问题(贪心算法vs动态规划)


1.基础知识

在讲解活动选择问题之前,我们首先来介绍一动态规划贪心算法的基础知识

1-1.动态规划

动态规划是用来求解多阶段决策过程最优化问题的一种方法。多阶段决策过程本意是指有这样一类活动,他们可以按照时间顺序分解为若干个互相联系的阶段,称为时段。每一个时段都要做出一个决策,使得整个活动的总体效果最优。

由上述可知,动态规划方法与时间关系很密切,随着时间过程的发展而决定各阶段的决策,产生一决策序列,这就是动态的意思。

动态规划座右铭

各阶段有机联系,互相影响,最终影响全局,达到最优。

1-2.贪心算法

贪心算法不同于动态规划,贪心算法只考虑本阶段需要作出的最优选择,一般不用从整体最优上进行考虑,所做到的就是局部意义上的最优解,再由局部最优解得到全局最优解。

因为每一步都只考虑对自己最优的情况,而忽略整体情况,故称之为贪心

贪心算法的座右铭

每一步都尽量做到最优,最终结果就算不是最优,那么也是次最优。

1-3.贪心算法vs动态规划

贪心算法与动态规划都是用来求解最优化问题的,他们之间有什么相似与相异的性质呢?

动态规划两大性质:

  • 最优子结构
  • 重叠子问题

贪心算法两大性质:

  • 最优子结构
  • 贪心选择

从上面的描述中知道,动态规划和贪心算法所能解决的是具有最优子结构性质的问题,这一点很重要,至于他们的差别,我们会在后续文章中继续讨论!


2.活动选择问题描述

假设我们存在这样一个活动集合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

isifi11423530645753965976108811981210214111216

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


3.活动选择问题最优子结构

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

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

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

即如下图所示

a1,a2,...,ai,ai+1,...,aj?1Sij+aj,...,an?1,anS

我们假设在有活动集合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

这里我们发现与之前讲过的动态规划有点类似,我们可以得到动态规划的递归式子:

c[i,j]=c[i,k]+c[k,j]+1

如果我们不知道ak的具体位置,那么我们需要便利ai到aj的所有位置来找到最大的兼容子集

c[i,j]={0max{c[i,k]+c[k,j]+1}(i≤k≤j)i=j?1i>=j

这里我们首先分析一下动态规划的代价,我们这里子问题数量为O(n2),每一个子问题有O(n)种选择,于是动态规划的时间代价为O(n3)。

上面的分析中,我们得到了两个对我们求解问题非常有帮助的东西:最优子结构递归式


4.活动选择问题算法设计

下面我们将采用几种方法来设计算法,主要是

  • 贪心算法之选择最早结束活动
  • 贪心算法之选择最短时长活动
  • 动态规划方法实现

4-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的一个最大兼容子集!

4-1-1.递归贪心算法

上面我们已经知道了贪心选择是什么,现在我们来看看怎么实现,我们首先选出最早结束的活动ai,那么之后最早结束活动一定是不和ai相交的,于是从i开始,一直找si<fm的那个活动,如果找到,就将活动加入到解里面,以此类推的寻找,下面我们采用递归和迭代的两种方式来实现代码:

递归方式

#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

4-1-2.迭代的方式进行

因为有大部分的代码是重复的,所以下面我们只贴出关键代码

/** 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

上面两个算法很好理解的,就是首先找到一个开始活动加入到解集里面,然后再向后继续寻找后面与当前选出的活动兼容的活动加入到集合中,直到遍历一边所有活动!

Created with Rapha?l 2.1.2代码逐步具体表现,a1,...,an结束时间已升序排序a1a1a2a2a3a3a4a4a5a5a6a6a7a7a8a8a9a9a10a10a11a11add a1f1(4)<=s2(3)?no, skip a2f1(4) <= s3(0)?no , skip a3f1(4) <= s4(5)?yes , add a4add a4f4(7) <= s5(3)?no , skip a5f4(7) <= s6(5)?no, skip a6f4(7) <= s7(6)?no, skip a7f4(7) <= s8(8)?yes, add a8add a8f8(11)<=a9(8)?no , skip a9f8(11) <= a10(2)?no , skip a10f8(11) <= a11(12)?yes , add a11add a11

于是最终选出活动a1,a4,a8,a11

4-2.贪心算法之选择最短时长活动

递归方式进行

因为大多数代码类似,所以只列出关键代码

/** 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

4-3.动态规划方法实现

我们之前分析过,如果要设计一个动态规划的算法,那么首先就要经历这几步:

- 首先做出一个选择,在这里我们选择活动ak

- 假设活动ak是最优解的一个选择

- 子问题产生,在选择ak以后会产生两个子问题Sij1和Si1j,这两个子问题的最优解加上ak构成原问题的最优解

- 证明:如果Sik?1或者Sk+1j子问题的解不是最优解,那么将最优解替换进去将会得到一个比原问题最优解更优的解,矛盾!

所以我们这里首先选出ak,于是有这样的子结构

A[i,j]=A[i,j1]+A[i1,j]+{ak}j1是小于k,且与ak兼容的活动标号i1是大于k,且与ak兼容的活动标号

由于我们的ak不确定,所以我们需要选出能产生最优解的那个ak值于是我们得到递归表达式:

A[i,j]={1max{A[i,j1]+A[i1,j]+1},i=j,i>j

有了递归表达式,我们接下来就可以用代码实现啦。为了与动态规划有对比,我们这里采用两种动态规划的方式实现,一种自上而下,一种自下而上:

4-3-1.自上而下的实现

#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

4-3-2.自下而上的实现

因为大部分代码相同,所以这里只展示部分代码

/** 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的

5.结论

通过上面的分析我们可知贪心算法也是要有最优子结构的,而且一旦决定了一种贪心选择,那么速度是远远快于动态规划的,难就难在怎么决定贪心选择,到底怎么选是贪心算法的难点

时间: 2024-10-04 16:33:12

活动选择问题(贪心算法vs动态规划)的相关文章

活动选择的贪心算法与动态规划(未完成)

// greedy_algorithm.cpp : 定义控制台应用程序的入口点. // #include "stdafx.h" #include<iostream> #include<queue> using namespace std; #define NofActivity 11 int c[NofActivity + 1][NofActivity + 1]; int reme[NofActivity + 1][NofActivity + 1]; //活动的

算法导论--贪心算法与动态规划(活动选择问题)

活动选择问题 有一个教室,而当天有多个活动,活动时间表如下:找出最大兼容活动集!活动已按结束时间升序排序. 动态规划 采用动态规划需要满足两个条件:1.最优子结构2.子问题重叠 令Sij表示在ai结束后和aj开始前活动的集合,假定Aij为活动集合Sij的最大兼容子集,其中包含活动ak.问题变成求Sik与Skj最大兼容活动子集Aik与Akjz.我们用c[i,j]表示Sij的最优解的大小. 则c[i,j] = c[i,k]+c[k,j]+1;最后我们需要遍历所有可能的k值,找出最大的一个划分作为c[

活动选择问题--贪心

活动选择 Time Limit: 1000ms   Memory limit: 65536K  有疑问?点这里^_^ 题目描述 学校的大学生艺术中心周日将面向全校各个学院的学生社团开放,但活动中心同时只能供一个社团活动使用,并且每一个社团活动开始后都不能中断.现在各个社团都提交了他们使用该中心的活动计划(即活动的开始时刻和截止时刻).请设计一个算法来找到一个最佳的分配序列,以能够在大学生艺术中心安排不冲突的尽可能多的社团活动. 比如有5个活动,开始与截止时刻分别为: 最佳安排序列为:1,4,5.

活动安排问题(贪心算法)

问题描述: 有n个活动的活动集合E ,其中每一个活动都要求使用同一个资源,而在同一个时刻内资源只能被一个活动使用,每一个活动都有开始是时间和结束时间,要求从活动集合E中选出m个活动,使着m个活动都能顺利进行,即也就是每个活动的活动时间都互相不交叉,求m的最大值和 被选中的活动序号. 例如输入: 活动编号   活动开始时间    活动结束时间 1                1                       4 2                3                 

贪心算法和动态规划算法

动态规划和贪心算法都是一种递推算法 即均由局部最优解来推导全局最优解 (不从整体最优解出发来考虑,总是做出在当前看来最好的选择.) 不同点: 贪心算法 与动态规划的区别:贪心算法中,作出的每步贪心决策都无法改变,由上一步的最优解推导下一步的最优解,所以上一部之前的最优解则不作保留. 能使用贪心法求解的条件:是否能找出一个贪心标准.我们看一个找币的例子,如果一个货币系统有三种币值,面值分别为一角.五分和一分,求最小找币数时,可以用贪心法求解:如果将这三种币值改为一角一分.五分和一分,就不能使用贪心

关于贪心算法和动态规划的学习 - 背包问题为例

说到背包问题,我看到了很多分类,不同的类似问题,有不一样的解法,看到的最多的两种方式是贪心算法和动态规划,于我来说,更迫切的应该是要认识一下,这些算法的区别和相同的地方,所以这节就来找资料学习一下贪心算法和动态规划的区别. 这是找到的一个对我帮助最大的,源地址:https://www.cnblogs.com/Renyi-Fan/p/7741089.html 看之前先摘一下这几个解决方案的区别:摘自网上 "所以一个问题,该用递推.贪心.搜索还是动态规划,完全是由这个问题本身阶段间状态的转移方式决定

数据结构与算法学习之路:背包问题的贪心算法和动态规划算法

一.背包问题描述: 有N种物品和一个重量为M的背包,第i种物品的重量是w[i],价值是p[i].求解将哪些物品装入背包可使这些物品的费用总和不超过背包重量,且价值总和最大. 二.解决方法: 1.贪心算法:贪心算法基于的思想是每一次选择都作当前最好的选择,这样最后的结果虽然不一定是最优解,但是也不会比最优解差很多. 举个例子说明可能好懂一些:一帮基友去聚餐,菜是一份一份上的,我每一次夹菜都只夹牛肉/海鲜吃,可能到最后我吃的牛肉/海鲜很多,但不一定代表我吃掉的东西的总价值最高,但是相对来说价值也很高

贪心算法和动态规划的区别与联系

联系 1.都是一种推导算法 2.都是分解成子问题来求解,都需要具有最优子结构 区别 1.贪心:每一步的最优解一定包含上一步的最优解,上一步之前的最优解则不作保留: 动态规划:全局最优解中一定包含某个局部最优解,但不一定包含前一个局部最优解,因此需要记录之前的所有的局部最优解 2.贪心:如果把所有的子问题看成一棵树的话,贪心从根出发,每次向下遍历最优子树即可(通常这个"最优"都是基于当前情况下显而易见的"最优"):这样的话,就不需要知道一个节点的所有子树情况,于是构不

从 活动选择问题 看动态规划和贪心算法的区别与联系

这篇文章主要用来记录我对<算法导论> 贪心算法一章中的“活动选择问题”的动态规划求解和贪心算法求解 的思路和理解. 主要涉及到以下几个方面的内容: ①什么是活动选择问题---粗略提下,详细请参考<算法导论> ②活动选择问题的DP(Dynamic programming)求解--DP求解问题的思路 ③活动选择问题的贪心算法求解 ④为什么这个问题可以用贪心算法求解? ⑤动态规划与贪心算法的一些区别与联系 ⑥活动选择问题的DP求解的JAVA语言实现以及时间复杂度分析 ⑦活动选择问题的Gr