1. 题目背景与需求分析
这道P6099 [USACO19FEB] Dishwashing G题目来自美国计算机奥林匹克竞赛(USACO)2019年2月银组比赛。题目描述了一个洗碗场景:有N个盘子需要清洗,每个盘子有一个污垢值d_i。洗碗机每次可以放入一个盘子,当放入新盘子时,如果当前洗碗机中最上面的盘子污垢值比新盘子小,就必须先取出所有这样的盘子才能放入新盘子。我们需要模拟这个过程,并计算总共需要多少次操作。
这类栈模拟问题在信息学竞赛中非常典型,主要考察选手对栈这种数据结构的理解和应用能力。题目中隐含的关键点在于:
- 操作次数的计算规则(每次放入和取出都算一次操作)
- 栈的LIFO(后进先出)特性
- 对题目描述的准确理解和建模能力
2. 解题思路与算法设计
2.1 问题建模
我们可以将洗碗机抽象为一个栈结构,每个盘子的放入和取出对应栈的push和pop操作。根据题目描述,我们需要维护以下规则:
- 每次只能操作最顶端的盘子(栈顶元素)
- 新盘子放入前,必须确保栈顶盘子的污垢值不小于新盘子
- 每次操作(放入或取出)都计入总操作次数
2.2 算法选择
这个问题最适合使用栈来模拟整个过程。具体算法步骤如下:
- 初始化一个空栈和操作计数器(初始为0)
- 按顺序处理每个盘子: a. 检查栈顶元素是否小于当前盘子 b. 如果是,则不断弹出栈顶元素,直到不满足条件或栈为空(每次弹出操作计数器+1) c. 将当前盘子压入栈中(操作计数器+1)
- 最后,将栈中剩余的所有盘子依次弹出(每次弹出操作计数器+1)
- 返回总操作次数
这个算法的时间复杂度是O(N),因为每个盘子最多被压入和弹出各一次。
3. C++代码实现
3.1 基础实现
#include <iostream> #include <stack> using namespace std; int main() { int N; cin >> N; stack<int> dishStack; int operations = 0; for (int i = 0; i < N; ++i) { int d; cin >> d; while (!dishStack.empty() && dishStack.top() < d) { dishStack.pop(); operations++; } dishStack.push(d); operations++; } // 弹出剩余所有盘子 while (!dishStack.empty()) { dishStack.pop(); operations++; } cout << operations << endl; return 0; }3.2 代码优化
我们可以做两个小优化:
- 避免最后单独弹出所有盘子,可以在输入处理时就完成所有操作
- 使用更快的输入输出方式(对于竞赛编程很重要)
优化后的代码:
#include <iostream> #include <stack> using namespace std; int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int N; cin >> N; stack<int> dishStack; int operations = 0; for (int i = 0; i < N; ++i) { int d; cin >> d; while (!dishStack.empty() && dishStack.top() < d) { dishStack.pop(); operations++; } dishStack.push(d); operations++; } operations += dishStack.size(); // 剩余盘子需要弹出 cout << operations << '\n'; return 0; }4. 测试用例与验证
4.1 示例测试
考虑USACO提供的样例输入:
5 3 2 4 1 5处理过程:
- 放入3(操作1)
- 放入2(操作2)
- 放入4:需要先弹出2(操作3),弹出3(操作4),然后放入4(操作5)
- 放入1(操作6)
- 放入5:需要先弹出1(操作7),弹出4(操作8),然后放入5(操作9)
- 弹出5(操作10)
总操作次数:10,与样例输出一致。
4.2 边界测试
需要考虑的特殊情况:
- 只有一个盘子:操作次数应为2(放入+弹出)
- 所有盘子污垢值相同:操作次数为2N(每个盘子放入和弹出各一次)
- 盘子按降序排列:操作次数为N+1(放入所有盘子后一次性弹出)
5. 算法分析与复杂度
5.1 时间复杂度
每个盘子最多被压入和弹出栈各一次,因此时间复杂度是O(N)。这是最优的解法,因为我们必须至少处理每个盘子一次。
5.2 空间复杂度
最坏情况下,所有盘子可能都在栈中(当输入序列是严格递增时),因此空间复杂度也是O(N)。
5.3 算法正确性证明
我们可以用循环不变量来证明算法的正确性:
- 不变量:栈中的盘子污垢值总是非递增的(从栈底到栈顶)
- 初始化:空栈满足这个条件
- 保持:每次放入新盘子前,我们会弹出所有比它小的盘子,因此放入后仍然保持非递增顺序
- 终止:最后栈中盘子按非递增顺序排列,可以依次弹出
6. 竞赛编程技巧
6.1 输入输出优化
在USACO等竞赛中,输入输出可能是性能瓶颈。建议:
- 使用
ios::sync_with_stdio(false);和cin.tie(nullptr);加速C++的输入输出 - 对于大量数据,考虑使用更快的输入方法(如自己实现读取函数)
6.2 调试技巧
这类模拟题容易因为边界条件出错,建议:
- 用小规模数据手工模拟
- 打印中间状态(如每次操作后的栈内容)
- 特别注意空栈的情况(
.top()在空栈上调用会导致运行时错误)
6.3 常见错误
- 忘记统计弹出操作次数
- 没有处理最后栈中剩余的盘子
- 错误理解题目条件(注意是"小于"而不是"小于等于")
- 使用错误的数据结构(如队列或优先队列)
7. 类似题目推荐
掌握了这道题后,可以尝试以下类似题目巩固栈的应用:
- POJ 2559 - Largest Rectangle in Histogram(直方图最大矩形)
- LeetCode 739 - Daily Temperatures(每日温度)
- Codeforces 5C - Longest Regular Bracket Sequence(最长合法括号序列)
- USACO 2019 January Silver - Grass Planting(草地种植)
- LeetCode 84 - Largest Rectangle in Histogram(与POJ 2559相同)
8. 栈的扩展应用
栈在算法竞赛中有广泛应用,主要包括:
- 表达式求值(中缀转后缀)
- 括号匹配问题
- 单调栈(如本题的应用)
- 深度优先搜索(DFS)的非递归实现
- 函数调用栈的模拟
理解栈的LIFO特性是解决这类问题的关键。在实际编程中,STL的stack容器提供了方便的接口,但需要注意:
top()前必须检查栈是否为空pop()不返回栈顶元素(需要先top()再pop())- 栈没有迭代器,不能遍历栈中所有元素
9. 个人实战经验
在解决这类栈模拟问题时,我总结了几个实用技巧:
- 先用小例子手工模拟,确保理解题意
- 在代码中加入调试输出(如打印每次操作后的栈状态)
- 特别注意边界条件:空栈、第一个/最后一个元素、所有元素相同等情况
- 对于操作计数类问题,明确什么算一次操作(有时放入和弹出都算,有时只算一种)
- 在竞赛中,如果时间允许,可以写一个暴力解法作为对拍程序
这道题的一个变种可能是询问某个特定盘子被操作的时间点,或者允许并行操作等。理解基础解法后,可以尝试思考这些扩展问题。