贪心法常用于这些近似算法的设计。贪心法和动态规划算法不一样,动态规划算法在某一步决定优化函数的最大或最小值时,需要考虑它的所有子问题的优化函数值,然后从中选出最优的结果。
贪心法的选择也是多步断,每步判断不考虑子问题计算结果,而是根据当时情况采取某种"只顾眼前"的贪心策略来决定取舍,此种决策的计算工作量比动态规划小得多,但这种"短视的"贪心策略有时候只能导致局部最优,而不是全局最优。
要选择合适的贪心策略并证明该策略的正确性就成了算法设计的关键。
引例–活动选择问题
有n项活动申请使用同一个礼堂。每个活动有一个开始时间和截止时间。如果两个活动不能同时举行,问如何选择这些活动从而使被安排的活动数量达到最多?
问题建模如下:
设 S = { 1 , 2 , . . , n } S=\{1,2,..,n\} S={1,2,..,n}为活动集合, s i s_i si和 f i f_i fi分为活动 i i i的开始时间和截止时间, i = 1 , 2 , . . n i=1,2,..n i=1,2,..n。定义:
活动 i i i与 j j j相容等价于 s i ≥ f j 或 s j ≥ f i , i ≠ j s_i≥f_j或s_j≥f_i,i≠j si≥fj或sj≥fi,i=j
求S的最大的两两相容的活动子集A。
以下策略都是基于人的直觉,以红色评析在后面。
可以有三种策略:
- 策略1:把活动按照开始时间从小到大排序,使得 s 1 ≤ s 2 ≤ . . . ≤ s n s_1≤s_2≤...≤s_n s1≤s2≤...≤sn,然后从前向后挑选,只要与前面的活动相容,就可以把这项活动选入A。(为了更早地开始占用,也许能安排得更早一些)
- 策略2:计算每个活动的占用时间,即 f i − s i f_i-s_i fi−si,然后按占用时间从小到大对活动排序, f 1 − s 1 ≤ f 2 − s 2 ≤ . . . ≤ f n − s n f_1-s_1≤f_2-s_2≤...≤f_n-s_n f1−s1≤f2−s2≤...≤fn−sn。然后从前向后挑选,只要与前面选的活动相容,就把这项活动选入A。(时间占用少的先安排,这样似乎可以腾出更多时间给其他活动)
- 策略3:把活动按照截至时间从小到大排序,使得 f 1 ≤ f 2 ≤ . . . ≤ f n f_1≤f_2≤...≤f_n f1≤f2≤...≤fn,然后从前向后挑选,只要与前面的活动相容,就把这项活动选入A。(早完成的先安排,以便给后面的活动留下时间)
对于一,二都有反例。策略三是正确的。
输入演示
活动的开始时间和截至时间(默认截至时间已经按升序排好)
i | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
---|---|---|---|---|---|---|---|---|---|---|
s i s_i si | 1 | 3 | 2 | 5 | 4 | 5 | 6 | 8 | 8 | 2 |
f i f_i fi | 4 | 5 | 6 | 7 | 9 | 9 | 10 | 11 | 12 | 13 |
代码
/*
* @Description:
* @Autor: dive668
* @Date: 2021-04-30 15:08:03
* @LastEditors: dive668
* @LastEditTime: 2021-04-30 20:49:00
*/
#include <iostream>
#include <sstream>
using namespace std;
string int2str(int i)
{
string temp;
stringstream ss;
ss<<i;
ss >> temp;
return temp;
}
int main()
{
int n;
cout << "input the number of the activities:";
cin>>n;
int *s = new int[n + 1];
int *f = new int[n + 1];
s[0] = 0;
f[0] = 0;
cout << "顺序输出开始时间和截至时间" << endl;
int i, j;
for (i = 1; i <= n;++i)
{
cout << "输入第" << i << "件活动的开始时间";
cin >> s[i];
cout << "输入第" << i << "件活动的截至时间";
cin >> f[i];
}
string A = "1";
j = 1;//记录已经选入的最后一个活动的标号
for (i = 2; i <= n;++i)
{
if(s[i]>f[j])//判断相容性
{
A += int2str(i);
j = i;
}
}
cout << "活动序列为:";
for (i = 0; i < A.size();++i)
{
cout << A[i];
}
return 0;
}
一点点优化
刚学过C++,可以使用结构体存储变量,或者说vector容器。按照
f
i
f_i
fi关键字排序。那么就无论用户输入,自动针对结束时间排好序了。
容器装载代码
#include <iostream>
#include <sstream>
#include <vector>
#include <algorithm>//含有一个传统的排序函数
using namespace std;
string int2str(int i)
{
string temp;
stringstream ss;
ss<<i;
ss >> temp;
return temp;
}
struct act
{
int s;
int f;
};
bool cmp(act a1,act a2)
{
return a1.f < a2.f;
}
int main()
{
int n;
cout << "input the number of the activities:";
cin>>n;
vector<act> a;//定义容器a
vector<act>::iterator p;//定义迭代器p
act t;//临时的结构体变量t
int i;
for (i = 0; i < n;++i)
{
cout << "输入活动的开始时间";
cin >> t.s;
cout << "输入活动的结束时间";
cin >> t.f;
a.push_back(t);//把变量压入容器
}
sort(a.begin(), a.end(), cmp);
for (p = a.begin(); p != a.end(); ++p)
{
cout << "start time:" << p->s << " end time:" << p->f<<endl;
}
演示样例及代码
考虑到如果要使用容器内元素,迭代器就不能仅仅使用一个。(一个迭代器先跑起来,如果后面的迭代器指向的s≥前面的迭代器指向的f,那么入队这个s的index索引。索引因为顺序已经被破坏以及重新排序,索引并没有了价值,放入map容器,最后再迭代输出也可以实现。(这里为了实现方便,依旧只用p-a.begin()获得其索引入队列)
C++中的迭代器(2)——迭代器运算)
而且对于选中的活动,可以使用队列入队。
/*
* @Description:
* @Author: dive668
* @Date: 2021-04-30 15:08:03
* @LastEditors: dive668
* @LastEditTime: 2021-04-30 21:40:00
*/
#include <iostream>
#include <sstream>
#include <vector>
#include <algorithm>//含有一个传统的排序函数
#include <queue>
using namespace std;
string int2str(int i)
{
string temp;
stringstream ss;
ss<<i;
ss >> temp;
return temp;
}
struct act
{
int s;
int f;
};
bool cmp(act a1,act a2)
{
return a1.f < a2.f;
}
int main()
{
int n;
cout << "input the number of the activities:";
cin>>n;
vector<act> a;//定义容器a
vector<act>::iterator p,p1;//定义迭代器p
act t;//临时的结构体变量t
int i;
for (i = 0; i < n;++i)
{
cout << "输入活动的开始时间";
cin >> t.s;
cout << "输入活动的结束时间";
cin >> t.f;
a.push_back(t);//把变量压入容器
}
sort(a.begin(), a.end(), cmp);
for (p = a.begin(); p != a.end(); ++p)//顺序输入活动的开始时间和截至时间
{
cout << "start time:" << p->s << " end time:" << p->f<<endl;
}
p = a.begin();
++p;
p1 = a.begin();
queue<int> q;
q.push(1);//第一个活动入队列
for (; p != a.end();++p)
{
if(p->s>p1->f)
{
p1 = p;
q.push(p - a.begin()+1);
}
}
cout << "the sequence is:";
while(!q.empty())//顺序出队
{
cout<<q.front();
q.pop();
}
return 0;
}
总结:贪心法设计要素
- 一,贪心法适合于组合优化问题,该问题满足优化原则
- 二,求解过程是多步判断过程,最终的判断序列对应于问题的最优解。
- 三,判断依据某种“短视的”贪心选择性质,性质的好坏决定了算法的成败。
- 四,贪心法必须进行正确性证明。
- 五,证明策略不正确,举反例。