1)广度优先搜索算法和狄克斯特拉算法
广度优先搜索是一种图算法。
解决最短路径问题的算法被称为广度优先搜索。
图:
图由节点和边组成。一个节点可能与众多节点直接项链,这些节点被称为邻居。
可以使用图模拟不同的东西是如何相连的。
广度优先搜索是一种用于图的查找算法,可以帮助回答两类问题。
第一,从节点A出发,有前往节点B的路径吗?
第二,从节点A出发,前往节点B的哪条路径最短?
在广度优先搜索的执行过程中,搜索范围从起点开始逐渐向外延伸,即先检查一度关系,再检查二度关系。
算法使用步骤:
*(1)使用图来建立问题模型;
*(2)使用广度优先搜索算法解决问题。
广度优先搜索的运行时间为
O(人数 + 边数),这通常写作O(V + E),其中V为顶点(vertice)数,E为边数。
例子:
假设你经营着一家芒果农场,需要寻找芒果销售商,以便将芒果卖给他。为此,我们可以通过广度优先搜索算法,在朋友中查找出符合条件的芒果销售商。
广度优先搜索是一种用于图的查找算法,可帮助我们回答两类问题:
第一类问题:从节点A出发,有前往节点B的路径吗?(在你的人际关系网中,有芒果销售商吗?)
第二类问题:从节点A出发,前往节点B的哪条路径最短?(哪个芒果销售商与你的关系最近?)
将下列关系图用散列表实现:
概述广度优先搜索算法的工作原理:
private static Map
static
{
graph.put("you", new ArrayList(Arrays.asList(new String[] { "alice", "bob", "claire" })));
graph.put("bob", new ArrayList(Arrays.asList(new String[] { "anuj", "peggy" })));
graph.put("alice", new ArrayList(Arrays.asList(new String[] { "peggy" })));
graph.put("claire", new ArrayList(Arrays.asList(new String[] { "thom", "jonny" })));
}
public static boolean BFSearch(String name)
{
//用队列来模拟广度优先搜索中的远近关系,比如以you为起点搜素,先对其邻居即一度关系中的alice、bob和claire进行搜索,如果搜索不满足条件,
//则再对一度关系的邻居即二度关系进行搜索,如peggy、anuj、thom、jonny,这样就需要先将一度关系加入搜索名单,再将二度关系加入搜索名单,
//队列正好可以实现这种数据结构要求。
Deque
searchDeque.addAll(graph.get(name));
System.out.println("searchDeque is " + searchDeque);
List searched = new ArrayList<>(); // 用于记录检查过的人,避免导致无限循环
while (searchDeque.getFirst() != null)
{
String person = searchDeque.pop();
System.out.println("person is " + person);
if (!searched.contains(person)) //仅当这个人没检查过时才检查
{
if (personIsSeller(person))
{
System.out.println(person + " is a mango seller!");
return true;
}
else
{
if (graph.get(person) != null)
{
searchDeque.addAll(graph.get(person));
searched.add(person);
//将这个人标记为检查过
}
}
}
}
return false;
}
private static boolean personIsSeller(String name)
{
//名字最后一个字母为m的为满足条件的人
return name.charAt(name.length() - 1) == 'm';
}
狄克斯特拉算法:
适用场景:有向无环加权图
*步骤:
*(1)找出开销最小的节点(节点的开销指的是从起点出发前往该节点需要多长时间);
*(2)遍历当前节点的所有邻居,如果经当前节点前往该邻居更近,就更新该邻居的开销,同时将该邻居的父节点设置为当前节点;
*(3)重复这个过程,直到对图中的每个节点都这样做了;
*(4)计算最终路径,从终点倒过来找最短路径。
例子:
第一步:找出最便宜的节点,假设需要时间无穷大,节点B是最近的——2分钟。
第二步:计算经节点B前往其各个邻居所需要的时间。
找到一条前往节点A的更短路径。
对于节点B的邻居,如果找到前往它的更短路径,就更新其开销。在这里,你找到了:
前往节点A的更短路径(时间从6分钟缩短到5分钟);前往终点的更短路径(时间从无穷大缩短到7分钟)。
第三步:重复!
重复第一步:找出可在最短时间内前往的节点。你对节点B执行了第二步,除节点B外,可在最短时间内前往的节点是节点A。
重复第二步:更新节点A的所有邻居的开销。
你发现前往终点的时间为6分钟!
你对每个节点都运行了狄克斯特拉算法(无需对终点这样做)。现在,你知道:前往节点B需要2分钟;前往节点A需要5分钟;前往终点需要6分钟。
对比:
广度优先搜索来查找两点之间的最短路径,那时“最短路径”的意思是段数最少。在狄克斯特拉算法中,你给每段都分配了一个数字或权重,因此狄克斯特拉算法找出的是总权重最小的路径。
2)贪心算法(贪婪算法)
贪婪算法(贪心算法)是指在对问题进行求解时,在每一步选择中都采取最好或者最优(即最有利)的选择,从而希望能够导致结果是最好或者最优的算法。
贪婪算法所得到的结果往往不是最优的结果(有时候会是最优解),但是都是相对近似(接近)最优解的结果。
** 贪婪算法并没有固定的算法解决框架,算法的关键是贪婪策略的选择,根据不同的问题选择不同的策略。
** 必须注意的是策略的选择必须具备无后效性,即某个状态的选择不会影响到之前的状态,只与当前状态有关,所以对采用的贪婪的策略一定要仔细分析其是否满足无后效性。
基本思路
其基本的解题思路为:
1.建立数学模型来描述问题
2.把求解的问题分成若干个子问题
3.对每一子问题求解,得到子问题的局部最优解
4.把子问题对应的局部最优解合成原来整个问题的一个近似最优解
案例一
区间调度问题:
假设有如下课程,希望尽可能多的将课程安排在一间教室里:
课程 开始时间 结束时间
美术 9AM 10AM
英语 9:30AM 10:30AM
数学 10AM 11AM
计算机 10:30AM 11:30AM
音乐 11AM 12PM
这个问题看似要思考很多,实际上算法很简单:
1.选择结束最早的课,便是要在这教室上课的第一节课 2.接下来,选择第一堂课结束后才开始的课,并且结束最早的课,这将是第二节在教室上的课。
重复这样做就能找出答案,这边的选择策略便是结束最早且和上一节课不冲突的课进行排序,因为每次都选择结束最早的,所以留给后面的时间也就越多,自然就能排下越多的课了。
每一节课的选择都是策略内的局部最优解(留给后面的时间最多),所以最终的结果也是近似最优解(这个案例上就是最优解)。 (该案例的代码实现,就是一个简单的时间遍历比较过程)
案例二
背包问题:有一个背包,容量为35磅 , 现有如下物品
物品 重量 价格
吉他 15 1500
音响 30 3000
笔记本电脑 20 2000
显示器 29 2999
笔 1 200
要求达到的目标为装入的背包的总价值最大,并且重量不超出。
方便计算所以只有3个物品,实际情况可能是成千上万。
同上使用贪婪算法,因为要总价值最大,所以每次每次都装入最贵的,然后在装入下一个最贵的,选择结果如下:
选择: 音响 + 笔,总价值3000 + 200 = 3200
并不是最优解: 吉他 + 笔记本电脑, 总价值1500 + 2000 = 3500
当然选择策略有时候并不是很固定,可能是如下:
(1)每次挑选价值最大的,并且最终重量不超出:
选择: 音响 + 笔,总价值3000 + 200 = 3200
(2)每次挑选重量最大的,并且最终重量不超出(可能如果要求装入最大的重量才会优先考虑):
选择: 音响 + 笔,总价值3000 + 200 = 3200
(3)每次挑选单位价值最大的(价格/重量),并且最终重量不超出:
选择: 笔+ 显示器,总价值200 + 2999 = 3199
如上最终的结果并不是最优解,在这个案例中贪婪算法并无法得出最优解,只能得到近似最优解,也算是该算法的局限性之一。该类问题中需要得到最优解的话可以采取动态规划算法。
案例三
集合覆盖问题:
假设存在如下表的需要付费的广播台,以及广播台信号可以覆盖的地区。 如何选择最少的广播台,让所有的地区都可以接收到信号。
广播台 覆盖地区
K1 ID,NV,UT
K2 WA,ID,MT
K3 OR,NV,CA
K4 NV,UT
K5 CA,AZ
... ...
如何找出覆盖所有地区的广播台的集合呢,听起来容易,实现起来很复杂,使用穷举法实现:
(1) 列出每个可能的广播台的集合,这被称为幂集。假设总的有n个广播台,则广播台的组合总共有2?个
(2) 在这些集合中,选出覆盖全部地区的最小的集合,假设n不在,但是当n非常大的时候,假设每秒可以计算10个子集
广播台数量n 子集总数2ⁿ 需要的时间
5 32 3.2秒
10 1024 102.4秒
32 4294967296 13.6年
100 1.26*100³º 4x10²³年
目前并没有算法可以快速计算得到准备的值, 而使用贪婪算法,则可以得到非常接近的解,并且效率高:
选择策略上,因为需要覆盖全部地区的最小集合:
(1) 选出一个广播台,即它覆盖了最多未覆盖的地区即便包含一些已覆盖的地区也没关系
(2) 重复第一步直到覆盖了全部的地区
这是一种近似算法(approximation algorithm,贪婪算法的一种)。在获取到精确的最优解需要的时间太长时,便可以使用近似算法,判断近似算法的优劣标准如下:
* 速度有多快
* 得到的近似解与最优解的接近程度
在本例中贪婪算法是个不错的选择,不仅运行速度快,本例运行时间O(n2),最坏的情况,假设n个广播台,每个广播台就覆盖1个地区,n个地区,总计需要查询n*n=O(n2),实现可查看后面的java代码实现。
此时算法选出的是K1, K2, K3, K5,符合覆盖了全部的地区,可能不是预期中的K2, K3,K4,K5(也许预期中的更便宜,更便于实施等等)
小结
1.贪婪算法可以寻找局部最优解,并尝试与这种方式获得全局最优解
2.得到的可能是近似最优解,但也可能便是最优解(区间调度问题,最短路径问题(广度优先、狄克斯特拉))
3.对于完全NP问题,目前并没有快速得到最优解的解决方案
4.面临NP完全问题,最佳的做法就是使用近似算法
5.贪婪算法(近似算法)在大部分情况下易于实现,并且效率不错
/**
*贪婪算法 - 集合覆盖问题
*
*@author Administrator
*
*/
public class GreedyAlgorithm2
{
public static void main(String[] args)
{
// 初始化广播台信息
HashMap
broadcasts.put("K1", new HashSet(Arrays.asList(new String[] { "ID", "NV", "UT" })));
broadcasts.put("K2", new HashSet(Arrays.asList(new String[] { "WA", "ID", "MT" })));
broadcasts.put("K3", new HashSet(Arrays.asList(new String[] { "OR", "NV", "CA" })));
broadcasts.put("K4", new HashSet(Arrays.asList(new String[] { "NV", "UT" })));
broadcasts.put("K5", new HashSet(Arrays.asList(new String[] { "CA", "AZ" })));
// 需要覆盖的全部地区
HashSet
// 所选择的广播台列表
List
HashSet tempSet = new HashSet();
String maxKey = null;
while (allAreas.size() != 0)
{
maxKey = null;
for (String key : broadcasts.keySet())
{
tempSet.clear();
HashSet areas = broadcasts.get(key);
tempSet.addAll(areas);
// 求出2个集合的交集,此时tempSet会被赋值为交集的内容,所以使用临时变量
tempSet.retainAll(allAreas);
// 如果该集合包含的地区数量比原本的集合多
if (tempSet.size() > 0 && (maxKey == null || tempSet.size() > broadcasts.get(maxKey).size()))
{
maxKey = key;
}
}
if (maxKey != null)
{
selects.add(maxKey);
allAreas.removeAll(broadcasts.get(maxKey));
}
}
System.out.print("selects:" + selects);
}
}
3)动态规划
1、什么是动态规划?
这里参考百度百科,动态规划是求解决策过程最优化的数学方法。把多阶段过程转化为一系列单阶段问题,利用各阶段之间的关系,逐个求解,创立了解决这类过程优化问题的新方法——动态规划。
2、什么时候要用动态规划?
如果要求一个问题的最优解(通常是最大值或者最小值),而且该问题能够分解成若干个子问题,并且小问题之间也存在重叠的子问题,则考虑采用动态规划。
3、怎么使用动态规划?
我把下面称为动态规划五部曲:
1. 判题题意是否为找出一个问题的最优解
2. 从上往下分析问题,大问题可以分解为子问题,子问题中还有更小的子问题
3. 从下往上分析问题 ,找出这些问题之间的关联(状态转移方程)
4. 讨论底层的边界问题
5. 解决问题(通常使用数组进行迭代求出最优解)
动态规划算法的两种形式
上面已经知道动态规划算法的核心是记住已经求过的解,记住求解的方式有两种:①自顶向下的备忘录法 ②自底向上。
例子1:
剑指Offer(第二版)面试题14:剪绳子
给你一根长度为n的绳子,请把绳子剪成m段 (m和n都是整数,n>1并且m>1)每段绳子的长度记为k[0],k[1],…,k[m].请问k[0]k[1]…*k[m]可能的最大乘积是多少?
例如,当绳子的长度为8时,我们把它剪成长度分别为2,3,3的三段,此时得到的最大乘积是18.
看完题目,我们按照上面提到的“动态规划五部”解决问题
1、判题题意是否为找出一个问题的最优解
看到字眼是“可能的最大乘积是多少”,判断是求最优解问题,可以用动态规划解决;
2、从上往下分析问题,大问题可以分解为子问题,子问题中还有更小的子问题
题目中举了个例子:当绳子的长度为8时,我们把它剪成长度分别为2,3,3的三段,此时得到的最大乘积是18;我们可以从这里开始突破,把长度为8绳子的最大乘积分解为数个子问题,长度为8我们可以把它看成长度为1和7的绳子的和,或者长度 为2和6的绳子的和,或者长度为3和5的绳子的和and so on!
到这里,相信大家已经看到一丝真理了吧?
3. 从下往上分析问题 ,找出这些问题之间的关联(状态转移方程)
在第二点时,我们已经从上到下分析问题了,现在我们要从下往上分析问题了。分析可知,
f(8) 的值就是f(1)*f(7),f(2)*f(6),f(3)*f(5),f(4)*f(4)它们之中的最小值,即f(8) = Max{f(1)*f(7),f(2)*f(6),f(3)*f(5),f(4)*f(4)}
只要知道f(1)到f(7)的值就能求出f(8);对于f(7),只要知道f(1)到f(6)的值就能求出f(6);对于f(6),只要知道f(1)到f(5)的值就能求出f(6);以些类推,我们只要知道前几个边界的值,就能一步步迭代出后续的结果!
状态转移方程:f(n)=Max{f(n-i)*f(i)} i={1,2,3,…,n/2}
4. 讨论底层的边界问题
底层的边界问题说的就是最小的前几个数值的f(n)的值,本题中就是f(0)、f(1)、f(2)、f(3)的值
对于f(0),长度为0的绳子,没办法剪,没有意义
对于f(1),长度为1的绳子,没办法剪,设为1
对于f(2),长度为2的绳子,只有一种剪法,剪成两段长度为1的绳子,但剪后的乘积为1,比自身更小;如果不是求自身的值,要求乘积最大值的话就没必要剪。
对于f(3),长度为3的绳子,只有一种剪法,剪成两段长度为1和2的绳子,但剪后的乘积为2,比自身更小;如果不是求自身的值,要求乘积最大值的话也没必要剪。
5、解决问题
这一步就是写代码了
public static int cutting(int n)
{
// 长度小于等等于1没办法剪
if (n <= 1)
return 0;
// 对于f(2),长度为2的绳子,只有一种剪法,剪成两段长度为1的绳子,剪后的乘积为1
if (n == 2)
return 1;
// 对于f(3),长度为3的绳子,只有一种剪法,剪成两段长度为1和2的绳子,但剪后的乘积为2
if (n == 3)
return 2;
int max = 0;
// 数组用于存储绳子乘积最大值
int value[] = new int[n + 1];
value[0] = 0;
value[1] = 1;
// 剪后的乘积为1,比自身更小;如果不是求自身的值,要求乘积最大值的话就没必要剪
value[2] = 2;
// 剪后的乘积为2,比自身更小;如果不是求自身的值,要求乘积最大值的话也没必要剪
value[3] = 3;
// 从f(4)开始迭代
for (int i = 4; i <= n; i++)
{
max = 0;
for (int j = 1; j <= i / 2; j++)
{
int val = value[j] * value[i - j];
max = val > max ? val : max;
}
value[i] = max;
}
max = value[n];
return max;
}
对于刚学习动态规划的同学,看完上面那题会有点吃力吧?那我们再来一题简单的,给大家增加信心!
例子2:
跳台阶问题
一只青蛙一次可以跳上1级台阶,也可以跳上2级台阶。求该青蛙跳上一个n级台阶总共有多少种跳法。
1、判题题意是否为找出一个问题的最优解
这个我还真的看不出,直觉判断这题可以通过动态规划迭代出来….有经验的网友可以分享下看法,指导一下本小白白。
2、从上往下分析问题,大问题可以分解为子问题,子问题中还有更小的子问题
题目中没有给粟子,我们可以自己举点粟子。例如,跳上一个6级台阶台阶,有多少种跳法;由于青蛙一次可以跳两阶,也可以跳一阶,所以我们可以分成两个情况
1)青蛙最后一次跳了两阶,问题变成了“跳上一个4级台阶台阶,有多少种跳法”
2)青蛙最后一次跳了一阶,问题变成了“跳上一个5级台阶台阶,有多少种跳法”
由上可得f(6) = f(5) + f(4);
由此类推,f(4)=f(3) +f(2)
3、从下往上分析问题 ,找出这些问题之间的关联(状态转移方程)
跟上面的例题一相同,可以由f(1)逐渐迭代上去
由2可得,状态转移方程为:f(n)=f(n-1)+f(n-2)
4、边界情况分析
跳一阶时,只有一种跳法,所以f(1)=1
跳两阶时,有两种跳法,直接跳2阶,两次每次跳1阶,所以f(2)=2
跳两阶以上可以分解成上面的情况
5、解决问题
public static int jump(int n)
{
// 无意义的情况
if (n <= 0)
return 0;
if (n == 1)
return 1;
if (n == 2)
return 2;
// 数组用于存储跳n阶的跳法数
int[] value = new int[n + 1];
value[0] = 0;
value[1] = 1;
value[2] = 2;
for (int i = 3; i <= n; i++)
{
value[i] = value[i -1] + value[i - 2];
}
return value[n];
}
动态规划详细内容:
https://mp.weixin.qq.com/s?__biz=MzIxMjE5MTE1Nw==&mid=2653190796&idx=1&sn=2bf42e5783f3efd03bfb0ecd3cbbc380&chksm=8c990856bbee8140055c3429f59c8f46dc05be20b859f00fe8168efe1e6a954fdc5cfc7246b0&scene=21#wechat_redirect
相关链接:
https://blog.csdn.net/weixin_38278878/article/details/80037455
https://blog.csdn.net/rock_joker/article/details/68928150
https://blog.csdn.net/u013309870/article/details/75193592
参考书目:《算法》、《算法图解》