41-贪心算法
1. 基本概念
所谓贪心算法是指,在对问题求解时,总是做出在当前看来是最好的选择,从而希望能够导致结果是最好或者最优的算法。
也就是说,它不从整体最优上加以考虑,只顾眼前不顾大局,所以它所做出的也仅仅是在某种意义上的局部最优解。
贪心算法不是对所有问题都能得到整体最好的解决办法,关键是贪心策略的选择,选择的贪心策略必须具备无后效性,即某个状态以前的状态不会影响以后的状态,只与当前状态有关。
虽然贪心算法不能对所有问题都得到整体最优解,但对许多问题它能产生整体最优解。如单源最短路径问题,最小生成树问题等。在一些情况下,即使贪心算法不能得到整体最优解,但其最终结果也都是近似(接近)最优解。
2. 基本思路
- 满足两个条件
- 贪心策略
- 贪心策略适用的前提:局部最优策略能导致产生全局最优解
- 步骤
- 建立数学模型来描述问题
- 把求解的问题分成若干个子问题
- 对每个子问题求解,得到子问题的局部最优解
- 把这些"局部最优解"叠起来,就"当作"整个问题的最优解
3. 贪心选择性质*
所谓贪心选择性质是指所求问题的整体最优解可以通过一系列局部最优的选择,换句话说,当考虑做何种选择的时候,我们只考虑对当前问题最佳的选择而不考虑子问题的结果。这是贪心算法可行的第一个基本要素。
贪心算法以迭代的方式作出相继的贪心选择,每作一次贪心选择就将所求问题简化为规模更小的子问题。
对于一个具体问题,要确定它是否具有贪心选择性质,必须证明每一步所作的贪心选择最终导致问题的整体最优解。
当一个问题的最优解包含其子问题的最优解时,称此问题具有最优子结构性质。问题的最优子结构性质是该问题可用贪心算法求解的关键特征。
3. 贪心和DP的区别
3.1 相同点
都是通过不停的解决子问题,来得到最终结果的。
3.2 不同点
- 解决问题的思路
- DP解决问题:自底向上
- 贪心解决问题:自顶向下
- 子问题
- DP 经分解得到子问题往往不是互相独立的 ( 即下一个子阶段的求解是建立在上一个子阶段的解的基础上,进行进一步的求解
- 贪心的子问题独立,总是做出在当前子问题看来是最好的选择
- 最优解
- DP 在分析子问题的时候,会使用前面子问题的最优结果,并且前面子问题的最后结果不受后面的影响,最后一个子问题即最优解
- 贪心得到的结果不一定是最优解,它是把这些个"局部最优解"叠起来,"当作"整个问题的最优解
3.3 summary
- 贪心解决不了就用动态规划
- 一般贪心算法的时间复杂度为O(nlgn),动态规划为O(n^2)
- 能用贪心解决就不用动态规划
4. 案例:集合覆盖
4.1 思路分析
穷举:
- 列出每个可能的广播台的集合,这被称为 "幂集";
- 假设总的有 n 个广播台,则广播台的组合总共有 2ⁿ - 1 个,假设每秒可以计算 10 个子集;
贪心:
- 目前并没有算法可以快速计算得到准备的值, 使用贪婪算法,则可以得到非常接近的解,并且效率高;
- 选择策略上,因为需要覆盖全部地区的最小集合;
- 遍历所有的广播电台,找到一个覆盖了最多未覆盖的地区的电台 (此电台可能包含一些已覆盖的地区,但没有关系);
- 将这个电台加入到一个集合中 (比如ArrayList),想办法把该电台覆盖的地区在下次比较时去掉;
- 重复第 1 步直到覆盖了全部的地区;
4.2 代码实现
public class GreedyDemo {
// 创建广播电台, 放入Map
static HashMap<String, HashSet<String>> broadcasts = new HashMap<>();
//allAreas 存放所有的地区
static HashSet<String> allAreas = new HashSet<String>();
// 创建ArrayList, 存放选择的电台
static ArrayList<String> selects = new ArrayList<>();
public static void main(String[] args) {
init();
overSet();
System.out.println(selects);
}
public static void overSet() {
// 一个临时集合, 用来存储当前电台与allAreas的交集地区
HashSet<String> tempSet = new HashSet<>();
// 用来保存覆盖最多未覆盖地区的电台名称
String maxKey = null;
while(allAreas.size() != 0) {
// 置空
maxKey = null;
// 遍历broadcasts, 找到本轮的maxKey
for(String key : broadcasts.keySet()) {
// 清空
tempSet.clear();
// 当前K所覆盖的地区
HashSet<String> areas = broadcasts.get(key);
tempSet.addAll(areas);
// tempSet = areas ∩ allAreas
tempSet.retainAll(allAreas);
// 如果当前key所包含的未覆盖地区数量比maxKey还多, 那当前key才应该是maxKey(Greedy)
if(tempSet.size() > 0 &&
(maxKey == null || tempSet.size() > broadcasts.get(maxKey).size()))
maxKey = key;
}
// 如果maxKey != null, 则会加入到selects里面
if(maxKey != null) {
selects.add(maxKey);
allAreas.removeAll(broadcasts.get(maxKey));
}
}
}
public static void init() {
allAreas.add("北京");
allAreas.add("上海");
allAreas.add("天津");
allAreas.add("广州");
allAreas.add("深圳");
allAreas.add("成都");
allAreas.add("杭州");
allAreas.add("大连");
HashSet<String> hashSet1 = new HashSet<String>();
hashSet1.add("北京");
hashSet1.add("上海");
hashSet1.add("天津");
HashSet<String> hashSet2 = new HashSet<String>();
hashSet2.add("广州");
hashSet2.add("北京");
hashSet2.add("深圳");
HashSet<String> hashSet3 = new HashSet<String>();
hashSet3.add("成都");
hashSet3.add("上海");
hashSet3.add("杭州");
HashSet<String> hashSet4 = new HashSet<String>();
hashSet4.add("上海");
hashSet4.add("天津");
HashSet<String> hashSet5 = new HashSet<String>();
hashSet5.add("杭州");
hashSet5.add("大连");
//加入到map
broadcasts.put("K1", hashSet1);
broadcasts.put("K2", hashSet2);
broadcasts.put("K3", hashSet3);
broadcasts.put("K4", hashSet4);
broadcasts.put("K5", hashSet5);
}
}
贪婪算法所得到的结果不一定是最优的结果 (有时候会是最优解),但是都是相对近似(接近) 最优解的结果。
比如上题的算法选出的是 {K1, K2, K3, K5},符合覆盖了全部的地区,但是我们发现 {K2, K3, K4, K5} 也可以覆盖全部地区,如果 K2 的使用成本低于 K1,那么我们上题的 {K1, K2, K3, K5} 虽然是满足条件,但是并不是最优的。