五大算法思想(二)贪心算法及常见例子

您所在的位置:网站首页 常见的解决问题的策略有哪四个 五大算法思想(二)贪心算法及常见例子

五大算法思想(二)贪心算法及常见例子

2024-07-15 00:13| 来源: 网络整理| 查看: 265

文章目录 一、理论基础1.1 适用场景1.2 使用步骤1.3 算法缺陷1.4 经典例子 二、常见例子2.1 活动选择问题2.2 钱币找零问题2.3 背包问题2.4 小船过河问题2.5 区间覆盖问题

一、理论基础

  贪心算法,指在对问题求解时,总是做出在当前看来是最好的选择。也就是说不从整体最优上加以考虑,算法得到的结果是在某种意义上的局部最优解。

1.1 适用场景

  可以用贪心算法解决的问题一般有如下特征:    1>贪心选择性质。一个问题的整体最优解可通过一系列局部的最优解的选择达到,并且每次的选择可以依赖以前作出的选择,但不依赖于后面要作出的选择。这就是贪心选择性质。对于一个具体问题,要确定它是否具有贪心选择性质,必须证明每一步所作的贪心选择最终导致问题的整体最优解。    2>最优子结构性质。当一个问题的最优解包含其子问题的最优解时,称此问题具有最优子结构性质。问题的最优子结构性质是该问题可用贪心法求解的关键所在。

1.2 使用步骤

  使用贪心算法的基本步骤:    1>建立数学模型来描述问题 。    2>把求解的问题分成若干个子问题 。    3>对每个子问题求解,得到子问题的局部最优解。    4>把子问题的解局部最优解合成原来解问题的一个解。   以上都是官方说法。依个人的理解,在贪心选择性质里,其实隐含着一个条件,就是:在使用贪心算法时,待选择的因素是有序的。在该前提下,才可能做出对当前最优的选择。用公式来表示的话,大致如下:

while(约束条件成立){   选择当前最优解,记录并累积到最后的解中   if(当前最优解使用完毕)     当前最优解 = 当前次优解 }

1.3 算法缺陷

  贪心算法也存在如下缺陷:    1>不能保证解是最佳的。因为贪心算法总是求的局部最优解,并未从整体考虑整体最优解。    2>贪心算法只能确定某些问题的可行性范围。

缺陷1和缺陷2表示的意思是类似的,就是贪心算法是有局限性的。此处以一个例子来说明:比如钱币找零问题,假如有15元,需要用11、5、1三种面额的钱币来表示,如果用贪心算法来选择的话,会从11开始选择,这样就会选出11+1+1+1+1的方案,用5张钱币来表示,但实际上,用5+5+5的方案,使用的钱币更少,此时贪心方案就选不出全局最优解。

   3>贪心算法一般用来解决求最大或最小解。

该缺陷也好理解,贪心算法是以某种策略选择一个值,而不是遍历出所有解,要遍历出某个问题所有解的话,常常用的是回溯法。

1.4 经典例子

  常见例子如下:    1>活动选择问题    2>钱币找零问题    3>再论背包问题    4>小船过河问题    5>区间覆盖问题   接下来将对这些例子进行实现,来探究贪心算法思想的具体使用方法。

二、常见例子 2.1 活动选择问题

  该问题是一般可以用贪心算法和动态规划两种方法解决,此篇文章主要介绍贪心算法实现方式。该类问题的形式一般如下:在某个地方需要举办不同的活动,每个活动要花费不同时间(起止时间不同),问怎样安排,可以安排尽量多的活动?此问题之所以可以用贪心算法解决,是因为下个活动的选择可以只取决于上个活动结束时间,而不必从全局考虑。   我们假设有个已经按结束时间排好序的活动列表,开始时间和结束时间如下:

活动1234567891011开始时间130535688212结束时间4567891011121314

  在解决该问题时,需要先对每个活动按结束时间进行排序。也许有人会问,为什么要对活动的结束时间,而不是开始时间进行排序,这样做的原因是当前活动的结束时间会影响到下一个活动的选择,而当前活动的开始时间不会影响下一个活动的选择。   整个的解题思路,大致如下:先把活动按结束时间排序,然后默认选取第一个活动,用变量来标识当前活动的结束日期,作为选择下一个活动的日期的判断标准,具体标准为:下一个选取的活动的开始时间要>=当前活动的结束时间。然后重复此过程,直到遍历完整个数组。   活动选择问题的迭代解法,示例代码如下:

private static List activityChoice(int[] startTime, int[] endTime){ List list = new ArrayList(); /*因为活动已经按结束时间进行过升序排列,所以要默认选中第一个活动*/ list.add(0); /*当前开始活动的结束时间*/ int curTime = endTime[0]; for(int i=1;i=之前选择的活动的结束时间,则选取该活动,更新当前时间*/ if (startTime[i]>=curTime) { list.add(i); curTime = endTime[i]; } } return list; }

  活动选择问题的递归解法,示例代码如下:

static List acivitySelector(int[] startTime, int[] endTime, int i, int n,List list) { /*因为活动已经按结束时间进行过升序排列,所以要默认选中第一个活动,+1是因为数组下标从0开始,此处是数组下标+1*/ if(i==0){ list.add(i+1); } int j=i+1; /*此处的循环是过滤掉不符合要求的活动*/ while (jstartTime[j]){ j++; } //找到一个满足f[i]需要将物品按照单位重量价值进行排序。    2>将尽可能多的单位重量价值最高的物品装入背包,若最大单位重量价值的物品全部装入背包后,背包还有多余容量,则选择单位重量价值次高的并尽可能多地装入背包。    3>如果最后一件物品无法全部装入,则计算可以装入的比例,然后按比例装入。   此处用迭代方式实现,示例代码如下:

package Greedy; /* * 给定n种物品,1个背包,背包容量为c,每个物品i的价值为vi,重量为wi,如何选择装入物品能使背包的总价值最大? */ public class Package { public static void main(String[] args) { /*每件物品的重量*/ int[] weights={10,30,20}; /*每件物品的价值*/ int[] values={60,120,100}; /*背包的容量限制*/ int packNum=50; maxPackageValue(weights,values,packNum); } private static void maxPackageValue(int[] weights,int[] values,int packNum){ /*单位重量价值比*/ double[] univalent=new double[values.length]; /*最后背包中存储的总价值*/ double allValue=0.0; for(int i=0;i最快的和最慢的过河,然后最快的将船划回来,最快的和次慢的过河,然后最快的将船划回来。过程如下:         从上图看出,该类方式所花费时间为:2time[0]+time[n-2]+time[n-1]。   上面讨论了当过河的人大于等于四个人的情况。每次送两个人过河的方式共有两种,在具体使用时可以比较一下用哪种方式比较快,再决定使用的方式。接下来还要看一下过河时人数小于等于三个的情况:    1>当过河人数为三个时,此固定用时time[0]+time[1]+time[2]。    2>当过河人数为二个时,也许有人会问过河人数为三时,不是包含了这个情况吗?确实是包含了,此处指的初始的人数就是两个,固定用时time[1]。    3>当过河人数为一个时,固定用时time[0]。   接下来就是代码实现,示例代码如下:

package Greedy; /* * 有n个人需要过河,只有一艘船,最多能乘2人,船的运行速度为2人中较慢一人的速度, * 过去后还需一个人把船划回来,把n个人运到对岸,最少需要多久。 */ public class River { public static void main(String[] args) { int[] times={1,2,4,5,8}; int result=crossRiver(times); System.out.println("所花时间为:"+result); } private static int crossRiver(int[] times){ /*n表示还未过河的人数,初始化为所有人*/ int n=times.length; int result=0; while(n>0){ if(n==1){ result=result+times[0]; break; }else if(n==2){ result=result+times[0]+times[1]; break; }else if(n==3){ result=result+times[0]+times[1]+times[2]; break; }else{ /*在每次过河时,在两种方式上进行比较,选择耗时更少的那个*/ result=result+Math.min(times[1]+times[0]+times[n-1]+times[1],times[n-1]+times[0]+times[n-2]+times[0]); /*无论采取哪种方式,最后的结果都是讲最慢的和次慢的运送过河,也就是数组的最后两位,所以此处可简单地将数组长度-2*/ n=n-2; } } return result; } }

  测试结果:

所花时间为:20

2.5 区间覆盖问题

  此问题的描述是:给定一个长度为m的区间,再给出n条线段的起点和终点(闭区间),求最少使用多少条线段可以将整个区间完全覆盖。要解此题的完整步骤如下:    1>在所有待选择的区间里,剔除起点和终点在所求范围之外的区间。    2>将所有区间按起点进行排序。    3>默认选中第一个点,然后在挑选点的过程中,需要遵循以下原则:1、新区间的起点要小于等于当前区间的终点;2、新区间的终点要大于当前区间的起点。    4>循环重复步骤3,直到当前区间的终点值>=预期的终点值,结束寻找区间的过程。   此处省略步骤1,重点关注后3步,示例代码如下:

package Greedy; /* * 数轴上有n个闭区间[ai, bi],选择尽量少的区间覆盖一条指定线段[s, t]。 */ public class CoverSection { public static void main(String[] args) { int[][] ections={{1,4},{2,4},{2,6},{3,5},{3,6},{3,7},{6,8}}; /*此处假设的是求[1,8区间,所需的最少区间数,因为起点是从1开始的, * 所以1不作为参数传入,只将终点值8作为参数传入即可]*/ int count =overSection(ections,8); System.out.println("最少需要"+count+"个区间"); } private static int overSection(int[][] ections,int length){ /*默认选中第一个*/ int count=1; System.out.println(ections[0][0]+","+ections[0][1]); /*初始化终点,默认为第一个区间的终点*/ int end=ections[0][1]; boolean stopFlag=false; for(int i=1; i=length,代表可以结束循环*/ if(end>=length){ stopFlag=true; } } } } /*结束循环*/ if(stopFlag) break; } return count; } }

  测试结果:

1,4 3,7 6,8 最少需要3个区间



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3