贪心算法
1. 贪心原理
原理
- 不同于动态规划,一般来说动态规划每次会有多种决策方式;贪心算法每次贪心的选择一种决策方式,我们要保证这样的贪心做法能得到最优解,这一般是需要我们证明的。这也是贪心最难的地方。
2. AcWing上的贪心算法题目
AcWing 905. 区间选点
问题描述
-
问题链接:AcWing 905. 区间选点
分析
-
本题的步骤如下:
(1)将每个区间按照右端点从小到大排序;
(2)从前向后依次枚举每个区间:如果当前区间已经包含点,直接继续考察下一个点;否则,选择当前区间的右端点为选出的点。
-
证明:设上述做法得到的点数为
cnt
,最优解对应的点数为ans
,则一定有cnt>=ans
;另外按照上面的贪心做法选出的点所有的区间一定没有交集,否则有交集的话就不会选择该区间的右端点了,因此覆盖这些区间至少需要cnt
个点,所以ans>=cnt
;因此有cnt==ans
。如下图:
代码
- C++
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 100010;
int n;
struct Range {
int l, r;
bool operator< (const Range &w) const {
return r < w.r;
}
} range[N];
int main() {
scanf("%d", &n);
for (int i = 0; i < n; i++) {
int l, r;
scanf("%d%d", &l, &r);
range[i] = {l, r};
}
sort(range, range + n);
int res = 0, ed = -2e9;
for (int i = 0; i < n; i++)
if (range[i].l > ed) {
res++;
ed = range[i].r;
}
printf("%d\n", res);
return 0;
}
AcWing 908. 最大不相交区间数量
问题描述
分析
-
这一题的解题过程和AcWing 905. 区间选点完全一样,代码也完全一样。下面是证明:
-
假设贪心做法选择出了
cnt
个点(按照905
的做法,这cnt
个点一定完全覆盖了所有区间),最优解中选出了ans
个点,则ans>=cnt
;下面证明ans<=cnt
,反证法,如果ans>cnt
,那么至少需要ans
个点才能覆盖这些区间,和用cnt
个点就一定可以覆盖了所有区间矛盾,因此假设不成立,所以有cnt==ans
。
代码
- C++
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 100010;
int n;
struct Range {
int l, r;
bool operator< (const Range &w) const {
return r < w.r;
}
} range[N];
int main() {
scanf("%d", &n);
for (int i = 0; i < n; i++) {
int l, r;
scanf("%d%d", &l, &r);
range[i] = {l, r};
}
sort(range, range + n);
int res = 0, ed = -2e9;
for (int i = 0; i < n; i++)
if (range[i].l > ed) {
res++;
ed = range[i].r;
}
printf("%d\n", res);
return 0;
}
AcWing 906. 区间分组
问题描述
-
问题链接:AcWing 906. 区间分组
分析
-
本题的做法如下:
(1)将每个区间按照左端点从小到大排序;
(2)从前向后依次枚举每个区间:记录每组中最右侧的端点,如果当前考察的区间的左端点为
l
,判断是否存在一组中区间最右侧的端点小于l
,如果存在,可以将这个区间加入到这一组中;否则不存在的话,说明这个区间和所有的组都存在交集,只能新开一组,同时将这新的一组右端点记录下来。 -
为了判断是否存在一组中区间最右侧的端点小于
l
,可以使用小根堆记录每组的右端点。 -
下面证明上述做法是正确的,假设最优解分组为
ans
,我们贪心解分组为cnt
,则ans<=cnt
;下面证明ans>=cnt
,考虑我们的贪心做法已经得到了cnt-1
组,则第一次得到第cnt
个组对应的区间一定和前面的cnt-1
个组有交集(否则按照我们的算法不会新开一组),因此至少需要开cnt
组,所以ans>=cnt
,这就证明了cnt==ans
。
代码
- C++
#include <iostream>
#include <algorithm>
#include <queue>
using namespace std;
const int N = 100010;
int n;
struct Range {
int l, r;
bool operator< (const Range &w) const {
return l < w.l;
}
} range[N];
int main() {
scanf("%d", &n);
for (int i = 0; i < n; i++) {
int l, r;
scanf("%d%d", &l, &r);
range[i] = {l, r};
}
sort(range, range + n);
priority_queue<int, vector<int>, greater<int>> heap;
for (int i = 0; i < n; i++) {
auto r = range[i];
if (heap.empty() || heap.top() >= r.l) heap.push(r.r);
else {
heap.pop();
heap.push(r.r);
}
}
printf("%d\n", heap.size());
return 0;
}
AcWing 907. 区间覆盖
问题描述
-
问题链接:AcWing 907. 区间覆盖
分析
-
本题的做法如下:
(1)将每个区间按照左端点从小到大排序;
(2)从前向后依次枚举每个区间:在所有能覆盖
start
的区间中,选择右端点最大的区间,然后将start
更新成右端点的最大值。 -
可以证明最优解通过替换一定可以替换为贪心解。因此贪心解就是最优解
代码
- C++
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 100010;
int st, ed;
int n;
struct Range {
int l, r;
bool operator< (const Range &w) const {
return l < w.l;
}
} range[N];
int main() {
scanf("%d%d", &st, &ed);
scanf("%d", &n);
for (int i = 0; i < n; i++) {
int l, r;
scanf("%d%d", &l, &r);
range[i] = {l, r};
}
sort(range, range + n);
int res = 0;
bool success = false;
for (int i = 0; i < n; i++) {
int j = i, r = -2e9;
while (j < n && range[j].l <= st) {
r = max(r, range[j].r);
j++;
}
if (r < st) {
res = -1;
break;
}
res++;
if (r >= ed) {
success = true;
break;
}
st = r;
i = j - 1;
}
if (!success) res = -1;
printf("%d\n", res);
return 0;
}
AcWing 148. 合并果子
问题描述
-
问题链接:AcWing 148. 合并果子
分析
- 经典哈夫曼树的模型,每次合并重量最小的两堆果子即可。
代码
- C++
#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);
}
int 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("%d\n", res);
return 0;
}
AcWing 913. 排队打水
问题描述
-
问题链接:AcWing 913. 排队打水
分析
-
结论是:按照从小到大的顺序排队,总时间最小。
-
假设有
n
个人,每个人的打水时间为t[1]、t[2]、......、t[n]
,则总等待时间为T = t[1]x(n-1) + t[2]x(n-2) + ... + t[n] x 0
。只有这个序列时升序排列的时候,这个T
才能最小。假设不成立的话,一定至少存在一对逆序(假设为a、b
,且a<b
),假设需要的时间为T'
,如果存在一对逆序则T'-T=b-a>0
,存在更多逆序对,则T'
会更大。
代码
- C++
#include <iostream>
#include <algorithm>
using namespace std;
typedef long long LL;
const int N = 100010;
int n;
int a[N];
int main() {
scanf("%d", &n);
for (int i = 1; i <= n; i++) scanf("%d", &a[i]);
sort(a + 1, a + n + 1);
LL res = 0;
for (int i = 1; i <= n; i++) res += a[i] * (n - i);
printf("%lld\n", res);
return 0;
}
AcWing 104. 货仓选址
问题描述
-
问题链接:AcWing 104. 货仓选址
分析
-
对于
x、y
,如果任选一个点a
,则有 ∣ x − a ∣ + ∣ y − a ∣ ≥ ∣ x − y ∣ |x-a|+|y-a| \ge |x-y| ∣x−a∣+∣y−a∣≥∣x−y∣。当a
在x、y
之间等号成立。 -
因此本题我们可以将数组升序排列,然后第一个数和最后一个数一组,第二个数和倒数第二个数一组,然后每两个数中间的点可以随便选,这里选择所有组数的交集。因此当数组元素为奇数个时,变为中位数即可;当为偶数个数时,假设中间两个数为
a, b(a<b)
,选取[a, b]
中间任意一个数都行。
代码
- C++
#include <iostream>
#include <algorithm>
using namespace std;
typedef long long LL;
const int N = 100010;
int n;
int a[N];
int main() {
scanf("%d", &n);
for (int i = 0; i < n; i++) scanf("%d", &a[i]);
sort(a, a + n);
LL res = 0;
for (int i = 0; i < n; i++) res += abs(a[i] - a[n / 2]);
printf("%lld\n", res);
return 0;
}
AcWing 125. 耍杂技的牛
问题描述
-
问题链接:AcWing 125. 耍杂技的牛
分析
-
结论:按照
w+s
从小到大(顶部是最小的)的顺序排列,最大的危险系数一定是最小的。可以使用反证法证明。 -
假设存在两头相邻的牛不是按照上述规则排序,例如第
i
头牛和第i+1
头牛,根据假设有w[i]+s[i]>w[i+1]+s[i+1]
,则交换前后,两头牛的危险系数如下图:
- 为了对比方便,将上图中的四项都加上
-(w[1]+w[2]+...+w[i-1])+s[i]+s[i+1]
,可以得到:
-
可以看到,交换后危险系数降低了,因此结论是正确的。
-
另外还要考虑如果
w[i]+s[i]==w[i+1]+s[i+1]
,较重的牛应该在下面,因为这是s
是危险系数,w
越大,s
越小,危险系数越小。
代码
- C++
#include <iostream>
#include <algorithm>
#define x first
#define y second
using namespace std;
typedef pair<int, int> PII;
const int N = 50010;
int n;
PII a[N];
int main() {
scanf("%d", &n);
for (int i = 0; i < n; i++) {
int w, s;
scanf("%d%d", &w, &s);
a[i] = {w + s, w};
}
sort(a, a + n);
int res = -1e9;
for (int i = 0, sum = 0; i < n; i++) {
int w = a[i].y, s = a[i].x - w;
res = max(res, sum - s);
sum += w;
}
printf("%d\n", res);
return 0;
}