元素码农
基础
UML建模
数据结构
算法
设计模式
网络
TCP/IP协议
HTTPS安全机制
WebSocket实时通信
数据库
sqlite
postgresql
clickhouse
后端
rust
go
java
php
mysql
redis
mongodb
etcd
nats
zincsearch
前端
浏览器
javascript
typescript
vue3
react
游戏
unity
unreal
C++
C#
Lua
App
android
ios
flutter
react-native
安全
Web安全
测试
软件测试
自动化测试 - Playwright
人工智能
Python
langChain
langGraph
运维
linux
docker
工具
git
svn
🌞
🌙
目录
▶
算法基础
▶
复杂度分析
时间复杂度
空间复杂度
渐进符号
▶
算法思想
分治法
贪心算法
回溯法
概率算法
枚举算法
递推算法
递归算法
动态规划
分支限界法
模拟算法
▶
算法分析
正确性证明
性能优化
算法比较
▶
搜索算法
▶
基本搜索
顺序搜索
二分搜索
插值搜索
▶
图搜索
深度优先搜索
广度优先搜索
启发式搜索
▶
高级搜索
双向搜索
A*算法
模式匹配
▶
排序算法
▶
比较类排序
冒泡排序
快速排序
归并排序
插入排序
选择排序
▶
非比较类排序
计数排序
基数排序
桶排序
▶
高级排序
堆排序
希尔排序
外部排序
拓扑排序
▶
图论算法
▶
图的表示
邻接矩阵
邻接表
边集数组
▶
最短路径
Dijkstra算法
Floyd算法
Bellman-Ford算法
最短路径概述
▶
生成树
Prim算法
Kruskal算法
并查集
最小生成树
▶
图的连通性
强连通分量
割点与桥
双连通分量
欧拉路径
▶
动态规划
▶
基础概念
最优子结构
重叠子问题
状态转移方程
▶
经典问题
背包问题
最长公共子序列
编辑距离
▶
优化技巧
空间优化
状态压缩
区间动态规划
▶
字符串算法
▶
字符串匹配
KMP算法
Boyer-Moore算法
Sunday算法
▶
字符串处理
字典树
后缀数组
字符串哈希
▶
压缩算法
游程编码
Huffman编码
LZ77算法
发布时间:
2025-03-21 19:38
↑
☰
# 动态规划算法 动态规划(Dynamic Programming,简称DP)是一种通过将复杂问题分解为更简单的子问题来解决问题的方法。本文将详细介绍动态规划的基本概念、实现方法和常见应用。 ## 基本概念 ### 最优子结构 如果问题的最优解包含其子问题的最优解,我们就称该问题具有最优子结构性质。动态规划正是利用这一性质,通过解决子问题来构建原问题的解。 ### 重叠子问题 在递归过程中,相同的子问题会被重复计算多次。动态规划通过保存已解决的子问题的结果(记忆化)来避免重复计算。 ### 状态转移方程 描述问题状态之间转移关系的数学表达式,是动态规划的核心。通过状态转移方程,我们可以利用已解决的子问题来解决当前问题。 ## 实现方法 ### 自顶向下(记忆化搜索) ```go // 斐波那契数列的记忆化搜索实现 func fibMemo(n int, memo map[int]int) int { if n <= 1 { return n } if val, exists := memo[n]; exists { return val } memo[n] = fibMemo(n-1, memo) + fibMemo(n-2, memo) return memo[n] } // 使用示例 func fib(n int) int { memo := make(map[int]int) return fibMemo(n, memo) } ``` ### 自底向上(迭代) ```go // 斐波那契数列的迭代实现 func fibDP(n int) int { if n <= 1 { return n } dp := make([]int, n+1) dp[0] = 0 dp[1] = 1 for i := 2; i <= n; i++ { dp[i] = dp[i-1] + dp[i-2] } return dp[n] } ``` ## 常见应用 ### 背包问题 ```go type Item struct { weight int value int } // 0-1背包问题 func knapsack(items []Item, capacity int) int { n := len(items) dp := make([][]int, n+1) for i := range dp { dp[i] = make([]int, capacity+1) } for i := 1; i <= n; i++ { for w := 0; w <= capacity; w++ { if items[i-1].weight <= w { // 选择当前物品或不选择 dp[i][w] = max(dp[i-1][w], dp[i-1][w-items[i-1].weight]+items[i-1].value) } else { dp[i][w] = dp[i-1][w] } } } return dp[n][capacity] } func max(a, b int) int { if a > b { return a } return b } ``` ### 最长公共子序列 ```go // 最长公共子序列(LCS) func longestCommonSubsequence(text1, text2 string) int { m, n := len(text1), len(text2) dp := make([][]int, m+1) for i := range dp { dp[i] = make([]int, n+1) } for i := 1; i <= m; i++ { for j := 1; j <= n; j++ { if text1[i-1] == text2[j-1] { dp[i][j] = dp[i-1][j-1] + 1 } else { dp[i][j] = max(dp[i-1][j], dp[i][j-1]) } } } return dp[m][n] } ``` ### 编辑距离 ```go // 编辑距离 func minDistance(word1, word2 string) int { m, n := len(word1), len(word2) dp := make([][]int, m+1) for i := range dp { dp[i] = make([]int, n+1) dp[i][0] = i // 初始化边界 } for j := 0; j <= n; j++ { dp[0][j] = j // 初始化边界 } for i := 1; i <= m; i++ { for j := 1; j <= n; j++ { if word1[i-1] == word2[j-1] { dp[i][j] = dp[i-1][j-1] } else { dp[i][j] = min(dp[i-1][j-1], // 替换 min(dp[i-1][j], // 删除 dp[i][j-1])) + 1 // 插入 } } } return dp[m][n] } func min(a, b int) int { if a < b { return a } return b } ``` ## 优化技巧 ### 空间优化 很多动态规划问题可以通过滚动数组来优化空间复杂度: ```go // 优化空间复杂度的斐波那契数列实现 func fibOptimized(n int) int { if n <= 1 { return n } prev, curr := 0, 1 for i := 2; i <= n; i++ { prev, curr = curr, prev+curr } return curr } ``` ### 状态压缩 对于某些问题,可以使用位运算来压缩状态,减少空间使用: ```go // 使用位运算的集合状态压缩 func subsetSum(nums []int, target int) bool { n := len(nums) states := 1 << n // 2^n个状态 dp := make([]bool, target+1) dp[0] = true for state := 0; state < states; state++ { sum := 0 for i := 0; i < n; i++ { if (state >> i) & 1 == 1 { sum += nums[i] } } if sum <= target { dp[sum] = true } } return dp[target] } ``` ## 实践建议 1. 问题分析 - 识别最优子结构 - 定义状态和状态转移方程 - 确定边界条件 2. 实现考虑 - 选择合适的实现方法(自顶向下或自底向上) - 注意数组边界 - 考虑空间优化可能性 3. 常见误区 - 忽略边界条件 - 状态转移方程错误 - 未考虑空间优化 ## 性能分析 ### 时间复杂度 - 一般情况:O(n×m),其中n和m是问题的规模 - 某些特殊情况可能更高,如O(n^3) - 空间优化通常不影响时间复杂度 ### 空间复杂度 1. 基本实现: - 一维DP:O(n) - 二维DP:O(n×m) 2. 优化后: - 滚动数组:O(n)或O(1) - 状态压缩:可能降低至O(2^n) ## 总结 1. 优点: - 避免重复计算 - 自底向上构建解 - 适用范围广 2. 缺点: - 空间消耗较大 - 某些情况下时间复杂度高 - 实现复杂度较高 3. 应用场景: - 最优化问题 - 计数问题 - 可行性问题