贪心法 C++
贪心法通过局部最优争取达到全局最优,虽然并不总是能导致全局最优
适用于贪心法问题的特征:
1.最优子结构性质。问题的最优解包含其子问题的最优解;
2.贪心选择性质。问题的最优解可以通过一系列局部最优解来得到。
常见问题分类
1.活动安排问题
HDOJ 2037 今年暑假不AC
解题思路:
考虑下面三种贪心策略:
1.最早开始时间(错误,如果一直不结束,后面的活动无法开始)
2.最早结束时间(正确,早结束早开始)(重点要想到这个)
3.用时最少(错误)
算法步骤如下:
1.把所有活动按照结束时间从早到晚排序
2.从第一个开始遍历,如果下一个活动的开始时间小于当前活动的结束时间,排除。
#include<bits/stdc++.h>
using namespace std;
struct point{
int start,end;
};
bool func(const point &a1, const point &a2){
return a1.end<a2.end; //自定义规则:返回true表示a1排在a2前面
}
int main(){
int n;
while(~scanf("%d",&n) && n!=0){
struct point time[n];
for(int i=0;i<n;i++)
scanf("%d %d",&time[i].start,&time[i].end);
sort(time+0,time+n,func); //对数组中下标为0~(n-1)的按照end时间从小到大排序
int sign=0;//记录结果
int e=-1;
for(int i=0;i<n;i++) //关键
if(time[i].start>=e){ //当前活动的start大于等于上一个活动的end
sign++;
e = time[i].end; //记录上一个活动的结束时间
}
printf("%d\n",sign);
}
return 0;
}
2.区间覆盖问题
POJ 2376 Cleaning Shifts
Description
John安排他的N(1<=N<=25000)个仆人在畜棚做日常打扫工作。他只想让仆人单独做完一件事。他把一天分成T(1<=T<=1000000)个区间.
每一个仆人只有在一些打扫工作的区间时间可以工作。任何被选中去打扫的仆人将会在他的间隙时间工作。
你的工作就是帮助John安排一些时间区间给仆人,这样I.每一个时间区间至少有一个仆人II.给尽可能少的仆人安排工作。如果不存在这样的安排,输出-1。Input
第一行:是N和T,空格分开
之后的N行:每一行包含两个数字,表示每一个仆人可以工作的的开始时间,以及在结束时间过后停止工作。Output
第一行:John使用的最少仆人数量或者-1
Sample Input
3 10
1 7
3 6
6 10Sample Output
2
解题思路:
贪心思路是尽可能找出工作时间最长的仆人
算法步骤如下:
1.把每个时间段按照左端点递增排序
2.设已覆盖的区间为 [ L, R ],在剩下的仆人中找出所有左端点小于等于R+1且右端点最大的仆人;判断是否可以把这个仆人的时间段加入到覆盖区间里并更新 [L, R]。
3.重复步骤2直到所有区间被覆盖。
#include<iostream>
#include<algorithm>
using namespace std;
const int N=25002;
struct point{
int start,end;
}time[N];
bool func(const point &a1, const point &a2){
return a1.start<a2.start; //自定义规则:返回true表示a1排在a2前面
}
int main(){
int n,t;
while(~scanf("%d %d",&n,&t)){
for(int i=0;i<n;i++)
scanf("%d %d",&time[i].start,&time[i].end);
sort(time+0,time+n,func); //对数组中下标为0~(n-1)的按照end时间从小到大排序
int l=time[0].start,r=time[0].end; //已覆盖区间 [L, R]
if(l!=1){
printf("-1\n");
break;
}
int i=1,number=1,max=0;
while(i<n){
if(r==t) break;
bool sign=0;
if(time[i].start<=(r+1)){ //当出现一个点s<(r+1)时
while(time[i].start<=(r+1) && i<n){ //找出之后的所有s<(r+1)的点
if(max<time[i].end){
max = time[i].end; //记录下最长的e
sign = 1; //表示有一个点可以最优扩大覆盖
}
i++;
}
if(sign){
number++;
r = max; //扩大了覆盖区域 ,更新(l,r)
}
}else break; //中间有一个区域无人能覆盖
}
if(r==t) printf("%d\n",number);
else printf("-1\n");
}
return 0;
}
3.最优装载问题
HDOJ 2570 迷瘴
#include<bits/stdc++.h>
using namespace std;
int main(){
int c;
scanf("%d",&c);
while(c--){
int n,v,w;
scanf("%d %d %d",&n,&v,&w);
int num[n];
for(int i=0;i<n;i++){
scanf("%d",&num[i]);
}
sort(num,num+n);
int number = 0;
double add = 0,old=0;
for(int i=0;i<n;i++){
number++;
add += num[i];
if(add/number>w){
number--;
break;
}else old = add;
}
if(!number) printf("0 0.00\n");
else printf("%d %.2f\n",number*v,old/number*0.01);
}
return 0;
}
4.多机调度问题
Description
设有n个独立的作业,由m台相同的计算机进行加工处理。作业 i i i所需的处理时间为 t i t_i ti。每个作业可以在任何一台计算机上加工处理,但不能间断、拆分。要求给出一种作业调度方案,在尽可能短的时间内,由m台计算机加工处理完成这n个作业。
求解多机调度的贪心策略是最长处理时间的作业优先,即把处理时间最长的作业分配给最先空闲的计算机。让处理时间长的作业得到优先处理,从而在整体上获得尽可能短的处理时间。
(1)如果n<=m,需要的时间就是n个作业当中最长的处理时间。
(2)如果n>m,首先将n个作业按照处理时间降序排列,然后按顺序把作业分配给空闲的计算机。
但是,这只是近似解,并不是最优解。
举例:
任务:给出处理时间分别为16 14 12 11 10 9 8的作业,交由三台计算机A、B、C处理。
贪心策略:(用时31)
计算机/时间 | 0 | 12 | +2 | +2 | +7 | +8(31) |
---|---|---|---|---|---|---|
A | A16 | 4 | 2 | A9 | 2 | |
B | B14 | 2 | B10 | 8 | 1 | |
C | C12 | C11 | 9 | 7 | C8 | 结束 |
最优策略:(用时27)
计算机/时间 | 0 | 10 | +4 | +2 | +3 | +8(27) |
---|---|---|---|---|---|---|
A | A16 | 6 | 2 | A11 | ||
B | B14 | 4 | B12 | 10 | ||
C | C10 | C9 | 5 | 3 | C8 | 结束 |
应用:Huffman编码
POJ 1521 Entropy
Description
简而言之,输入一个字符串,分别用ASCII编码(每个字符8bit)和Huffman编码,输出编码后的bit长度以及压缩比。
Sample Input
AAAAABCD
THE_CAT_IN_THE_HAT
ENDSample Output
64 13 4.9
144 51 2.8
#include <string>
#include <vector>
#include <iostream>
#include <queue>
#include <algorithm>
using namespace std;
int main(){
string s;
priority_queue<int, vector<int>, greater<int> > Q;//保持最小的永远在前面
while (getline(std::cin,s) && s!="END"){
sort(s.begin(), s.end());
int t=1;
for(int i=1;i<s.length();i++) //统计每个字符的频数
if(s[i]!=s[i-1]){
Q.push(t);
t=1;
}else t++;
Q.push(t);
if(Q.size() == 1) {
printf("%d %d 8.0\n", s.length()*8, s.length());
Q.pop();
continue;
}
int sum=0;
while (Q.size()>1){ //挑选最小的两个,组成一个节点的两个分支,向上延伸
int a = Q.top();Q.pop();
int b = Q.top();Q.pop();
Q.push(a+b);
sum += a+b; //重点思考:为什么能直接相加?
// 因为上一行,这两个分支的和已经又包括在Q当中了。在之后的每一次向上延伸,都会算一次并纳入总长度当中。
}
printf("%d %d %.1f\n",s.length()*8,sum,(double)s.length()*8.0/(double)sum);
Q.pop();
}
return 0;
}
应用:模拟退火
HDOJ 2899 Strange fuction
Description
函数F(x) = 6x^7 +8x^6 +7x^3 +5x^2-y*x (0 <= x <=100)
输入测试案例个数 T(1<=T<=100) 和 y(0 < y <1e10),输出函数的最小值(4位小数)。Sample Input
2
100
100Sample Output
-74.4291
-178.8534
模拟退火算法的主要步骤如下:
(1)设置一个初始温度T;
(2)温度下降,状态转移。从当前温度按降温系数下降到下一个温度,在新的温度计算当前状态;
(3)如果温度下降到设定的下限,程序停止。
伪代码
eps = 1e-8; //终止温度,接近0,用于控制精度
T = 100; //初始温度,应该是高温,以100为例
decrease = 0.98; //降温系数,控制退火的快慢,小于1
g(x); //温度为x时的评价函数,例如物理意义上的能量,或者本题的函数值
now,next; //当前温度,新温度
while(T>eps){
g(now),g(next);
dE = g(next)-g(next)
if( dE>=0 ) now=next; //新状态更好,接受新状态
else if(exp(dE/T) > rand()) //新状态更差,在一定概率下接受它
now = next;
T *= decrease;
}
#include <bits/stdc++.h>
using namespace std;
const double eps = 1e-8; //终止温度
double y;
const double decrease = 0.98; //温度递减系数
double func(double x){ //评价当前状态的函数
return 6*pow(x,7.0)+8*pow(x,6.0)+7*pow(x,3.0)+5*pow(x,2.0)-y*x;
}
double solve(){
double T = 100; //初始温度
double x = 50; //x的初始值
double now = func(x);
double res = now;
while (T > eps){
int f[2] = {1,-1};
double newx = x + f[rand()%2]*T; //按概率改变x
if(0<=newx && newx<=100){
double next = func(newx);
res = min(now, next);
if(now - next > eps){
x = newx;
now = next;
}
}
T *= decrease;
}
return res;
}
int main(){
int n;
scanf("%d",&n);
while(n--){
scanf("%lf",&y);
printf("%.4f\n",solve());
}
return 0;
}