UVa 10962 Don Giovanni‘s Last Dinner

题目描述

Don Giovanni\texttt{Don Giovanni}Don Giovanni 正在享用大餐,他的仆人 Leporello\texttt{Leporello}Leporello 总是在主人吃完一盘菜后,偷偷吃掉剩下的部分。但是, Leporello\texttt{Leporello}Leporello 必须随时回应主人的召唤。如果主人叫他时他嘴里还塞着食物,那就很尴尬了。

你的任务是编写一个程序,判断每次 Don Giovanni\texttt{Don Giovanni}Don GiovanniLeporello\texttt{Leporello}Leporello 时,他是否正在吃东西。

输入格式

输入的第一行是一个正整数,表示测试用例的数量。每个测试用例之间用一个空行分隔。

每个测试用例的第一行包含四个整数:

  • RRR : Don Giovanni\texttt{Don Giovanni}Don Giovanni 的进食速率(单位/时间)
  • rrr : Leporello\texttt{Leporello}Leporello 的进食速率(单位/时间)
  • NNN : 菜肴的数量
  • CCC : 召唤的次数

接下来有 NNN 行,每行包含两个整数 sssggg ,其中 s>g>0s > g > 0s>g>0

  • sss : 菜肴初始的食物总量
  • ggg : Don Giovanni\texttt{Don Giovanni}Don Giovanni 吃掉的部分

然后有 CCC 行,每行包含一个整数 ttt ,表示召唤的时间点。召唤时间按升序排列。

所有输入的数字都是不超过 200020002000 的正整数。

输出格式

对于每个测试用例,对于每个召唤,输出一行:

  • 如果 Leporello\texttt{Leporello}Leporello 在召唤时间点正在吃东西,输出 full
  • 否则输出 clear

边界情况说明 :如果 Leporello\texttt{Leporello}Leporello 刚好在召唤时间点开始吃或结束吃,也算作 full 。例如,他在时间 202020 收到食物,在时间 404040 吃完,如果在这两个时间点被召唤,都应输出 full 。这也包括他在时间 353535 吃完一盘菜,同时在时间 353535 收到下一盘菜并被召唤的情况。

样例输入

1

10 7 5 4
60 30
150 125
55 35
70 40
42 38
2
10
19
23

样例输出

clear
clear
full
full

题目分析

问题本质

这是一个时间线模拟问题,核心在于准确追踪两个角色( Don Giovanni\texttt{Don Giovanni}Don GiovanniLeporello\texttt{Leporello}Leporello )的进食时间线,并判断在特定时间点 Leporello\texttt{Leporello}Leporello 是否处于进食状态。

关键约束

  1. 顺序性 :两人都按照给定的菜肴顺序进食。
  2. 连续性Don Giovanni\texttt{Don Giovanni}Don Giovanni 吃完一盘菜后立即开始下一盘,而 Leporello\texttt{Leporello}Leporello 在拿到菜后立即开始吃,吃完后等待下一盘。
  3. 时间重叠 :如果 Leporello\texttt{Leporello}Leporello 还在吃上一盘菜时,新的一盘菜已经到来,他需要等吃完当前菜后才能开始吃新菜。
  4. 精确比较 :由于时间可能涉及分数,需要避免浮点数精度问题,使用分数表示时间。

解题思路

步骤 1:理解时间线

我们需要模拟两个并行的过程:

  1. Don Giovanni\texttt{Don Giovanni}Don Giovanni 的时间线 :他按顺序吃每盘菜,吃每盘菜的时间为 gR\frac{g}{R}Rg 。我们可以累加这些时间得到他吃完每盘菜的时刻。

  2. Leporello\texttt{Leporello}Leporello 的时间线 :他在 Don Giovanni\texttt{Don Giovanni}Don Giovanni 吃完一盘菜后立即拿到剩余食物 s−gs - gsg ,然后以速率 rrr 开始吃,需要时间 s−gr\frac{s-g}{r}rsg 。但这里有一个关键点:如果 Leporello\texttt{Leporello}Leporello 还在吃上一盘菜时新菜已到,他需要等吃完当前菜后才能开始吃新菜。

步骤 2:模拟 Leporello\texttt{Leporello}Leporello 的进食过程

我们可以将问题转化为区间合并问题

  1. 对于每盘菜,计算 Leporello\texttt{Leporello}Leporello 可以开始吃的时间(即 Don Giovanni\texttt{Don Giovanni}Don Giovanni 吃完这盘菜的时间,但如果 Leporello\texttt{Leporello}Leporello 还在吃上一盘菜,则需要等到上一盘菜吃完)。
  2. 计算吃完这盘菜的时间:开始时间 + s−gr\frac{s-g}{r}rsg
  3. 将这段时间表示为区间 [start,end][start, end][start,end]
  4. 如果新区间的开始时间等于上一个区间的结束时间,则合并这两个区间(因为 Leporello\texttt{Leporello}Leporello 连续进食)。
步骤 3:处理召唤

对于每个召唤时间 ttt ,检查 ttt 是否在任意一个进食区间内(包含端点)。如果在,则输出 full ,否则输出 clear

步骤 4:处理精度问题

由于时间可能不是整数,我们需要使用分数来精确表示所有时间,避免浮点精度误差。对于分数,我们需要实现:

  • 分数的加法和比较运算
  • 分数的简化(约分)

算法设计

数据结构

我们定义一个 Fraction 结构体来表示分数:

  • n : 分子
  • d : 分母

支持的操作:

  • 构造时自动约分
  • 加法: ab+cd=ad+bcbd\frac{a}{b} + \frac{c}{d} = \frac{ad + bc}{bd}ba+dc=bdad+bc
  • 比较:通过交叉相乘比较大小

算法流程

  1. 读取输入,处理多个测试用例。
  2. 对于每个测试用例:
    • 读取 R,r,N,CR, r, N, CR,r,N,C
    • 初始化 donTime = 0 表示 Don Giovanni\texttt{Don Giovanni}Don Giovanni 的当前时间。
    • 创建一个数组 events ,存储每盘菜的信息: (donGiovanniFinishTime, remainingFood)
  3. 计算 events
    • 对于每盘菜 (s,g)(s, g)(s,g)
      • 计算 Don Giovanni\texttt{Don Giovanni}Don Giovanni 的进食时间: gR\frac{g}{R}Rg
      • 更新 donTime = donTime + g/R
      • 记录事件:在时间 donTime ,有 s-g 的食物留给 Leporello\texttt{Leporello}Leporello
  4. 模拟 Leporello\texttt{Leporello}Leporello 的进食时间线
    • 初始化一个空数组 intervals 存储进食区间。
    • 遍历每个事件 (time, food)
      • 计算开始时间:如果 intervals 为空,则开始时间为 time ;否则为 max(intervals.last().end, time)
      • 计算结束时间:开始时间 + foodr\frac{food}{r}rfood
      • 如果 intervals 为空或新区间的开始时间 > 上一个区间的结束时间,则添加新区间。
      • 否则,合并区间:将上一个区间的结束时间更新为当前结束时间。
  5. 处理召唤
    • 对于每个召唤时间 ttt ,检查 ttt 是否在任意一个进食区间内(包含端点)。
    • 输出 fullclear

时间复杂度

  • 对于每个测试用例:
    • 计算 events : O(N)O(N)O(N)
    • 模拟进食时间线 : O(N)O(N)O(N)
    • 处理召唤 : O(C×M)O(C \times M)O(C×M) ,其中 MMM 是区间数量(最坏情况下 M=NM = NM=N ,但通常会更少)
  • 总复杂度: O(N+C×N)O(N + C \times N)O(N+C×N) ,在题目限制下( N,C≤2000N, C \leq 2000N,C2000 )可以接受。

空间复杂度

  • 存储 events : O(N)O(N)O(N)
  • 存储 intervals : O(N)O(N)O(N)
  • 总空间复杂度: O(N)O(N)O(N)

代码实现

// Don Giovanni's Last Dinner
// UVa ID: 10962
// Verdict: Accepted
// Submission Date: 2026-01-18
// UVa Run Time: 0.000s
//
// 版权所有(C)2026,邱秋。metaphysis # yeah dot net

#include <bits/stdc++.h>
using namespace std;

// 分数结构体
struct Fraction {
    long long n, d;  // 分子,分母
    Fraction() : n(0), d(1) {}
    Fraction(long long n, long long d = 1) : n(n), d(d) { simplify(); }
    // 简化分数(约分)
    void simplify() {
        if (d < 0) n = -n, d = -d;
        long long g = __gcd(abs(n), d);
        if (g) n /= g, d /= g;
    }
    // 分数加法
    Fraction operator+(const Fraction& other) const {
        return Fraction(n * other.d + other.n * d, d * other.d);
    }
    // 比较运算符
    bool operator<(const Fraction& other) const { return n * other.d < other.n * d; }
    bool operator<=(const Fraction& other) const { return n * other.d <= other.n * d; }
    bool operator==(const Fraction& other) const { return n * other.d == other.n * d; }
};

int main() {
    int T;
    cin >> T;
    while (T--) {
        int R, r, N, C;
        cin >> R >> r >> N >> C;
        Fraction donTime(0, 1);
        vector<pair<Fraction, int>> events;
        // 读取每盘菜的信息,计算 Don Giovanni 吃完每盘菜的时间
        for (int i = 0; i < N; i++) {
            int s, g;
            cin >> s >> g;
            donTime = donTime + Fraction(g, R);
            events.push_back({donTime, s - g});
        }
        // 模拟 Leporello 的进食过程,合并连续进食区间
        vector<pair<Fraction, Fraction>> intervals;
        for (auto& evt : events) {
            // 计算开始吃这盘菜的时间
            Fraction startTime = intervals.empty() ? evt.first : intervals.back().second;
            if (startTime < evt.first) startTime = evt.first;
            // 计算吃完这盘菜的时间
            Fraction endTime = startTime + Fraction(evt.second, r);
            // 判断是否需要合并区间
            if (intervals.empty() || intervals.back().second < startTime)
                intervals.push_back({startTime, endTime});
            else
                intervals.back().second = endTime;
        }
        // 处理每个召唤
        for (int i = 0; i < C; i++) {
            int t;
            cin >> t;
            Fraction callTime(t, 1);
            bool full = false;
            // 检查召唤时间是否在任意一个进食区间内
            for (auto& seg : intervals)
                if (seg.first <= callTime && callTime <= seg.second) {
                    full = true;
                    break;
                }
            cout << (full ? "full" : "clear") << '\n';
        }
        // 测试用例之间输出空行
        if (T) cout << '\n';
    }
    return 0;
}

总结

本题的关键在于:

  1. 理解时间线的并行性Don Giovanni\texttt{Don Giovanni}Don GiovanniLeporello\texttt{Leporello}Leporello 的进食过程需要同时追踪。
  2. 正确处理等待情况 :当 Leporello\texttt{Leporello}Leporello 还在吃上一盘菜时,新菜需要等待。
  3. 使用分数避免精度问题 :这是解决本题的核心技巧,确保边界情况的正确性。
  4. 区间合并 :将连续的进食时间合并为单个区间,简化判断逻辑。

通过将问题分解为 Don Giovanni\texttt{Don Giovanni}Don Giovanni 的时间线计算、 Leporello\texttt{Leporello}Leporello 的进食区间合并和召唤时间判断三个步骤,我们可以清晰地解决这个看似复杂的时间模拟问题。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值