动态规划(Dynamic Programming, DP) 是一种在数学、计算机科学和经济学中使用的,通过把原问题分解为相对简单的子问题的方式来求解复杂问题的方法。动态规划常常适用于有重叠子问题和最优子结构性质的问题。
动态规划的核心概念
- 最优子结构:如果问题的最优解所包含的子问题的解也是最优的,就称该问题具有最优子结构。
- 无后效性:即“未来与过去无关”,只与当前的状态有关。
- 子问题重叠:在求解问题时,每次产生的子问题并不总是唯一的,有些子问题会被重复计算多次。动态规划算法正是利用了这种子问题的重叠性质,对每一个子问题只计算一次,然后将其计算结果保存在一个表格中,当再次需要计算已经计算过的子问题时,就可以直接查表,从而避免了大量的重复计算。
例子:斐波那契数列
斐波那契数列是一个很好的动态规划入门例子。斐波那契数列是这样一个数列:0、1、1、2、3、5、8、13、21、34... ,即第一项和第二项都是1,从第三项开始,每一项都等于前两项之和。
如果我们使用递归的方法来求解斐波那契数列的第n项,会有很多重复的计算。例如,为了计算f(5),我们需要计算f(4)和f(3),但是为了计算f(4),我们又需要计算f(3)和f(2),这样就造成了f(3)被重复计算了。
使用动态规划,我们可以避免这种重复计算。我们可以使用一个数组来保存已经计算过的斐波那契数,当需要计算某个斐波那契数时,首先检查这个数是否已经被计算过,如果是,则直接返回结果;否则,进行计算并保存结果。
伪代码
| 初始化 dp[0] = 0, dp[1] = 1  | |
| 对于 i 从 2 到 n:  | |
| dp[i] = dp[i-1] + dp[i-2]  | |
| 返回 dp[n] | 
这个伪代码展示了如何使用动态规划来计算斐波那契数列的第n项。数组dp用来保存已经计算过的斐波那契数,dp[i]表示斐波那契数列的第i项。通过遍历从2到n的所有整数,我们可以计算出斐波那契数列的第n项,并且避免了重复计算。
分治算法(Divide and Conquer) 是一种将问题分解为更小、独立的子问题,递归地解决这些子问题,然后将子问题的解合并起来以得出原问题的解的算法设计策略。分治算法通常涉及三个步骤:
- 分解(Divide):将原问题分解为若干个规模较小、相互独立、与原问题形式相同的子问题。
- 解决(Conquer):若子问题规模较小而容易被解决则直接解之,否则递归地解决各子问题。
- 合并(Combine):将各子问题的解合并为原问题的解。
例子:归并排序(Merge Sort)
归并排序是一个典型的分治算法应用。其基本思想是将两个(或两个以上)有序表合并成一个新的有序表,即把待排序序列分为若干个子序列,每个子序列是有序的。然后再把有序子序列合并为整体有序序列。
归并排序的步骤:
- 分解:将待排序的序列从中间位置分成两个长度相等的子序列(如果序列长度为奇数,则其中一个子序列的长度会多1)。
- 递归解决:对这两个子序列分别进行归并排序。
- 合并:将两个已排序的子序列合并成一个有序的序列。
伪代码:
| function mergeSort(arr)  | |
| if length(arr) <= 1  | |
| return arr  | |
| // 分解  | |
| mid = length(arr) / 2  | |
| left = mergeSort(arr[0...mid-1])  | |
| right = mergeSort(arr[mid...])  | |
| // 合并  | |
| return merge(left, right)  | |
| function merge(left, right)  | |
| result = []  | |
| i = 0  | |
| j = 0  | |
| // 合并两个已排序的列表  | |
| while i < length(left) and j < length(right)  | |
| if left[i] <= right[j]  | |
| append left[i] to result  | |
| i = i + 1  | |
| else  | |
| append right[j] to result  | |
| j = j + 1  | |
| // 添加剩余的元素(如果有的话)  | |
| while i < length(left)  | |
| append left[i] to result  | |
| i = i + 1  | |
| while j < length(right)  | |
| append right[j] to result  | |
| j = j + 1  | |
| return result | 
在这个例子中,归并排序首先将数组分成两半,然后递归地对这两半进行排序。当递归到只有一个元素的子数组时,排序就完成了(因为单个元素的数组已经是有序的)。然后,算法开始合并这些已排序的子数组,直到最终合并为一个完全排序的数组。