交互题

参考资料

函数式交互题的本地测试方式
交互题本机评测器
关于如何在Windows下测交互题
自制题目、SPJ、交互等 详细教程(含举例)
交互题使用指南

[学习笔记] 这就是交互题
交互题入门与常见模型

交互杂题选做

知识

分为 IO交互题 和 函数式交互题,一般 OI 比赛都是函数式交互题。

IO 交互题

一般来说要求你实现一个完整程序,与交互库实现交互,一定要记得输出完后要刷新缓冲区,比赛时需要写 freopen。

  • 如果是 cin/cout,输出时需要 cout<<endl
  • 如果是 printf,输出后要记得 fflush(stdout)

函数式交互题

对于函数式交互题,你需要在你的 code.cpp 实现一个它要求的函数,不需要刷新缓冲区,比赛时不需要写freopen。

注意事项:

  1. 要在你的 code.cpp 中加入 include "game.h",这个 game.h 是提供的交互库。
  2. 提供的还有一份 cpp 文件,我们想在本地运行时可以在提供的 'cpp' 文件里面加入,注意要写双引号:
#include "game.h"
#include "code.cpp"

trick

  1. 通常可以通过次数限制猜测大概的做法,比如说带 \(\log\) 的一般考虑使用二分、分治;而 \(t\times n\) 的一般其实我们用增量或者分组。

  2. 无论在哪里,分类讨论思想永远是非常重要的。

  3. 与构造类似地,在⼀些操作次数量级明显低于总操作次数的题中,我们可以试图固定些参数。例如集合可以固定为区间、某个参数固定为常数等。不过目前我没有太多的见到这类题,所以理解不是很深刻,纯粹般课件上的。

  4. 自适应题一般卡随机化做法,所以不自适应可以考虑考虑随机化。

  5. 找一些特殊情况,比如在分治二分最后时候的情况,可以想想能否省掉一些询问次数。

  6. 均摊。

题目

CF1764G3 Doremy's Perfect DS Class (Hard Version) \(\checkmark\)

题目大意

交互题,有一个 \(1 \sim n\) 的排列 \(a\),可以调用 \(Q(l, r, k)\),返回

\[\left\lfloor \frac{a_l}{k} \right\rfloor, \left\lfloor \frac{a_{l+1}}{k} \right\rfloor, \dots, \left\lfloor \frac{a_r}{k} \right\rfloor \]

中数的种类数,求出 1 的位置。
\(3 \leq n \leq 1024\),调用次数限制有三档部分分9:G1: 30,G2: 25,G3: 20。

解题思路

我们考虑什么样的 \(k\) 是有用的,首先容易发现 \(k=1\) 毫无用处,\(k=n\) 可以确定该区间是否有 \(n\) ,\(k=2\) 是非常优秀的,这样可把所有数两两一组(除了 偶数的 \(n\) 和 1 除外)。
我们可以通过他返回的不同的数的个数,推出他有多少个落单的数。

当 \(n\) 为奇数时:
分成 \([1,mid]\) 和 \([mid,n]\),询问,看看那边多就往哪边走,如果两边一样就是 \(mid\),这样递归下去。

当 \(n\) 为偶数时:
1 和 n 都各成一种,分成 \([1,mid]\) 和 \([mid+1,n]\)(注意这里真的不是询问 \([1,n]\)),询问,得到两边的落单情况,分两种情况:

  • 左边 = 右边,说明左右各有一个 1/n,再令 \(k=n\) 询问其中一边,便可知道 1 在哪边,然后走下去。
  • \(|左边 - 右边| = 1\),说明 1 和 \(n\) 在其中一边。

上述最多是 \(2\log n+1=21\) 次。

发现 左边 \(=\) 右边 的判定较难优化,顶满次数的贡献是最后 \(r-l+1=2\) 的最后情况决定的,因此考虑优化 \(r - l + 1 = 2\) 的情况,如果能一次完成就可以了,花费次数就可以变成 20 次。

如果剩下的两个数是 \(1,n\),那么花费一次查询 \(n\) 即可。所以比较难处理的是 \(1\) 和其他数。
考虑记忆化 \(a_p = Q(1, p, 2)\) 和 \(b_p = Q(p, n, 2)\),则处理到 \([l, r]\) 时
\(a_r, a_{l-1}, b_l, b_{r+1}\) 都已确定(预处理 \(a_n = b_1 = \lfloor \frac{n}{2} \rfloor + 1,a_0=b_{n+1}=0\))。现在令 \(A=a_r-a_{l-1},B=a_l-a_{r+1}\),1 对 \((A,B)\) 的贡献就是 \((1,1)\),而其他数 \(x\) 的贡献是 \((1,0)\) 或者 \((0,1)\),所以 \((A,B)\) 如果是 \((2,1)\) 说明 \(x\) 能对左边贡献,否则就是对右边贡献,然后根据对左边还是右边进行一次询问即可。注意这里的 \(a_p\) 是组数,不是落单的个数。

启发:

  1. 首先要想到有哪些 \(k\) 是有用的,哪些是没有用的,这样就可以大大简化问题。

  2. 分类讨论思想无论在哪都非常有用!

  3. 看到这个带上 \(\log\) 的操作次数,联想到二分/分治这些,这道题用分治更好做一些。

P17121 [ICPC 2025 Shanghai R] Menji, we miss you!

题目大意

给定一棵 \(n\) 个点的二叉树,根为 1,边的长度均为 1。有一个隐藏节点编号为 \(x\),你不知道 \(x\) 是什么。每次询问可给出 \(u, k\),若 \(\text{dis}(u, x)\) 不超过 \(k\) 则返回 1,否则返回 0。

在不超过 40 次操作内求出 \(x\)。

\(T \leq 100, 2 \leq n \leq 3 \times 10^4\)。

解题思路

法一:

首先一个基本的想法是话 \(\log\) 次求出 \(x\) 在以 1 为跟的子树中的深度。
接下来可能会有很多候选点,我们考虑不断缩小候选范围,注意到这是一颗二叉树,任意一个点作为根那他最多只有 3 个儿子,我们每次找到一个点 \(u\) 询问,判断 \(x\) 是否在 \(u\) 的子树内,这样每次可以叉掉父亲上方或者其中一颗子树。由于最多只有三个儿子,所以我们每次找到重心,最差情况下也是删掉 \(1\over 3\),所以总询问次数是 \(\log_2 n+\log_{2\over 3} n\le 40\),根本卡不满,足够通过。

值得一提的是,这里的重心并不是传统意义上的重心,如果直接找重心可能会导致找到的节点没有父亲,无法比较好的判断删掉哪个?有以下解决方法,加入重儿子的判断,最后拿来询问的其实是重儿子,另外一个方法是将两个儿子合在一起,与父亲做。

法二:

我们不需要求出 \(x\) 的深度,每次直接缩小候选点集 \(S\) 的范围,每次从 \(S\) 中随机选出一个数 \(u\),将其作为根做一遍 \(dfs\),找出距离的中位数,询问一下,叉掉一些答案,一直做下去,直到找到答案,不会分析,\(O(能过)\),实测连 20 次都不用。

启发:

  1. 很常用的一个 trick,无法直接确定答案,不妨不断缩小答案范围。

QOJ9432. 排列

题目大意

交互库有一个长为 \(n\) 的排列,每次可以询问一个值域为 \([1, n]\) 的序列,交互库会给出有多少个位置和隐藏的排列相同。请在 6666 次询问内还原交互库的排列。交互库不自适应。

\(1 \le n \le 10^3\)。

解题思路

注意到这个询问次数大概是 \(\log n\) 次左右的,我们往二分/分治方面去想,这里提供分治做法。

分治递归每次传三个参数 \(l,r,S\),左端点、右端点、可选的数的集合,每次从集合中选出两个数 \(x,y\),分别填到 \([l,mid]\) 和 \([mid+1,r]\),如果返回值为 0/2,那么可以确定这两个数具体填哪边;否则如果返回值为 1,先把这两个数捆在一起,然后继续往下看 \(y,z\),如果确定了 \(y\) 也就确定了 \(x\),然后往下分治下去。

分析次数,有 \(1\over 2\) 的概率返回值为 0/2,那么可以剩下可选数集合大小-2,否则-1,期望每次减少 \(3\over 2\),期望操作次数就是 \({2\over 3}n\) 次,总操作次数是 \({2\over 3}n\log n\),足够。

CF1364E X-OR

题目大意

交互库有一个隐藏的长为 \(n\) 的排列 \(p\),其元素为 \(0 \sim n - 1\) 的整数。每次你可以选择两个位置 \(i, j(i \neq j)\) 进行询问,交互库会给出按位或 \(p_i | p_j\)。
最多询问 4269 次,要求得到这个排列。交互库非自适应,\(3 \leq n \leq 2048\)。

解题思路

以下做法来自课件。

题目的做法比较多,这里介绍一个容易用形式化方法想到的做法。

首先我们必须关注到 0 是最重要的元素,因为我们知道 0 的位置后只需要 \(n - 1\) 次即可还原整个排列。

现在我们希望用 \(n + C\) 次询问得到 0 的位置,其中 \(C\) 约为 175。

允许我们用 \(n\) 次询问,首先考虑增量,每次拓展一个位置。发现我们难以直接确定 0 在哪里,但我们可以尽量缩小 0 的可能范围。

我们发现可以在增量的过程中将 0 限制在两个位置中。具体地,假设我们现在已知 0 在位置 \(x, y\) 中并且已知 \(a = p_x | p_y\)。那么现在加入位置 \(i\) 并询问 \(b = p_y | p_i\)。

  • 若 \(a > b\),则 \(p_x \neq 0\),否则有 \(p_y > (p_y | p_i)\) 矛盾。令 \(x', y', a \leftarrow y, i, b\) 即可。
  • 若 \(a < b\),则 \(p_i \neq 0\),与上面同理。令 \(x', y', a \leftarrow x, y, a\) 即可。
  • 若 \(a = b\) 则 \(p_y \neq 0\),否则有 \(p_x = p_i\) 矛盾。但这里会有个问题,就是上面两种情况我们都已知 \(p_{x'} | p_{y'}\),但是这里我们还需要再询问 \(p_x | p_i\),这样就要多一次询问了。

但实际
上,上面第三种情况是非常少的。

由于交互库是非自适应的,所以我们可以预先随机打乱增量顺序,然后在期望意义下计算期望次数。

我们可以按上界估计一下,不妨简化为整个排列中有多少相邻三个位置,满足
\(p_{i-1}|p_i = p_i|p_{i+1}\)。我们可以枚举所有 \(p_i\) 的可能然后算一下期望,限制大概就是
\(p_{i-1}, p_{i+1}\) 在 \(p_i\) 的 0 位完全相同。计算出这个界大概是 85 次左右,但其实实际数值应该远比这个小(一方面,并不是相邻三个位置。另一方面,1 比较多的数更难以作为中间位置)。

接下来还有一个问题,就是如何从两个位置 \(x, y\) 中找到 0 的准确位置。这是容易的,因为其至少有 1 位不同。我们只要不断随机直到某个 \(i\) 满足 \(p_x | p_i \neq p_y | p_i\) 即可。由于只要 \(p_i\) 在此位不同就一定能判断 0 的准确位置。此部分可以按照 \(2 \times 2\) 作为估计。

至此,按照前面的上界估计大约总共需要 \(n - 1 + 85 + 4 + n - 1 \approx 4183\) 次,可以通过。

但实际上,精细实现以上算法(记忆化已经询问过的数对等)在 codeforces 上最多询问可以做到 \(2n\) 以内。

posted @ 2026-09-22 21:26  lcx_OIer  阅读(27)  评论(0)    收藏  举报