外卖平台百万用户优惠券最优分配:从暴力穷举到拉格朗日乘子法实践

3 阅读2分钟

一、问题背景与核心前提

核心目标:在总预算≤200万元的约束下,为100万名用户分配0元(不发)、5元、10元优惠券,最大化整体下单增量(Uplift)。

核心前提:通过Uplift Model已预测每个用户在不同券值下的下单概率,进而计算出每种券值对应的增量收益(Uplift)——即“发券比不发券多带来的下单概率提升”。示例数据如下(简化版):

用户 IDP(不发)P(5元券)P(10元券)5元增量 (Uplift)10元增量 (Uplift)
用户 A0.10.120.130.020.03
用户 B0.40.700.850.300.45
用户 C0.80.830.870.030.07
用户 D0.90.910.920.010.02

注:0元券即不发券,增量为0,成本也为0。

二、基础解法的局限:暴力穷举与简单贪心为何失效?

面对优惠券分配问题,最直观的两种思路是“暴力穷举”和“简单贪心”,但在实际业务场景(百万级用户、有限预算)中均会失效,核心问题如下。

2.1 暴力穷举:指数级爆炸+约束失效

暴力穷举的逻辑的是:每个用户有3种选择(0元/5元/10元),遍历所有组合,筛选出“总预算≤200万”且“总增量最大”的组合。举个具体例子,结合前文4个简化用户(A、B、C、D),其所有组合共3⁴=81种,部分组合及对应效果如下:

组合求解非常的直观,每个用户有3种面额(0,5,10)可以选择,那么总过的组合数就有3^4种选择,如下所示

组合用户A用户B用户C用户D增量收益
100000
100050.01×D50.01 \times D-5
1000100.02×D100.02 \times D-10
100500.03×C50.03 \times C-5
100550.03×C5+0.01×D5 0.03 \times C-5+0.01 \times D-5
1005100.03×C5+0.0.2×D100.03 \times C-5+0.0.2 \times D-10
...

这种“暴力穷举”的方法会遇到两个致命的问题

1. 指数级爆炸 (Complexity)

如果有1000万个用户,那么总的组合是3^10000000种组合,电脑直接计算所有组合是不可能的

2. 约束条件的硬性限制

假设我们的预算是30元,暴力穷举遍历所有81种组合时,会优先选择“总净收益最高”的组合,而非“预算内增量最大”的组合。比如

  • 组合“0元/5元/0元/0元”,总预算5元,总净收益=0.30-5=-4.7;
  • 组合“0元/10元/0元/0元”,总预算10元,总净收益=0.45-10=-9.55;
  • 组合“5元/5元/0元/0元”,总预算10元,总净收益=0.02+0.30-10=-9.68。

对比来看,组合“0元/5元/0元/0元”的净收益(-4.7)是三者中最高的,算法会优先选择这个组合,此时仅花费5元预算,远低于30元的给定预算。

核心原因的是:当券值成本过高、导致“增量收益无法覆盖成本”时,暴力穷举的净收益计算逻辑会让算法主动放弃发放更多优惠券,哪怕有剩余预算,也不会为了“花完预算”而选择净收益更低的组合。

而在实际业务中,老板可能为了抢占市场份额、提升用户活跃度,要求必须花完指定预算,此时暴力穷举的这种逻辑就会完全失效,无法满足业务约束。

  • 上面公式: max(UpliftCost)\text{max}(\text{Uplift}-\text{Cost}) 。结果可能是:为了利润最大化,算法建议只花 10 万块。但老板的目标是市场份额,要求必须花完 100 万。
  • 实际解法: 这是一个​带约束的优化问题​。我们需要在Cost<=100W\sum \text{Cost} <= 100W的前提下,最大化Uplift\sum \text{Uplift}

贪心求解

贪心求解也非常的直观简单,我们知道了用户在不同金额下的增量收益,直接按照每个用户增量收益的最大值倒排就可以。然后依次选取用户,直到满足预算要求

用户 ID5元增量 (Uplift)10元增量 (Uplift)取值
用户 A0.02A-50.03A-10max(0.02A-5,0.03A-10)
用户 B0.30B-50.45B-10max(0.30B-5,0.45B-10)
用户 C0.03C-50.07C-10max(0.03C-5,0.07C-10)
用户 D0.01D-50.02D-10max(0.01D-5,0.02D-10)

这种“按每个用户最大收益倒排”的贪心策略非常直观,但它在资源受限(有总预算)的情况下,会掉入一个经典的“局部最优陷阱”。

这种做法存在两个核心逻辑漏洞,会导致你最终的总增量收益远低于分数阶背包(边际性价比排序)方案:

1. 忽略了“成本占位” (The Efficiency Problem)

贪心算法如果只看“绝对增量”,会优先把钱分给那些“胃口大”的用户,导致预算被迅速耗尽。

举个例子: 假设你剩下 10 元预算:

  • 用户 A: 10元券带来 0.1 增量。按你的逻辑,他的 max 收益是 0.1,排在前面。
  • 用户 B: 5元券带来 0.08 增量。
  • 用户 C: 5元券带来 0.08 增量。

简单贪心策略: 把 10 元全部给用户 A。​总收益 = 0.1​。

背包策略(看性价比): 用户 B 和 C 的性价比(0.08/5=0.016)高于用户 A(0.1/10=0.01)。 结果: 把 10 元拆开给 B 和 C。​总收益 = 0.08 + 0.08 = 0.16​。

结论: 只看增量最大值,会让你为了抢几个“大客户”,而丢掉了一群“高性价比小客户”。

2. 忽视了“升档的代价” (The Marginal Gain Problem)

在多面额场景下,每个用户内部的选项是互斥的。

假设用户 B:

  • 5元增量:0.30
  • 10元增量:0.32

按简单贪心策略,用户 B 的 max 是 0.32(10元档)。你会直接花 10 元在他身上。 但实际上,​多花的那 5 元钱只带来了 0.02 的极小增量​。 如果这 5 元钱省下来发给另一个“5元增量为 0.1”的用户 D,你原本可以用同样的 10 元预算换取 0.3+0.1=0.4的收益,而不是 0.32。

三、工业界最优解:约束规划与拉格朗日乘子法

在工业界,当面对几千万用户、多个面额、多种预算限制时,你之前提到的“穷举组合”或“简单贪心”都会失效。约束规划(Constrained Optimization) 的本质是把你的业务目标和限制条件翻译成数学语言,然后用高效的算法求出那个“最优组合”。

3.1 约束规划:将业务目标转化为数学语言

目标函数: 最大化总增量(如总 GMV 增量或总转化人数)。

maxijxi,jUplifti,j\max \sum _{i}\sum _{j}x_{i,j}\cdot \text{Uplift}_{i,j}

约束条件:

  1. 预算约束:ijxi,jCostjTotal Budget\sum_i \sum_j x_{i,j} \cdot \text{Cost}_j \le \text{Total Budget} (总预算不能超)。
  2. 唯一性约束: jxi,j\sum_j x_{i,j} (每个用户只能拿到一种券或不拿)。
  3. ROI 约束(可选): 例如,增量 GMV / 投入成本 >= 目标阈值,10元券的总数不能超过5万张等 。

由于用户量太大,直接解这个 0/1 整数规划(IP)是 NP-Hard 问题,算不动。工业界的标准解法使用拉格朗日乘子法。

通过拉格朗日乘子 λ\lambda,把​“预算约束”​从限制条件里挪到目标函数里,变成:

Object=Upliftλ×(Cost - Budget)\text{Object} = \text{Uplift} - \lambda \times (\text{Cost - Budget})

这里的λ\lambda就像是一个动态杠杆 或者 ​影子价格,它代表了:在当前预算下,你每多花 1 块钱,必须至少换回多少增量。

拉格朗日乘子法

当用户规模达到千万甚至亿级时,直接解线性规划(LP)太慢。工业界常用​分数阶背包(Fractional Knapsack)​的思想:

  1. 计算性价比 (Efficiency Score): 对于每个用户,计算每一档券的“单位成本增量”:

    Ei,j=Uplifti,jCostjE_{i,j}=\frac{\text{Uplift}_{i,j}}{\text{Cost}_{j}}
  2. 寻找最优阈值λ\lambda 通过二分查找或随机梯度下降,找到一个全局的 λ\lambda代表每一块钱预算能买到的最小增量)。

  3. 决策逻辑: 只有当某个券额带来的增益 Ei,j>λE_{i,j}>\lambda时,才发放该券。如果有多档券满足,选Ei,jE_{i,j}最大的那一档。

1. 数据准备(离线打分)

首先,通过 Uplift Model 为每个用户预测在不同券值下的​下单概率​。

用户 IDP(不发)P(5元券)P(10元券)5元增量 (Uplift)10元增量 (Uplift)
用户 A0.10.120.130.020.03
用户 B0.40.700.850.300.45
用户 C0.80.810.820.010.02

2. 计算“性价比系数” (ROI/Efficiency)

计算每一块钱能换回多少“增量下单概率”:

  • 用户 A:
    • 5元券性价比:0.02/5=0.004
    • 10元券性价比:0.03/10=0.003
  • 用户 B:
    • 5元券性价比:0.3/5=0.06
    • 10元券性价比:0.45/10=0.045
  • 用户 C:
    • 性价比极低(近乎 0),属于“无论如何都会买”的自然转化。

3. 寻找全局最优阈值 (λ\lambda)

系统会对全量用户的所有券种性价比进行​倒序排列​。 假设根据 200万 预算,算出的全局性价比阈值λ=0.005\lambda=0.005

  1. 最终分配决策逻辑

对于每个用户,我们只选 性价比>λ性价比 > \lambda性价比最高 的那一档:

  • 用户 A: 5元和10元的性价比(0.004, 0.003)都低于阈值 0.005。
    • 决策:不发券​(省钱,因为他不敏感)。
  • 用户 B: 5元(0.06) 和 10元(0.045) 都高于阈值。
    • 虽然 10 元券带来的绝对增量(0.45)更大,但 ​5元券的性价比(0.06)更高​。
    • 决策:发 5 元券​(为了在预算限制下覆盖更多像 B 这样的人)。
  • 用户 C: 性价比太低。
    • 决策:不发券​(避免浪费)。

四、拉格朗日乘子法的核心证明(通俗理解)

1. 直观证明:边际性价比相等即最优

假设存在一个比拉格朗日法更好的方案,那意味着在这个方案里,一定存在两个用户 A 和 B,他们的边际 ROI 不相等(比如 A 是 5,B 是 3)。

  • 既然不相等,我就可以从 B 那里扣掉 1 元钱,补给 A。
  • B 损失了 3 元收益,但 A 创造了 5 元收益。
  • 总收益增加了 2 元,且总预算没变。
  • 这说明原方案不是最优的。只有当所有人的边际收益都等于同一个λ\lambda时,你才无法通过“挪动预算”来增加总收益。这就是全局最优。

2. 数学条件:KKT条件与强对偶性

1. 目标函数的拆解(解耦证明)
L=ijxijRijλ(ijxijCijB)\begin{aligned} L&=\sum _{i}\sum _{j}x_{ij}R_{ij}-\lambda \left(\sum _{i}\sum _{j}x_{ij}C_{ij}-B\right) \end{aligned}
  1. RR:这是你想​“赚”​到的东西。
  2. λC\lambda C:这是你为了赚这些钱所​**“付出的代价”**​,经过一个系数λ\lambda(汇率)折算后的结果。
  3. λ\lambda​:这是一个​**“转换杠杆”**​。
    • 如果λ=3\lambda = 3,意思是在你眼里,1块钱的成本支出必须能换回​3块钱的增量收益​,这笔买卖才算保本(净增益为0)。
    • 如果某个方案的RλC>0R-\lambda C > 0 ,说明这个方案的 ROI 超过了你的及格线,​值得发券​。

但这里隐藏了两个关键约束

  1. 排他性约束(每个用户只能领一张券)
    jxij1\sum _{j}x_{ij}\le 1
  • ii是第i个用户
  • jj是第j种优惠券金额(例如0,3,5)
  1. 预算约束(总钱数不能超)
ijxijCijB\sum _{i}\sum _{j}x_{ij}C_{ij}\le B
  • CiC_i是你给第ii 个用户发券的成本。
  • BB 是财务或老板批给你的总经费(比如:这次双11红包活动总共只能发 1000 万元)。

求解最值时(暂时忽略常数B)

L=maxi[jxij(RijλCij)]L=\max \sum _{i}\left[\sum _{j}x_{ij}(R_{ij}-\lambda C_{ij})\right]
  • 这个公式是一个​大求和​。
  • 每一个用户ii 对应的部分jxij(RijλCij)\sum _{j}x_{ij}(R_{ij}-\lambda C_{ij})是相互独立的。
  • 因为每个用户只能选一张券(即每个ii对应的xijx_{ij}只有一个能等于 1),所以对于每个用户ii 来说,他这一项能提供的最大贡献,就是选出那个能让 xij(RijλCij)x_{ij}(R_{ij}-\lambda C_{ij}) 结果最大的 jj

如果每一项加数都是它能达到的最大值,那么它们的总和必然也是最大值。这就是拉格朗日法把“全局大难题”化解为“个人小计算”的魔力。

2. KKT 条件(最优解的必要条件)

对于带约束的优化问题,最优解必须满足 ​KKT 条件​。其中最核心的一条是:​梯度对齐​。

  • 在最优状态下,目标函数的梯度R\nabla R 和约束条件的梯度 C\nabla C必须方向一致,比例就是λ\lambda
  • 即:R1C1=R2C2=...=λ\frac{\partial R_1}{\partial C_1} = \frac{\partial R_2}{\partial C_2} = ... = \lambda

这证明了:最优分配时,所有人的“边际性价比”必须相等。 如果 A 的边际性价比是 5,B 是 3,那你显然应该把给 B 的钱挪给 A,直到两者的边际收益相等为止。

3. 强对偶性(Strong Duality)的保证(没看懂)
  • 在优惠券场景中,如果我们将收益函数看作是拟凸的(或者在离散情况下取凸包),该问题就满足 ​Slater 条件​。
  • 数学证明,当满足这些条件时,​原问题的最优解等于对偶问题的最优解​(即强对偶性成立)。
  • 这意味着,我们去寻找那个让预算刚好耗尽的 λ\lambda,这个过程本身就在逼近原问题的全局最优解。

3. 具体操作

第一步:设定一个全场统一的“及格线”λ\lambda

这个 λ\lambda的物理意义是:​**“每多花 1 元预算,必须至少多换回λ\lambda元的收益”**​。

  • 如果 λ=2\lambda=2,说明你要求每一块钱投入至少产出 2 块钱增量。
  • 你可以先随手猜一个数字,比如λ=3\lambda=3

第二步:让每个用户“各扫门前雪”(独立决策)

这是拉格朗日法最天才的地方。​不需要全局排序​,每个用户 ii只需要在自己面前的几种面额(0元, 5元, 10元...)中选一个最划算的。

选哪个呢?选能让下面这个​“净收益”​最大的面额xix_i

个人净收益=该面额带来的GMV增益λ×该面额的成本个人净收益=该面额带来的GMV增益-\lambda \times 该面额的成本
  • 例子​:用户 A 面前有两张券:
    • 5元券​:产出 20 元,净收益=203×5=5净收益 =20-3 \times 5 = 5
    • 10元券​:产出 22 元,净收益=223×10=8净收益 =22-3 \times 10=-8
    • 结果​:在 λ=3\lambda = 3 的要求下,用户 A 选 5元券

第三步:看总钱数,调“水坝高度” (λ\lambda)

当所有用户都选好后,系统把大家选的券金额加起来,看看​总预算​:

  • 如果钱花多了​(超过预算):说明及格线λ\lambda定得太低了,要求太松。​调高λ\lambda (比如调到 5),这会挤掉那些性价比不高的券。
  • 如果钱没花完​(剩太多):说明及格线λ\lambda定太高了。​**调低 λ\lambda**​(比如调到 1.5),让更多人能领到券。

拉格朗日法通过 λ\lambda预先协调了所有人。它告诉所有人:“大家的‘机会成本’都是λ\lambda

  • 这样,当 A 选择了一个大面额券时,是因为他确实能产生比λ\lambda更高的边际收益;
  • 如果 A 产生不了那么高的收益,在RλCR-\lambda C的法则下,他会自动缩减到小面额券,把预算“让”给更有潜力的 B。