贪心
基本原理
- 核心:局部最优到全局最优
- 贪心策略:使用贪心时采取的策略
适用条件
- 异常广泛,不需要特别注意
注意事项
- 贪心策略:必须具备无后效性,即某个状态以前的状态不会影响以后的状态
个人总结
贪心的精髓在于贪心策略的选取,目前已有明显的贪心策略包含:
- 局部最优
- 只考虑一侧
- 动态局部最优(哈夫曼树类)
- 搭积木模型
以下为例题
【深基12.例1】部分背包问题
题目描述
阿里巴巴走进了装满宝藏的藏宝洞。藏宝洞里面有 N ( N ≤ 100 ) N(N \le 100) N(N≤100) 堆金币,第 i i i 堆金币的总重量和总价值分别是 m i , v i ( 1 ≤ m i , v i ≤ 100 ) m_i,v_i(1\le m_i,v_i \le 100) mi,vi(1≤mi,vi≤100)。阿里巴巴有一个承重量为 T ( T ≤ 1000 ) T(T \le 1000) T(T≤1000) 的背包,但并不一定有办法将全部的金币都装进去。他想装走尽可能多价值的金币。所有金币都可以随意分割,分割完的金币重量价值比(也就是单位价格)不变。请问阿里巴巴最多可以拿走多少价值的金币?
输入格式
第一行两个整数
N
,
T
N,T
N,T。
接下来
N
N
N 行,每行两个整数
m
i
,
v
i
m_i,v_i
mi,vi。
输出格式
一个实数表示答案,输出两位小数
解题思路
- 贪心策略:局部最优,优先装"性价比最高“的金币
- 关键操作:对性价比(v/m)进行排序
#include<bits/stdc++.h>
using namespace std;
using ll = long long;
int n;
double t,m, v, ans = 0;
struct coin {
double m, v,p;//p是比例
coin(double a, double b) :m(a), v(b), p(b / a){}
};
vector<coin>vec;
bool cmp(const coin&a,const coin&b) {
return a.p > b.p;
}
void solve() {
cin >> n >> t;
while (n--) {
cin >> m >> v;
vec.push_back(coin(m, v));
}
sort(vec.begin(), vec.end(), cmp);
for (int i = 0; i < vec.size(); i++) {
if (t - vec[i].m >= 0) {
t -= vec[i].m;
ans += vec[i].v;
}
else {
ans += vec[i].v * t / vec[i].m;
break;
}
}
printf("%.2f", ans);
}
signed main() {
std::ios::sync_with_stdio(false);
std::cin.tie(0); std::cout.tie(0);
solve();
return 0;
}
凌乱的yyy / 线段覆盖
题目背景
快 noip 了,yyy 很紧张!
题目描述
现在各大 oj 上有 n n n 个比赛,每个比赛的开始、结束的时间点是知道的。
yyy 认为,参加越多的比赛,noip 就能考的越好(假的)。
所以,他想知道他最多能参加几个比赛。
由于 yyy 是蒟蒻,如果要参加一个比赛必须善始善终,而且不能同时参加 2 2 2 个及以上的比赛。
输入格式
第一行是一个整数 n n n,接下来 n n n 行每行是 2 2 2 个整数 a i , b i ( a i < b i ) a_{i},b_{i}\ (a_{i}<b_{i}) ai,bi (ai<bi),表示比赛开始、结束的时间。
输出格式
一个整数最多参加的比赛数目。
解题思路
- 贪心策略:只考虑一侧(控制变量);
- 题目本质:区间的交集问题,保证如果有多个区间有交集,即
A∩B∩C∩D∩...!=空集
,当作一次比赛
#include<bits/stdc++.h>
using namespace std;
using ll = long long;
int n,ct;
struct date {
int start, end;
date(){}
date(int a,int b):start(a),end(b){}
};
bool cmp(const date&a,const date&b) {
return a.end < b.end;
}
void solve() {
cin >> n;
if (n == 1) {
cout << 1; return;//特判
}
vector<date>vec(n, date());
for (int i = 0; i < n; i++) {
cin >> vec[i].start >> vec[i].end;
}
sort(vec.begin(), vec.end(), cmp);
int pre = vec[0].end;
for (int i = 1; i < n; i++) {
if (vec[i].start >= pre) {//[0,2]和[2,4]交集为空
pre = vec[i].end;
ct++;//有交集只能算一个
}
}
cout << ++ct;//自己也算一个交集
}
signed main() {
std::ios::sync_with_stdio(false);
std::cin.tie(0); std::cout.tie(0);
solve();
return 0;
}
[NOIP2004 提高组] 合并果子 / [USACO06NOV] Fence Repair G
题目描述
在一个果园里,多多已经将所有的果子打了下来,而且按果子的不同种类分成了不同的堆。多多决定把所有的果子合成一堆。
每一次合并,多多可以把两堆果子合并到一起,消耗的体力等于两堆果子的重量之和。可以看出,所有的果子经过 n − 1 n-1 n−1 次合并之后, 就只剩下一堆了。多多在合并果子时总共消耗的体力等于每次合并所耗体力之和。
因为还要花大力气把这些果子搬回家,所以多多在合并果子时要尽可能地节省体力。假定每个果子重量都为 1 1 1 ,并且已知果子的种类 数和每种果子的数目,你的任务是设计出合并的次序方案,使多多耗费的体力最少,并输出这个最小的体力耗费值。
例如有 3 3 3 种果子,数目依次为 1 1 1 , 2 2 2 , 9 9 9 。可以先将 1 1 1 、 2 2 2 堆合并,新堆数目为 3 3 3 ,耗费体力为 3 3 3 。接着,将新堆与原先的第三堆合并,又得到新的堆,数目为 12 12 12 ,耗费体力为 12 12 12 。所以多多总共耗费体力 = 3 + 12 = 15 =3+12=15 =3+12=15 。可以证明 15 15 15 为最小的体力耗费值。
输入格式
共两行。
第一行是一个整数
n
(
1
≤
n
≤
10000
)
n(1\leq n\leq 10000)
n(1≤n≤10000) ,表示果子的种类数。
第二行包含 n n n 个整数,用空格分隔,第 i i i 个整数 a i ( 1 ≤ a i ≤ 20000 ) a_i(1\leq a_i\leq 20000) ai(1≤ai≤20000) 是第 i i i 种果子的数目。
输出格式
一个整数,也就是最小的体力耗费值。输入数据保证这个值小于 2 31 2^{31} 231 。
解题思路
- 贪心策略:哈夫曼树—树的带权路径和最小(我称之为动态局部最优)
- 解题技巧:使用优先级队列模拟哈夫曼树.
- 经典错误:优先合并小的策略是错误的,没有考虑到合并后数组的排序也应该动态变化;
用贪心的原理解释是:合并后新的元素影响了原数组的排序,具有后效性
using namespace std;
using ll = long long;
int n,num,sum;
class cmp {
public:
bool operator()(int a,int b) {
return a > b;
}
};
priority_queue<int, vector<int>, cmp>q;
void solve() {
cin >> n;
while (n--) {
cin >> num;
q.push(num);
}
while (q.size() >= 2) {
int pre = q.top(); q.pop();
int next = q.top(); q.pop();
sum += pre + next;
q.push(pre + next);
}
cout << sum;
}
signed main() {
std::ios::sync_with_stdio(false);
std::cin.tie(0); std::cout.tie(0);
solve();
return 0;
}
以下为练习
小A的糖果
题目描述
小 A 有 n n n 个糖果盒,第 i i i 个盒中有 a i a_i ai 颗糖果。
小 A 每次可以从其中一盒糖果中吃掉一颗,他想知道,要让任意两个相邻的盒子中糖的个数之和都不大于 x x x,至少得吃掉几颗糖。
输入格式
输入的第一行是两个用空格隔开的整数,代表糖果盒的个数 n n n 和给定的参数 x x x。
第二行有 n n n 个用空格隔开的整数,第 i i i 个整数代表第 i i i 盒糖的糖果个数 a i a_i ai。
输出格式
输出一行一个整数,代表最少要吃掉的糖果的数量。
样例 #1
样例输入 #1
3 3
2 2 2
样例 #2
样例输入 #2
6 1
1 6 1 2 0 4
个人代码
- 贪心策略:考虑一侧
#include<bits/stdc++.h>
using namespace std;
using ll = long long;
int n;
ll x,ans=0;
void solve() {
cin >> n >> x;
vector<ll>vec(n, 0);
for (int i = 0; i < n; i++) {
cin >> vec[i];
}
int temp = 0;
for (int i = 0; i < n - 1; i++) {
if (vec[i] + vec[i + 1] > x) {
temp = vec[i + 1];
vec[i + 1] = vec[i + 1] >= vec[i + 1] + vec[i] - x ? x-vec[i] : 0;
ans += temp - vec[i + 1];
}
}
for (int i = 1; i < n; i++) {
if (vec[i] + vec[i - 1] > x) {
temp = vec[i - 1];
vec[i-1] = vec[i - 1] >= vec[i] + vec[i - 1] - x ? x-vec[i] : 0;
ans += temp - vec[i - 1];
}
}
cout << ans;
}
signed main() {
std::ios::sync_with_stdio(false);
std::cin.tie(0); std::cout.tie(0);
solve();
return 0;
}
[NOIP2013 提高组] 积木大赛
题目背景
NOIP2013 提高组 D2T1
题目描述
春春幼儿园举办了一年一度的“积木大赛”。今年比赛的内容是搭建一座宽度为 n n n 的大厦,大厦可以看成由 n n n 块宽度为 1 1 1 的积木组成,第 i i i 块积木的最终高度需要是 h i h_i hi。
在搭建开始之前,没有任何积木(可以看成 n n n 块高度为 0 0 0 的积木)。接下来每次操作,小朋友们可以选择一段连续区间 [ l , r ] [l, r] [l,r],然后将第 L L L 块到第 R R R 块之间(含第 L L L 块和第 R R R 块)所有积木的高度分别增加 1 1 1。
小 M 是个聪明的小朋友,她很快想出了建造大厦的最佳策略,使得建造所需的操作次数最少。但她不是一个勤于动手的孩子,所以想请你帮忙实现这个策略,并求出最少的操作次数。
输入格式
包含两行,第一行包含一个整数 n n n,表示大厦的宽度。
第二行包含 n n n 个整数,第 i i i 个整数为 h i h_i hi。
输出格式
建造所需的最少操作数。
样例 #1
样例输入 #1
5
2 3 4 1 2
样例输出 #1
5
解题思路
- 经典的搭积木问题:见下一题
using namespace std;
using ll = long long;
int n,ans=0;
void solve() {
cin >> n;
vector<int>vec(100009, 0);
for (int i = 1; i <= n; i++) {
cin >> vec[i];
}
for (int i = 1; i <= n; i++) {
if (vec[i] > vec[i - 1])ans += vec[i] - vec[i - 1];
}
cout << ans;
}
signed main() {
std::ios::sync_with_stdio(false);
std::cin.tie(0); std::cout.tie(0);
solve();
return 0;
}
[NOIP2018 提高组] 铺设道路
题目背景
NOIP2018 提高组 D1T1
题目描述
春春是一名道路工程师,负责铺设一条长度为 n n n 的道路。
铺设道路的主要工作是填平下陷的地表。整段道路可以看作是 n n n 块首尾相连的区域,一开始,第 i i i 块区域下陷的深度为 d i d_i di 。
春春每天可以选择一段连续区间 [ L , R ] [L,R] [L,R] ,填充这段区间中的每块区域,让其下陷深度减少 1 1 1。在选择区间时,需要保证,区间内的每块区域在填充前下陷深度均不为 0 0 0 。
春春希望你能帮他设计一种方案,可以在最短的时间内将整段道路的下陷深度都变为 0 0 0 。
输入格式
输入文件包含两行,第一行包含一个整数 n n n,表示道路的长度。 第二行包含 n n n 个整数,相邻两数间用一个空格隔开,第 i i i 个整数为 d i d_i di 。
输出格式
输出文件仅包含一个整数,即最少需要多少天才能完成任务。
解题思路
- 将数组几何化,本质上等价于堆积木,如果ai-1<ai,则在堆ai时顺便也完成了ai-1的工作
- 例如堆第4列时已经完成了第3列的工作,只需要额外完成第4列多出来的工作,所以有ai-ai-1
以下图例不是我的,引自大佬 ans+vec[1]
是因为默认先搭建第一列,剩下所有的积木都是相对于第一列搭建的
#include<bits/stdc++.h>
using namespace std;
using ll = long long;
int n;
ll ans = 0;
vector<ll>vec(100009, 0);
void solve() {
cin >> n;
for (int i = 1; i <= n; i++) {
cin >> vec[i];
}
for (int i = 2; i <= n; i++) {
if (vec[i] > vec[i - 1]) {
ans += vec[i] - vec[i - 1];
}
}
cout << ans+vec[1];
}
signed main() {
std::ios::sync_with_stdio(false);
std::cin.tie(0); std::cout.tie(0);
solve();
return 0;
}
跳跳!
题目描述
你是一只小跳蛙,你特别擅长在各种地方跳来跳去。
这一天,你和朋友小 F 一起出去玩耍的时候,遇到了一堆高矮不同的石头,其中第
i
i
i 块的石头高度为
h
i
h_i
hi,地面的高度是
h
0
=
0
h_0 = 0
h0=0。你估计着,从第
i
i
i 块石头跳到第
j
j
j 块石头上耗费的体力值为
(
h
i
−
h
j
)
2
(h_i - h_j) ^ 2
(hi−hj)2,从地面跳到第
i
i
i 块石头耗费的体力值是
(
h
i
)
2
(h_i) ^ 2
(hi)2。
为了给小 F 展现你超级跳的本领,你决定跳到每个石头上各一次,并最终停在任意一块石头上,并且小跳蛙想耗费尽可能多的体力值。
当然,你只是一只小跳蛙,你只会跳,不知道怎么跳才能让本领更充分地展现。
不过你有救啦!小 F 给你递来了一个写着 AK 的电脑,你可以使用计算机程序帮你解决这个问题,万能的计算机会告诉你怎么跳。
那就请你——会写代码的小跳蛙——写下这个程序,为你 NOIp AK 踏出坚实的一步吧!
输入格式
输入一行一个正整数
n
n
n,表示石头个数。
输入第二行
n
n
n 个正整数,表示第
i
i
i 块石头的高度
h
i
h_i
hi。
输出格式
输出一行一个正整数,表示你可以耗费的体力值的最大值。
解题思路
- 十年oi一场空,不开__见祖宗
- 贪心策略:局部最优,优先选高度差距最大的石头跳
using namespace std;
using ll = long long;
int n;ll ans=0;
void solve() {
cin >> n;
vector<ll>vec(n, 0);
for (int i = 0; i < n; i++) {
cin >> vec[i];
}
sort(vec.begin(), vec.end());
int l = 0, r = n - 1, cur = r;
ans += vec[r] * vec[r];
while (l < r) {
if (cur > l) {
ans += (vec[cur] - vec[l]) * (vec[cur] - vec[l]);
r--;
cur = l;
}
else {
ans += (vec[r] - vec[cur]) * (vec[r] - vec[cur]);
l++;
cur = r;
}
/*cout << ans << endl;*/
}
cout << ans;
}
signed main() {
std::ios::sync_with_stdio(false);
std::cin.tie(0); std::cout.tie(0);
solve();
return 0;
}