1549 增加饮水机,提供三种解法方案(推荐贪心算法+优先队列)

🏆本文收录于 《全栈Bug调优(实战版)》 专栏,该专栏专注于分享我在真实项目开发中遇到的各类疑难Bug及其深层成因,并系统提供高效、可复现的解决思路和实操方案。无论你是刚入行的新手开发者,还是拥有多年项目经验的资深工程师,本专栏都将为你提供一条系统化、高质量的问题排查与优化路径,助力你加速成长,攻克技术壁垒,迈向技术价值最大化与职业发展的更高峰🚀!
  
📌 特别说明: 文中部分技术问题来源于真实生产环境及网络公开案例,均经过精挑细选与系统化整理,并结合多位一线资深架构师和工程师多年实战经验沉淀,提炼出多种经过验证的高可行性解决方案,供开发者们参考与借鉴。
  
欢迎 关注、收藏并订阅本专栏,持续更新的干货内容将与您同行,让我们携手精进,技术跃迁,步步高升!

📢 问题描述

问题来源:https://ask.csdn.net/questions/xxx

描述:

童程学校原来只有一台饮水机,同学们只能排队打水,消耗了课间时间。学校决定改善设备,增加到了r台饮水机。但是鲁丽还是想知道,学生们按照一个顺序去打水,总的花费时间最少,这样不是更节省时间吗?
现在,有n 个同学排队到 r 台饮水机去打水,他们装满水杯的时间 t1、t2…tn,这些数为整数,应如何安排他们的打水顺序才能使他们总共花费的时间最少?

输入描述:

第一行 n,r (n≤300,r≤100)
第二行为 n 个同学打水所用的时间 ti ( ti≤30)

输出描述:

最少的花费时间

样例输入1

4 2
3 2 1 2

样例输出2

11

提示

数据范围与提示:

n≤300,r≤100,ti≤30

📣 请知悉:如下方案不保证一定适配你的问题!

  如下是针对上述问题进行专业角度剖析答疑,不喜勿喷,仅供参考:

✅️问题理解

这是一个多台饮水机排队优化的贪心算法问题。具体分析如下:

问题核心

  • n个同学需要到r台饮水机打水
  • 每个同学打水时间为ti
  • 目标是最小化所有同学的总完成时间(不是总等待时间)

关键理解

  • 总花费时间 = 每个同学完成打水的时刻之和
  • 饮水机可以并行工作,需要合理分配同学到不同饮水机
  • 这是一个典型的任务调度优化问题

样例分析

4个同学,2台饮水机,时间[3,2,1,2]
最优分配:
饮水机1: 时间1的同学(01) + 时间2的同学(13) = 完成时间[1,3]
饮水机2: 时间2的同学(02) + 时间3的同学(25) = 完成时间[2,5]
总时间 = 1+2+3+5 = 11

✅️问题解决方案

方案一:贪心算法 + 优先队列(推荐)

核心思想

  1. 将同学按打水时间从小到大排序(短任务优先)
  2. 使用小根堆维护r台饮水机的完成时间
  3. 每次将下一个同学分配给最早空闲的饮水机
#include <iostream>
#include <vector>
#include <queue>
#include <algorithm>
using namespace std;

class WaterDispenser {
public:
    static int minimumTotalTime(int n, int r, vector<int>& times) {
        // 贪心策略:短任务优先
        sort(times.begin(), times.end());
        
        // 使用小根堆维护每台饮水机的完成时间
        priority_queue<int, vector<int>, greater<int>> machines;
        
        // 初始化r台饮水机,完成时间都为0
        for (int i = 0; i < r; i++) {
            machines.push(0);
        }
        
        int totalTime = 0;
        
        // 为每个同学分配饮水机
        for (int i = 0; i < n; i++) {
            // 取出最早完成的饮水机
            int earliestFinishTime = machines.top();
            machines.pop();
            
            // 计算该同学的完成时间
            int currentFinishTime = earliestFinishTime + times[i];
            totalTime += currentFinishTime;
            
            // 更新饮水机状态
            machines.push(currentFinishTime);
        }
        
        return totalTime;
    }
};

int main() {
    int n, r;
    cin >> n >> r;
    
    vector<int> times(n);
    for (int i = 0; i < n; i++) {
        cin >> times[i];
    }
    
    cout << WaterDispenser::minimumTotalTime(n, r, times) << endl;
    
    return 0;
}
方案二:动态规划优化版本

适用于数据规模较大的情况

#include <iostream>
#include <vector>
#include <algorithm>
#include <climits>
using namespace std;

class WaterDispenserDP {
public:
    static int minimumTotalTimeDP(int n, int r, vector<int>& times) {
        sort(times.begin(), times.end());
        
        // dp[i][j] = 前i个同学使用j台饮水机的最小总时间
        vector<vector<int>> dp(n + 1, vector<int>(r + 1, INT_MAX));
        
        // 初始化:0个同学使用任意台饮水机的时间为0
        for (int j = 0; j <= r; j++) {
            dp[0][j] = 0;
        }
        
        // 前缀和数组,用于快速计算连续同学使用同一台饮水机的总时间
        vector<int> prefixSum(n + 1, 0);
        vector<int> prefixTimeSum(n + 1, 0);
        
        for (int i = 1; i <= n; i++) {
            prefixSum[i] = prefixSum[i-1] + times[i-1] * i;
            prefixTimeSum[i] = prefixTimeSum[i-1] + times[i-1];
        }
        
        // 填充DP表
        for (int i = 1; i <= n; i++) {
            for (int j = 1; j <= min(i, r); j++) {
                if (j == 1) {
                    // 只有一台饮水机,所有同学排队
                    dp[i][1] = prefixSum[i];
                } else {
                    // 尝试将前k个同学分配给前j-1台饮水机
                    for (int k = j-1; k < i; k++) {
                        if (dp[k][j-1] != INT_MAX) {
                            int lastMachineTime = 0;
                            for (int l = k + 1; l <= i; l++) {
                                lastMachineTime += times[l-1] * (l - k);
                            }
                            dp[i][j] = min(dp[i][j], dp[k][j-1] + lastMachineTime);
                        }
                    }
                }
            }
        }
        
        return dp[n][r];
    }
};
方案三:模拟法(便于理解)

直观的模拟实现,适合调试和理解

#include <iostream>
#include <vector>
#include <algorithm>
#include <set>
using namespace std;

class WaterDispenserSimulation {
private:
    struct Event {
        int time;
        int machineId;
        int studentId;
        
        bool operator<(const Event& other) const {
            if (time != other.time) return time < other.time;
            return machineId < other.machineId;
        }
    };
    
public:
    static int minimumTotalTimeSimulation(int n, int r, vector<int>& times) {
        // 创建(时间,原始索引)对并排序
        vector<pair<int, int>> students;
        for (int i = 0; i < n; i++) {
            students.push_back({times[i], i});
        }
        sort(students.begin(), students.end());
        
        // 事件队列和饮水机状态
        set<Event> events;
        vector<int> machineStatus(r, 0);  // 每台饮水机的下次空闲时间
        vector<int> completionTime(n);   // 每个同学的完成时间
        
        // 为每个同学分配饮水机
        for (int i = 0; i < n; i++) {
            int drinkTime = students[i].first;
            int studentId = students[i].second;
            
            // 找到最早空闲的饮水机
            int bestMachine = 0;
            for (int j = 1; j < r; j++) {
                if (machineStatus[j] < machineStatus[bestMachine]) {
                    bestMachine = j;
                }
            }
            
            // 分配该同学到最早空闲的饮水机
            int startTime = machineStatus[bestMachine];
            int finishTime = startTime + drinkTime;
            
            completionTime[studentId] = finishTime;
            machineStatus[bestMachine] = finishTime;
        }
        
        // 计算总时间
        int totalTime = 0;
        for (int i = 0; i < n; i++) {
            totalTime += completionTime[i];
        }
        
        return totalTime;
    }
};
方案四:优化输入输出版本

针对竞赛环境的高效实现

#include <cstdio>
#include <algorithm>
#include <queue>
using namespace std;

int main() {
    int n, r;
    scanf("%d %d", &n, &r);
    
    int times[300];
    for (int i = 0; i < n; i++) {
        scanf("%d", &times[i]);
    }
    
    // 快速排序
    sort(times, times + n);
    
    // 使用数组模拟小根堆(当r较小时更高效)
    int machines[100];
    for (int i = 0; i < r; i++) {
        machines[i] = 0;
    }
    
    int totalTime = 0;
    
    for (int i = 0; i < n; i++) {
        // 找到最小值的位置
        int minPos = 0;
        for (int j = 1; j < r; j++) {
            if (machines[j] < machines[minPos]) {
                minPos = j;
            }
        }
        
        // 更新该饮水机的完成时间
        machines[minPos] += times[i];
        totalTime += machines[minPos];
    }
    
    printf("%d\n", totalTime);
    
    return 0;
}

✅️问题延伸

1. 算法复杂度分析

时间复杂度

  • 排序:O(n log n)
  • 主循环:O(n log r)(使用优先队列)或O(n × r)(使用数组)
  • 总体:O(n log n + n log r)

空间复杂度:O®

优化技巧

// 当r较小时,直接使用数组比优先队列更快
class OptimizedSolver {
public:
    static int solve(int n, int r, vector<int>& times) {
        sort(times.begin(), times.end());
        
        if (r >= 100) {
            // r较大时使用优先队列
            return solveLargeR(n, r, times);
        } else {
            // r较小时使用数组
            return solveSmallR(n, r, times);
        }
    }
    
private:
    static int solveSmallR(int n, int r, vector<int>& times) {
        vector<int> machines(r, 0);
        int totalTime = 0;
        
        for (int time : times) {
            int minPos = min_element(machines.begin(), machines.end()) - machines.begin();
            machines[minPos] += time;
            totalTime += machines[minPos];
        }
        
        return totalTime;
    }
    
    static int solveLargeR(int n, int r, vector<int>& times) {
        priority_queue<int, vector<int>, greater<int>> pq;
        for (int i = 0; i < r; i++) pq.push(0);
        
        int totalTime = 0;
        for (int time : times) {
            int earliest = pq.top();
            pq.pop();
            pq.push(earliest + time);
            totalTime += earliest + time;
        }
        
        return totalTime;
    }
};
2. 变种问题扩展

问题变种1:最小化最大完成时间

// 目标:最小化所有同学中最晚完成的时间(makespan)
int minimizeMaxCompletionTime(int n, int r, vector<int>& times) {
    sort(times.begin(), times.end());
    vector<int> machines(r, 0);
    
    for (int time : times) {
        int minPos = min_element(machines.begin(), machines.end()) - machines.begin();
        machines[minPos] += time;
    }
    
    return *max_element(machines.begin(), machines.end());
}

问题变种2:考虑移动时间

// 同学在饮水机之间移动需要额外时间
struct Position {
    int x, y;
    int distance(const Position& other) {
        return abs(x - other.x) + abs(y - other.y);
    }
};

int solveWithMovement(int n, int r, vector<int>& times, 
                     vector<Position>& students, 
                     vector<Position>& machines) {
    // 需要考虑分配时的移动成本
    // 这将变成一个更复杂的分配问题
}
3. 多目标优化

Mermaid流程图 - 算法决策流程

r < 100
r >= 100
输入: n个同学, r台饮水机, 时间数组
对时间数组排序
r的大小?
使用数组模拟
使用优先队列
遍历排序后的同学
找到最早空闲的饮水机
分配当前同学到该饮水机
更新饮水机状态
累加完成时间
还有同学?
输出总时间

✅️问题预测

1. 数据规模扩展问题

大数据量优化

// 当n很大时的分块处理
class LargeScaleSolver {
public:
    static long long solveLarge(int n, int r, vector<int>& times) {
        sort(times.begin(), times.end());
        
        // 使用更高效的数据结构
        if (n > 10000) {
            return solveWithSegmentTree(n, r, times);
        } else {
            return solveStandard(n, r, times);
        }
    }
    
private:
    static long long solveWithSegmentTree(int n, int r, vector<int>& times) {
        // 使用线段树维护饮水机状态,支持快速查询和更新
        // 时间复杂度:O(n log r)
        // 实现略...
        return 0;
    }
};
2. 内存限制问题

空间优化版本

// 当内存受限时的优化
int solveMemoryOptimized(int n, int r) {
    // 不存储完整数组,在线处理
    vector<int> machines(r, 0);
    long long totalTime = 0;
    
    // 先读取所有时间并排序
    vector<int> times(n);
    for (int i = 0; i < n; i++) {
        cin >> times[i];
    }
    sort(times.begin(), times.end());
    
    // 在线处理
    for (int time : times) {
        int minPos = 0;
        for (int j = 1; j < r; j++) {
            if (machines[j] < machines[minPos]) {
                minPos = j;
            }
        }
        machines[minPos] += time;
        totalTime += machines[minPos];
    }
    
    return totalTime;
}
3. 实时动态问题

支持动态添加同学

class DynamicWaterDispenser {
private:
    priority_queue<int, vector<int>, greater<int>> machines;
    long long currentTotal;
    
public:
    DynamicWaterDispenser(int r) : currentTotal(0) {
        for (int i = 0; i < r; i++) {
            machines.push(0);
        }
    }
    
    void addStudent(int drinkTime) {
        int earliest = machines.top();
        machines.pop();
        
        int finishTime = earliest + drinkTime;
        currentTotal += finishTime;
        
        machines.push(finishTime);
    }
    
    long long getTotalTime() const {
        return currentTotal;
    }
};

✅️小结

算法核心:使用贪心策略 + 优先队列实现最优任务调度

关键洞察

  1. 短任务优先:将打水时间短的同学优先安排,减少后续同学的等待时间
  2. 负载均衡:始终选择最早空闲的饮水机,保持各饮水机负载相对均衡
  3. 最优子结构:每一步的局部最优选择导致全局最优解

实现要点

  • 排序:O(n log n),为贪心策略做准备
  • 优先队列:O(log r),高效维护饮水机状态
  • 累计求和:计算每个同学的完成时间并累加

性能对比

  • 时间复杂度:O(n log n + n log r)
  • 空间复杂度:O®
  • 适用范围:n ≤ 300, r ≤ 100时性能优异

竞赛技巧

  • 当r较小时,用数组代替优先队列更快
  • 注意数据类型,防止整数溢出
  • 可以预处理排序,在线处理查询

这道题完美展现了贪心算法在任务调度中的应用,通过合理的策略选择实现了最优的时间分配方案。

  希望如上措施及解决方案能够帮到有需要的你。

  PS:如若遇到采纳如下方案还是未解决的同学,希望不要抱怨&&急躁,毕竟影响因素众多,我写出来也是希望能够尽最大努力帮助到同类似问题的小伙伴,即把你未解决或者产生新Bug黏贴在评论区,我们大家一起来努力,一起帮你看看,可以不咯。

  若有对当前Bug有与如下提供的方法不一致,有个不情之请,希望你能把你的新思路或新方法分享到评论区,一起学习,目的就是帮助更多所需要的同学,正所谓「赠人玫瑰,手留余香」。

🧧🧧 文末福利,等你来拿!🧧🧧

  如上问题有的来自我自身项目开发,有的收集网站,有的来自读者…如有侵权,立马删除。再者,针对此专栏中部分问题及其问题的解答思路或步骤等,存在少部分搜集于全网社区及人工智能问答等渠道,若最后实在是没能帮助到你,还望见谅!并非所有的解答都能解决每个人的问题,在此希望屏幕前的你能够给予宝贵的理解,而不是立刻指责或者抱怨!如果你有更优解,那建议你出教程写方案,一同学习!共同进步。

  ok,以上就是我这期的Bug修复内容啦,如果还想查找更多解决方案,你可以看看我专门收集Bug及提供解决方案的专栏《全栈Bug调优(实战版)》,都是实战中碰到的Bug,希望对你有所帮助。到此,咱们下期拜拜。

码字不易,如果这篇文章对你有所帮助,帮忙给 bug菌 来个一键三连(关注、点赞、收藏) ,您的支持就是我坚持写作分享知识点传播技术的最大动力。

同时也推荐大家关注我的硬核公众号:「猿圈奇妙屋」 ;以第一手学习bug菌的首发干货,不仅能学习更多技术硬货,还可白嫖最新BAT大厂面试真题、4000G Pdf技术书籍、万份简历/PPT模板、技术文章Markdown文档等海量资料,你想要的我都有!

🫵 Who am I?

我是bug菌,CSDN | 掘金 | InfoQ | 51CTO | 华为云 | 阿里云 | 腾讯云 等社区博客专家,C站博客之星Top30,华为云多年度十佳博主,掘金多年度人气作者Top40,掘金等各大社区平台签约作者,51CTO年度博主Top12,掘金/InfoQ/51CTO等社区优质创作者;全网粉丝合计 30w+;更多精彩福利点击这里;硬核微信公众号「猿圈奇妙屋」,欢迎你的加入!免费白嫖最新BAT互联网公司面试真题、4000G PDF电子书籍、简历模板等海量资料,你想要的我都有,关键是你不来拿。

-End-

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

bug菌¹

你的鼓励将是我创作的最大动力。

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值