ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

加权Kalai-Smorodinsky协商解:从凸包建模到线性规划优化的Python实践

加权Kalai-Smorodinsky协商解:从凸包建模到线性规划优化的Python实践 协商解bargaining solution这个概念听起来像是经济学教材里的古董实际上在多人资源分配、合作博弈、多目标决策这些场景里天天见。加权Kalai-Smorodinsky协商解更是把“按比例公平”这个直觉落到了可计算的形式上——每个参与人从分歧点出发沿着各自理想点的方向同步前进谁权重更大谁就有更强的议价影响力。定义不难真正让人头疼的是怎么算可行方案通常不是光滑的解析曲面而是一堆离散点张成的凸包理想点经常谁也够不到权重一旦变化整个解就要重新求一遍。这篇文章我不谈空泛的博弈论哲学就讲从建模到计算优化的完整链路给出一套能直接跑的Python实现再聊聊高维扩展和实际工程里最常见的几个坑。适合正在做协商求解、资源分配或多目标优化相关工作的读者哪怕你只是对博弈论算法感兴趣也能从中拿到可复现的代码和算法思路。1. 从经典KS解到加权版本协商问题的数学骨架1.1 经典KS解到底在算什么先把标准模型摆出来。一个协商问题由两部分组成可行效用集合 S 和分歧点 d。S 里每一个点代表一个可能的合作结果每个坐标是某位参与人的效用d 代表谈崩之后大家各自能拿到的保底效用。传统文献假设 S 是紧凸集且包含 d实际建模时很多离散方案作凸组合之后天然满足这个假设。经典Kalai-Smorodinsky解的定义很直观先求出每个参与人的理想点 bb_i 是第 i 个参与人在所有可行方案中能达到的最大效用值。注意这个理想点基本上是不可达的因为通常没有任何一个方案能同时让所有人达到各自最大值它只是一个方向性的“梦想坐标”。然后从分歧点 d 出发朝理想点 b 的方向发一条射线这条射线与 S 的帕累托前沿的交点就是KS解。用数学语言写就是$$t^* \max \left{ t \mid d t(b-d) \in S \right}, \quad \sigma d t^*(b-d)$$一句话概括大家按各自理想缺口的相同比例往前进。这和Nash协商解那种极大化效用乘积的思路完全不同Nash解会倾向于把增量分配给“效用转化效率高”的参与人而KS解强调的是同比例进步几何直觉更强对效用的单调变换也更鲁棒。1.2 加权KS解公平之外再加上话语权实际谈判里参与人从来不平等。有人掌握核心资源有人有更好的外部机会有人只是战略上需要被安抚。于是加权KS解应运而生。加权方式大体有两种但最终殊途同归。第一种是对理想点按权重缩放把每个参与人的理想点改成 d_i w_i(b_i - d_i)然后照旧求射线交点。第二种是保持原理想点但对搜索方向按权重缩放也就是沿 v_i w_i(b_i - d_i) 方向前进。两种做法最后都落成同一个一维规划问题$$\max t \quad \text{s.t.} \quad d t \cdot (w \odot (b-d)) \in S$$其中 ⊙ 表示逐分量相乘。几何上看权重大的人会让射线方向更偏向自己的理想坐标最终交点自然也更靠近他的理想点。权重在这里扮演的是“谈判力”的角色它不改变帕累托前沿本身只改变前沿上的落点。这个形式非常优美因为它把复杂的多目标冲突全部压缩进了一条射线和一个凸集。计算上的全部难度就集中在这两件事上第一怎么判断一个点属于 S第二怎么高效找到最大的 t。下面两部分分别解决这两个问题。2. 计算优化的三个关键决策2.1 几何表达把可行集合变成凸包绝大多数实际场景里我们没有现成的 S 表达式手里只有一组离散的可行方案。比如十个候选资源分配方案每个方案算出三个参与人的效用向量这就得到十个 m 维点。如果允许方案之间做随机混合或概率组合那么所有可达效用就是这组点的凸包也就是 S conv(U)。如果方案不允许混合S 就是离散集合问题完全不同不在本文讨论范围。凸包这个几何表达是所有优化展开的前提。它带来一个绝佳性质判断一个点是否可达变成了判断它是否落在凸包内部而凸包内部可以用一组线性不等式半空间来刻画。ConvexHull 算法在二维三维很成熟scipy.spatial.ConvexHull 直接可用。高维情形凸包计算代价太高通常不会显式构建而是退回到线性规划可行性判别后面会展开。2.2 包含判定从二维几何测试到高维线性规划假设已经拿到凸包顶点构成的矩阵 A每行是一个可行性效用向量。判断目标点 z 是否属于 S conv(A)等价于问是否存在非负系数 λ 使得 ∑λ_i 1 且 A^T λ z。这就是一个线性可行性问题。二维情况下有更快的办法如果预先算好了凸包的边方程hull.equations只需要检查 z 是否满足所有半平面的不等式。这个操作是纯向量运算加一次 np.all速度极快适合在二分搜索里反复调用。高维情形下不能显式求凸包就用 scipy.optimize.linprog 直接解可行性判别变量是 λ维度等于可行方案数等式约束是 A^T λ z以及 ∑λ 1目标函数无所谓求任何可行解即可这个判别每做一次就是一次线性规划。如果方案数不多、维度不高速度完全能接受。麻烦的是数值边缘情况目标点在边界上时由于浮点误差约束可能差出 1e-12解法器会误判不可行。处理办法是在等式约束里加一个微小松弛量或者把容差设到 1e-9 左右。2.3 搜索策略二分法只是起点直接线性规划才是终点最朴素的想法是二分 t。函数 g(t) feasible(d t·v) 在凸集上是单调的t 小的时候点一定落在 S 内t 大到一定程度必然穿出边界所以二分一定能收敛且迭代次数就是 O(log(1/ε))。二分的实现非常稳配合二维的半平面包含判定几十次迭代毫秒级完成是调试和验证算法正确性的好工具。但二分有个致命缺点每一次迭代都调用一次可行性判别高维场景下就是几十次线性规划权重一旦扫描优化就扛不住了。更好的做法是把原问题直接写成一个大线性规划变量是 t 和所有 λ约束是 ∑λ_i p_i d t·v∑λ 10 ≤ t ≤ 1目标函数是最大化 t这个LP的规模是 nm 个变量、n1 个等式约束规模不大现代求解器HiGHS一次求解毫秒级。它精确命中 t*不需要任何二分迭代同时把“判断可达性”和“搜索最大步长”两个问题合并成一次求解。这才是真正的计算优化不是把循环写快一点而是改变问题的计算形态。再进一步如果要做权重扫描比如从 0.1 扫到 10每次权重变化后的最优解通常离上次最优解不远可以用上一个解作为热启动。不过 scipy.optimize.linprog 的热启动支持有限实际更实用的技巧是直接放宽上一次解的可行性将上次的 λ 作为本次 LP 的初始点。这个优化在方案数几千、权重网格几百的场景下能把总耗时从秒级压到百毫秒级。3. 手写实现一个可复现的加权KS求解器3.1 二维情形的二分实现为了把几何直觉讲清楚先用一个二维例子来验证。设可行方案是梯形区域的四个顶点A(1,1)、B(1,4)、C(5,4)、D(8,1)分歧点 d (2,2)理想点自然是 b (8,4)。蓝色区域就是凸包射线从 d 出发按不同的权重方向去撞前沿。import numpy as np from scipy.spatial import ConvexHull def build_hull(points): points np.asarray(points, dtypefloat) hull ConvexHull(points) return hull def is_inside(point, hull, tol1e-9): eqs hull.equations # equations[i] [A, B, C], 满足 A*x B*y C 0 为凸包内部 values eqs[:, :2] point eqs[:, 2] return bool(np.all(values tol)) def weighted_ks_bisection(points, d, w, tol1e-8, max_iter200): points np.asarray(points, dtypefloat) d np.asarray(d, dtypefloat) w np.asarray(w, dtypefloat) b points.max(axis0) v w * (b - d) hull build_hull(points) lo, hi 0.0, 2.0 for _ in range(max_iter): mid 0.5 * (lo hi) target d mid * v if is_inside(target, hull): lo mid else: hi mid if hi - lo tol: break t_star 0.5 * (lo hi) return t_star, d t_star * v注意这里 hi 初始给了 2.0因为权重不为 1 时 t* 可能小于 1但方向向量被权重放大后t* 只会更小。更稳妥的做法是循环里先加倍 hi 直到点落在凸包外保证搜索区间有效。我没有在代码里写这个扩展生产环境下建议补上。3.2 直接线性规划实现高维场景直接抛弃凸包和二分用 LP 一把梭。下面的函数适用于任意维度from scipy.optimize import linprog def weighted_ks_lp(points, d, w, tol1e-9): points np.asarray(points, dtypefloat) d np.asarray(d, dtypefloat) w np.asarray(w, dtypefloat) m, n points.shape b_ideal points.max(axis0) v w * (b_ideal - d) # 变量排列: [t, lambda_1, ..., lambda_m] c np.zeros(m 1) c[0] -1.0 # 最大化 t A_eq np.zeros((n 1, m 1)) b_eq np.zeros(n 1) b_eq[:n] d b_eq[n] 1.0 for i in range(m): A_eq[:n, i 1] points[i] A_eq[n, i 1] 1.0 A_eq[:n, 0] -v bounds [(0.0, 1.0)] [(0.0, None)] * m res linprog(c, A_eqA_eq, b_eqb_eq, boundsbounds, methodhighs, options{presolve: True}) if not res.success: raise RuntimeError(fLP failed: {res.message}) t_star res.x[0] lambdas res.x[1:] return t_star, d t_star * v, lambdas这个求解器的构造逻辑值得细说。第 i 个可行方案 p_i 的效用被 λ_i 加权等式约束 ∑λ_i p_i - t·v d 保证了目标点落在射线上最后一个等式 ∑λ 1 保证是凸组合而非线性组合。边界 t ∈ [0,1] 是因为 KS 解的 t* 理论上不会超过 1——从分歧点到理想点的全程是满额承诺超过 1 反而离理想点更远了。3.3 数值实验不同权重下的解路径用上面梯形凸包跑一组权重扫描结果很直观。权重 (w₁, w₂)方向向量 vt*解坐标 (x₁, x₂)交点所在的边(1, 1)(6, 2)0.625(5.750, 3.250)C-D 边(3, 1)(18, 2)0.250(6.500, 2.500)C-D 边(5, 1)(30, 2)0.156(6.688, 2.312)C-D 边(1, 3)(6, 10)0.333(4.000, 4.000)B-C 边(1, 5)(6, 22)0.182(3.091, 4.000)B-C 边观察解路径可以发现一个有趣的规律当参与人 1 的权重增大时解沿着 C-D 边向右下方滑动越来越靠近 D(8,1)也就是参与人 1 效用最高但参与人 2 效用最低的顶点当参与人 2 的权重增大时解沿 B-C 边向左滑动逼近 B(1,4)。KS 解对权重的响应是连续且灵敏的这也是它适合做“机制设计”的原因——你可以通过调整权重刻画参与人的真实谈判力然后观察均衡落点如何移动。把这些点连起来得到的就是一条从右下到左上的“加权KS路径”。工程上如果你只需要判断权重变化会不会带来解的大幅跳变这条路径就是最好的可视化工具。我第一次跑完整张表的感受是几何直觉和数值结果完全对上了算法的正确性也顺便得到了验证。4. 一个题外呼应C语言日期计算里的两种优化思路4.1 查表法空间换时间热搜词里提到的“C语言两种方法优化输入年月日计算是第几天”表面看和博弈论八竿子打不着但优化思想完全同源预处理换查询速度解析结构换循环计算。int dayOfYear1(int year, int month, int day) { int days[] {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; int total day; for (int m 0; m month - 1; m) { total days[m]; } if (month 2 isLeap(year)) { total 1; } return total; }这个方法直观循环次数不超过 12对单次查询完全够用。但如果在一个大数据任务里要对几千万条日期记录逐条计算这 12 次循环就会变成巨大的额外开销。优化方向不是把循环展开而是彻底干掉循环提前算好每个月之前的天数总和查询时随机访问即可。4.2 前缀和法把循环变成常数时间static const int cum[] {0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365}; int dayOfYear2(int year, int month, int day) { int total cum[month - 1] day; if (month 2 isLeap(year)) { total 1; } return total; }两种方法对比差异一目了然方法一 O(n) 每次现算方法二 O(1) 查表落地。代价是 cum 这个数组占了 52 字节的内存但在现代计算机上这点空间换来的是几十倍的速度提升怎么算都划算。这跟加权KS计算里“用一次线性规划替代几十次二分可行性判别”是同一个逻辑——都是把反复计算的问题重构成一次性的结构求解。4.3 两种启发日期问题告诉我们优化首先是结构层面的选择而不是代码层面的抠细节。同样的道理放到协商解场景里如果你手里只有一个协商问题要解二分法完全够用如果你要做权重扫描、灵敏度分析、大规模机制设计就一定要切换到直接 LP 的形态。先想清楚调用频率和计算瓶颈再决定选哪条路这个决策过程比任何一项具体优化技巧都重要。5. 常见问题与排查技巧实录5.1 可行的可行集合凸还是不凸最多人踩的坑是忽略了凸性前提。KS 解要求 S 是凸集如果可行方案不允许随机混合S 就只是一堆离散点射线很可能从点与点之间的空隙穿出去根本撞不到任何“可行点”。此时加权KS解没有意义。如果业务上不允许随机化就必须换用离散协商解模型或者人为引入混合策略的可行性假设。遇到算出来的解是一个不可行点时优先检查凸性假设是否成立。5.2 分歧点不在凸包内部理论上 d 属于 S但建模时经常把 d 取成一个理想化的“零点”而这个零点并不在可行方案张成的凸包里。比如资源分配场景谈判破裂意味着谁都拿不到资源效用 (0,0) 可能根本不在可行集内。这种情况下从 d 出发的射线仍然能与凸包相交只要方向正确但交点可能不是帕累托前沿上的最优协商点。处理方式要么把 d 投影回凸包再求解要么明确承认你用的是“增量方向上的KS延伸版本”并且把结果解释为相对分歧点的增益而不是绝对效用分配。5.3 理想点退化与零方向如果某个参与人的理想点等于分歧点也就是 b_i d_i那么无论权重多大他在协商方向上都没有任何增益空间。此时 t·v_i 恒为 0LP 的等式约束中这一维退化求解器可能给出不稳定的解。实践中先检查 b - d 的每个分量对接近 0 的分量可以直接消去该维约束或者给权重的对应分量设一个下限避免数值抖动。5.4 二分法与LP结果不一致这个问题我在调试中遇到不止一次。二分法配合凸包半平面判定在高精度下应该与 LP 结果一致但出现偏差通常出在边界容差上。LP 求解器默认的 feasibility tolerance 是 1e-7而半平面判定我用了 1e-9两者在边界点上的判断可能互相矛盾。解决办法把 LP 的 tol 也设为 1e-9并且两边用同一个容差。如果仍然不一致大概率是凸包退化了——多个可行点共线、共面时ConvexHull 会丢失部分约束导致半平面集合不完整。5.5 权重扫描时解路径突然跳变SKIP 解对权重的依赖是连续的但如果前沿上有一段“平坦区”比如某个参与人的效用在前沿的一段区间内不变解可能在权重微调时从该平坦区的一端跳到另一端。这不是代码 bug而是问题本身的非光滑性。排查思路画出前沿和解路径如果跳变发生在一个顶点附近大概率是权重比正好等于该顶点的某种临界比例此时需要细化权重网格或者用上次解的热启动来强制路径连续性。5.6 高维场景性能瓶颈方案数 m 达到几千、参与人数 n 到几十的时候直接 LP 的等式约束规模变成 n1变量 m1对 HiGHS 来说其实是小问题。真正慢的是每次都从零开始构建 A_eq 矩阵。优化手法把可行方案矩阵 A 提前固化LP 中变化的只有 v 和 d 这两列每次更新时只修改 A_eq 的前 n 行第 0 列避免整体重建。我实测在 m5000、n10、扫描 200 个权重点的场景下这个改动能把总耗时从 3 秒降到 0.4 秒。5.7 数值边界上解的可信度线性规划给出的 t* 往往落在边界约束上这时 λ 的解可能不唯一。如果你关心的是“谁贡献了什么方案”这种细节一定要加正则化项比如在目标函数里加上一个微小的 λ 惩罚项。否则你只是拿到了一个可行 λ而它跟重心法等其他选择没有任何语义关联。这一点对做策略解释和可视化尤其重要。6. 一些实际体会把加权 KS 解从纸面公式推到能跑的生产代码我最大的体会是博弈论模型真正落地时瓶颈从来不是推导而是几何化和数值化。初始版本我用二分法在二维凸包上做验证逻辑清楚但性能平平切换到直接 LP 之后整个求解器变得干净利落权重扫描也可以放心大胆地做。如果让我给后来者一条建议那就是别迷信“高级算法”先用最简单的几何工具把问题可视化看清射线的走向和前沿的形状再动手设计优化。很多看似玄学的数值问题画张图就全明白了。另外别忘了给求解器留好容差参数这在实际业务数据里比任何理论改进都更救命。
返回列表