用户有10件待结算商品,平台提供满100减20/200减50/300减80三种优惠券,如何精确计算出所有能触发的优惠券组合?这就是组合总和问题的实战场景!
一、问题场景:电商促销系统困境
业务需求 :
某电商结算页需实现智能优惠推荐,规则如下:
- 可用优惠券:[20, 50, 80](满减门槛值)
- 用户订单金额:200元
- 要求输出所有组合总和=200的优惠券使用方案(券可重复使用)
用户痛点:
- 人工试算效率低下
- 优惠券组合存在多种排列方式
- 系统需在300ms内响应计算结果
二、解决方案:DFS+回溯算法实践
核心思路:
graph TB
A[开始遍历] --> B{当前累计金额=目标值?}
B -->|是| C[记录有效组合]
B -->|否| D{当前金额<目标值?}
D -->|是| E[继续添加优惠券]
D -->|否| F[回溯到上一步]
E --> B
算法实现:
javascript
// 优惠券智能匹配引擎
function couponCombination(coupons, target) {
const results = [];
// 🔍 关键决策1:排序便于剪枝
coupons.sort((a, b) => a - b);
/**
* DFS递归搜索
* @param {number} start - 当前遍历起点(避免重复组合)
* @param {number[]} path - 当前选择路径
* @param {number} sum - 当前路径总和
*/
function dfs(start, path, sum) {
// 🔍 关键决策2:达到目标值终止条件
if (sum === target) {
results.push([...path]); // 拷贝组合方案
return;
}
for (let i = start; i < coupons.length; i++) {
const coupon = coupons[i];
// 🔍 关键决策3:剪枝优化(跳过后续无效分支)
if (sum + coupon > target) break;
// 选择当前优惠券
path.push(coupon);
// 🔍 关键决策4:允许重复使用同一券(i不增加)
dfs(i, path, sum + coupon);
// 回溯撤销选择
path.pop();
}
}
dfs(0, [], 0);
return results;
}
// 实际业务调用
const coupons = [20, 50, 80];
const orderAmount = 200;
console.log(couponCombination(coupons, orderAmount));
/* 输出:[
[20,20,20,20,20,20,20,20,20,20],
[20,20,20,20,20,20,50,50],
[20,20,20,50,50,50],
[50,50,50,50],
[20,20,80,80],
// ...共8种组合
] */
关键代码解析:
- 排序优化 :
coupons.sort()
使小券优先,提前触发剪枝条件 - DFS核心参数 :
start
保证组合有序性(避免[20,50]和[50,20]重复)path
记录当前选择路径sum
动态计算当前金额
- 剪枝条件 :
sum + coupon > target
时终止分支搜索 - 回溯操作 :
path.pop()
撤销当前选择,尝试其他分支
三、原理深度剖析
1. 递归调用栈运作机制
以优惠券[20,50], 目标100为例:
sequenceDiagram
participant Main
participant DFS1 as dfs(0, [], 0)
participant DFS2 as dfs(0, [20], 20)
participant DFS3 as dfs(0, [20,20], 40)
participant DFS4 as dfs(0, [20,20,20], 60)
Main ->> DFS1: 初始调用
DFS1 ->> DFS2: 选择20
DFS2 ->> DFS3: 继续选20
DFS3 ->> DFS4: 继续选20
DFS4 --x DFS3: 60<100 继续
Note over DFS3: 尝试50: 20+20+50=90<100
DFS3 ->> DFS3: 新分支: [20,20,50]
DFS3 --x DFS2: 回溯
DFS2 ->> DFS2: 尝试50: 20+50=70<100
Note over DFS2: 新分支: [20,50]
DFS2 ->> DFS3: 继续选20→[20,50,20]=90
2. 时间复杂度优化策略
优化手段 | 未优化复杂度 | 优化后复杂度 | 原理 |
---|---|---|---|
排序+剪枝 | O(n^n) | O(2^n) | 提前终止无效分支 |
索引传递(start) | 包含排列组合 | 仅保留组合 | 避免顺序不同造成重复 |
尾递归优化 | 堆栈溢出风险 | 安全处理100层 | 现代JS引擎自动优化 |
四、方案对比:DFS vs 动态规划
维度 | DFS+回溯 | 动态规划 |
---|---|---|
适用场景 | 需输出所有具体组合方案 | 仅求方案总数 |
空间消耗 | O(递归深度) | O(target) |
实现难度 | 中等(需理解递归树) | 高(状态转移方程) |
业务案例 | 优惠券组合推荐 | 计算中奖概率 |
五、工业级代码优化技巧
1. 内存优化 - 路径共享技术
javascript
function dfs(start, pathIndex, sum) {
// 共用全局路径池
const path = globalPath[pathIndex];
if (sum === target) {
// 深拷贝仅在此处触发
results.push([...globalPath[pathIndex]]);
return;
}
for (let i = start; ...) {
path.push(coupon);
// 创建新路径节点
globalPath.push([...path]);
dfs(i, globalPath.length-1, sum+coupon);
path.pop();
}
}
2. 多线程分治
javascript
// 主线程拆分任务
const workerPromises = [];
for (let i = 0; i < coupons.length; i++) {
workerPromises.push(
new Worker('./dfs-worker.js')
.postMessage({start: i, path:[coupons[i]], sum:coupons[i]})
);
}
// Worker线程处理子任务
onmessage = function({start, path, sum}) {
// 执行局部分治的DFS
// 将结果postMessage回主线程
}
3. 缓存中间结果
javascript
const memo = new Map();
function dfs(start, sum) {
const key = `${start}_${sum}`;
if (memo.has(key)) return memo.get(key);
// ...计算过程
memo.set(key, results);
return results;
}
六、举一反三:变体场景实战
1. 限量优惠券场景(每券限用1次)
diff
function dfs(start, path, sum) {
// ...
for (let i = start; i < coupons.length; i++) {
- dfs(i, path, sum + coupon); // 旧逻辑
+ dfs(i + 1, path, sum + coupon); // 移动索引避免复用
}
}
2. 组合优先级加权(大额券优先)
javascript
coupons.sort((a, b) => b - a); // 改为降序排序
function dfs(start, path, sum) {
// 新增优先使用条件
if (sum + coupons[0] * (remainLen) < target)
return;
// ...其余逻辑不变
}
3. 多维度约束(金额+品类)
javascript
function dfs(start, path, sum, categorySet) {
// 新增品类校验
if (!isValidCategory(coupon.category, categorySet)) continue;
// 递归时传递更新品类集
dfs(i, path, sum + coupon.value,
new Set([...categorySet, coupon.category]));
}
七、真实业务扩展包
优惠引擎配置片段(支持生产环境):
javascript
// coupon-engine.config.js
module.exports = {
maxDepth: 100, // 最大组合深度
timeout: 300, // 超时时间(ms)
fallbackStrategy: 'nearest' // 无解时策略: nearest/greedy
};
// 使用适配器
const engine = createEngine(
coupons,
orderAmount,
require('./coupon-engine.config')
);
小结
- 资源组合优化:服务器资源调度系统
- 金融组合投资:多标的资产配置工具
- 游戏装备合成:多材料配方计算引擎
在DFS的回溯操作中,恰当的回头不是失败,而是为了找到更多可能------这既是算法智慧,也是工程师面对复杂系统时应有的哲学。