贪心算法

贪心算法

概念

贪心是人的本能,贪心算法是在贪心决策上进行统筹规划的统称。在问题的解决过程中,总是做出在当前看来最好的选择。

举个简单的例子

假定现在有1元、2元、5元、10元、20元、50元面值的硬币,假设每种面额的硬币都有无数枚,现在要用这些硬币来支付A元,请问最少需要多少枚硬币?

这样一个生活化的例子中,凭借直觉就可以做出选择,题目要求所用的硬币最少,那么应该多用大面额的硬币,再选择次大的,直到凑成A元。

算法思想

一个大的问题可以被分成多个阶段来解决,每一个阶段总是选择当前最优的解法,问题的规模在每一步做出贪心选择后得到了缩小。

装载问题

【问题描述】

现有一个最大载重量为M的卡车和N种食品,每种食品都是散装的可拆分。已知第i种食品的最多拥有Wi公斤,其商品价值为Vi元/公斤,请确定一个装货方案,使得装入卡车中的所有物品总价值最大。

【输入】

n+1行
第一行:输入两个数分别是货车的最大载重量m(公斤)和货物的种类n
接下来n行:每行两个数,第一个是货物的拥有量wi公斤,货物的单价vi元/公斤
1≤m,n,wi,vi≤100

【输出】

一个数,最大价值

【样例输入】

5 100
78
22
13
56
64

【样例输出】

3

思路

因为每一个物品都是可以被拆分为最小单位,显然单位利益越大,总的收益才越大。所以通过局部最优达到了全局最优。可以使用贪心算法来解题。

参考程序

#include<cstdio>
#include<algorithm>
#define MAXN 100 + 15
struct food{
	int w;
	int v;
}s[MAXN]; 

bool cmp(food a, food b) { //按照价值从大到小排序
	return a.v > b.v;
}

int main() {
	int m, n, sum = 0, weight = 0, j = 1;
	scanf("%d%d", &m, &n);
	for(int i = 1; i <= n; i++) {
		scanf("%d %d", &s[i].w, &s[i].v);
	}
	std::sort(s+1, s+1+n, cmp);
	while(weight + s[j].w <= m) {	//如果当前种类的物品全部都能装进去
		weight += s[j].w;
		sum += s[j].w * s[j].v;
		j++;
	}
	sum += (m - weight) * s[j].v;	//把剩余空间装满
	printf("%d\n", sum);
	return 0;
}

小结

对于一个详细的问题,怎么知道是否可用贪心算法解此问题,以及是否能得到问题的最优解呢?

往往贪心类的题目都具有以下两个性质:

  • 贪心选择性质

    所谓贪心选择性质是指所求问题的总体最优解能够通过一系列局部最优的选择,换句话说,当考虑做何种选择的时候,我们仅仅考虑对当前问题最佳的选择而不考虑子问题的结果。这是贪心算法可行的第一个基本要素。运用贪心策略解题,一般来说需要一步一步的进行多次的贪心选择,每一次选择过后,原问题都会变为一个相似的、但规模更小的问题,并且每个选择都只做一次

  • 最优子结构性质

    当一个问题的最优解包括其子问题的最优解时,称此问题具有最优子结构性质。问题的最优子结构性质是该问题可用贪心算法求解的关键特征。贪心的法则是从上而下,从问题初始阶段就开始为每一个阶段做贪心选择,转化问题规模。

例1、排队打水

【题目描述】

有n个同学在一个水龙头排队打水,每个人的打水时间是ti,请你思考,能不能找出一种排队次序,使得所有同学打完水的总时间最少

【输入】

共两行
第1行,排队人数n(n小于等于100)
第2行,n个数,n个人打水时所要用的时间ti(10 ≤ ti ≤ 60),两两之间用空格隔开

【输出】

每个人都打完水的时间总和的最小情况

【样例输入】

6
3 7 1 9 5 11

【样例输出】

91

思路:

假定打水总时间为T(n),打水的总时间为F(n),等待的总时间为S(n)

T(n) = F(n) + S(n)

其中F(n)是固定的,想要总打水时间最少,那么需要减少每个人的等待时间。显然,打水时间越小的排在前面,后面人的等待时间越短。

参考程序

#include <cstdio>
#include <algorithm>
#define MAXN 100 + 10
int a[MAXN], b[MAXN]; //b数组记录前缀和,累加就是所有人的等待时间
int main() {
	int n, sum = 0;
	scanf("%d", &n);
	for(int i = 1; i <= n; i++) 
		scanf("%d", &a[i]);
	std::sort(a+1, a+1+n);	//从小到大排序

	for(int i = 1; i <= n; i++) {
		b[i] += b[i-1] + a[i]; 
		sum += a[i] + b[i-1]; //a[i]表示当前打水时间
	}
	printf("%d\n", sum);
	return 0;
} 

例2、不相交区间

【问题描述】

数轴上有n个开区间(ai, bi)。选择尽量多个区间,使得这些区间两两没有公共点。

【输入】

n+1行
第一行:n,表示区间的数量
接下来n行:每行两个数,表示这个区间的起始点和终止点

【输出】

一个数,最大不相交区间个数

【输入样例】

11
3 5
1 4
3 8
5 7
1 6
6 10
8 12
8 11
12 14
2 13
5 9

【输出样例】

4

解题思路

首先思考贪心策略

我们可以将线段的右端点按照从小到大的顺序排序,每次选择右端点靠前的线段。

策略证明:只有选择右端点靠前的线段,才能给后面的线段留出尽可能多的空间,才会有尽可能多的区域。

参考程序

#include <cstdio>
#include <algorithm>
#define MAXN 55
 
struct node{
    int start;
    int end;
}a[MAXN];
 
bool cmp(node x, node y) {
    return x.end < y.end;
}
 
int main() {
    int n, sum = 1, now;
    scanf("%d", &n);
    for(int i = 1; i <= n; i++) {
        scanf("%d%d", &a[i].start, &a[i].end);
    }
    std::sort(a+1, a+1+n, cmp);
    now = a[1].end;
    for(int i = 2; i <= n; i++) {
        if(a[i].start > now) {
            sum++;
            now = a[i].end; 
        }
    }
    printf("%d\n", sum);
    return 0;
} 

例3、Saruman's Army(POJ 3069)

【问题描述】

直线上有N个点,点i的位置是Xi,从这N个点中选择若干个点加上标记。使得其中每一个点,在距离为R以内的区域中必须有带有标记的点。在满足这个条件的情况下,要尽可能的少添加标记,请问至少要有多少点被加上标记?

【输入】

多组测试数据,每组测试数据先读入一行r n

接着是这n个点的坐标

当n = r = -1 结束读入

【输出】

输出多行,每个测试数据一个答案

【限制条件】

1 <= n <= 1000

0 <= r <= 1000

0 <= Xi <= 1000

【输入样例】

0 3
10 20 20
10 7
70 30 1 7 15 20 50
-1 -1

【输出样例】

2
4

参考思路:

首先要思考贪心选择怎么做?题目中说要尽可能的少选择点。那么标记的点选择的位置,应该要尽可能地覆盖左右两边的区域。

那么可以从头开始判定,对于a[i]+r的范围内,最右边的点(假定为k)可以设置为标记那么a[k]左边的点都能被R的范围覆盖,右边需要找到a[k]+r范围外的第一个点,从那里开始继续选择标记点。重复这个过程就可以找出最少的点。

参考程序:

#include <iostream>
#include <algorithm>
#define N 1005
using namespace std;

int a[N];

int main () {
	ios::sync_with_stdio(0);
	int n, r, cnt;
	// cin >> n;
	while (1) {
		cin >> r >> n;
		if (n == -1 && r == -1) break;
		cnt = 0;
		for (int i = 1; i <= n; i++)
			cin >> a[i];
		sort (a+1, a+1+n);
		for (int i = 1; i <= n; ) {
			int t = a[i] + r;
			while (a[i] <= t) i ++;
			i --;
			t = a[i] + r;
			while (a[i] <= t) i ++;
			cnt ++;
		}
		cout << cnt << endl;
	}

}

例4、分组

【问题描述】

现在有n个数要进行分组,要求每组数值必须连续递增,每组中不能出现重复的数字。要求求出人数最少的组的人数最大值是多少?

【输入】

输入有两行

第一行:一个正整数n (n ≤ 100000)

第二行:n个整数(∣a[i]∣ ≤ 10^9)

【输出】

一行,表示人数最少的组的人数最大值

【输入样例】

7
4 5 2 3 -4 -3 -5

【输出样例】

3

对于样例来说,可以分成两组,一组为[-5,-4,-3]一组为[2,3,4,5],人数最少的组的最大值为3

解题思路

该题的贪心策略考虑周全有些难度,由于题目要求组内元素的值要连续,并且不能有重复,首先可以对原数组进行排序。可能会有这样的贪心思路,从头到尾扫描一遍,判断相邻的两个元素差是否为1,如果是1就分为一组并统计人数,如果不是1就新开一组,这样做复杂度是O(n),但是该方法是错误的,举例来说,假定序列为1 1 1 2 2 2 ,有多个相同的序列,正确的分组应该是[1,2]、[1,2]、[1,2]。

正确的策略是,我们可以把每组看成一个队列。从头到尾扫描一下排序后的数组,如果数组当前元素的值-1是某个队列的队尾,那么这个元素就排到那个队列中去,如果所有队列的队尾都没有该元素,那么新开一个队列,该元素放置为队首。这样当所有的元素都入队后,找出队列长度最短的那个就是答案。

需要注意的点,可以用一个数组来保存每个队列的队尾元素,因为只有队尾元素对我们有效。这样在将元素分组的时候,可以用二分查找来找所有队列的队尾构成的序列,为了保证队尾构成的序列是单调递增的,假如元素k可以插入相同的好几个队列中,那么选择排序最后一个的队列插入。

参考程序

#include <iostream>
#include <cstring>
#include <algorithm>
#define N 100005
using namespace std;


int s[N], ed[N], sum[N]; //ed用来存储每一个队列的队尾元素,sum用于存储每一个队列的长度
int qn; 

int find (int x) { //二分查找,查找ed中x的位置,如果有多个相同的值,那么返回最后一个位置
	int l = 1, r = qn;
	while (l + 1 < r) {
		int mid = l + r >> 1;
		if (ed[mid] <= x) l = mid;
		else r = mid - 1;
	}
	if (ed[r] == x) return r;
	if (ed[l] == x) return l;
	return 0;
}

int main () {
	int n, ans = 0x7fffffff;
	memset(ed, 0x3f, sizeof(ed));
	cin >> n;
	for (int i = 1; i <= n; i++) 
		cin >> s[i];
	sort (s+1, s+1+n);
	for (int i = 1; i <= n; i++) {
		int loc = find(s[i]-1); //队列中查找
		if (loc) {
			ed[loc] = s[i]; // 找到后更新队尾元素
			sum[loc] += 1;
		} else {
			ed[++qn] = s[i]; // 没找到则新开一个队
			sum[qn] = 1;
		}
	}
	for (int i = 1; i <= qn; i++)  
		ans = min(ans, sum[i]);
	cout << ans;
	return 0;
}

总结

贪心算法是一种很常见且通用的算法,有时候也会结合其他的算法及数据结构,使得问题的难度增加。重点是要掌握贪心算法的原理,贪心算法有两个最重要的要素

  • 贪心策略

    贪心策略往往都是局部的最优解来达成全局最优解,经过多次贪心选择。解题过程中尝试多角度、多方法来考虑问题。

  • 最优子结构

    贪心算法一定是满足最优子结构的,也就是子问题的最优能组成全局的最优。

思考出贪心的策略后,一定要给予证明。没有经过证明的贪心算法是不可取的,通常可以采取归纳法反证法或者取极限情况来测试自己的策略是否正确。

解决问题时一定要思考是否适用于贪心,例如下题

有n件物品,每件物品的重量为w[i],价值为c[i]。现有一个容量为V的背包,问如何选取物品放入背包,使得背包内物品的总价值最大。其中每 种物品都只有一件。

如果是用贪心来决策,我们应该先选择重量低的还是价格高的?似乎都不是正确答案,稍加证明即可得到。实际该题是不合适使用贪心策略的。因此解题时一定要注意自己算法的正确性

posted @ 2020-09-13 16:26  S_K_P  阅读(236)  评论(0)    收藏  举报