贪心算法

贪心算法-集合覆盖

参考博文:https://blog.csdn.net/a8082649/article/details/82079779

贪心算法与动态规划算法的区别

  1. 两者都是将问题拆分为规模更小的子问题
  2. 动态规划是分治法解决冗余,将子问题的解保存,后面再次遇到直接可以用,避免重复的计算。
  3. 贪心算法的每一次操作都会对结果产生直接的影响,问题变得越来越小,而动态规划不是。
  4. 贪心算法对每一个子问题都做出选择,不能回退。动态规划会根据以前的选择结果对当前进行选择,有回退的功能(背包问题)。
  5. 动态规划主要用于二维或者三维的问题,而贪心一般是一维的问题。
  6. 贪心算法结果是最优的近似解,有的情况就是最优解,而动态规划是最优解

概念:

  1. 贪心算法是指在对问题进行求解时,在每一步选择中都采取最好或者最优的选择,从而希望导致结果是最好或者最优算法。
  2. 贪心算法所得到的结果不一定是最优的结果(有时候会是最优解),但是都是相对近似(接近)最优解的结果。

 

 

案例1

区间调度问题:

假设有如下课程,希望尽可能多的将课程安排在一间教室里:

课程开始结束
美术9:0010:00
英语9:3010:30
数学10:0011:00
计算机10:3011:30
音乐11:0012:00

 

 

 

 

 

 

 

解法:

第一步:选择结束最早的课,便是要在这教室上课的第一节课

第二步:选择第一堂课结束后才开始的课,并且结束最早的课,这将是第二节在教室上的课。

重复这样做就能找出答案,这边的选择策略便是结束最早且和上一节课不冲突的课进行排序,因为每次都选择结束最早的,所以留给后面的时间也就越多,自然就能排下越多的课了。

每一节课的选择都是策略内的局部最优解(留给后面的时间最多),所以最终的结果也是近似最优解(这个案例上就是最优解)。 (该案例的代码实现,就是一个简单的时间遍历比较过程)。

 

案例2

背包问题:

背包问题:有一个背包,容量为35磅 , 现有如下物品物品,要求达到的目标为装入的背包的总价值最大,并且重量不超出。

物品重量价值
吉他151500
音响303000
笔记本电脑202000
显示器292999
1200


 

 

 

 

 

 

方便计算所以只有3个物品,实际情况可能是成千上万。

同上使用贪婪算法,因为要总价值最大,所以每次每次都装入最贵的,然后在装入下一个最贵的,选择结果如下:

选择: 音响 + 笔,总价值 3000 + 200 = 3200

并不是最优解: 吉他 + 笔记本电脑, 总价值 1500 + 2000 = 3500

当然选择策略有时候并不是很固定,可能是如下:

(1)每次挑选价值最大的,并且最终重量不超出:

选择: 音响 + 笔,总价值 3000 + 200 = 3200

(2)每次挑选重量最大的,并且最终重量不超出(可能如果要求装入最大的重量才会优先考虑):

选择: 音响 + 笔,总价值 3000 + 200 = 3200

(3)每次挑选单位价值最大的(价格/重量),并且最终重量不超出:

选择: 笔+ 显示器,总价值 200 + 2999 = 3199

如上最终的结果并不是最优解,在这个案例中贪婪算法并无法得出最优解,只能得到近似最优解,也算是该算法的局限性之一。该类问题中需要得到最优解的话可以采取动态规划算法。

 

案例3

集合覆盖问题:

假设存在如下表的需要付费的广播台,以及广播台信号可以覆盖的地区。 如何选择最少的广播台,让所有的地区都可以接收到信号。

广播台能覆盖的地区
K1"1号楼","2号楼","3号楼
K2"4号楼","1号楼","5号楼"
K3"6号楼","2号楼","7号楼"
K4"2号楼","3号楼"
K5"7号楼","8号楼"

 

 

 

 

 

 

 

如何找出覆盖所有地区的广播台的集合呢,最简单就是使用穷举法实现

(1) 列出每个可能的广播台的集合,这被称为幂集。假设总的有n个广播台,则广播台的组合总共有2ⁿ-1个,足够大时就是2ⁿ

(2) 在这些集合中,选出覆盖全部地区的最小的集合,假设n不在,但是当n非常大的时候,假设每秒可以计算10个子集

广播台数量子集总数需要的时间贪心算法时间
5323.2秒2.5秒
101024102.4秒10秒
32429496729613.6年102.4秒
1001.26*100³º4x10²³年1000秒

 

 

 

 

 

 

目前并没有算法可以快速计算得到准备的值, 而使用贪婪算法,则可以得到非常接近的解,并且效率高:

选择策略上,因为需要覆盖全部地区的最小集合:

(1) 遍历所有的广播电台,选出一个广播台覆盖了最多的未覆盖地区的,即它覆盖了一些已覆盖的地区也没关系

(2) 将这个电台加入到一个集合中(ArrayList),想办法吧该2电台覆盖的地区在下次比较时去掉。

(3) 重复第一步,直到覆盖了全部的区域。

这是一种近似算法(贪婪算法的一种)。在获取到精确的最优解需要的时间太长时,便可以使用近似算法,判断近似算法的优劣标准如下:

  •     速度有多快
  •     得到的近似解与最优解的接近程度

在本例中贪婪算法是个不错的选择,不仅运行速度快,本例运行时间O(n²),最坏的情况,假设n个广播台,每个广播台就覆盖1个地区,n个地区,总计需要查询n*n=O(n²),实现可查看后面的java代码实现


此时算法选出的是K1, K2, K3, K5,符合覆盖了全部的地区,可能不是预期中的K2, K3,K4,K5(也许预期中的更便宜,更便于实施等等)。

案例4

NP完全问题

NP完全问题(NP-C问题),是世界七大数学难题之一。 NP的英文全称是Non-deterministic Polynomial的问题,即多项式复杂程度的非确定性问题。简单的写法是 NP=P?,问题就在这个问号上,到底是NP等于P,还是NP不等于P。

假设有旅行商需要从下面三个城市的某一个城市出发,如何规划路线获取行程的最短路径。

存在3!(阶乘)=6种可能情况:

A->B->C
A->C->B
B->A->C
B->C->A
C->A->B
C->B->A

这边和求最短路径的算法(广度搜索),最大的差别是没有固定源点(起点),,每一个节点都可能是源点,并且需要经过每一个节点,所以若穷举法则不得不找出每一种可能并进行比较。

数量总数!穷举时间贪婪时间
5120120秒12.5秒
103224天50秒

 

 

 

 

类似上述集合覆盖问题、旅行商问题,都属于NP完全问题,在数学领域上并没有快速得到最优解的方案,贪婪算法是最适合处理这类问题的了。

如何判断是NP完全问题的:

1.元素较少时,一般运行速度很快,但随着元素数量增多,速度会变得非常慢

2.涉及到需要计算比较"所有的组合"情况的通常是NP完全问题

3.无法分割成小问题,必须考虑各种可能的情况。这可能是NP完全问题

4.如果问题涉及序列(如旅行商问题中的城市序列)且难以解决,它可能就是NP完全问题

5.如果问题涉及集合(如广播台集合)且难以解决,它可能就是NP完全问题

6.如果问题可转换为集合覆盖问题或旅行商问题,那它肯定是NP完全问题

 

集合覆盖的代码示例

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;

public class ListCover {

	public static void main(String[] args){
		//初始化广播台信息
		HashMap<String,HashSet<String>> broadcasts = new HashMap<String,HashSet<String>>();
		broadcasts.put("1号广播", new HashSet(Arrays.asList(new String[] {"1号楼","2号楼","3号楼"})));
		broadcasts.put("2号广播", new HashSet(Arrays.asList(new String[] {"4号楼","1号楼","5号楼"})));
		broadcasts.put("3号广播", new HashSet(Arrays.asList(new String[] {"6号楼","2号楼","7号楼"})));
		broadcasts.put("4号广播", new HashSet(Arrays.asList(new String[] {"2号楼","3号楼"})));
		broadcasts.put("5号广播", new HashSet(Arrays.asList(new String[] {"7号楼","8号楼"})));
		//需要覆盖的全部楼
		HashSet<String> allAreas = new HashSet(Arrays.asList(new String[] {"1号楼","2号楼","3号楼","4号楼","5号楼","6号楼","7号楼","8号楼"}));
		//所选择的广播台列表
		List<String> selects = new ArrayList<String>();
		//中间变量,主要是判断交集,如果根本就没包含,那就不需要执行了。
		HashSet<String> tempSet = new HashSet<String>();
		//这个的意思就是,假如你第一次的tempSet是k4的值, 那么maxKey就是k4, 但是如果你第二次的tempSet是k1的值,那么就需要
		//交换,maxKey就是k1了。
		String maxKey = null;
		while(allAreas.size()!=0) {
			maxKey = null;
			for(String key : broadcasts.keySet()) {
				System.out.println(key);
				tempSet.clear();//清空中间变量每次循环
				HashSet<String> areas = broadcasts.get(key);  //获取到这个key的所有覆盖区域
				tempSet.addAll(areas);  //加入到中间变量里边
				//可以看到这个方法改变了集合A中的元素,将存在于集合A中但不存在于集合B中的元素移除。
				//每个key的值与所有区域的交集没有就是null
				//求出这两个集合的交集,此时tempSet会被赋值为交集的内容,所以使用临时变量
				tempSet.retainAll(allAreas);
				for (String s : tempSet) {
					System.out.println(s);
				}
				//如果该集合包含的楼数量比原本的集合多
				System.out.println("tempSet.size()======="+tempSet.size());
				if(maxKey!=null){
					System.out.println("broadcasts.get(maxKey).size()==="+broadcasts.get(maxKey).size());
				}
				//假设
				//第一次遍历
					//第一次 tempSet.size()=3, maxKey=null, broadcasts.get(null).size()不执行;   maxKey = k1;
					//第二次tempSet.size()=3, maxKey=k1, broadcasts.get(k1).size()=3;     maxKey = k1;
					//第三次tempSet.size()=3, maxKey=k1, broadcasts.get(k1).size()=3;     maxKey = k1;
					//第四次tempSet.size()=2, maxKey=k1, broadcasts.get(k1).size()=3;     maxKsy = k1;
					//第五次tempSet.size()=2, maxKey=k1, broadcasts.get(k1).size()=3;     maxKsy = k1;
							//选出k1保存到 selects中  去掉k1的目前的区域  "4号楼","5号楼","6号楼","7号楼","8号楼"
				//第二次遍历
					//第一次 tempSet.size()=0, maxKey=null, broadcasts.get(null).size()不执行;   maxKey = null;
					//第二次tempSet.size()=2, maxKey=null, broadcasts.get().size()     maxKey = k2;
					//第三次tempSet.size()=2, maxKey=k2, broadcasts.get(k2).size()=3;     maxKey = k2;
					//第四次tempSet.size()=0, maxKey=k2, broadcasts.get(k2).size()=3;     maxKsy = k2;
					//第五次tempSet.size()=2, maxKey=k2, broadcasts.get(k2).size()=3;     maxKsy = k2;
							//选出k2保存到 selects中  去掉k2的目前的区域  "6号楼","7号楼","8号楼"
				//第三次遍历
					//第一次 tempSet.size()=0, maxKey=null, broadcasts.get(null).size()不执行;   maxKey = null;
					//第二次tempSet.size()=0, maxKey=null, broadcasts.get().size()     maxKey = null;
					//第三次tempSet.size()=2, maxKey=null, broadcasts.get(null).size()不执行;     maxKey = k3;
					//第四次tempSet.size()=0, maxKey=k3, broadcasts.get(k3).size()=3;     maxKsy = k3;
					//第五次tempSet.size()=2, maxKey=k3, broadcasts.get(k3).size()=3;     maxKsy = k3;
						//选出k3保存到 selects中  去掉k3的目前的区域  "8号楼"
				//当tempSet为空的时候,根本就不执行  
				//tempSet.size() > broadcasts.get(maxKey).size()体现了贪心算法的本质
				if (tempSet.size()>0 && (maxKey == null || tempSet.size() > broadcasts.get(maxKey).size())) {
					maxKey = key;
				}
				System.out.println("maxKey===="+maxKey);
			}
			/
			if (maxKey != null) {
				//这个就是当前的所有区域,挑选出来包含最多楼的那个key。他就被选中了
				selects.add(maxKey);
				//选中之后从所有区域里把他移除掉,下次就不判断他了。
				allAreas.removeAll(broadcasts.get(maxKey));
			}
		}
		System.out.print("贪心算法被选中的能覆盖所有区域的广播:" + selects);
	}
}

 

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值