Live2D

Solution -「UR #21」「UOJ #632」挑战最大团

Description

  Link.

  对于简单无向图 G=(V,E),定义它是“优美”的,当且仅当

{a,b,c,d}V,((a,b),(b,c),(c,d)E)(a,c)E(b,d)E(a,d)E

  给定一个“优美”的简单无向图 G,对于所有 i[1,n],求有多少个 SV|S|=iS 的导出子图是完全图。

Solution

  直接做铁铁的 NPC,必须从“优美”的性质入手分析。

  性质一,G 的直径不超过 2。由定义显然。

  性质二,G 的补图为 G¯,则 GG¯ 不同时连通。证明如下:

  归纳,当点集 |V|=2 时,显然成立。若当 |V|<n 时命题成立,并假设当 |V|=n 时命题成立:

  取任意 uV,由于命题不成立,故在 G 或是 G¯ 中,u 都至少有一条连入点集 V{u} 的边。设点集 V{u} 的导出子图为 G,补图为 G¯,由于 |V|{u}=n1,由归纳,GG¯ 不同时连通。

  不妨设 G 不连通,则令其极大连通分量为 G1,G2,,Gk (k>1)。由于 uG¯ 中右边,所以必然存在 tGi,使得 (u,t)E。接着研究这个 Gi,设其点集为 Vi,并令 S={vVi | (u,v)E},T=ViS。由于假设有 G 连通,所以 |S|,|T|>0。任取 vS,因为 Gi 连通,故存在 wT,使得 (v,w)E,最后,再取任意 xGj (ji) 使得 (u,x)E。考虑点集 {x,u,v,w}

  • (x,u),(u,v),(v,w)E
  • (x,v),(u,w),(x,w)E

故该图不合条件,假设不成立,原命题成立。

  利用“不连通”带来的子问题结构,我们分别考虑原问题在原图与补图的体现:

  • 对于原图,假设其由 G1,G2,,Gk (k>1) 组成,设 F(G) 为图 G 的答案 OGF,显然有

    F(G)=i=1kF(Gi)

  • 对于补图,假设其由 G1¯,G2¯,,Gk¯ (k>1) 组成,设 F¯(G¯) 为图 G¯ 中独立集数量关于其大小的 OGF,就有

    F¯(G¯)=i=1kF¯(Gi¯)

    而显然 F(G)=F¯(G¯),所以我们确实可以分治求解。

  但是,本题的难点在于构造分治结构,即确定当前层是按原图的连通块划分还是补图的连通块划分。一种优秀的策略是:选择 G 中度数最小的点 x 和度数最大(即 G¯ 中度数最小)的点 y,以 x 为源点在 G 中,y 为源点在 G¯ 中分别 BFS:先扩展一层,然后交替扩展第二层(最多两层)直到能够判断某点所在连通块 是/不是 整个图。此后,利用不连通的一个点 BFS 得到的标记数组进行连通块划分,递归建图。

  考虑复杂度,瓶颈在于第二层搜索,设 x,y 的度数大小分别是 dx,dy,复杂度上界即是 O(dxm+dym),其中 m 为当前图的点集大小。记被分离的连通块大小为 s,那么这一上界必然不超过 O(sm)。为方便复杂度分析,设留下部分大小为 t,有 st,故 sm=s2+st=O(st)。考虑分治树所描述的事件:初始时有 n 个点,每次将剩下的点分为 x,y 两部分,代价为 xy,再递归处理 xy。则树上每两片叶子在 LCA 处贡献复杂度一次,总复杂度 O(n2)

Code

/* Clearink */

#include <cstdio>
#include <vector>

#define rep( i, l, r ) for ( int i = l, rep##i = r; i <= rep##i; ++i )
#define per( i, r, l ) for ( int i = r, per##i = l; i >= per##i; --i )

typedef std::pair<int, int> PII; 
#define fi first
#define se second
#define getv( c ) ( '0' <= c && c <= '9' ? c ^ '0' : 10 + c - 'A' )

const int MAXN = 8e3, MOD = 998244353;
int n, node;
bool e[MAXN + 5][MAXN + 5], side[MAXN * 2 + 5];
std::vector<int> son[MAXN * 2 + 5];

inline int imin( const int a, const int b ) { return a < b ? a : b; }
inline int imax( const int a, const int b ) { return a < b ? b : a; }
inline void addeq( int& a, const int b ) { ( a += b ) >= MOD && ( a -= MOD ); }
inline int mul( const long long a, const int b ) { return int( a * b % MOD ); }

inline int build( const std::vector<PII>& vec, const bool curs ) {
	int o = ++node;
	if ( vec.size() == 1 ) return side[o] = curs, o;
	
	static int vis[2][MAXN + 5], que[2][MAXN + 5];
	
	int n = int( vec.size() ), s[2] = {};
	rep ( i, 1, n - 1 ) {
		if ( vec[i].se < vec[s[0]].se ) s[0] = i;
		if ( vec[i].se > vec[s[1]].se ) s[1] = i;
	}
	s[0] = vec[s[0]].fi, s[1] = vec[s[1]].fi;
	vis[0][s[0]] = vis[1][s[1]] = true;
	
	int sz[2] = {};
	rep ( sd, 0, 1 ) for ( PII u: vec ) {
		if ( !vis[sd][u.fi] && e[s[sd]][u.fi] ^ curs ^ sd ) {
			vis[sd][que[sd][++sz[sd]] = u.fi] = true;
		}
	}
	
	int ex[2] = { sz[0], sz[1] };
	bool divs;
	rep ( i, 0, imax( sz[0], sz[1] ) ) {
		if ( i ) rep ( sd, 0, 1 ) if ( i <= sz[sd] ) {
			int u = que[sd][i];
			for ( PII v: vec ) {
				if ( !vis[sd][v.fi] && e[u][v.fi] ^ curs ^ sd ) {
					vis[sd][que[sd][++ex[sd]] = v.fi] = true;
				}
			}
		}
		
		if ( i == imin( sz[0], sz[1] ) ) {
			if ( ex[0] == n ) divs = curs;
			else if ( ex[1] == n ) divs = !curs;
			else { divs = curs ^ ( sz[0] < sz[1] ); break; }
		}
	}
	
	side[o] = divs;
	std::vector<PII> sub[2];
	if ( divs == curs ) {
		for ( PII u: vec ) {
			sub[!vis[1][u.fi]].push_back( { u.fi, n - u.se - 1 } );
			vis[0][u.fi] = vis[1][u.fi] = false;
		}
		for ( PII& u: sub[0] ) for ( PII& v: sub[1] ) {
			if ( e[u.fi][v.fi] ^ curs ^ 1 ) {
				--u.se, --v.se;
			}
		}
	} else {
		for ( PII u: vec ) {
			sub[!vis[0][u.fi]].push_back( u );
			vis[0][u.fi] = vis[1][u.fi] = false;
		}
	}
	int lc = build( sub[0], !divs ), rc = build( sub[1], !divs );
	if ( side[rc] == divs ) son[o] = son[rc];
	else son[o].push_back( rc );
	son[o].push_back( lc );
	return o;
}

int f[MAXN * 2 + 5][MAXN + 5], siz[MAXN + 5];

inline void solve( const int u ) {
	static int tmp[MAXN + 5];
	
	if ( son[u].empty() ) return void( f[u][0] = f[u][1] = siz[u] = 1 );
	
	f[u][0] = 1;
	for ( int v: son[u] ) {
		solve( v );
		if ( !side[u] ) { // IG merge.
			rep ( i, 0, siz[u] ) rep ( j, 0, siz[v] ) {
				addeq( tmp[i + j], mul( f[u][i], f[v][j] ) );
			}
			rep ( i, 0, siz[u] += siz[v] ) f[u][i] = tmp[i], tmp[i] = 0;
		} else { // G merge.
			rep ( i, 1, siz[u] = imax( siz[u], siz[v] ) ) {
				addeq( f[u][i], f[v][i] );
			}
		}
	}
}

int main() {
	scanf( "%d", &n );
	rep ( i, 1, n - 1 ) {
		static char str[MAXN + 5];
		scanf( "%s", str );
		rep ( j, 0, n - i - 1 ) {
			e[i][i + j + 1] = e[i + j + 1][i] =
				getv( str[j >> 2] ) >> ( j & 3 ) & 1;
		}
	}
	std::vector<PII> oriv; oriv.resize( n );
	rep ( i, 1, n ) {
		int d = 0;
		rep ( j, 1, n ) d += e[i][j];
		oriv[i - 1] = { i, d };
	}

	int rt = build( oriv, false );
	solve( rt );
	
	rep ( i, 1, n ) printf( "%d%c", f[rt][i], i < n ? ' ' : '\n' );
	return 0;
}
posted @   Rainybunny  阅读(150)  评论(0编辑  收藏  举报
编辑推荐:
· AI与.NET技术实操系列:基于图像分类模型对图像进行分类
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 25岁的心里话
· 按钮权限的设计及实现
点击右上角即可分享
微信分享提示