1. 程式人生 > 實用技巧 >Solution -「CF 1370F2」The Hidden Pair (Hard Version)

Solution -「CF 1370F2」The Hidden Pair (Hard Version)

\(\mathcal{Description}\)

  Link (hard) & Link (easy).

  這是一道互動題。

  給定一棵 \(n\) 個結點的樹,其中有兩個是特殊結點。每次你可以提出形如 \(x~c_1~c_2~\cdots~c_x\) 的詢問,互動器會回答在點集 \(\{c_x\}\) 中,到兩個特殊結點距離之和最小的結點 \(p\) 和這個最小距離和 \(d\)(若有多個 \(d\),回答任意一個)。你需要猜出兩個特殊結點的編號。

  \(n\le10^3\)\(T\le10\) 組資料,詢問次數上限為 \(11\) 次。

\(\mathcal{Solution}\)

  第一次詢問,顯然問所有的 \(n\) 個點,就能得到特殊點 \(x,y\) 間的距離 \(d\)\(x,y\) 路徑上的一個結點 \(p\)

  以 \(p\) 為根,\(x,y\) 顯然在 \(p\) 的兩棵子樹內。接下來,以 \(p\) 為圓心“畫圓”——詢問所有到 \(p\) 的距離為某一定值 \(l\) 的點集,就能得到一個新的距離 \(d'\)。若 \(d'=d\),說明 \(x,y\)\(p\) 較遠的一個點到 \(p\) 的距離 \(\ge d'\),否則,就 \(<d'\),所以可以二分 \(l\),花 \(\mathcal O(\log n)\) 次詢問找到離 \(p\)

較遠的那個特殊點。

  最後,設較遠點 \(x\)\(p\) 的距離為 \(l\),把到 \(p\) 距離為 \(d-l\) 的所有點拿出來再問一次就得到另一個特殊點 \(y\) 了(注意排除掉在 \(p\)\(x\) 路徑上的點)。

  但是,最壞情況會有 \(1+\lceil\log_210^5\rceil+1=12\) 次詢問,剛好多一次 qwq。

  不過二分找到是“較遠點”,所以其到 \(p\) 的距離一定在 \([\lfloor\frac{d}2\rfloor,d]\) 之間,取這個區間作為二分上下界。其大小顯然不超過 \(\frac{n}2\),所以剛好能節約一次二分的詢問。

  複雜度 \(\mathcal O(Tn)\)

\(\mathcal{Code}\)

/* Clearink */

#include <cstdio>
#include <vector>
#include <assert.h>

inline int rint () {
	int x = 0, f = 1; char s = getchar ();
	for ( ; s < '0' || '9' < s; s = getchar () ) f = s == '-' ? -f : f;
	for ( ; '0' <= s && s <= '9'; s = getchar () ) x = x * 10 + ( s ^ '0' );
	return x * f;
}

const int MAXN = 1000;
int n, ecnt, mxd, head[MAXN + 5], fa[MAXN + 5], dep[MAXN + 5];
std::vector<int> all, eqdis[MAXN + 5];
bool ban[MAXN + 5];

struct Edge { int to, nxt; } graph[MAXN * 2 + 5];

inline void link ( const int s, const int t ) {
	graph[++ ecnt] = { t, head[s] };
	head[s] = ecnt;
}

inline void collect ( const int u, const int d ) {
	eqdis[dep[u] = d].push_back ( u ), mxd = d < mxd ? mxd : d;
	for ( int i = head[u], v; i; i = graph[i].nxt ) {
		if ( ( v = graph[i].to ) ^ fa[u] ) {
			fa[v] = u, collect ( v, d + 1 );
		}
	}
}

inline void inter ( const std::vector<int>& pts, int& p, int& dis ) {
	if ( pts.empty () ) return void ( p = dis = 0 );
	printf ( "? %d", ( int ) pts.size () );
	for ( int u: pts ) printf ( " %d", u );
	putchar ( '\n' ), fflush ( stdout );
	assert ( ~( p = rint (), dis = rint () ) );
}

inline void clear () {
	ecnt = mxd = 0, all.clear (), eqdis[0].clear ();
	for ( int i = 1; i <= n; ++ i ) {
		head[i] = dep[i] = fa[i] = ban[i] = 0;
		eqdis[i].clear ();
	}
}

int main () {
	char rep[20];
	for ( int T = rint (); T --; ) {
		clear ();
		n = rint ();
		for ( int i = 1, u, v; i < n; ++ i ) {
			all.push_back ( i );
			u = rint (), v = rint ();
			link ( u, v ), link ( v, u );
		}
		int p, dis;
		all.push_back ( n ), inter ( all, p, dis );
		collect ( p, 0 );
		int l = dis + 1 >> 1, r = mxd < dis ? mxd : dis, S = 0;
		while ( l < r ) {
			int mid = l + r + 1 >> 1, curp, curd;
			inter ( eqdis[mid], curp, curd );
			if ( curd > dis ) r = mid - 1;
			else S = curp, l = mid;
		}
		if ( !S ) inter ( eqdis[l], S, r );
		int oth = dis - dep[S], Q, tmp;
		for ( int u = S; u ^ p; u = fa[u] ) ban[u] = true;
		for ( auto it ( eqdis[oth].begin () ); it != eqdis[oth].end (); ++ it ) {
			if ( ban[*it] ) {
				eqdis[oth].erase ( it );
				break;
			}
		}
		inter ( eqdis[oth], Q, tmp );
		printf ( "! %d %d\n", S, Q ), fflush ( stdout );
		scanf ( "%s", rep ), assert ( rep[0] == 'C' );
	}
	return 0;
}

\(\mathcal{Details}\)

  一眼出 \(12\) 次詢問的方法然後卡了半天 qwq……養成卡二分上下界的習慣對常數有極大好處。(