回溯算法、贪心算法、分治算法、动态规划

好的,这是一个非常核心的算法面试和编程主题。我将为你系统地梳理这四大算法思想,包括它们的核心思想、特点、适用场景以及相互之间的区别。


1. 回溯算法

回溯算法是一种通过试错来寻找所有(或部分)可行解的算法。它通过深度优先搜索策略,一步一步地构建候选解,并在确定当前步骤不可能得到正确解时,撤销上一步或几步的计算,再通过其他的可能路径继续尝试。

核心思想: “尝试与回退”。可以看作是对决策树的深度优先遍历,并在遍历过程中剪枝。

关键特征:

  • 系统性: 会系统地搜索整个解空间。
  • 试探与回退: 走到“死胡同”时,会撤销最后的选择(回溯),回到上一个决策点,选择另一条路继续走。
  • 通常用于求所有解/任意解: 非常适合解决组合、排列、子集、棋盘类(如N皇后)等问题。

算法框架(伪代码):

def backtrack(路径, 选择列表):
    if 满足结束条件:
        结果集.add(路径) # 或 return 结果
        return

    for 选择 in 选择列表:
        if 选择不合法: # 剪枝,跳过无效选择
            continue
        做选择(将选择加入路径)
        backtrack(路径, 新的选择列表) # 递归进入下一层决策树
        撤销选择(将选择从路径中移除) # 核心:回溯

经典问题:

  • 全排列问题
  • N皇后问题
  • 组合总和问题
  • 子集问题
  • 数独 solver

2. 贪心算法

贪心算法在每一步选择中都采取在当前状态下最好或最优(即最有利) 的选择,从而希望导致结果是全局最好或最优的算法。

核心思想: “局部最优,希望全局最优”。它只关心眼前的最大利益,不回退。

关键特征:

  • 局部最优选择: 每一步都做一个贪婪的、局部最优的选择。
  • 不可回退: 一旦做出选择,就不可撤销。
  • 高效: 通常时间复杂度较低。
  • 不保证全局最优: 只有在“贪心选择性质”和“最优子结构”的问题上才能得到全局最优解。很多时候只是一个近似解。

算法框架(伪代码):

def greedy(问题):
    初始化解决方案 solution = []
    while 问题未解决 and 还有选择:
        从候选集合中,根据贪心策略选择一个最优的候选 c
        if c 是可行的: # 有时不需要可行性判断
           将 c 加入到 solution 中
    返回 solution

经典问题:

  • 硬币找零问题(特定面额下,如[1, 5, 10, 20])
  • 活动选择问题(安排最多的不重叠活动)
  • 霍夫曼编码(数据压缩)
  • 最小生成树(Prim, Kruskal算法)
  • 单源最短路径(Dijkstra算法)

3. 分治算法

分治算法的核心思想是“分而治之”,它将一个复杂的问题分解成两个或多个相同或相似的子问题,直到子问题可以简单直接地求解,最后将子问题的解合并起来,得到原问题的解。

核心思想: “分解 -> 解决 -> 合并”。

关键特征:

  • 递归结构: 问题可以递归地分解。
  • 子问题独立: 分解出的子问题之间是相互独立的,这是与动态规划的关键区别之一。
  • 可合并: 子问题的解可以合并为原问题的解。

算法框架(伪代码):

def divide_and_conquer(问题):
    if 问题规模足够小,可以直接解决:
        return 直接求解的结果

    # 分解
    将问题分解为 k 个规模更小的子问题

    # 解决
    for 每个子问题:
        解[i] = divide_and_conquer(子问题[i])

    # 合并
    结果 = merge(解[1], 解[2], ..., 解[k])

    return 结果

经典问题:

  • 归并排序
  • 快速排序
  • 二分查找
  • 汉诺塔问题
  • 大整数乘法(如Karatsuba算法)

4. 动态规划

动态规划用于解决具有重叠子问题最优子结构性质的问题。它通过将原问题分解为相对简单的子问题的方式,来求解复杂问题,并避免重复计算已经计算过的子问题。

核心思想: “记住求过的解来节省时间”,即空间换时间

关键特征:

  • 最优子结构: 一个问题的最优解包含其子问题的最优解。
  • 重叠子问题: 在递归求解过程中,会反复遇到相同的子问题。
  • 状态存储: 使用一个表(通常是数组或哈希表)来存储已解决的子问题的答案。
  • 无后效性: 当前状态一旦确定,后续过程的演变就不再受之前各种状态及决策的影响。

两种实现方式:

  1. 自顶向下(记忆化搜索): 从原问题开始,递归地求解子问题,但在求解前先查表看是否已经计算过。
  2. 自底向上(制表法): 从最小的子问题开始,逐步迭代求解更大的子问题,直到求解原问题。这是更常见的DP形式。

算法框架(伪代码,以自底向上为例):

def dp(问题):
    # 1. 定义dp数组的含义
    dp = [0] * (n+1)

    # 2. 初始化 base case
    dp[0] = ...
    dp[1] = ...

    # 3. 状态转移(填表)
    for i in range(2, n+1):
        # 根据状态转移方程,计算 dp[i]
        # 状态转移方程是DP的核心,描述了问题是如何由子问题构建的
        dp[i] = ... # 例如:dp[i] = max(dp[i-1], dp[i-2] + ...)

    return dp[n]

经典问题:

  • 斐波那契数列计算
  • 背包问题
  • 最长公共子序列
  • 最长递增子序列
  • 硬币找零问题(求最小硬币数,通用面额)
  • 编辑距离

四大算法对比总结

特性 回溯 贪心 分治 动态规划
核心思想 试错,走不通就回退 每一步都选局部最优 分解 -> 解决 -> 合并 存储子问题的解,避免重复计算
解的性质 能求所有解或任一解 求可行解(不保证最优)或特定问题的最优解 通常不是最优化问题 求最优解或计数等
是否回退 否(递归到底) 否(按顺序计算)
子问题 决策树上的路径 无明确子问题概念 子问题独立 子问题重叠
效率 指数级,靠剪枝优化 通常很高(多项式) 通常O(n log n) 多项式时间
存储 存储当前路径 通常很少 递归栈开销 DP表/数组(空间换时间)
关键步骤 选择、递归、撤销 制定贪心策略 分解与合并 定义状态 & 状态转移方程

如何选择?

  1. 需要找出所有可能的方案? -> 回溯算法
  2. 问题可以分解为独立的子问题,且子问题结构相同? -> 分治算法
  3. 问题要求最优解,并且具有“最优子结构”和“重叠子问题”? -> 动态规划
  4. 问题要求最优解,并且可以通过一系列的局部最优选择来达到全局最优? -> 贪心算法(注意:需要证明其正确性,否则可能只是近似解)

希望这个全面的总结能帮助你清晰地理解和区分这四大核心算法思想!

posted @ 2025-10-15 10:20  阿木隆1237  阅读(134)  评论(0)    收藏  举报