题目
点这里看题目。
分析
交互题好难啊.jpg
我们先来分析一下怎么才能找出来一条边。假如编号为 \(k\) 的边被加入到 \(G\) 中的询问集合为 \(Q_k\),则询问必须满足对于任意的存在公共点的 \(e_1,e_2\),\(Q_{e_1},Q_{e_2}\) 之间不存在包含关系。否则我们无法准确地确定边的端点。
但是,我们并不知道哪些边存在公共点,所以对于任意两条边,它们的 \(Q\) 都必须不同。
这个构造还是比较经典的。第一种方法是二进制分组,需要 \(2\lceil\log_2(n-1)\rceil\) 次询问;第二种方法是重标号(所有的标号所含的 \(1\) 的位数相同)之后二进制分组,需要的询问次数略大于 \(\log_2(n-1)\)。
Remark.
如果分析不出来这里的性质,该怎么想到二进制分组?
数据范围明示 \(\log n\) 次询问,能够达到这个询问次数的策略不多,其实就只有:二分搜索、分治、随机化折半、二进制分组等等。全部试一遍,只要不搞忘就不怕想不到 。
有了询问的构造之后(第二种构造方法),如何推理出边?暴力的方法是,枚举所有边 \((u,v)\)。假如新的标号总位数为偶数 \(m\),则 \((u,v)\) 在同一条边中当且仅当在恰好 \(\frac m 2\) 次询问中 \(u,v\) 同属一个连通块。这样复杂度是 \(O(n^2\log n)\) 的。
怎么加速这个过程?我们选择树上的一些特殊的结点入手,在这个“删边连通性”的条件下,我们选择讨论叶子。
如果一个点是叶子,则它必然在恰好 \(\frac m 2\) 次询问中是孤立点。这样我们可以逐个剥叶子直到树变为空。反过来,我们逐个加入叶子,并确定叶子连出的边的另一个端点。如果我们确定最后被删的结点为根,与叶子相邻的结点为 \(w\),则必然存在一个连通块,其包含了 \(w\) 却不包含 \(w\) 的父亲。在这样一个连通块中,\(w\) 作为最浅的结点存在。因此,我们取这 \(\frac m 2\) 个连通块的最浅的结点中最深的那一个,就找到了 \(w\)。
最终可以实现一个 \(O(n\log n)\) 的算法。
代码
#include <vector>
#include <algorithm>
#include "tree.h"
namespace Studio {
#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 = 2e5 + 5, MAXLOG = 25;
typedef std :: pair<int, int> Edge;
typedef std :: vector<Edge> RetType;
typedef std :: vector<int> Component;
typedef std :: vector<Component> QueryType;
int C[21][21];
int leaf[MAXN], h, t;
std :: vector<int> qry;
QueryType cpnt[MAXLOG];
RetType ans;
int seq[MAXN], tot;
int dep[MAXN];
int hgst[MAXLOG][MAXN];
int rem[MAXLOG][MAXN];
int col[MAXLOG][MAXN];
bool tkn[MAXN];
int n, m;
inline bool ChkLeaf( const int &u ) {
int sgl = 0;
rep( i, 0, m - 1 )
sgl += rem[i][col[i][u]] == 1;
return sgl == ( m >> 1 );
}
inline void Remove( const int &u ) {
rep( i, 0, m - 1 )
if( -- rem[i][col[i][u]] == 1 ) {
int c = col[i][u];
for( const int &x : cpnt[i][c] )
if( ! tkn[x] ) {
if( ChkLeaf( x ) )
leaf[++ t] = x;
break;
}
}
}
inline void Insert( const int &u ) {
rep( i, 0, m - 1 ) {
int c = col[i][u];
if( hgst[i][c] == -1 || dep[hgst[i][c]] > dep[u] )
hgst[i][c] = u;
rem[i][c] ++;
}
}
inline int FindFath( const int &u ) {
int f = -1, c;
rep( i, 0, m - 1 )
if( rem[i][c = col[i][u]] )
if( f == -1 || dep[f] < dep[hgst[i][c]] )
f = hgst[i][c];
return f;
}
RetType Solve( const int &N ) {
// n = N, m = n <= 2000 ? 14 : 20;
n = N;
rep( i, 0, 20 ) {
C[i][0] = C[i][i] = 1;
rep( j, 1, i - 1 )
C[i][j] = C[i - 1][j] + C[i - 1][j - 1];
}
rep( i, 0, 20 )
if( ! ( i & 1 ) && C[i][i >> 1] >= n - 1 ) {
m = i; break;
}
for( int S = 0 ; S < ( 1 << m ) ; S ++ )
if( __builtin_popcount( S ) == m / 2 )
seq[tot ++] = S;
qry.resize( n - 1 );
rep( i, 0, m - 1 ) {
rep( j, 0, n - 2 )
qry[j] = seq[j] >> i & 1;
cpnt[i] = query( qry );
int c = cpnt[i].size();
rep( j, 0, c - 1 ) {
hgst[i][j] = -1;
rem[i][j] = cpnt[i][j].size();
for( const int &x : cpnt[i][j] )
col[i][x] = j;
}
}
h = 1, t = 0;
rep( u, 0, n - 1 )
if( ChkLeaf( u ) )
leaf[++ t] = u;
while( h <= t ) {
int u = leaf[h ++];
tkn[u] = true, Remove( u );
}
dep[leaf[t]] = 0;
Insert( leaf[t] );
per( i, t - 1, 1 ) {
int u = leaf[i],
f = FindFath( u );
ans.push_back( { u, f } );
dep[u] = dep[f] + 1, Insert( u );
}
return ans;
}
}
std :: vector<std :: pair<int, int> > solve( int n ) {
return Studio :: Solve( n );
}