引言:从斐波那契数列看动态规划
斐波那契数列:Fn = Fn-1 + Fn-2 ( n = 1,2 fib(1) = fib(2) = 1)
练习:使用递归和非递归的方法来求解斐波那契数列的第 n 项
代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
|
为了让我们的说服更有理一些,这里写了一个装饰器,我们通过运行时间看。同样对于上面两个函数,一个递归,一个非递归,我们输入 n=15
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
总结来说,就是递归非常非常的慢,那非递归相对来说就比较快了。那为什么呢?就是为什么递归的效率低。我们上面代码也说过了,就是对子问题进行重复计算了。那第二个函数为什么快呢,我们将每次的计算结果存在了函数里,直接调用,避免了重复计算(当然不是说所有的递归都会重复计算子问题),第二个函数我们其实可以看做是动态规划的思想,从上面的代码来看:
动态规划的思想==递推式+重复子问题
怎么理解呢,下面继续学习。
1,什么是动态规划
动态规划(dynamic programming)是运筹学的一个分支,是求解决策过程(decision process)最优化的数学方法。把多阶段过程转化为一系列单阶段问题,利用各阶段之间的关系,逐个求解,创立了解决这类过程优化问题的新方法——动态规划。
1.1,使用动态规划特征
- 1. 求一个问题的最优解
- 2. 大问题可以分解为子问题,子问题还有重叠的更小的子问题
- 3. 整体问题最优解取决于子问题的最优解(状态转移方程)
- 4. 从上往下分析问题,从下往上解决问题
- 5. 讨论底层的边界问题
1.2,动态规划的基本思想
若要解一个给定问题,我们需要解其不同部分(即子问题),再合并子问题的解以得出原问题的解。通常许多子问题非常相似,为此动态规划法试图仅仅解决每个子问题一次,从而减少计算量:一旦某个给定子问题的解已经算出,则将其记忆化存储,以便下次需要同一个子问题解之时直接查表。这种做法在重复子问题的数目关于输入的规模呈指数增长时特别有效。
动态规划最重要的有三个概念:1、最优子结构 2、边界 3、状态转移方程
所以我们在学习动态规划要明白三件事情:
1,目标问题
2,状态的定义:opt[n]
3,状态转移方差:opt[n] = best_of(opt[n-1], opt[n-2])
2,钢条切割问题
某公司出售钢条,出售价格与钢条长度直接的关系如下表:
问题:现在有一条长度为 n 的钢条和上面的价格表,求切割钢条方案,使得总收益最大。
分析:长度为4的钢条的所有切割方案如下:(C方案最优)
思考:长度为 n 的钢条的不同切割方案有几种?
下面是当长度为n的时候,最优价格的表格( i 表示长度为 n ,r[i] 表示最优价格)
2.1,递推式解决钢条切割问题
设长度为 n 的钢条切割后最优收益值为 Rn,可以得到递推式:
第一个参数Pn 表示不切割
其他 n-1个参数分别表示另外 n-1种不同切割方案,对方案 i=1,2,...n-1 将钢条切割为长度为 i 和 n-i 两段
方案 i 的收益为切割两段的最优收益之和,考察所有的 i,选择其中收益最大的方案
2.2,最优子结构解决钢条切割问题
可以将求解规模为 n 的原问题,划分为规模更小的子问题:完成一次切割后,可以将产生的两段钢条看成两个独立的钢条切割问题。
组合两个子问题的最优解,并在所有可能的两段切割方案中选取组合收益最大的,构成原问题的最优解。
钢条切割满足最优子结构:问题的最优解由相关子问题的最优解组合而成,这些子问题可以独立求解。
钢条切割问题还存在更简单的递归求解方法:
- 从钢条的左边切割下长度为 i 的一段,只对右边剩下的一段继续进行切割,左边的不再切割
- 递推式简化为:
- 不做切割的方案就可以描述为:左边一段长度为 n,收益为 pn,剩下一段长度为0,收益为 r0=0.
2.3,钢条切割问题——自顶向下递归代码及其时间复杂度
代码如下:
1 2 3 4 5 6 7 |
|
如下图所示,当钢条的长度增加时候,切割的方案次数随着指数增加。当n=1的时候切割1次,n=2的时候切割2次,n=3的时候切割4次,n=4的时候切割8次。。。。
所以:自顶向下递归实现的时间复杂度为 O(2n)
2.4,两种方法的代码实现
代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 |
|
我们通过计算时间,发现第二个递归方法明显比第一个递归方法快很多。那么是否还有更简单的方法呢?肯定有,下面学习动态规划。
2.5,动态规划解决钢条切割问题
递归算法由于重复求解相同子问题,效率极低。即使优化过后的递归也效率不高。那这里使用动态规划。
动态规划的思想:
- 每个子问题只求解一次,保存求解结果
- 之后需要此问题时,只需要查找保存的结果
动态规划解法代码:
1 2 3 4 5 6 7 8 |
|
或者便于理解这样:
1 2 3 4 5 6 7 8 |
|
时间复杂度: O(n2)
2.6,钢条切割问题——重构解
如何修改动态规划算法,使其不仅输出最优解,还输出最优切割方案?
对于每个子问题,保存切割一次时左边切下的长度
下图为r[i] 表示最优切割的价格,s[i]表示切割左边的长度。
代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
|
2.7,什么问题可以使用动态规划方法?
1,最优子结构
- 原问题的最优解中涉及多少个子问题
- 在确定最优解使用那些子问题时,需要考虑多少种选择
2,重叠子问题
3,最长公共子序列
一个序列的子序列是在该序列中删去若干元素后得到的序列。例如:ABCD 和 BDF 都是 ABCDEFG 的子序列。
在一个序列中,子串是连续的,子序列可以不连续。
最常公共子序列(LCS)问题:给定两个序列 X 和 Y,求 X 和 Y 长度最大的公共子序列。例如 X = ABBCBDE, Y = DBBCDB , LCS(X, Y) = BBCD 。
应用场景:字符串相似度比对。
3.1,最长公共子序列的思路——暴力穷举法
当X的长度为m,Y的长度为n,则时间复杂度为: 2^(m+n)
虽然我们最先想到的时暴力穷举法,但是很显然,由其时间复杂度可知,这是不可取的。
3.2,最长公共子序列的思路——LCS是否具有最优子结构性质
例如:要求 a = ABCBDAB 与 b = BDCABA 的LCS:
由于最后一位 B!= A
因此LCS(a, b)应该来源于 LCS(a[: -1], b)与 LCS(a, b[: -1]) 中更大的哪一个。
最优解的递推式如下:
c[i,j] 表示 Xi 和 Yj 的LCS 长度。
3.3,最长公共子序列的代码
代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 |
|
4,最大子序和
给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大值。
示例:输入:[-2, 1, -3, 4, -1, 2, 1, -5, 4] 输出:输出:6
思路:我们首先分析题目,为什么最大和的连续子数组不包括其他的元素而是这几个呢?如果我们想在现有的基础上去扩展当前连续子数组,相邻的元素是一定要被加入的,而相邻元素可能会减损当前的和。
4.1,遍历法
算法过程:遍历数组,用 sum 去维护当前元素加起来的和,当 sum 出现小于0的情况时,我们把它设为0,然后每次都更新全局最大值。
1 2 3 4 5 6 7 8 9 |
|
那看起来这么简单,如何理解呢?一开始思考数组是个空的,我们每次选一个 nums[i] 加入当前数组中新增了一个元素,也就是用动态的眼光去考虑。代码简单为什么就能达到效果呢?
我们进行的加和是按照顺序来的,当我们i 选出来后,加入当前 sum,这时候有两种情况:
1,假设我们当前 sum 一致都大于零,那每一次计算的 sum 都是包括开头元素的一端子序列。
2,出现小于0的时候,说明我们当前子序列第一次小于零,所以我们现在形成的连续数组不能包括之前的连续子序了,于是抛弃他们,重新开始新的子序。
4.2,动态规划
设sum[i]为以第i个元素结尾的最大的连续子数组的和。假设对于元素i,所有以它前面的元素结尾的子数组的长度都已经求得,那么以第i个元素结尾且和最大的连续子数组实际上,要么是以第i-1个元素结尾且和最大的连续子数组加上这个元素,要么是只包含第i个元素,即sum[i]= max(sum[i-1] + a[i], a[i])。可以通过判断sum[i-1] + a[i]是否大于a[i]来做选择,而这实际上等价于判断sum[i-1]是否大于0。由于每次运算只需要前一次的结果,因此并不需要像普通的动态规划那样保留之前所有的计算结果,只需要保留上一次的即可,因此算法的时间和空间复杂度都很小。
代码如下:
1 2 3 4 5 6 7 8 |
|
只要遍历一遍。nums[i]表示的是以当前这第i号元素结尾(看清了一定要包含当前的这个i)的话,最大的值无非就是看以i-1结尾的最大和的子序能不能加上我这个nums[i],如果nums[i]>0的话,则加上。注意我代码中没有显式地去这样判断,不过我的Max表达的就是这个意思,然后我们把nums[i]确定下来。
动态规划需要和回溯法搭配着使用,动态规划只负责求最优解,而回溯法则可以找到最优值的路径。
5,回溯法
回溯法是一种选优搜索法,按选优条件向前搜索,以达到目标。但当探索到某一步时,发现原先选择并不优或达不到目标,就退回一步重新选择,这种走不通就退回再走的技术为回溯法,而满足回溯条件的某个状态的点称为 “回溯点”。许多复杂的,规模较大的问题都可以使用回溯法,有“通用解题方法”的美称。回溯法有“通用的解题法”之称,也叫试探法,它是一种系统的搜索问题的解的方法。简单来说,回溯法采用试错的方法解决问题,一旦发现当前步骤失败,回溯方法就返回上一个步骤,选择另一种方案继续试错。
5.1 回溯法的基本思想
从一条路往前走,能进则进,不能进则退回来,换一条路再试。
5.2 回溯法的一般步骤
1,定义一个解空间(子集树,排序树二选一)
2,利用适用于搜索的方法组织解空间
3,利用深度优先法搜索解空间
4,利用剪枝函数避免移动到不可能产生解的子空间
5.3 回溯法针对问题的特点
回溯算法针对的大多数问题有以下特点:问题的答案有多个元素(可向下成走迷宫是有多个决定),答案需要一些约束(比如数独),寻找答案的方式在每一个步骤相同。回溯算法逐步构建答案,并在确定候选元素不满足约束后立刻放弃候选元素(一旦碰墙就返回),直到找到答案的所有元素。
5.4回溯法题目——查找单词
问题描述:你玩过报纸上那种查找单词的游戏吗?就是那种在一堆字母中横向或竖向找出单词的游戏。小明在玩一个和那个很像的游戏,只不过现在不仅可以上下左右连接字母,还可以拐弯。如图所示,输入world,就会输出“找到了”。
5.5 回溯法题目——遍历所有的排列方式
问题描述:小米最近有四本想读的书:《红色的北京》,《黄色的巴黎》,《蓝色的夏威夷》,《绿色的哈萨里》,如果小明每次只能从图书馆借一本书,他一共有多少种借书的顺序呢?
回溯法是一种通过探索所有可能的候选解来找出所欲的解的算法。如果候选解被确认,不是一个解的话(或者至少不是最后一个解),回溯算法会通过在上一步进行一些变换排期该解。即回溯并且再次尝试。
这里有一个回溯函数,使用第一个整数的索引作为参数 backtrack(first)。
1,如果第一个整数有索引 n,意味着当前排列已完成。
2,遍历索引 first 到索引 n-1 的所有整数 ,则:
- 在排列中放置第 i 个整数,即 swap(nums[first], nums[i])
- 继续生成从第 i 个整数开始的所有排列:backtrack(first +1)
- 现在回溯,通过 swap(nums[first], nums[i]) 还原。
代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
|
便于理解的代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
方法二:走捷径(直接使用Python的库函数,迭代函数itertools)
1 2 3 4 5 |
|
5.6 回溯法问题——经典问题的组合
问题描述:小明想上两门选修课,他有四种选择:A微积分,B音乐,C烹饪,D设计,小明一共有多少种不同的选课组合?
当然第一个方法就是走捷径!,直接使用python的库函数itertools进行迭代:
1 2 3 4 5 |
|
下面开始回溯法的学习。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
5.7 回溯法问题——八皇后问题
问题描述:保安负责人小安面临一个难题,他需要在一个8x8公里的区域里修建8个保安站点,并确保每一行、每一列和每一条斜线上都只有一个保安站点。苦恼的小安试着尝试布置了很多遍,但每一次都不符合要求。小安求助程序员小明,没过多久小明就把好几个布置方案(实际上,这个问题有92种答案)发给了小安,其中包括下面执行结果截图,试问小明是怎么做到的。
6,算法综合作业
这是所有的算法学完后的综合作业,当然这也是算法学习的一个总结。当然下面的问题我都有涉及,这里不做一一解答。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
原文地址:https://www.cnblogs.com/cider/p/11831174.html