贪心:贪心是一种在每次决策时采取当前意义下最优策略的算法,因此,使用贪心法要求问题的整体最优性可以由局部最优性导出。贪心算法的正确性需要证明,常用的证明方法有:
1.微扰(邻项交换)
证明在任意局面下,任何对局部最优策略的微小改变都会造成整体结果变差。经常用于以“排序”为贪心策略的证明。
2.范围缩放
证明任何对局部最优策略作用范围的扩展都不会造成整体结果变差。
3.决策包容性
证明在任意局面下,作出局部最优决策以后,在问题状态空间中的可达集合包含了作出其他任何决策后可达的集合。换言之,这个局部最优策略提供的可能性包含其他所有策略提供的可能性。
4.反证法
5.数学归纳法
题目一:防晒
有C头奶牛进行日光浴,第i头奶牛需要minSPF[i]到maxSPF[i]单位强度之间的阳光。
每头奶牛在日光浴前必须涂防晒霜,防晒霜有L种,涂上第i种之后,身体接收到的阳光强度就会稳定为SPF[i],第i种防晒霜有cover[i]瓶。
求最多可以满足多少头奶牛进行日光浴。
输入格式
第一行输入整数C和L。
接下来的C行,按次序每行输入一头牛的minSPF和maxSPF值,即第i行输入minSPF[i]和maxSPF[i]。
再接下来的L行,按次序每行输入一种防晒霜的SPF和cover值,即第i行输入SPF[i]和cover[i]。
每行的数据之间用空格隔开。
输出格式
输出一个整数,代表最多可以满足奶牛日光浴的奶牛数目。
数据范围
1≤C,L≤2500,
1≤minSPF≤maxSPF≤1000,
1≤SPF≤1000
输入样例:
3 2
3 10
2 5
1 5
6 2
4 1
输出样例:
2
题目思想:
1.按照minSPF递减的顺序把奶牛排序,依次考虑每头奶牛
2.对于每头奶牛,扫描一遍所有的防晒霜,在这头奶牛能用(能用指的是该防晒霜的强度符合这头奶牛的范围,并且瓶数还有剩余)的防晒霜里找SPF值最大的使用。
以上算法的贪心策略是在满足条件的前提下每次选SPF最大的防晒霜。这个策略为什么是正确的呢?我们考虑这一步策略的作用范围扩展到后续其他奶牛之后产生的影响。每瓶防晒霜是否可用,会被minSPF与maxSPF两个条件限制,因为奶牛按照minSPF递减排序,所以每一个不低于当前奶牛minSPF值的防晒霜,都不会低于后面其他奶牛的minSPF。也就是说,对于当前奶牛可用的任意两瓶防晒霜x与y,如果SPF[X]<SPF[Y],那么后面其他奶牛只可能出现“x与y都能用”,“x与y都不能用”,“x能用,y不能用”这三种情况之一。因此当前奶牛选择maxSPF较大的y去使用,对于整体问题的影响显然比选择maxSPF较小的x更好。
另外,每头奶牛对答案的贡献至多是1。即使让当前这头奶牛放弃曰光浴,留下防晒霜给后面的某一头奶牛使用,对答案的贡献也不会变得更大。
// 贪心
//将所有奶牛按照 minSPF 从大到小的顺序排序,然后依次考虑每头奶牛;
//对于每头奶牛,扫描当前所有能用的防晒霜,选择 SPF 值最大的防晒霜来用;
#include <bits/stdc++.h>
using namespace std;
typedef pair<int,int> PII;
const int N = 2510;
int n, m, res = 0;
PII cows[N];
map<int, int> spfs;
int main()
{
cin >> n >> m;
for (int i = 0; i < n; i ++ )
cin >> cows[i].first >> cows[i].second;// n头牛的minSPF值和maxSPF值
for (int i = 0; i < m; i ++ )
{
int spf, cover;
cin >> spf >> cover;// 防晒霜的spf值和数量
spfs[spf] += cover; // spf值可能相同
}
sort(cows, cows + n);// 将所有奶牛按照 minSPF 从小到大的顺序排序
spfs[0] = spfs[1001] = n;// 哨兵
for (int i = n - 1; i >= 0; i -- )// 倒序(拨正排序)
{
auto spf = spfs.upper_bound(cows[i].second);// 大于第i头牛的maxSPF的最小元素
spf --;// spf--即为小于等于第i头牛的maxSPF值的最大元素
if (spf -> first >= cows[i].first)// 如果spf值大于第i头牛的minSPF值
{
res ++ ;// 发放防晒霜
if (--spf -> second == 0)// 数量为零,删除防晒霜名字
spfs.erase(spf);
}
}
cout << res << endl;
return 0;
}
题目二:畜栏预定
有N头牛在畜栏中吃草。
每个畜栏在同一时间段只能提供给一头牛吃草,所以可能会需要多个畜栏。
给定N头牛和每头牛开始吃草的时间A以及结束吃草的时间B,每头牛在[A,B]这一时间段内都会一直吃草。
当两头牛的吃草区间存在交集时(包括端点),这两头牛不能被安排在同一个畜栏吃草。
求需要的最小畜栏数目和每头牛对应的畜栏方案。
输入格式
第1行:输入一个整数N。
第2…N+1行:第i+1行输入第i头牛的开始吃草时间A以及结束吃草时间B,数之间用空格隔开。
输出格式
第1行:输入一个整数,代表所需最小畜栏数。
第2…N+1行:第i+1行输入第i头牛被安排到的畜栏编号,编号是从1开始的 连续 整数,只要方案合法即可。
数据范围
1≤N≤50000,
1≤A,B≤1000000
输入样例:
5
1 10
2 4
3 6
5 8
4 7
输出样例:
4
1
2
3
2
4
题目思想:按照开始吃草的时间把牛排序。维护一个数组S,记录当前每个畜栏安排进行的最后一头牛,最初没有畜栏。依次对于每头牛,扫描数组S,找到任意一上畜栏,满足当前的牛开始吃草的时间晚于畜栏中最后一头牛结束吃草的时间。如果这样的畜栏不存在,则为其新建一个畜栏。
这个贪心算法的时间复杂度是O(N^2)。其是我们可以用一个小根堆,维护每个畜栏最后一头牛结束吃草的时间,每头牛直接与堆顶比较就可以了。这样的时间复杂度为O(NlogN)。
#include<iostream>
#include<algorithm>
#include<queue>
using namespace std;
typedef pair<int,int> PII;
const int N = 50010;
int n;
int id[N]; //用来存放第i头牛的畜栏编号
pair<PII,int> cows[N]; // 依次存放 牛的开始吃草时间,结束吃草时间 该牛是第几头牛。
int main(){
cin>>n;
for(int i=0;i<n;i++){
cin>>cows[i].first.first>>cows[i].first.second;
cows[i].second = i;
}
sort(cows,cows+n);
priority_queue<PII, vector<PII>, greater<PII>> heap; //小根堆
for(int i=0;i<n;i++){
if(heap.empty()||heap.top().first>=cows[i].first.first){ // 如果是空堆或者最早结束吃草的时间要大于我当前牛开始吃草时间,就是没有空畜栏了
// 就要添加畜栏
PII stall = {cows[i].first.second,heap.size()}; //每个结点 存放结束吃草的时间 和 当前堆的大小(就是畜栏号)
id[cows[i].second] = stall.second; // 用id数组 存放第i头牛的畜栏号
heap.push(stall); //入堆
}else{ //最早结束吃草的时间要小于我当前牛开始吃草时间,有空畜栏了
auto stall = heap.top();
heap.pop();
stall.first = cows[i].first.second; // 取堆顶,修改结束吃草时间,
id[cows[i].second] = stall.second; //存放第i头牛的畜栏号
heap.push(stall); //入堆
}
}
cout<<heap.size()<<endl;
for(int i=0;i<n;i++) cout<<id[i]+1<<endl;
return 0;
}
题目三:雷达设备
假设海岸是一条无限长的直线,陆地位于海岸的一侧,海洋位于另外一侧。
每个小岛都位于海洋一侧的某个点上。
雷达装置均位于海岸线上,且雷达的监测范围为d,当小岛与某雷达的距离不超过d时,该小岛可以被雷达覆盖。
我们使用笛卡尔坐标系,定义海岸线为x轴,海的一侧在x轴上方,陆地一侧在x轴下方。
现在给出每个小岛的具体坐标以及雷达的检测范围,请你求出能够使所有小岛都被雷达覆盖所需的最小雷达数目。
输入格式
第一行输入两个整数n和d,分别代表小岛数目和雷达检测范围。
接下来n行,每行输入两个整数,分别代表小岛的x,y轴坐标。
同一行数据之间用空格隔开。
输出格式
输出一个整数,代表所需的最小雷达数目,若没有解决方案则所需数目输出“-1”。
数据范围
1≤n≤1000
输入样例:
3 2
1 2
-3 1
2 1
输出样例:
2
题目思想:对于x轴上方的每个岛屿,可以计算出x轴上的一段能管辖它的区间[l,r]。问题就转化为:给定N个区间,在x轴上放置最少的点,使每个区间包含至少一个点。
1.将所有区间的右端点从小到大排序,
2.依次考虑每个区间:
如果当前区间包含最后一个选择的点,则直接跳过
如果当前区间不包含最后一个选择的点,则在当前区间的右端点的位置选一个新的点
#include<iostream>
#include<algorithm>
#include<cmath>
using namespace std;
typedef pair<double,double> PDD;
const int N = 1010;
const double eps = 1e-6 , INF = 1e10; //eps表示精度
int n,d;
PDD seg[N]; //用来存储区间的左右端点
int main(){
bool success = true;
cin>>n>>d;
for(int i=0;i<n;i++){
int x,y;
cin>>x>>y;
if(y>d) { //如果有一个点的y坐标大于d,那么无论选择那个点,都无法检测到
success = false ;
break;
}
auto len = sqrt(d*d - y*y);
seg[i] = {x+len,x-len}; //存放[y,x] 因为排序是按右端点从小到大,这样存储,就可以使用sort函数
}
if(!success) cout<<"-1"<<endl;
else {
sort(seg,seg+n);
int res = 0;
double last = -INF;
for(int i=0;i<n;i++){
if(seg[i].second>last+eps){ //如果当前区间的起点大于当前雷达点,那么就需要建设新的雷达点
res++;
last = seg[i].first; //将新的雷达点建设到当前区间的右端点。
}
}
cout<<res<<endl;
}
return 0;
}
题目四:国王游戏
恰逢 H 国国庆,国王邀请 n 位大臣来玩一个有奖游戏。
首先,他让每个大臣在左、右手上面分别写下一个整数,国王自己也在左、右手上各写一个整数。
然后,让这 n 位大臣排成一排,国王站在队伍的最前面。
排好队后,所有的大臣都会获得国王奖赏的若干金币,每位大臣获得的金币数分别是:
排在该大臣前面的所有人的左手上的数的乘积除以他自己右手上的数,然后向下取整得到的结果。
国王不希望某一个大臣获得特别多的奖赏,所以他想请你帮他重新安排一下队伍的顺序,使得获得奖赏最多的大臣,所获奖赏尽可能的少。
注意,国王的位置始终在队伍的最前面。
输入格式
第一行包含一个整数 n,表示大臣的人数。
第二行包含两个整数 a 和 b,之间用一个空格隔开,分别表示国王左手和右手上的整数。
接下来 n 行,每行包含两个整数 a 和 b,之间用一个空格隔开,分别表示每个大臣左手和右手上的整数。
输出格式
输出只有一行,包含一个整数,表示重新排列后的队伍中获奖赏最多的大臣所获得的金币数。
数据范围
1≤n≤1000
0<a,b<10000
输入样例:
3
1 1
2 3
7 4
4 6
输出样例:
2
题目思想:按照每个大臣左、右手上的数的乘积从小到大排序,就是最优排队方案,这个贪心算法可以使用微扰(邻项交换)证明。
#include<iostream>
#include<algorithm>
#include<vector>
using namespace std;
typedef pair<int,int> PII;
const int N = 1010;
int n;
PII p[N];
vector<int> mul(vector<int> a,int b){
int t=0;
vector<int> c;
for(int i=0;i<a.size();i++){
t+=a[i]*b;
c.push_back(t%10);
t=t/10;
}
while(t){
c.push_back(t%10);
t/=10;
}
return c;
}
vector<int> div(vector<int> a,int b){
vector<int> c;
int t=0;
bool if_first = true;
for(int i=a.size()-1;i>=0;i--){
t=t*10+a[i];
int x = t/b;
if(!if_first || x){
if_first = false;
c.push_back(x);
}
t%=b;
}
reverse(c.begin(), c.end());
return c;
}
vector<int> max_vec(vector<int> a,vector<int> b){
if(a.size()>b.size()) return a;
if(a.size()<b.size()) return b;
for(int i=a.size()-1;i>=0;i--){
if(a[i] == b[i]) continue;
if(a[i]>b[i]) return a;
else return b;
}
}
int main(){
cin>>n;
for(int i=0;i<=n;i++){
int x,y;
cin>>x>>y;
p[i] = {x*y,x};
}
sort(p+1,p+n+1);
vector<int> product(1,1);
vector<int> res(1,0);
for(int i=0;i<=n;i++){
if(i) res = max_vec(res,div(product,p[i].first/p[i].second));
product = mul(product,p[i].second);
}
for(int i=res.size()-1;i>=0;i--) cout<<res[i];
return 0;
}
题目五:给树染色
一颗树有 n 个节点,这些节点被标号为:1,2,3…n,每个节点 i 都有一个权值 A[i]。
现在要把这棵树的节点全部染色,染色的规则是:
根节点R可以随时被染色;对于其他节点,在被染色之前它的父亲节点必须已经染上了色。
每次染色的代价为T*A[i],其中T代表当前是第几次染色。
求把这棵树染色的最小总代价。
输入格式
第一行包含两个整数 n 和 R ,分别代表树的节点数以及根节点的序号。
第二行包含 n 个整数,代表所有节点的权值,第 i 个数即为第 i 个节点的权值 A[i]。
接下来n-1行,每行包含两个整数 a 和 b ,代表两个节点的序号,两节点满足关系: a 节点是 b 节点的父节点。
除根节点外的其他 n-1 个节点的父节点和它们本身会在这 n-1 行中表示出来。
同一行内的数用空格隔开。
输出格式
输出一个整数,代表把这棵树染色的最小总代价。
数据范围
1≤n≤1000,
1≤A[i]≤1000
输入样例:
5 1
1 2 1 2 4
1 2
1 3
2 4
3 5
输出样例:
33
题目思想:
#include<iostream>
#include<algorithm>
using namespace std;
const int N = 1010;
struct node{
int value,sum,father;
double avg;
}node[N];
int n,root;
int find(){
double avg = 0;
int res = -1;
for(int i=1;i<=n;i++){
if(i!=root && node[i].avg > avg ){
res = i;
avg = node[i].avg;
}
}
return res;
}
int main(){
cin>>n>>root;
int ans =0;
for(int i=1;i<=n;i++){
int a;
cin>>a;
node[i].value = node[i].avg = a;
node[i].sum = 1;
ans+=a;
}
for(int i=0;i<n-1;i++){
int a,b;
cin>>a>>b;
node[b].father = a;
}
for(int i=0;i<n-1;i++){
int p = find();
int father = node[p].father;
ans+=node[father].sum*node[p].value;
node[p].avg = -1;
for(int i=1;i<=n;i++){
if(node[i].father == p)
node[i].father = father;
}
node[father].sum +=node[p].sum;
node[father].value+=node[p].value;
node[father].avg = (double)node[father].value / node[father].sum;
}
cout << ans << endl;
return 0;
}