目录
动态规划(Dynamic Programming)是一种解决复杂问题的算法设计思想,通常用于解决具有重叠子问题和最优子结构性质的问题。它将问题分解成多个小问题,通过解决小问题来解决原始问题。
案例分析一:动态规划——经典案例分析
案例四:0-1背包问题
给定一组物品,每个物品有一个重量和一个价值,同时给定一个背包容量限制。目标是选择一些物品放入背包中,使得在满足背包容量限制的前提下,所选择的物品的总价值最大。
方法:可以使用一个二维数组 dp
,其中 dp[i][j]
表示在前 i
个物品中,背包容量为 j
时的最大价值。具体的算法如下:
- 初始化一个
(n+1)*(W+1)
的二维数组dp
,其中n
是物品的个数,W
是背包的容量。 - 逐个遍历物品,对于第
i
个物品:- 如果
weights[i]
大于当前背包容量j
,则无法放入背包,dp[i][j]
等于上一个物品在相同容量下的最大价值,即dp[i-1][j]
。 - 否则,可以选择放入或不放入当前物品。如果选择放入,那么最大价值就是当前物品的价值
values[i]
加上剩余容量下放入前i-1
个物品的最大价值dp[i-1][j-weights[i]]
;如果选择不放入,那么最大价值就是放入前i-1
个物品的最大价值dp[i-1][j]
。 - 取两者中的最大值作为当前状态的最大价值,即
dp[i][j] = max(dp[i-1][j], values[i] + dp[i-1][j-weights[i]])
。
- 如果
- 最终的最大价值就是
dp[n][W]
。
def knapsack_01(weights, values, W):
n = len(weights)
dp = [[0] * (W + 1) for _ in range(n + 1)]
for i in range(1, n + 1):
for j in range(1, W + 1):
if weights[i - 1] > j:
dp[i][j] = dp[i - 1][j]
else:
dp[i][j] = max(dp[i - 1][j], values[i - 1] + dp[i - 1][j - weights[i - 1]])
return dp[n][W]
# 测试
weights = [2, 3, 4, 5]
values = [3, 4, 5, 6]
W = 5
result = knapsack_01(weights, values, W)
print(result) # 输出 11
案例五:货郎担问题
Traveling Salesman Problem,假设有一个货郎要从起始点出发,经过一系列的城市,最终回到起始点。每两个城市之间都有一个距离,货郎希望找到一条路径,使得他经过的城市总距离最短。
方法:货郎担问题是一个NP难问题,没有多项式时间的解法,但可以使用动态规划来近似解决。一种常用的方法是使用状态压缩动态规划。具体的算法如下:
- 将问题抽象成一个图,其中节点表示城市,边表示城市间的距离。
- 定义一个二维数组
dp[mask][i]
,其中mask
代表经过的城市的集合(用二进制表示,第i
位为1表示经过第i
个城市),i
表示当前所在的城市。 - 使用状态转移方程进行动态规划:
dp[mask][i] = min(dp[mask ^ (1 << j)][j] + distance(i, j))
,其中j
表示未经过的城市,distance(i, j)
表示从城市i
到城市j
的距离。- 这里的
mask ^ (1 << j)
表示将第j
位取反,表示加入了城市j
。 dp[mask][i]
表示在经过mask
中的城市后,以i
为结束城市的最短路径长度。
- 最终的最短路径长度就是
min(dp[(1 << n) - 1][i] + distance(i, 0))
,其中n
是城市的数量,i
可以是任意一个经过的城市。
def tsp(graph):
n = len(graph)
dp = [[float('inf')] * n for _ in range(1 << n)]
dp[1][0] = 0
for mask in range(1, 1 << n):
for u in range(n):
if (mask >> u) & 1:
for v in range(n):
if u != v and (mask >> v) & 1:
dp[mask][u] = min(dp[mask][u], dp[mask ^ (1 << u)][v] + graph[v][u])
mask = (1 << n) - 1
u = 0
min_dist = min(dp[mask][v] + graph[v][u] for v in range(1, n))
return min_dist
# 测试
graph = [
[0, 29, 20, 21],
[29, 0, 15, 17],
[20, 15, 0, 18],
[21, 17, 18, 0]
]
result = tsp(graph)
print(result) # 输出 85
在这个例子中,我们使用了状态压缩动态规划来解决货郎担问题。graph
表示城市之间的距离,函数 tsp
返回从起始城市出发,经过所有城市后回到起始城市的最短路径长度。
需要注意的是,货郎担问题是一个组合优化问题,随着城市数量的增加,问题的复杂度会急剧增加,因此实际应用中可能需要使用一些启发式算法来近似解决
案例六:流水线调度问题
假设有两条流水线,每条流水线有多个工位,产品从第一条流水线的第一个工位开始,然后经过各个工位,最终到达第二条流水线的最后一个工位。每个工位都有一个加工时间,表示在该工位上加工一个产品所需的时间。
另外,产品在两条流水线之间转移的时间也是固定的。目标是找到一条合适的生产路径,使得生产一个产品所需的总时间最短。
方法:可以使用两个数组 T1
和 T2
来记录从起始点到当前工位的最短时间,同时使用两个数组 L1
和 L2
来记录前一条流水线和后一条流水线的前一工位是哪一个。具体的算法如下:
- 初始化两个数组
T1
和T2
,分别记录第一条流水线和第二条流水线的到达每个工位的最短时间,初始化数组L1
和L2
分别记录前一条流水线和后一条流水线的前一工位是哪一个。 - 逐个遍历各个工位,根据当前工位的加工时间和转移时间,更新
T1
和T2
数组。 - 找到总时间最短的最后一个工位,然后根据
L1
和L2
数组回溯找到最短路径。
def assembly_line_scheduling(a, t, e, x):
n = len(a)
T1 = [0] * n
T2 = [0] * n
L1 = [0] * n
L2 = [0] * n
T1[0] = e[0] + a[0][0]
T2[0] = e[1] + a[1][0]
for i in range(1, n):
if T1[i - 1] <= T2[i - 1] + t[1][i - 1]:
T1[i] = T1[i - 1] + a[0][i]
L1[i] = 1
else:
T1[i] = T2[i - 1] + t[1][i - 1] + a[0][i]
L1[i] = 2
if T2[i - 1] <= T1[i - 1] + t[0][i - 1]:
T2[i] = T2[i - 1] + a[1][i]
L2[i] = 2
else:
T2[i] = T1[i - 1] + t[0][i - 1] + a[1][i]
L2[i] = 1
if T1[n - 1] + x[0] <= T2[n - 1] + x[1]:
F = T1[n - 1] + x[0]
L = 1
else:
F = T2[n - 1] + x[1]
L = 2
path = [L]
for i in range(n - 1, 0, -1):
if L == 1:
L = L1[i]
else:
L = L2[i]
path.insert(0, L)
return F, path
# 测试
a = [[7, 9, 3, 4, 8], [8, 5, 6, 4, 5]]
t = [[2, 3, 1, 3], [2, 1, 2, 2]]
e = [2, 4]
x = [3, 2]
min_time, path = assembly_line_scheduling(a, t, e, x)
print(f"最短时间:{min_time}")
print(f"最短路径:{path}")