前言
贪心算法是算法基础里最后一章,也是较为常见的思路,在例题区间题解的过程中很像之前的蒙德里安的梦想中数形结合时候的考虑方式。huffman树又使用了小跟堆的操作,以及排序不等式,绝对值不等式,以及地推公式。之所以称之为贪心算法,就是因为要做到全局最优解,就要做到局部最优解,但是依靠这种局部的算法,并不能做到整体最优解。
区间DP
给定 N 个闭区间 [ai,bi],请你在数轴上选择尽量少的点,使得每个区间内至少包含一个选出的点。
输出选择的点的最小数量。位于区间端点上的点也算作区间内。
还是将每个区间按照右端点从小到大进行排序,然后是枚举覆盖,如果不能的话就增加新的断点。
贪心算法区间的存储结构和运算符重载
struct Range
{
int l, r;
bool operator< (const Range &W)const
{
return r < W.r;
}
}range[N];
执行函数,断点标记
int main()
{
scanf("%d", &n);
for (int i = 0; i < n; i ++ ) scanf("%d%d", &range[i].l, &range[i].r);
sort(range, range + n);
int res = 0, ed = -2e9;
for (int i = 0; i < n; i ++ )
if (ed < range[i].l)
{
res ++ ;
ed = range[i].r;
}
printf("%d\n", res);
return 0;
}
区间分组
给定N个闭区间,将其分为若干个组,使得每组内部的区间两两之间包括端点没有交集,并且使得组数尽可能小。输出最小组数
从前往后枚举每个区间,判断此区间能否将其放到现有的组中
如果一个区间的左端点比最小组的右端点要小,ranges[i].l<=heap.top() , 就开一个新组 heap.push(range[i].r);
如果一个区间的左端点比最小组的右端点要大,则放在该组, heap.pop(), heap.push(range[i].r);
每组去除右端点最小的区间,只保留一个右端点较大的区间,这样heap有多少区间,就有多少组。
区间分组,在组内区间不相交的前提下,分成尽可能少的组。
而不是尽可能多的组,因为一个区间一组,就是尽可能多组的答案。
等效于把尽可能多的区间塞进同一组,要满足range[i].l > heap.top。
heap 存储的是每个组的最右的端点,由于是小根堆heap.top()是对应的最小的最右点。
那如果遇到,塞不进去的情况呢?
就是heap.top >= range[i].l, 当前区间的左端点比最小的右端点还要小,放到任何一组都会有相交部分。
那就需要新开一组,heap.push(range[i].r).
把所有区间按照左端点从小到大排序
从前往后枚举每个区间,判断此区间能否将其放到现有的组中
heap有多少区间,就有多少组
代码使用小跟堆快捷
struct Range
{
int l, r;
bool operator< (const Range &W)const
{
return l < W.l;
}
}range[N];
int main()
{
scanf("%d", &n);
for (int i = 0; i < n; i ++ )
{
int l, r;
scanf("%d%d", &l, &r);
range[i] = {l, r};
}
sort(range, range + n);
priority_queue<int, vector<int>, greater<int>> heap;
for (int i = 0; i < n; i ++ )
{
if (heap.empty() || heap.top() >= range[i].l){
heap.push(range[i].r);
}
else {
heap.pop();
heap.push(range[i].r);
}
}
printf("%d\n", heap.size());
return 0;
}
区间覆盖
使用尽可能少的小区间覆盖大区间
从前往后枚举每个区间,在所有能覆盖start的区间中,选择右端点的最大区间,然后将start更新成右端点的最大值
for (int i = 0; i < n; i ++ )
{
int j = i, r = -2e9;
while (j < n && range[j].l <= st)
{
r = max(r, range[j].r);
j ++ ;
}
if (r < st)
{
res = -1;
break;
}
res ++ ;
if (r >= ed)
{
success = true;
break;
}
st = r;
i = j - 1;
}
if (!success) res = -1;
printf("%d\n", res);
Huffman树木
合并果子
每一次合并,达达可以把两堆果子合并到一起,消耗的体力等于两堆果子的重量之和。可以看出,所有的果子经过 n−1n−1 次合并之后,就只剩下一堆了。达达在合并果子时总共消耗的体力等于每次合并所耗体力之和。假定每个果子重量都为 11,并且已知果子的种类数和每种果子的数目,你的任务是设计出合并的次序方案,使达达耗费的体力最少,并输出这个最小的体力耗费值。
思路是最优个例解,可以手写堆,也可以直接使用小跟堆,时间复杂度是一致的
priority_queue<int, vector<int>, greater<int> > q; //定义一个名为q的小根堆
int res = 0;
while (q.size() > 1) //当还没有只剩一个果子堆的时候,即还没有合并完的时候
{
int a = q.top(); q.pop(); //取第一小值
int b = q.top(); q.pop(); //取第二小值
int c = a + b; //消耗体力值 与 新堆果子数
res += c;
q.push(c); //插入新果子堆
}
printf("%d\n", res);
排序不等式
排队打水
有 n 个人排队到 1 个水龙头处打水,第 i 个人装满水桶所需的时间是 ti,请问如何安排他们的打水顺序才能使所有人的等待时间之和最小?
思路很简单,也许是为了培养意识吧,
sort(q,q+n);
LL res=0;
for(int i=0;i<n-1;++i)
res+=q[i]*(n-1-i);
cout<<res<<endl;
绝对值不等式
货物选址
中位数有非常优秀的性质,比如说在这道题目中,每一个点到中位数的距离,都是满足全局的最有性,而不是局部最优性。具体的来说,我们设在仓库左边的所有点,到仓库的距离之和为pp,右边的距离之和则为qq,那么我们就必须让p+qp+q的值尽量小。当仓库向左移动的话,pp会减少xx,但是qq会增加n−xn−x,所以说当为仓库中位数的时候,p+qp+q最小。还是同样的一句话,画图理解很重要。
sort(a+1,a+1+n);//排序
int sm=a[n/2+1];//中位数
for (i=1;i<=n;i++)
ans=ans+abs(a[i]-sm);//统计和中位数之间的差
cout<<ans;
推公式
耍杂技的牛
农民约翰的 N 头奶牛(编号为 1…N)计划逃跑并加入马戏团,为此它们决定练习表演杂技。
奶牛们不是非常有创意,只提出了一个杂技表演:
叠罗汉,表演时,奶牛们站在彼此的身上,形成一个高高的垂直堆叠。
奶牛们正在试图找到自己在这个堆叠中应该所处的位置顺序。
这 N 头奶牛中的每一头都有着自己的重量 Wi 以及自己的强壮程度 Si。
一头牛支撑不住的可能性取决于它头上所有牛的总重量(不包括它自己)减去它的身体强壮程度的值,现在称该数值为风险值,风险值越大,这只牛撑不住的可能性越高。
您的任务是确定奶牛的排序,使得所有奶牛的风险值中的最大值尽可能的小。
对于任意一个叠罗汉的方案, 如果存在两头相邻的牛, W_i+1 + s_i+1 < w_i + s_i
就可以将这两头牛交换, 可以证明出, 将两头牛交换之后, 最大值不会变大
最后排序结果 就必然可以变成所有牛 从小到大排序的 顺序
#include <algorithm>
#include <iostream>
#define x first
#define y second
using namespace std;
typedef pair<int, int> pii;
const int N = 5e4 + 10;
int n;
pii a[N];
int main()
{
cin >> n;
for (int i = 0; i < n; i++) {
int w, s;
cin >> w >> s;
a[i] = {w + s, w};
}
sort(a, a + n);
int res = -1e9;
for (int i = 0, sum = 0; i < n; i++) {
int w = a[i].y, s = a[i].x - w;
res = max(res, sum - s);
sum += w;
}
cout << res << endl;
return 0;
}
结尾
贪心算法就是一种思想,和DP一样,只不过DP有模板更易套用,贪心需要的是研究到本质的逻辑,代码就会很简单。