4大经典算法问题
如果我们将这四种算法思想分一下类,那贪心、回溯、动态规划可以归为一类,而分治单独可以作为一类,因为它跟其他三个都不大一样。为什么这么说呢?前三个算法解决问题的模型,都可以抽象成我们今天讲的那个多阶段决策最优解模型,而分治算法解决的问题尽管大部分也是最优解问题,但是,大部分都不能抽象成多阶段决策模型
贪心
就是每次取最优,最大解。局部最优,得出全局最优。它是动态规划的一种特例。
实际上,用贪心算法解决问题的思路,并不总能给出最优解,目前使用的比较少,需要针对非常特殊的数据场景。
分治算法(divide and conquer)
分治算法用四个字概括就是“分而治之”,将原问题划分成 n 个规模较小而结构与原问题相似的子问题,递归地解决这些子问题,然后再合并其结果,就得到原问题的解
分治算法是一种处理问题的思想,递归是一种编程技巧
要解决这种数据量大到内存装不下的问题,我们就可以利用分治的思想。我们可以将海量的数据集合根据某种方法,划分为几个小的数据集合,每个小的数据集合单独加载到内存来解决,然后再将小数据集合合并成大数据集合。实际上,利用这种分治的处理思路,不仅仅能克服内存的限制,还能利用多线程或者多机处理,加快处理的速度。
for example
在统计方面比较多,比如统计我国人口,要知道我国人口就要先知道每个省人口,要知道省人口就要知道每个市人口,要知道市人口就要知道每个区县人口,直到村社区,然后汇总求的总人数。
回溯
回溯的处理思想,有点类似枚举搜索。我们枚举所有的解,找到满足期望的解。为了有规律地枚举所有可能的解,避免遗漏和重复,我们把问题求解的过程分为多个阶段。每个阶段,我们都会面对一个岔路口,我们先随意选一条路走,当发现这条路走不通的时候(不符合期望的解),就回退到上一个岔路口,另选一种走法继续走。
常见问题,八皇后和背包的问题。
回溯的问题就是,时间复杂度为O(n^2), 指数级别,复杂度很高。一般使用递归来实现,我们会使用备忘录去记录已经计算过的情况,避免重复计算,有时候我们也可以根据实际情况进行剪枝,加快查询。
动态规划 dynamic programing 又叫 DP
我们把问题分解为多个阶段,每个阶段对应一个决策。我们记录每一个阶段可达的状态集合(去掉重复的),然后通过当前阶段的状态集合,来推导下一个阶段的状态集合,动态地往前推进。
大部分动态规划能解决的问题,都可以通过回溯算法来解决,只不过回溯算法解决起来效率比较低,时间复杂度是指数级的。动态规划算法,在执行效率方面,要高很多。尽管执行效率提高了,但是动态规划的空间复杂度也提高了,所以,很多时候,我们会说,动态规划是一种空间换时间的算法思想。
什么样的问题适合用动态规划来解决呢?换句话说,动态规划能解决的问题有什么规律可循呢?
一般是用动态规划来解决最优问题,而解决问题的过程,需要经历多个决策阶段。每个决策阶段都对应着一组状态。然后我们寻找一组决策序列,经过这组决策序列,能够产生最终期望求解的最优值。
三个特征
1. 最优子结构
最优子结构指的是,问题的最优解包含子问题的最优解。反过来说就是,我们可以通过子问题的最优解,推导出问题的最优解。
我们也可以理解为,后面阶段的状态可以通过前面阶段的状态推导出来。
2. 无后效性
后效性有两层含义,含义是,在推导后面阶段的状态的时候,我们只关心前面阶段的状态值,不关心这个状态是怎么一步一步推导出来的,且当前阶段的状态不受后面阶段的状态影响。
3. 重复子问题
不同的决策序列,到达某个相同的阶段时,可能会产生重复的状态。所以一般可以缓存一起来,避免重复计算。
动态规划DP解法思路
先通过回溯分析出是否存在重复子问题,以及是怎么发生的,然后尝试在纸上画一下,递归树,看是否能用回溯+备忘录的方式解决重复子问题。这样得到的结果与DP最后得到的结果效率差不多。
1. 状态转移表法/ 状态转移方程法
我们先画出一个状态表。状态表一般都是二维的(可能是三维/四维复杂场景),所以你可以把它想象成二维数组。其中,每个状态包含三个变量,行、列、数组值。我们根据决策的先后过程,从前往后,根据递推关系,分阶段填充状态表中的每个状态。最后,我们将这个递推填表的过程,翻译成代码,就是动态规划代码了。
比如: matrix[i][j] + Math.min(states[i][j-1], states[i-1][j]);
第一个,第二个默认值先找出来。
状态转移方程法有点类似递归的解题思路。我们需要分析,某个问题如何通过子问题来递归求解,也就是所谓的最优子结构。根据最优子结构,写出递归公式,也就是所谓的状态转移方程。有了状态转移方程,代码实现就非常简单了。一般情况下,我们有两种代码实现方法,一种是递归加“备忘录”,另一种是迭代递推。
min_dist(i, j) = w[i][j] + min(min_dist(i, j-1), min_dist(i-1, j))
状态转移方程是解决动态规划的关键
LeetCode
1. 最小路径和
https://leetcode-cn.com/problems/minimum-path-sum/ 采用dp方程
class Solution { public int minPathSum(int[][] grid) { if (grid == null || grid.length == 0 || grid[0].length == 0) { return 0; } int rows = grid.length, columns = grid[0].length; int[][] dp = new int[rows][columns]; dp[0][0] = grid[0][0]; for (int i = 1; i < rows; i++) { dp[i][0] = dp[i - 1][0] + grid[i][0]; } for (int j = 1; j < columns; j++) { dp[0][j] = dp[0][j - 1] + grid[0][j]; } for (int i = 1; i < rows; i++) { for (int j = 1; j < columns; j++) { dp[i][j] = Math.min(dp[i - 1][j], dp[i][j - 1]) + grid[i][j]; } } return dp[rows - 1][columns - 1]; } }
2. 硬币兑换
https://leetcode-cn.com/problems/coin-change/solution/322-ling-qian-dui-huan-by-leetcode-solution/
coins = [1, 2, 5], amount = 11
F(11)=min(F(11−1),F(11−2),F(11−5))+1
3. 乘积最大子数组
4. 三角形最小路径和
https://leetcode-cn.com/problems/triangle/
[ [2], [3,4], [6,5,7], [4,1,8,3] ]
定义二维 dp 数组「自底向上的递推」。
a. 状态定义:
dp[i][j]表示从点 (i, j) 到底边的最小路径和。
b. 状态转移:
dp[i][j] = min(dp[i + 1][j], dp[i + 1][j + 1]) + triangle[i][j]dp[i][j]=min(dp[i+1][j],dp[i+1][j+1])+triangle[i][j]
class Solution { public int minimumTotal(List<List<Integer>> triangle) { int n = triangle.size(); // dp[i][j] 表示从点 (i, j) 到底边的最小路径和。 int[][] dp = new int[n + 1][n + 1]; // 从三角形的最后一行开始递推。 for (int i = n - 1; i >= 0; i--) { for (int j = 0; j <= i; j++) { dp[i][j] = Math.min(dp[i + 1][j], dp[i + 1][j + 1]) + triangle.get(i).get(j); } } return dp[0][0]; } } 作者:sweetiee 链接:https://leetcode-cn.com/problems/triangle/solution/di-gui-ji-yi-hua-dp-bi-xu-miao-dong-by-sweetiee/ 来源:力扣(LeetCode) 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。