贪心算法
一、基本概念
贪心算法是从问题的初始状态出发,通过若干次的贪心选择而得到的最优值的一种求解策略,即贪心策略;
即为在对问题求解时,总是做出在当前看来是最好的选择,不从整体最优上加以考虑,所做出的仅是在某种意义上的局部最优解的算法;
贪心算法不是对所有问题都能得到整体最优解,关键是贪心策略的选择,选择的贪心策略必须具备无后效性,即,某个状态以前的过程不会影响以后的状态,只与当前状态有关的性质;
当一个问题的最优解包含其子问题的最优解时,称此问题具有最优子结构性质,问题的最优子结构性质是该问题可以用动态规划或者贪心算法求解的关键特征;
二、基本思路
-
建立数学模型来描述问题;
-
把求解的问题分成若干个子问题;
-
对每一子问题求解,得到子问题的局部最优解;
-
把子问题的解局部最优解合成原来解问题的一个解;
三、常见贪心算法类型
1. 最优选择问题
问题
给出 n n n 个物品,第 i i i 个物品重量为 w i w_i wi ,在总重量不超过 C C C 的情况下选择尽量多的物品,求最多可选物品数;
思路
由于只关心物品重量,又要选择尽量多的物品,所以应多选重量偏小的物品,因此可将所有物品按重量从小到大排序,依次选择每个物品,直到装不下为止;
代码
#include <cstdio>
#include <algorithm>
#define MAXN 10005
using namespace std;
int n, c, a[MAXN], tot = 1;
int main() {
scanf("%d %d", &n, &c);
for (int i = 1; i <= n; i++) {
scanf("%d", &a[i]);
}
sort(1 + a, 1 + a + n);
while (c > 0) {
c -= a[tot];
tot++;
}
if (c == 0) tot--;
else tot -= 2;
printf("%d", tot);
return 0;
}
2. 部分背包问题
问题
给出 n n n 个物品,第 i i i 个物品重量为 w i w_i wi ,价值为 c i c_i ci ,每一个物品可以取走一部分,价值与重量按比例计算,求在总重量不超过 C C C 的情况下总价值的最大值;
思路
此问题本质则为在最优选择问题的基础上加了价值项;
由于价值与重量按比例计算,即可优先选择单位重量下价值最大的,即通过价值除以重量排序,再依次选择,直到重量和为 C C C ;
注意,由于每个物品只能选择一部分,因此一定可以让总重量恰好为 C C C ,而且除了最后一个物品以外,其他物品要么不选要么全选;
代码
#include <cstdio>
#include <algorithm>
#define MAXN 105
using namespace std;
int n, c;
double ans = 0;
struct stone {
int w, c;
double p;
bool operator < (const stone a) const {
return p > a.p;
}
} a[MAXN];
int main() {
scanf("%d %d", &n, &c);
for (int i = 1; i <= n; i++) {
scanf("%d %d", &a[i].w, &a[i].c);
a[i].p = (double)1.0 * a[i].c / a[i].w;
}
sort (1 + a, 1 + a + n);
for (int i = 1; i <= n; i++) {
if (a[i].w <= c) {
c -= a[i].w;
ans += a[i].c;
} else {
ans += (double)c * a[i].p;
break;
}
}
printf("%.2lf\n", ans);
return 0;
}
3. 乘船问题
问题
有 n n n 个人,第 i i i 个人重量为 w i w_i wi ,每艘船载重为 C C C ,最多可乘 2 个人,现在想用最少的船将所有人运走,问船的数量;
思路
由于要用最少的船,即每一只船应载最多的重量,则将所有人按重量从小到大排序,依次考虑最轻的人 i i i ,若剩下的每个人都不能与他一起乘船,则剩下的只能每个人都乘一条船,否则,其能选择能与他一起乘的人中重量最大的一个 j j j ,使得眼前的浪费最少;
代码
#include <cstdio>
#include <algorithm>
#define MAXN 300005
using namespace std;
int n, c, a[MAXN], ans = 0;
int main() {
scanf("%d %d", &n, &c);
for (int i = 1; i <= n; i++) {
scanf("%d", &a[i]);
}
sort(1 + a, 1 + a + n);
int i = 1, j = n;
while (i <= j) {
ans++;
if (a[i] + a[j] <= c || i == j) {
i++;
}
j--;
}
printf("%d", ans);
return 0;
}
4. 选择不相交区间问题
问题
给定 n n n 个开区间 ( a , b ) (a, b) (a,b) ,选择尽量多个区间,使得这些区间两两没有公共点,求选择的区间数量;
思路
将每个区间按结束时间从小到大排序,最初选择结束时间最早的活动 tot = r[1]
,然后每次考虑最早的开始时间 l[i]
,如果比当前选择的区间的结束时间要晚,即 l[i] > tot
,那么就选择这个区间,有 tot = r[i], ans++
,直到遍历完所有区间为止;
代码
#include <cstdio>
#include <algorithm>
#define MAXN 1000005
using namespace std;
int n, ans = 0, tot;
struct line {
int l, r;
bool operator < (const line a) const {
return r < a.r;
}
} a[MAXN];
int main() {
scanf("%d", &n);
for (int i = 1; i <= n; i++) {
scanf("%d %d", &a[i].l, &a[i].r);
}
sort(1 + a, 1 + a + n);
tot = a[1].r;
ans++;
for (int i = 2; i <= n; i++) {
if (a[i].l >= tot) {
tot = a[i].r;
ans++;
}
}
printf("%d", ans);
return 0;
}
5. 区间选点问题
问题
给定 n n n 个闭区间 [ a , b ] [a,b] [a,b] ,在数轴上选尽量少的点,使得每个区间 i i i 内都至少有 k i k_i ki 个点,求最少点的数量;
思路
由于要求点的数量最少,则应将点尽量放在区间的交汇处;
首先按照区间右端点从小到大排序,然后从区间 1 到区间 n 进行选择,对于当前区间,若集合中的数不能覆盖,则将集合末尾的数加入需要覆盖的集合即可;
代码
#include <cstdio>
#include <algorithm>
#define MAXN 30005
using namespace std;
int n, h, tot, ans;
bool tree[MAXN];
struct que {
int l, r, t;
bool operator < (const que a) const {
if (r == a.r) return l < a.l;
return r < a.r;
}
} a[MAXN];
int main() {
scanf("%d %d", &n, &h);
for (int i = 1; i <= h; i++) {
scanf("%d %d %d", &a[i].l, &a[i].r, &a[i].t);
}
sort(1 + a, 1 + a + h);
for (int i = 1; i <= h; i++) {
tot = 0;
for (int j = a[i].l; j <= a[i].r; j++) if (tree[j]) tot++;
if (tot < a[i].t) {
for (int j = a[i].r; j >= a[i].l; j--) {
if (!tree[j]) {
tree[j] = 1;
tot++;
ans++;
if (tot == a[i].t) break;
}
}
}
}
printf("%d", ans);
return 0;
}
6. 区间覆盖问题
问题
给 n n n 个闭区间 [ a , b ] [a,b] [a,b] ,选择尽量少的区间覆盖一条指定的线段区间 [ s , t ] [s,t] [s,t] ,求区间个数;
思路
由于要求区间最少,则每一个区间覆盖范围应尽量大,重合部分应尽量少;
将所有的区间按左端点从小到大排序,依次处理每个区间,每次选择覆盖点 s s s 的区间中右端点坐标最大的一个,并将 s s s 更新为该区间的右端点坐标,直到选择的区间包含 t t t 为止;
代码
#include <cstdio>
#include <algorithm>
#define MAXN 1000005
using namespace std;
int n, s, t, tot, ans, len = 0;
struct line {
int l, r;
bool operator < (const line a) const {
if (l == a.l) return r > a.r;
return l < a.l;
}
} a[MAXN];
int main() {
int m;
scanf("%d %d %d", &m, &s, &t);
for (int i = 1; i <= m; i++) {
int x, y;
scanf("%d %d", &x, &y);
if ((x < s && y < s) || (x > t && y > t)) {
continue;
} else {
a[++n].l = x;
a[n].r = y;
}
}
sort(1 + a, 1 + a + n);
tot = len = s;
while(tot < t) {
int max = 0, tot1 = -1;
for (int i = 1; i <= n; i++) {
if (a[i].l <= tot) {
if (a[i].r - tot > max) {
max = a[i].r - tot;
tot1 = i;
}
} else {
break;
}
}
if (tot1 == -1) {
printf("No Solution");
return 0;
} else {
len += max;
ans++;
tot = a[tot1].r;
}
}
printf("%d", ans);
return 0;
}
7. 带限期与罚款的单位时间任务调度
问题
n n n 个任务,每个任务需要一个单位时间执行,任务 i i i 的截止时间 d i d_i di 表示任务 i i i 在时间 d i d_i di 前必须完成,误时罚款 w i w_i wi 表示任务 i i i 若未在 d i d_i di 前完成,导致 w i w_i wi 的罚款,确定所有任务的执行顺序使得罚款最少,求获得最多的钱数;
思路
要使罚款最少,就尽量先完成 w [ i ] w[i] w[i] 值较大的任务,则将任务按 w [ i ] w[i] w[i] 从大到小排序,按排好的顺序对任务进行安排,使得处理任务 i i i 的时间在 d [ i ] d[i] d[i] 之内又尽量靠后,如果 d [ i ] d[i] d[i] 时间之内的时间都已排满,就放弃处理该任务;
代码
#include <cstdio>
#include <algorithm>
#define MAXN 505
using namespace std;
int m, n;
bool flag[MAXN];
struct project {
int t, p;
bool operator < (const project a) const {
return p > a.p;
}
} a[MAXN];
int main() {
scanf("%d %d", &m, &n);
for (int i = 1; i <= n; i++) {
scanf("%d", &a[i].t);
}
for (int i = 1; i <= n; i++) {
scanf("%d", &a[i].p);
}
sort(1 + a, 1 + a + n);
for (int i = 1; i <= n; i++) {
int tot = -1;
for (int j = 1; j <= a[i].t; j++) {
if (flag[j] == false) {
tot = j;
}
}
if (tot == -1) {
m -= a[i].p;
} else {
flag[tot] = true;
}
}
printf("%d", m);
return 0;
}