贪心
贪⼼
贪⼼算法是两极分化很严重的算法。简单的问题会让你觉得理所应当,难⼀点的问题会让你怀疑⼈
⽣。
- 什么是贪⼼算法?
贪⼼算法,或者说是贪⼼策略:企图⽤局部最优找出全局最优。 - 把解决问题的过程分成若⼲步;
- 解决每⼀步时,都选择"当前看起来最优的"解法;
- "希望"得到全局的最优解。
- 贪⼼算法的特点
- 对于⼤多数题⽬,贪⼼策略的提出并不是很难,难的是证明它是正确的。因为贪⼼算法相较于暴
⼒枚举,每⼀步并不是把所有情况都考虑进去,⽽是只考虑当前看起来最优的情况。但是,局部
最优并不等于全局最优,所以我们必须要能严谨的证明我们的贪⼼策略是正确的。
⼀般证明策略有:反证法,数学归纳法,交换论证法等等。 - 当问题的场景不同时,贪⼼的策略也会不同。因此,贪⼼策略的提出是没有固定的套路和模板
的。我们后⾯讲的题⽬虽然分类,但是⼤家会发现具体的策略还是相差很⼤。
因此,不要妄想做⼏道贪⼼题⽬就能遇到⼀个会⼀个。有可能做完 50 道贪⼼题⽬之后,第 51
道还是没有任何思路。 - 如何学习贪⼼?
先有⼀个认知:做了⼏⼗道贪⼼的题⽬,遇到⼀个新的⼜没有思路,这时很正常的现象,把⼼态放
平。 - 前期学习的时候,重点放在各种各样的策略上,把各种策略当成经验来吸收;
- 在平常学习的时候,我们尽可能的证明⼀下这个贪⼼策略是否正确,这样有利于培养我们严谨的
思维。但是在⽐赛中,能想出来⼀个策略就已经不错了,如果再花费⼤量的时间去证明,有点得
不偿失。这个时候,如果根据贪⼼策略想出来的若⼲个边界情况都能过的话,就可以尝试去写代
码了。


P1842 USACO05NOV 奶牛玩杂技
题目背景
Farmer John 养了 NNN 头牛,她们已经按 1∼N1\sim N1∼N 依次编上了号。FJ 所不知道的是,他的所有牛都梦想着从农场逃走,去参加马戏团的演出。可奶牛们很快发现她们那笨拙的蹄子根本无法在钢丝或晃动的秋千上站稳(她们还尝试过把自己装在大炮里发射出去,但可想而知,结果是悲惨的) 。最终,她们决定练习一种最简单的杂技:把所有牛都摞在一起, 比如说, 第一头牛站在第二头的身上, 同时第二头牛又站在第三头牛的身上...最底下的是第 NNN 头牛。
题目描述
每头牛都有自己的体重以及力量,编号为 iii 的奶牛的体重为 WiW_iWi,力量为 SiS_iSi。
当某头牛身上站着另一些牛时它就会在一定程度上被压扁,我们不妨把它被压扁的程度叫做它的压扁指数。对于任意的牛,她的压扁指数等于摞在她上面的所有奶牛的总重(当然不包括她自己)减去她的力量。奶牛们按照一定的顺序摞在一起后, 她们的总压扁指数就是被压得最扁的那头奶牛的压扁指数。
你的任务就是帮助奶牛们找出一个摞在一起的顺序,使得总压扁指数最小。
输入格式
第一行一个整数 NNN。
接下来 NNN 行,每行两个整数 WiW_iWi 和 SiS_iSi。
输出格式
一行一个整数表示最小总压扁指数。
输入输出样例 #1
输入 #1
3
10 3
2 5
3 3
输出 #1
2
说明/提示
对于 100%100\%100% 的数据,1≤N≤5×1041 \le N \le 5\times 10^41≤N≤5×104,1≤Wi≤1041 \le W_i \le 10^41≤Wi≤104,1≤Si≤1091 \le S_i \le 10^91≤Si≤109。
cpp
#include<iostream>
#include<algorithm>
using namespace std;
typedef struct
{
int w;
int s;
}node;
const int N=5e4+10;
node arr[N];
bool cmp(node &x, node &y)
{
return x.w-(x.s+y.s)<y.w-(y.s+x.s);
}
int main()
{
int n;
cin>>n;
for(int i=0;i<n;i++)
{
int w,s;
cin>>w>>s;
arr[i].s=s;
arr[i].w=w;
}
sort(arr,arr+n,cmp);
long long ans=0;
long long m=0;
for(int i=0;i<n;i++)
{
if(m-arr[i].s>0)
ans=max(m-arr[i].s,ans);
m+=arr[i].w;
}
cout<<ans;
return 0;
}
我们发现,在在⼀个序列中,任意交换相邻两头⽜ 的顺序之后,区间 以及
内每⼀头⽜的压扁指数都是不变。因此我们可以找⼀种⽐较⽅式,对整个数组排序,最终的
结果就是最优序列。
ai, ai + 1 1,i − 1
i + 1, n
设交换的相邻两头⽜的下标为 , 区间内所有奶⽜的重量为 。由此可
得:
i, j (i + 1 = j) 1,i − 1 W
• 交换前:
第 i 头⽜的压扁指数为: W − si ;
第 j 头⽜的压扁指数为: W + wi − sj ;
被压得最扁的那头奶⽜的压扁指数: k1 = max(W − si, W + wi − sj )
• 交换后:
第 j 头⽜的压扁指数为: W − sj ;
第 i 头⽜的压扁指数为: W + wj − si ;
被压得最扁的那头奶⽜的压扁指数: k2 = max(W − sj
, W + wj − si)
由于 ⾥⾯都包含 ,可以统⼀删除。如果 在⽐ 在前⽐较好,也就是总体的压扁指数要
尽可能的⼩。那么得要求 ,也就是: 。
8.3 哈夫曼编码
主要记⼀下流程
cpp
#include <iostream>
#include <vector>
#include <queue>
using namespace std;
typedef long long LL;
const int N = 2e5 + 10;
int n;
priority_queue<LL, vector<LL>, greater<LL>> heap; // ⼩根堆
int main()
{
cin >> n;
for(int i = 1; i <= n; i++)
{
LL x; cin >> x;
heap.push(x);
}
LL len = 0;
while(heap.size() > 1)
{
// 每次拿出权值最⼩的两棵树合并
LL x = heap.top(); heap.pop();
LL y = heap.top(); heap.pop();
LL t = x + y;
len += t;
heap.push(t);
}
cout << len << endl;
return 0;
}
8.4 区间问题
P1803 凌乱的yyy / 线段覆盖
题目背景
Python 用户可以尝试使用 pypy3 提交试题。
快 noip 了,yyy 很紧张!
题目描述
现在各大 oj 上有 nnn 个比赛,每个比赛的开始、结束的时间点是知道的。
yyy 认为,参加越多的比赛,noip 就能考的越好(假的)。
所以,他想知道他最多能参加几个比赛。
由于 yyy 是蒟蒻,如果要参加一个比赛必须善始善终,而且不能同时参加 222 个及以上的比赛。
输入格式
第一行是一个整数 nnn,接下来 nnn 行每行是 222 个整数 ai,bi (ai<bi)a_{i},b_{i}\ (a_{i}<b_{i})ai,bi (ai<bi),表示比赛开始、结束的时间。
输出格式
一个整数最多参加的比赛数目。
输入输出样例 #1
输入 #1
3
0 2
2 4
1 3
输出 #1
2
说明/提示
- 对于 20%20\%20% 的数据,n≤10n \le 10n≤10;
- 对于 50%50\%50% 的数据,n≤103n \le 10^3n≤103;
- 对于 70%70\%70% 的数据,n≤105n \le 10^{5}n≤105;
- 对于 100%100\%100% 的数据,1≤n≤1061\le n \le 10^{6}1≤n≤106,0≤ai<bi≤1060 \le a_{i} < b_{i} \le 10^60≤ai<bi≤106。
cpp
#include <iostream>
#include <algorithm>
using namespace std;
typedef pair<int, int> PII;
const int N = 1e6 + 10;
int n;
PII a[N];
int main()
{
cin >> n;
for(int i = 1; i <= n; i++) cin >> a[i].first >> a[i].second;
sort(a + 1, a + 1 + n); // 默然按照⾸元素⽐较
int r = a[1].second, ret = 1;
for(int i = 2; i <= n; i++)
{
int x = a[i].first, y = a[i].second;
if(r <= x) // 没有重叠
{
ret++;
r = y;
}
else // 有重叠,就选右边界最⼩的那⼀个
{
r = min(r, y);
}
}
cout << ret << endl;
return 0;
}
1。按照左端升序排列
当区间重叠时,尽可能保留右端点较小的区间
当区间不重叠,以下一位区间为基准,继续判断
P2887 USACO07NOV Sunscreen G
题目描述
有 CCC 头奶牛进行日光浴,第 iii 头奶牛需要 minSPFiminSPFiminSPFi 到 maxSPFimaxSPFimaxSPFi 单位强度之间的阳光。
每头奶牛在日光浴前必须涂防晒霜,防晒霜有 LLL 种,涂上第 iii 种之后,身体接收到的阳光强度就会稳定为 SPFiSPFiSPFi,第 iii 种防晒霜有 covericovericoveri 瓶。
求最多可以满足多少头奶牛进行日光浴。
输入格式
第一行输入整数 CCC 和 LLL。
接下来的 CCC 行,按次序每行输入一头牛的 minSPFminSPFminSPF 和 maxSPFmaxSPFmaxSPF 值,即第 iii 行输入 minSPFiminSPFiminSPFi 和 maxSPFimaxSPFimaxSPFi。
再接下来的 LLL 行,按次序每行输入一种防晒霜的 SPFSPFSPF 和 covercovercover 值,即第 iii 行输入 SPFiSPFiSPFi 和 covericovericoveri。
每行的数据之间用空格隔开。
输出格式
输出一个整数,代表最多可以满足奶牛日光浴的奶牛数目。
输入输出样例 #1
输入 #1
3 2
3 10
2 5
1 5
6 2
4 1
输出 #1
2
说明/提示
样例解释:给第一头奶牛涂第一种防晒霜,第二头奶牛涂第二种防晒霜。
对于 100%100\%100% 的数据,1≤C,L≤25001\le C,L\le 25001≤C,L≤2500,1≤minSPFi≤maxSPFi≤10001\le minSPFi\le maxSPFi\le 10001≤minSPFi≤maxSPFi≤1000,1≤SPFi≤10001\le SPFi\le 10001≤SPFi≤1000,1≤coveri≤25001\le coveri\le 25001≤coveri≤2500。
cpp
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 2510;
int n, m;
struct node
{
int x, y;
}a[N], b[N];
bool cmp1(node& x, node& y)
{
return x.y < y.y;
}
bool cmp2(node& x, node& y)
{
return x.x < y.x;
}
bool cmp3(node& x, node& y)
{
return x.x > y.x;
}
// 所有区间按照右端点从⼩到⼤排序
// 所有点按照从⼩到⼤排序
void solve1()
{
sort(a + 1, a + 1 + n, cmp1);
sort(b + 1, b + 1 + m, cmp2);
int ret = 0;
for(int i = 1; i <= n; i++)
{
int l = a[i].x, r = a[i].y;
for(int j = 1; j <= m; j++)
{
// 选⼀个最⼩的,符合要求的点
int &sp = b[j].x, &cnt = b[j].y;
if(!cnt || sp < l) continue;
if(sp > r) break;
cnt--;
ret++;
break;
}
}
cout << ret << endl;
}
// 所有区间按照左端点从⼤到⼩排列
// 所有点从⼤到⼩排列
void solve2()
{
sort(a + 1, a + 1 + n, cmp3);
sort(b + 1, b + 1 + m, cmp3);
int ret = 0;
for(int i = 1; i <= n; i++)
{
int l = a[i].x, r = a[i].y;
for(int j = 1; j <= m; j++)
{
// 选⼀个最⼤的,符合要求的点
int &sp = b[j].x, &cnt = b[j].y;
if(!cnt || sp > r) continue;
if(sp < l) break;
ret++;
cnt--;
break;
}
}
cout << ret << endl;
}
int main()
{
cin >> n >> m;
for(int i = 1; i <= n; i++) cin >> a[i].x >> a[i].y;
for(int i = 1; i <= m; i++) cin >> b[i].x >> b[i].y;
// solve1(); // 按照右端点排序
solve2(); // 按照左端点排序
-
倍增思想
利⽤「⼆进制」以及「倍增」的思想,通过⼀个具体的例⼦说明,⽐如 3
11 :
• 幂运算的运算法则: a
b+c = a
b × a
c ;
• 通过⼀个数的⼆进制表⽰,可以写成若⼲数相加:
11 = (1011)2 = 1 × 2
3 + 0 × 2
2 + 1 × 2
1 + 1 × 2
0
• 两者结合: 3
11 = 3 = (1011)2 3
1×2
3+0×2
2+1×2
1+1×2
0 = 3
8 × 3
0 × 3
2 × 3
1
因此,当我们知道 之后,只⽤做⼏次乘法就能计算出 。 3
1
, 3
2
, 3
4
, 3
8
, ..., 3
log2
n
3
11
如何快速算出 。其实很简单,从前往后看,后⼀个数是前⼀个数的平⽅: 3
1
, 3
2
, 3
3
, ..., 3
log2
n
3
1 = 3
3
2 = 3
1 × 3
1 = 9
3
4 = 3
2 × 3
2 = 81
3
8 = 3
4 × 3
4 = 6561
因此,计算 3
11 ,我们只需将 11 的⼆进制表⽰中 1 所对应的幂乘起来即可。
如何实现这个算法呢,以 a
b 为例:
• ⼀边提取 b 的⼆进制表⽰中的每⼀位;
• ⼀边让 a = a ∗ a ,不断变成之前的平⽅(倍增的思想);
• 在提取 b 的⼆进制表⽰时,如果这⼀位是 1 ,就乘上对应位置的权值。
-
离散化
【算法原理】
当题⽬中数据范围很⼤,但是数据的总量不是很⼤,并且我们需要⽤数据的值来映射数组的下标时,
我们就可以⽤离散化的思想先预处理⼀下所有的数据,使的每⼀个数据都映射成⼀个范围较⼩的值。
⽐如: 9, 99, 9999, 999999 离散之后就变成 1, 2, 3, 4 。
cpp
#include<iostream>
#include<algorithm>
using namespace std;
const int N=1e5+10;
int n;
int a[N];
int pos;//标记去重过后的元素
int disc[N];
int find(int x)
{
int l=1,r=pos;
while(l<r)
{
int mid=(l+r)/2;
if(disc[mid]>=x) r=mid;
else l=mid+1;
}
return l;
}
int main()
{
cin>>n;
for(int i=1;i<=n;i++)
{
cin>>a[i];
disc[++pos]=a[i];
}
sort(disc+1,disc+1+pos);
pos=unique(disc+1,disc+1+pos)-(disc+1);
for(int i=1;i<=n;i++)
{
cout<<a[i]<<"离散化之后:"<<find(a[i])<<endl;
}
return 0;
}
cpp
#include<iostream>
#include<algorithm>
#include<unordered_map>
using namespace std;
const int N=1e5+10;
int n;
int a[N];
int pos;//标记去重过后的元素
int disc[N];
unordered_map<int,int> id;
int main()
{
cin>>n;
for(int i=1;i<=n;i++)
{
cin>>a[i];
disc[++pos]=a[i];
}
sort(disc+1,disc+1+pos);
int cnt=1;
for(int i=1;i<=pos;i++)
{
int x=disc[i];
if(id.count(x)) continue;
cnt++;
id[x]=cnt;
}
for(int i=1;i<=n;i++)
{
cout<<a[i]<<"离散化之后:"<<id[a[i]]<<endl;
}
return 0;
}
- 递归初阶
递归:函数⾃⼰调⽤⾃⼰。
在本⼩结⾥,我们会通过⼀些简单题⽬了解递归的本质。先带着⼤家消除对递归的恐惧,为我们后续
学习更⾼级的算法做铺垫。
P10457 占卜DIY
题目描述
lyd 学会了使用扑克 DIY 占卜。方法如下:一副去掉大小王的扑克共 525252 张,打乱后均分为 131313 堆,编号 1∼131\sim 131∼13,每堆 444 张,其中第 131313 堆称作"生命牌",也就是说你有 444 条命。这里边,444 张 KKK 被称作死神。
初始状态下,所有的牌背面朝上扣下。
流程如下:
- 抽取生命牌中的最上面一张(第一张)。
- 把这张牌翻开,正面朝上,放到牌上的数字所对应编号的堆的最上边。(例如抽到 222,正面朝上放到第 222 堆牌最上面,又比如抽到 JJJ,放到第 111111 堆牌最上边,注意是正面朝上放)
- 从刚放了牌的那一堆最底下(最后一张)抽取一张牌,重复第 222 步。(例如你上次抽了 222,放到了第二堆顶部,现在抽第二堆最后一张发现是 888,又放到第 888 堆顶部......)
- 在抽牌过程中如果抽到 KKK,则称死了一条命,就扔掉 KKK 再从第 111 步开始。
- 当发现四条命都死了以后,统计现在每堆牌上边正面朝上的牌的数目,只要同一数字的牌出现 444 张正面朝上的牌(比如 444 个 AAA),则称"开了一对",当然 444 个 KKK 是不算的。
- 统计一共开了多少对,开了 000 对称作"极凶",1∼21\sim 21∼2 对为"大凶",333 对为"凶",4∼54\sim 54∼5 对为"小凶",666 对为"中庸",7∼87\sim 87∼8 对"小吉",999 对为"吉",10∼1110\sim1110∼11 对为"大吉",121212 对为"满堂开花,极吉"。
如果还不明白,请参考样例。
输入格式
一共 131313 行,为每堆牌的具体牌是什么(不区分花色只区分数字),每堆输入的顺序为从上到下。
为了便于读入,用 000 代表 101010。
输出格式
输出一共开了多少对。
输入输出样例 #1
输入 #1
8 5 A A
K 5 3 2
9 6 0 6
3 4 3 4
3 4 4 5
5 6 7 6
8 7 7 7
9 9 8 8
9 0 0 0
K J J J
Q A Q K
J Q 2 2
A K Q 2
输出 #1
9
说明/提示
注解:第一条命死后前 121212 堆牌变成了这个样子:
A A A 8
K 5 3 2
3 9 6 0
4 4 3 4
5 5 3 4
6 6 5 6
7 7 7 7
8 8 8 9
9 9 0 0
0 K J J
J Q A Q//抽到这里的K死掉了
J Q 2 2
第二条命由于 KKK 在生命牌中,所以直接死掉,不变。
第三条命死后前 121212 堆牌变成了这个样子:
A A A A
2 2 2 K
3 3 3 3
4 4 4 4
5 5 5 5
6 6 6 6
7 7 7 7
8 8 8 8
9 9 9 9
0 0 0 0//抽到这里的K死掉了
J J J Q
Q Q J Q
第四条命死后前 121212 堆牌变成了这个样子:
A A A A
2 2 2 2//抽到这里的K死掉了
3 3 3 3
4 4 4 4
5 5 5 5
6 6 6 6
7 7 7 7
8 8 8 8
9 9 9 9
0 0 0 0
J J J Q
Q Q J Q
最后发现在所有已经正面朝上的牌中,A 2 3 4 5 6 7 8 10A\ 2\ 3\ 4\ 5\ 6\ 7\ 8\ 10A 2 3 4 5 6 7 8 10 这 999 对牌"开"了,(注意,第 999 堆牌中的最后一张虽然也是 999,但是并没有被翻开!所以不能算)因此输出 999。
cpp
#include<iostream>
using namespace std;
string s;
char check(int le,int rg)
{
char first=s[le];
for(int i=le+1;i<=rg;i++)
{
if(first!=s[i])
return 'F';
}
if(first=='1')
return 'I';
else
return 'B';
}
void bfs(int le,int rg)
{
if(le!=rg)
{
int mid = (le + rg) / 2;
bfs(le, mid);
bfs(mid + 1, rg);
}
char ans=check(le,rg);
cout<<ans;
}
int main()
{
int n;
cin>>n;
cin>>s;
bfs(0,s.size()-1);
return 0;
}
- 分治
分治,字⾯上的解释是「分⽽治之」,就是把⼀个复杂的问题分成两个或更多的相同或相似的⼦问
题,直到最后⼦问题可以简单的直接求解,原问题的解即⼦问题的解的合并。
P1908 逆序对
题目描述
猫猫 TOM 和小老鼠 JERRY 最近又较量上了,但是毕竟都是成年人,他们已经不喜欢再玩那种你追我赶的游戏,现在他们喜欢玩统计。
最近,TOM 老猫查阅到一个人类称之为"逆序对"的东西,这东西是这样定义的:对于给定的一段正整数序列,逆序对就是序列中 ai>aja_i>a_jai>aj 且 i<ji<ji<j 的有序对。知道这概念后,他们就比赛谁先算出给定的一段正整数序列中逆序对的数目。注意序列中可能有重复数字。
Update:数据已加强。
输入格式
第一行,一个数 nnn,表示序列中有 nnn 个数。
第二行 nnn 个数,表示给定的序列。序列中每个数字不超过 10910^9109。
输出格式
输出序列中逆序对的数目。
输入输出样例 #1
输入 #1
6
5 4 2 6 3 1
输出 #1
11
说明/提示
对于 25%25\%25% 的数据,n≤2500n \leq 2500n≤2500。
对于 50%50\%50% 的数据,n≤4×104n \leq 4 \times 10^4n≤4×104。
对于所有数据,1≤n≤5×1051 \leq n \leq 5 \times 10^51≤n≤5×105。
应该不会有人 O(n2)O(n^2)O(n2) 过 50 万吧 ------ 2018.8 chen_zhe。
cpp
#include <iostream>
using namespace std;
typedef long long LL;
const int N = 5e5 + 10;
int n;
int arr[N], tmp[N];
LL merge(int l, int r)
{
if(l >= r) return 0;
int mid = (l + r) / 2;
// [l, mid] [mid + 1, r]
LL ret = 0;
// 先去算算左右两部分逆序对的和,顺便给左右两边排排序
ret += merge(l, mid); ret += merge(mid + 1, r);
// 计算⼀左⼀右时,逆序对的个数
int cur1 = l, cur2 = mid + 1, i = l;
while(cur1 <= mid && cur2 <= r)
{
if(arr[cur1] <= arr[cur2])
{
tmp[i++] = arr[cur1++];
}
else // 右边元素⽐左边⼩时,产⽣逆序对
{
// 统计逆序对的个数
ret += (mid - cur1 + 1);
tmp[i++] = arr[cur2++];
}
}
while(cur1 <= mid) tmp[i++] = arr[cur1++];
while(cur2 <= r) tmp[i++] = arr[cur2++];
for(int j = l; j <= r; j++) arr[j] = tmp[j];
return ret;
}
int main()
{
cin >> n;
for(int i = 1; i <= n; i++) cin >> arr[i];
cout << merge(1, n) << endl;
// for(int i = 1; i <= n; i++) cout << arr[i] << " ";
return 0;
}