当前位置: 首页 > news >正文

动态规划:为什么暴力算法会有重复子问题

第一步:先明确 “子问题” 和 “重复子问题” 的定义

在算法中,“子问题” 不是泛指 “小一点的问题”,而是具有明确 “状态参数” 的、可独立求解的问题单元

  • 状态参数:描述子问题核心信息的变量(比如 01 背包中的 “剩余物品范围” 和 “剩余背包容量”,斐波那契中的 “第 n 项”)。
  • 重复子问题:若两个子问题的 “所有状态参数完全相同”,则它们是重复子问题 —— 意味着这两个子问题的解完全一致,无需重复计算。

第二步:以 01 背包为例,拆解暴力递归的重复子问题

01 背包的暴力解法是 “递归枚举每个物品的‘选 / 不选’”,我们通过具体路径分析重复子问题的产生过程。

1. 01 背包的暴力递归逻辑回顾

暴力递归函数定义:dfs(i, c) = 考虑前i+1个物品(0~i)、剩余容量c时的最大价值。
对每个物品i,有两种选择:

  • 不选:递归调用dfs(i-1, c)
  • 选(若w[i] ≤ c):递归调用v[i] + dfs(i-1, c - w[i])

2. 暴力枚举的 “路径冗余” 导致重复子问题

假设我们有 3 个物品:w = [2,3,4],v = [3,4,5],背包容量C=7。我们分析 “计算dfs(2, 7)(前 3 个物品,容量 7)” 时的路径:

为了计算dfs(2,7),需要先计算两个子问题:

  • 不选物品 2(重量 4):需计算dfs(1,7)(前 2 个物品,容量 7);
  • 选物品 2(重量 4):需计算4 ≤7,因此需计算5 + dfs(1, 7-4)=5 + dfs(1,3)(前 2 个物品,容量 3)。

接下来计算dfs(1,7)(前 2 个物品,容量 7):
dfs(1,7)又依赖两个子问题:

  • 不选物品 1(重量 3):需计算dfs(0,7)(前 1 个物品,容量 7);
  • 选物品 1(重量 3):需计算4 + dfs(0, 7-3)=4 + dfs(0,4)(前 1 个物品,容量 4)。

再计算dfs(1,3)(前 2 个物品,容量 3):
dfs(1,3)也依赖两个子问题:

  • 不选物品 1(重量 3):需计算dfs(0,3)(前 1 个物品,容量 3);
  • 选物品 1(重量 3):需计算4 + dfs(0, 3-3)=4 + dfs(0,0)(前 1 个物品,容量 0)。

现在重点来了:
如果我们继续枚举其他路径(比如 “选物品 0、不选物品 1” 和 “不选物品 0、选物品 1”),会发现:

计算dfs(0,7)dfs(0,4)dfs(0,3)dfs(0,0)这些子问题时,它们的状态参数(i=0+ 不同c)会在多个不同的 “选 / 不选” 组合路径中反复出现。

比如dfs(0,3)(前 1 个物品,容量 3):

  • 路径 1:不选物品 2 → 不选物品 1 → 处理物品 0(此时状态是i=0, c=3);
  • 路径 2:选物品 2 → 不选物品 1 → 处理物品 0(此时状态也是i=0, c=3)。

这两条完全不同的路径,最终指向了同一个子问题(i=0, c=3),暴力解法会对这个子问题重复计算两次 —— 这就是重复子问题的根源。

第三步:再看斐波那契,理解 “指数级重复” 的本质

斐波那契的暴力递归(无记忆化)是 “重复子问题” 的极端案例,能更直观看到 “重复计算的爆炸式增长”。

1. 斐波那契的暴力递归逻辑

fib(n) = fib(n-1) + fib(n-2),终止条件fib(0)=0fib(1)=1

2. 递归树中的重复子问题

fib(5)为例,其递归树如下:

fib(5)
├─ fib(4)
│  ├─ fib(3)
│  │  ├─ fib(2)
│  │  │  ├─ fib(1)  # 重复计算
│  │  │  └─ fib(0)  # 重复计算
│  │  └─ fib(1)      # 重复计算
│  └─ fib(2)
│     ├─ fib(1)      # 重复计算
│     └─ fib(0)      # 重复计算
└─ fib(3)├─ fib(2)│  ├─ fib(1)      # 重复计算│  └─ fib(0)      # 重复计算└─ fib(1)          # 重复计算

可以看到:

  • fib(3)被计算了 2 次,fib(2)被计算了 3 次,fib(1)被计算了 5 次 —— 子问题的重复次数随n呈指数级增长。
  • 暴力解法没有记录这些子问题的解,每次遇到fib(k)都要重新递归到底,导致时间复杂度达到O(2^n)

第四步:总结暴力算法产生重复子问题的核心原因

暴力算法(尤其是递归枚举类)之所以会有重复子问题,本质是 **“枚举路径的冗余” 与 “子问题状态的共享” 之间的矛盾 **:

  1. 枚举路径的冗余:暴力解法为了覆盖 “所有可能的解”,会枚举大量不同的选择路径(比如 01 背包的 “选 / 不选” 组合、斐波那契的 “先算 n-1 还是 n-2”)。这些路径看似不同,但在 “逐步拆解为子问题” 的过程中,会不可避免地交汇到相同的状态。
  2. 子问题状态的共享:子问题的解只由 “状态参数” 决定(与到达该状态的路径无关)。比如 01 背包的dfs(i,c),无论通过 “选 A 不选 B” 还是 “不选 A 选 B” 到达,解都是相同的 —— 但暴力解法没有利用这种 “共享性”,而是对每条路径上的相同状态重复计算。

关键对比:为什么分治算法(如归并排序)没有重复子问题?

很多人会疑惑:“分治也拆分子问题,为什么没有重复子问题?”—— 这能帮我们进一步理解本质:
分治算法(如归并排序、快速排序)的子问题是 “不重叠、无共享” 的。比如归并排序将数组拆分为 “左半部分” 和 “右半部分”,左半部分的子问题和右半部分的子问题完全独立(状态参数无交集),不会出现 “同一个状态被两个不同子问题依赖” 的情况,因此无需处理重复子问题。

而暴力递归(如 01 背包、斐波那契)的子问题是 “重叠、共享” 的 —— 不同的父问题可能依赖同一个子问题,这才导致了重复计算。

最终结论

暴力算法的核心目标是 “枚举所有可能解”,但为了覆盖所有解,它会产生大量 “路径冗余”;而子问题的解只由 “状态参数” 决定(与路径无关),这就导致不同路径会反复遇到相同的子问题 —— 这就是暴力算法存在重复子问题的根本原因。

而动态规划(DP)的核心价值,正是通过 “记忆化” 或 “递推表” 记录这些重复子问题的解,让每个子问题只计算一次,从而将时间复杂度从暴力的 “指数级” 降到 “多项式级”。

http://www.dtcms.com/a/351427.html

相关文章:

  • 深度学习自动驾驶BEV【专业名词解释汇总】
  • VS中创建Linux项目
  • Tomcat的VM options
  • 我在TSX开发中的实用指南:从基础到实战
  • Java大厂面试实战:从Spring Boot到微服务架构的全链路技术解析
  • swift 开发抠图工具实现思路,与代码详解
  • Java全栈开发面试实录:从基础到实战的深度解析
  • Nginx如何实现反向代理和负载均衡器等功能的
  • 要闻集锦|阿里官网调整为四大业务板块;华为云重组多个事业部涉及上千人;群核科技在港交所更新招股书
  • Swift高阶函数-contains、allSatisfy、reversed、lexicographicallyPrecedes
  • 【大前端】实现一个前端埋点SDK,并封装成NPM包
  • 如何避免频繁切换npm源
  • Redis相关命令详解及其原理
  • AI在提升阅读效率的同时,如何加强理解深度?
  • 嵌入式(day34) http协议
  • 使用Java对接印度股票市场API开发指南
  • Markdown学习笔记(4)
  • 计算神经科学数学建模编程深度前沿方向研究(上)
  • 新手向:pip安装指南
  • 《数据之心》——鱼小妖的觉醒(科研篇)
  • DAY 57 经典时序预测模型1
  • 如何在PC上轻松访问iPhone照片(已解决)
  • UE5 PCG 笔记(三) Normal To Density 节点
  • 神经网络参数量计算详解
  • linux服务器监控平台搭建流程
  • 深度学习:卷积神经网络(CNN)
  • [Mysql数据库] 知识点总结2
  • 高教杯数学建模2021-C 生产企业原材料的订购与运输
  • 将vmware workstation虚拟机迁移至pve
  • 基础|Golang内存分配