
1. 从现实问题到数学模型为什么资源分配需要矩阵做项目、管仓库、搞物流甚至是安排公司里几个团队的出差计划你大概率都遇到过同一个核心难题手头有一堆“供给点”比如几个工厂、几个仓库、几个有闲人的团队也有一堆“需求点”比如几个客户、几个门店、几个急等支援的项目每个供给点能提供的资源量不同每个需求点要的资源量也不同把资源从A地运到B地还得花成本。你的任务就是怎么安排运输计划才能在满足所有需求的前提下让总成本可能是运费、时间、或者损耗降到最低这问题听起来就头大尤其是供给地和需求地数量一多人脑基本就算不过来了。这时候数学建模和编程就成了我们手里的“超级计算器”。而Python凭借其简洁的语法和强大的科学计算库像NumPy, SciPy几乎是解决这类优化问题的首选工具。今天要聊的就是如何用Python特别是利用矩阵这个强大的数学工具来清晰、高效地定义和求解“多供给地-多需求地资源分配问题”。你可能听过“线性规划”或者“运输问题”。对我们今天要构建的本质上就是一个经典的运输问题模型。它的厉害之处在于能把一堆杂乱的实际约束谁有多少货、谁要多少货、哪到哪运费多少用非常规整的数学语言——矩阵和向量——表达出来。一旦表达成了标准形式剩下的就可以交给成熟的优化求解器去计算我们只需要关心如何把问题“喂”给求解器以及如何理解求解器吐出来的结果。为什么非得用矩阵想象一下你有3个工厂供给地和4个客户需求地。你不用矩阵的话可能需要定义12个变量x_11, x_12, ..., x_34代表从每个工厂到每个客户的运量然后写一长串的等式和不等式。而用矩阵你可以把所有的运量排列成一个3行4列的矩阵X把单位运费也排成一个同样大小的成本矩阵C把每个工厂的产能和每个客户的需求分别写成两个向量。这样一来总成本就是矩阵C和X对应位置相乘再求和在数学上叫Frobenius内积编程上就是np.sum(C * X)约束条件也可以非常简洁地用矩阵乘法来表示。这种表达方式不仅代码写起来干净更重要的是思维上更清晰更容易发现问题的结构也方便我们处理成百上千个供给点和需求点的大规模问题。所以这篇内容的目标很明确我们不空谈理论而是手把手带你走一遍从实际问题抽象成矩阵模型再用Python建模并求解的完整流程。你会看到如何用NumPy创建和管理这些矩阵向量如何用SciPy的线性规划库或者更专业的PuLP、ortools来构建和求解模型以及如何解读和验证求解结果。过程中我会穿插很多我实际使用时踩过的坑和总结的技巧比如数据格式的处理、模型无解时的调试思路、以及如何对结果进行简单的可视化分析。2. 问题拆解与数学模型构建定义你的矩阵和向量在打开编辑器写第一行代码之前我们必须先把乱麻一样的问题理清楚用数学语言给它拍张“X光片”。这一步做扎实了后面的编程就是按图索骥。2.1 明确问题要素与假设面对一个具体的资源分配场景比如“从三个仓库调货到五个超市”我们需要抽取出以下几个核心要素供给地Sources 假设有m个。每个供给地i有一个固定的供给量或产能s_i。比如仓库A最多能发出100箱货。需求地Destinations 假设有n个。每个需求地j有一个必须满足的需求量d_j。比如超市B需要收到80箱货。运输成本Cost 从供给地i运送一个单位资源到需求地j所需要的成本记为c_{ij}。这个成本可以是金钱、时间、距离等。这是整个模型中最关键的参数矩阵。决策变量Decision Variables 我们要决定的就是从每个i到每个j的实际运输量记为x_{ij}。这是我们要求解的对象。这里需要一个重要的平衡假设总供给量等于总需求量。即sum(s_i) sum(d_j)。这是一个经典运输问题的标准假设。如果现实中不相等怎么办别急有标准的处理方法如果供给大于需求我们就虚拟一个“剩余需求地”可以理解为就地存储或废弃其运输成本为0如果需求大于供给那就虚拟一个“缺货供给地”可以理解为外部采购或延迟满足其运输成本为一个很高的惩罚值M。这样就能把不平衡问题转化为平衡问题。在编程时我们可以先检查数据自动完成这个“平衡化”处理。2.2 构建标准数学模型基于以上要素我们可以写出这个问题的标准数学模型。我们的目标是最小化总运输成本。目标函数Objective FunctionMinimize Z sum_{i1}^{m} sum_{j1}^{n} c_{ij} * x_{ij}用大白话说就是把所有可能的运输路线上的单位成本 × 运量加起来让这个总和最小。约束条件Constraints供给约束从每个供给地i运出的总量不能超过其供给量。sum_{j1}^{n} x_{ij} s_i 对于所有i 1, 2, ..., m。 如果是严格的“恰好运完”则用等号。通常更通用。需求约束运到每个需求地j的总量必须满足其需求量。sum_{i1}^{m} x_{ij} d_j 对于所有j 1, 2, ..., n。 同样严格满足用允许超额满足用。经典运输问题通常用。非负约束运输量不能为负数。x_{ij} 0 对于所有i,j。2.3 引入矩阵与向量表示现在我们把上面这些“求和号”语言翻译成更简洁、更适合编程的矩阵语言。决策变量矩阵 X 一个m行n列的矩阵。X [[x_11, x_12, ..., x_1n], [x_21, x_22, ..., x_2n], ..., [x_m1, x_m2, ..., x_mn]]在Python里这就是一个NumPy的二维数组shape为(m, n)。成本矩阵 C 同样是一个m x n的矩阵每个位置C[i,j]存放着c_{ij}。C [[c_11, c_12, ..., c_1n], [c_21, c_22, ..., c_2n], ..., [c_m1, c_m2, ..., c_mn]]目标函数Z就可以写成np.sum(C * X)对应元素相乘再求和。供给向量 S 一个长度为m的列向量或一维数组。S [s_1, s_2, ..., s_m]^TT表示转置在计算时我们关心的是维度。需求向量 D 一个长度为n的行向量或一维数组。D [d_1, d_2, ..., d_n]。那么那两个关键的约束条件怎么用矩阵表示呢供给约束sum_{j} x_{ij} s_i意味着把X矩阵的每一行加起来得到一个长度为m的向量这个向量里的每个元素就是对应供给地的总运出量。这个操作就是X乘以一个全1的列向量np.ones(n)。所以供给约束可以写成X · ones_n S 其中ones_n是元素全为1、长度为n的列向量。结果是一个长度为m的向量不等式。需求约束sum_{i} x_{ij} d_j意味着把X矩阵的每一列加起来得到一个长度为n的向量这个向量里的每个元素就是对应需求地的总运入量。这个操作就是X的转置乘以一个全1的列向量或者等价地用一个全1的行向量乘以X。所以需求约束可以写成ones_m^T · X D 其中ones_m是长度为m的全1列向量ones_m^T就是全1的行向量。结果是一个长度为n的向量不等式。看到这里你可能觉得矩阵表达似乎也没简单多少。但它的巨大优势在于标准化和可扩展性。当我们用SciPy的linprog等求解器时它要求我们把所有约束都写成A_ub x b_ub或A_eq x b_eq的形式这里的x是把所有决策变量拉直成一个一维向量。我们的矩阵X拉直后A_ub和A_eq这两个大矩阵的构造恰恰就对应了上面行和与列和的操作。用矩阵思维能帮助我们更高效、更不易出错地构造这些庞大的约束矩阵。这是从“会做题”到“能解决实际问题”的关键一步。3. Python实战使用SciPy与PuLP两种方法求解理论铺垫完成我们进入实战环节。我将用同一个简单的例子演示两种最常用的Python求解方法SciPy的linprog和专门用于建模的PuLP库。你会看到矩阵思想是如何贯穿其中的。3.1 案例数据定义假设我们有2个仓库供给地3家商店需求地。供给量S [20, 30]仓库1有20单位仓库2有30单位需求量D [10, 28, 12]商店1、2、3分别需要10, 28, 12单位总供给50总需求50恰好平衡。单位运输成本矩阵C行代表仓库列代表商店C [[4, 5, 3], # 从仓库1到商店1,2,3的成本 [6, 7, 5]] # 从仓库2到商店1,2,3的成本我们的任务找到运输量矩阵X使得总成本最低。首先导入必要的库。import numpy as np from scipy.optimize import linprog import pulp3.2 方法一SciPy.optimize.linprogSciPy是科学计算的瑞士军刀它的linprog函数可以求解线性规划问题。我们需要把问题转化成它要求的标准形式。第一步将决策变量矩阵X拉直成向量x我们的变量是x_11, x_12, x_13, x_21, x_22, x_23。按行优先的顺序拉直成一个一维向量x [x_11, x_12, x_13, x_21, x_22, x_23]对应的成本矩阵C也要拉直成向量c作为目标函数的系数c [4, 5, 3, 6, 7, 5]第二步构建约束矩阵A_eq和向量b_eq我们有供给和需求两类等式约束因为假设是平衡问题且要求恰好运完/满足。供给约束2个等式x_11 x_12 x_13 20和x_21 x_22 x_23 30。 这对应一个2行6列的矩阵A_eq_supply[[1, 1, 1, 0, 0, 0], # 第一行对应仓库1的运出量之和 [0, 0, 0, 1, 1, 1]] # 第二行对应仓库2的运出量之和b_eq_supply [20, 30]需求约束3个等式x_11 x_21 10,x_12 x_22 28,x_13 x_23 12。 这对应一个3行6列的矩阵A_eq_demand[[1, 0, 0, 1, 0, 0], # 第一列运到商店1的总量 [0, 1, 0, 0, 1, 0], # 第二列运到商店2的总量 [0, 0, 1, 0, 0, 1]] # 第三列运到商店3的总量b_eq_demand [10, 28, 12]我们把这两个约束上下拼接起来得到完整的等式约束A_eq和b_eq。第三步定义变量的边界bounds运输量非负所以每个x_i的下界是0上界是无穷None。第四步调用linprog求解linprog默认是最小化所以正好。# 1. 定义数据 C np.array([[4, 5, 3], [6, 7, 5]]) S np.array([20, 30]) D np.array([10, 28, 12]) # 2. 拉直成本向量c c C.flatten() # 默认按行优先拉直得到 [4,5,3,6,7,5] # 3. 构建等式约束矩阵 A_eq m, n C.shape num_vars m * n # 3.1 供给约束矩阵 (m行 m*n列) A_eq_supply np.zeros((m, num_vars)) for i in range(m): A_eq_supply[i, i*n : (i1)*n] 1 # 将第i行的所有变量系数设为1 # 3.2 需求约束矩阵 (n行 m*n列) A_eq_demand np.zeros((n, num_vars)) for j in range(n): for i in range(m): A_eq_demand[j, i*n j] 1 # 将第j列的所有变量系数设为1 # 3.3 合并约束 A_eq np.vstack([A_eq_supply, A_eq_demand]) b_eq np.hstack([S, D]) # 4. 定义变量边界 bounds [(0, None)] * num_vars # 每个变量 0 # 5. 求解 result linprog(c, A_eqA_eq, b_eqb_eq, boundsbounds, methodhighs) # 推荐使用highs求解器 # 6. 输出结果 if result.success: print(优化成功) print(f最低总成本: {result.fun}) # 将解向量重塑回矩阵形式 X_opt result.x.reshape((m, n)) print(最优运输方案矩阵 X:) print(X_opt) else: print(优化失败:, result.message)运行这段代码你会得到类似下面的输出优化成功 最低总成本: 244.0 最优运输方案矩阵 X: [[10. 0. 10.] [ 0. 28. 2.]]结果解读总成本最低为244。最优运输方案是从仓库1运10单位到商店1运10单位到商店3。从仓库2运28单位到商店2运2单位到商店3。 这个方案满足了所有约束并且直观上避开了成本较高的路线如仓库2到商店1的成本6仓库1到商店2的成本5优先使用了成本最低的路线仓库1到商店3成本3仓库1到商店1成本4。注意linprog的method参数很重要。对于这类问题highs默认且高效或revised simplex都是不错的选择。如果问题规模很大变量成千上万可能需要考虑更专业的商业求解器但highs对于中小规模问题绰绰有余。3.3 方法二使用PuLP建模PuLP是一个更“建模友好”的线性规划库。它的思路更贴近我们写数学模型的自然语言先创建问题再添加变量、目标函数和约束。对于复杂问题用PuLP写起来更直观也更不容易在构建约束矩阵时出错。# 1. 定义问题 prob pulp.LpProblem(Transportation_Problem, pulp.LpMinimize) # 最小化问题 # 2. 定义决策变量矩阵 X m, n C.shape # 创建变量字典lowBound0表示下界为0catContinuous表示连续变量 x_vars pulp.LpVariable.dicts(x, ((i, j) for i in range(m) for j in range(n)), lowBound0, catContinuous) # 3. 定义目标函数 prob pulp.lpSum(C[i][j] * x_vars[i, j] for i in range(m) for j in range(n)) # 4. 添加供给约束 for i in range(m): prob pulp.lpSum(x_vars[i, j] for j in range(n)) S[i], fSupply_Constraint_{i} # 5. 添加需求约束 for j in range(n): prob pulp.lpSum(x_vars[i, j] for i in range(m)) D[j], fDemand_Constraint_{j} # 6. 求解问题 prob.solve(pulp.PULP_CBC_CMD(msgFalse)) # 使用CBC求解器msgFalse关闭求解日志 # 7. 输出结果 print(f优化状态: {pulp.LpStatus[prob.status]}) print(f最低总成本: {pulp.value(prob.objective)}) print(最优运输方案矩阵 X:) X_opt_pulp np.zeros((m, n)) for i in range(m): row [] for j in range(n): value pulp.value(x_vars[i, j]) X_opt_pulp[i, j] value row.append(value) print(row)运行后你会得到与SciPy方法完全相同的结果。PuLP的代码更像是在“描述”问题可读性更强。特别是约束的添加直接对应了数学公式sum_{j} x_{ij} s_i非常直观。3.4 两种方法对比与选择SciPy.optimize.linprog优点SciPy是科学计算的基础库通常已安装或易于安装。求解器集成度高对于标准形式的线性规划问题一旦构造好矩阵调用非常简洁。缺点需要手动将问题转化为标准形式并构造庞大的约束矩阵A_eq,A_ub这个过程容易出错尤其是当问题有大量变量和复杂约束时。代码的“建模”意味较弱。适用场景问题规模适中约束结构相对简单、规整如运输问题、指派问题或者你希望减少外部依赖。PuLP优点建模过程直观自然几乎是对数学模型的直接翻译。添加约束和变量非常灵活适合约束复杂、变量类型多如整数变量的问题。可读性极高易于调试和修改。缺点需要额外安装。默认的CBC求解器对于超大规模问题可能不如商业求解器快但对我们遇到的大部分问题足够快。适用场景绝大多数线性/整数规划建模场景。特别是对于初学者或者问题约束逻辑复杂时强烈推荐使用PuLP它能让你更专注于问题本身而不是矩阵下标的转换。个人建议如果你是初学者或者经常需要处理不同结构的优化问题直接从PuLP开始。它的学习曲线更平缓代码更健壮。当你对线性规划的标准形式非常熟悉后可以再根据需要使用SciPy的linprog。4. 关键细节、踩坑点与模型扩展把模型跑通只是第一步。在实际应用中你会遇到各种预料之外的情况。下面分享几个关键的细节处理和常见“坑”。4.1 处理供给与需求不平衡前面我们假设了sum(S) sum(D)。但现实往往骨感。处理方法是引入“虚拟节点”。情况一供大于求 (sum(S) sum(D))总供给多了surplus sum(S) - sum(D)个单位。我们虚拟一个“需求地”比如叫“库存”或“废弃”编号为n1。它的需求量d_{n1} surplus。从所有真实供给地i到这个虚拟需求地的单位成本c_{i, n1} 0因为没运出去成本为零。如果你觉得存储有成本可以设为一个小的存储费。这样原问题就变成了一个具有m个供给地和n1个需求地的平衡问题。求解后运往虚拟需求地的量x_{i, n1}就代表了供给地i的剩余库存。情况二供不应求 (sum(S) sum(D))总需求多了shortage sum(D) - sum(S)个单位。我们虚拟一个“供给地”比如叫“外部采购”或“缺货”编号为m1。它的供给量s_{m1} shortage。从这个虚拟供给地到所有真实需求地j的单位成本c_{m1, j} M其中M是一个非常大的正数比如1e9。这相当于一个惩罚项告诉求解器“尽量不要用这个供给地”但如果用了就代表需求地j有shortage单位无法被满足缺货。求解后从虚拟供给地运出的量x_{m1, j}就代表了需求地j的缺货量。在代码中如何实现以PuLP为例在定义变量和约束前先对数据做平衡化处理def balance_transportation_supply_demand(S, D, C): total_supply sum(S) total_demand sum(D) if abs(total_supply - total_demand) 1e-9: # 浮点数比较近似相等 return S.copy(), D.copy(), C.copy() elif total_supply total_demand: # 供大于求加虚拟需求地 surplus total_supply - total_demand D_balanced np.append(D, surplus) # 成本矩阵增加一列成本为0 C_balanced np.hstack([C, np.zeros((len(S), 1))]) return S.copy(), D_balanced, C_balanced else: # 供不应求加虚拟供给地 shortage total_demand - total_supply S_balanced np.append(S, shortage) # 成本矩阵增加一行成本为一个大数M M 1e9 C_balanced np.vstack([C, M * np.ones((1, len(D)))]) return S_balanced, D.copy(), C_balanced # 使用平衡后的数据建模 S_bal, D_bal, C_bal balance_transportation_supply_demand(S, D, C) # 然后用 S_bal, D_bal, C_bal 去构建 PuLP 或 SciPy 模型4.2 模型无解或结果异常怎么办有时运行代码会得到“不可行”Infeasible或“无界”Unbounded的结果或者结果看起来很奇怪比如大量运量集中在惩罚成本M的路线上。检查数据一致性这是最常见的原因。确保所有供给量、需求量、成本都是非负数。特别检查在平衡化处理后总供给和总需求是否真的相等考虑浮点数误差。检查约束冲突比如某个需求地的需求量比所有能到达它的供给地的供给量总和还大那么即使加了虚拟供给地惩罚成本也会极高可能暗示原始问题定义有误。或者你错误地同时使用了和约束导致没有可行域。检查变量边界确保没有错误地限制了变量的上界导致无法满足需求。解读惩罚项如果结果中虚拟节点的运量很大说明原始问题不平衡严重或者真实成本矩阵下确实无法满足所有约束比如某些路线的真实成本也极高。你需要回到业务层面审视是数据错了还是业务假设如必须100%满足需要调整使用求解器诊断PuLP在求解时可以输出更详细的日志msgTrue。对于SciPy可以检查result对象的status和message属性。有时求解器会提示哪类约束可能导致了不可行。4.3 从运输问题到更一般的线性规划我们解决的运输问题其实是线性规划Linear Programming, LP的一个特例具有非常特殊的约束结构行和与列和。理解了它就掌握了LP建模的核心思想。更复杂的目标目标函数不一定是最小化成本。可以是最大化利润将成本改为负利润或者最小化最长运输时间这需要引入额外变量和约束可能变成线性规划或混合整数规划。更复杂的约束路径容量限制某条具体路线i-j有最大运量限制即x_{ij} U_{ij}。这直接在变量上添加一个上界bounds即可。需求弹性某个需求地j的需求量不是一个固定值d_j而是在[d_j_min, d_j_max]范围内满足越多收益越高。这需要引入额外的决策变量和约束将问题转化为一个“带需求弹性的运输问题”或“网络流问题”。多商品流同时运输多种不同的货物它们共享运输能力但可能有不同的成本和需求。这需要为每种商品定义一个决策变量矩阵X^k并添加耦合约束如总运力约束。整数约束如果资源必须整箱、整辆车运输就需要决策变量x_{ij}为整数。这问题就变成了整数线性规划ILP或更特殊的整数运输问题。在PuLP中定义变量时只需设置catInteger即可。但要注意整数规划求解难度和时间会大大增加。4.4 结果分析与可视化得到最优解X_opt后不能只看一个总成本数字。方案解读将X_opt矩阵与成本矩阵C对照查看。哪些路线被使用了为什么是这些路线是否符合你的业务直觉比如是否优先利用了低成本路线高成本路线是否因为产能或需求限制而不得不使用灵敏度分析影子价格这是线性规划的一个强大功能。它告诉你如果某个供给地的产能s_i稍微增加一个单位总成本能降低多少这个值称为该供给约束的影子价格Shadow Price或对偶变量。同样需求约束的影子价格表示需求增加一个单位会导致总成本增加多少。在SciPy的linprog结果中可以通过result.slack和result.con的相关属性获取需注意符号。在PuLP中可以使用constraint.pi属性来获取约束的对偶值。这能帮你识别出供应链中的瓶颈在哪里。简单可视化对于小规模问题可以用热力图直观展示。import matplotlib.pyplot as plt import seaborn as sns plt.figure(figsize(8, 6)) sns.heatmap(X_opt, annotTrue, fmt.1f, cmapYlOrRd, xticklabels[fStore{j1} for j in range(n)], yticklabels[fWarehouse{i1} for i in range(m)]) plt.title(Optimal Transportation Plan (Volume)) plt.xlabel(Destination (Demand)) plt.ylabel(Source (Supply)) plt.show() plt.figure(figsize(8, 6)) sns.heatmap(C, annotTrue, fmtd, cmapBlues, xticklabels[fStore{j1} for j in range(n)], yticklabels[fWarehouse{i1} for i in range(m)]) plt.title(Unit Transportation Cost) plt.xlabel(Destination (Demand)) plt.ylabel(Source (Supply)) plt.show()将运量热力图和成本热力图放在一起对比可以清晰看到优化方案是如何规避高成本区域的。5. 性能优化与大规模问题处理当供给地和需求地数量上升到几百甚至上千时直接使用上述方法可能会遇到性能瓶颈。这里有一些优化思路。5.1 稀疏矩阵与模型构建优化在运输问题中虽然成本矩阵C通常是稠密的每个路线都有成本但最优解X往往是非常稀疏的——只有少数路线有运量。对于超大规模问题如m, n 1000构造完整的m x n个变量和约束会消耗大量内存。PuLP的惰性创建PuLP在创建变量和约束时是相对高效的但变量数量巨大时仍会变慢。一个技巧是如果某些路线i-j的成本极高或逻辑上不可能你可以在建模时就不创建这个变量从而减少问题规模。这需要你在建模前对成本矩阵进行预处理。使用专业求解器接口PuLP支持调用更强大的商业求解器如Gurobi, CPLEX或开源求解器如COIN-OR CBC。对于大规模问题这些求解器内部的算法优化比SciPy的linprog要高效得多。安装并配置好这些求解器后在prob.solve()时指定即可例如prob.solve(pulp.GUROBI())。利用问题结构运输问题是一种特殊的最小费用流问题有专门针对网络流设计的算法如网络单纯形法。SciPy的linprog的highs方法内部可能自动识别并利用这种结构。PuLP调用的CBC求解器也对网络流问题有很好的优化。5.2 分块求解与启发式算法如果问题规模实在太大无法一次性求解可以考虑地理分块如果供给地和需求地有明显的区域聚集性比如华东区、华北区可以先按区域分解成几个小问题独立求解再处理区域间的调拨。这属于“分解协调”的思想。启发式算法当精确的线性规划求解太慢时可以求助于启发式算法来快速得到一个“还不错”的可行解。对于运输问题经典的启发式算法有最小成本法Matrix Minimum Method每次选择当前成本矩阵中最低的c_{ij}尽可能多地分配运量直到所有供给和需求被满足。Vogel近似法Vogel‘s Approximation Method, VAM比最小成本法更聪明通过计算每行和每列的“罚数”次小成本与最小成本的差优先在罚数最大的行或列中分配最小成本单元格的运量。VAM通常能得到一个非常接近最优解的初始可行解。 你可以用Python实现这些启发式算法得到一个初始解。这个初始解既可以作为最终方案对于精度要求不高的场景也可以作为精确求解器如linprog的初始点以加速求解过程。这里提供一个Vogel近似法的简单实现框架帮助你理解def vogels_approximation_method(C, S, D): 使用Vogel近似法求运输问题的一个初始可行解。 返回一个与C同形的运量矩阵X。 注意这是一个简化实现假设问题平衡且未处理退化情况。 m, n C.shape X np.zeros((m, n)) supply S.copy() demand D.copy() cost C.copy() while supply.sum() 0: # 还有供给剩余 # 计算每行和每列的罚数 row_penalties [] for i in range(m): if supply[i] 0: # 取该行所有有效成本对应需求0的列 row_costs [cost[i][j] for j in range(n) if demand[j] 0] if len(row_costs) 2: sorted_costs sorted(row_costs) penalty sorted_costs[1] - sorted_costs[0] else: penalty 0 row_penalties.append(penalty) else: row_penalties.append(-1) # 标记供给已耗尽的列 col_penalties [] for j in range(n): if demand[j] 0: col_costs [cost[i][j] for i in range(m) if supply[i] 0] if len(col_costs) 2: sorted_costs sorted(col_costs) penalty sorted_costs[1] - sorted_costs[0] else: penalty 0 col_penalties.append(penalty) else: col_penalties.append(-1) # 找到最大罚数及其位置行或列 max_row_pen max(row_penalties) max_col_pen max(col_penalties) if max_row_pen max_col_pen: # 在行中分配 i row_penalties.index(max_row_pen) # 找到该行中在有效列里成本最小的列j valid_j [j for j in range(n) if demand[j] 0] if not valid_j: break j_min min(valid_j, keylambda j: cost[i][j]) else: # 在列中分配 j col_penalties.index(max_col_pen) # 找到该列中在有效行里成本最小的行i valid_i [i for i in range(m) if supply[i] 0] if not valid_i: break i_min min(valid_i, keylambda i: cost[i][j]) i, j i_min, j # 在单元格(i, j)分配运量 alloc min(supply[i], demand[j]) X[i][j] alloc supply[i] - alloc demand[j] - alloc # 如果供给或需求耗尽可以将对应行或列的成本标记为无穷大或一个很大的数 # 以在后续迭代中忽略这里简化处理依赖while循环条件 return X这个初始解X_init的总成本np.sum(C * X_init)通常会比最优解高但可以作为参考或者作为linprog的初始可行解通过x0参数传入有时能减少迭代次数。6. 举一反三模型在其他场景下的应用这个“多供给地-多需求地”的模型框架极其通用绝不仅限于物流运输。只要你面对的是“资源从多处来到多处去要优化分配”的问题都可以尝试套用。生产计划多个工厂供给地生产同一种产品供应多个市场需求地。每个工厂有最大产能供给量每个市场有预测需求需求量从工厂到市场的运输有成本或时间。目标是最小化总成本或总交付时间。人员调度多个项目组需求地需要具有特定技能的员工公司内部有多个空闲团队或个人供给地可供调配。调配会产生一定的“成本”如适应时间、差旅费。目标是在满足项目需求的前提下最小化总调配成本。云资源分配多个数据中心供给地有可用的计算/存储资源多个业务应用需求地需要资源。在不同的数据中心部署应用网络延迟和带宽成本不同。目标是在满足应用性能需求可转化为需求量的前提下最小化总延迟或总带宽成本。投资组合这可以看作一个“单供给地-多需求地”的特例。你有一笔总资金供给量要分配到多个投资标的需求地每个标的有一个最低/最高投资额需求。每个标的有不同的预期回报率和风险可以转化为“成本”追求高回报即最小化负回报。目标是在风险约束下最大化总回报即最小化总负回报。关键在于识别出问题中的“供给”、“需求”、“成本/收益”和“决策变量”。一旦抽象出来剩下的就是调整模型细节比如约束是、还是目标是最小化还是最大化变量是否需要整数约束然后就可以用同样的Python工具链来求解了。我自己在做一个内部工具调度项目时就用了这个模型。当时有十几台测试服务器供给地供给量是服务器可用的CPU核时和几十个待执行的测试任务需求地需求量是任务预估需要的CPU核时每个任务对服务器有偏好某些任务在特定服务器上跑得快成本低。我用PuLP建了个模几分钟就算出了最优分配方案比手动排期效率高多了还避免了资源闲置。最大的体会是把业务问题准确地翻译成数学模型比写求解代码本身更需要功夫。多和业务方沟通明确每一个数字成本、供给、需求的真实含义是模型成功的前提。