AIGC标识 双 11 凑单:满 300 减 50 怎么搭最省,12 个 SKU 同屏枚举实测

图 1:满减凑单计算器首屏。12 行商品清单(无线耳机 199、蓝牙音箱 259、机械键盘 169……)下面是三档满减规则:满 300 减 50、满 600 减 130、满 1000 减 250,包邮门槛 99 元

同样 12 件商品,原价 1007.40 元。

按「单价从高到低累加、过档就结算」的贪心法凑,实付 907.40 元。

换成子集枚举加动态规划,实付 757.40 元。

差 150 元。

差的不是优惠力度,是凑法:贪心拆成两单,各吃一次满 300 减 50;最优解把它们并成一单,吃满 1000 减 250。

我把这套求解写成了一个单文件网页,双击就能跑,不联网,不引第三方库。

下面每个数字都能在页面上点「求最优解」复算出来。

一、为什么值得看:满减是阶梯,过档不等于凑得最省

三档的折扣率分别是 16.7%、21.7%、25%,档次越高越划算。

麻烦在于购物车总额是固定的。

贪心在 458 元的时候就结算了第一单,理由很朴素:已经过 300 了。

一结算,后面剩下的商品就再也爬不到 1000 这条线。

12 件 1007.40 元的车,贪心拆成 458.00 元和 337.00 元两单各减 50,第三单 212.40 元没有任何档位可用,实付 907.40 元。

最优解是一单吃满 1000 减 250,实付 757.40 元。

150.00 元就是这个「提前结算」的代价。

这也是它不能靠手算的原因:12 件商品有 4,096 种划分方式,人眼在购物车里枚举不完。

二、怎么算:子集枚举铺底,分层 DP 兜住「同一档能不能重复吃」

算法一共三步。

第一步,把每件商品看成一个原子,n 件商品有 2^n 个子集。

先把每个子集的原价和算出来,再算出「这个子集单独成一单时最省花多少」,也就是在它有资格命中的档位里挑减得最多的那一档。

第二步,按「同一档满减能否被多单重复命中」分两条路走。

允许重复时用一维完全划分 DP:dp[mask] = min(dp[rest] + 单子集最优),每次只枚举包含 mask 最低位的那部分子集,同一种划分就不会算两遍。

不允许重复时改成 K 层 DP:第 t 层表示「前 t 档各用一次」,按门槛从小到大递推,取最大总优惠。

核心递推就这几行:

for(let mask=1;mask<size;mask++){
  const lowbit=mask&(-mask);
  let best=Infinity, sub=mask;
  while(sub>0){
    if(sub&lowbit) best=Math.min(best, dp[mask^sub]+singleCost[sub]);
    sub=(sub-1)&mask;
  }
  dp[mask]=best;
}

第三步,把最优值回溯成方案明细,顺便收集并列的其它最优解。

回溯是这轮唯一翻车的地方,第四节细讲。

三、实测:12 件和 14 件两组购物车

两组数据都是在本机真跑的,参数和结果放在同一张表里。

购物车 原价合计 贪心实付 最优实付 差额 子集数 2^n DP 转移 求解耗时
12 件(默认示例) ¥1007.40 ¥907.40 ¥757.40 ¥150.00 4,096 9,669 13.30 ms
14 件(数码清单) ¥2346.00 ¥2096.00 ¥1916.00 ¥180.00 16,384 3,693,968 29.70 ms

图 2:默认 12 件购物车的结果。大号字是最优实付 ¥757.40 和总优惠 ¥250.00,下面一排标签给出贪心实付 ¥907.40 与差额 ¥150.00,再下面是贪心明细和最优方案明细两张表

默认这组的最优解很干脆:12 件全部放进一单,原价 1007.40 元,命中满 1000 减 250,实付 757.40 元。

贪心的明细是三单:458.00 元和 337.00 元各减 50,剩下的 212.40 元连 300 都没到。

14 件那组更能说明问题。

最优解把购物车拆成三单,分别命中满 1000 减 250、满 600 减 130、满 300 减 50,总优惠 430 元,实付 1916 元。

贪心在同样的 14 件上只拿到 250 元优惠:它连开五单,每单刚过 300 就结算,最后剩 264 元那笔一分钱都减不掉。

差额 180 元,比 12 件那组还大。

图 3:14 件购物车的三单拆分方案。最优方案 1 到方案 5 并列列出,每单的原价、命中档位、优惠和实付逐行对齐,五个方案的实付都是 ¥1916.00

顺带一个反直觉的观察。

把「同档可重复叠加」开关打开,12 件那组的实付一分没变,还是 757.40 元。

但页面上报的 DP 转移次数从 9,669 涨到了 265,720。

耗时反而从 16.4 毫秒掉到了 2.9 毫秒。

原因是两种模式的计数口径不一样:允许重复的模式统计的是枚举次数,不允许重复的模式统计的是命中次数。

所以这个数字在两种模式之间不可比,只有耗时是同一把尺子。

四、验证与踩坑:一个「钱算对了却报无解」的真 bug

页面上有一块自检面板,7 条断言每次求解都会重算一遍。

图 4:自检面板。最优解不劣于贪心解、枚举组合数、求解耗时、实付非负、优惠非负、包邮、求解状态共 7 条断言,最后一条写着「成功,共找到 5 个最优方案」

14 件购物车第一次跑出来的时候,自检最后一条是红的。

提示原文是「求解状态:无解:未找到可行方案,请检查输入」。

可同一屏上方的金额已经算好了:最优实付 ¥1916.00,总优惠 ¥430.00。

也就是说,价格算对了,方案列表却是空的。

最小复现:购物车填到 14 件,满减三档照旧,点「求最优解」。

我把中间量打印出来,先确认最优值本身没错。

最大优惠正好是 430,而且有 586 个 mask 都能取到这个值。

问题出在回溯函数的一个守卫条件上。

// 错的:当前 mask 在上一层拿不到优惠时,整个「把某个子集分给这一档」的分支被跳过
if(prev[mask]>-Infinity){ ... }

prev[mask] 问的是「这堆商品在第 t-1 层能不能拿到优惠」,而这一层真正该判断的是「扣掉这一单之后剩下的商品在上一层有没有值」。

一行修复,把守卫换成当前层的目标值:

if(target>-Infinity){ ... }

子集能不能用,循环里的 prev[mask^sub]>-Infinity 已经在管了;外面再拿 prev[mask] 卡一道,会把「必须在这一层分配档位」的最优路径整条砍掉。

改完重跑,14 件给出 5 个并列最优方案,每个都是 4 单,实付都是 1916.00 元。

自检最后一条也变成绿色,文案是「成功,共找到 5 个最优方案」。

这个 bug 讨厌的地方在于它不影响价格,只影响复现路径。

只看金额的话,会一直以为它是好的。

五、本地复现

源码在公开仓库,单文件,没有构建步骤:

https://atomgit.com/deli007/demo_park/tree/main/codearts-coupon-stacking-lab

下载后双击 index.html 就能用,或者起一个本地静态服务:

git clone https://atomgit.com/deli007/demo_park.git
cd demo_park/codearts-coupon-stacking-lab
python -m http.server 8080

失败路径也列出来,方便对着提示原文核一遍:

输入 页面提示原文
购物车清空 购物车为空,请添加商品。
单价填 -5 第 1 行单价不能为负数。
满减门槛填 0 第 1 档满减门槛必须大于 0。
满 300 那一档填成减 400 第 1 档减额(400)不能大于门槛(300)。
商品总件数 16 件 商品总件数 16 超过子集枚举上限 15,请减少商品数量或种类。
一档满减都不填 实付等于原价,优惠 ¥0.00,不报错

图 5:失败路径。第一行单价填 -5 后点求解,顶部出现红色提示条「第 1 行单价不能为负数。」,下面的商品清单和上次的有效结果都原样保留

非法输入时页面不会清空,上一次的有效方案还留在下面。

15 件这个上限是刻意设的:14 件是 16,384 个子集,15 件是 32,768 个,浏览器还能在几十毫秒内跑完,再往上就只能干等。

六、准备环境:进入码道 Web

浏览器打开码道 Web 版:https://devcloud.cn-north-4.huaweicloud.com/chat?source=dmzntgwlb&sourcead=dmzntgwlb1&invitecode=INV-IXhxk5bwWx_qKSP1FcgSuiOEf6ruDXtjvLFiCH9nMs5dZGLapaSSylwt-cKRj5LoT3MlxikWxy8VhqplfvtQjefQOq31Tbp3R3wYP3K535Cn4XTaUzqqExKPYTWvvw4LijV14IS1torXdTGp7oyXb6SR5VxgYTg50jM,登录后就能在对话窗口里输入需求,不需要装软件。

码道有三种使用方式:WebUI(浏览器对话)、TUI(终端命令行)和桌面 IDE(IDE 插件)。本文用的是 WebUI 版。

这轮的原始需求一句话就能说清:做一个满减凑单计算器,用子集枚举加动态规划求最省方案,并把贪心结果并列对照。

其余细节都是在这句话后面补的:12 个示例 SKU、三档满减可增删、包邮门槛、同档是否可重复叠加、非法输入要保留上次结果、页面里要有自检面板。

七、使用码道体会

  • 「页面里要有自检面板,把断言结果显示出来」是这轮最值钱的一句需求。它把验证从我的动作变成页面的一部分,截图里就有能复算的数字。
  • 需求里点名要对照,比只说「做个计算器」有效。我写了「同时给出贪心凑单结果作为对照」,出来的界面上直接有一排对照标签。
  • 交付后逐张放大截图再提修改点,比笼统说「再优化一下」有效。第一版数字区字号偏小、命中档位这一列太挤,都是看图才发现的。
  • 模型给的算法骨架是对的,边界守卫不一定对。这次错的不是 DP 本体,是回溯里的一个条件判断,只有把手算不出来的那组数据喂进去才暴露。
  • 上限要写进需求。我把枚举上限设成 15 件并且要提示原文,超限时的行为才是确定的。

八、总结

满减凑单本质是一个带阶梯折扣的集合划分问题,贪心在这里是真的会输:12 件输 150 元,14 件输 180 元。

工具的做法是子集枚举加 DP,全程在前端算完,14 件购物车 29.70 毫秒,五个并列最优方案一起列出来。

页面上那 7 条自检每次求解都会重算,其中一条就是「最优解不劣于贪心解」,等于每次都给结果做一次交叉验证。

最后提醒一句:文中的价格和满减档位只是演示用的示例数据,真下单前记得按你自己购物车里实际能叠的券再核一遍。

posted @ 2026-11-04 06:56  新科技观察员  阅读(11)  评论(0)    收藏  举报