「CF923F」Public Service
题目
点这里看题目。
分析
VK Cup 的题都不是很简单 qwq。
假设题目中的两棵树,一棵叫 \(T_1\),另一棵叫 \(T_2\)。
看到这么复杂的问题,能想到的其实就是从简单情况入手考虑。
先考虑无解的情况——显然如果 \(T_1\) 是菊花或者 \(T_2\) 是菊花,则不可能有方案。顺着这条路下去更容易想到情况就是——如果 \(T_1\) 不是菊花且 \(T_2\) 不是菊花,是不是就一定有方案?
手玩一下,不难发现 \(T_1\) 或 \(T_2\) 中的最大度数越大,则这个问题越容易。最大度数为 \(n-1\) 时,这个问题太容易了,以至于我们可以直接断言它没有解;而最大度数为 \(n-2\) 的时候,构造方法也不复杂。
不妨假设 \(T_1\) 中存在一个度数为 \(n-2\) 的点,记为 \(u\)。那么 \(T_1\) 的形态几乎就是一朵菊花,只不过有且仅有一个点连在了菊花的一个花瓣上(而不是 \(u\) 上)。我们记这个例外的点为 \(v\)。显然 \(v\) 是叶子,所以我们记 \(v\) 的邻接点为 \(w\)。
由于 \(u\) 的度数为 \(n-2\),所以它在 \(T_2\) 中的对应点 \(P_u\) 的度数只能为 1。于是我们可以从 \(T_2\) 里面拿出一个叶子 \(u'\) 来,并且记 \(u'\) 在 \(T_2\) 的邻接点为 \(v'\)。显然,如果 \(P_u=u'\),则 \(v'\) 只能对应 \(v\)。
到目前,这些推断都很自然。下面我们来策略性地思考一下:设 \(T_1\) 的点集为 \(V_1\),则由于 \(T_1\) 的结构,实际上 \(V_1\setminus\{u,v,w\}\) 里面的点的在 \(V_2\) 中的对应点,内部连边是不受限制的。考虑到这一点,我们只需要给 \(w\) 找一个 \(P_w\),那么剩下的问题就变得非常容易了。这很简单,我们只需要从 \(T_2\) 中找到一个 \(w'\),保证 \(w'\) 不和 \(v'\) 相连即可。这样一定可行,因为 \(T_2\) 不是一朵菊花。令 \(P_w=w'\),这样我们就基本上完成了整个构造过程。
考虑完了简单的情况,一种思路就出来了——能不能想办法把一般性的问题都归约到上面的简单情况去?
答案是肯定的,但是是题解告诉我的。
容易想到的归纳过程是,我们钦定 \(T_1\) 中的 \(u\) 和 \(T_2\) 中的 \(v\) 进行匹配,并考虑去掉 \(u\) 的 \(T_1\) 的诱导子图和去掉 \(v\) 的 \(T_2\) 的诱导子图之间的匹配,最后加上 \(u,v\) 之间的匹配,完成证明。
当然,这个朴素的过程实际上和暴力枚举匹配没有任何区别,并且对于 \(u,v\) 的选择必然有极强的限制。认识到这一点之后,我们发现这个做法必然不适合证明或实现,所以舍弃它。
说简单一点,我们设 \(T_1=(V_1,E_1),T_2=(V_2,E_2)\),并且 \(T_1\) 和 \(T_2\) 中的最大度数不超过 \(|V_1|-3\)。设 \(G(V')\) 表示由 \(V'\subseteq V\) 在 \(G\) 上生成的一个诱导子图。
归纳法想要干的事情,实际上是取 \(S_1\subseteq V_1,S_2\subseteq V_2,|S_1|=|S_2|\),然后构造 \(T_1(V_1\setminus S_1)\) 和 \(T_2(V_2\setminus S_2)\) 的映射,最后把 \(S_1\) 和 \(S_2\) 的映射凑到先前的映射上。
所以可以发现,当 \(|S_1|\) 越大,则 \(S_1,S_2\) 的选择越少,但是 \(S_1,S_2\) 的映射方式更多。\(S_1,S_2\) 映射方式多,则说明透过这种拆分方式获得合法映射的概率越大,也即对于 \(S_1,S_2\) 的选择的限制越少。考虑到我们希望 \(S_1,S_2\) 最好是随意选择的,因此我们不妨从小到大考虑 \(|S_1|\)。
考虑 \(|S_1|=|S_2|=2\) 的情况。为了保证结构简单,我们要求 \(T_1(V_1\setminus S_1)\) 和 \(T_2(V_2\setminus S_2)\) 都是树,也即 \(S_1,S_2\) 只包含了叶子。此外,如果 \(S_1\) 或 \(S_2\) 中的两个叶子拥有共享一个邻接点,那么当我们尝试改变映射方式时,我们却改变不了限制,这自然是糟糕的。因此,我们要求 \(S_1\) 和 \(S_2\) 中的两个点的邻接点不相同。
接着,按照图的大小进行归纳,我们看一下可以得到什么。根据归纳假设,我们知道 \(T_1(V_1\setminus S_1)\) 和 \(T_2(V_2\setminus S_2)\) 可以得到一个原始排列 \(P'\),我们需要扩充它。
不妨设 \(S_1=\{x,y\}\),\(S_2=\{x',y'\}\),设 \(x\) 的邻接点为 \(x^\circ\),\(y\) 的为 \(y^\circ\)。如果我们令 \(x\rightarrow x',y\rightarrow y'\),则有可能出现 \((x',P_{x^\circ})\in E_2\) 或 \((y',P_{y^\circ})\in E_2\) 的情况。但是在这种情况下,我们取另外一种映射方式,也就是 \(x\rightarrow y',y\rightarrow x'\)——以 \((x',P_{x^\circ})\in E_2\) 为例,如果在这个情况下还有 \((y',P_{x^\circ})\in E_2\),则不满足 \(x',y'\) 的选择条件;否则,如果 \((x',P_{y^\circ})\),则 \(x'\) 就不是叶子了;当 \((y',P_{y^\circ})\in E_2\) 时讨论类似——所以我们可以说明,\(S_1,S_2\) 的两种映射方式中必有一种成立,构造过程也就完成了。
现在只要我们可以选出满足结构的 \(S_1,S_2\) 即可完成归纳。不过可以证明,当 \(|V|>5\) 的时候必然有满足条件的 \(S\),而 \(|V|\le 5\) 的时候我们直接枚举所有映射方式即可。
补充说明:在 \(|V|>5\) 的时候,如果树上的最大度数为 \(|V|-3\),则我们必须取一个和度数最大的点相邻的叶子,避免到时候诱导子图变成了菊花。
通过归纳方式,我们成功找到了一种构造方法。在实现过程中,我们需要找到相邻点不同的两个叶子,这个可以通过维护一个点的邻接叶子集和邻接点中有叶子的点的集合快速做到。使用 set
即可获得 \(O(n\log n)\) 的优秀算法。
小结:
- 从比较简单的情况入手去思考问题;
- 从归纳的角度去规约问题,这个想法很常用;
- 感性的初步分析可以确定之后的方向,这个其实是一个偏哲学的论题了。
- 这题中维护叶子的方法挺有意思的。
代码
#include <set>
#include <cstdio>
#include <utility>
#include <algorithm>
#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 = 10005;
template<typename _T>
void read( _T &x ) {
x = 0; char s = getchar(); bool f = false;
while( s < '0' || '9' < s ) { f = s == '-', s = getchar(); }
while( '0' <= s && s <= '9' ) { x = ( x << 3 ) + ( x << 1 ) + ( s - '0' ), s = getchar(); }
if( f ) x = -x;
}
template<typename _T>
void write( _T x ) {
if( x < 0 ) putchar( '-' ), x = -x;
if( 9 < x ) write( x / 10 );
putchar( x % 10 + '0' );
}
template<typename _T>
_T MAX( const _T a, const _T b ) {
return a > b ? a : b;
}
struct MyGraph {
std :: set<int> nei[MAXN], exi;
std :: set<int> leaf[MAXN], nearLeaf;
std :: set<std :: pair<int, int> > mxDeg;
int deg[MAXN];
int n;
MyGraph(): deg{}, n( 0 ) {}
void AddE( const int from, const int to ) {
nei[from].insert( to ), deg[from] ++;
nei[to].insert( from ), deg[to] ++;
}
void Init( const int N ) {
exi.clear(), n = N;
rep( i, 1, n ) {
exi.insert( i );
nei[i].clear(), leaf[i].clear();
}
}
void Prepare() {
mxDeg.clear(), nearLeaf.clear();
rep( i, 1, n ) {
mxDeg.insert( { deg[i], i } );
for( const int& v : nei[i] )
if( deg[v] == 1 ) leaf[i].insert( v );
if( ! leaf[i].empty() ) nearLeaf.insert( i );
}
}
void Remove( const int x ) {
if( exi.find( x ) == exi.end() ) return ;
exi.erase( x );
for( const int& v : nei[x] ) {
mxDeg.erase( { deg[v], v } );
nei[v].erase( x ), deg[v] --;
mxDeg.insert( { deg[v], v } );
if( deg[v] == 1 )
// let v be a new leaf
for( const int& w : nei[v] ) {
// can visit x no more
leaf[w].insert( v );
if( leaf[w].size() == 1u )
nearLeaf.insert( w );
}
}
if( deg[x] == 1 ) {
// remove x that used to be a leaf
int v = * nei[x].begin();
leaf[v].erase( x );
if( leaf[v].empty() )
nearLeaf.erase( v );
}
}
int Size() const { return exi.size(); }
inline int GetStarness() const {
return Size() - mxDeg.rbegin() -> first - 1;
}
std :: pair<int, int> GetPair() const {
int cen = mxDeg.rbegin() -> second;
if( GetStarness() == 2 )
return std :: make_pair( * leaf[cen].begin(),
* leaf[* nearLeaf.begin() == cen ? * nearLeaf.rbegin() : * nearLeaf.begin()].begin() );
return std :: make_pair( * leaf[* nearLeaf.begin()].begin(),
* leaf[* nearLeaf.rbegin()].begin() );
}
};
MyGraph T1, T2;
bool g1[10][10], g2[10][10];
int q1[10], q2[10], tmp[10];
int ref1[MAXN], ref2[MAXN];
int ans[MAXN];
int N;
void Construct() {
if( T1.Size() <= 5 ) {
int t1 = 0, t2 = 0, n = T1.Size();
rep( i, 1, n ) rep( j, 1, n ) g1[i][j] = g2[i][j] = false;
for( const int& u : T1.exi ) q1[ref1[u] = ++ t1] = u;
for( const int& u : T2.exi ) q2[ref2[u] = ++ t2] = u;
for( const int& u : T1.exi )
for( const int& v : T1.nei[u] )
g1[ref1[u]][ref1[v]] = true;
for( const int& u : T2.exi )
for( const int &v : T2.nei[u] )
g2[ref2[u]][ref2[v]] = true;
rep( i, 1, n ) tmp[i] = i;
do {
bool flg = true;
for( int i = 1 ; i <= n && flg ; i ++ )
for( int j = 1 ; j <= n && flg ; j ++ )
if( ( g1[i][j] || g1[j][i] ) && ( g2[tmp[i]][tmp[j]] || g2[tmp[j]][tmp[i]] ) )
flg = false;
if( flg ) break;
} while( std :: next_permutation( tmp + 1, tmp + 1 + n ) );
rep( i, 1, n ) ans[q1[i]] = q2[tmp[i]];
return ;
}
bool swp = false;
if( T2.GetStarness() == 1 )
std :: swap( T1, T2 ), swp = true;
if( T1.GetStarness() == 1 ) {
// | /
// u - w - v
int u = T1.mxDeg.rbegin() -> second;
int v = * T1.leaf[* T1.nearLeaf.begin() == u ?
* T1.nearLeaf.rbegin() : * T1.nearLeaf.begin()].begin();
int w = * T1.nei[v].begin();
int u_ = * T2.leaf[* T2.nearLeaf.begin()].begin();
int v_ = * T2.nei[u_].begin(), w_;
for( const int& x : T2.exi )
if( T2.nei[v_].find( x ) == T2.nei[v_].end() && v_ ^ x )
{ w_ = x; break; }
if( ! swp ) ans[u] = u_, ans[v] = v_, ans[w] = w_;
else ans[u_] = u, ans[v_] = v, ans[w_] = w;
for( std :: set<int> :: const_iterator
p1 = T1.exi.begin(), p2 = T2.exi.begin() ;
p1 != T1.exi.end() && p2 != T2.exi.end() ;
p1 ++, p2 ++ ) {
while( * p1 == u || * p1 == v || * p1 == w ) p1 ++;
while( * p2 == u_ || * p2 == v_ || * p2 == w_ ) p2 ++;
if( ! swp ) ans[* p1] = * p2;
else ans[* p2] = * p1;
}
if( swp ) std :: swap( T1, T2 );
} else {
std :: pair<int, int>
p1 = T1.GetPair(),
p2 = T2.GetPair();
T1.Remove( p1.first ), T1.Remove( p1.second );
T2.Remove( p2.first ), T2.Remove( p2.second );
Construct();
int npu = * T1.nei[p1.first].begin(),
npv = * T1.nei[p1.second].begin(),
npu_ = * T2.nei[p2.first].begin(),
npv_ = * T2.nei[p2.second].begin();
if( ans[npu] == npu_ || ans[npv] == npv_ )
ans[p1.first] = p2.second, ans[p1.second] = p2.first;
else ans[p1.first] = p2.first, ans[p1.second] = p2.second;
}
}
int main() {
read( N );
T1.Init( N ), T2.Init( N );
rep( i, 1, N - 1 ) {
int u, v;
read( u ), read( v );
T1.AddE( u, v );
}
rep( i, 1, N - 1 ) {
int u, v;
read( u ), read( v );
T2.AddE( u - N, v - N );
}
T1.Prepare(), T2.Prepare();
if( T1.GetStarness() == 0 || T2.GetStarness() == 0 )
return puts( "No" ), 0;
puts( "Yes" );
Construct();
rep( i, 1, N )
write( ans[i] + N ), putchar( i == N ? '\n' : ' ' );
return 0;
}