贪心是一种在每次决策时采取当前意义下最优策略的算法,
使用贪心算法要求问题的整体最优性可以由局部最优性推导出。
贪心算法的正确性需要证明,常见手法为:
- 微扰(邻项交换)
- 证明在任意局面下,任何对局部最优策略的微小改变都会造成整体结果变差。经常用于以 “排序” 为贪心策略的证明。
- 范围缩放
- 证明任何局部最优策略作用范围的扩展都不会造成整体结果变差。
- 决策包容性
- 证明在任意局面下,做出局部最优决策以后,在问题状态空间中的可达集合包含了做出其他任何决策后可达集合。换而言之这个局部最优策略提供的可能性包含其他所有策略提供的可能性。
- 反证法
- 数学归纳法
在比赛中,有两种类型的题目:
- 根据题目信息,可以一步一步推出解法
- 需要先猜一个解法,然后证明其正确性。
前者通常是图论、数据结构等,后者则是动态规划、贪心这些。所以书上的分析也是直接给出结论,然后证明。
不过贪心的题目通过的成功率通常是通过较多的题目开拓视野、归纳总结。
所以学贪心,第一次见的题目是很难想出来。
范围缩放
【例题】防晒
有 C C C 头奶牛进行日光浴,第 i i i 头奶牛需要 m i n S P F [ i ] minSPF[i] minSPF[i] 到 m a x S P F [ i ] maxSPF[i] maxSPF[i] 单位强度之间的阳光。
每头奶牛在日光浴前必须涂防晒霜,防晒霜有 L L L 种,涂上第 i i i 种之后,身体接收到的阳光强度就会稳定为 S P F [ i ] SPF[i] SPF[i] ,第 i i i 种防晒霜有 c o v e r [ i ] cover[i] cover[i] 瓶。
求最多可以满足多少头奶牛进行日光浴。
1 ≤ C , L ≤ 2500 , 1 ≤ m i n S P F ≤ m a x S P F ≤ 1000 , 1 ≤ S P F ≤ 1000 1\le C,L\le2500,\\ 1\le minSPF \le maxSPF\le 1000,\\ 1\le SPF\le 1000 1≤C,L≤2500,1≤minSPF≤maxSPF≤1000,1≤SPF≤1000
分析:
贪心策略:按照 m i n S P F minSPF minSPF 递减排序所以奶牛,然后考虑每头奶牛能用的 S P F SPF SPF 值最大的。
书上给的证明是范围缩放,但是y总使用的是匈牙利算法进行反证,y总的证明要严谨的多,对匈牙利算法有了解的可以看他的证明:y总的证明 。
虽然不严谨,但是书上提供的证明也是以后说服队友或者自己很好方式,毕竟经验。
既然是范围缩放,首先我们把范围放大,也就是拓展到后续其他奶牛。每瓶防晒霜是否可用,会被 m i n S P F minSPF minSPF 和 m a x S P F maxSPF maxSPF 两个条件限制。又因为奶牛是按照 m i n S P F minSPF minSPF 递减排序的,所以每一个不低于当前奶牛 m i n S P F minSPF minSPF 值得防晒霜,都不会低于后面其他奶牛的 m i n S P F minSPF minSPF 。
之后我们在将范围缩小,对于当前奶牛可用的任意两瓶防晒霜 x x x 与 y y y ,如果 S P F [ x ] < S P F [ y ] SPF[x] < SPF[y] SPF[x]<SPF[y] ,那么后面其他奶牛只可能出现 “ x , y x,y x,y 能用” 、 “ x , y x,y x,y 都不能用” 或者 “ x x x 能用, y y y 不能用” 这三种情况之一。这时范围在扩大到全局,显然当前奶牛使用 m a x S P F maxSPF maxSPF 较大的 y y y ,对于整体问题的影响会小于 m a x S P F maxSPF maxSPF 较小的 x x x 更好。
在找最大的 S P F SPF SPF 可以使用 m a p map map 将复杂度降为 O ( l o g ( n ) ) O(log(n)) O(log(n)) 。当然我不怎么理解,所以还是使用的 O ( n ) O(n) O(n) 的找,所以时间复杂度为 O ( n 2 ) O(n^2) O(n2) 。
代码如下:
#include <bits/stdc++.h>
using namespace std;
const int N = 2510;
struct Cow {
int l, r;
bool operator < (const Cow &_x) const {
return l > _x. l;
}
} cows[N];
int n, m;
struct Hus {
int v, cnt;
} hus[N];
int main()
{
scanf("%d%d", &n, &m);
for(int i = 0; i < n; ++i)
scanf("%d%d", &cows[i].l, &cows[i].r);
for(int i = 0; i < m; ++i)
scanf("%d%d", &hus[i].v, &hus[i].cnt);
sort(cows, cows + n);
int res = 0;
for(int i = 0; i < n; ++i) {
int t = -1;
for(int j = 0; j < m; ++j) {
if(cows[i].l <= hus[j].v && hus[j].v <= cows[i].r && hus[j].cnt
&& (t == -1 || hus[t].v < hus[j].v)) {
t = j;
}
}
if(t == -1) continue;
res ++ ;
hus[t].cnt--;
}
cout << res << endl;
return 0;
}
【例题】畜栏预定
有 N N N 头牛在畜栏中吃草。
每个畜栏在同一时间段只能提供给一头牛吃草,所以可能会需要多个畜栏。
给定 N N N 头牛和每头牛开始吃草的时间 A A A 以及结束吃草的时间 B B B,每头牛在 [ A , B ] [A,B] [A,B] 这一时间段内都会一直吃草。
当两头牛的吃草区间存在交集时(包括端点),这两头牛不能被安排在同一个畜栏吃草。
求需要的最小畜栏数目和每头牛对应的畜栏方案。
1 ≤ N ≤ 50000 , 1 ≤ A , B ≤ 1000000 1\le N\le 50000 ,\\ 1\le A,B\le1000000 1≤N≤50000,1≤A,B≤1000000
分析:
贪心策略:维护一个房间数组,房间信息记录了什么时候结束,每安排一只牛,它的房间结束时间就会变成牛结束的时间,之后进来的牛,如果它的开始时间时,有空房间,就把它安排在那里,否则就重新申请一个房间。
一样,要对牛开始时间递增排序。
现在使用上述范围缩放来证明这个贪心,首先我们考虑一头牛是否能进入房间得是这个房间的结束时间是小于它的开始时间,因为牛的开始时间已经递增排序了,所以每一个不高于当前牛开始时间的房间,都不会高于后面其他牛的开始时间。也就是说,对于当前牛可用的任意两个房间 x x x 与 y y y ,如果 l a s t [ x ] < l a s t [ y ] last[x] < last[y] last[x]<last[y] ,那么后面其他奶牛只可能出现 “ x , y x,y x,y 都能用” 、 “ x , y x,y x,y 都不能用” 或者 “ y y y 能用, x x x 不能用” 。因此,当前奶牛要选择房间结束时间 l a s t [ x ] last[x] last[x] ,也就是结束时间早的那个对于整体问题影响小。
这个算法的时间复杂度是 O ( N 2 ) O(N^2) O(N2) ,但是题目数据不允许,可用使用优先队列以 O ( l o g ( n ) ) O(log(n)) O(log(n)) 找出结束时间最早的那个,时间复杂度为 O ( n l o g ( n ) ) O(n~log(n)) O(n log(n)) 。
代码如下:
#include <iostream>
#include <cstring>
#include <cstdio>
#include <algorithm>
#include <queue>
using namespace std;
typedef long long LL;
typedef pair<int, int> PII;
const int N = 50010;
int n;
int ans[N];
struct Room {
int id;
int last;
bool operator < (const Room &x) const{
return last > x.last;
}
};
struct Mom {
int id;
int l, r;
bool operator < (const Mom &x) const {
return l < x.l;
}
} mom[N];
int main() {
scanf("%d", &n);
for(int i = 0; i < n; ++i) {
scanf("%d%d", &mom[i].l, &mom[i].r);
mom[i].id = i + 1;
}
sort(mom, mom + n);
priority_queue<Room> heap;
int num = 1;
for(int i = 0; i < n; ++i) {
if(heap.empty()) {
heap.push({num, mom[i].r});
ans[mom[i].id] = num++;
} else {
Room p = heap.top();
if(p.last < mom[i].l) {
heap.pop();
heap.push({p.id, mom[i].r});
ans[mom[i].id] = p.id;
} else {
heap.push({num, mom[i].r});
ans[mom[i].id] = num++;
}
}
}
printf("%d\n", num - 1);
for(int i = 1; i <= n; ++i) printf("%d\n", ans[i]);
return 0;
}
决策包容性
【例题】雷达设备
假设海岸是一条无限长的直线,陆地位于海岸的一侧,海洋位于另外一侧。
每个小岛都位于海洋一侧的某个点上。
雷达装置均位于海岸线上,且雷达的监测范围为 d d d,当小岛与某雷达的距离不超过 d d d 时,该小岛可以被雷达覆盖。
我们使用笛卡尔坐标系,定义海岸线为 x x x 轴,海的一侧在 x x x 轴上方,陆地一侧在 x x x 轴下方。
现在给出每个小岛的具体坐标以及雷达的检测范围,请你求出能够使所有小岛都被雷达覆盖所需的最小雷达数目。
1 ≤ n ≤ 1000 , − 1000 ≤ x , y ≤ 1000 1\le n\le1000,\\ −1000\le x,y\le 1000 1≤n≤1000,−1000≤x,y≤1000
分析:
这道题需要换个角度来思考,如果直接从 x x x 轴上看能不能覆盖小岛很麻烦,但是从每个小岛看那些区间能覆盖它就很轻松了。
所以我们对每一个小岛,可用计算出它在 x x x 轴上的能检测到它的区间 l [ i ] l[i] l[i] ~ r [ i ] r[i] r[i] 。所以问题就变为:给定 N N N 个区间,在 x x x 轴上放置最少的点,使每个区间包含至少一个岛。
贪心策略:对每个区间的左端点 l [ i ] l[i] l[i] 从小到大排序,用一个变量维护已经安放的最后一台雷达的坐标 p o s pos pos ,起初 p o s pos pos 为负无穷。依次考虑每个小岛,如果小岛的左端点大于了最后一台雷达的坐标 p o s pos pos ,则新增一台设备, p o s pos pos 更新成 r [ i ] r[i] r[i] 。否则使用前面已有的雷达,更新雷达的 p o s pos pos 为 min ( r [ i ] , p o s ) \min(r[i], pos) min(r[i],pos) 。
依次类推,直到所有区间被检测,输出安放个数即可。
书上所介绍的证明方法就是 “决策包容性” ,我们要先看对于任意局面下,有那些决策,这道题显然是对于任意区间 l [ i ] l[i] l[i] ~ r [ i ] r[i] r[i] ,有两种选择:
- 使用已有的雷达检测它
- 新建一台雷达检测它。
我们的局部最优策略是,当 1 1 1 可行时,不会选择 2 2 2 。 选择 1 1 1 可用在任意位置上一台雷达。选择 2 2 2 则需要在 l [ i ] l[i] l[i] ~ r [ i ] r[i] r[i] 之间新建雷达,也就是说选择 1 1 1 未来可能到达的状态包含了选择 2 2 2 的。
其次,在选择 1 1 1 后,我们会更新设备位置为 min ( p o s , r [ i ] ) \min(pos,r[i]) min(pos,r[i]) ,这显然是在 i i i 检测的前提下尽量往后放,这个策略未来的可达状态显然包含了 “放在更前的位置” 未来的可达状态。而按照 l [ i ] l[i] l[i] 排序后,显然这种调整不会影响前面已经被检测的区间。
代码如下:
#include<iostream>
#include<algorithm>
#include<cmath>
#include<cstdio>
#define l first
#define r second
using namespace std;
typedef pair<double,double> PDD;
const int N = 1010;
PDD segs[N];
int n,d;
int solve(){
sort(segs,segs+n);
double range=segs[0].r,ans=1;
for(int i = 1; i < n; ++ i){
if(range >= segs[i].l)
range = (range > segs[i].r ) ? segs[i].r : range;
else
range = segs[i].r, ans ++;
}
return ans;
}
int main()
{
cin>>n>>d;
bool res=true;
for(int i=0;i<n;++i){
double x,y;
cin>>x>>y;
if(y>d) res=false;
segs[i].l=x-sqrt(d*d-y*y);
segs[i].r=x+sqrt(d*d-y*y);
}
if(res)
printf("%d", solve());
else
printf("-1");
return 0;
}
微扰
【例题】国王游戏
恰逢 H H H 国国庆,国王邀请 n n n 位大臣来玩一个有奖游戏。
首先,他让每个大臣在左、右手上面分别写下一个整数,国王自己也在左、右手上各写一个整数。
然后,让这 n n n 位大臣排成一排,国王站在队伍的最前面。
排好队后,所有的大臣都会获得国王奖赏的若干金币,每位大臣获得的金币数分别是:
排在该大臣前面的所有人的左手上的数的乘积除以他自己右手上的数,然后向下取整得到的结果。
国王不希望某一个大臣获得特别多的奖赏,所以他想请你帮他重新安排一下队伍的顺序,使得获得奖赏最多的大臣,所获奖赏尽可能的少。
注意,国王的位置始终在队伍的最前面。
1 ≤ n ≤ 1000 0 < a , b < 10000 1\le n\le 1000\\ 0<a,b<10000 1≤n≤10000<a,b<10000
分析:
贪心策略,按照每个大臣的左、右手数的乘积从小到大排序,就是最优排队方案。这种排序类的贪心用微扰证明太合适不过了。
对于任意一种顺序,设 n n n 名大臣左、右手上的数分别是 L [ 1 ] L[1] L[1] ~ L [ n ] L[n] L[n] 与 R [ 1 ] R[1] R[1] ~ R [ n ] R[n] R[n] 。国王手里的数为 L [ 0 ] L[0] L[0] 和 R [ 0 ] R[0] R[0] 。
对于两个相邻大臣
i
i
i 与
i
+
1
i + 1
i+1 ,在交换前这两个大臣获得的奖励是:
1
R
[
i
]
×
∏
j
=
0
i
−
1
L
[
j
]
,
L
[
i
]
R
[
i
+
1
]
×
∏
j
=
0
i
−
1
L
[
j
]
\frac{1}{R[i]}\times\prod \limits_{j=0}^{i - 1}L[j],~\frac{L[i]}{R[i + 1]}\times\prod \limits_{j=0}^{i - 1}L[j]
R[i]1×j=0∏i−1L[j], R[i+1]L[i]×j=0∏i−1L[j]
交换后
1
R
[
i
+
1
]
×
∏
j
=
0
i
−
1
L
[
j
]
,
L
[
i
+
1
]
R
[
i
]
×
∏
j
=
0
i
−
1
L
[
j
]
\frac{1}{R[i + 1]}\times\prod \limits_{j=0}^{i - 1}L[j],~\frac{L[i+1]}{R[i]}\times\prod \limits_{j=0}^{i - 1}L[j]
R[i+1]1×j=0∏i−1L[j], R[i]L[i+1]×j=0∏i−1L[j]
他们两个交换顺序对于其他人是没有任何影响的,因此对于整体影响的就就只有这两种顺序,我们只需要比较那种更优。
比较两式的大小关系:
max
(
1
R
[
i
]
,
L
[
i
]
R
[
i
+
1
]
)
max
(
1
R
[
i
+
1
]
,
L
[
i
+
1
]
R
[
i
]
)
\max(\frac{1}{R[i]},\frac{L[i]}{R[i+1]})~\max(\frac{1}{R[i + 1]},\frac{L[i+1]}{R[i]})
max(R[i]1,R[i+1]L[i]) max(R[i+1]1,R[i]L[i+1])
两边同时乘上
R
[
i
+
1
]
×
R
[
i
]
R[i + 1]\times R[i]
R[i+1]×R[i] ,两式就变为了
max
(
R
[
i
+
1
]
,
L
[
i
]
×
R
[
i
]
)
max
(
R
[
i
]
,
L
[
i
+
1
]
×
R
[
i
+
1
]
)
\max(R[i+1],L[i]\times R[i])~\max(R[i ],L[i+1]\times R[i+1])
max(R[i+1],L[i]×R[i]) max(R[i],L[i+1]×R[i+1])
因为
L
[
i
]
,
R
[
i
]
L[i],R[i]
L[i],R[i] 都是正整数,所有
R
[
i
]
≤
A
L
[
i
]
×
R
[
i
]
R[i] \le AL[i]\times R[i]
R[i]≤AL[i]×R[i],所以上述等式只需要比较
L
[
i
]
×
R
[
i
]
,
L
[
i
+
1
]
×
R
[
i
+
1
]
L[i]\times R[i],~L[i+1]\times R[i+1]
L[i]×R[i], L[i+1]×R[i+1]
所有按照 L [ i ] × R [ i ] L[i] \times R[i] L[i]×R[i] 排序,就是最优排队,当然最大的获奖大臣不一定是最后一个大臣,而且这道题是高精度。
代码如下:
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 1010;
int l[N], r[N];
int gl, gr;
int n;
bool check(int i, int j) {
return (LL)l[i] * r[i] > (LL)l[j] * r[j];
}
vector<int> mul(vector<int> &A, int b)
{
vector<int> C;
int t = 0;
for (int i = 0; i < A.size() || t; i ++ )
{
if (i < A.size()) t += A[i] * b;
C.push_back(t % 10);
t /= 10;
}
while (C.size() > 1 && C.back() == 0) C.pop_back();
return C;
}
vector<int> div(vector<int> &A, int b, int &r)
{
vector<int> C;
r = 0;
for (int i = A.size() - 1; i >= 0; i -- )
{
r = r * 10 + A[i];
C.push_back(r / b);
r %= b;
}
reverse(C.begin(), C.end());
while (C.size() > 1 && C.back() == 0) C.pop_back();
return C;
}
vector<int> max_v(vector<int> _1, vector<int> _2) {
if(_1.size() > _2.size()) return _1;
if(_1.size() < _2.size()) return _2;
for (int i = _1.size() - 1; i >= 0; i -- ) {
if(_1[i] > _2[i]) return _1;
if(_1[i] < _2[i]) return _2;
}
return _2;
}
int main()
{
cin >> n;
cin >> gl >> gr;
for(int i = 0; i < n; ++i)
scanf("%d%d", &l[i], &r[i]);
for(int i = 1; i <= n; ++i) {
for(int j = 0; j < n - i; ++j) {
if(check(j, j + 1)) {
swap(l[j], l[j + 1]);
swap(r[j], r[j + 1]);
}
}
}
vector<int> ans, res;
vector<int> temp;
temp.push_back(gl);
int r_d;
for(int i = 0; i < n; ++i) {
res = div(temp, r[i], r_d);
ans = max_v(ans, res);
temp = mul(temp, l[i]);
}
for(int i = ans.size() - 1; i >= 0; --i)
cout << ans[i];
return 0;
}
【例题】给树染色
一颗树有 n n n 个节点,这些节点被标号为: 1 , 2 , 3 … n 1,2,3…n 1,2,3…n ,每个节点 i i i 都有一个权值 A [ i ] A[i] A[i]。
现在要把这棵树的节点全部染色,染色的规则是:
根节点 R R R 可以随时被染色;对于其他节点,在被染色之前它的父亲节点必须已经染上了色。
每次染色的代价为 T × A [ i ] T\times A[i] T×A[i],其中 T T T 代表当前是第几次染色。
求把这棵树染色的最小总代价。
1 ≤ n ≤ 1000 , 1 ≤ A [ i ] ≤ 1000 1\le n\le1000,\\ 1\le A[i]\le1000 1≤n≤1000,1≤A[i]≤1000
分析:
这道题好难,就算看了题解知道思路了,也写不出代码,难受~。
感兴趣的话还是去看 y总的吧,链接在这里:y总的视频
证明也是微扰,只不过是树上点的微扰。