1. 动态规划入门:从斐波那契数列说起
我第一次接触动态规划是在解决斐波那契数列问题时。当时用递归方法计算fib(50)竟然要几分钟,这让我意识到算法效率的重要性。斐波那契数列的递推公式是:
python
def fib(n):
if n <= 1:
return n
return fib(n-1) + fib(n-2)
这种暴力递归的时间复杂度高达O(2^n)。后来我发现可以用备忘录法优化:
python
memo = {}
def fib_memo(n):
if n not in memo:
memo[n] = n if n <= 1 else fib_memo(n-1) + fib_memo(n-2)
return memo[n]
再进一步,可以用自底向上的迭代法:
python
def fib_dp(n):
a, b = 0, 1
for _ in range(n):
a, b = b, a + b
return a
这个例子揭示了动态规划的三个关键特征:
- 重叠子问题:计算fib(5)时会重复计算fib(3)
- 最优子结构:fib(n)的解依赖于fib(n-1)和fib(n-2)的解
- 状态转移方程:fib(n) = fib(n-1) + fib(n-2)
2. 资源分配问题的动态规划解法
去年我在做一个投资优化项目时,遇到了典型的资源分配问题:公司有100万资金,需要在3个项目间分配,每个项目的收益函数不同,如何分配才能总收益最大?
2.1 问题建模
设总资金为W,有n个项目,第i个项目投资x_i的收益为g_i(x_i)。问题可以表示为:
max Σg_i(x_i)
s.t. Σx_i ≤ W
x_i ≥ 0
2.2 状态定义
我们定义f(k,w)为前k个项目分配w资金时的最大收益。状态转移方程为:
f(k,w) = max{g_k(x) + f(k-1,w-x)} for 0 ≤ x ≤ w
2.3 实际案例
假设有三个项目,收益函数分别为:
- g1(x) = 7√x
- g2(x) = 3x
- g3(x) = 2x^2
用Python实现:
python
def resource_allocation(W, profits):
n = len(profits)
dp = [[0]*(W+1) for _ in range(n+1)]
for i in range(1, n+1):
for w in range(1, W+1):
max_val = 0
for x in range(w + 1):
current = profits[i-1](x) + dp[i-1][w-x]
if current > max_val:
max_val = current
dp[i][w] = max_val
return dp[n][W]
3. 最短路径问题的动态规划应用
在开发导航系统时,我遇到了经典的最短路径问题。不同于Dijkstra算法,动态规划提供了另一种解决思路。
3.1 多阶段图问题
考虑一个分阶段的有向图,节点被分成k个阶段,只能从前一阶段到后一阶段。定义d(i,j)为节点i到节点j的距离,f(i)为从阶段i到终点的最短距离。
状态转移方程:
f(i) = min{d(i,j) + f(j)} for all j in next stage
3.2 Floyd算法
对于任意两点间的最短路径,Floyd算法是典型的动态规划应用:
python
def floyd(graph):
n = len(graph)
dist = [row[:] for row in graph]
for k in range(n):
for i in range(n):
for j in range(n):
if dist[i][j] > dist[i][k] + dist[k][j]:
dist[i][j] = dist[i][k] + dist[k][j]
return dist
算法的时间复杂度是O(n^3),空间复杂度O(n^2)。在实际项目中,我通过滚动数组优化将空间降到了O(n)。
4. 背包问题的动态规划实践
背包问题是我面试时经常被问到的题目,也是动态规划的经典应用。
4.1 0-1背包问题
每个物品要么选要么不选。定义f(i,j)为前i个物品装入容量j的背包的最大价值:
python
def knapsack01(weights, values, capacity):
n = len(weights)
dp = [[0]*(capacity+1) for _ in range(n+1)]
for i in range(1, n+1):
for j in range(1, capacity+1):
if weights[i-1] <= j:
dp[i][j] = max(dp[i-1][j],
dp[i-1][j-weights[i-1]] + values[i-1])
else:
dp[i][j] = dp[i-1][j]
return dp[n][capacity]
4.2 完全背包问题
每个物品可以选多次,只需将状态转移方程稍作修改:
python
dp[i][j] = max(dp[i-1][j],
dp[i][j-weights[i-1]] + values[i-1])
在实际编码中,我常用一维数组优化空间:
python
def knapsack_complete(weights, values, capacity):
dp = [0]*(capacity+1)
for i in range(len(weights)):
for j in range(weights[i], capacity+1):
dp[j] = max(dp[j], dp[j-weights[i]] + values[i])
return dp[capacity]
5. 动态规划的优化技巧
经过多个项目的实践,我总结出几个优化动态规划的有效方法:
5.1 状态压缩
当状态转移只依赖前几个状态时,可以用滚动数组减少空间复杂度。比如斐波那契数列只需要存储前两个状态。
5.2 记忆化搜索
对于不容易确定计算顺序的问题,可以采用记忆化递归的方式:
python
from functools import lru_cache
@lru_cache(maxsize=None)
def fib_memo(n):
if n <= 1:
return n
return fib_memo(n-1) + fib_memo(n-2)
5.3 预处理和剪枝
在一些问题中,提前排序或预处理数据可以大大减少计算量。比如在背包问题中,先按单位重量价值排序可以提前剪枝。
6. 动态规划的常见误区
新手在使用动态规划时常犯的几个错误:
-
错误的状态定义:状态应该包含解决问题的所有必要信息。我曾经因为状态定义不全导致解不正确。
-
忽略边界条件:比如背包问题中容量为0时的初始化。
-
混淆状态转移方向:自顶向下和自底向上的实现方式不同,容易混淆。
-
过度优化:有时为了追求空间优化,反而使代码难以理解和维护。
7. 实际工程中的应用建议
根据我的项目经验,给出以下建议:
- 先写暴力解法:理解问题本质后再优化
- 画状态转移图:用纸笔画出状态之间的关系
- 从小规模数据开始:验证算法正确性
- 添加日志输出:调试时打印中间状态
- 考虑空间优化:在确保正确性的前提下优化
动态规划就像搭积木,把大问题分解为小问题,记住已经解决的子问题,避免重复计算。掌握了这个思想,很多看似复杂的问题都能迎刃而解。