1.什么是贪心
贪心算法就是从问题的初始状态出发,经过若干次的贪心选择而得到的最优值(或较优值)的一种求解问题的策略,即贪心策略
换句话说,贪心策略是在每次选择时采取目前看来是最优的解(也就是局部最优解),通过不断地重复从而得到整个问题的最优解。但是要注意的是:局部最优解并不一定是全局最优解,只有当局部最优解能推出全局最优解的时候,贪心算法才适用
例题:洛谷P1216数字三角形
本蒟蒻(刚学OI时)博客:传送门
这道题如果正着推,虽然你每一步都贪心选择当前情况下的最大值,但是并不能保证这是整个问题的最大值,而如果我们选择使用动态规划,就可以在最后一行选择最大值输出
但是如果倒着推,从倒数第二行开始,对于每一个点,从它下面的两个选择一个最大的贪心加到这个点上,最后就可以在第一行第一个得到最大值
2.贪心算法的特点
1、贪心选择
所谓贪心选择是指应用同一个规则,江源文体变为一个相似的但规模更小的子问题,而后的每一步都是当前看似最佳的选择,而且这种选择只依赖于已经做出的选择,不依赖于未作出的选择
2.最优子结构
执行算法时,每一次得到的结果虽然都是当前问题的最优解,但只有满足全局最优解包含局部最优解是,才能保证最终得到的结果是最优解
3.几个贪心的实例
1.选择不相交区间问题(区别于线段覆盖问题)
给定n个开区间(a,b),选择尽量多的区间,使得这些区间两两没有公共部分
思路:按照结束点从小到大排序,遍历每一个区间,如果没有和已经选择的活动冲突就选,否则就不选
例题:活动安排
代码:
#include<cstdio> #include<iostream> #include<cstdlib> #include<iomanip> #include<cmath> #include<cstring> #include<string> #include<algorithm> #include<time.h> #include<queue> using namespace std; typedef long long ll; typedef long double ld; typedef pair<int,int> pr; const double pi=acos(-1); #define rep(i,a,n) for(int i=a;i<=n;i++) #define per(i,n,a) for(int i=n;i>=a;i--) #define Rep(i,u) for(int i=head[u];i;i=Next[i]) #define clr(a) memset(a,0,sizeof a) #define pb push_back #define mp make_pair #define fi first #define sc second ld eps=1e-9; ll pp=1000000007; ll mo(ll a,ll pp){if(a>=0 && a<pp)return a;a%=pp;if(a<0)a+=pp;return a;} ll powmod(ll a,ll b,ll pp){ll ans=1;for(;b;b>>=1,a=mo(a*a,pp))if(b&1)ans=mo(ans*a,pp);return ans;} ll read(){ ll ans=0; char last=' ',ch=getchar(); while(ch<'0' || ch>'9')last=ch,ch=getchar(); while(ch>='0' && ch<='9')ans=ans*10+ch-'0',ch=getchar(); if(last=='-')ans=-ans; return ans; } //head int n; struct activity { int si,fi; }act[1005]; bool cmp(activity a,activity b) { return a.fi<b.fi; } int main() { n=read(); rep(i,1,n) { act[i].si=read(),act[i].fi=read(); } sort(act+1,act+1+n,cmp); int ans=0; int t; for(int i=1;i<=n;i++) { if(act[i].si>=t) { ans++; t=act[i].fi; } } cout<<ans; }
2.区间选点问题
给定n个闭区间[a,b],在数轴上选择尽量少的点,使得每一个区间内至少有一个点(不同区间内含的点可以是同一个)
思路:按照右端点从小到大排序,遍历每个区间,如果已经有点包含就转到下一个区间,否则选择最后一个点标记
因为如果要使需要的点尽可能少,就需要一个点尽可能多的使用,这样只需要在重叠区间标记就可以了
例题:种树
代码:
#include<cstdio> #include<iostream> #include<cstdlib> #include<iomanip> #include<cmath> #include<cstring> #include<string> #include<algorithm> #include<time.h> #include<queue> using namespace std; typedef long long ll; typedef long double ld; typedef pair<int,int> pr; const double pi=acos(-1); #define rep(i,a,n) for(int i=a;i<=n;i++) #define per(i,n,a) for(int i=n;i>=a;i--) #define Rep(i,u) for(int i=head[u];i;i=Next[i]) #define clr(a) memset(a,0,sizeof a) #define pb push_back #define mp make_pair #define fi first #define sc second ld eps=1e-9; ll pp=1000000007; ll mo(ll a,ll pp){if(a>=0 && a<pp)return a;a%=pp;if(a<0)a+=pp;return a;} ll powmod(ll a,ll b,ll pp){ll ans=1;for(;b;b>>=1,a=mo(a*a,pp))if(b&1)ans=mo(ans*a,pp);return ans;} ll read(){ ll ans=0; char last=' ',ch=getchar(); while(ch<'0' || ch>'9')last=ch,ch=getchar(); while(ch>='0' && ch<='9')ans=ans*10+ch-'0',ch=getchar(); if(last=='-')ans=-ans; return ans; } //head int n,m; bool vis[30005]; struct people { int st,ed,nu; }peo[30005]; bool cmp(people a,people b) { return a.ed<b.ed; } int main() { n=read(),m=read(); rep(i,1,m) peo[i].st=read(),peo[i].ed=read(),peo[i].nu=read(); sort(peo+1,peo+1+m,cmp); int sum=0; rep(i,1,m) { int t=0; rep(j,peo[i].st,peo[i].ed) { if(vis[j]==1)t++; } if(t>=peo[i].nu)continue; per(j,peo[i].ed,peo[i].st) { if(vis[j]==0) { vis[j]=1; t++; sum++; if(t==peo[i].nu)break; } } } cout<<sum; }
3.区间覆盖问题
这是一个比较经典的题目,给定一条线段和n个闭区间[a,b],选择尽可能少的线段,使它们能够覆盖整条线段
思路:将所有的区间按左端点从小到大排序,依次处理每一个区间,每次选择覆盖点s的区间中右端点最小的一个,直到区间已经包含了这个区间内所有的点为止
例题:洛谷P1803
代码:
#include<iostream> #include<cstdio> #include<cmath> #include<cstdlib> #include<iomanip> #include<cstring> #include<algorithm> using namespace std; int n,begin[1000001],end[1000001]; void inti() { cin>>n; for(int i=1;i<=n;i++) cin>>begin[i]>>end[i]; } void qsort(int x,int y) { int i,j,mid,t; i=x;j=y;mid=end[(x+y)/2]; while(i<=j) { while(end[i]<mid) i++; while(end[j]>mid) j--; if(i<=j) { t=end[j];end[j]=end[i];end[i]=t; t=begin[j];begin[j]=begin[i];begin[i]=t; i++;j--; } } if(x<j) qsort(x,j); if(i<y) qsort(i,y); } void solve() { int ans=0; for(int i=1,t=-1;i<=n;i++) if(begin[i]>=t) {ans++;t=end[i];} cout<<ans<<endl; } int main() { inti(); qsort(1,n); solve(); return 0; }
一般来说比较常用的也就是这些,还有两个部分感觉平常不太能用的到,这里就不写了(以后如果遇到这样的题可能还会写一写)
总结一下
1.什么样的题能够用贪心?
①可以把大问题分解成很多结构类似的小问题,并且可以对每一个小问题比较轻松的求出当前最优解
②局部最优解能推出整体最优解
2.一些比较常见的贪心的板子
需要注意的是,一个题如果包含了比较常见的贪心思想(比较难的题),它的贪心做法往往是最难推出来的,但也往往是最优的