复健计划(2)贪心(经典的贪心题目、模型和思路)
复健题目:POJ 2376,POJ 1328,POJ 3190,POJ 2393,POJ 1017和ACwing上的贪心模板题目.
一、区间问题
区间问题一般可分为四类问题,简称为区间选点、区间分组、区间覆盖和最大不相交区间数量
1、区间选点
题目的一般描述为:给定N个闭区间,请你在数轴上选择尽量少的点,使得每个区间内至少包含一个选出的点。输出选择的点的最小数量。位于区间端点上的点也算作区间内。
思路:1)先按区间右端点升序排序;2)令比较初值st为-INF。枚举每个区间,若当前区间不包含点st则点数加一,并且st变为该区间右端点.
代码:
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
const int N=100010,INF=0x3f3f3f3f;
int cnt,n;
struct node{
int s,t;
};
node a[N];
bool cmp(node a,node b){
return a.t<b.t;
}
int main(){
cin>>n;
for(int i=1;i<=n;i++)
scanf("%d%d",&a[i].s,&a[i].t);
sort(a+1,a+n+1,cmp);
int st=-INF;
for(int i=1;i<=n;i++)
if(a[i].s>st){
cnt++;
st=a[i].t;
}
cout<<cnt<<endl;
return 0;
}
2、区间分组
题目的一般描述:给定N个闭区间,请你将这些区间分成若干组,使得每组内部的区间两两之间(包括端点)没有交集,并使得组数尽可能小。
思路:1)先按照区间右端点升序排序;2)之后用堆(优先队列)维护在每一组的最后一个区间的右端点,令st=q.top();同时一次枚举区间,若不冲突则不增加组数,st=该区间右端点,q.push(st);若冲突,则增加组数,q.push(该区间右端点) .
经典例题POJ 3190代码:
因为此题输出要求更高,需要存下原序号相关信息,所以这里的优先队列在插入时不是一个数字,而是一个结构体,则需要重载运算符来满足要求,此处这个操作我应当掌握.
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
#include <queue>
using namespace std;
const int N=50010;
struct node{
int s,t;
int num;
};
node a[N];
int n,ans[N],cnt; //ans 原序号对应的
bool cmp(node a,node b){
return a.s<b.s;
}
struct node_2{
int val,num;
};
bool operator <(const node_2 &a,const node_2 &b){
return a.val>b.val;
}
priority_queue<node_2> q;
int main(){
cin>>n;
for(int i=1;i<=n;i++){
int x,y;
scanf("%d%d",&x,&y);
a[i].s=x,a[i].t=y,a[i].num=i;
}
sort(a+1,a+n+1,cmp);
cnt++;
node_2 st;
st.num=a[1].num;
st.val=a[1].t;
q.push(st);
ans[a[1].num]=cnt;
for(int i=2;i<=n;i++){
st=q.top();
if(a[i].s<=st.val){
cnt++;
ans[a[i].num]=cnt;
node_2 ne;
ne.val=a[i].t;
ne.num=a[i].num;
q.push(ne);
}
else{
ans[a[i].num]=ans[st.num];
q.pop();
node_2 ne;
ne.val=a[i].t;
ne.num=a[i].num;
q.push(ne);
}
//cout<<q.top()<<endl;
}
cout<<cnt<<endl;
for(int i=1;i<=n;i++) printf("%d\n",ans[i]);
return 0;
}
3、区间覆盖
问题一般描述:给定N个闭区间以及一个线段区间[S.T],请你选择尽量少的区间,将指定线段区间完全覆盖。输出最少区间数,如果无法完全覆盖则输出-1。
思路:1)按区间左端点升序排列;2)初值令st=第一个区间的右端点(在S一定被覆盖到的情况下),依次枚举区间,若当前区间的左端点<=st,则取st=min(st,当前区间的右端点);若所有区间都不满足该条件,则输出-1.
经典例题:POJ 2376代码:
此题要求和一般描述略有不同,因为该题的”区间“实际上是若干个离散的点,因此判断时要注意细节.
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
const int N=25010;
struct node{
int s,t;
int span;
};
node a[N];
int n,t;
int ans;
bool v[N];
bool cmp(node a,node b){
return a.t>b.t;
}
int main(){
scanf("%d%d",&n,&t);
for(int i=1;i<=n;i++) {
scanf("%d%d",&a[i].s,&a[i].t);
a[i].span=a[i].t-a[i].s;
}
sort(a+1,a+n+1,cmp);
memset(v,0,sizeof(v));
int st=1;
while(st<=t){
bool flag=true;
for(int i=1;i<=n;i++){
if(a[i].s<=st && !v[i]){
st=a[i].t+1;
ans++;
v[i]=1;
flag=false;
break;
}
}
if((ans==n && st<=t) || flag){
cout<<-1<<endl;
return 0;
}
}
cout<<ans<<endl;
return 0;
}
类似题目POJ 1328:
首先要对此题做转化,将小岛转化成在X轴上的区间,然后对这些区间分组,使组数尽量小并且每组中任意两个区间都存在交集.
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <math.h>
using namespace std;
const int N=1010;
int n,d;
struct node{
double s,t;
};
node a[N];
double sqrt (double x);
bool cmp(node a,node b){
return a.s<b.s;
}
int ans,cnt;
int main(){
while(1){
cnt++;
int my=0;
ans=0;
cin>>n>>d;
if(n==0 && d==0) break;
memset(a,0,sizeof(a));
for(int i=1;i<=n;i++){
int x,y;
scanf("%d%d",&x,&y);
my=max(my,abs(y));
a[i].s=-1*sqrt((double)(d*d-y*y))+x;
a[i].t=sqrt((double)(d*d-y*y))+x;
}
if(my>d) {
printf("Case %d: -1\n",cnt);
continue;
}
sort(a+1,a+n+1,cmp);
double st=a[1].t;
ans++;
for(int i=2;i<=n;i++){
if(st>=a[i].s){
st=min(st,a[i].t);
}
else{
ans++;
st=a[i].t;
}
}
printf("Case %d: %d\n",cnt,ans);
}
return 0;
}
二、Huffman树、不等式类、凑硬币类问题
1、Huffman树问题题目POJ 3253和NOIP2004年的合并果子都是很经典的题目.
思路简单说一下就是维护一个小根堆,然后连续两次取出堆顶元素合并,合并之后再放入堆中,直到堆中只剩一个元素即可.
2、不等式类问题最典型的题目是利用绝对值不等式的仓库选址和利用排序不等式的打水问题
即仓库选址由三角不等式的缩放而选择中位数,而打水问题则因为顺序<=乱序<=逆序的原因从小到大排列所需时间.
3、凑硬币类问题的一般描述:给定不同面值的硬币各若干枚,然后要凑出一个较大的面额,问怎样凑用的硬币数量最少.
稍微有所改变的例题如POJ 1017
以上类型题目因为比较特殊,也无固定模板,就不贴代码了.
4、贪心题目实际上没有定式与模板,目前对我来说除了常见的题型,就只能凭感觉猜再找反例证明.