贪心
MoonCake2011 · · 算法·理论
贪心策略是一种可以比较快求解的方法,它一般配着排序使用,贪心是以局部最优解顶替全局最优解,通常贪心需要证明。 —— MoonCake2011 2.5 years ago as Libingyue2011
贪心的核心:每一步都做“当前看起来最划算/最优”的选择,并希望这些局部最优选择最终拼出全局最优解。
它常见于竞赛里,优点是:思路清晰、实现简单、复杂度低。难点是:不是所有题都能贪心,关键在于证明。
1. 贪心算法的一般套路
- 定义“每一步怎么选”(选择规则 / 排序规则)
- 证明这个规则不会把最优解弄丢
- 实现(通常:排序 + 扫描 / 优先队列)
很多贪心都长这样:
- 先按某个关键字排序(比如按结束时间、按代价、按右端点……)
- 从左到右扫描,能选就选/必须选就选
- 维护当前状态(当前覆盖到哪、当前选了多少、当前代价最小……)
2. 贪心什么时候“可能正确”
一个直觉判断:
- 你做的“当前最优选择”不会影响后面做出同样风格的最优选择
- 或者:如果最优解第一步不这么选,你能把它“换成”这么选,答案不变或更好
这就引出两种常用证明方式。
3. 两种最常用的证明方法(新手必会)
3.1 反证法 / 交换法
思路:假设有个最优解
3.2 归纳 / 最优子结构
证明第一步贪心选完后,剩下的子问题仍然是同类问题,并且你的选择让子问题“最好做/不更差”。 于是:最优解 = 当前贪心一步 + 子问题最优解。
题目
给
贪心策略
按结束时间
为什么对(交换论证直觉)
如果最优解里第一个选的活动结束得更晚,那么把它换成“结束更早”的活动,不会减少后面能选的数量(反而更可能留出空间)。
实现要点
维护上一个选中活动的结束时间,扫描即可。
题目
给很多区间
贪心策略(非常经典)
- 先按
l 升序排序。 - 从当前覆盖到的位置
pos 出发,在所有l \le pos 的区间里选r 最大的那个,把覆盖尽量向右推。 - 推不动则无解。
为什么对(直觉)
你在某一步必须选一个能接上
题目
给
贪心策略
按右端点
为什么对
把点放得越靠右,越可能覆盖后面的区间;对当前区间来说,放在
题目
部分背包。
背包容量为
贪心策略
按单位价值
为什么对(反证法)
如果你拿了单位价值低的部分,却没拿单位价值高的部分,把它们交换会让价值变大,不可能是最优。
代码 code by MoonCake2011 2.5 years ago as Libingyue2011:
#include<bits/stdc++.h>
using namespace std;
struct coin{
int m,v;
}a[110];
bool operator < (coin x,coin y){
return x.v*y.m<y.v*x.m;
}
bool operator > (coin x,coin y){
return x.v*y.m>y.v*x.m;
}
void quicksort(int l,int r){
int i=l,j=r;
coin flag=a[l+r>>1];
do{
while(a[i]>flag) i++;
while(a[j]<flag) j--;
if(i<=j) swap(a[i++],a[j--]);
}while(i<=j);
if(l<j) quicksort(l,j);
if(i<r) quicksort(i,r);
}
int n;
int t,l;
double ans;
int main() {
cin>>n>>t;
for(int i=1;i<=n;i++)
cin>>a[i].m>>a[i].v;
quicksort(1,n);
for(int i=1;i<=n;i++)
if(l+a[i].m<t)
ans+=a[i].v,l+=a[i].m;
else{
ans+=(t-l)*1.0/a[i].m*a[i].v;//装剩余空间
break;
}
printf("%.2lf",ans);
return 0;
}
哈夫曼编码
题目
荷马史诗。
给字符频率,构造前缀编码,使总编码长度最小。
贪心策略
每次取频率最小的
为什么对(直觉)
频率最低的字符对总长度贡献最小,把它们放在更深处(更长编码)损失也最小;合并就是把“更深一层”的代价累加起来。
问题
但这样会出现一些问题,因为这样做会导致最后一次合并的可能不是
解决方案
加入一些出现次数为
5. 做贪心题的一些方法
- 我这一步的选择规则是什么?
- 如果最优解不这么选,能不能把它换成这么选且不变差?(邻项交换)
- 选完这一步后,剩下的还是同类问题吗?(归纳/最优子结构)
- 有没有反例能打爆我的贪心?(随手构造小数据试试)
当然,你可以猜结论。
例题:P1080。
假设只有两个大臣,为 1 号与 2 号。
手上两个数分别是
假设 把 1 号排 2 号前面最优,1 号排 2 号前面的答案为
2 号排 1 号前面的答案为
列出不等式
同除
显然
那么就是
因为
所以按
再看数据范围,
这种证明方法就是交换法。
duel。
从大往小枚举,最优策略绝对是能攻击就攻击。
直接模拟即可。
为何,如果一个怪兽不被先枚举到的怪兽
sa。
发现每个车的超速的时间是一个区间,于是求出后跑经典的区间覆盖就行了。
S。
首先,一段出现
那我们用
对于每个段单独考虑,从左往右扫,如果遇到一个一个前缀出现了
P。
对于一个子序列
于是有个暴力就是每个询问依次向后扫,直接判断就行了。
从
此时可以将每个数对应的位置存下来,二分查找就行了。
fish。
我的做法:
先对每个可以随意交换的连续段处理出来,并把每个连续段的 0/1 的个数求出来。
从前往后枚举,对于一个两个都不能修改的位置,直接不修改跳过就行了。
对于一个位置,如果只有其中一个不能修改,那我们考虑用哪个可修改的所在的连续段的 0/1 尝试匹配(这就是为什么要求出连续段的 0/1 个数,因为那里的 0/1 可以随便移动)。
记得没配上桶里也要减减。
这一步为何最优:因为每一个 0/1 最多产生一个贡献,迟早要产生贡献还不如先产生贡献了。
接着对于两边都可以随意移动的,随便匹配一个 0/1 就行,其实匹配 0/1 都没关系的,还是上面那个结论。
记得没配上桶里也要减减。
然后就做完了。
This。
直接复制我的题解。
注意:此文中下标从
首先,一看数据范围,再看计算方式。
就知道可能会爆 long long。
上 __int128。
第二,因为会拼接无限个
第三,既然都非负了,所以第三个操作就没用了。
设
证第三个只用证
其实
因为
对于每个
又因为
所以
故成立。
那么现在只考虑前两个操作。
现在我们是对整体进行考虑,那么顺序就没用喽。
从小到大排个序。
因为你删数一定先删较小数。
这样更能让序列加的数更多,序列和更容易非负。
排序后,枚举需要做几次
根据上面的讨论。
做
剩余的用前缀和计算是否非负。
注意:你可以做
计算答案很简单吧。
就做完了。
注意不能将数列删空。
好玩的。
将与
先找出任意一棵生成树。
对于图中的所有的环,我们发现,对于只有一条非树边单独构成的环塞进线性基里异或可以覆盖所有的情况。
因为两个相交的环异或起来就可以成为一个大环。
又因为可以重复走,所以环可以随便走。
找到生成树上
膜拜 meng0740。
发现数据随机,开始骗分。
有一个伪的贪心就是按
因为数据随机,这个断点可以枚举中点附近的
然后每次在断点附近的
Problwm。
一个贪心,我们尽量让性价比最高的物体选的越多越好,但是这是错的。
但是这样可能会出现缝隙填不满背包导致不优,所以可以尝试先选择性价比最高的物体
解决这种背包的解法一般是同于最短路,于是考虑以物品
设
接下来我们可以用转圈来做了。
This is a waterest problem。
先将每个区间的价值转化为前缀和形式。
先考虑
接着我们要求,每次查询都用时比较短。
那我们尝试将所有起点的答案建个结构放入堆里,排序,每次取堆顶,删决策。
假设
于是我们可以在结构体里中套一个堆进行这个区间答案的维护。
每次删点的时候相当于将一个子区间的答案删掉,加入两个新的子区间,用堆很好做主要是因为我们要删除的那个绝对是堆顶。
记得在 st 表里维护位置。
这样算算复杂度,就是
当然还有一个更加优秀的实现方式,就是不用在结构体里维护堆,直接在结构体里维护管辖区间。
每次删决策是直接在大堆里删堆顶,加入两个子区间。
做完了,个人认为评不到紫。
本文章梳理知识点部分的框架由 AI 辅助生成,保证人的贡献远大于 AI。