1.贪心算法的概念
顾名思义,贪心算法总是作出在当前看来最好的选择。也就是说贪心算法并不从整体最优考虑,它所作出的选择只是在某种意义上的局部最优选择。当然,希望贪心算法得到的最终结果也是整体最优的。
虽然贪心算法不能对所有问题都得到整体最优解,但对许多问题它能产生整体最优解。
如通过贪心算法解决以下问题:
(1)活动安排问题;(2)最优装载问题;
(3)哈夫曼编码;(4)单源最短路径;
(5)最小生成树;(6)多机调度问题。
在一些情况下,即使贪心算法不能得到整体最优解,其最终结果却是最优解的近似解。注意:贪心算法不能对所有适合的问题都得到整体最优解。
贪心算法在对问题求解时,总是做出在当前看来是最好的选择。也就是说,不从整体最优上加以考虑,所做出的仅是在某种意义上的局部最优解。贪心算法不是对所有问题都能得到整体最优解,但对范围相当广泛的许多问题他能产生整体最优解或者是整体最优解的近似解。
2.题目实例
题目一:会场安排问题
假设要在足够多的会场里安排一批活动,并希望使用尽可能少的会场。设计一个有效的贪心算法来进行安排。试编程实现对于给定的k个待安排活动,计算使用的最少会场。输入数据中,第一行是k的值,接下来的k行中,每行有2个正整数,分别表示k个待安排活动的开始时间和结束时间,时间以0点开始的分钟计。输出为最少的会场数。
输入数据示例
5
1 23
12 28
25 35
27 80
36 50
输出数据
3
分析:活动以其完成时间的非增序排列,所以算法GreedySelector每次总是选择具有最早完成时间的相容活动加入集合A中。直观上,按这种方法选择相容活动为未安排活动留下尽可能少的时间。也就是说,该算法的贪心选择的意义是使剩余的可安排时间段极小化,以便安排尽可能少的相容活动。
代码实现
struct point
{
int t;
bool f;
};
bool cmp(point x, point y)
{
return x.t<y.t;//升序排列,如果改为return x.t>y.t,则为降序
}
int greedy(vector<point> x)
{
int sum = 0, cur = 0, n = x.size();
sort(x.begin(), x.end(), cmp);
for (int i = 0; i<n; i++)
{
if (x[i].f)
cur++;
else
cur--;
if (cur>sum)
sum = cur;
}
return sum;
}
int main()
{
vector<point> x;
int n, i;
point temp;
while (cin >> n, n)
{
for (i = 0; i<n; i++)
{
temp.f = true;
cin >> temp.t;
x.push_back(temp);
temp.f = false;
cin >> temp.t;
x.push_back(temp);
}
cout << greedy(x) << endl;
x.clear();
题目二:凑钱问题
题目描述:小明手中有 1,5,10,50,100 五种面额的纸币,每种纸币对应张数分别为 5,2,2,3,5 张。若小明需要支付 456 元,则需要多少张纸币?
题目分析
(1)建立数学模型
设小明每次选择纸币面额为 Xi ,需要的纸币张数为 n 张,剩余待支付金额为 V ,则有:
X1 + X2 + … + Xn = 456.
(2)问题拆分为子问题
小明选择纸币进行支付的过程,可以划分为n个子问题:即每个子问题对应为:
在未超过456的前提下,在剩余的纸币中选择一张纸币。
(3)制定贪心策略,求解子问题
制定的贪心策略为:在允许的条件下选择面额最大的纸币。则整个求解过程如下:
-
选取面值为 100 的纸币,则 X1 = 100, V = 456 – 100 = 356;
-
继续选取面值为 100 的纸币,则 X2 = 100, V = 356 – 100 = 256;
-
继续选取面值为 100 的纸币,则 X3 = 100, V = 256 – 100 = 156;
-
继续选取面值为 100 的纸币,则 X4 = 100, V = 156 – 100 = 56;
-
选取面值为 50 的纸币,则 X5 = 50, V = 56 – 50 = 6;
-
选取面值为 5 的纸币,则 X6 = 5, V = 6 – 5 = 1;
-
选取面值为 1 的纸币,则 X7 = 1, V = 1 – 1 = 0;求解结束
(4)将所有解元素合并为原问题的解
小明需要支付的纸币张数为 7 张,其中面值 100 元的 4 张,50 元 1 张,5 元 1 张,1 元 1 张。
代码实现
const int N = 5;
int Count[N] = {5,2,2,3,5};//每一张纸币的数量
int Value[N] = {1,5,10,50,100};
int solve(int money) {
int num = 0;
for(int i = N-1;i>=0;i--) {
int c = min(money/Value[i],Count[i]);//每一个所需要的张数
money = money-c*Value[i];
num += c;//总张数
}
if(money>0) num=-1;
return num;
}
题目三:集合覆盖问题
假如存在下面需要付费的广播台,以及广播台信号可以覆盖的地区,如何选择最少的广播台,让所有地区都可以接收到信号?
广播台 | 覆盖地区 |
---|---|
k1 | 北京、上海、天津 |
k2 | 北京、广州、深圳 |
k3 | 上海、成都、杭州 |
k4 | 上海、天津 |
k5 | 杭州、大连 |
也就是说现在地区总共有8个,即北京、上海、天津、广州、深圳、成都、杭州、大连,如何订购最少的广播台,可以收听到这8个地区的广播。
这个问题就是经典的用贪心算法求解的问题。「贪心算法」是指在每一步选择中都采取最优的策略,从而希望能够导致结果是最优的一种算法。贪心算法所得到的结果并不一定是最优的解,但都是相对接近最优解的结果。
「二、案例:」
要解决上面的问题,该怎么做呢?常规的做法如下:
-
列出k1、k2、k3、k4、k5的所有可能组合,总共就有
2^5 = 32种
组合。怎么来的?就是5个数不考虑顺序进行排列组合嘛。 -
在这32种组合中挑选一种可以覆盖到8个地区,并且广播台最少的组合,那就是本题的解了。
这样做显然很麻烦,要是有100个广播台,那不是完犊子了。但是可以使用贪心算法,提高效率。「贪心算法步骤如下:」
-
遍历所有的广播台,找到一个包含了最多当前还未覆盖地区的广播台;
-
将这个广播台存起来,想办法把该广播台覆盖的地区中下次选择时,用别的广播台代替;
-
重复上面的步骤直到覆盖了所有的地区。
如果用上面的案例来说的话,那么步骤就是:
-
遍历广播台,一开始所有地区都还没覆盖,遍历后发现k1、k2、k3都是覆盖了3个地区,选择这三个任何一个都可以,我们按照遍历顺序,选择k1。将k1用一个ArrayList保存起来;
-
把k1覆盖的地区从保存地区的集合中去掉,那么现在就只剩下5个地区没覆盖了;
-
再次遍历广播台的集合,现在剩下5个地区未覆盖,即广州、深圳、成都、杭州、大连。哪个广播台包含最多未覆盖的地区,那就选哪个。现在k2、k3、k5都是包含了两个还未被覆盖的地区。按照遍历顺序,选择k2;
-
再把k2覆盖的地区从保存地区的集合中去掉,那么现在就剩下成都、杭州、大连三个地方未覆盖了;
-
遍历广播台集合,发现k3和k5都可以覆盖两个,按照遍历顺序,选择k3;
-
再把k3覆盖的地区从保存地区的集合中去掉,那么现在就剩下大连未覆盖了;
-
毫无疑问,最后要选择k5,因为只有k5能够覆盖大连。
所以最终的选择结果是k1、k2、k3、k5。
「三、代码实现:」
将上面的问题用代码实现出来。
public class GreedyDemo {
public static void main(String[] args) {
// 广播电台及其对应覆盖地区用map保存
Map<String, Set<String>> map = new HashMap<>();
Set<String> areaSet1 = new HashSet<>();
areaSet1.add("北京");
areaSet1.add("上海");
areaSet1.add("天津");
Set<String> areaSet2 = new HashSet<>();
areaSet2.add("北京");
areaSet2.add("广州");
areaSet2.add("深圳");
Set<String> areaSet3 = new HashSet<>();
areaSet3.add("上海");
areaSet3.add("成都");
areaSet3.add("杭州");
Set<String> areaSet4 = new HashSet<>();
areaSet4.add("上海");
areaSet4.add("天津");
Set<String> areaSet5 = new HashSet<>();
areaSet5.add("杭州");
areaSet5.add("大连");
map.put("k1", areaSet1);
map.put("k2", areaSet2);
map.put("k3", areaSet3);
map.put("k4", areaSet4);
map.put("k5", areaSet5);
System.out.println(greedy(map));;
}
public static List<String> greedy(Map<String, Set<String>> map){
// 遍历map,拿到所有地区,保存起来
Set<String> allArea = new HashSet<>();
for(String key : map.keySet()) {
allArea.addAll(map.get(key));
}
// 用来保存所选电台的集合
List<String> selected = new ArrayList<>();
Set<String> temp = new HashSet<>();
String selectedKey = null;
while (allArea.size() != 0) {
for (String key : map.keySet()) {
temp.clear();
selectedKey = null;
Set<String> area = map.get(key);
temp.addAll(area);
// 跟allArea求交集
temp.retainAll(allArea);
if (temp.size() > 0 && (selectedKey == null || temp.size() > map.get(selectedKey).size())) {
selectedKey = key;
}
// 找到了当前这一轮选择的广播台
if (selectedKey != null) {
selected.add(selectedKey);
allArea.removeAll(map.get(selectedKey));
}
}
}
return selected;
}
}