一、算法介绍
贪心算法(greedy algorithm ,又称贪婪算法)是指,在对问题求解时,总是做出在当前看来是最好的选择。也就是说,不从整体最优上加以考虑,算法得到的是在某种意义上的局部最优解。
-
贪心算法不是对所有问题都能得到整体最优解,关键是贪心策略的选择。
-
贪心算法所得到的结果不一定是最优的结果(有时候会是最优解),但是都是相对接近最优解的结果。
二、应用场景-集合覆盖问题
2.1 问题引出
假设存在如下表的需要付费的广播台以及广播台信号可以覆盖的地区。问:如何选择最少的广播台,让所有的地区都可以接收到信号 ?
广播台 | 覆盖地区 |
---|---|
K1 | “北京”、“上海”、“天津” |
K2 | “广州”、“北京”、“深圳” |
K3 | “成都”、“上海”、“杭州” |
K4 | “上海”、“天津” |
K5 | “杭州”、“大连” |
2.2 思路分析
- 解决方式一:使用穷举法实现,列出每个可能的广播台的集合,这被称为幂集。
- 假设总的有 n 个广播台,则广播台的组合总共有 2ⁿ -1 个,假设每秒可以计算 10 个子集(如下图所示)。
广播台数量n | 子集总数2ⁿ | 需要时间 |
---|---|---|
5 | 32 | 3.2秒 |
10 | 1024 | 102.4秒 |
32 | 4294967296 | 13.6年 |
100 | 1.26*100³º | 4x10²³年 |
- 解决方式二:使用贪心算法,可以得到非常接近的解,并且效率高。
- 算法步骤:
- 创建一个散列表 broadcast,广播台作为键,对应覆盖的区域集合作为值;
- 创建一个
Set
集合 allAreas,存放需要覆盖的所有区域; - 创建一个散列表 selects,存放被选中的广播台;
- 遍历散列表,找到覆盖了最多未覆盖的区域的广播台,将其加入到 selects 中;
- 将上一步选择的广播台所覆盖的区域从 allAreas 中移除,同时将广播台从散列表中移除;
- 重复执行 4、5 步,直至区域集合为空。
- 示意图:
2.3 代码示例
public class GreedyAlgorithmDemo {
public static void main(String[] args) {
// 各广播台的区域集合。
HashSet<String> k1Areas = new HashSet<>();
k1Areas.add("北京");
k1Areas.add("上海");
k1Areas.add("天津");
HashSet<String> k2Areas = new HashSet<>();
k2Areas.add("广州");
k2Areas.add("北京");
k2Areas.add("深圳");
HashSet<String> k3Areas = new HashSet<>();
k3Areas.add("成都");
k3Areas.add("上海");
k3Areas.add("杭州");
HashSet<String> k4Areas = new HashSet<>();
k4Areas.add("上海");
k4Areas.add("天津");
HashSet<String> k5Areas = new HashSet<>();
k5Areas.add("杭州");
k5Areas.add("大连");
// 1.映射表:{广播台=[对应覆盖区域集合]}。
HashMap<String, HashSet<String>> broadcasts = new HashMap<>();
broadcasts.put("K1", k1Areas);
broadcasts.put("K2", k2Areas);
broadcasts.put("K3", k3Areas);
broadcasts.put("K4", k4Areas);
broadcasts.put("K5", k5Areas);
// 2.需要覆盖的所有区域。
HashSet<String> allAreas = new HashSet<>();
allAreas.add("北京");
allAreas.add("上海");
allAreas.add("天津");
allAreas.add("广州");
allAreas.add("深圳");
allAreas.add("成都");
allAreas.add("杭州");
allAreas.add("大连");
// 排序输出。
List<String> all = allAreas.stream().sorted().collect(Collectors.toList());
System.out.println("需要覆盖的所有区域有:" + all);
// 3.存放被选中的广播台。
HashMap<String, HashSet<String>> selects = new HashMap<>();
// 如果还有区域没被覆盖到。
while (!allAreas.isEmpty()) {
// 每次循环时需要重置,目的是用于记录当前能覆盖最多未覆盖区域的广播台。
String maxKey = null;
for (String key : broadcasts.keySet()) {
// 当前广播台能覆盖的区域。
HashSet<String> areas = broadcasts.get(key);
// 求当前广播台能覆盖的区域 与 需要覆盖的所有区域交集(即找未覆盖的区域)。
areas.retainAll(allAreas);
// 交集个数。
int num = areas.size();
if (null == maxKey && 0 < num) {
// 记录。
maxKey = key;
// maxKey 不为空(说明已记录有广播台),就和当前广播台比较谁覆盖的交集多。
} else if (null != maxKey) {
// 之前记录的最多未覆盖区域的广播台。
HashSet<String> maxAreas = broadcasts.get(maxKey);
// 找到之前的 maxKey 覆盖了哪些区域。
maxAreas.retainAll(allAreas);
int maxNum = maxAreas.size();
if (maxNum < num) {
maxKey = key;
}
}
}
// 一轮for循环结束,可以得到覆盖区域最多的广播台。
// 将广播台添加至选中列表。
selects.put(maxKey, broadcasts.get(maxKey));
// 移除已覆盖的区域。
allAreas.removeAll(broadcasts.get(maxKey));
// 移除已选中的广播台。
broadcasts.remove(maxKey);
}
System.out.println("已被选中的广播台有:" + selects.keySet());
// 将已选中的广播覆盖的区域集合排序输出(目的是对比需要覆盖所有的区域,结果是否符合预期)。
List<String> list = new ArrayList<>();
selects.forEach((key, value) -> list.addAll(value));
List<String> results = list.stream().sorted().collect(Collectors.toList());
System.out.println("这些广播台所覆盖的区域有:" + results);
// 需要覆盖的所有区域有:[上海, 北京, 大连, 天津, 广州, 成都, 杭州, 深圳]
// 已被选中的广播台有:[K1, K2, K3, K5]
// 这些广播台所覆盖的区域有:[上海, 北京, 大连, 天津, 广州, 成都, 杭州, 深圳]
}
}
三、结束语
“-------怕什么真理无穷,进一寸有一寸的欢喜。”
微信公众号搜索:饺子泡牛奶。