活动选择问题(贪心算法)

概述

设有n个活动的集合E={1,2,…,n},其中每个活动都要求使用同一资源,如演讲会场等,而在同一时间内只有一个活动能使用这一资源。每个活动i都有一个要求使用该资源的起始时间$s_i$和一个结束时间$f_i$,且$s_i$<$f_i$。如果选择了活动i,则它在半开时间区间[$s_i$, $f_i$)内占用资源。若区间[$s_i$, $f_i$)与区间[$s_j$, $f_j$)不相交,则称活动i与活动j是相容的。也就是说,当$s_i$≥$f_j$或$s_j$≥$f_i$时,活动i与活动j相容。活动安排问题就是要在所给的活动集合中选出最大的相容活动子集合。

解题思路

假定活动已按结束时间的单调递增顺序排序(小到大排序):f1 ≤ f2 ≤ f3 ≤....≤ fn-1 ≤ fn
用i代表第i个活动,s[i]代表第i个活动开始时间,f[i]代表第i个活动的结束时间。按照从小到大排序,挑选出结束时间尽量早的活动,并且满足后一个活动的起始时间晚于前一个活动的结束时间,全部找出这些活动就是最大的相容活动子集合。
事实上系统一次检查活动i是否与当前已选择的所有活动相容。若相容活动i加入已选择活动的集合中,否则,不选择活动i,而继续i位置的下一个活动与集合A中活动比较相容性(集合A中的活动也就是上次比较的活动)。若活动i与之相容,则i成为最近加入集合A的活动,并取代活动j的位置(迭代方法)。

有2种解题方法

迭代方法

每次总是选择具有最早完成时间的相容活动加入集合A中(默认第1个加入集合A中)。直观上,按这种方法选择相容活动为未安排活动留下尽可能多的时间(因为添加到集合A中的时间最短,剩余的时间就长)。也就是说,该算法的贪心选择的意义是使剩余的可安排时间段极大化,以便安排尽可能多的相容活动。
迭代方法的计算过程如下图所示。图中每行相应于算法的一次迭代。阴影长条表示的活动是已选入集合A的活动,而空白长条表示的活动是当前正在检查相容性的活动。
若被检查的活动i的开始时间$s_i$小于最近选择的活动j的结束时间$f_i$,则不选择活动i,否则选择活动i加入集合A中。

迭代方法的效率极高。当输入的活动已按结束时间的非减序排列,算法只需O(n)的时间安排n个活动,使最多的活动能相容地使用公共资源。如果所给出的活动未按非减序排列,可以用O(nlogn)的时间重排。

递归方法

每次都是从相同位置开始,SF开始比较是否兼容,如果F(i)<S(M),标记位M放入集合A中,下一次比较从M+1标记位开始。

程序代码

迭代方法

伪代码

1
2
3
4
5
6
7
8
9
GreedyActivitySelector(s,f)
n <- length(s)
A <- {a1}
i <- 1
for m <- 2 to n
do if Sm ≤ Fi
then A <- A U {am}
i <- m
return A

C++版本

定义:s[]代表开始时间,f[]代表结束时间,n表示活动个数。
准备工作:先对f[]进行排序,而且保持s[]f[]一致。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static int[] GreedyAc(int s[], int f[], int n){
int x[] = new int[n];
//默认标记第一个标记位i存入集合A中
x[0] = 1;
int j = 0; //j用来表示f数组的下标值
for(int i = 1; i < n; i++){
//判断下一个活动的开始时间是否比当前活动的结束时间晚
if(s[i] >= f[j]){
x[i] = 1;//标记i位置存入集合A中
j = i;//i位置存入集合A中,那下次比较f的时候,就从i位置开始,i已经在集合活动中,下一个活动时间要比i来的晚
}
}
return x;
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
//4d1 活动安排问题 贪心算法  
#include "stdafx.h"
#include <iostream>
using namespace std;

template<class Type>
void GreedySelector(int n, Type s[], Type f[], bool A[]);

const int N = 11;

int main()
{
//下标从1开始,存储活动开始时间
int s[] = {0,1,3,0,5,3,5,6,8,8,2,12};

//下标从1开始,存储活动结束时间
int f[] = {0,4,5,6,7,8,9,10,11,12,13,14};

bool A[N+1];

cout<<"各活动的开始时间,结束时间分别为:"<<endl;
for(int i=1;i<=N;i++)
{
cout<<"["<<i<<"]:"<<"("<<s[i]<<","<<f[i]<<")"<<endl;
}
GreedySelector(N,s,f,A);
cout<<"最大相容活动子集为:"<<endl;
for(int i=1;i<=N;i++)
{
if(A[i]){
cout<<"["<<i<<"]:"<<"("<<s[i]<<","<<f[i]<<")"<<endl;
}
}

return 0;
}

template<class Type>
void GreedySelector(int n, Type s[], Type f[], bool A[])
{
A[1]=true;
int j=1;//记录第1次加入A中的活动

for (int i=2;i<=n;i++)//依次检查活动i是否与当前已选择的活动相容
{
if (s[i]>=f[j])
{
A[i]=true;
j=i;
}
else
{
A[i]=false;
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#include<stdio.h>
# define N 11

void GreadyActivitySelector(int *s,int *f,int *A,int n);
void RecursiveActivitySelector(int *s,int *f,int *A,int i,int n,int k);

void main()
{
int s[N]={1,3,0,5,3,5,6,8,8,2,12};//开始时间
int f[N]={4,5,6,7,8,9,10,11,12,13,14};//结束时间
int A[N]={0};//初始化
int n=N;
GreadyActivitySelector(s,f,A,n);//迭代版本
// RecursiveActivitySelector(s,f,A,0,n,0);//递归版本
for(int i=0;i<n;i++)
printf("%d ",A[i]);//被选择的活动
}

/****************************************************\
函数功能:选择最佳的活动安排
输入: 各个活动的起始时间和结束时间、待存储被选择活动的数组A、活动个数
输出: 无
\****************************************************/

void GreadyActivitySelector(int *s,int *f,int *A,int n)//迭代版本
{
A[0]=1;
int i=0;
int j=1;
for(int m=1;m<n;m++)
{
if(s[m]>=f[i])//开始时间大于上个活动的结束时间
{
i=m;
A[j]=m+1;//注意下标与第几位差一
j++;
}
}
}

Java版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
package demo2;

/**
* 活动安排问题(贪心算法)
*
* 迭代方法
*
*/
public class ActionOrder {


/**
* 迭代方法
*
* @param s 开始时间数组
* @param f 结束时间数组
* @param a 集合数组
* @return
*/
public int greedySelector(int[] s, int[] f, boolean[] a) {
int n = s.length - 1;
// 第一个活动被选中
a[1] = true;
int j = 1;
// 被选中活动的数量,默认第一个活动被选中
int count = 1;
for (int i = 2; i <= n; i++) {
// 下一个活动开始时间大于大于等于上一个活动结束时间
if (s[i] >= f[j]) {
//当前时间是相互兼容的
a[i] = true;
//当前时间开始的标记 赋值 时间结束的标记位,下次比较,下一个开始时间和当前结束时间进行比较
j = i;
count++;
} else {//i活动不兼容
a[i] = false;
}
}
return count;
}

public static void main(String[] args) {
// 默认下标从1开始(已非减序排好序),初始的-1无用
int s[] = { -1, 1, 3, 0, 5, 3, 5, 6, 8, 8, 2, 12 };
int f[] = { -1, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14 };
boolean[] a = new boolean[s.length];
ActionOrder ac = new ActionOrder();
int counts = ac.greedySelector(s, f, a);
System.out.println("活动集合中最大相容活动数量为:" + counts);
for (int i = 1; i <= s.length - 1; i++) {
if (a[i]) {
System.out.println("第" + i + "活动被选中,其开始时间为:" + s[i] + ",结束时间为:" + f[i]);
}
}
}
}

结果:

1
2
3
4
5
活动集合中最大相容活动数量为:4  
第1活动被选中,其开始时间为:1,结束时间为:4
第4活动被选中,其开始时间为:5,结束时间为:7
第8活动被选中,其开始时间为:8,结束时间为:11
第11活动被选中,其开始时间为:12,结束时间为:14

递归方法

//s 开始时间集合
//f 结束时间集合
//A 符合条件的集合
//i 结束集合的标记位
//n 总数目
//k A集合的标记位

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/****************************************************\


\****************************************************/
void RecursiveActivitySelector(int *s,int *f,int *A,int i,int n,int k)//递归版本
{
int j=k;
int m=i;
//找到结束时间大于上个活动开始时间的活动
while((m<n)&&(s[m]<f[i])&&(m!=0)){
m=m+1;
}
if(m<n)
{
A[j]=m+1;//将被选择的活动存储起来
j++;
RecursiveActivitySelector(s,f,A,m+1,n,j);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static void main(String[] args){
T *test=new T();

int s[N]={1,3,0,5,3,5,6,8,8,2,12};//开始时间
int f[N]={4,5,6,7,8,9,10,11,12,13,14};//结束时间
int A[N]={0};//初始化
int n=N;

test->RecursiveActivitySelector(s,f,A,0,n,0);//递归版本
//被选择的活动
for(int i=0;i<n;i++) {
printf("%d ",A[i]);
}
}

第1次执行时:m没有变化,因为m=0
第2次执行时:m=i=1,进入循环体m+1,i不变,m=2,i=1f[1]s[2]比较,2位置开始的时间大于等于1位置的结束时间,否则m+1,继续下一个位置和f[i]比较,直到m=5时,i=1时,s[m]=f[i],开始的时间=结束的时间,m处于集合A中。
第3次执行时:m=i=5,重复第2次执行的逻辑
递归每次调用时候,sf都是从同一个位置开始执行比较是否兼容。

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×