贪心算法
基本概念
所谓贪心算法是指,在对问题求解时,总是做出在当前看来是最好的选择。也就是说,不从整体最优上加以考虑,它所做出的仅仅是在某种意义上的局部最优解。贪心算法没有固定的算法框架,算法设计的关键是贪心策略的选择。必须注意的是,贪心算法不是对所有问题都能得到整体最优解,选择的贪心策略必须具备无后效性(即某个状态以后的过程不会影响以前的状态,只与当前状态有关。)
基本例题
case 1:会议室安排
**问题描述:**设有n
个会议的集合C={1,2,…,n}
,其中每个会议都要求使用同一个资源(如会议室)而在同一时间内只能有一个会议使用该资源。每个会议i都有要求使用该资源的起始时间bi
和结束时间ei
,且bi < ei
。如果选择了会议i使用会议室,则它在半开区间[bi, ei)
内占用该资源。如果[bi, ei)
与[bj , ej)
不相交,则称会议i
与会议j
是相容的。会场安排问题要求在所给的会议集合中选出最大的相容活动子集,也即尽可能地选择更多的会议来使用资源。
**求解思路:**直观上想,一个会议的结束时间越早,留下给其他会议的时间也就越多,进而可能安排的会议数目也就越多。将各个会议按照结束时间从小到大排序,每次所选择的下一个会议需要满足两个条件:该会议的开始时间大于等于上一个会议的结束时间,且是所满足条件的会议中结束时间最早的那一个。
示例代码
#include <iostream>
#include <algorithm>
using namespace std;
struct Meeting {
int index;
int startTime;
int endTime;
};
bool cmp(Meeting x, Meeting y) {
return x.endTime < y.endTime;
}
void Print(Meeting x) {
cout<<x.index<<" "<<x.startTime<<" "<<x.endTime<<endl;
}
int main() {
int n;
cin>>n;
Meeting meeting[n];
for(int i=0; i<n; i++) {
cin>>meeting[i].index>>meeting[i].startTime>>meeting[i].endTime;
}
sort(meeting,meeting+n,cmp);
cout<<"======会议安排如下========"<<endl;
Print(meeting[0]);
int lastTime = meeting[0].endTime;
for(int i=1; i<n; i++) {
if(meeting[i].startTime >= lastTime) {
Print(meeting[i]);
lastTime = meeting[i].endTime;
}
}
return 0;
}
case 2:分配畜栏
**问题描述:**农场有N
头牛,每头牛会在一个特定的时间区间[A, B]
(包括A
和B
)在畜栏里挤奶,且一个畜栏里同时只能有一头牛在挤奶。现在农场主希望知道最少几个畜栏能满足上述要求,并要求给出每头牛被安排的方案。对于多种可行方案,只输出一种即可。
**输入:**输入的第一行包含一个整数N(1 ≤ N ≤ 50, 000)
,表示有N
牛头;接下来N
行每行包含两个数,分别表示这头牛的挤奶时间[Ai, Bi](1 ≤ A≤ B ≤ 1, 000, 000)
**输出:**输出的第一行包含一个整数,表示最少需要的畜栏数;接下来N
行,第i+1
行描述了第i
头牛所被分配的畜栏编号(从1开始)。
**解题思路:**每头牛都有一个挤奶开始时间和结束时间,当然要优先处理挤奶开始时间早的奶牛。因此首先将所有奶牛按照挤奶开始时间从小到大进行排序,然后按照顺序处理即可,在处理第i
头奶牛的时候,首先先查看已经建成的畜栏中是否有空着的(此处可以采用队列,将畜栏按照结束时间从小到大进行排序,队头的畜栏永远是最先结束的,在判断时,只需要判断第一个即可。畜栏的结束时间也就是该畜栏里奶牛挤奶的结束时间),如果有空着的,直接将该头奶牛放进已建成的畜栏即可,如果没有空着的,那么就需要新建一个畜栏,将奶牛放进去挤奶。
示例代码
#include <iostream>
#include <algorithm>
#include <queue>
#include <vector>
using namespace std;
struct Cow {
int a,b; // 挤奶区间的起终点
int No; // 编号
bool operator<(const Cow & c) const {
return a < c.a;
}
} cows[50100];
int pos[50100]; // pos[i]表示编号为i的奶牛去的畜栏编号
struct Stall {
int end; // 结束时间
int No; // 编号
// 重载<,规定了畜栏被排序的规则:结束时间最早的位于队列的最前方
bool operator<(const Stall & s) const {
return end > s.end;
}
Stall(int e, int n):end(e),No(n) { }
};
int main() {
// 读入数据
int n;
scanf("%d", &n);
for(int i=0; i<n; i++) {
scanf("%d%d", &cows[i].a, &cows[i].b);
cows[i].No = i;
}
sort(cows,cows+n); // 对奶牛按照开始挤奶的时间从小到大排序
int total = 0; // 记录总共分配了多少畜栏
priority_queue<Stall> pq; // 优先队列,用来存放畜栏,按照结束时间从小到大排序
for(int i=0; i<n; i++) {
if(pq.empty()) {
total ++;
pq.push(Stall(cows[i].b, total));
pos[cows[i].No] = total;
} else {
Stall st = pq.top();
if(st.end < cows[i].a) {
pq.pop();
pos[cows[i].No] = st.No;
pq.push(Stall(cows[i].b,st.No));
} else {
total ++;
pq.push(Stall{cows[i].b, total});
pos[cows[i].No] = total;
}
}
}
cout<<"供分配了 "<<total <<" 个畜栏"<<endl;
for(int i=0; i<n; i++) {
cout<<"奶牛编号: "<<cows[i].No+1<<" 畜栏编号: "<<pos[i]<<endl;
}
return 0;
}
case 3:放置雷达
问题描述:x
轴是海岸线,x
轴上方是海洋。海洋中有n
个岛屿,可以看作点。给定每个岛屿的坐标(x,y)
,x,y
都是整数。当一个雷达(可看作点)到岛屿的距离不超过d
(整数),则认为该雷达覆盖了该岛屿。雷达只能放置在x
轴上。问至少需要多少个雷达才可以覆盖全部岛屿。
**求解思路:**原问题比较复杂,首先我们将问题进行一个转化:对于每个岛屿,可以计算出,覆盖它的雷达,必须位于x
轴上的区间[Ps,Pe]
。如下图所示:
如果有雷达位于某个x
轴区间[a,b]
,称该雷达覆盖此区间。问题转化为,至少要在x
轴上放置几个雷达(点),才能覆盖全部区间。
- 重要结论:如果可以找到一个雷达同时覆盖多个区间,那么把这多个区间按照起点坐标从小到大排序,则最后一个区间(起点最靠右)
k
的起点,就能覆盖所有区间。因此,我们就可以只挑区间的起点来放置雷达了。 - 首先将所有区间按照从小到大排序,并编号
0
~n-1
。依次考察每个区间的起点,看要不要在那里放置雷达。开始,所有的区间都没有被覆盖,定义一个变量firstNoConvered
指向编号最小的且未被覆盖的区间编号,因此最初始状态firstNoConvered=0
。接着我们依次遍历起点,在第0
个区间起点可放置的情况下,先不着急放置雷达,再看放置第1
个区间起点是否可以覆盖前面的0
区间,如果可以继续往下看第2
个区间的起点,看放置在该点是否可以覆盖前面的0,1
号区间,如果不可以的话,就将第一个雷达放置在1
号区间的起点,然后firstNoConvered=2
指向第二个区间;如果可以的话继续按照该思路往下看
**输入与输出:**输入有两部分构成。第一行输入m
和r
,m
代表海上岛屿的数量,r
代表雷达的覆盖半径。接下来的m
行分别表示各个雷达的坐标(x,y)
。输出放置最小数量的雷达数目即可。
示例代码
#include <iostream>
#include <cmath>
#include <algorithm>
using namespace std;
int R = 2;
struct Point {
int x;
int y;
};
struct QuJian {
int No;
int left;
int right;
bool operator<(const QuJian & qujian) const {
return left < qujian.left;
}
};
int main() {
// 输入数据
int m,r;
cin>>m>>r;
Point point[m];
for(int i=0; i<m; i++) {
cin>>point[i].x>>point[i].y;
}
// 转化为区间
QuJian qj[m];
for(int i=0; i<m; i++) {
qj[i].left = point[i].x - sqrt(R*R-point[i].y*point[i].y);
qj[i].right = point[i].x + sqrt(R*R-point[i].y*point[i].y);
}
// 区间按照左端点从小到大排序
sort(qj,qj+m);
int total = 0;
for(int i=0; i<m; i++) {
if(i == m-1) {
cout<<"最少放置雷达数: "<<total+1<<endl;
return 0;
}
int minRight = qj[i].right;
for(int j=i+1; j<m; j++) {
if(qj[j].left <= minRight) {
minRight = min(minRight,qj[j].right);
if(j == m-1) {
cout<<"最少放置雷达数: "<<total+1<<endl;
return 0;
}
} else {
total ++; // 放置雷达在编号为j-1的区间起点
i = j;
i--; // break之后i还要+1,所以要先-1,才能将j的值赋给i
break;
}
}
}
return 0;
}