【算法设计与分析】活动安排问题(贪心经典问题)

说明:本博客主要代码及方法介绍来源于:【算法设计与分析(第5版)】【王晓东】

一、题目背景介绍

设有n个活动的集合E={1,2,…,n},其中每个活动都要求使用同一资源,如演讲会场等,而在同一时间内只有一个活动能使用这一资源。

每个活动i都有一个要求使用该资源的起始时间si和一个结束时间fi,且si<fi 。

如果选择了活动i,则它在半开时间区间[si, fi)内占用资源。

活动i与活动j是相容的

若区间[si, fi)与区间[sj, fj)不相交,则称活动i与活动j是相容的。

也就是说,当si≥fjsj≥fi时,活动i与活动j相容。

 

二、书上代码示例

下面给出解活动安排问题的贪心算法GreedySelector :

   s[i]:i个活动的开始时间,

   f[i]:i个活动的结束时间 并且 f1<=f2<=f3<=...<=fn

// 各活动的起始时间和结束时间存储于数组s和f中且按结束时间的非减序排列。
// 如果所给岀的活动未按此序排列,可以用O(nlogn)的时间重排。
// 算法 GreedySelector 用集合 A 来存储所选择的活动。 
// 活动 f 在集合 A 中,当且仅当A[i]的值为 true。变量j用以记录最近一次加入到A中的活动。 
template <class Type>
void GreedySelector(int n, Type s[], Type f[], bool A[])
{
    A[1]=true;
    int j=1;
    for (int i=2; i<=n; i++) {
        if (s[i]>=f[j]) {
            A[i]=true;
            j=i;
        } else A[i]=false;
    }
}

三、结合书上代码完整实现版

1、默认输入数据有序,数组存储开始结束时间

/*顺序数据输入: 
11 
1 3 0 5 3 5 6 8 8 2 12
4 5 6 7 8 9 10 11 12 13 14*/ 
#include <bits/stdc++.h>
using namespace std;
void GreedySelector(int n,int *s,int *f,bool *A)
{
	A[1]=true;
	int j=1;
	for(int i=2; i<=n; i++) {
		if(s[i]>=f[j]) {
			A[i]=true;
			j=i;
		} 
		else A[i]=false;
	}
}
int main()
{
	int s[100],f[100],n;
	bool a[100];
	cin>>n;		//输入活动总数n
	//输出各个活动开始结束时间,这里默认已经按结束时间递增排序 
	for(int i=1; i<=n; i++)
		cin>>s[i];
	for(int i=1; i<=n; i++)
		cin>>f[i];
	GreedySelector(n,s,f,a);
	for(int i=1;i<=n;i++)//输出最优解安排的活动号 
		if(a[i])
		cout<<i<<" ";
	return 0;
}
//1 4 8 11

2.考虑输入数据无序情况,开始结束时间用结构体存储

/*乱序数据输入: 
11
8 0 12 1 3 3 5 5 6 8 2
12 6 14 4 8 5 7 9 10 11 13*/
#include <bits/stdc++.h>
using namespace std;
struct node{
	int s,f;
	operator< (const node& b) const//结构体 <号重载 
	{
		return f<b.f;
	}
};
void GreedySelector(int n,node *act,bool *A)
{
	A[1]=true;
	int j=1;
	for(int i=2; i<=n; i++) {
		if(act[i].s>=act[j].f) {
			A[i]=true;
			j=i;
		} 
		else A[i]=false;
	}
}
int main()
{
	int n;
	bool a[100];
	node act[100];
	cin>>n;		//输入活动总数n
	for(int i=1; i<=n; i++)
		cin>>act[i].s;
	for(int i=1; i<=n; i++)
		cin>>act[i].f;
	sort(act+1,act+n+1);//这里存储是从下标1开始的,切记记得+1 
	for(int i=1;i<=n;i++)
		cout<<act[i].f<<" ";
	
	GreedySelector(n,act,a);
	for(int i=1;i<=n;i++)//输出最优解安排的活动号 
		if(a[i])
		cout<<i<<" ";
	return 0;
}
//1 4 8 11 

四、算法思想分析

由于输入的活动以其完成时间的非减序排列,所以算法GreedySelector每次总是选择具有最早完成时间的相容活动加入集合A中。

直观上,按这种方法选择相容活动为未安排活动留下尽可能多的时间。也就是说,该算法的贪心选择的意义是使剩余的可安排时间段极大化,以便安排尽可能多的相容活动。

算法greedySelector的效率极高。当输入的活动已按结束时间的非减序排列,算法只需O(n)的时间安排n个活动,使最多的活动能相容地使用公共资源。

如果所给出的活动未按非减序排列,可以用O(nlogn)的时间重排。

 

五、举例分析

设待安排的11个活动的开始时间和结束时间按结束时间的非减序排列如下:

i

1

2

3

4

5

6

7

8

9

10

11

s[i]

1

3

0

5

3

5

6

8

8

2

12

f[i]

4

5

6

7

8

9

10

11

12

13

14

 

算法greedySelector 的计算过程如图所示。

图中每行相应于算法的一次迭代;

阴影长条表示的活动是已选入集合A的活动;

空白长条表示的活动是当前正在检查相容性的活动

六、贪心法所得解为最优解证明

若被检查的活动i的开始时间si小于最近选择的活动j的结束时间fj,则不选择活动i,否则选择活动i加入集合A中。

贪心算法并不总能求得问题的整体最优解

但对于活动安排问题,贪心算法greedySelector却总能求得的整体最优解,即它最终所确定的相容活动集合A的规模最大。这个结论可以用数学归纳法证明。

证明:用数学归纳法证明

E={12n}为所给的活动集合。由于E中活动按结束时间的非减序排列,故活动1具有最早完成时间。

先证:活动安排问题有一个最优解以贪心选择开始,即该最优解中包含活动1。设AÍE是所给的活动安排问题的一个最优解,且A中活动也按结束时间非减序排列,A中的第一个活动是k。若k1,则A就是一个以贪心选择开始的最优解。若k >1,则设B =(A{k}){1} 。由于f1fk A中活动是互为相容的,故B中的活动也互为相容.又由于B中活动个数与A中活动个数相同,且A是最优的,故B也是最优的。即B是一个以贪心选择活动1开始的最优活动安排。因此证明了总存在一个以贪心开始的最优活动安排方案。

再证:选择活动1后,原问题就简化为对E中所有与活动1相容的活动进行安排的子问题,即若A是原问题的一个最优解,则A'A-{1}是活动安排E'={iE: sif1}问题的一个最优解.

(反证)若不然,若我们能找到E'的一个解B' ,它包含比A'更多的活动,则将活动1加入到B'中将产生的一个解B,它包含比A更多的活动。这与A是最优性矛盾。因此,每一步所作的贪心选择都将问题简化为一个更小的与原问题具有相同形式的子问题。对贪心选择次数用数学归纳法,即知,贪心算法GreedySelector最终产生原问题的一个最优解。

 

 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值