灶台导航 (六):时间统筹算法——让多道菜同时上桌

发布时间:2026/6/17 1:01:11

灶台导航 (六):时间统筹算法——让多道菜同时上桌 大家好这是我们“灶台导航”项目专栏的第四篇。在分享了数据库设计和 RAG 检索方案后今天想和大家聊一个非常“接地气”但算法含量不低的问题如何让多道菜同时上桌你是否也有过这样的经历想给家人做一顿丰盛的晚餐三菜一汤结果忙活了两个小时最后一道菜出锅时第一道菜已经凉透了。今天要分享的时间统筹算法就是为了解决这个厨房里的“经典难题”。一、问题场景多菜烹饪的挑战1.1 一个典型的困境假设我们要同时做 3 道菜红烧肉总耗时 60 分钟其中大量时间是炖煮等待清炒时蔬总耗时 15 分钟番茄蛋汤总耗时 20 分钟如果按顺序执行做完一道再做下一道结果是这样的错误安排顺序执行红烧肉 0-60分钟 → 时蔬 60-75分钟 → 蛋汤 75-95分钟结果最后一道菜出锅时第一道已经凉了而正确的安排应该是这样的红烧肉 0-60分钟主要等待时间在炖煮 时蔬 45-60分钟最后15分钟开始 蛋汤 40-60分钟提前20分钟开始 结果三道菜同时出锅上桌1.2 算法目标我们的时间统筹算法要实现以下四个核心目标目标说明同时完成所有菜品尽可能在同一时间上桌最少等待减少用户空闲等待时间清晰指引明确每个时间点该做什么操作容错处理支持烹饪过程中的暂停和调整二、逆向调度思想2.1 核心原理这个算法的核心思想很简单从终点倒推起点。我们不是从“现在开始做”往后推而是从“目标完成时间”往前倒推每一步应该什么时候开始。目标完成时间12:00午餐时间红烧肉总时长60分钟→ 11:00 开始番茄蛋汤总时长20分钟→ 11:40 开始清炒时蔬总时长15分钟→ 11:45 开始这种“逆向调度”的思路确保了所有菜品能在同一时间点完成而不是一个接一个地出锅。2.2 步骤类型分析不同类型的烹饪步骤在调度中的策略完全不同步骤类型特点调度策略prepare准备需要专注操作如切菜、腌制优先安排不能并行cook烹饪需要专注操作如翻炒、调味顺序执行不能并行wait等待无需操作如炖煮、腌制入味可并行穿插安排其他任务识别出“等待”步骤是算法的关键。正是这些等待窗口为我们提供了处理其他菜品的“时间缝隙”。2.3 并行窗口识别// 步骤示例 const steps [ { type: prepare, duration: 5 }, // 准备工作 { type: cook, duration: 10 }, // 烹饪操作 { type: wait, duration: 45 }, // 等待可并行 { type: cook, duration: 5 } // 最后收尾 ] // wait 步骤就是并行窗口 // 在这个窗口内用户可以处理其他菜品的步骤三、算法实现3.1 数据结构首先定义清晰的数据结构// 菜谱步骤结构 interface RecipeStep { order: number; // 步骤序号 content: string; // 步骤内容 duration: number; // 时长秒 type: prepare | cook | wait; // 步骤类型 } // 调度任务结构 interface ScheduledTask { recipeId: string; recipeName: string; step: RecipeStep; startTime: number; // 相对于计划开始时间的偏移秒 endTime: number; parallel: boolean; // 是否可并行 }3.2 核心算法/** * 多菜谱时间统筹算法 */ function calculateSchedule(recipes) { if (!recipes || recipes.length 0) { return { tasks: [], totalTime: 0 } } // 单菜谱直接返回 if (recipes.length 1) { return singleRecipeSchedule(recipes[0]) } // 多菜谱统筹计算 return multiRecipeSchedule(recipes) } /** * 多菜谱统筹调度 */ function multiRecipeSchedule(recipes) { // 1. 分析每道菜的步骤和可并行窗口 const recipeAnalysis recipes.map(recipe ({ recipe, totalDuration: recipe.steps.reduce((sum, s) sum s.duration, 0), waitWindows: findWaitWindows(recipe.steps) })) // 2. 找到最长的菜谱决定总时长 const longestRecipe recipeAnalysis.reduce((max, r) r.totalDuration max.totalDuration ? r : max ) const totalDuration longestRecipe.totalDuration // 3. 从完成时间倒推安排每道菜 const allTasks [] for (const analysis of recipeAnalysis) { const { recipe, totalDuration: duration } analysis const startOffset totalDuration - duration // 倒推开始时间 let currentTime startOffset for (const step of recipe.steps) { const task { recipeId: recipe._id, recipeName: recipe.name, step: step, startTime: currentTime, endTime: currentTime step.duration, parallel: step.type wait } allTasks.push(task) currentTime step.duration } } // 4. 检测冲突并调整 const adjustedTasks resolveConflicts(allTasks) return { tasks: adjustedTasks, totalDuration, recipeCount: recipes.length } } /** * 解决任务冲突 */ function resolveConflicts(tasks) { const exclusiveTasks tasks.filter(t !t.parallel) const parallelTasks tasks.filter(t t.parallel) // 互斥任务prepare/cook不能重叠 const resolved [] let lastExclusiveEnd 0 for (const task of exclusiveTasks) { if (task.startTime lastExclusiveEnd) { // 有冲突调整开始时间 const delay lastExclusiveEnd - task.startTime task.startTime delay task.endTime delay } resolved.push(task) lastExclusiveEnd Math.max(lastExclusiveEnd, task.endTime) } // 并行任务wait可以重叠 resolved.push(...parallelTasks) // 重新排序 return resolved.sort((a, b) a.startTime - b.startTime) }3.3 时间轴生成/** * 生成时间轴展示数据 */ function generateTimeline(schedule) { const { tasks, totalDuration } schedule const timeline [] // 按时间点分组 const timePoints new Set() tasks.forEach(t { timePoints.add(t.startTime) timePoints.add(t.endTime) }) const sortedTimes [...timePoints].sort((a, b) a - b) for (const time of sortedTimes) { const startingTasks tasks.filter(t t.startTime time) const endingTasks tasks.filter(t t.endTime time) if (startingTasks.length 0 || endingTasks.length 0) { timeline.push({ time, timeFormatted: formatTime(time), starting: startingTasks.map(t ({ recipeName: t.recipeName, content: t.step.content, duration: t.step.duration })), ending: endingTasks.map(t ({ recipeName: t.recipeName, step: t.step.order })), active: tasks.filter(t t.startTime time t.endTime time) }) } } return timeline } /** * 格式化时间 */ function formatTime(seconds) { const minutes Math.floor(seconds / 60) const secs seconds % 60 if (secs 0) { return ${minutes}分钟 } return ${minutes}分${secs}秒 }四、云函数实现// cloudfunctions/cookSchedule/index.js const cloud require(wx-server-sdk) cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }) const db cloud.database() exports.main async (event, context) { const { action, recipeIds } event if (action calculate) { return await calculateMultiCookSchedule(recipeIds) } return { errCode: 400, errMsg: 未知操作 } } /** * 计算多菜谱统筹方案 */ async function calculateMultiCookSchedule(recipeIds) { if (!recipeIds || recipeIds.length 0) { return { errCode: 400, errMsg: 请选择菜谱 } } // 获取所有菜谱详情 const recipes [] for (const id of recipeIds) { const res await db.collection(recipes).doc(id).get() if (res.data) { recipes.push(res.data) } } // 计算统筹方案 const schedule calculateSchedule(recipes) return { errCode: 0, data: { schedule, summary: { totalDuration: schedule.totalDuration, recipeCount: recipes.length, parallelTime: calculateParallelTime(schedule.tasks) } } } }五、实时调整5.1 动态调整类/** * 实际烹饪中动态调整 */ class CookingScheduler { constructor(schedule) { this.originalSchedule schedule this.currentTaskIndex 0 this.startTime null this.adjustments [] } start() { this.startTime Date.now() } /** * 获取当前应执行的任务 */ getCurrentTasks() { const elapsed (Date.now() - this.startTime) / 1000 return this.originalSchedule.tasks.filter(task { return task.startTime elapsed task.endTime elapsed }) } /** * 暂停记录调整 */ pause() { const elapsed (Date.now() - this.startTime) / 1000 this.adjustments.push({ type: pause, time: elapsed }) } /** * 继续调整时间 */ resume() { const pauseDuration (Date.now() - this.pauseTime) / 1000 // 调整后续任务时间 this.originalSchedule.tasks.forEach(task { if (task.startTime this.elapsedBeforePause) { task.startTime pauseDuration task.endTime pauseDuration } }) } /** * 跳过某步骤 */ skipStep(recipeId, stepOrder) { // 重新计算后续任务 // ... } }5,2 容错处理在真实烹饪场景中时间不可能完全精准。因此我们加入了暂停/继续用户需要临时离开时可以暂停计时步骤跳过如果某个步骤提前完成可以手动触发下一步时间偏移所有后续任务会自动重新计算保持计划同步六、踩坑记录问题1等待窗口识别不准现象某些步骤明明不需要操作但没有被识别为wait类型导致失去了并行机会。解决在菜谱数据结构中明确标注type字段并在录入数据时严格区分。同时对于“炖煮XX分钟”这类文本也可以辅助用正则表达式自动识别。问题2冲突解决导致总时间延长现象当多道菜的prepare步骤同时开始时冲突解决会让某些任务后移导致总时间超出预期。解决改进算法——让总时间最长的菜谱优先安排其他菜谱的prepare步骤在其等待窗口内穿插。同时增加了一个优化策略如果某道菜的prepare步骤可以提前完成不影响后续等待就尽量提前。问题3时间轴展示信息过载现象当菜品较多时5道以上时间轴上的信息密密麻麻用户难以阅读。解决引入“折叠”机制——默认只显示当前时间点前后10分钟的任务用户可以通过滚动或点击展开查看更多。七、总结时间统筹算法的核心要点可以总结为以下五点要点说明逆向调度从完成时间倒推开始时间确保同时出锅步骤分类区分 prepare/cook/wait不同类型不同策略并行识别wait 步骤是并行窗口用于穿插其他菜品的任务冲突解决互斥任务不能重叠需要排队执行动态调整支持暂停、跳过、时间偏移适应真实场景通过合理的时间统筹可以让多道菜同时上桌大幅提升烹饪效率和用餐体验。这个算法不仅适用于厨房也适用于任何需要“多任务并行 最终同步完成”的场景。

相关新闻