运输问题表上作业法精解
运输问题是一类特殊的线性规划问题,自然可以应用单纯形法这一经典算法来求解。基于运输问题的特点,可以将单纯形法的迭代过程直观地表示在单位运价表上,这样不仅便于理解和操作,而且可以直观地展示求解过程和结果,这种方法的直观性和实用性使得它在解决实际运输问题中得到了广泛应用。表上作业法就是这样一种用于解决平衡型运输问题的方法,首先,构建一个单位运价表,列出从每个供应点到需求点的运输成本。然后,通过最小元素法或Vogel逼近法等规则,确定初始的可行解,使总运输成本尽量低。接下来,进行最优性检验,计算非基变量的检验数。如果所有检验数为非负,当前方案即为最优;否则,需调整运输量,优化方案以降低成本。该方法由其求解过程可在单位运价表上操作而得名。
平衡型运输问题 | 表上作业法流程 |
---|---|
![]() |
![]() |
一、初始调运方案的确定——最小元素法
例1:考虑如下的3个产地和4个销地的运输问题,是确定其初始调运方案。
From | City 1 | City 2 | City 3 | City 4 | Supply (million kwh) |
---|---|---|---|---|---|
Plant 1 | 8 | 6 | 10 | 9 | 35 |
Plant 2 | 9 | 12 | 13 | 7 | 50 |
Plant 3 | 14 | 9 | 16 | 5 | 40 |
Demand (million kwh) | 45 | 20 | 30 | 30 | 125\125 |
数学模型:
1.1 最小元素法
最小元素法是运输问题中用于找到一个基本可行解的启发式方法。它通过优先选择单位运输成本最小的运输路线,逐步分配运输量,直至满足供需平衡。与西北角法相比,最小元素法能够在初始阶段就通过考虑运输成本,生成一个相对较优的基本可行解。
最小元素法的步骤
- 选择单位运价最小的变量分配运输量
在初始步骤中,首先列出运输问题的成本矩阵,其中每个元素代表从第个产地运输到第个销地的单位运输成本。接下来,从成本矩阵中选出最小的单位运输成本所在的单元格,并尽可能多地分配运输量。 - 分配最大可能的运输量
在选定的运输路线中,分配的运输量应尽量最大化,通常是该产地的剩余供应量与销地的剩余需求量中的较小值。即,设为产地的剩余供应量,为销地的剩余需求量,则在路线上分配的运输量为。 - 调整供应量和需求量
将运输量分配到选定的路线后,更新对应的供应量和需求量。即,更新和为新的剩余值:
如果此时某个产地的剩余供应量为零,或某个销地的剩余需求量为零,则将该行或列划去,表示该产地或销地的需求已经完全满足。
注意事项
- 当时的处理
当某个产地的供应量与某个销地的需求量同时变为零时,最小元素法要求只划去其中的一行或一列,不能同时划去。通常根据问题的需求或选择优先顺序,决定是划去产地行还是销地列。 - 划去行或列前必须填入一个数
在划去某一行或一列之前,必须确保在对应的单元格中填入一个运输量,即在满足产地或销地供应、需求平衡的同时,尽量保证基变量个数达到。如果某行或列在划去前剩余的供应量或需求量为零,则在划去之前,将该零值分配到该行或列中的最后一个未分配的单元格。 - 基变量个数要求
为了保证解的可行性和基本解的性质,最终得到的基变量(即实际分配了运输量的单元格)个数应满足个。这里,是产地的数量,是销地的数量。基变量的个数不足会导致解不可行,超过则意味着可能存在冗余约束。
最小元素法的优缺点
相比于西北角法,最小元素法更考虑了运输成本。在分配运输量的过程中,最小元素法始终优先选择成本最低的运输路线,从而生成一个更接近于最优解的基本可行解。这种方法在初始阶段就大幅降低了总运输成本,因此在许多情况下,使用最小元素法生成的初始解比西北角法生成的解要好。
尽管最小元素法在降低初始运输成本方面表现较好,但它并不一定能找到全局最优解。由于它仅仅是在每一步选择当前成本最低的路径,而没有考虑后续的成本分配情况,因此最终得到的解有时仍需通过其他优化方法,如梯度法、修正配比法等进行进一步优化。此外,最小元素法在某些特殊情况下可能会出现退化问题(基变量少于个),需要额外的处理方法来补救。
1.2 计算初始调运方案
例2:例1的初始调运方案为
From/To | City 1 (million kwh) | City 2 (million kwh) | City 3 (million kwh) | City 4 (million kwh) | Supply (million kwh) |
---|---|---|---|---|---|
Plant 1 | 15 | 20 | 35 | ||
Plant 2 | 30 | 20 | 50 | ||
Plant 3 | 10 | 30 | 40 | ||
Demand | 45 | 20 | 30 | 30 |
import numpy as np
# 数据初始化
cost = np.array([[8, 6, 10, 9], # Plant 1 to Cities
[9, 12, 13, 7], # Plant 2 to Cities
[14, 9, 16, 5]], dtype=float) # Plant 3 to Cities
supply = np.array([35, 50, 40]) # Supply from Plants
demand = np.array([45, 20, 30, 30]) # Demand in Cities
# 初始化调运方案表
allocation = np.zeros_like(cost)
# 最小元素法
def least_cost_method(cost, supply, demand):
allocation = np.zeros_like(cost) # 调运方案初始化
supply_copy = supply.copy() # 剩余供应量
demand_copy = demand.copy() # 剩余需求量
while np.sum(supply_copy) > 0 and np.sum(demand_copy) > 0:
# 找出最小的单位运输成本的索引
min_idx = np.unravel_index(np.argmin(cost, axis=None), cost.shape)
i, j = min_idx
# 确定调运量(为剩余供应量和需求量的最小值)
allocated = min(supply_copy[i], demand_copy[j])
allocation[i, j] = allocated
# 更新剩余供应量和需求量
supply_copy[i] -= allocated
demand_copy[j] -= allocated
# 若供应量或需求量已为0,则将其对应行或列标记为不可选(设置为无穷大)
if supply_copy[i] == 0:
cost[i, :] = np.inf
if demand_copy[j] == 0:
cost[:, j] = np.inf
return allocation
# 计算调运方案
allocation_plan = least_cost_method(cost, supply, demand)
print(allocation_plan)
最小元素法通过优先选择运输成本最小的路径,为运输问题提供了一个相对较优的基本可行解。然而,由于它只是一种启发式方法,最终解的质量取决于问题的复杂性,可能需要后续的优化步骤来提升解的质量。
二、检验数的计算
2.1 闭回路法
初始调运方案确定以后,就会得到运为输问题的一组基,基变量个数为,这样在单位运价表上就有个有数格(尽管有时为零,但仍认为是有数格,这些有数格必须有个,不足必须补齐,其对应的基向量是线性无关的)对应基变量,其他空格对应非基变量。
闭回路:对于表上作业法得到的初始调运方案,从调运方案表上的一个空格出发,存在—条且仅存在一条以该空格(用 表示)为起点,以其他填有数格为其他顶点的闭合回路,简称闭回路。这个闭回路是唯一确定的,没有第二个闭回路,本质上是向量之间的唯一线性表出。闭回路还是一个局部的调运方案,检验数衡量的是这个局部调运方案的优劣。其特点如下:
• 除了出发点是空格外,每个顶点都是有数格,都是闭合回路的转角点;
• 闭合回路是一条封闭折线,每一条边都是水平或垂直的,没有斜线;
• 每一行 ( 列 ) 若有闭合回路的顶点,则必有两个(起点所在的行(列)除外)。
• 任一空格的闭合回路不仅是存在的,而且是唯一的。
• 只有从空格出发,其余各转角点所对应的方格内均填有数字时,所构成的闭合回路,才是我们这里所说的闭回路。
运价 | B1 | B2 | B3 | B4 | 供应量(t) | 初始调运方案 | B1 | B2 | B3 | B4 | 供应量(t) |
---|---|---|---|---|---|---|---|---|---|---|---|
A1 | 3 | 11 | 3 | 10 | 700 | A1 | 400 | 300 | 700 | ||
A2 | 1 | 9 | 2 | 8 | 400 | A2 | 300 | 100 | 400 | ||
A3 | 7 | 4 | 10 | 5 | 900 | A3 | 600 | 300 | 900 | ||
需求量(t) | 300 | 600 | 500 | 600 | 2000 | 需求量(t) | 300 | 600 | 500 | 600 | 2000 |
初始调运方案 | 闭回路 |
---|---|
![]() |
![]() |
参看上图空格(1,1)的闭回路:(1,1)—(1,3)—(2,3)—(2,1)一(1,1);空格(3,1)的闭回路: (3,I)—(2,1)—(2,3)—(1,3)一(1,4)—(3,4)—(3,1)。
对所有的空格,都可以用同样的方法画出一条闭回路,不管是否穿越有数格(表中仅仅是示意路线,并不是实际的运输路线),只要找到这个唯一的闭回路即可。
检验数的计算:在调运方案的每个空格所形成的闭回路上,作单位物资的运量调整,总可以计算出相应的运费是增加还是减少。把所计算出来的每条闭回路上调整单位运量而使运输费用发生变化的增减值,称其为检验数。这个检验数表征该闭回路的调运方案是否合理,如果合理调整后运费会上升,所以这个局部调运方案就是局部最优的。只有局部最优才能导致全局最优。
• 如果检验数小于零,表示在该空格的闭回路上调整运量使运费减少;
• 如果检验数大于零,则表示在该空格的闭回路上调整运量会使运费增加。
最优方案的判定准则:初始调运方案中,如果它所有的检验数都是非负的,那么这个初始调运方案最优。否则。这一调运方案不一定是最优的。(如果所有空格的检验数都小于零,那么如果再对调运方案进行任何调整,都会增加运输费用)
例3:计算和
使用闭回路法计算检验数,首先要明确闭回路;以非基变量为起点,然后构造回路。
(1,1)闭回路 | (3,1)闭回路 |
---|---|
![]() |
![]() |
2.2 位势法
位势法求解检验数的步骤:
- 定义位势:
对每个工厂赋予一个位势,对每个需求点赋予一个位势。通常,我们将其中一个位势(例如)设为零,其他位势通过已分配的单元格的成本关系来计算。 - 计算位势:
对于每个已分配的运输单元格,它满足以下关系式:通过这个公式可以根据已知的位势逐步求解出所有工厂和需求点的位势。 - 计算检验数:
对于未分配的运输单元格,其检验数的计算公式为: - 判断最优解
如果检验数,表示当前运输方案已经是最优解;否则,选择检验数为负的单元作为进入变量进行下一步调整。
例4:3个同一商品的供应商,每日能提供的数量分别是A1为7个单位,A2为4个单位,A3为9个单位。现在要把这些产品全部运到4个门店,各门店每日销量B1为3个单位,B2为6个单位,B3为5个单位,B4为6个单位。已知从各供应商到各门店的单位产品运价,问题是如何调运产品,在满足各门店需求量的前提下总运费最少。
运价 | B1 | B2 | B3 | B4 | 产量 | 初始调运方案 | B1 | B2 | B3 | B4 | 产量 |
---|---|---|---|---|---|---|---|---|---|---|---|
A1 | 3 | 11 | 4 | 4 | 7 | A1 | 1 | 6 | 7 | ||
A2 | 7 | 7 | 3 | 8 | 4 | A2 | 4 | 4 | |||
A3 | 1 | 2 | 10 | 6 | 9 | A3 | 3 | 6 | 0 | 9 | |
销量 | 3 | 6 | 5 | 6 | 20 | 销量 | 3 | 6 | 5 | 6 | 20 |
import numpy as np
# 成本矩阵 (c[i][j])
c = np.array([[3, 11, 4, 4],
[7, 7, 3, 8],
[1, 2, 10, 6]])
# 初始调运方案矩阵 (-1 表示没有运输, 其他数字表示运输量)
x = np.array([[-1, -1, 1, 6],
[-1, -1, 4, -1],
[3, 6, -1, 0]])
# 供应量和需求量
supply = [7, 4, 9]
demand = [3, 6, 5, 6]
# 初始化位势 u 和 v
u = [None, None, None] # 工厂的位势
v = [None, None, None, None] # 市场的位势
# 假设 u[0] = 0,开始位势计算
u[0] = 0
# 计算已分配单元格的位势
updated = True
while updated:
updated = False
for i in range(3):
for j in range(4):
if x[i][j] != -1: # 如果是已分配单元格(即有运输量,不是-1)
if u[i] is not None and v[j] is None:
v[j] = c[i][j] - u[i]
updated = True
elif v[j] is not None and u[i] is None:
u[i] = c[i][j] - v[j]
updated = True
# 检查位势计算是否完整
if None in u or None in v:
print("位势计算不完整,请检查初始方案是否正确。")
else:
# 计算检验数
delta = np.zeros((3, 4)) # 初始化检验数矩阵
for i in range(3):
for j in range(4):
if x[i][j] == -1: # 未分配单元格
delta[i][j] = c[i][j] - (u[i] + v[j])
# 输出结果
print("位势 u:", u)
print("位势 v:", v)
print("检验数矩阵:")
print(delta)
位势 u: [0, -1, 2]
位势 v: [-1, 0, 4, 4]
检验数矩阵:
[[ 4. 11. 0. 0.]
[ 9. 8. 0. 5.]
[ 0. 0. 4. 0.]]
2.3 调整调运方案
当判定一个初始调运方案不是最优调运方案时,就要在检验数出现负值的该空格内进行调整;如果检验数是负值的空格不只一个时,一般选择检验数为负值且绝对值最大的空格 作为具体的调整对象。调整过程:
(1)作出负值所在空格的闭回路,本例为空格,闭回路如下图所示。
(2)沿闭回路在各奇数次转角点中挑选运量的最小数值作为调整量。本例是将方格的100作为调整量,将这个数填入空格内,同时调整该闭回路中其他转角点上的运量,使各行、列保持原来的供需平衡.这样使得到一个新的调运方案。
调整后的结果如下
闭回路调整 | 调整后的调运方案 |
---|---|
![]() |
![]() |
三、运输问题的Python求解
例5:一家公司有 3 个工厂A、E 和 K,在 B、C、D 和 M 有四个主要仓库。A、E、K 的平均日产品供应分别为 30、40 和 50 个单位。该产品在 B、C、D 和 M 的平均日需求量分别为 35、28、32、25 单位。从每个工厂到每个仓库的每单位产品的运输成本如下:
Factory | B | C | D | M | Supply |
---|---|---|---|---|---|
A | 6 | 8 | 8 | 5 | 30 |
E | 5 | 11 | 9 | 7 | 40 |
K | 8 | 9 | 7 | 13 | 50 |
Demand | 35 | 28 | 32 | 25 | 120\120 |
试确定最小化总运输成本的调运计划。
数学模型:
- 目标函数:
其中:ijx_{ij}ij$运输的货物量。运输成本矩阵为:
- 约束条件:
供应约束:每个工厂的总运输量不能超过其供应量
工厂A: ;工厂 E:;工厂 K:
需求约束**:每个需求点的接收总量必须满足其需求量
需求点 B:;需求点 C:;需求点 D:;需求点 M:
- 非负约束:运输量不能为负
- 数学模型汇总
import numpy as np
from scipy.optimize import linprog
# 成本矩阵 (转化为一维数组形式)
c = [6, 8, 8, 5, 5, 11, 9, 7, 8, 9, 7, 13]
# 供应量
supply = [30, 40, 50]
# 需求量
demand = [35, 28, 32, 25]
# 构建约束矩阵
# 对应 3个工厂的供应约束 (每一行是一个工厂)
A_eq = [
[1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0], # 工厂A的供应
[0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0], # 工厂E的供应
[0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1], # 工厂K的供应
]
# 对应4个需求点的需求约束 (每一列是一个需求点)
A_eq += [
[1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0], # B的总需求
[0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0], # C的总需求
[0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0], # D的总需求
[0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1], # M的总需求
]
# 将供应量和需求量拼接为右侧向量
b_eq = supply + demand
# 非负约束
x_bounds = [(0, None) for _ in range(12)]
# 使用线性规划求解
result = linprog(c, A_eq=A_eq, b_eq=b_eq, bounds=x_bounds, method='highs')
# 输出结果
if result.success:
x_optimal = result.x.reshape(3, 4)
print("最优解的运输量分配为:")
print(x_optimal)
print(f"最小总成本为: {result.fun}")
else:
print("求解失败")
最优解的运输量分配为:
[[ 0. 10. 0. 20.]
[35. 0. 0. 5.]
[ 0. 18. 32. 0.]]
最小总成本为: 776.0
四、转换为最大流最小费用求解
可以将运输问题转化为一个图,其中工厂和需求点分别为图中的节点,运输路径为边,边的容量是工厂的供应量和需求点的需求量,边的费用则是每条路径的运价。问题的转化思路:将工厂和需求点表示为节点;从源节点(source)连接到所有工厂,每条边的容量为相应工厂的供应量,费用为无穷;从所有需求点连接到汇节点(sink),每条边的容量为相应需求点的需求量,费用为无穷;从工厂连接到需求点的边,容量为无穷大(或者设定为较大的值),费用为相应的运价。
import networkx as nx
# 创建有向图
G = nx.DiGraph()
# 工厂的供应量
supply = {'A1': 7, 'A2': 4, 'A3': 9}
# 需求点的需求量
demand = {'B1': 3, 'B2': 6, 'B3': 5, 'B4': 6}
# 运费矩阵 (每条边的费用)
cost = {
('A1', 'B1'): 3, ('A1', 'B2'): 11, ('A1', 'B3'): 4, ('A1', 'B4'): 4,
('A2', 'B1'): 7, ('A2', 'B2'): 7, ('A2', 'B3'): 3, ('A2', 'B4'): 8,
('A3', 'B1'): 1, ('A3', 'B2'): 2, ('A3', 'B3'): 10, ('A3', 'B4'): 6
}
# 添加超级源到工厂的边,容量为工厂的供应量,费用为0
for factory, supply_value in supply.items():
G.add_edge('source', factory, capacity=supply_value, weight=0)
# 添加工厂到需求点的边,容量为无穷大,费用为运费
for factory in supply:
for demand_point in demand:
G.add_edge(factory, demand_point, capacity=float('inf'), weight=cost[(factory, demand_point)])
# 添加需求点到超级汇的边,容量为需求点的需求量,费用为0
for demand_point, demand_value in demand.items():
G.add_edge(demand_point, 'sink', capacity=demand_value, weight=0)
# 求解最大流最小费用问题
flow_dict = nx.max_flow_min_cost(G, 'source', 'sink')
min_cost = nx.cost_of_flow(G, flow_dict)
# 输出最大流和最小费用
print("最大流的流量分布:")
for u in flow_dict:
for v in flow_dict[u]:
if flow_dict[u][v] > 0:
print(f"从 {u} 到 {v} 的流量: {flow_dict[u][v]}")
print(f"最小总费用为: {min_cost}")
最大流的流量分布:
从 source 到 A1 的流量: 7
从 source 到 A2 的流量: 4
从 source 到 A3 的流量: 9
从 A1 到 B3 的流量: 1
从 A1 到 B4 的流量: 6
从 A2 到 B3 的流量: 4
从 A3 到 B1 的流量: 3
从 A3 到 B2 的流量: 6
从 B1 到 sink 的流量: 3
从 B2 到 sink 的流量: 6
从 B3 到 sink 的流量: 5
从 B4 到 sink 的流量: 6
最小总费用为: 55
总结
表上作业法(Transportation Tableau Method)是一种求解运输问题的手工方法,分为三个主要步骤:
初始解:通过常用的三种方法之一(如西北角法、最小元素法或Vogel逼近法)构造初始解。这些方法确定初始的运输量分配,并保证满足供需平衡。
检验数计算:计算每个未分配变量的检验数,用于判断当前解是否最优。检验数是通过位势法(u-v法)计算的,即通过给定解的潜在值判断运输路径的优化可能性。
调整迭代:若存在负的检验数,则说明当前解还不是最优解。选择最小的负检验数对应的单元格,并通过闭回路法重新分配运输量,更新解。重复计算检验数并迭代,直到所有检验数非负,表明达到了最优解。
这一方法在手工计算中应用广泛,能够快速获得运输问题的最优解。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!