分治、贪心、回溯、动态规划:四种经典算法思想

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

它们解决问题的方式完全不同,但本质上都是在想办法把一个复杂问题变简单。


一、分治:把大问题拆成小问题

**分治(Divide and Conquer)**的核心思想很简单:

把一个大问题拆成多个规模更小、相互独立的子问题,分别解决,再合并结果。

基本过程:

markdown 复制代码
大问题
  ↓
拆分
  ↓
子问题1  子问题2  子问题3
  ↓       ↓       ↓
分别解决
  ↓       ↓       ↓
      合并结果

经典例子

归并排序就是典型的分治算法。

css 复制代码
[8, 4, 5, 7, 1, 3, 6, 2]

        ↓ 分

[8,4,5,7]     [1,3,6,2]

    ↓             ↓

[4,8] [5,7]     [1,3] [2,6]

        ↓ 合并

[1,2,3,4,5,6,7,8]

归并排序不断把数组拆成更小的部分,然后将排序后的结果合并。

时间复杂度:

scss 复制代码
O(n log n)

常见分治算法

  • 归并排序
  • 快速排序
  • 二分查找
  • 大整数乘法
  • 最近点对问题

关键词:拆分 + 独立解决 + 合并


二、贪心:每一步都选择当前最优

**贪心(Greedy)**的思想是:

每一步都选择当前看起来最好的方案,希望最终得到全局最优解。

例如:

markdown 复制代码
有 10 个活动

当前选择结束时间最早的活动
        ↓
再选择下一个能参加的活动
        ↓
继续选择
        ↓
得到最多的活动数量

经典问题是活动选择问题

假设有很多活动,每个活动都有开始时间和结束时间,希望参加尽可能多的活动。

贪心策略:

每次选择结束时间最早的活动。

为什么?

因为结束得越早,给后面的活动留下的时间越多。

贪心的特点

贪心并不是"看到最优就选"。

真正重要的是:

当前的最优选择能够保证最终得到全局最优解。

例如:

  • 活动选择
  • 区间调度
  • 霍夫曼编码
  • 最小生成树
  • Dijkstra(非负权图)

关键词:局部最优 → 全局最优

不过需要注意:

不是所有问题都适合贪心。


三、回溯:不断尝试,走不通就退回来

**回溯(Backtracking)**可以理解成:

做选择 → 继续尝试 → 如果发现走不通,就撤销选择,换一条路。

它通常会构建一棵决策树

例如全排列:

css 复制代码
        []
      / | \
     1  2  3
    / \ ...
   12 13
   ↓
  123

代码通常长这样:

go 复制代码
func backtrack(path []int) {
    if 满足条件 {
        保存结果
        return
    }

    for _, choice := range choices {
        if 不能选择 {
            continue
        }

        path = append(path, choice)

        backtrack(path)

        // 撤销选择
        path = path[:len(path)-1]
    }
}

这里最关键的就是:

复制代码
选择
 ↓
递归
 ↓
撤销选择

经典问题

  • 全排列
  • 组合
  • 子集
  • N 皇后
  • 数独
  • 迷宫
  • 组合求和

回溯的本质其实就是:

暴力搜索 + 剪枝

如果搜索空间非常大,通常需要通过剪枝减少无效搜索。

关键词:尝试 + 递归 + 撤销


四、动态规划:把重复计算保存下来

**动态规划(Dynamic Programming,DP)**主要解决的是:

一个大问题可以拆成很多子问题,而且这些子问题会重复出现。

例如经典的斐波那契数列:

scss 复制代码
f(n) = f(n-1) + f(n-2)

如果直接递归:

scss 复制代码
                f(5)
              /     \
           f(4)     f(3)
          /   \     /   \
       f(3) f(2) f(2) f(1)

可以发现:

scss 复制代码
f(3)
f(2)

被重复计算了。

动态规划的思路就是:

计算过一次,就保存下来,下次直接使用。

例如:

css 复制代码
dp[0] = 0
dp[1] = 1

for i := 2; i <= n; i++ {
    dp[i] = dp[i-1] + dp[i-2]
}

这样时间复杂度从递归的指数级降低到:

scss 复制代码
O(n)

五、四种算法到底有什么区别?

可以用一句话理解:

算法思想 核心思路
分治 把大问题拆成小问题
贪心 每一步选择当前最优
回溯 不断尝试,走不通就撤销
动态规划 保存重复子问题的结果

再换一种方式:

复制代码
分治
大问题
 ↓
拆成独立的小问题
 ↓
分别解决
 ↓
合并


贪心
当前状态
 ↓
选择当前最优
 ↓
继续


回溯
当前状态
 ↓
做一个选择
 ↓
继续搜索
 ↓
不行?
 ↓
撤销选择


动态规划
大问题
 ↓
拆成子问题
 ↓
发现子问题重复
 ↓
保存结果
 ↓
避免重复计算

六、分治和动态规划最容易混淆

两者都是"把大问题拆成小问题",但有一个非常重要的区别:

分治

子问题通常是相互独立的。

例如归并排序:

复制代码
左半部分
右半部分

左边怎么排序,不影响右边。

动态规划

子问题通常存在重复计算和依赖关系

例如:

scss 复制代码
f(5)
├── f(4)
│   ├── f(3)
│   └── f(2)
└── f(3)

f(3) 被重复使用。

所以可以简单记:

独立子问题 → 分治
重复子问题 → 动态规划


七、回溯和动态规划也经常一起出现

回溯是在:

搜索所有可能性

动态规划是在:

避免重复搜索相同状态

例如一个问题可以先写成回溯:

css 复制代码
选择 A
选择 B
选择 C
...

如果发现:

复制代码
很多不同路径
最终都到达同一个状态

那么就可以考虑:

这个状态能不能缓存?

如果可以,就可能从"回溯搜索"进一步优化成"动态规划"。


八、如何判断使用哪一种?

遇到算法题时,可以按照下面的思路判断:

① 能不能拆成独立的子问题?

可以 → 考虑分治

例如:

复制代码
归并排序
快速排序
二分查找

② 每一步是否存在一个可靠的局部最优选择?

是 → 考虑贪心

例如:

复制代码
活动选择
区间调度
最小生成树

③ 是否需要枚举大量可能的选择?

是 → 考虑回溯

例如:

复制代码
全排列
N 皇后
数独
组合问题

④ 子问题是否存在大量重复?

是 → 考虑动态规划

例如:

复制代码
背包问题
最长公共子序列
最长递增子序列
编辑距离
股票问题

九、最后总结

这四种算法思想可以这样记:

复制代码
分治:拆
贪心:选
回溯:试
动态规划:存

它们并不是完全独立的。

很多复杂算法题实际上会组合使用:

复制代码
回溯 + 剪枝
分治 + 递归
动态规划 + 贪心思想
分治 + 动态规划

真正学习算法时,不应该只记"某道题使用什么算法",而应该理解:

为什么这个问题适合这种算法思想。

掌握了这一点,遇到新的算法题时,才有能力自己判断解题方向。

如果你准备接着写算法系列,我建议下一篇可以专门写 「回溯 vs 动态规划:从暴力搜索到状态转移」,这两个最值得放在一起对比。

相关推荐
203号居民1 小时前
LeetCode hot 100 — 138. 随机链表的复制
算法·leetcode·链表
凌晨1681 小时前
OpenCV(十一)人脸识别三大算法LBPH、EigenFace、FisherFace
人工智能·opencv·算法
Johnston_man1 小时前
golang 安装 protoc、protoc-gen-go、protoc-gen-go-grpc、protoc-gen-grpc-gateway
后端·golang
ttwuai1 小时前
Go 后台接入单点登录后,JWT、菜单权限和接口 403 怎么验?
开发语言·后端·golang
落木萧萧8252 小时前
MyBatis 启动的时候都在干什么:从 MappedStatement 说起
java·数据库·后端
国奉2 小时前
iOS 如何处理 GB 级大文件?从 FileHandle、分块读写到进度、取消与异常恢复
后端
Thomas21432 小时前
spark sort shuffer 和 hash shuffer 图解对比
算法·哈希算法
Json____2 小时前
基于 FastAPI + Vue3 的在线拍卖系统技术解析
spring boot·后端·fastapi·wwwoop.com
创新技术阁2 小时前
FastapiAdmin 实战:二次开发前的准备(环境配置与项目启动)
前端·后端·fastapi