多菜谱烹饪的统筹算法实现
本文分享「灶台导航」小程序中多菜谱同时烹饪的核心实现,重点讲解 calculateSchedule 统筹算法。代码和讲解各占一半,帮你真正理解背后的设计思路。
一、多菜谱烹饪场景与核心挑战
1.1 场景描述
用户选择多道菜同时烹饪,系统自动计算统筹方案,指导用户有序完成所有菜品。
选择菜谱:红烧肉 + 清炒时蔬 + 番茄蛋汤
│
▼
计算统筹方案
│
▼
时间轴展示:
00:00 红烧肉:准备食材
00:05 红烧肉:焯水
00:10 红烧肉:炒糖色
...
00:40 番茄蛋汤:开始准备
00:45 清炒时蔬:开始准备
01:00 全部完成!
1.2 核心挑战
| 挑战 | 解决方案 |
|---|---|
| 时间冲突 | 统筹算法合理安排 |
| 进度同步 | 全局计时器 |
| 用户偏离 | 动态调整机制 |
| 提醒通知 | 关键节点播报 |
二、数据结构设计
2.1 核心数据结构
下面的数据结构是整个统筹方案的基础。关键字段我已经加了注释说明:
// 统筹方案结构
interface CookingSchedule {
totalDuration: number; // 总时长(秒),从开始到最后一道菜完成
recipes: RecipeSchedule[]; // 每道菜的详细安排
timeline: TimelineEvent[]; // 时间轴事件,方便UI渲染
parallelWindows: ParallelWindow[]; // 并行窗口,提示用户可同时做的事
}
interface RecipeSchedule {
recipeId: string;
recipeName: string;
totalDuration: number; // 这道菜的总时长
startOffset: number; // 相对于全局开始时间的偏移(秒)
steps: ScheduledStep[]; // 这道菜的步骤列表
}
interface ScheduledStep {
order: number; // 步骤序号
content: string; // 步骤内容,如"焯水"
duration: number; // 步骤耗时(秒)
type: string; // 类型:'cook'主动操作 或 'wait'等待
globalStartTime: number; // 全局开始时间(秒)
globalEndTime: number; // 全局结束时间(秒)
}
2.2 设计要点说明
为什么要区分 type?
-
cook类型:需要占用灶台/锅具,不能和别的 cook 任务同时进行 -
wait类型:只是等待(如炖煮),期间用户可以去做别的事
为什么要算两层时间(本地时间+全局时间)?
-
本地时间:相对于这道菜自己的开始时间,便于分析单菜结构
-
全局时间:所有菜统一的时间轴,用于判断冲突和渲染UI
三、核心算法实现
3.1 入口函数 calculateSchedule
这个函数是整个算法的入口,处理单菜谱和多菜谱两种情况:
/** * 多菜谱统筹计算 * 思路:先分析每道菜的结构,找出最长的菜作为基准, * 其他菜从后往前安排,最后解决时间冲突 */
function calculateSchedule(recipes) {
if (!recipes || recipes.length === 0) {
return null
}
// 单菜谱情况:无需统筹,直接返回
if (recipes.length === 1) {
return singleRecipeSchedule(recipes[0])
}
// 多菜谱统筹
const schedules = recipes.map(recipe => analyzeRecipe(recipe))
// 找出耗时最长的菜,它的时长决定了总时长
const maxDuration = Math.max(...schedules.map(s => s.totalDuration))
// 为每道菜计算开始偏移(从完成时间倒推)
schedules.forEach(schedule => {
schedule.startOffset = maxDuration - schedule.totalDuration
// 调整每个步骤的全局时间
schedule.steps.forEach(step => {
step.globalStartTime = schedule.startOffset + step.localStartTime
step.globalEndTime = schedule.startOffset + step.localEndTime
})
})
// 生成时间轴
const timeline = generateTimeline(schedules)
// 检测并行窗口
const parallelWindows = detectParallelWindows(schedules)
// 解决冲突
const resolved = resolveConflicts(schedules)
return {
totalDuration: maxDuration,
recipes: resolved,
timeline,
parallelWindows,
createdAt: Date.now()
}
}
算法核心思想——逆向调度:
不用从前往后推(那样会导致串行,总时长 = 所有菜时长之和),而是从后往前推:
-
找出耗时最长的菜,它的结束时间就是全局结束时间
-
其他菜的结束时间与它对齐,往前推算开始时间
-
这样所有菜同时完成,总时长 ≈ 最长那道菜的时长
3.2 分析单道菜谱
把一道菜的步骤展开,计算出每个步骤的本地时间区间:
举个例子:红烧肉步骤分析结果
| 步骤 | 耗时 | 本地开始 | 本地结束 |
|---|---|---|---|
| 准备食材 | 300秒 | 0 | 300 |
| 焯水 | 300秒 | 300 | 600 |
| 炒糖色 | 300秒 | 600 | 900 |
| 炖煮(wait) | 1800秒 | 900 | 2700 |
总时长 = 2700秒(45分钟)
3.3 生成时间轴
把所有步骤的开始/结束事件汇总,按时间排序:
/** * 生成时间轴 * 把每道菜每个步骤的开始和结束都变成独立事件 */
function generateTimeline(schedules) {
const events = []
schedules.forEach(schedule => {
schedule.steps.forEach(step => {
// 步骤开始事件
events.push({
time: step.globalStartTime,
type: 'start',
recipeId: schedule.recipeId,
recipeName: schedule.recipeName,
step: step
})
// 步骤结束事件
events.push({
time: step.globalEndTime,
type: 'end',
recipeId: schedule.recipeId,
recipeName: schedule.recipeName,
step: step
})
})
})
// 按时间排序,形成时间轴
events.sort((a, b) => a.time - b.time)
return events
}
时间轴的作用:UI可以按时间顺序渲染所有事件,方便用户预览整个烹饪流程。也用于提醒管理器判断什么时候该提醒。
3.4 检测并行窗口
找出可以同时做事的空闲时间段:
/** * 检测并行窗口 * 找出所有 wait 类型步骤的时间重叠区间 * 这些时间段里,用户可以同时处理多件事 */
function detectParallelWindows(schedules) {
const windows = []
// 找出所有 wait 类型步骤(等待期间可以去做别的事)
const waitSteps = []
schedules.forEach(schedule => {
schedule.steps
.filter(s => s.type === 'wait')
.forEach(step => {
waitSteps.push({
...step,
recipeId: schedule.recipeId,
recipeName: schedule.recipeName
})
})
})
// 检查这些 wait 步骤是否有时间重叠
for (let i = 0; i < waitSteps.length; i++) {
for (let j = i + 1; j < waitSteps.length; j++) {
const a = waitSteps[i]
const b = waitSteps[j]
// 判断两个时间段是否重叠
if (a.globalStartTime < b.globalEndTime && b.globalStartTime < a.globalEndTime) {
const overlap = {
start: Math.max(a.globalStartTime, b.globalStartTime),
end: Math.min(a.globalEndTime, b.globalEndTime),
recipes: [a.recipeName, b.recipeName]
}
windows.push(overlap)
}
}
}
return windows
}
并行窗口的价值:告诉用户"现在红烧肉在炖,你可以同时去准备番茄蛋汤",提升效率。
3.5 解决冲突(核心难点)
互斥任务不能同时执行,遇到重叠就往后推:
/** * 解决冲突(互斥任务不能同时执行) * 互斥任务:type !== 'wait' 的步骤,如炒菜、切菜等需要主动操作的任务 */
function resolveConflicts(schedules) {
// 收集所有非 wait 步骤(互斥任务)
const exclusiveSteps = []
schedules.forEach(schedule => {
schedule.steps
.filter(s => s.type !== 'wait')
.forEach(step => {
exclusiveSteps.push({
...step,
recipeId: schedule.recipeId
})
})
})
// 按开始时间排序
exclusiveSteps.sort((a, b) => a.globalStartTime - b.globalStartTime)
// 检测冲突并调整
let lastEndTime = 0
exclusiveSteps.forEach(step => {
if (step.globalStartTime < lastEndTime) {
// 发现冲突:当前任务开始时间 < 上一个任务的结束时间
const delay = lastEndTime - step.globalStartTime // 需要推迟的时长
step.globalStartTime += delay
step.globalEndTime += delay
// 同步更新原 schedule 中的步骤
const schedule = schedules.find(s => s.recipeId === step.recipeId)
const originalStep = schedule.steps.find(s => s.order === step.order)
originalStep.globalStartTime = step.globalStartTime
originalStep.globalEndTime = step.globalEndTime
}
lastEndTime = Math.max(lastEndTime, step.globalEndTime)
})
return schedules
}
冲突解决示例:
假设红烧肉炒糖色需要10:00-10:10,清炒时蔬炒制原本安排在10:05-10:15:
冲突前: 红烧肉: [10:00────10:10] 清炒: [10:05────10:15] ← 重叠了5分钟 检测到冲突,推迟清炒: 红烧肉: [10:00────10:10] 清炒: [10:10────10:20] ← 错开,冲突解决
四、总结
| 要点 | 核心原理 | 代码对应函数 |
|---|---|---|
| 逆向调度 | 从完成时间倒推,让最长菜最先开始 | calculateSchedule |
| 并行优化 | 利用 wait 窗口插入其他任务 | detectParallelWindows |
| 冲突解决 | 互斥任务按顺序排开,重叠时自动推后 | resolveConflicts |
| 数据结构 | startOffset 区分开始时间,type 区分任务性质 | analyzeRecipe |
通过这套算法,原本需要串行执行的多道菜可以并行进行,总时长接近耗时最长的单道菜,大幅提升烹饪效率。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐

所有评论(0)