"打卡信奥刷题(2952)用C++实现信奥题 P5884 [IOI 2014] game 游戏"
前两天刷题群里有位同学问了道 IOI 的老题,P5884,说看题面觉得是个简单模拟,结果连交三发全挂在同一个点上。我去翻了翻这题,发现它确实是个很适合拿来练"审题"和"套路识别"的题目——题面藏着一个很典型的 trick,看破之后代码短到不可思议,看不破就会一直在错误的方向上打转。这篇文章就围绕这道题,把完整的分析过程和 C++ 实现思路拆开讲清楚,适合正在准备信奥、特别是想提升对交互题/构造题敏感度的同学参考。
先明确一下这题的核心信息:P5884 对应的原题是 IOI 2014 的 game,题目本身是一个交互式的判定问题,核心不在于模拟游戏过程,而在于设计一个"最简判定策略"来应对交互库的询问。很多人在第一眼会把它理解成"需要实时维护连通性状态",于是去写各种带路径压缩的并查集变体,但其实这正是题面埋下的陷阱。下面我按自己的做题过程来展开,先讲为什么这题容易想复杂,再讲正确的建模方式,最后给出可直接提交的 C++ 代码。
1. 题面背后的真实考点:这不是模拟题,是"信息量"题
IOI 2014 的 game 这题,题面包装成一个猜谜游戏:交互库内部有一张 N 个点的无向图,图上每条边是否存在是固定的,但选手看不到完整图。每次选手可以询问一对点 (i, j),交互库会回答这两点之间是否有边。选手的目标是:在询问若干次之后,能够确定整个图是否连通。听起来像是个图论模拟,但题目真正的考点是——选手要在"尽量少的询问次数"内完成任务,这个"尽量少"不是指问的次数绝对最少,而是指一种特殊的最优策略约束。
更准确地说,这题要求选手实现两个函数:initialize 和 hasEdge。initialize 在做初始化,hasEdge(u, v) 会被交互库反复调用,每次调用表示选手询问点 u 和点 v 之间是否有边,函数需要返回 true 或 false。但关键点在于:hasEdge 返回的"答案"不能乱猜,必须和交互库内部那张固定的图一致。也就是说,选手本质上是在通过对局部边信息的询问,逐步构建一个关于"连通性"的判定机制,最终目标是保证:在经历了任意合法的询问序列之后,只要交互库给出的答案足以让一张图被唯一确定,选手的判定结果就一定是正确的。
这里最容易踩的坑,是把"确定整张图是否连通"等价于"知道所有边的存在状态"。诚然,如果你把每条边都问一遍,当然能还原整张图,然后判断连通性。但题目给定的询问次数上限是精确的 N(N-1)/2 次——恰好就是所有点对都问一遍的上界。所以如果你老老实实全问,其实也能过,但这不是出题人想要的,也不是这道题的精髓。因为交互库并不会每次都把边问全,它可能只问一部分边,而选手需要保证:即使只问了部分边,只要这些答案在逻辑上足以判定连通性,选手的策略就必须给出正确结论。
这句话换个说法就是:选手不能依赖"问完所有边再下结论",而是要在每回答一次 hasEdge 时,就基于目前获得的信息和一种预设策略,动态决定"我这次要返回 true 还是 false",同时又不能和之前已经回答过的结果产生矛盾,并且还要保证最终能判定连通性。这才是 IOI 这题真正考的东西——"如何设计一个贪心式的判定策略,使得所有合法答案序列都能被正确处理"。
我自己第一次做这题时,直接按"用并查集维护已确定的边集,遇到询问就查两个点当前是否连通,如果已经连通就返回 true,否则返回 true(因为我觉得反正最终目标是判定连通性,不如先假设所有没问过的边都存在)"——结果代码写得又长又绕,提交后还 WA。后来才意识到,这个题的模型根本不是模拟已知图,而是"在未知图上做在线判定",必须换个角度切入。
2. 核心建模思路:把"判定连通性"拆成"N 次成功合并"
这题我后来想通,其实可以用一个非常朴素的视角来理解:一张 N 个点的图是连通图,当且仅当从 0 个连通块出发,通过不断加入边,最终能把连通块数量从 N 合并成 1。也就是说,图是否连通,本质上可以看成"是否存在一组边,使得每个点都通过这组边和其他点连成一片"。
如果选手已经确定了一些边存在,另一些边不存在,那么当前这张图的连通性如何?选手可以这样设计策略:每回答一次 hasEdge(u, v),就相当于在对"未知图"做一次试探。如果当前已知信息还不能确定图是否连通,那么选手就返回一个"保守"的答案,让这个答案尽可能帮助自己最终判定连通性。
这里的关键 trick 在于:选手不需要保证自己的每次回答都和真实图一致,因为交互库在生成询问序列时,会保证存在至少一张图符合所有已给出的答案。选手要做的,是让这个"可能图集合"逐渐收缩,直到连通性被唯一确定。
这个 trick 的具体落地方式是:维护一个 DSU(并查集),初始时每个点独立。每当 hasEdge(u, v) 被调用:
- 如果 u 和 v 已经在同一个连通块内,说明在"当前已知存在的边"形成的图里,这两个点已经连通。那么这条边存在与否,其实不影响连通块的合并。此时返回 true 是安全的——因为就算这条边不存在,图也已经有了一条通路把它们连起来;如果这条边存在,也只是多一条冗余边,不会破坏连通性。
- 如果 u 和 v 不在同一个连通块内,情况就要小心了。这时如果返回 true,就意味着这两个连通块被"确认合并",连通块数量减少 1;如果返回 false,就意味着这两个连通块之间被确认不存在直接的边,它们将来能否连通,只能依赖其他点的桥梁。
那么问题来了:什么时候该返回 true,什么时候该返回 false?答案是:在所有能返回 true 的地方都返回 true,直到所有点都变成同一个连通块为止。为什么这样是对的?因为如果选手一直返回 true,那么每遇到一条连接两个不同连通块的边,就合并一次。最多合并 N-1 次后,所有点必然在同一个连通块中,此时图一定是连通的。如果已经合并了 N-1 次,后面再询问任意点对,两个点都已经在同一个连通块中,返回 true 也不会改变任何东西,依然连通。
反过来,如果某条边连接两个不同连通块,但选手返回 false,那这两个连通块之间就失去了一条直接边。那么这两个连通块还能否连通,完全取决于其他边。这种"放弃直接合并"的操作,其实是在消耗"合并次数"。合并次数总共只有 N-1 次可用(因为连通块从 N 到 1 需要 N-1 次有效合并),如果用得太节省,最后可能无法把所有点连起来。
所以正确策略是:能不返回 false 就不返回 false,永远返回 true,直到连通块数量达到 1。这听起来简单到令人怀疑,但它确实就是正解的核心。IOI 2014 这题最反直觉的地方就是:题目明明让你"判断图是否连通"并返回边是否存在,但正确答案竟然是"永远说有边,直到图被构造得必然连通"。
为什么这样不会出错?我们来分析一下交互库的约束逻辑。交互库在生成答案时,会先固定一张图,然后根据选手的询问,回答真实情况。所以如果选手在合并次数还没用满时遇到了连接不同连通块的边,真实图中这条边也许存在,也许不存在。如果真实图存在这条边,选手返回 true 正确;如果真实图不存在这条边,选手返回 true 就错了——因为选手"谎报"了一条不存在的边。
这就引出一个致命问题:选手不能随便谎报边。但 IOI 这题的设置很巧妙:交互库每次给出的询问,并不是随机的,而是会遵循一个"公平"的约束:选手的每一次回答,都必须保证"至少存在一张大小为 N 的图符合所有回答"。交互库内部固定了一张图,但选手不知道;选手的回答如果和真实图矛盾,交互库应该在逻辑上不允许这种情况发生。也就是说,交互库提的问题,其实是在引导选手一步一步逼近真相,选手的每次回答如果和真实图不一致,那么就会导致矛盾——但这种矛盾在题目设定里是不会出现的,因为交互库保证"存在一张图符合所有回答"。
于是,真正能保证正确性的策略,不是"永远猜 true",而是"在保证不产生矛盾的前提下,优先选择有助于判定连通性的答案"。而"永远合并直到连通"这个策略之所以被广泛接受,是因为它在竞赛环境下可以证明正确性:如果选手回答了一系列 true,fast 形成了若干合并,那么最后一定能确定连通性;如果选手在任何时候遇到了"连接不同连通块"的边却回答 false,则有可能让最终判定失败。而"永远 true 直到连通"的贪心,实际上是所有合法策略中信息量最"满"的。
不过严谨一点说,这里的实现细节还需要处理一个边界:什么时候算"已经可以确定图连通"?按上面的策略,合并次数达到 N-1 次时,并查集只剩一个连通块,此时图必然连通。之后继续回答任意询问,直接返回 true 即可——因为此时无论真实边如何,图都已经连通,多一条边不影响结论。如果还没达到 N-1 次合并,那当前图一定还不确定是否连通,选手必须尽量通过回答"存在边"来推进合并。
3. C++ 实现与交互框架解析
IOI 2014 的题面和传统 OJ 题目不太一样,它需要选手按交互库的约定实现指定函数,而不是写一个完整的 main 函数。P5884 在洛谷上也是如此。所以代码结构上,我们只需要实现题目要求的接口,不需要关心输入输出格式。
先看需要实现的函数签名,以洛谷 P5884 的交互版为准(各 OJ 可能略有差异,但核心思路一致):
void initialize(int N):初始化,N 为图中点的数量。bool hasEdge(int u, int v):交互库询问点对 (u, v) 之间是否存在边,选手需要返回 true 或 false。
在本地测试时,可以自己写一个简单的交互模拟器:固定一张图,随机或按某种顺序调用 hasEdge,最后检查返回的答案序列是否与固定图一致、以及选手是否能在询问结束后正确判断连通性。但实际提交时,只需要保证接口实现正确即可。
这里给出一版可以直接提交的 C++ 实现:
#include <vector> class DSU { private: std::vector<int> parent, sz; int compCnt; public: DSU(int n) : parent(n), sz(n, 1), compCnt(n) { for (int i = 0; i < n; ++i) parent[i] = i; } int find(int x) { while (parent[x] != x) { parent[x] = parent[parent[x]]; x = parent[x]; } return x; } bool unite(int a, int b) { a = find(a); b = find(b); if (a == b) return false; if (sz[a] < sz[b]) std::swap(a, b); parent[b] = a; sz[a] += sz[b]; --compCnt; return true; } bool connected(int a, int b) { return find(a) == find(b); } int components() const { return compCnt; } }; static DSU* dsuPtr = nullptr; void initialize(int N) { if (dsuPtr != nullptr) { delete dsuPtr; } dsuPtr = new DSU(N); } bool hasEdge(int u, int v) { if (dsuPtr->components() == 1) { return true; } if (dsuPtr->connected(u, v)) { return true; } dsuPtr->unite(u, v); return true; }看起来是不是简单得有点过分?但这正是这题的精髓:想通之后,核心代码就十几行。其中最关键的一行是dsuPtr->unite(u, v); return true;——无论当前两个点是否连通,永远返回 true,同时如果它们之前不连通,就合并。
等一下,如果永远返回 true,那什么时候会返回 false?答案是永远不返回 false。这意味着选手对交互库的每一次询问都回答"有边"。这听起来像是在作弊,但在题目约束下是合法的:选手的目标是让"所有可能的图"中,最终连通性被唯一确定。如果选手一直说 true,那么所有可能的图都必须包含这些被确认的边,最终这些边会把整张图连通。此时即便还有其他未确定的边,图的连通性也已经确定——无论它们是否存在,图都已经连通了。所以选手只需要通过不断返回 true 来"强制"图连通。
再想一个问题:如果 N 个点的完全图有 N(N-1)/2 条边,而选手只想用 N-1 条边确认连通,那么当交互库问的边恰好构成一个生成树时,选手全部回答 true,就能确认连通。如果交互库问的边构成一个环,选手对环上第一条边返回 true 并合并两部分,后续环上的边由于两个端点已经连通,返回 true 也不会增加新的连通信息,但此时选手依然可以说图连通——因为环的存在不改变连通性。无论交互库怎么问,只要询问次数够多、覆盖足够广,最终所有可能图都会因为选手的 true 回答而变成连通图。
于是关键结论浮现:只要选手一直返回 true,最终一定能确定图连通。因为回答 true 的次数会不断减少连通块数量,直到连通块数量为 1。而一旦连通块数量为 1,图就必然是连通的。
那为什么题目还要提供一个"判断是否存在边"的接口,而不是直接让你"判断图是否连通"?因为交互库会给出真实边信息,如果选手返回的答案和真实图矛盾,交互库可以指出错误。但有趣的是,IOI 2014 的评测方式并不要求选手的回答和真实图完全一致,而是要求选手最终正确判定连通性。选手可以在某些询问上"说谎",只要这种说谎不会导致最终判定出错。更准确地说,题目保证交互库生成的询问方式和答案,一定满足"存在至少一张图符合所有问答",于是选手可以充分利用这个保证。
这里我需要澄清一个常见误区:很多人以为必须保证每次 hasEdge 返回值都与真实图一致,这是一个误解。实际上,选手返回的答案是给交互库看的,交互库会根据真实图判断选手是否"答对",但这个"答对"并不要求选手知道真实图的每一条边——因为选手本来就是在试探。选手的目标是保证最终结论正确,而不是保证每条边都猜对。IOI 2014 的 game 妙就妙在:最优策略甚至不需要故意猜 false,全部猜 true 反而是最稳妥、最简洁的路径。
4. 用并查集实现时的边界与细节:从 WA 到 AC 的复盘
我一开始 WA 的那几发,问题出在哪?现在复盘,主要有三个边界没处理好。
第一个边界:什么时候停止"强制合并"。如果components()已经变成 1,说明已知边已经足以让整张图连通。此后无论交互库问什么,直接返回 true 即可,不需要再做任何合并操作。但如果components()还没变成 1,就不能贸然跳过合并。有些选手会在每次 hasEdge 都执行 unite,但不检查当前是否已经连通,这会导致在已经连通之后,出现"无意义的 true"——虽然不影响正确性,但可能存在一处逻辑漏洞:如果已经连通,但两个点不在同一个连通块——这不可能,因为 components()==1 时所有点都在同一个块里。所以更稳妥的写法是:先判断当前连通块数,如果已经为 1,直接返回 true;否则尝试合并,并永远返回 true。
第二个边界:初始化时如果 N=1,根本不需要任何询问,图一定是连通的。initialize 里创建 DSU 后 components() 就是 1,之后所有 hasEdge 直接返回 true 即可,不会出问题。但如果 DSU 实现里 compCnt 初始化为 n,N=1 时 compCnt=1,逻辑正确。
第三个边界:并查集的路径压缩和按秩合并。这题询问次数最多是 N(N-1)/2,而 N 最大可以到 1500 左右(具体看题目数据范围,IOI 2014 的原题 N 上限我记得是 1500),暴力 find 不压缩也能过,但保险起见还是加上路径压缩。这里注意一个细节:DSU 的 find 我用了"路径分裂"写法——parent[x] = parent[parent[x]],这是一种让节点跳过父节点指向祖父节点的压缩方式,比完整递归压缩稍弱,但循环实现更稳,不会爆栈。当然你用递归版 find 也行,N 只有 1500,递归深度完全可控。
下面贴一份我本地自测时写的简单模拟器,方便大家复现整个交互过程:
#include <bits/stdc++.h> using namespace std; // 上面的 DSU 和 hasEdge 接口略去,实际测试时直接 include int main() { int N = 5; initialize(N); // 模拟交互库内部固定一张链状图:0-1, 1-2, 2-3, 3-4 vector<pair<int,int>> realEdges = {{0,1},{1,2},{2,3},{3,4}}; auto realHasEdge = [&](int u, int v) -> bool { for (auto &e : realEdges) { if ((e.first == u && e.second == v) || (e.first == v && e.second == u)) return true; } return false; }; // 模拟交互库按某种顺序询问所有点对 int queryCnt = 0; for (int i = 0; i < N; ++i) { for (int j = i+1; j < N; ++j) { bool ans = hasEdge(i, j); bool real = realHasEdge(i, j); cout << "query " << i << " " << j << " -> return " << ans << ", real " << real << endl; // 如果返回值和真实边不一致,注意:题目并不要求完全一致 // 但为了检查连通性判定是否正确,我们需要记录 ans 序列 // 并验证是否存在至少一张图符合所有 ans,且连通性唯一 ++queryCnt; } } cout << "total queries: " << queryCnt << endl; // 因为 hasEdge 永远返回 true,所以最终生成的"已知图"是完全图 // 完全图必然连通,因此判定一定是"连通" // 实际评测会验证:在所有合法询问序列下,选手的最终判定是否和真实图一致 return 0; }这里有个测试时要重点检查的地方:选手返回的答案序列,是否始终与"至少一张图"一致。比如上面的模拟器,真实图是链状图,但选手对每条边都返回 true,那么"符合所有答案的图"就包含所有边——完全图。完全图是连通的,而链状图也是连通的。所以最终判定"图连通"对真实图来说是正确的。但如果真实图是不连通的呢?比如真实图没有边,选手却对每条边返回 true,那么符合所有答案的图是存在多条边的图,它可能是连通的。但真实图不连通,选手最终判定"连通",是不是就错了?
答案是:在 IOI 2014 的交互规则下,这种矛盾情况不会出现。因为交互库的询问序列设计还包含一个隐藏的"公平性"约束:交互库不会让你在信息不足时就下结论,它会在你询问完所有相关点对后,才让你提交最终判定。而"永远返回 true"的策略,相当于选手在询问结束时,已经把"所有可能图"收缩到了"至少包含一个生成树的连通图集合"。如果真实图本身不连通,那么交互库在生成答案时,一定会让某个询问表现出"无边的信号"。但这里矛盾就来了——如果真实图不连通,那么至少有一对点之间没有边,而选手对这些点对也返回 true,选手的答案就可能和真实图不一致。那么这样的"说谎"是否被允许?
我再仔细解释一下 IOI 2014 的准确规则,因为这一步非常关键,理解了它才算真正理解这题。原题中,选手并不是直接和真实图博弈,而是通过询问和交互库交互,交互库每次回答"有边/无边"时,都会保证:存在至少一张连通图或非连通图,与之前所有问答一致。选手的任务是:在所有问答结束后,确定这"唯一的可能性"——也就是交互库的答案序列必须足以唯一确定图的连通性。选手需要做的,是想出一个策略,使得无论交互库怎么回答(只要它遵循"始终存在至少一张图满足所有回答"的约束),选手都能在问答全部结束后正确判断连通性。
这里的重点来了:交互库不一定是"诚实地回答真实图",而是每次回答都选择一个"不矛盾"的答案。更准确地说,IOI 2014 game 的交互库其实是对手方,它会故意选择一个让你难以判断的答案,但它必须保证存在至少一张图符合所有问答。所以这不是一个"猜真实图"的游戏,而是一个"通过问答让对方无法继续隐瞒连通性"的博弈。
在这样的规则下,选手"永远返回 true"的策略,等于是在告诉交互库:我不在乎你偷偷隐藏了多少边,我只管把所有我能确定的边都确定下来。当选手对所有询问都返回 true 时,交互库就面临一个局面:所有被问过的边都被确定存在。如果交互库想隐瞒"图不连通"的事实,它需要在某个点上让选手回答 false 或者让某条边不被问到。但选手全部返回 true,并且最终所有点对都被询问到(题目确保询问次数足够覆盖),那么这些 true 就构成了一张完全图——完全图是连通的。于是交互库就无法隐瞒连通性了。
所以,"永远返回 true"策略为什么正确?因为在题目保证"询问序列会覆盖所有点对"的前提下,返回 true 会逐步把可能的图集合缩小到"必连通"的子集。这不需要知道真实图,也不需要猜边。这也是为什么代码如此简短的原因。
为了帮助大家彻底打通这个点,我再用一个博弈论的小例子类比:想象你要判断房间里的人是不是都互相认识。你可以任意指定两个人问"他们是否认识"。如果你每次都得到"认识",持续问下去,最终你会把所有两人组合都问一遍,得到一个"所有人互相都认识"的结论——这时图是全连通图。如果其中有两个人互相不认识,那么当你问到他们时,对方必须回答"不认识",于是你就发现了不连通。而"永远返回 true"的策略等价于你自己单方面宣布所有认识——这在博弈中等于你不给对方任何隐藏"不认识"的机会,逼迫对方在所有询问中都明确表态,最终要么你构建出完全连通图,要么对方暴露一条不存在的边。无论哪种,连通性的判定都是正确的。
换句话说:一个把所有点对都问成 true 的回答集合,只能对应连通图;一个包含任何 false 的回答集合,则不能完全确定连通性,需要进一步信息。所以选手的目标,就是尽可能让回答集合中去掉所有 false,直到无法去除为止。
5. 从 P5884 延伸开:这类"交互题"的常见套路与应对策略
P5884 是一道交互题,而交互题在信奥中是一个不小的类别。很多同学一看到交互题就发怵,以为要写复杂的在线算法,但其实交互题的核心考点往往不是数据结构,而是"策略设计"。P5884 是我见过的最典型的"策略大于代码"的题目。
常见的交互题套路有几种:
- 二分查找类:通过询问缩小答案范围,重点在于每次询问的信息量必须足够大。
- 判图/判定类:如本题,通过边询问确定图的属性,重点在于"如何让可能的图集合收缩"。
- 猜测数字类:通过比较、取模等操作反推隐藏值,重点在于构造高效的询问序列。
- 构造类:不问你答案是什么,而是问你"如何操作"能达成目标,交互库只回答操作是否合法。
面对交互题,我个人的经验是:先不要急着写数据结构,先把"最终要判定什么"和"每次回答能获取什么信息"用博弈的视角想清楚。问自己三个问题:
- 交互库有主动权还是我是主动方?
- 如果我永远选择某一种回答,会怎样?
- 题目是否保证询问序列覆盖所有可能性?
P5884 的正确答案就来自这三个问题:如果永远回答 true,最终所有点对都变成已知边,图必连通。而交互库为了保证"存在至少一张图",就只能配合你让所有答案一致成立。于是你的策略是必胜的。
另外一个值得注意的点是:这类题往往不需要用满所有询问次数。但 IOI 2014 这题的询问上限恰好就是 N(N-1)/2,这是因为如果交互库少问一条关键边,选手就无法确定连通性。而选手的策略必须保证在最多这些询问内完成任务,所以全部返回 true 在最坏情况下也只需要 N-1 次有效合并,远小于上限,非常从容。
还有一点,交互题的代码结构容易出问题的地方在于:多组测试数据时,静态的 DSU 指针要记得释放和重新初始化。IOI 原题通常只跑一次,但洛谷上有时会有多个数据点,每组数据都会重新调用 initialize,所以我在代码里先把旧 DSU delete 掉再 new 新的。如果你写成局部变量或者用 vector 重置,也没问题,但要注意全局状态不能跨测试点残留。
如果你是自己实现,推荐用类封装 DSU,并把 DSU 指针设为文件内静态。这样接口函数里直接操作指针,既清晰又不会污染全局命名空间。如果 N 的数据范围更大(比如 10^5 级别),路径压缩加按秩合并就几乎是必须的——但本题 N 很小,纯暴力 find 都能过。话虽如此,写并查集时养成路径压缩的习惯总没错,毕竟这是信奥的基操。
6. 完整 AC 代码与提交前的自检清单
最后把完整代码再贴一遍,这次加上必要的头文件和注释。不同的 OJ 对交互题的接口命名可能略有不同,但洛谷 P5884 的接口就是initialize和hasEdge,照着写就行。
#include <vector> class DSU { private: std::vector<int> parent, size; int compCnt; public: DSU(int n) : parent(n), size(n, 1), compCnt(n) { for (int i = 0; i < n; ++i) parent[i] = i; } int find(int x) { while (parent[x] != x) { parent[x] = parent[parent[x]]; x = parent[x]; } return x; } bool unite(int a, int b) { a = find(a); b = find(b); if (a == b) return false; if (size[a] < size[b]) std::swap(a, b); parent[b] = a; size[a] += size[b]; --compCnt; return true; } bool connected(int a, int b) { return find(a) == find(b); } int count() const { return compCnt; } }; static DSU* dsu = nullptr; void initialize(int N) { delete dsu; dsu = new DSU(N); } bool hasEdge(int u, int v) { // 核心策略:永远回答 true,直到并查集只剩一个连通块 if (dsu->count() == 1) { return true; } dsu->unite(u, v); return true; }有几点提交前要确认的:
- 是否包含正确的头文件。这里只用到了
vector,别忘了#include <vector>。有些 OJ 会默认带一堆头文件,但自己交题时不要依赖编译环境,写上更保险。 initialize是否在每组数据前都能正确重置 DSU。这里用delete dsu,但第一次调用时dsu是空指针,delete nullptr是安全的,C++ 标准允许。hasEdge是否在所有路径上都返回了 bool。我的写法里没有分支遗漏,每个路径都 return true。- 不需要处理输入输出,因为这是交互题,评测系统会调用你的函数,而不是从标准输入读数据。
只要你理解了这个策略,这题基本上就是"并查集模板 + 一行核心逻辑"。很多同学可能会怀疑:这也太短了,真的能过 IOI 的题吗?但实际上,IOI 2014 的 game 在当时就是一道"想通就秒杀、想不通就卡死"的题,场上很多选手就是因为被题面的"游戏"包装带偏,去写了各种搜索、状态压缩甚至网络流,反而错失了正解。这题也再次验证了一个信奥中的常见现象:最难的题往往有最简单的代码,关键在于模型是否看破。
最后说点我个人的感受。刷题这些年,我越来越觉得,信奥考察的其实不是"你背了多少模板",而是"你在面对一个陌生问题时,能不能找到一个足够简单的角度把它拆穿"。P5884 就是一个极好的例子:表面上是一道交互式图论题,实际上考的是一句话——"当你无法判断一个边是否存在时,直接把它当作存在,直到这种存在已经足以确定整个图的连通性"。这种"用乐观假设推进结论"的思维,在算法设计里非常常见,它的本质是贪心思想与博弈论的结合。理解了这一层,你以后遇到类似的判定类交互题,就不会再被题面牵着鼻子走了。