首页 > 技术文章 > 「UOJ461」新年的 dog 划分

crashed 2022-05-23 11:33 原文

题目

点这里看题目。

分析

假如已知原图是一个二分图,能不能确定原图的一部?

注意到,在二分图中,如果我们可以给出它的一棵生成树,则即可黑白染色并找出一部。那么,问题就归结到了“如何生成一棵生成树”。

由于生成树是边极少且保证连通的边子集,因此我们可以尽量删边。一旦删了边之后图会不连通,就说明这条边在生成树上。这样我们即可找出一棵生成树。

暴力做的询问次数为 \(\binom{n}{2}\)。找下一条树边的过程显然可以二分(将所有可能的边拉出来排成一个序列),于是优化到了 \((n-1)\log_2\binom{n}{2}< 2n\log_2n\),还是不够优秀。

题解给出的做法为 BFS 过程中执行二分过程,从而去掉了 2 的常数,不过说实话没看懂。尝试对于这个序列进行分块,块长为 \(T\)由于 \(n-1\) 条边分布在 \(\binom{n}{2}\) 条边中非常的稀疏,则我们可以预先检查某一块内部有没有树边,有的话再进行二分,询问次数降到了 \(\frac{\binom{n}{2}}{T}+(n-1)\log_2T<\frac{n^2}{T}+n\log_2T\)

假设 \(T=n^x\),则相当于求 \(\min_{x\in[0,2]}n^{2-x}+xn\log_2n\)。算一下大概可以得到在 \(x=1+\frac{\ln\ln 2}{\ln n}\) 时取到最小值。

接下来,怎么检查是不是二分图?即便不是二分图,我们也可以得到一棵生成树。对于整个图进行 BFS 分层,则二分图只会存在奇偶性不同的层之间的边,树边也包括在其中。所以,我们可以只保留跨奇偶的边中的树边,再尝试删除每一条树边并检查连通性。可以发现,原图是二分图,当且仅当每次删掉一条树边后,图都不再连通

小结:

  1. 本题中对于二分图的处理都值得学习。包括“用生成树定一部”、“删树边验证二分性质”等。

  2. 在稀疏序列中分块优化查找元素的方法,很适合用在“容易询问某一个集合是否包含至少一个指定元素”的情景之下。

代码

#include "graph.h"
#include <vector>

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 = 205;

	bool onTre[MAXN][MAXN];

	int dep[MAXN], q[MAXN];

	std :: vector<int> ans;
	std :: vector<int> neigh[MAXN];

	std :: vector<std :: pair<int, int> > tmp, alr;
	
	void AddE( const int &from, const int &to ) {
		neigh[from].push_back( to );
		neigh[to].push_back( from );
	}

	std :: vector<int> Work( const int &n ) {
		rep( i, 0, n - 2 ) {
			int lim = i + 1;
			while( lim <= n - 1 ) {
				tmp = alr;
				rep( j, lim, n - 1 )
					tmp.push_back( { i, j } );
				if( query( tmp ) ) break;
				int l = lim, r = n - 1, mid;
				while( l < r ) {
					tmp = alr;
					mid = ( l + r ) >> 1;
					rep( j, lim, mid )
						tmp.push_back( { i, j } );
					if( query( tmp ) ) l = mid + 1;
					else r = mid;
				}
				AddE( i, l );
				rep( j, lim, l - 1 )
					alr.push_back( { i, j } );
				lim = l + 1;
			}
		}
		rep( i, 0, n - 1 ) dep[i] = -1;
		int h = 1, t = 0;
		dep[q[++ t] = 0] = 0;
		while( h <= t ) {
			int u = q[h ++];
			for( const int &v : neigh[u] )
				if( dep[v] == -1 ) dep[q[++ t] = v] = dep[u] + 1;
		}
		rep( u, 0, n - 1 )
			for( const int &v : neigh[u] )
				onTre[u][v] = onTre[v][u] = true;
		alr.clear();
		rep( i, 0, n - 1 )
			rep( j, i + 1, n - 1 )
				if( ( dep[i] & 1 ) != ( dep[j] & 1 )
				    && ! onTre[i][j] ) alr.push_back( { i, j } );
		rep( i, 0, n - 1 )
			rep( j, i + 1, n - 1 )
				if( onTre[i][j] ) {
					tmp = alr, tmp.push_back( { i, j } );
					if( query( tmp ) ) return std :: vector<int> ();
				}
		ans.clear();
		rep( i, 0, n - 1 )
			if( dep[i] & 1 )
				ans.push_back( i );
		return ans;
	}
}

std :: vector<int> check_bipartite( int n ) {
	return Studio :: Work( n );
}

推荐阅读