贪心算法总结

贪心算法

一、基本概念

贪心算法是从问题的初始状态出发,通过若干次的贪心选择而得到的最优值的一种求解策略,即贪心策略;

即为在对问题求解时,总是做出在当前看来是最好的选择,不从整体最优上加以考虑,所做出的仅是在某种意义上的局部最优解的算法;

贪心算法不是对所有问题都能得到整体最优解,关键是贪心策略的选择,选择的贪心策略必须具备无后效性,即,某个状态以前的过程不会影响以后的状态,只与当前状态有关的性质;

当一个问题的最优解包含其子问题的最优解时,称此问题具有最优子结构性质,问题的最优子结构性质是该问题可以用动态规划或者贪心算法求解的关键特征;

二、基本思路

  1. 建立数学模型来描述问题;

  2. 把求解的问题分成若干个子问题;

  3. 对每一子问题求解,得到子问题的局部最优解;

  4. 把子问题的解局部最优解合成原来解问题的一个解;

三、常见贪心算法类型

1. 最优选择问题

问题

给出 nnn 个物品,第 iii 个物品重量为 wiw_iwi ,在总重量不超过 CCC 的情况下选择尽量多的物品,求最多可选物品数;

思路

由于只关心物品重量,又要选择尽量多的物品,所以应多选重量偏小的物品,因此可将所有物品按重量从小到大排序,依次选择每个物品,直到装不下为止;

代码
#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. 部分背包问题

问题

给出 nnn 个物品,第 iii 个物品重量为 wiw_iwi ,价值为 cic_ici ,每一个物品可以取走一部分,价值与重量按比例计算,求在总重量不超过 CCC 的情况下总价值的最大值;

思路

此问题本质则为在最优选择问题的基础上加了价值项;

由于价值与重量按比例计算,即可优先选择单位重量下价值最大的,即通过价值除以重量排序,再依次选择,直到重量和为 CCC

注意,由于每个物品只能选择一部分,因此一定可以让总重量恰好为 CCC ,而且除了最后一个物品以外,其他物品要么不选要么全选;

代码
#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. 乘船问题

问题

nnn 个人,第 iii 个人重量为 wiw_iwi ,每艘船载重为 CCC ,最多可乘 2 个人,现在想用最少的船将所有人运走,问船的数量;

思路

由于要用最少的船,即每一只船应载最多的重量,则将所有人按重量从小到大排序,依次考虑最轻的人 iii ,若剩下的每个人都不能与他一起乘船,则剩下的只能每个人都乘一条船,否则,其能选择能与他一起乘的人中重量最大的一个 jjj ,使得眼前的浪费最少;

代码
#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. 选择不相交区间问题

问题

给定 nnn 个开区间 (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. 区间选点问题

问题

给定 nnn 个闭区间 [a,b][a,b][a,b] ,在数轴上选尽量少的点,使得每个区间 iii 内都至少有 kik_iki 个点,求最少点的数量;

思路

由于要求点的数量最少,则应将点尽量放在区间的交汇处;

首先按照区间右端点从小到大排序,然后从区间 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. 区间覆盖问题

问题

nnn 个闭区间 [a,b][a,b][a,b] ,选择尽量少的区间覆盖一条指定的线段区间 [s,t][s,t][s,t] ,求区间个数;

思路

由于要求区间最少,则每一个区间覆盖范围应尽量大,重合部分应尽量少;

将所有的区间按左端点从小到大排序,依次处理每个区间,每次选择覆盖点 sss 的区间中右端点坐标最大的一个,并将 sss 更新为该区间的右端点坐标,直到选择的区间包含 ttt 为止;

代码
#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. 带限期与罚款的单位时间任务调度

问题

nnn 个任务,每个任务需要一个单位时间执行,任务 iii 的截止时间 did_idi 表示任务 iii 在时间 did_idi 前必须完成,误时罚款 wiw_iwi 表示任务 iii 若未在 did_idi 前完成,导致 wiw_iwi 的罚款,确定所有任务的执行顺序使得罚款最少,求获得最多的钱数;

思路

要使罚款最少,就尽量先完成 w[i]w[i]w[i] 值较大的任务,则将任务按 w[i]w[i]w[i] 从大到小排序,按排好的顺序对任务进行安排,使得处理任务 iii 的时间在 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;
}
### 贪心算法的核心思想 贪心算法是一种通过逐步做出局部最优的选择来解决问题的方法,其目标是在每一步都选择当前情况下最好的选项,从而期望最终得到全局最优解[^2]。然而需要注意的是,这种策略并不总是能够保证获得真正的全局最优解,因此在应用时需谨慎验证。 #### 局部最优与整体最优的关系 贪心算法关键在于如何定义“局部最优”。例如,在找零钱问题中,优先使用面额较大的钞票可以减少所需钞票的数量,这是一种直观的局部最优策略[^2]。但是并非所有问题都能如此简单地解决;某些复杂场景下可能需要更深入的理解才能正确设计出有效的贪心规则。 #### 学习过程中的挑战与成长 对于初学者而言,掌握贪心算法可能会遇到一定困难。正如一位学习者所描述,“刚开始都不怎么会做”,这表明初次接触此类方法时常感到困惑甚至无从下手[^1]。面对这些问题,可以通过观看在线教程、查阅参考资料以及与其他同学交流等方式逐渐积累经验并形成自己的思考模式。“慢慢地才对贪心算法有了一些规律上的总结”,说明持续的努力加上实践是非常重要的环节之一[^1]。 以下是基于C++实现的一个典型例子——跳跃游戏(Jump Game),它展示了如何利用贪心思路高效求解特定类型的问题: ```cpp #include <iostream> using namespace std; int canJump(int* nums, int size){ int lastPos = size - 1; for (int i = size - 1; i >= 0; --i){ if (i + nums[i] >= lastPos){ lastPos = i; } } return lastPos == 0 ? 1 : 0; } int main(){ int a[] = {2,3,1,1,4}; cout << canJump(a,sizeof(a)/sizeof(*a))<< endl; } ``` 此程序片段实现了这样一个功能:给定一系列非负整数表示每个位置的最大跳远距离,请判断是否可以从第一个位置出发到达最后一个位置。这里采用逆向思维的方式,不断更新最远可达到的位置直到起点为止[^3]。 ### 结论 综上所述,虽然贪心算法看似简单易懂,但在实际运用过程中仍需仔细考量各种因素以确保解决方案的有效性和准确性。随着不断的练习和探索,人们不仅可以提升对该技术的认识水平,而且还能培养良好的编程习惯和技术素养。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值