一、动态规划(Dynamic Programming)
术语“动态规划”最初是在 1940 年代由 理查德·贝尔曼 用来描述解决问题的过程,在这个过程中,人们需要一个接一个地找到最佳决策。到 1953 年,他将其精炼成为现代的含义,特别是指将较小的决策问题嵌套在较大的决策中,并且该领域随后被电气电子工程师学会认可为系统分析和工程学主题。贝尔曼的贡献以贝尔曼方程的名义被铭记,它是动态规划的核心结果,它以递归 (计算机科学)形式重申了优化问题。
动态规划是一种解决多阶段决策问题的优化方法。通过将问题分解为一系列重叠的子问题,并使用子问题的解来构建更大问题的解。动态规划通常用于优化递归算法,避免重复计算,提高效率。
说人话,动态规划是一种在数学、管理科学、计算机科学、经济学和生物信息学中使用的,通过把原问题分解为相对简单的子问题的方式求解复杂问题的方法。
动态规划背后的基本思想非常简单。大致上,若要解一个给定问题,我们需要解其不同部分(即子问题),再根据子问题的解以得出原问题的解。
通常许多子问题非常相似,为此动态规划法试图仅仅解决每个子问题一次,从而减少计算量:一旦某个给定子问题的解已经算出,则将其记忆化存储,以便下次需要同一个子问题解之时直接查表。这种做法在重复子问题的数目关于输入的规模呈指数增长时特别有用。
基本思想:
-
找到最优子结构(Optimal Substructure):问题的最优解可以由子问题的最优解构建而成。
-
存储中间结果(Memoization):为了避免重复计算,需要将每个子问题的解存储起来。
-
自底向上构建解(Bottom-Up Approach):通过先解决较小的子问题,逐步构建出更大问题的解。
TypeScript 动态规划示例:
考虑一个简单的动态规划问题 - 计算斐波那契数列的第 n 项。
function fibonacciDP(n: number): number {
if (n <= 1) {
return n;
}
const memo: number[] = new Array(n + 1).fill(-1);
function fib(n: number): number {
if (n <= 1) {
return n;
}
// 检查是否已经计算过该子问题
if (memo[n] !== -1) {
return memo[n];
}
// 计算并存储中间结果
memo[n] = fib(n - 1) + fib(n - 2);
return memo[n];
}
return fib(n);
}
// 示例:计算斐波那契数列的第 5 项
console.log(fibonacciDP(5)); // 输出 5
在上面的例子中,使用了动态规划的思想,通过 memo
数组存储了中间结果,避免了重复计算,提高了计算效率。
二、贪心算法(Greedy Algorithm):
贪心算法是一种通过每一步选择局部最优解,从而期望最终达到全局最优解的算法。在每一步做出局部最优选择的同时,并不考虑全局的影响。贪心算法通常适用于问题具有贪心选择性质,并且最优解的整体结构可以通过局部最优选择推导出来。
基本思想:
-
贪心选择性质:每一步都选择当前状态下的最优解,不考虑未来的影响。
-
无后效性(No Future Consideration):在做某个局部最优选择时,不需要考虑到后面的选择会如何影响前面已经做过的选择。
-
构建最优解:通过一系列局部最优选择,期望得到整体最优解。
TypeScript 贪心算法示例:
考虑一个简单的贪心算法问题 - 找零钱的问题。
function makeChange(coins: number[], amount: number): number[] {
coins.sort((a, b) => b - a); // 将硬币按面值降序排列
const result: number[] = [];
for (const coin of coins) {
while (amount >= coin) {
result.push(coin);
amount -= coin;
}
}
if (amount === 0) {
return result;
} else {
// 无法找零
return [];
}
}
// 示例:找零 63 元的最少硬币数
const coins = [25, 10, 5, 1];
console.log(makeChange(coins, 63)); // 输出 [25, 25, 10, 1, 1, 1]
在上面的例子中,贪心算法通过每次选择面值最大的硬币,直到找零金额为零。这种策略在这个特定问题中确实是最优解,但并不是对所有找零问题都适用。因此,贪心算法的选择性质需要根据具体问题来确定。