G1. Ruler (easy version) - 题解
比赛与标签
比赛: Codeforces Round 964 (Div. 4) 标签: binary search, interactive, *1500 难度: *1500
题目大意喵~
主人你好呀~ 这是一道有趣的交互题哦!
我们有一个神秘的数字 x(它的范围是 2 到 999),它藏在一把有点奇怪的尺子里。这把尺子是这样工作的喵:
- 如果你要测量的长度
y小于x,尺子会正确地显示长度y。 - 如果你要测量的长度
y大于或等于x,尺子就会“犯迷糊”,显示一个错误的长度y + 1。
我们的任务就是找出这个神秘的 x!为了做到这一点,我们可以向系统进行查询。每次查询,我们可以提供一个矩形的长 a 和宽 b(格式是 ? a b)。系统会用这把奇怪的尺子分别测量 a 和 b,得到测量值 m(a) 和 m(b),然后返回它们的乘积 m(a) * m(b) 给我们。
我们需要在最多 10 次查询内,准确地找到 x 的值,然后用 ! x 的格式告诉系统,的说!
思路分析喵~
这道题的查询次数限制是 10 次,而 x 的范围是 [2, 999],差不多是 1000。看到这个组合,聪明的猫娘我呀,第一反应就是——二分查找!因为 log₂(1000) 大约就是 10,正好能对上,喵~
但是,怎么用 ? a b 这种返回乘积的查询来进行二分呢?这就要动动小脑筋啦!
核心思想: 我们的目标是利用查询来判断一个我们猜测的数
k是大于、小于还是等于真正的x。如果能做到这一点,二分查找就可行了。问题的关键在于,查询? a b返回的是m(a) * m(b),是一个乘积,我们很难直接从中分离出关于x的信息。关键观察: 我们来想一想,有没有什么办法能让这个乘法变得简单呢?比如说,让其中一个乘数变成 1? 让我们试试查询
? 1 k会发生什么,呐。- 题目告诉我们,秘密数字
x的范围是2 ≤ x ≤ 999。 - 这意味着,我们查询的边长
1永远都小于x(1 < x)! - 根据尺子的规则,当测量长度小于
x时,结果是准确的。所以,m(1)的值永远都是1。 - 那么,查询
? 1 k返回的结果就是m(1) * m(k) = 1 * m(k) = m(k)。 喵呀!这太棒了!通过查询? 1 k,我们就能直接得到长度k的测量值m(k)!这个发现是解决问题的突破口的说!
- 题目告诉我们,秘密数字
算法流程: 现在我们有了一个强大的工具,可以检查任意
k的测量值,二分查找的流程就非常清晰了:- 我们要在
[2, 999]的范围里寻找x。设我们的搜索区间为[l, r],初始时l = 2,r = 999。 - 在循环中,我们取中间值
mid = l + (r - l) / 2。 - 我们向系统发起查询
? 1 mid,然后读取返回的结果response。 - 现在分析
response:- 如果
response == mid:这说明m(mid)等于mid。根据规则,这只有在mid < x的时候才会发生。既然mid都比x小,那x肯定在mid的右边。所以,我们就可以放心地把搜索区间的左边界更新为l = mid + 1。 - 如果
response == mid + 1:这说明m(mid)等于mid + 1。根据规则,这发生在mid ≥ x的时候。这说明x可能就是mid,或者比mid还要小。所以,我们要把搜索区间的右边界更新为r = mid,来继续在左半边寻找。
- 如果
- 我们不断重复这个过程,
l和r会不断靠近,直到l == r。此时,它们共同指向的值,就是我们苦苦寻找的那个最小的、使得m(k) = k + 1的k,也就是我们的答案x啦! - 最后,我们愉快地输出
! l就大功告成啦,喵~
- 我们要在
代码实现
#include <iostream>
#include <cstdlib>
// 这个函数处理单个测试用例喵~
void solve() {
// 我们要找的秘密数字 x 在 [2, 999] 这个范围里。
// 用二分查找来找到 x 是再好不过的选择了!
int l = 2, r = 999;
// 我们二分的依据是查询 "? 1 k"。
// 因为 x >= 2,所以长度 1 永远小于 x,它的测量值 m(1) 总是 1。
// 那么查询 "? 1 k" 就会返回 m(1) * m(k) = 1 * m(k) = m(k)。
// - 如果 m(k) = k,说明 k < x。
// - 如果 m(k) = k + 1,说明 k >= x。
// "k >= x" 这个条件是单调的:对于 k < x 的值是 false,对于 k >= x 的值是 true。
// 我们要找的就是第一个让这个条件为 true 的 k,也就是 x 本身。
// 这是典型的二分查找问题,寻找 F...FT...T 序列中第一个 T 的位置,的说。
while (l < r) {
int mid = l + (r - l) / 2;
// 发起查询,记得要刷新缓冲区哦!
std::cout << "? 1 " << mid << std::endl;
int response;
std::cin >> response;
if (response == -1) {
// 如果返回 -1,说明查询出错了(比如查询次数超了)。
// 按照题目要求,要立刻退出程序。
exit(0);
}
if (response == mid + 1) {
// 这意味着 m(mid) = mid + 1,所以 mid >= x。
// 答案可能是 mid,也可能在 mid 的左边。
// 所以我们把搜索范围缩小到左半部分 [l, mid]。
r = mid;
} else { // response == mid
// 这意味着 m(mid) = mid,所以 mid < x。
// 答案肯定在 mid 的右边。
// 所以我们把搜索范围缩小到右半部分 [mid + 1, r]。
l = mid + 1;
}
}
// 当循环结束时,l 和 r 相遇了,它们指向的就是最终的答案 x!
std::cout << "! " << l << std::endl;
}
int main() {
// 用这两行代码可以让 C++ 的输入输出快一点,喵~
std::ios_base::sync_with_stdio(false);
std::cin.tie(NULL);
int t;
std::cin >> t;
while (t--) {
solve();
}
return 0;
}复杂度分析
- 时间复杂度: O(log R) 的说。这里的
R是x的可能取值范围大小,大约是 1000。每次二分查询都会将搜索范围减半,所以总的查询次数是对数级别的。对于R=999,log₂(999)约等于 9.96,所以最多 10 次查询就足够了,完全符合题目要求。 - 空间复杂度: O(1) 的说。我们在解决问题时只用了
l,r,mid等几个变量,占用的空间是固定的,不会随着问题规模的增大而增大,所以是常数空间复杂度。
知识点总结
解决这个问题,我们主要用到了这些可爱的知识点,方便主人复习,的说喵~
- 交互式问题 (Interactive Problem): 学会了如何通过标准输入输出与判题系统进行一问一答的交流,并且记得每次输出后都要
flush缓冲区! - 二分查找 (Binary Search): 这是解题的核心算法。特别是这种在单调序列中寻找“分界点”的二分模型,非常经典和常用。
- 问题转化 (Problem Reduction): 将一个看起来复杂的查询(返回乘积),通过巧妙地固定一个参数(
a=1),转化成一个简单的查询(直接返回测量值),这是解题的智慧所在,呐!