交互题入门与常见模型
普通题把完整输入一次性交给程序,交互题却只给出一部分信息。我们需要主动发出询问,评测机根据询问返回结果,再用这些结果决定下一步问什么。
交互题的代码通常不长,难点主要有两个:怎样让一次询问提供足够多的信息,以及怎样证明最坏情况下不会超过询问次数。二分、奇偶性、异或、前缀差分和状态计数,都是设计询问时常见的工具。
这一篇先整理交互协议与代码写法,再通过六道例题说明几种常见模型。正文不依赖配图,重点放在询问的构造、信息量与次数证明。
交互题怎样运行
程序与评测机轮流工作
普通题的过程是:
1 | 读入完整数据 -> 计算答案 -> 输出答案 |
交互题则更像一段对话:
1 | 读取初始信息 |
以最常见的询问格式为例:
1 | ? l r |
程序输出询问以后必须停下来读取回答,不能把后面所有询问提前输出。评测机给出的回答本身也是输入的一部分,只是它取决于我们刚才输出了什么。
query 与 print
为了避免在主逻辑中反复写输出、刷新和读入,最好把一次完整交互封装成函数:
1 | int query(int l, int r) { |
调用 query(l,r) 时,函数负责完成“输出询问、刷新、读取回答”三个动作;print只负责提交最终答案。
本文代码保留默认的流同步,并使用 fflush(stdout) 刷新,和后文的代码风格保持一致。如果写了 ios::sync_with_stdio(false),就应该改用 cout.flush(),不要再依赖C语言的 fflush 刷新已经解除同步的C++流。
为什么一定要刷新
输出通常会先进入缓冲区,积累到一定大小以后才真正发送。普通题最后一次性发送没有问题,但交互题的评测机只有收到询问才会给出回答。
如果询问仍然留在缓冲区中,程序在等待回答,评测机也在等待询问,最后通常得到 Idleness limit exceeded 或 TLE。
下面三种写法都可以主动刷新:
1 | cout << "? " << x << endl; |
endl会同时换行和刷新,写起来最短,但普通题中频繁使用会增加额外开销。交互题的询问次数通常不大,三种写法都足够;本文统一使用第三种。
-1、非法询问与立即退出
很多交互题会在询问非法、次数超限或答案已经错误时返回$-1$。此时交互已经结束,继续读取只会从关闭的输入流中得到无效数据,因此应该立即退出:
1 | int t; cin >> t; |
还要逐项检查题面的协议:
- 询问的下标能否重复;
- 区间是否允许为空;
- 输出答案是否计入询问次数;
- 多组数据之间是否需要额外读取判定结果;
- 输出答案以后是继续下一组,还是立即结束整个程序。
交互题没有统一格式,上一道题的 query 不能不看题面直接复制到下一道题。
自适应与非自适应交互器
非自适应交互器会在开始前固定隐藏数据,此后只负责如实回答。自适应交互器则可以在不违背已有回答的前提下继续调整隐藏数据。
面对自适应交互器,仅仅找到一个“可能的答案”并不够。所有回答共同构成的约束必须唯一确定答案,否则交互器仍然可以选择另一个同样合法的隐藏状态。
因此,证明一个交互方案时要回答三件事:
- 每次回答能推出什么确定信息;
- 这些信息能否唯一确定答案;
- 最坏情况下用了多少次询问。
怎样设计询问
先写出回答的数学含义
不要一上来就猜询问格式。先把评测机的回答写成关于隐藏量的式子,再观察两次回答之间有哪些量会抵消。
常见的构造方式包括:
| 方法 | 询问提供的信息 | 常见目标 |
|---|---|---|
| 区间计数 | 某个前缀、后缀或矩形中的数量 | 二分缺失位置 |
| 只替换一个元素 | 两次回答只相差一个未知量 | 求每个位置的值 |
| 奇偶性或异或 | 只保留出现奇数次的贡献 | 消去成对元素 |
| 特殊输入 | 让不同隐藏操作得到明显不同的结果 | 判断运算类型 |
| 枚举顺序 | 相邻回答共享较长前缀 | 恢复边或状态转移 |
一个好的询问,通常会让大部分未知量彼此抵消,只留下当前真正想知道的部分。
维护没有被排除的状态
普通二分维护候选区间,交互二分也一样。假设当前答案一定在$[l,r]$中,一次询问把它分成两部分,回答必须能够确定答案落在哪一侧。
如果一次询问最多把候选状态分成两类,那么$q$次询问最多区分$2^q$种情况,因此至少需要:
$$
q\ge\lceil\log_2 S\rceil
$$
其中$S$是初始可能状态的数量。这不是所有交互题的严格下界,但可以帮助判断询问上限是否暗示二分、按位确定或其他倍增信息的做法。
先计算询问次数,再写代码
假设一个位置需要二分$\lceil\log_2 n\rceil$次,共有三个位置要找,那么最坏询问数就是:
$$
3\lceil\log_2 n\rceil
$$
如果题目只允许$33$次,而$n\le1000$,就有:
$$
3\lceil\log_2(2n+1)\rceil\le3\times11=33
$$
这种计算应该在实现以前完成。只写“复杂度大约是对数级”不够,交互题限制的是具体次数,差一次同样会得到错误答案。
调试时不要污染标准输出
标准输出中的每个字符都可能被交互器当成命令。调试信息应该输出到 cerr:
1 | cerr << "l = " << l << ", r = " << r << '\n'; |
提交以前仍然建议删除调试输出。更稳妥的做法是自己写一个本地交互器,随机生成隐藏数据,并检查询问是否合法、次数是否超限、最终答案是否正确。
例题与典型应用
下面六道题从普通二分开始,逐步加入异或方程、进位、奇偶性、DAG计数和位运算构造。
| 题目 | 核心信息 | 最坏询问次数 |
|---|---|---|
| AtCoder ABC269 E | 矩形中的棋子数量 | $2\lceil\log_2 n\rceil$ |
| AtCoder ABC313 D | 奇偶性与异或方程 | $n$ |
| 洛谷 P14843 | 数位和与进位 | $73$ |
| Codeforces 2219B2 | 频次奇偶性与三次出现 | $33$ |
| Codeforces 2196C2 | 字典序路径与DAG计数 | $n+m$ |
| Codeforces 2222E | 特殊输入、按位恢复与分类 | $n+3$ |
例题一:AtCoder ABC269 E Last Rook
题目链接:AtCoder ABC269 E Last Rook
有一个$n\times n$棋盘,上面放了$n-1$个车,并且任意两辆车都不在同一行或同一列。每次可以询问一个矩形中有多少辆车,需要找出可以放置最后一辆车的位置。
因为$n-1$辆车占据了$n-1$个不同的行和$n-1$个不同的列,所以恰好缺少一行和一列。分别找出缺失行与缺失列即可。
设缺失行为$x$。询问前$mid$行和全部列组成的矩形,若$x>mid$,前$mid$行每行都有一辆车,回答为$mid$;若$x\le mid$,其中恰好缺少一辆,回答为$mid-1$。因此:
$$
query(1,mid,1,n)<mid\Longleftrightarrow x\le mid
$$
这个判定具有单调性,可以二分第一个满足条件的位置。缺失列完全相同,只要把询问改成全部行与前$mid$列。
点击展开完整代码
1 | /* Fufffh */ |
寻找一维位置需要$\lceil\log_2n\rceil$次询问,行列各做一次,总询问数不超过$2\lceil\log_2n\rceil$。由于$n\le1000$,最多只需$20$次,恰好满足限制。
例题二:AtCoder ABC313 D Odd or Even
题目链接:AtCoder ABC313 D Odd or Even
有一个长度为$n$的隐藏01序列$a$。每次选择$k$个不同位置,评测机返回这些位置之和的奇偶性,其中$k$为奇数。最多询问$n$次,需要确定整个序列。
奇偶性可以直接看成异或。先只考虑前$k+1$个位置。对每个$i\in[1,k+1]$,询问除$i$以外的另外$k$个位置,记回答为$b_i$。
设前$k+1$个数的异或和为:
$$
s=a_1\oplus a_2\oplus\cdots\oplus a_{k+1}
$$
由于第$i$次恰好没有选择$a_i$,所以:
$$
b_i=s\oplus a_i
$$
再把所有$b_i$异或起来。每个$a_j$一共出现在$k$次询问中,而$k$是奇数,因此:
$$
b_1\oplus b_2\oplus\cdots\oplus b_{k+1}=s
$$
于是前$k+1$个数都可以由$a_i=s\oplus b_i$恢复。
剩余位置更简单。固定前$k-1$个已知位置,再加入一个未知位置$i$询问。设前$k-1$个数的异或和为$pre$,回答就是$pre\oplus a_i$,因此$a_i=pre\oplus query$。
点击展开完整代码
1 | /* Fufffh */ |
前$k+1$个位置使用$k+1$次询问,其余位置使用$n-k-1$次,总数正好为$n$。即使交互器可以自适应,这$n$个独立关系也会唯一确定整个序列。
例题三:洛谷 P14843 Interactive Number Guessing
题目链接:洛谷 P14843 Interactive Number Guessing
评测机保存了一个小于$10^{18}$的非负整数$x$。每次选择非负整数$a$,可以得到$x+a$的十进制数位和。最多询问$75$次,需要确定$x$。
先询问$a=0$,得到$x$本身的数位和,记作$sum$。
现在单独考虑第$i$位,位权为$p=10^i$,这一位数字为$d$。询问$m\cdot p$,其中$0\le m\le9$。
如果$d+m\le9$,这一位不会产生进位,新的数位和恰好为:
$$
sum+m
$$
如果$d+m\ge10$,至少会产生一次进位。一次进位会让当前位减少$10$,并让更高一位增加$1$,数位和相对没有进位的情况少$9$;进位每向高位继续传播一次,还会再少$9$。所以回答不再等于$sum+m$。
因此“回答是否等于$sum+m$”关于$m$单调。最大的合法$m$恰好是$9-d$,从而得到:
$$
d=9-m
$$
对每一位分别在$[0,9]$中二分即可。
点击展开完整代码
1 | /* Fufffh */ |
第一次询问得到原数位和,每一位最多二分$4$次,共有$18$位,因此最坏询问数为:
$$
1+18\times4=73
$$
这比限制的$75$次少$2$次。
例题四:Codeforces 2219B2 Unique Values
题目链接:Codeforces 2219B2 Unique Values
隐藏数组长度为$2n+1$。其中一个值出现三次,其余$n-1$个值各出现两次。每次选择若干不同位置,评测机返回在这些位置中恰好出现一次的值有多少个,需要在$33$次询问内找出特殊值的三个位置。
设一次询问选择了$k$个位置,回答为$u$。先只看一个普通值:
| 在询问中出现次数 | 对$k$奇偶性的贡献 | 是否计入$u$ |
|---|---|---|
| $0$ | $0$ | $0$ |
| $1$ | $1$ | $1$ |
| $2$ | $0$ | $0$ |
普通值无论出现零次、一次还是两次,对$k$和$u$的奇偶性贡献都相同。特殊值出现零到两次时也一样,只有三个位置全部被选中时,它对$k$贡献奇数,却不会被计入$u$。
因此:
$$
(u\bmod2)\oplus(k\bmod2)=1
$$
当且仅当询问集合包含特殊值的全部三个位置。
先询问前缀$[1,mid]$,二分第一个包含全部三个位置的前缀,它的右端点就是最右侧位置$qr$。再询问后缀$[mid,2n+1]$,二分最后一个包含全部三个位置的后缀,得到最左侧位置$ql$。
最后把$ql,qr$固定加入询问,再对中间区间二分。此时选中的特殊值已经出现两次,当前区间包含中间位置时才会变成三次,仍然可以使用同一个奇偶判定。
点击展开完整代码
1 | /* Fufffh */ |
数组长度不超过$2001$,一次二分最多询问$11$次,三个位置合计不超过$33$次。最后一段只有一个位置时可以直接确定,不需要继续询问。
例题五:Codeforces 2196C2 Interactive Graph
题目链接:Codeforces 2196C2 Interactive Graph
评测机保存了一个$n$个点、$m$条边的有向无环图。所有路径按照顶点序列的字典序排列,每次可以询问第$k$条路径,需要在$n+m$次询问内还原整张图。
对于顶点$v$,设$cnt_v$为从$v$开始的路径数量。只包含$v$自己的序列也是一条路径;随后每条出边$v\to u$都可以接上任意一条从$u$开始的路径,因此:
$$
cnt_v=1+\sum_{v\to u}cnt_u
$$
图是DAG,所以这个递归一定会结束。
字典序路径可以看成对“路径前缀树”进行先序遍历。相邻两条路径 pre 与 cur 会共享一段最长公共前缀。设公共前缀长度为$t$,那么当前第一次进入的新分支对应边:
$$
cur_{t-1}\to cur_t
$$
如果顶点$cur_t$以前从未完整处理,就继续询问下一条路径;如果已经求出$cnt_{cur_t}$,说明这个顶点后面的所有路径结构都已经见过,可以直接把$k$增加$cnt_{cur_t}$,跳过整段重复后缀。
当枚举离开某个已经收集完全部出边的顶点时,再用上面的递推式计算它的 cnt。这样每次询问要么发现一条新边,要么完成一个新顶点,不会逐条走过指数级的全部路径。
点击展开完整代码
1 | /* Fufffh */ |
所有从同一个顶点开始的后缀路径只会被完整展开一次,之后都通过 cnt 整段跳过。询问可以分别归到一个新顶点或一条新边上,因此总询问数不超过$n+m$。
例题六:Codeforces 2222E Seek the Truth
题目链接:Codeforces 2222E Seek the Truth
有两个隐藏整数$k,c$,其中$k\in{1,2,3}$,分别表示按位与、按位或、按位异或:
$$
k=1:\qquad f(x)=x\mathbin{\mathrm{AND}}c
$$
$$
k=2:\qquad f(x)=x\mathbin{|}c
$$
$$
k=3:\qquad f(x)=x\oplus c
$$
评测机维护一个集合$S$。我们先选择一个初始元素,之后有两类询问:把$f(x)$插入集合并返回集合大小,或者询问集合中不小于$y$的元素数量。需要在$n+3$次以内求出$k,c$。
先令初始集合为$S={0}$,再插入$f(0)$:
$$
0\mathbin{\mathrm{AND}}c=0,\qquad 0\mathbin{|}c=0\oplus c=c
$$
如果集合大小仍然为$1$,运算一定是按位与;否则运算是按位或或按位异或,并且此时$S={0,c}$。
按位与
依次插入$f(2^i)$。当$c$的第$i$位为$1$时,结果是$2^i$,集合大小增加;否则结果为$0$,集合不变。这样用$n$次询问就能恢复$c$的每一位。
按位或或按位异或
此时集合只有$0,c$。对于$y\ge1$,询问集合中不小于$y$的元素数量,结果大于$0$当且仅当$c\ge y$,因此可以在$[1,2^n-1]$中二分出$c$。
最后区分按位或与按位异或。
如果$c$不是二的幂,取$c$最低的一个$1$为$b$。按位或得到$c$,不会加入新元素;按位异或得到$c\oplus b$,它既不是$0$也不是$c$,集合大小会增加。
如果$c$是二的幂,选择另一个二进制位$b$,并插入$f(c\mathbin{|}b)$。按位或会插入$c\mathbin{|}b$,按位异或只会插入$b$。两种情况集合大小都会增加一次,但再询问集合中是否存在不小于$c\mathbin{|}b$的元素,就可以区分二者。
点击展开完整代码
1 | /* Fufffh */ |
按位与分支使用$n+1$次询问。另一个分支先用一次询问排除按位与,再用$n$次二分恢复$c$;区分运算最多再用两次,总数不超过$n+3$。
常见错误与询问次数
常见错误
忘记刷新缓冲区
询问已经写进代码,却没有真正发送给评测机,程序和评测机互相等待。把“输出、换行、刷新、读入”统一放进 query,最不容易漏。
收到 -1 后继续运行
$-1$通常表示交互已经失败。继续把它当成普通回答参与计算,后面的下标与询问格式只会进一步失控。读到$-1$应立即退出。
只证明平均询问次数
评测限制看的是最坏情况。二分时应使用上取整,多个阶段的次数要相加,还要算上初始化、分类与最终验证使用的询问。
询问集合中出现重复下标
有些题要求询问的所有下标互不相同。把固定位置和二分区间拼在一起时,要先证明它们不会重叠,否则询问会被直接判为非法。
把非自适应思路用在自适应交互器上
自适应交互器不必提前固定唯一隐藏数组。最终输出以前,必须证明已有回答只对应一个合法状态,而不是只构造出其中一种可能。
按普通题方式读取全部输入
交互样例只是一次对话记录,不能把评测机后续回答当成开局就存在的完整输入。每个回答都必须在对应询问输出以后再读取。
忽略题目归档后的非交互版本
Codeforces等平台会为交互题提供 Hacks 格式,归档后也可能要求提交读取隐藏数据的非交互版本。本文完整代码对应原始交互协议,用于理解交互解法;实际提交以前应再次检查当前页面要求的输入格式。
询问次数与普通复杂度
交互题通常同时分析两个量:程序自身的运行复杂度,以及与评测机通信的询问次数。后者往往更加严格。
| 例题 | 询问次数 | 程序自身的主要复杂度 |
|---|---|---|
| ABC269 E | $O(\log n)$ | $O(\log n)$ |
| ABC313 D | $n$ | $O(nk)$ |
| 洛谷 P14843 | $73$ | $O(1)$ |
| CF2219B2 | $O(\log n)$,且不超过$33$ | $O(n\log n)$输出量 |
| CF2196C2 | 不超过$n+m$ | $O(n(n+m))$ |
| CF2222E | 不超过$n+3$ | $O(n)$ |
交互题真正要优化的并不一定是计算量,而是每次通信能排除多少状态。先把回答写成数学关系,再考虑二分、异或、奇偶性或特殊输入,通常比直接尝试猜答案更容易找到突破口。






