1、递归:
递归算法是一种直接或者间接调用自身函数或者方法的算法。说简单了就是程序自身的调用。
递归的两个必要条件
1、存在限制条件,当满足这个条件时,递归便不再继续。
2、每次递归调用之后越来越接近这个限制条件。
递归算法就是将原问题不断分解为规模缩小的子问题,然后递归调用方法来表示
问题的解。(用同一个方法去解决规模不同的问题)
- 递去:将递归问题分解为若干个规模较小,与原问题形式相同的子问题,这些子问题可以用相同的解题思路来解决
- 归来:当你将问题不断缩小规模递去的时候,必须有一个明确的结束递去的临界点(递归出口),一旦达到这个临界点即就从该点原路返回到原点,最终问题得到解决。
2、递推:
迭代不断使用变量的旧值推导出新值的过程,这一点与递推是一样的,递推一般要用到数组,会占用一定的空间,但更直观;迭代反复使用变量,占用空间较少。
3、分治法:
思想:
(1)将原始问题划分或者归结为规模更小的子问题
(2)递归或者迭代求解每个子问题
(3)将子问题的解综合得到原问题的解
应满足的条件:
(1)子问题与原始问题性质完全一样
(2)子问题之间可以彼此独立地求解
(3)递归停止时子问题可以直接求解
分治算法的特点:
• 将原问题归约为规模小的子问题,子问题与原 问题具有相同的性质.
• 子问题规模足够小时可直接求解.
• 算法可以递归也可以迭代实现.
• 算法的分析方法:递推方程.
例如:二分检索算法
方法一:循环
/**
* 循环版二分查找
*
* @param nums 数组
* @param n 数组长度
* @param value 要查找的值
* @return
*/
private static int bserach(int[] nums, int n, int value) {
int low = 0;
int high = n - 1;
while (low <= high) {
// 找出中间下标
int mid = low + ((high - low) >> 1);
if (nums[mid] > value) {
high = mid - 1;
} else if (nums[mid] < value) {
low = mid + 1;
} else {
return mid;
}
}
return -1;
}
方法一:递归
/**
* 递归算法实现二分查找
*
* @param nums 数组
* @param low 左下标
* @param high 右下标
* @param value 要查找的值
* @return
*/
private static int recursiveBserach(int[] nums, int low, int high, int value) {
if (low > high) return -1;
// 找出中间下标
int mid = low + ((high - low) >> 1);
if (nums[mid] == value) {
return mid;
} else if (nums[mid] > value) {
return recursiveBserach(nums, low, mid - 1, value);
} else {
return recursiveBserach(nums, mid + 1, high, value);
}
}
动态规划、分治、递归、递推和迭代是常见的问题求解方法,它们之间存在一定的关系。
1. 动态规划(Dynamic Programming):
动态规划是一种通过将问题分解为子问题并保存已解决子问题的结果来避免重复计算的方法。动态规划通常用于求解具有重叠子问题性质的问题。它可以采用递归或迭代的方式实现。
无后效性是动态规划算法及贪心算法的前提条件
无后效性:某阶段的状态一旦确定,则此后过程的决策不再受此前各种状态及决策的影响。
有后效性:就是某个状态之后要做的决策会受之前的状态及决策的影响。
举例:如下图有四乘四的网格,要从左上角走的右下角,条件是每次只能向下或向右走。
如下图从起点走到黑色圆圈位置S(2,2)有两种方案,但是S(2,2)接下来所做的决策不用考虑之前的决策,故是无后效性。
如果把条件改为:可以往前后左右走但是不能走重复的格子,那么接下来要做的决策就需要考虑之前的决策,故此时是有后效性。
动态规划有几个典型特征,最优子结构、状态转移方程、边界、重叠子问题。
2. 分治(Divide and Conquer):
分治是一种将复杂问题分解为若干个相互独立且具有相同结构的子问题,并将子问题的解合并得到原问题解的方法。分治通常通过递归的方式实现。如:二分检索
3. 递归(Recursion):
递归是一种通过调用自身来解决问题的方法。递归通常用于解决可以被分解为同类子问题的问题。递归可以是无限递归(没有终止条件)或有限递归(有终止条件),在有限递归中,递归可以是直接递归(函数直接调用自身)或间接递归(函数调用其他函数,最终间接调用到自身)。
4. 递推(Recurrence):
递推是一种通过已知的初始条件,利用递推关系式逐步推导出问题的解的方法。递推通常是自底向上地计算,在每个阶段都利用前一个阶段的结果来计算当前阶段的结果。递推可以是迭代的方式实现。
5. 迭代(Iteration):
迭代是一种通过多次重复执行相同的操作来逐步逼近问题解的方法。迭代通常用于在每一次迭代中,根据上一次迭代的结果进行更新,直到满足终止条件。迭代可以用循环结构实现。
总的来说,动态规划、分治、递归、递推和迭代都是问题求解的方法,它们在具体问题求解时的思路和实现方式上有所不同,但有时它们之间可以相互转化或结合使用。
重叠子问题性质 :
这个递归树怎么理解?就是说想要计算原问题 f(20),我就得先计算出子问题 f(19) 和 f(18),然后要计算 f(19),我就要先算出子问题 f(18) 和 f(17),以此类推。最后遇到 f(1) 或者 f(2) 的时候,结果已知,就能直接返回结果,递归树不再向下生长了。
递归算法的时间复杂度怎么计算?子问题个数乘以解决一个子问题需要的时间。
子问题个数,即递归树中节点的总数。显然二叉树节点总数为指数级别,所以子问题个数为 O(2^n)。
解决一个子问题的时间,在本算法中,没有循环,只有 f(n - 1) + f(n - 2) 一个加法操作,时间为 O(1)。
所以,这个算法的时间复杂度为 O(2^n),指数级别,爆炸。
观察递归树,很明显发现了算法低效的原因:存在大量重复计算,比如 f(18) 被计算了两次,而且你可以看到,以 f(18) 为根的这个递归树体量巨大,多算一遍,会耗费巨大的时间。更何况,还不止 f(18) 这一个节点被重复计算,所以这个算法及其低效。
这就是动态规划问题的第一个性质:重叠子问题。
1. 最优子结构
要符合「最优子结构」,子问题间必须互相独立。**啥叫相互独立?你肯定不想看数学证明,我用一个直观的例子来讲解。
比如说,你的原问题是考出最高的总成绩,那么你的子问题就是要把语文考到最高,数学考到最高… 为了每门课考到最高,你要把每门课相应的选择题分数拿到最高,填空题分数拿到最高… 当然,最终就是你每门课都是满分,这就是最高的总成绩。
得到了正确的结果:最高的总成绩就是总分。因为这个过程符合最优子结构,“每门科目考到最高”这些子问题是互相独立,互不干扰的。
但是,如果加一个条件:你的语文成绩和数学成绩会互相制约,此消彼长。这样的话,显然你能考到的最高总成绩就达不到总分了,按刚才那个思路就会得到错误的结果。因为子问题并不独立,语文数学成绩无法同时最优,所以最优子结构被破坏。
首先必须要明确一个问题:「最优子结构」是某些问题的一种特定性质,并不是动态规划问题专有的。 也就是说,很多问题其实都具有最优子结构(例如我们的贪心算法就具有最优子结构),只是其中大部分不具有重叠子问题,所以我们不把它们归为动态规划系列问题而已。
1.1 直接最优子结构
我先举个很容易理解的例子:
假设你们学校有 10 个班,你已有的数据是10个班同学的所有成绩。现在,你已经计算出了每个班的最高考试成绩。那么现在我要求你计算全校最高的成绩,你会不会算?
- 当然会,而且你不用重新遍历全校学生的分数进行比较,而是只要在这 10 个最高成绩中取最大的就是全校的最高成绩。
我给你提出的这个问题就符合最优子结构:可以从子问题的最优结果推出更大规模问题的最优结果。让你算每个班的最优成绩就是子问题,你知道所有子问题的答案后,就可以借此推出全校学生的最优成绩这个规模更大的问题的答案。
你看,这么简单的问题都有最优子结构性质,只是因为显然没有重叠子问题,所以我们简单地求最值肯定用不出动态规划。
1.2 间接最优子结构
再举个例子:
假设你们学校有 10 个班,你已知计算了每个班的最大分数差(最高分和最低分的差值)。那么现在我让你计算全校学生中的最大分数差,你会不会算?
- 可以想办法算,但是肯定不能通过已知的这 10 个班的最大分数差推到出来。因为这 10 个班的最大分数差不一定就包含全校学生的最大分数差,比如全校的最大分数差可能是 3 班的最高分和 6 班的最低分之差。
这次我给你提出的问题就不符合最优子结构,因为你没办通过每个班的最优值推出全校的最优值,没办法通过子问题的最优值推出规模更大的问题的最优值。想满足最优子结构,子问题之间必须互相独立。 全校的最大分数差可能出现在两个班之间,显然子问题不独立,所以这个问题本身不符合最优子结构。
那么遇到这种最优子结构失效情况,怎么办?策略是:改造问题 。对于最大分数差这个问题,我们不是没办法利用已知的每个班的分数差吗,那我只能这样写一段暴力代码:
int result = 0;
for (Student a : school) {
for (Student b : school) {
if (a is b) continue;
result = max(result, |a.score - b.score|);
}
}
return result;
改造问题,也就是把问题等价转化:最大分数差,不就等价于最高分数和最低分数的差么,那不就是要求最高和最低分数么,不就是我们讨论的第一个问题么,不就具有最优子结构了么?那现在改变思路,借助最优子结构解决最值问题,再回过头解决最大分数差问题,是不是就高效多了?
当然,上面这个例子太简单了,不过请读者回顾一下,我们做动态规划问题,是不是一直在求各种最值,本质跟我们举的例子没啥区别,无非就是需要再处理一下重叠子问题。
前文「不同定义不同解法」和「高楼扔鸡蛋进阶」就展示了如何改造问题,不同的最优子结构,可能导致不同的解法和效率。
再举个常见但也十分简单的例子,求一棵二叉树的最大值,不难吧(简单起见,假设节点中的值都是非负数):
int maxVal(TreeNode root) {
if (root == null)
return -1;
int left = maxVal(root.left);
int right = maxVal(root.right);
return max(root.val, left, right);
}
你看这个问题也符合最优子结构,以 root
为根的树的最大值,可以通过两边子树(子问题)的最大值推导出来,结合刚才学校和班级的例子,很容易理解吧。
当然这也不是动态规划问题,旨在说明,最优子结构并不是动态规划独有的一种性质,能求最值的问题大部分都具有这个性质;但反过来,最优子结构性质作为动态规划问题的必要条件,一定是让你求最值的,以后碰到那种恶心人的最值题,思路往动态规划想就对了,这就是套路。
动态规划不就是从最简单的 base case 往后推导吗,可以想象成一个链式反应,以小博大。但只有符合最优子结构的问题,才有发生这种链式反应的性质。
找最优子结构的过程,其实就是思考状态转移方程的过程,方程体现符合最优子结构,之后就可以写暴力解了,写出暴力解就可以看出有没有重叠子问题了,有则优化,无则 OK。这也是套路,经常刷题的朋友应该能体会。
这里就不举那些正宗动态规划的例子了,读者可以翻翻历史文章,看看状态转移是如何遵循最优子结构的,这个话题就聊到这,下面再来看另外个动态规划迷惑行为。
2. dp数组的遍历方向
我相信读者做动态规问题时,肯定会对递推写法(这里不考虑递推写法的动态规划)的dp
数组的遍历顺序有些头疼。我们拿二维 dp
数组来举例,有时候我们是正向遍历(所谓正向遍历,就是从数组下标小的地方开始):
int[][] dp = new int[m][n];
for (int i = 0; i < m; i++)
for (int j = 0; j < n; j++)
// 计算 dp[i][j]
有时候我们反向遍历:
for (int i = m - 1; i >= 0; i--)
for (int j = n - 1; j >= 0; j--)
// 计算 dp[i][j]
有时候可能会斜向遍历:
// 斜着遍历数组
for (int l = 2; l <= n; l++) {
for (int i = 0; i <= n - l; i++) {
int j = l + i - 1;
// 计算 dp[i][j]
}
}
甚至更让人迷惑的是,有时候发现正向反向遍历都可以得到正确答案,比如我们在「团灭股票问题」中有的地方就正反皆可。
那么,如果仔细观察的话可以发现其中的原因的。你只要把住两点就行了:
- 遍历的过程中,所需的状态必须是已经计算出来的。
- 遍历的终点必须是存储结果的那个位置。
下面来具体解释上面两个原则是什么意思。
比如编辑距离这个经典的问题,详解见前文「编辑距离详解」,我们通过对 dp
数组的定义,确定了 base case 是 dp[..][0]
和 dp[0][..]
,最终答案是 dp[m][n]
;而且我们通过状态转移方程知道 dp[i][j]
需要从 dp[i-1][j]
, dp[i][j-1]
, dp[i-1][j-1]
转移而来,如下图:
那么,参考刚才说的两条原则,你该怎么遍历 dp 数组?肯定是正向遍历:
for (int i = 1; i < m; i++)
for (int j = 1; j < n; j++)
// 通过 dp[i-1][j], dp[i][j - 1], dp[i-1][j-1]
// 计算 dp[i][j]
因为,这样每一步迭代的左边、上边、左上边的位置都是 base case 或者之前计算过的,而且最终结束在我们想要的答案 dp[m][n]
。
再举一例,回文子序列问题,详见前文 「子序列问题模板」 ,我们通过对 dp
数组的定义,确定了 base case 处在中间的对角线,dp[i][j]
需要从 dp[i+1][j]
, dp[i][j-1]
, dp[i+1][j-1]
转移而来,想要求的最终答案是 dp[0][n-1]
,如下图:
这种情况根据刚才的两个原则,就可以有两种正确的遍历方式:
要么从左至右斜着遍历,要么从下向上从左到右遍历,这样才能保证每次 dp[i][j] 的左边、下边、左下边已经计算完毕,得到正确结果。
现在,你应该理解了这两个原则,主要就是看 base case 和 最终结果 的存储位置,保证遍历过程中使用的数据都是计算完毕的就行,有时候确实存在多种方法可以得到正确答案,可根据个人口味自行选择。
0-1背包问题:(最优子结构性质)
解决你的疑惑
有的人会这么认为(我最开始也是这个疑惑):
假如面对第n个物品时候,背包剩余空间为5,此时第n个物品为占据空间6,价值为10的物品,可以将原来背包里面占据空间为1的,价值为2的物品换出来啊!并把第n个物品加进去,这样总价值多了10-2=8,这不就说明没有最优子结构了么?
而实际上,当你不考虑第n个物品时候(即在子问题中),你的背包容量也是将第n个物品所占体积减去后的容量(记住这句话,看完后面后可以回来再体会一下)
应该这样理解:
当你面对所有n个物品时,选择了一号物品即y1=1时,他的子问题便是背包总空间减去w1y1时候,选择后n-1个物品的最优解。就像上面疑惑中举出的例子,说明总体最优解包含第n个物品,说明在选择前n-1个物品的最优解时(子问题),此时背包容量仅为最初容量减去6,并考虑此时的最优解,此时的最优解也一定不会包括那个被“替换”出去的占据空间为1的,价值为2的物品,因为选择这件物品的话,面对第n个物品最后的空间会为5,而非要求的6(总体最优解中面对第n个物品时候需要的剩余容量)。
备忘录:
- 第一步,f(10)= f(9) + f(8),f(9) 和f(8)都需要计算出来,然后再加到备忘录中,如下:
- 第二步, f(9) = f(8)+ f(7),f(8)= f(7)+ f(6), 因为 f(8) 已经在备忘录中啦,所以可以省掉,f(7),f(6)都需要计算出来,加到备忘录中~
第三步, f(8) = f(7)+ f(6),发现f(8),f(7),f(6)全部都在备忘录上了,所以都可以剪掉。
所以呢,用了备忘录递归算法,递归树变成光秃秃的树干咯,如下:
此处可以理解为深度遍历
1动态规划的递归写法和递归推法
动态规划(Dynamic Programming, DP) 是一种用来解决一类最优化问题的算法思想。简单来说,动态规划将一一个 复杂的问题分解成若千个子问题,通过综合子问题的最优解来得到原问题的最优解。需要注意的是,动态规划会将每个求解过的子问题的解记录下来,这样当下一次碰到同样的子问题时,就可以直接使用之前记录的结果,而不是重复计算。注意:虽然动态规划采用这种方式来提高计算效率,但不能说这种做法就是动态规划的核心^(后面会说明这一点)。
一般可以使用递归或者递推的写法来实现动态规划,其中递归写法在此处又称作记忆化搜索。
1.1动态规划的递归写法
先来讲解递归写法。通过这部分内容的学习,读者应能理解动态规划是如何记录子问题的解,来避免下次遇到相同的子问题时的重复计算的。
以斐波那契(Fibonacci) 数列为例,斐波那契数列的定义为E =1,F=1,F=F-+F-2(n≥2)。在4.3节中是按下面的代码来计算的:
int F(int n){
if(n==0||n==1)
return 1;
else
return F(n-1)+F(n-2);
}
递归时间复杂度 = 解决一个子问题时间*子问题个数
事实上,这个递归会涉及很多重复的计算。如图11-1所示,当n=5时,可以得到F(5)= F(4)+ F(3), 接下来在计算F(4)时又会有F(4)= F(3)+ F(2)。 这时候如果不采取措施,F(3)将会被计算两次。可以推知,如果n很大,重复计算的次数将难以想象。事实上,由于没有及时保存中间计算的结果,实际复杂度会高达O(2^n),即每次都会计算F(n- 1)和F(n - 2)这两个分支,基本不能承受n较大的情况。
为了避免重复计算,可以开一个一-维数组dp,用以保存已经计算过的结果中dp[n]记录F(n)的结果,并用dp[n]=-1表示F(n)当前还没被计算过。
int dp[MAXN];
memset(a,-1,sizeof(a));//注意memset在<string>中,且只能赋值0或-1;
然后就可以在递归当中判断dp[n]是否是-1:如果不是-1,说明已经计算过F(n),直接返回dp[n]就是结果;否则,按照递归式进行递归。代码如下:
<span style="color:#000000"><span style="background-color:#282c34"><code>int F(int n){
if(n==0||n==1)
return 1;
if(dp[n]!=-1)
return dp[n];
else
{
dp[n]=F(n-1)+F(n-2);
return dp[n];
}
}
</code></span></span>
- 计算过的内容记录了下来,于是当下次再碰到需要计算相同的内容时,就能直接使用上次计算的结果,这可以省去大半无效计算,而这也是记忆化搜索这个名字的由来。如图11-2所示,通过记忆化搜索,把复杂度从0(2")降到了O(n),也就是说,用一个O(n)空间的力量就让复杂度从指数级别降低到了线性级别。
通过上面的例子可以引申出一个概念:如果-一个问题可以被分解为若千个子问题,且这些子问题会重复出现,那么就称这个问题拥有重叠子问题(Overlapping Subproblems)。 动态规划通过记录重叠子问题的解,来使下次碰到相同的子问题时直接使用之前记录的结果,以此避免大量重复计算。因此,一个问题必须拥有重叠子问题,才能使用动态规划去解决。
1.3动态规划的递推写法
以经典的数塔问题为例,将一些数字排成数塔的形状,其中第- -层有一个数字,第二层有两个数字…第n层有n个数字。现在要从第- -层走到第n层,每次只能走向下一层连接的两个数字中的一个,问:最后将路径上所有数字相加后得到的和最大是多少?
按照题目的描述,如果开一个二维数组f,其中fi]i]存f[1][1]=5, f[2][1]= 8, f[2][2]=3, f[3][1]= 12,… f[5][4]=9, f[5][5]=4。
此时,如果尝试穷举所有路径,然后记录路径上数字和的最大值,那么由于每层中 8 3的每个数字都会有两条分支路径,因此可以得到时间复杂度为0(2"),这在n很大的情况 16下是不可接受的。那么,产生这么大复杂度的原因是什么?下面来分析一下。
一开始,从第一层的5出发,按5→8→7的路线来到7,并枚举从7出发的到达最底 9 5 3 9 4层的所有路径。但是,之后当按5→3→7的图11-3 数塔问题示意图路线再次来到7时,又会去枚举从7出发的到达最底层的所有路径,这就导致了从7出发的到达最底层的所有路径都被反复地访问,做了许多多余的计算。事实上,可以在第一次枚举从7出发的到达最底层的所有路径时就把路径上能产生的最大和记录下来,这样当再次访问到7这个数字时就可以直接获取这个最大值,避免重复计算。
由上面的考虑,不妨令dp[i][j]表示从第i行第j个数字出发的到达最底层的所有路径中能得到的最大和,例如dp[3][2]就是图中的7到最底层的路径最大和。在定义这个数组之后,dp[1][1]就是最终想要的答案,现在想办法求出它。
注意到一个细节:如果要求出“从位置(1, 1)到达最底层的最大和”dp[1][1],那么一-定要先求出它的两个子问题“从位置(2, 1)到达最底层的最大和dp[2][1]”和“从位置(2, 2)到达最底层的最大和dp[2][2]”,即进行了一次决策:走数字5的左下还是右下。于是dp[1][1]就是dp[2][1]和dp[2][2]的较大值加上5。写成式子就是:
dp[1][1]= max(dp[2][1],dp[2][2])+ f[1][1]
由此可以归纳得到这么一一个信息:如果要求出dp[i][i],那么- -定要先求出它的两个子问题“从位置(i + 1, j)到达最底层的最大和dp[i+ 1][i]” 和“从位置(i + 1,j+ 1)到达最底层的最大和dp[i+ 1][j+ 1]”,即进行了一次决策:走位置(i, j)的左下还是右下。于是dp[i][i]就是dp[i+ 1][i]和dp[i+ 1][ + 1]的较大值加上f[i][i]。写成式子就是:
<span style="color:#000000"><span style="background-color:#282c34"><code> dp[i][j]= max(dp[i + 1][j],dp[i+ 1l[j+ 1])+ f[i][ij]
</code></span></span>
- 1
把dp[i][i]称为问题的状态,而把上面的式子称作状态转移方程,它把状态dp[i]ij]转移为dp[i+1][i]和dp[i+1][i+1]。可以发现,状态dp[i][j]只与第i + 1层的状态有关,而与其他层的状态无关,这样层号为i的状态就总是可以由层号为i+ 1的两个子状态得到。那么,如果总是将层号增大,什么时候会到头呢?可以发现,数塔的最后一-层的dp值总是等于元素本身,即dp[n]ij] = f[n][i] (1≤j≤n),把这种可以直接确定其结果的部分称为边界,而动态规划的递推写法总是从这些边界出发,通过状态转移方程扩散到整个dp数组。
这样就可以从最底层各位置的dp值开始,不断往上求出每一层各位置的 dp值,最后就会得到dp[1][1],即为想要的答案。下面根据这种思想写出动态规划的代码:
<span style="color:#000000"><span style="background-color:#282c34"><code> #include<iostream>
#include<cstdio>
#include<algorithm>
using namespace std;
const int maxn=1000;
int f[maxn][maxn],dp[maxn][maxn];
int main()
{
int n;
for(int i=1;i<=n;i++)
for(int j=1;j<=i;j++)
scanf("%d",&f[i][j]);//输入数塔
for(int j=1;j<=n;j++)
dp[n][j]=f[n][j];//输入边界;
//从n-1层不断往上计算出dp[i][j];
for(int i=n-1;i>=1;i--)
{
for(int j=1;j<=n;j++)
//状态转移方程
{
dp[i][j]=max(dp[i+1][j],dp[i+1][j+1])+f[i][j];
}
}
printf("%d\n",dp[1][1]);//dp[1][1]即为需要的答案
return 0;
}
</code></span></span>
显然,使用递归也可以实现上面的例子(即从dp[1][1]开始递归,直至到达边界时返回结果)。两者的区别在于:使用递推写法的计算方式是自底向上(Bottom-up Approach),即从边界开始,不断向上解决问题,直到解决了目标问题;而使用递归写法的计算方式是自顶向下(Top-down Approach),即从目标问题开始,将它分解成子问题的组合,直到分解至边界为止。
通过上面的例子再引申出一个概念:如果一个问题的最优解可以由其子问题的最优解有效地构造出来,那么称这个问题拥有最优子结构(Optimal Substructure)。最优子结构保证了动态规划中原问题的最优解可以由子问题的最优解推导而来。因此,一个问题必须拥有最优子结构,才能使用动态规划去解决。例如数塔问题中,每-一个位置的dp值都可以由它的两个子问题推导得到。
至此,重叠子问题和最优子结构的内容已介绍完毕。需要指出,一个问题必须拥有重叠子问题和最优子结构,才能使用动态规划去解决。、下面指出这两个概念的区别:
①分治与动态规划。分治和动态规划都是将问题分解为子问题,然后合并子问题的解得到原问题的解。但是不同的是,分治法分解出的子问题是不重叠的,因此分治法解决的问题不拥有重叠子问题,而动态规划解决的问题拥有重叠子问题。例如,归并排序和快速排序都是分别处理左序列和右序列,然后将左右序列的结果合并,过程中不出现重叠子问题,因此它们使用的都是分治法。另外,分治法解决的问题不一- 定是最优化问题,而动态规划解决的问题一-定是最优化问题。
②贪心与动态规划。贪心和动态规划都要求原问题必须拥有最优子结构。二者的区别在于,贪心法采用的计算方式类似于上面介绍的“自顶向下”,但是并不等待子问题求解完毕后再选择使用哪一一个,而是通过一种策略直接选择-一个子 问题去求解,没被选择的子问题就不去求解了,直接抛弃。也就是说,它总是只在上一步选择的基础上继续选择,因此整个过程以一"种单链的流水方式进行,显然这种所谓“最优选择”的正确性需要用归纳法证明。例如对数塔问题而言,贪心法从最上层开始,每次选择左下和右下两个数字中较大的一个,一直到最底层得到最后结果,显然这不一定可以得到最优解。而动态规划不管是采用自底向上还是自顶向下的计算方式,都是从边界开始向上得到目标问题的解。也就是说,它总是会考虑所有子问题,并选择继承能得到最优结果的那个,对暂时没被继承的子问题,由于重叠子问题的存在,后期可能会再次考虑它们,因此还有机会成为全局最优的一部分,不需要放弃。所以贪心是一种壮士断腕的决策,只要进行了选择,就不后悔;动态规划则要看哪个选择笑到了最后,暂时的领先说明不了什么。