「LOJ6734」图上的游戏
题目
点这里看题目。
有一张 个点 条边的无自环连通无向图,点编号为 ,边编号为 。
你可以执行不超过 次以下询问:
给出一个结点 和一个边的子集 ,交互库会告诉你是否存在一条从 到 的、只经过 中的边的路径。
你需要猜出这张图,也就是猜出每条边连接的两个结点的编号。
所有数据满足 。
分析
Subtasks 1,2
。
首先处理出一个边集 ,该边集中包含一棵生成树。这很容易,只需要暴力地迭代就行了,复杂度为 。可以顺便求出每条树边连接了哪两个点,相当于直接确定了生成树的形态。
然后处理非树边。从叶子开始往上处理每个结点 :每次删除 的父边,加入一条非树边(非树边不能指向自己子树)并检查连通性,复杂度为 。这样可以确定跨子树非树边。
最后还会剩下祖孙非树边。这类树边的较深结点已经确定,可以直接二分出另一个端点。
这个貌似做复杂了,不过能过就行。
Subtask 3
,图的形态为以 为端点的一条链。
最开始想的是“拆边分治”,但是发现拆边不便于点集的划分。
那就反过来,每次拿出来一个点 ,找出它的所有祖先边,这样边就自然被划分为了两个部分。再找出 的祖先和子树中的点,于是点集也被划分为了两个部分。两个部分为结构相似的子问题,递归下去即可。
随机选择 即可得到 的期望复杂度。
Subtask 5
对于 ,第 条边连接 。
现在,每条非树边对应一个区间。一次询问可以告诉我们某个元素是否被某个区间集合内的任何一个区间包含。
若能对于每个区间找到它最靠左的元素,就可以二分出右端点。而从左往右枚举每一个元素,并二分出哪些区间包含它,就可以找到每个区间的最左元素。复杂度为 。
Subtask 4
。
可以随便找一个点 ,然后通过二分找出 的所有祖先边。
然后不会了,就去看题解了。
为了保证复杂度不会过高,我们可以反复执行该二分操作,但是二分出一条边后就不再对它进行二分了。考察我们得到的东西:将同一个 二分出来的所有边染成一个颜色,这样就得到了题解中所谓的“链划分”的形式。
“链划分”仅仅将每一条边分到了一个链里面,但是没有进行点的划分;划分完点之后,我们还得接着确定链内部的顺序和链之间的连接点。
首先解决点的划分,即找出“每个点属于哪条链”。考虑枚举点 :
-
如果 的祖先边中还存在没有被划分到某条链中的边(可以仅保留已经入链的边,检查连通性),那么就可以找出所有这样的祖先边,并生成一条新的链。
-
否则,我们需要找到 在哪一条链。对于所在链的时间进行二分即可。
这样就可以 完成边的划分和点的划分。接下来,链内部的定序就是 Subtask 3 了,总共 。
最后要完成链之间的连接,也就是找到链顶的父亲。注意到,如果按照链产生的时间处理的话,我们可以得到点的一个 DFS 序。这个顺序满足“ 的祖先一定出现在 之前”,所以按照 DFS 序二分即可。
Remark.
首先,这个做法可以看作是题解中链做法的拓展。
其次,合我胃口的思路是:
因为询问涉及的是单点和边集,所以最多只能对于边集二分;
对于边集二分可以帮助我们找到所有祖先边,但是每个点都找一遍太浪费时间了;
如果每条边仅允许被二分到一次,观察产生的东西的结构,就得到了“链划分”这么一个东西;
Subtask 6
编号 的边构成一棵树。
用 Subtask 4 的方法,把树建出来先。
仍然考虑像 Subtask 5 一样,枚举点 ,删掉它的父边,然后二分出哪些点连接了两个连通块。但是为了避免二分到子树内的边,也为了处理非祖孙非树边,我们需要限制二分范围:二分涉及到的边一定不能连接 的子树内非 的结点。
这样,一条边至多被二分两次。如果一条边被二分到两次,则它是非祖孙非树边;否则,它是祖孙非树边,而我们的二分确定了它的一个端点,二分出另外一个即可。
Subtask 7
没有限制!!!没有限制!!!
现在需要攻克的难关,就是找到一棵生成树的边集!!!只需要找到边集就可以了!!!
Subtask 4 不是要二分祖先边吗?这个二分过程可以直接套过来:枚举每个点 ,找到最长的一个编号前缀,使得删掉这个前缀恰好可以保证 能到 (再多删一个就会爆炸)。可以发现,下一条边一定在某个生成树上。反复迭代即可找到一棵生成树。
这个过程可以和 Subtask 4 的“链划分”过程合在一起,所以并没有需要特别注意的部分。
交互次数上界在 ,但实际上询问次数在 ,还是非常优秀的。
代码
#include <vector>
#include <random>
#include <utility>
#include "graph.h"
namespace Workplace {
#define rep( i, a, b ) for( int i = (a) ; i <= (b) ; i ++ )
#define per( i, a, b ) for( int i = (a) ; i >= (b) ; i -- )
const int MAXN = 605;
std :: vector<int> chnEdg[MAXN], chnNod[MAXN];
std :: pair<int, int> ans[MAXN];
std :: vector<int> subtre[MAXN];
int faE[MAXN], fath[MAXN];
int ord[MAXN];
int bel[MAXN], tot = 0;
bool onTre[MAXN], vis[MAXN], solved[MAXN];
int n, m;
inline bool Query( const int &u, const std :: vector<int> &E ) {
static std :: vector<int> s( m, 0 );
rep( i, 0, m - 1 ) s[i] = 0;
for( const int &e : E ) s[e] = 1;
return query( u, s );
}
inline std :: vector<int> Slice( const std :: vector<int> &vec, const int &l, const int &r ) {
return std :: vector<int> ( vec.begin() + l, vec.begin() + r );
}
inline std :: vector<int> operator + ( const std :: vector<int> &A, const std :: vector<int> &B ){
std :: vector<int> C( A );
C.insert( C.end(), B.begin(), B.end() );
return C;
}
inline std :: vector<int>& operator += ( std :: vector<int> &A, const std :: vector<int> &B ) {
return ( A.insert( A.end(), B.begin(), B.end() ), A );
}
void BuildChain( const std :: vector<int> &nod, const std :: vector<int> &edg ) {
static std :: mt19937 rng( 1145141 );
if( edg.empty() ) return ;
if( edg.size() == 1u ) {
ans[edg[0]] = { nod[0], nod[1] };
faE[nod[1]] = edg[0], fath[nod[1]] = nod[0];
ord[++ ord[0]] = nod[1];
return ;
}
int V = nod.size(), E = edg.size();
int p = std :: uniform_int_distribution<> ( 1, V - 1 ) ( rng );
std :: vector<int> lNod, rNod, lEdg, rEdg, ance, qry;
for( int u = nod[0] ; u ; ance.push_back( faE[u] ), u = fath[u] );
rep( i, 0, E - 1 ) {
qry = ance;
rep( j, 0, E - 1 )
if( i ^ j ) qry.push_back( edg[j] );
( Query( nod[p], qry ) ? rEdg : lEdg ).push_back( edg[i] );
}
qry = ance + lEdg, rNod.push_back( nod[p] );
rep( i, 0, V - 1 )
( Query( nod[i], qry ) ? lNod : rNod ).push_back( nod[i] );
BuildChain( lNod, lEdg );
BuildChain( rNod, rEdg );
}
inline void BuildTree() {
std :: vector<int> tre, qry;
rep( i, 0, m - 1 ) onTre[i] = false;
// form a spanning tree.
// at the same time, construct a chain division.
rep( i, 1, n - 1 ) {
std :: vector<int> ance( tre ), avai, nw;
rep( j, 0, m - 1 )
if( ! onTre[j] )
avai.push_back( j );
int num = avai.size();
for( int lst = 0 ; lst < num ; ) {
if( Query( i, ance ) ) break;
int l = lst, r = num - 1, mid;
while( l < r ) {
mid = ( l + r ) >> 1;
if( ! Query( i, ance + Slice( avai, mid + 1, num ) ) ) r = mid;
else l = mid + 1;
}
nw.push_back( avai[l] );
ance.push_back( avai[l] );
onTre[avai[l]] = true, lst = l + 1;
}
tre.swap( ance );
if( ! nw.empty() ) {
bel[i] = ++ tot;
chnEdg[tot].swap( nw );
chnNod[tot].push_back( i );
} else {
int l = 1, r = tot, mid;
while( l < r ) {
mid = ( l + r ) >> 1, qry.clear();
rep( j, 1, mid ) qry += chnEdg[j];
if( Query( i, qry ) ) r = mid;
else l = mid + 1;
}
chnNod[bel[i] = l].push_back( i );
}
}
// construct the spanning tree by the division.
vis[ord[ord[0] = 1] = 0] = true;
rep( i, 1, tot ) {
int l = 1, r = ord[0], mid;
while( l < r ) {
mid = ( l + r ) >> 1, qry = chnEdg[i];
rep( j, 2, mid ) qry.push_back( faE[ord[j]] );
if( Query( chnNod[i][0], qry ) ) r = mid;
else l = mid + 1;
}
std :: vector<int> nod;
nod.push_back( ord[l] ), nod += chnNod[i];
BuildChain( nod, chnEdg[i] );
}
}
inline void FindRemaining() {
rep( i, 0, n - 1 ) vis[i] = false;
per( i, ord[0], 2 ) {
int u = ord[i];
std :: vector<int> tre, notTre;
for( const int &v : subtre[u] ) vis[v] = true;
rep( j, 0, m - 1 )
if( onTre[j] ) {
if( j != faE[u] )
tre.push_back( j );
} else {
if( ( ans[j]. first == -1 || ! vis[ans[j]. first] ) &&
( ans[j].second == -1 || ! vis[ans[j].second] ) )
notTre.push_back( j );
}
for( const int &v : subtre[u] ) vis[v] = false;
int num = notTre.size();
for( int lst = 0 ; lst < num ; ) {
if( ! Query( u, tre + Slice( notTre, lst, num ) ) ) break;
int l = lst, r = num - 1, mid;
while( l < r ) {
mid = ( l + r ) >> 1;
if( Query( u, tre + Slice( notTre, lst, mid + 1 ) ) ) r = mid;
else l = mid + 1;
}
int cur = notTre[l];
( ~ ans[cur].first ? ans[cur].second : ans[cur].first ) = u;
lst = l + 1;
}
}
rep( i, 0, m - 1 )
if( ans[i].second == -1 ) {
int u = ans[i].first;
std :: vector<int> anceNod, anceEdg;
for( int x = u ; x ; x = fath[x] )
anceNod.push_back( x ), anceEdg.push_back( faE[x] );
anceNod.push_back( 0 );
int num = anceEdg.size(), l = 0, r = num - 1, mid;
while( l < r ) {
mid = ( l + r + 1 ) >> 1;
if( Query( u, Slice( anceEdg, mid + 1, num ) + ( std :: vector<int> ) { i } ) ) l = mid;
else r = mid - 1;
}
ans[i].second = anceNod[l + 1];
}
}
inline std :: vector<std :: pair<int, int> >
Solve( const int &inputN, const int &inputM ) {
n = inputN, m = inputM;
rep( i, 0, m - 1 ) ans[i] = { -1, -1 };
BuildTree();
rep( i, 1, n - 1 )
for( int u = fath[i] ; u ; u = fath[u] )
subtre[u].push_back( i );
FindRemaining();
return std :: vector<std :: pair<int, int> > ( ans, ans + m );
}
#undef rep
#undef per
}
std :: vector<std :: pair<int, int> > solve( int n, int m ) {
return Workplace :: Solve( n, m );
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!
2020-05-14 min_25 筛入门
2020-05-14 [湖南集训]更为厉害/谈笑风生