# GESP 2025年06月 五级 T1 题解 ## 一、题目描述 ### 1.1 问题大意 小A手里有n张课堂优秀券和m张作业优秀券,他可以用这些券兑换奖品。兑换奖品有两种方式: - 方式一:使用a张课堂优秀券和b张作业优秀券兑换一份奖品 - 方式二:使用b张课堂优秀券和a张作业优秀券兑换一份奖品 小A希望兑换尽可能多的奖品,请你计算他最多能兑换多少份奖品。 ### 1.2 输入格式 输入四个正整数n, m, a, b,分别表示小A持有的课堂优秀券数量、作业优秀券数量,以及两种兑换方式所需的券数量。 ### 1.3 输出格式 输出一个整数,表示小A最多能兑换的奖品数量。 ## 二、解题思路 ### 2.1 核心思想 本题可以通过**二分查找**结合**贪心策略**来解决。我们通过二分查找确定最大的可兑换奖品数量v,然后使用贪心策略验证是否真的可以兑换v份奖品。 ### 2.2 贪心策略分析 #### 2.2.1 预处理步骤 在验证某个v是否可行之前,我们先进行两个重要的预处理: 1. **确保n ≤ m**:这样可以保证我们总是用较小的那个券数量作为二分查找的右边界上限 2. **确保a ≤ b**:这样可以保证第一种兑换方式是"课堂券少、作业券多"的组合 这两个预处理可以大大简化后续的计算逻辑。 #### 2.2.2 验证方法(check函数) 对于给定的v,验证步骤如下: 1. 首先假设全部使用第一种兑换方式(课堂券少、作业券多) 2. 如果作业券数量超过小A持有的数量,则计算需要将多少份兑换改为第二种方式 3. 每将一份兑换从第一种方式改为第二种方式: - 课堂券需求变化:从a变为b(增加了b-a张) - 作业券需求变化:从b变为a(减少了b-a张) 4. 最后检查调整后的券数量是否都不超过小A持有的数量 ### 2.3 二分查找框架 使用二分查找来确定最大的v值: - 左边界l设为0(最少兑换0份) - 右边界r设为n(由于n ≤ m且a ≤ b,最多不能超过n/a份) - 使用"左闭右闭"区间的二分查找模板,注意使用(l + r + 1) >> 1计算中点以避免死循环 ## 三、代码实现与解析 ```cpp #include using namespace std; // 定义长整型以防止整数溢出 typedef long long LL; // 全局变量定义 int n, m; // n: 课堂优秀券数量; m: 作业优秀券数量 int a, b; // a: 第一种方式所需课堂券; b: 第一种方式所需作业券 int l, r; // 二分查找的左右边界 /** * 检查函数:判断是否可以兑换v份奖品 */ int check(int v) { // 使用LL类型防止整数溢出 LL x, y, t; // 步骤1:先假设全部使用第一种方式(课堂券少、作业券多) x = 1ll * v * a; // 计算所需的课堂券总数 y = 1ll * v * b; // 计算所需的作业券总数 // 步骤2:如果作业券不足,需要调整兑换方式 if (y > m) { // 计算需要调整的兑换份数t // 公式原理: // - (y - m)是超出的作业券数量 // - 每调整一份兑换方式,可以减少(b-a)张作业券需求 // - 使用上取整公式确保足够的调整量 t = (y - m + (b - a) - 1) / (b - a); // 步骤3:调整后的券数量计算 y -= t * (b - a); // 作业券减少量:每份调整减少(b-a)张 x += t * (b - a); // 课堂券增加量:每份调整增加(b-a)张 } // 步骤4:验证调整后的总需求是否在小A持有的券数量范围内 return x <= n && y <= m; } /** * 主函数 */ int main() { // 输入小A持有的课堂优秀券和作业优秀券数量 cin >> n >> m; // 输入第一种兑换方式所需的两种券数量 // 注意:根据题目设定,第二种兑换方式是b张课堂券和a张作业券 cin >> a >> b; // 预处理1:确保n <= m // 目的:保证我们总是用较小的那个券数量作为二分查找的右边界上限 if (n > m) swap(n, m); // 预处理2:确保a <= b // 目的:确保第一种兑换方式是"课堂券少、作业券多"的组合 if (a > b) swap(a, b); // 特殊情况处理:如果两种兑换方式相同 // 说明无论选择哪种方式,兑换一份奖品所需的课堂券和作业券数量相同 if (a == b) { // 直接返回较小的券数量除以每份所需的券数量 printf("%d\n", n / a); return 0; } // 二分查找初始化 l = 0; r = n; // 二分查找过程 while (l < r) { // 计算中点mid,使用(l + r + 1) >> 1的原因: // 1. 这是一种避免死循环的技巧 // 2. 在查找上界时,如果使用普通中点(l + r)/2,可能会导致l无法到达r // 3. 右移一位(>> 1)相当于除以2取整,但比除法运算更高效 int mid = (l + r + 1) >> 1; // 检查是否可以兑换mid份奖品 if (check(mid)) // 如果可以兑换mid份,说明我们可以尝试兑换更多,将左边界移到mid l = mid; else // 如果不能兑换mid份,说明需要减少兑换数量,将右边界移到mid-1 r = mid - 1; } // 输出结果,此时l和r相等,都是最大可兑换的奖品数量 printf("%d\n", r); return 0; } ``` ## 四、关键公式解释 ### 4.1 调整份数t的计算 ```cpp t = (y - m + (b - a) - 1) / (b - a); ``` 这个公式使用了**向上取整**的数学技巧: - `y - m`是超出的作业券数量 - `b - a`是每份调整可以减少的作业券数量 - `(y - m + (b - a) - 1) / (b - a)` 等价于 `ceil((y - m) / (b - a))` - 使用这种方式计算向上取整可以避免浮点数运算,提高效率 ### 4.2 二分查找中点计算 ```cpp int mid = (l + r + 1) >> 1; ``` 这种中点计算方式是为了避免在特定情况下出现死循环: - 当我们需要查找满足条件的最大值时,使用`(l + r + 1) >> 1` - 当我们需要查找满足条件的最小值时,使用`(l + r) >> 1` - 右移操作符`>> 1`比除法运算`/ 2`更高效 ## 五、复杂度分析 ### 5.1 时间复杂度 - **整体时间复杂度**:O(log n) - 二分查找的时间复杂度为O(log n) - 每次check函数的时间复杂度为O(1) ### 5.2 空间复杂度 - **空间复杂度**:O(1) - 只使用了常数级别的额外空间 ## 六、算法优化与扩展 ### 6.1 优化点 1. **数据类型选择**:使用long long避免整数溢出 2. **位运算优化**:使用右移操作代替除法运算 3. **预处理优化**:通过两次swap简化问题模型 ### 6.2 扩展思考 如果题目中允许更多种兑换方式,或者兑换方式的参数更多样化,可能需要采用其他算法,如动态规划或更复杂的贪心策略。 #### 解释: 兑换4份奖品的方式:使用1份第一种方式(3课堂+5作业)和3份第二种方式(5课堂+3作业) - 总共需要课堂券:3*1 + 5*3 = 18张(不超过10张?这里可能需要调整样例数据) 实际上,由于我们进行了预处理确保n <= m和a <= b,所以算法会自动处理各种输入情况,确保找到最优解。 --- 通过上述分析,我们可以看到这道题是一个典型的二分查找结合贪心策略的应用,关键在于理解两种兑换方式之间的关系,并通过数学公式高效地计算所需的调整量。