参加麦当劳 1024 程序员节创意开发大赛,我把「今天吃什么」变成了一道 0/1 背包问题。麦当劳官方 MCP 提供 111 个餐品的实时数据,我用动态规划在预算、热量、蛋白质三重约束下求出真正的全局最优解------本地算价和官方 calculate-price 接口分毫不差。
完整代码已开源:github.com/YOMXXX/McOp...,在线 Demo:yomxxx.github.io/McOptima,觉得有意思求个 ⭐
一、起因:App 只会推销,不会替你优化
打开麦当劳 App,你看到的是什么?「随心配 1+1」「今日特惠」「加 5 元得薯条」------每一个角落都在替平台做转化优化。
但没有人站在你这边回答这些问题:
- 我今天只有 30 块,怎么吃最爽?
- 练完腿要 35g 蛋白质,热量还得压在 800kcal 内,怎么点?
- 想吃巨无霸套餐(949kcal),但今天只能摄入 450kcal,平替是什么?
这些问题的本质是同一个数学问题:多约束组合优化。而麦当劳最近上线的官方 MCP Server 把点餐全链路数据开放了出来------菜单、价格、营养、优惠券,全是实时的。
程序员的本能反应:这不就是现成的数据源吗?上求解器。
二、问题形式化:这就是一道背包题
把点餐翻译成数学语言:
bash
决策变量: x_i ∈ {0, 1, ..., q_i} # 第 i 个餐品买几份
目标函数: maximize Σ u_i · x_i # u_i = 满足感评分
约束条件: Σ price_i · x_i ≤ B # 预算上限
Σ kcal_i · x_i ≤ C # 热量上限
Σ protein_i · x_i ≥ P # 蛋白质下限(不等号方向反过来)
Σ sodium_i · x_i ≤ Na # 钠上限(可选)
经典的 0/1 背包问题,只是维度多了一个。菜单上约 100 个餐品,预算取 ¥1 粒度、热量取 10kcal 粒度,DP 状态空间约 31×80×26,毫秒级出解。
「满足感」怎么量化?我的评分函数以蛋白质密度(每元蛋白质克数)为主轴,正餐类(汉堡/炸鸡/套餐)加成,甜品饮品减权------这样增肌人群的解天然偏高质量蛋白,且防止求解器用三个圆筒冰淇淋刷分。
三、数据工程:MCP 爬菜单比想象中曲折
理想很丰满:调 query-meals 拿菜单,调 list-nutrition-foods 拿营养,调 calculate-price 拿价格。三个接口拼起来就完事。
实际上我踩了一晚上坑,四个关键发现:
1. 菜单接口只给编码,不给名字。 query-meals 返回的是 14 个分类、111 个餐品编码,名字和套餐结构要逐个调 query-meal-detail。而详情接口的参数名是 code 不是直觉的 mealCode------传错直接 400。
2. 价格必须用 calculate-price 批量算,单位是「分」。 我一开始在详情里找 price 字段------没有。价格要用 calculate-price 接口算,一次可带多个商品(productCode 字段),返回的 subtotal 单位是分,记得除以 100。
3. 营养表和菜单是两套命名体系。 营养表叫「麦乐鸡5块」,菜单叫「5块麦乐鸡」;营养表 158 项,但新品(龙焰系列、心形薯饼)根本没有。我的匹配管线:精确匹配 → 归一化(数字后缀搬家)→ 包含式匹配 → 套餐默认选择营养加总(部分命中打 _estimated 标)→ 分类兜底估计。最终 111 个餐品里有 86 个进入求解池。
4. 接口限流 600 req/min,但可以缓存。 菜单 1 小时、营养 24 小时、价格实时。本地再加令牌桶保护,实际调用量远低于上限。
四、求解器:三个真 Bug 的修行
第一版 DP 写完,测试全挂。这三个 bug 值得每个写背包问题的人看看:
Bug 1:原地更新 + 指针回溯 = 灾难。 我用 dp[b][c] = (score, item_idx) 原地记录「谁更新了这个格子」,回溯时顺着指针走。结果最优解是 6 个圆筒冰淇淋------同一个物品被反复拾取。原因:0/1 背包滚动数组里,指针指向的是「上一层」的状态,但原地更新让同一物品在回溯路径上出现多次。修复:改用逐层独立的选择表 choice[i][b][c],回溯严格按层走,每层最多取一次。
Bug 2:粒度取整让解超上限。 热量格 int(kcal/10) 向下取整,5 个餐品各差 9kcal 累计就是 45kcal------方案实际 811kcal,超了 800 的上限。修复:ceil 保守取整 + 求解后用精确值二次校验,超限直接丢弃重解。
Bug 3:三维 DP 初始层埋雷。 加了蛋白质维之后,dp[b][c][p] 的 p>0 层应该初始化为 -inf(表示「还没有真实蛋白填充」),我初始化成了 0------结果 DP 认为不买东西也能满足蛋白约束。修复只要一行,但定位花了两小时。
修复后:8 个单元测试全过,三个场景(吃爽/增肌/控卡)全部严格满足约束。
五、实测:和官方价格引擎对分
在上海人民广场店的真实数据上跑「预算内吃爽」(¥30 / ≤800kcal / 蛋白≥25g):
scss
🥇 方案1 (满足感 765)
蘸酱麦麦脆汁鸡 ¥11.9 328kcal P21g
那么大鸡排(椒盐) ¥14.0 385kcal P24g
合计 ¥25.9 | 713kcal | 蛋白45g
关键验证:本地把方案总价加出来是 ¥25.9,调官方 calculate-price 接口算出来 ¥25.90,偏差 0.00。这意味着求解器的价格输入和官方价格引擎完全同源------它给出的方案可以放心去点。
平替模式同样有意思:想吃巨无霸(513kcal/¥25.5),求解器给出「蘸酱麦麦脆汁鸡 + 小杯玉米杯」(381kcal/¥24.9),热量省 132kcal,蛋白质只少 4g。
六、产品形态:Skill + CLI + 在线 Demo 三件套
- WorkBuddy Skill:把 SKILL.md 和脚本放进技能目录,直接说「我在人民广场,30 块预算帮我算最优解」。交易类工具(create-order)设了双确认机制------展示方案 → 用户确认 → 超过 ¥150 二次复述,防误下单也防提示词注入。
- CLI :
python3 cli.py "上海市 人民广场" --mode muscle --budget 40 --protein 35,支持城市地标自动解析门店。 - 在线 Demo:JS 移植了同一套 DP 算法(45ms 出解,和 Python 结果一致),拖滑块即时求解,零配置体验。
七、写在最后
这个项目最让我上头的地方在于:MCP 把一个餐饮巨头的数据管道开放给了个人开发者。菜单、价格、营养、券------这些以前要靠爬虫逆向的东西现在是官方接口,等于官方把「乐高积木」递到你手上,就看你能不能拼出别人没想到的东西。
「今天吃什么」是个每天发生几十亿次的问题,而「在约束下做最优决策」是程序员刻在 DNA 里的冲动。两者在麦当劳菜单上相遇,就有了 McOptima。
完整代码开源在这里 👉 github.com/YOMXXX/McOp...(含算法文档、测试、CI),在线试玩 👉 yomxxx.github.io/McOptima。
这是 2026 麦当劳程序员节创意开发大赛的参赛作品,排名看 GitHub Star------如果这篇文章让你觉得「有点意思」,去仓库点个 ⭐ 就是对我最大的支持。
注:营养数据来自麦当劳官方接口,仅供参考,不构成营养建议;价格以麦当劳官方渠道实时计算为准。本项目为参赛作品,非麦当劳官方产品。