贪心算法 greedy algorithm
目录
- 贪心算法的简介
- 1.线段覆盖
- 2.区间选点
- 3.区间覆盖
贪心算法简介
贪心算法(又称贪婪算法)是指,在对问题求解时,总是做出在当前看来是最好的选择。也就是说,不从整体最优上加以考虑,只做出在某种意义上的局部最优解。贪心算法不是对所有问题都能得到整体最优解,关键是贪心策略的选择,选择的贪心策略必须具备无后效性,即某个状态以前的过程不会影响以后的状态,只与当前状态有关。
核心:根据题意选取一种量度标准
解题要点
贪心算法是指:在每一步求解的步骤中,它要求“贪婪”的选择最佳操作,并希望通过一系列的最优选择,能够产生一个问题的(全局的)最优解。
贪心算法每一步必须满足一下条件:
1、可行的:即它必须满足问题的约束。
2、局部最优:他是当前步骤中所有可行选择中最佳的局部选择。
3、不可取消:即选择一旦做出,在算法的后面步骤就不可改变了。
解题步骤
解题的一般步骤是:
1.建立数学模型来描述问题;
2.把求解的问题分成若干个子问题;
3.对每一子问题求解,得到子问题的局部最优解;
4.把子问题的局部最优解合成原来问题的一个解。
简单举例
最少硬币
有1、2、5、10、20、50、100七种面值的硬币,要支付指定的金额,问怎么支付所用的硬币个数最少。
这是一个非常日常化的问题,马上我们会想到,尽可能先用大面值的硬币,就能使支付的硬币尽可能少。这就是“贪心选择”。
经典例题
1.线段覆盖
NYOJ 14 会场安排问题
有n个活动要在小礼堂举行,小礼堂同一时刻只能由一个活动使用,要使举办尽量多的活动。i活动举行的时间段为[si,ei]
2
1 10
10 11
贪心思路: 用贪心法的话思想很简单:活动越早结束,剩余的时间是不是越多?那我就找最早结束的那个活动,找到后在剩下的活动中再找最早结束的不就得了
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OBLTys5R-1572958963662)(evernotecid://2C354EAE-828A-4D61-888B-9453DC360564/appyinxiangcom/25418762/ENResource/p300)]
题解如下
#include<stdio.h>
#include<algorithm>
using namespace std;
struct Node
{
int s,e;
};
bool cmp_sort(Node a,Node b)
{
if(a.e==b.e)
{
return a.s>b.s;
}
return a.e<b.e;
}
int main()
{
int n;
scanf("%d",&n);
Node node[n];
for(int i=0;i<n;i++)
{
scanf("%d%d",&node[i].s,&node[i].e);
}
sort(node,node+n,cmp_sort);
int ans=1;
int t=0;
for(int i=1;i<n;i++)
{
if(node[i].s>node[i].e)
{
ans++;
t=i;
}
}
printf("%d",ans);
return 0;
}
2.区间选点
找点
Description
上数学课时,老师给了LYH一些闭区间,让他取尽量少的点,使得每个闭区间内至少有一个点。但是这几天LYH太忙了,你们帮帮他吗?
Input
多组测试数据。
每组数据先输入一个N,表示有N个闭区间(N≤100)。
接下来N行,每行输入两个数a,b(0≤a≤b≤100),表示区间的两个端点。
Output
输出一个整数,表示最少需要找几个点。
Sample Input 1
4
1 5
2 4
1 4
2 3
3
1 2
3 4
5 6
1
2 2
Sample Output 1
1
3
1
贪心思路:
这一题思路与上一题相同,只是变变了样子
题解如下:
#include<stdio.h>
#include<iostream>
#include<algorithm>
using namespace std;
struct Activity
{
int s,e;
};
bool cmp_sort(Activity a,Activity b)
{
if(a.e==b.e)
{
return a.s>b.s;
}
return a.e<b.e;
}
int main()
{
int n;
while(~scanf("%d",&n))
{
Activity aty[n];
for(int i=0;i<n;i++)
{
scanf("%d%d",&aty[i].s,&aty[i].e);
}
sort(aty,aty+n,cmp_sort);
int aty_count=1;
int last_can_aty_end_time=aty[0].e;//第一个活动一定举办,
for(int i=1;i<n;i++) //所以上个能举办的活动结束时间为aty[9].e
{ //所以for循环要从第二个开始进行判断
if(aty[i].s>last_can_aty_end_time)
{
aty_count++;
last_can_aty_end_time=aty[i].e; //更新结束时间。
}
}
printf("%d\n",aty_count);
}
return 0;
}
区间覆盖类(一)
Description
好吧。这道题的目的在于让大家认识贪心问题的三个区间经典问题:区间选点问题,区间覆盖问题,以及该题的选择不相交区间问题。有许多的贪心问题可以转化为这三类的问题。
那么,对于该问题。就是给一系列的区间,求最多的区间,要求区间个数最多,这些区间不相交,需要注意的是这些区间都是闭区间。
Input
第一行一个数n为区间个数(n<=1000)
接下来有n行,每行有两个数a,b分别为区间的两个端点,a,b在int范围。
EOF结尾。
Output
输出如样例所示
Sample Input 1
2
1 10
10 11
3
1 10
10 11
11 20
Sample Output 1
Case 1:
1.
Case 2:
2.
Hint
**注意,A完这道题,如果你对上面的三个问题还没有太理解,请去学会。**
贪心思路:这一题与前两题思路相同,不过这一题给的一对端点,不知哪个大,那个小,需要单独进行一下判断。
题解如下
#include<stdio.h>
#include<iostream>
#include<algorithm>
using namespace std;
struct Activity
{
int s,e;
};
bool cmp_sort(Activity a,Activity b)
{
if(a.e==b.e)
{
return a.s>b.s;
}
return a.e<b.e;
}
int main()
{
int n;
int counter=1;
while(~scanf("%d",&n))
{
Activity aty[n];
for(int i=0;i<n;i++)
{
int a,b;
scanf("%d%d",&a,&b);
if(a>b)
{
int temp=a;
a=b;
b=temp;
}
aty[i].s=a;
aty[i].e=b;
}
sort(aty,aty+n,cmp_sort);
int aty_count=1;
int last_can_aty_end_time=aty[0].e;//第一个区间一定要取到,
for(int i=1;i<n;i++)
{ //所以for循环要从第二个区间开始进行判断
if(aty[i].s>last_can_aty_end_time)
{
aty_count++;
last_can_aty_end_time=aty[i].e; //更新结束时间。
}
}
printf("Case %d:\n%d.\n",counter,aty_count);
}
return 0;
}
区间覆盖 (二)
给定一个长度为m的区.0间,再给出n条线段的起点和终点(注意这里是闭区间),求最少使用多少条线段可以将整个区间完全覆盖 样例:
区间长度8,可选的覆盖线段[2,6],[1,4],[3,6],[3,7],[6,8],[2,4],[3,5]
每次选择从当前起点能覆盖到的最长的区间
题解如下
#include<stdio.h>
#include<algorithm>
using namespace std;
struct Line
{
int s,e;
};
bool cmp_sort(Line a,Line b)
{
if(a.s==b.s)
{
return a.e>b.e;
}
return a.s<b.s;
}
int main()
{
int m,n;
scanf("%d%d",&m,&n);
Line line[n];
for(int i=0;i<n;i++)
{
scanf("%d%d",&line[i].s,&line[i].e);
}
sort(line, line+n, cmp_sort);
int ans=1;
int temp_end=line[0].e;
int end_pos=temp_end;
for(int i=1;i<n;i++)
{
if(line[i].s>end_pos)
{
ans++;
end_pos=temp_end;
temp_end=line[i].e;
}
else
{
temp_end=max(temp_end,line[i].e);
}
}
if(end_pos<m)
{
ans++;
}
printf("%d",ans);
return 0;
}
区间类覆盖(三)
Description
有一块草坪,横向长w,纵向长为h,在它的橫向中心线上不同位置处装有n(n<=10000)个点状的喷水装置,每个喷水装置i喷水的效果是让以它为中心半径为Ri的圆都被润湿。请在给出的喷水装置中选择尽量少的喷水装置,把整个草坪全部润湿。
Input
第一行输入一个正整数N表示共有n次测试数据。
每一组测试数据的第一行有三个整数n,w,h,n表示共有n个喷水装置,w表示草坪的横向长度,h表示草坪的纵向长度。
随后的n行,都有两个整数xi和ri,xi表示第i个喷水装置的的横坐标(最左边为0),ri表示该喷水装置能覆盖的圆的半径。
Output
每组测试数据输出一个正整数,表示共需要多少个喷水装置,每个输出单独占一行。
如果不存在一种能够把整个草坪湿润的方案,请输出0。
Sample Input 1
2
2 8 6
1 1
4 5
2 10 6
4 5
6 5
Sample Output 1
1
2
贪心思路
这一题首先要判断所给,的某个喷水器能否使用,如果不能使用,就把它标记为假值(以后直接跳过不考虑),而其他的喷水器都可用,然后要通过喷水器的横坐标与半径,求出该喷水器的作用区间,然后就通过两层for循环去找最优解。
题解如下
#include<stdio.h>
#include<math.h>
#include<algorithm>
using namespace std;
struct None
{
int center;
double x,y; //x 起始坐标 ,y区间结束坐标
int r;
bool can_use; //喷水器能否使用
};
int main()
{
int t;
scanf("%d",&t);
while (t--)
{
int start_place=0; //初始化一开始的区间结束位置
int n,w,h;
scanf("%d%d%d",&n,&w,&h);
None none[n];
for(int i=0;i<n;i++)
{
scanf("%d%d",&none[i].center,&none[i].r);
if(2*none[i].r<=h)
{
none[i].can_use=0;
}
else
{
none[i].can_use=1;
}
//根据横坐标去去转化成区间
none[i].x=none[i].center-sqrt(none[i].r*none[i].r-(h/2.0)*(h/2.0));
none[i].y=none[i].center+sqrt(none[i].r*none[i].r-(h/2.0)*(h/2.0));
}
int sum_count=0; //总数
int flag_1; //两个判断标志
int flag_2;
double max_end; //某种情况最有区间结束位置
for( ; ; )
{
flag_1=0;
flag_2=0;
int time=0;
for(int j=0;j<n;j++)
{
if(none[j].x<=start_place&&none[j].y>start_place&&none[j].can_use)//判断某个区间是否是最有区间
{
none[j].can_use=0;
if(time==0) //初始化给max_end赋一个初始值
{
max_end=none[j].y;
time++;
flag_1=1;
}
else if(max_end<=none[j].y)
{
max_end=none[j].y;
flag_1=1;
}
}
}
if(flag_1==1)
{
// printf("#\n");
sum_count++;
start_place=max_end;
if(start_place>=w)
{
flag_2=1;
break;
}
}
else if(flag_1==0)
{
break;
}
}
if(flag_1==1&&flag_2==1) // 两个条件同时满足才可以,全覆盖
{
printf("%d\n",sum_count);
}
else
{
printf("0\n");
}
}
return 0;
}