贪心题型困难之处在于既无固定模板也无固定套路
往往需要先猜(做题经验很重要),然后证明方法的正确性(平时靠数学证明,笔试靠提交AC哈哈哈)

区间问题

AcWing 905. 区间选点

难度:简单

题目描述

给定N个闭区间[ai,bi],请你在数轴上选择尽量少的点,使得每个区间内至少包含一个选出的点。

输出选择的点的最小数量。

位于区间端点上的点也算作区间内。

输入格式

第一行包含整数N,表示区间数。

接下来N行,每行包含两个整数ai,bi,表示一个区间的两个端点。

输出格式

输出一个整数,表示所需的点的最小数量。

数据范围

1N105,1≤N≤10^5,
109aibi109−10^9≤ai≤bi≤10^9

1
2
3
4
5
6
7
8
9
10
输入样例:

3
-1 1
2 4
3 5

输出样例:

2

贪心

做法

  1. 将每个区间按右端点从小到大排序
  2. 从前往后枚举每个区间
    若已包含点,则pass
    否则(区间左端点 > 该点),选择当前区间的右断端点

证明:求得点数为cnt,答案数为ans,即证cnt = ans

  1. ans <= cnt: 方案可行,且ans可行方案中最小点数的方案

  2. ans >= cnt:

    点分布在cnt个互不连接的区间上,要覆盖这cnt个区间,需要的点数ans >= 区间数cnt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include <iostream>
#include <algorithm>

using namespace std;

const int kN = 1e5 + 10;

struct Range {
int l, r;
// 务必掌握 结构体重载运算符 的写法
bool operator< (const Range &W) const {
return r < W.r;
}
}ranges[kN];

int main() {
int n;
scanf("%d", &n);
for (int i = 0; i < n; i++) {
scanf("%d%d", &ranges[i].l, &ranges[i].r);
}
sort(ranges, ranges + n); // 别忘sort()
int res = 0, ed = -2e9;
for (int i = 0; i < n; i++) {
if (ed < ranges[i].l) {
res++;
ed = ranges[i].r;
}
}
printf("%d\n", res);
return 0;
}

AcWing 908. 最大不相交区间数量

难度:简单

题目描述

给定N个闭区间[ai,bi],请你在数轴上选择若干区间,使得选中的区间之间互不相交(包括端点)。

输出可选取区间的最大数量。

输入格式

第一行包含整数N,表示区间数。

接下来N行,每行包含两个整数ai,bi,表示一个区间的两个端点。

输出格式

输出一个整数,表示可选取区间的最大数量。

数据范围

1N105,1≤N≤10^5,
109aibi109−10^9≤ai≤bi≤10^9

1
2
3
4
5
6
7
8
9
10
输入样例:

3
-1 1
2 4
3 5

输出样例:

2

贪心

做法与上一题一致。

证明:

  1. ans >= cnt 做法可行,且ans为max的可行

  2. ans <= cnt

    z若存在这ans个两两不相交的区间,那么要使得每个区间上至少一个点,至少需要cnt个点,那么cnt >= ans

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include <iostream>
#include <algorithm>

using namespace std;

const int kN = 1e5 + 10;

struct Range {
int l, r;
bool operator< (const Range &W) const {
return r < W.r;
}
}ranges[kN];

int main() {
int n;
scanf("%d", &n);
for (int i = 0; i < n; i++) {
scanf("%d%d", &ranges[i].l, &ranges[i].r);
}

sort(ranges, ranges + n);
int res = 0, ed = -2e9;
for (int i = 0; i < n; i++) {
if (ed < ranges[i].l) {
res++;
ed = ranges[i].r;
}
}
printf("%d\n", res);
return 0;
}

AcWing 906. 区间分组

给定 N 个闭区间 [ai,bi][a_i, b_i],请你将这些区间分成若干组,使得每组内部的区间两两之间(包括端点)没有交集,并使得组数尽可能小。

输出最小组数。

输入格式
第一行包含整数 N,表示区间数。

接下来 N 行,每行包含两个整数 ai, bia_i,\ b_i,表示一个区间的两个端点。

输出格式
输出一个整数,表示最小组数。

数据范围

1N1051\le N \le 10^5

109aibi109-10^9 \le a_i \le b_i \le 10^9

输入样例:

1
2
3
4
3
-1 1
2 4
3 5

输出样例:

1
2

贪心

做法

  1. 区间按左端点排序

  2. 从前往后处理每个区间

    判断是否能将其放到某个现有的组中 l[i] > Max_r

    1. 若不存在这样的组,则开新组,然后将其放入

    2. 若存在,则将其放入,并更新当前组的Max_r

      若存在多个,则随便挑一个放入就行

证明

  1. ans <= cnt 合法且…

  2. ans >= cnt

    在即将产生第cnt个分组的时刻,这时l[i]大于所有组的Max_r

    即存在cnt个相互有重叠的区间,所以ans >= cnt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include <iostream>
#include <algorithm>
#include <queue>

using namespace std;

const int kN = 1e5 + 10;

struct Range {
int l, r;
bool operator< (const Range &W) const {
return l < W.l;
}
}ranges[kN];

int main() {
int n;
scanf("%d", &n);
for (int i = 0; i < n; i++) {
scanf("%d%d", &ranges[i].l, &ranges[i].r);
}
sort(ranges, ranges + n);
priority_queue<int, vector<int>, greater<int>> heap;
for (int i = 0; i < n; i++) {
if (heap.empty() || heap.top() >= ranges[i].l) {
heap.push(ranges[i].r);
} else {
heap.pop();
heap.push(ranges[i].r);
}
}
printf("%d\n", heap.size());// heap.size()为结果
return 0;
}

AcWing 907. 区间覆盖

给定 N 个闭区间 [ai,bi][a_i,b_i] 以及一个线段区间 [s,t][s,t],请你选择尽量少的区间,将指定线段区间完全覆盖。

输出最少区间数,如果无法完全覆盖则输出 −1。

输入格式

第一行包含两个整数 s 和 t,表示给定线段区间的两个端点。

第二行包含整数 N,表示给定区间数。

接下来 N 行,每行包含两个整数 ai, bia_i,\ b_i,表示一个区间的两个端点。

输出格式

输出一个整数,表示所需最少区间数。

如果无解,则输出 −1。

数据范围

1N105,1\le N \le 10^5,

109ab109,-10^9\le a\le b\le 10^9,

109ab109-10^9\le a\le b\le 10^9

输入样例:

1
2
3
4
5
1 5
3
-1 3
2 4
3 5

输出样例:

1
2

贪心

做法

  1. 将所有区间按左端点从小到大排序
  2. 从前往后枚举每个区间
    在所有能覆盖start的区间中,选择右端点最大的区间
    然后将start更新为右端点的最大值

证明

  1. ans <= cnt 合法且ans为…

  2. ans >= cnt

    调整法:找到ans对应选择的区间与cnt对应区间的第一个不同的区间,一定能将ans中这个区间替换为cnt方案中的区间,替换后循环下去…
    最后ans对应方案和cnt中完全一致

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
#include <iostream>
#include <algorithm>

using namespace std;

const int kN = 1e5 + 10;

struct Range {
int l, r;
bool operator< (const Range &W) const {
return l < W.l;
}
}ranges[kN];

int main() {
int st, ed;
scanf("%d%d", &st, &ed);
int n;
scanf("%d", &n);
for (int i = 0; i < n; i++) {
scanf("%d%d", &ranges[i].l, &ranges[i].r);
}
sort(ranges, ranges + n);
int res = 0;
bool success = false;
for (int i = 0; i < n; i++) {
int j = i, r = -2e9;
while (j < n && ranges[j].l <= st) {
r = max(r, ranges[j].r);
j++;
}
if (r < st) {
res = -1;
break;
}
res++;
if (r >= ed) {
success = true; // 一定要success, 循环结束后可能还没覆盖ed
break;
}
st = r;
i = j - 1;
}
if (!success) res = -1;
printf("%d\n", res);
return 0;
}

Huffman树

AcWing 148. 合并果子

与DP章中“合并石子”不同指出在于:合并石子必须是相邻两堆,而合并果子可以不相邻。

在一个果园里,达达已经将所有的果子打了下来,而且按果子的不同种类分成了不同的堆。

达达决定把所有的果子合成一堆。

每一次合并,达达可以把两堆果子合并到一起,消耗的体力等于两堆果子的重量之和。

可以看出,所有的果子经过 n−1 次合并之后,就只剩下一堆了。

达达在合并果子时总共消耗的体力等于每次合并所耗体力之和。

因为还要花大力气把这些果子搬回家,所以达达在合并果子时要尽可能地节省体力。

假定每个果子重量都为 1,并且已知果子的种类数和每种果子的数目,你的任务是设计出合并的次序方案,使达达耗费的体力最少,并输出这个最小的体力耗费值。

例如有 3 种果子,数目依次为 1,2,9。

可以先将 1、2 堆合并,新堆数目为 3,耗费体力为 3。

接着,将新堆与原先的第三堆合并,又得到新的堆,数目为 12,耗费体力为 12。

所以达达总共耗费体力=3+12=15。

可以证明 15 为最小的体力耗费值。

输入格式

输入包括两行,第一行是一个整数 n,表示果子的种类数。

第二行包含 n 个整数,用空格分隔,第 i 个整数 aia_i 是第 i 种果子的数目。

输出格式

输出包括一行,这一行只包含一个整数,也就是最小的体力耗费值。

输入数据保证这个值小于 231。

数据范围

1b10000,1\le b\le 10000,

1ai200001\le a_i\le 20000

输入样例:

1
2
3 
1 2 9

输出样例:

1
15

贪心

做法

每次选择重量最小的2堆合并

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <iostream>
#include <queue>

using namespace std;

int main() {
int n;
scanf("%d", &n);
priority_queue<int, vector<int>, greater<int>> heap;
for (int i = 0; i < n; i++) {
int x;
scanf("%d", &x);
heap.push(x);
}
long long res = 0;
while (heap.size() > 1) {
int a = heap.top(); heap.pop();
int b = heap.top(); heap.pop();
res += (a + b);
heap.push(a + b);
}
printf("%lld\n", res); // %lld
return 0;
}

排序不等式

AcWing 913. 排队打水

有 n 个人排队到 1 个水龙头处打水,第 i 个人装满水桶所需的时间是 tit_i,请问如何安排他们的打水顺序才能使所有人的等待时间之和最小?

输入格式

第一行包含整数 n。

第二行包含 n 个整数,其中第 i 个整数表示第 i 个人装满水桶所花费的时间 tit_i

输出格式

输出一个整数,表示最小的等待时间之和。

数据范围

1n105,1\le n\le 10^5,

1ti1041\le t_i\le 10^4

输入样例:

1
2
7
3 6 1 4 2 5 7

输出样例:

1
56

贪心

做法 用时短的放在前面。(结合公式 + 调整法易证)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <iostream>
#include <algorithm>

using namespace std;

const int kN = 1e5 + 10;

int a[kN];

int main() {
int n;
scanf("%d", &n);
for (int i = 0; i < n; i++) {
scanf("%d", &a[i]);
}
sort(a, a + n);
long long res = 0;
for (int i = 0; i < n; i++) {
// 下标为i的第(i+1)个人后会有(n-i-1)个人需要等这个a[i]时间
res += a[i] * (n - i - 1);
}
printf("%lld\n", res);
return 0;
}

绝对值不等式

AcWing 104. 货仓选址

在一条数轴上有 N 家商店,它们的坐标分别为 A1ANA_1\sim A_N

现在需要在数轴上建立一家货仓,每天清晨,从货仓到每家商店都要运送一车商品。

为了提高效率,求把货仓建在何处,可以使得货仓到每家商店的距离之和最小。

输入格式

第一行输入整数 N。

第二行 N 个整数 A1ANA_1\sim A_N

输出格式

输出一个整数,表示距离之和的最小值。

数据范围

1N100000,1\le N\le 100000,

0Ai400000\le A_i\le 40000

输入样例:

1
2
4
6 2 9 1

输出样例:

1
12

贪心

做法 取中点作为仓库

对于奇数来说,中点为最中间的点
对于偶数来说,中点可以取最中间的两个点中的任意一个,或者二者之间任意点皆可

区间[left, right]的中点为(left + right) >> 1, 因此这里取a[n >> 1]为中点即可

分析

f(x)=xx1+xx2++xxn=(xx1+xx2)+(xx2+xxn1)+(xnx1)+(xn1x2)+\begin{align} f(x) &= |x-x_1|+|x-x_2|+\cdots + |x-x_n|\\ &=(|x-x_1| + |x-x_2|) + (|x-x_2|+|x-x_{n-1}|) +\cdots\\ &\ge(x_n - x_1)+(x_{n-1}-x_2)+\cdots \\ \end{align}

当x取重点时都能取到等号,所以\ge后的值即为最小值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <iostream>
#include <algorithm>

using namespace std;

const int kN = 1e5 + 10;

int a[kN];

int main() {
int n;
scanf("%d", &n);
for (int i = 0; i < n; i++) {
scanf("%d", &a[i]);
}
sort(a, a + n);
long long res = 0;
for (int i = 0; i < n; i++) {
res += abs(a[i] - a[n >> 1]);
}
printf("%lld\n", res);
return 0;
}

推公式

AcWing 125. 耍杂技的牛

农民约翰的 N 头奶牛(编号为 1…N)计划逃跑并加入马戏团,为此它们决定练习表演杂技。

奶牛们不是非常有创意,只提出了一个杂技表演:

叠罗汉,表演时,奶牛们站在彼此的身上,形成一个高高的垂直堆叠。

奶牛们正在试图找到自己在这个堆叠中应该所处的位置顺序。

这 N 头奶牛中的每一头都有着自己的重量 WiW_i 以及自己的强壮程度 SiS_i

一头牛支撑不住的可能性取决于它头上所有牛的总重量(不包括它自己)减去它的身体强壮程度的值,现在称该数值为风险值,风险值越大,这只牛撑不住的可能性越高。

您的任务是确定奶牛的排序,使得所有奶牛的风险值中的最大值尽可能的小。

输入格式

第一行输入整数 N,表示奶牛数量。

接下来 N 行,每行输入两个整数,表示牛的重量和强壮程度,第 i 行表示第 i 头牛的重量 WiW_i 以及它的强壮程度 SiS_i

输出格式

输出一个整数,表示最大风险值的最小可能值。

数据范围

1N50000,1≤N≤50000,
1Wi10,000,1≤Wi≤10,000,
1Si1,000,000,0001≤Si≤1,000,000,000

输入样例:

1
2
3
4
3
10 3
2 5
3 3

输出样例:

1
2

贪心

做法 按照wi+siw_i+s_i从小到大排序,最大的危险系数一定是最小的。

思路:这TM谁能想到?(
每头牛的危险值 = 它上面牛的重量和 - 自身的强壮值
要使得某头牛的危险值最小,显然与w和s都有关……然后就靠猜了

证明 采用调整法

若不按照上述方法,一定会存在wi+si>wi+1+si+1w_i+s_i > w_{i+1} + s_{i+ 1}

关注这i和(i+1)层交换前后牛的危险系数(其他位置不受影响):

第i位置的牛 第i+1位置的牛
交换前 j=1i1wjsi\sum_{j=1}^{i-1}w_j-s_i j=1i1wj+wisi+1\sum_{j=1}^{i-1}w_j+w_i-s_{i+1}
交换后 j=1i1wjsi+1\sum_{j=1}^{i-1}w_j-s_{i+1} j=1i1wj+wi+1si\sum_{j=1}^{i-1}w_j+w_{i+1}-s_i

每个式子将去j=1i1wj\sum_{j=1}^{i-1}w_j后,再加上si+si+1s_i+s_{i+1}得到:

第i位置的牛 第i+1位置的牛
交换前 si+1s_{i+1} wi+siw_i+s_i
交换后 sis_i wi+1+si+1w_{i+1}+s_{i+1}

由于 wi+si>max(si,wi+1+si+1)w_i+s_i > max(s_i, w_{i+1}+s_{i+1})

所以 max(si+1,wi+si)>max(si,wi+1+si+1)max(s_{i+1}, w_i+s_i) > max(s_i, w_{i+1}+s_{i+1})

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <iostream>
#include <algorithm>

using namespace std;

typedef pair<int, int> PII;

const int kN = 5e5 + 10;

PII cows[kN];

int main() {
int n;
scanf("%d", &n);
for (int i = 0; i < n; i++) {
int w, s;
scanf("%d%d", &w, &s);
cows[i] = {w + s, s};
}
sort(cows, cows + n);
int res = -2e9, sum = 0; // res初始化要足够小: 最大危险系数可能为大负数
for (int i = 0; i < n; i++) {
int w = cows[i].first - cows[i].second, s = cows[i].second;
res = max(res, sum - s);
sum += w;
}
printf("%d\n", res);
return 0;
}