贪心算法(Greedy Algorithm)是一种在每一步选择中都采取在当前状态下最好或最优(即最有利)的选择,从而希望导致结果是全局最好或最优的算法。贪心算法并不保证得到问题的最优解,但在很多情况下,它能够得到一个足够好的解,甚至是最优解。贪心算法的主要特点是简单高效,易于实现。(思路、证明起来很难,代码较为简单)
题目:905. 区间选点 - AcWing题库
![](https://i-blog.csdnimg.cn/direct/61a594a917ce4700823f1858e77d2358.png)
贪心思路
有如下两种,在此之前先来证明一下
为什么对于本题而言局部最优解即为整体最优解
设ans为合法的选择的点的最小数量,cnt为合法的选择的点。那么有①:ans <= cnt
如果所有的区间都如图一的第一条线与第四条线,即两两无重合,那么至少需要cnt个合法的点,则②:ans >= cnt。综上ans == cnt
①以左端点进行排序:
如图一所示,要想每个区间至少包含一个选出的点,那么至少需要如粉色线的两点。我们只需要保证当前区间的右端点小于相邻最近的区间的左端点,那么选出的点就需要 + 1。只关心局部最优即可
代码:
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
int n;
vector<pair<int, int>> a;
int main()
{
cin >> n;
while(n -- )
{
pair<int, int> b;
cin >> b.first >> b.second;
a.push_back(b);
}
sort(a.begin(), a.end());
int res = 1;
for(int i = 1, j = 0; i < a.size(); i ++ )
{
if(a[j].second < a[i].first) res ++ , j = i;
/*
若上一个区间覆盖了当前区间,那么以当前区间的右端点为基准。如图二所示,即使上一个区间覆盖了当前区间,那么仍然以当前区间的右端点为基准
*/
else if(a[j].second > a[i].second) j = i;
}
cout << res << endl;
return 0;
}
②以右端点进行升序排序: 我们只需要保证当前区间的左端点大于相邻最近的区间的右端点即可
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
typedef pair<int, int> pii;
int n;
vector<pii> a;
//sort排序默认排序first。这里重载运算符,使其对second排序
bool cmp(pii x, pii y)
{
return y.second > x.second;
}
int main()
{
cin >> n;
while(n -- )
{
pii b;
cin >> b.first >> b.second;
a.push_back(b);
}
sort(a.begin(), a.end(), cmp);
int res = 1;
for(int i = 1, j = 0; i < a.size(); i ++ )
{
if(a[j].second < a[i].first) res ++ , j = i;
/*
如上图所示,即使上一区间被相邻区间所包含,那也以上一区间为基准,因为是按照second进行排序的
*/
}
cout << res << endl;
return 0;
}
题目:AcWing 908. 最大不相交区间数量 - AcWing
设可选区间的最大数量为ans,可以选择的区间为cnt。那么即有:①ans >= cnt
在①的基础上我们有ans > cnt,反证:如上图一,ans的最大值为2,即选择第一条线与第四条线,那么cnt < ans = 2,即cnt < 2那么cnt只能等于1。选择整个大区间使得cnt = 1,当cnt=1时,整个大区间内的子区间必然两两相交,这样才稳固1的局面,但是图一中的第一条线与第四条线不相交,cnt >= ans,所以ans = cnt。
代码与上一题一样。
题目:906. 区间分组 - AcWing题库
![](https://i-blog.csdnimg.cn/direct/10bb16822e1b496e9f3c64713e2187ee.png)
贪心思路
简单的贪心图:如图一所示,第一条可以与第四条线分为一组,二、三各成一组。即先只考虑当前状况,对于第一条线,只要后续的区间的左端点小于其右端点,那么就可以放成一组,放完之后,此时这一组的右端点即为第四条线的右端点,那么只需要后续的区间的左端点小于其右端点,就能放到这一组......。对于不能放到该组的二、三组端,我们只需要判断二、三组的左端点是否小于当前组的右端点即可,如果小于,那么肯定与该组有交集,所以需要新开一组
我们用小根堆来存储每一组的最小的右端点
Q:为什么存储每一组最小的右端点。A:只有对最小值进行操作,组别才有可能最大
Q:为什么用小根堆。A:既然对最小值进行操作,那么小根堆较为合适
模拟样例:一开始为空,将(1,6)的右端点放入堆中;接着,(2,8)的左端点小于堆中的右端点,那么将(2, 8)的右端点放入堆中;接着(7, 13)的左端点小于堆中右端点的最小值, 所以将(1, 6)与(7,13)放成一组...
最后分为:(1, 6),(7, 13); (2, 8), (9, 14); (11, 16)三组
Q:为什么要取堆中的最小值呢?A:因为是将尽可能多的区间放到同一个组,即贪婪的寻找小于当前堆中最小值的左端点。 (只有对最小值进行操作,组别才有可能最大)
(1, 6)与(7, 13)放成一组后需要将当前堆中的最小值弹出,因为下一个点可以与(1, 6)没有交集
但是不一定与(7, 13)没有交集,所以需要弹出(贪婪求解的过程中,需要合法)
贪心的思维很难的,我看了几乎所有人的题解都是对着答案写思路......而y总也说贪心问题很难找到具体的思考模式,基本都是经验的累积。所以莫要着急(这一题的思路理解起来很是简单,不过在思路是如何建立却困惑了我一下午......)
代码:
#include<bits/stdc++.h>
using namespace std;
const int N = 100010;
int n;
struct E
{
int l, r;
bool operator < (const E & W) const
{
return l < W.l;
}
}e[N];
int main()
{
cin >> n;
for(int i = 0; i < n; i ++ ) cin >> e[i].l >> e[i].r;
sort(e, e + n);
//小根堆
priority_queue<int, vector<int>, greater<int>> q;
for(int i = 0; i < n; i ++ )
{
//如果堆中最小的右端点都 >= 下一个点的左端点,说明与堆中的所有点都有交集,那么单开一组
// = 是因为有一个点重合也算
if(q.empty() || q.top() >= e[i].l) q.push(e[i].r);
else
{
//否则的话,加入堆。
/*
加入之前需要将最小的右端点弹出,因为该点是与最小的右端点分在同一组,即添加在最小的右端点之后
即使下一个点与最小端点无交集,但是不能与改点无交集
*/
q.pop();
q.push(e[i].r);
}
}
//最后能留在堆里的右端点,即使每一组的最右边的点的右端点
cout << q.size() << endl;
return 0;
}
题目:907. 区间覆盖 - AcWing题库
![](https://i-blog.csdnimg.cn/direct/8bef339c51b042bda58f790a57084164.png)
贪心思路
如右图,黑色线是需要覆盖的线段区间(依然是对左端点升序进行排序)。
思考一开始所选择的空间应满足以下条件:Ⅰ包含左端点;Ⅱ向右延申应越长越好。
为何要满足以上两点?Ⅰ:既然是要覆盖整个黑色线段区间,那么理应包含左端点,使其合法 。Ⅱ:需要使用最少的区间将其覆盖,那么向右延申的越长,越有利于减少区间使用量。
如图:一开始的三个区间,我们来选择:首先判断是否符合Ⅰ,①,②两条线合法、③不合法;接着应允Ⅱ,贪婪的寻找向右延申最长的区间,那么顺序为:③、①、②,但是由于③不合法,所以我们在进行贪心之前应该判断是否合法。综上选择①
第一次贪心结束以后,如右图:我们需要将 所需要覆盖的区间 的左端点的值更新为上一次贪心所寻找的最大的右端点的值。接着继续贪心寻找即可,通过贪心我们可以选择到③、④两条线,一共选择的区间数位3个。
但如果仅仅判断左端点,可能会出现如右图的情况,所以我们还需对最后所贪心出来的区间右端点进行判断,只有包含了right,才算贪心成功
局部贪心最优解解即为整体最优解
代码:
#include<bits/stdc++.h>
using namespace std;
int n;
struct Range
{
int l, r;
bool operator< (const Range &W)const
{
return l < W.l;
}
}e[100010];
int main()
{
int st, ed;
cin >> st >> ed;
cin >> n;
for (int i = 0; i < n; i ++ )
{
cin >> e[i].l >> e[i].r;
}
sort(e, e + n);
bool success = false;
int res = 0;
for(int i = 0; i < n; i ++ )
{
int j = i, r = -2e9;
//判断是否合法
while(j < n && e[j].l <= st)
{
//贪心
r = max(r, e[j].r);
j ++ ;
}
//贪心结束,选择该区间
res ++;
//上一区间包含了下一最优区间,那么不合法
if(st > r)
{
res = -1;
break;
}
if(r >= ed)
{
//判断结尾
success = true;
break;
}
//将左端点的值更新为 向右延申最长区间的右端点的值
st = r;
i = j - 1;
}
if(!success) res = -1;
cout << res << endl;
return 0;
}
也可以不使用双指针:
for(int i = 0; ; )
{
while(i < n && e[i].l <= st)
{
r = max(r, e[i].r);
i ++ ;
}
res ++;
if(st > r)
{
res = -1;
break;
}
if(r >= ed)
{
//判断结尾
success = true;
break;
}
//将左端点的值更新为 向右延申最长区间的右端点的值
st = r;
}