[ACNOI2021]数树

122 篇文章 0 订阅
98 篇文章 0 订阅

题目

题目描述
给出两棵树 T 1 T1 T1 T 2 T2 T2,求有多少个 T 1 T1 T1 的连通块与 T 2 T2 T2 同构。对于同构的定义:存在双射 f ( x ) → y    ( x ∈ S ⫅ T 1 ,    y ∈ T 2 ) f(x)\rightarrow y\;(x\in S\subseteqq T1,\;y\in T2) f(x)y(xST1,yT2) 使得边 ⟨ a , b ⟩ ∈ S \langle a,b\rangle\in S a,bS 当且仅当 ⟨ f ( a ) , f ( b ) ⟩ ∈ T 2 \langle f(a),f(b)\rangle\in T2 f(a),f(b)T2

数据范围与提示
∣ T 1 ∣ ⩽ 2 × 1 0 3 |T1|\leqslant 2\times 10^3 T12×103 ∣ T 2 ∣ ⩽ 10 |T2|\leqslant 10 T210

思路

考虑求双射 f ( x ) f(x) f(x) 的数量。很明显会重复,但是我们也别无他法。只能想想怎么去重了。

如果存在两个不同的 S S S T 2 T2 T2 的同构双射 f 1 ( x ) f_1(x) f1(x) f 2 ( x ) f_2(x) f2(x),则必然存在双射 g [ f 1 ( x ) ] → f 2 ( x ) g[f_1(x)]\rightarrow f_2(x) g[f1(x)]f2(x),这代表一个 T 2 T2 T2 T 2 T2 T2 的重构;另一方面,只要存在 T 2 T2 T2 T 2 T2 T2 的同构双射 g ( x ) g(x) g(x),和一个 S S S T 2 T2 T2 的同构双射 f 1 ( x ) f_1(x) f1(x),可以发现 f 2 ( x ) = g [ f 1 ( x ) ] f_2(x)=g[f_1(x)] f2(x)=g[f1(x)] 也是一个 S S S T 2 T2 T2 的同构双射。

于是考虑用 b u r n s i d e \tt burnside burnside 计算一下。置换数量就是 T 2 T2 T2 的自同构双射数量——显然自同构是群。而不动点数量之和呢?显然只有 f ( x ) = x f(x)=x f(x)=x 这个 T 2 T2 T2 的自同构双射,会使得所有 S S S T 2 T2 T2 的同构双射都是不动点,而其他的贡献都是 0 0 0 。所以答案是 ∣ f S → T 2 ∣ ÷ ∣ f T 2 → T 2 ∣ |f_{S\rightarrow T2}|\div|f_{T2\rightarrow T2}| fST2÷fT2T2

而求出 T 2 T2 T2 T 2 T2 T2 的同构双射数量,显然是弱于 T 1 T1 T1 连通块与 T 2 T2 T2 的同构双射数量的,故我们只讨论第二个问题。

考虑树 d p \tt dp dp,用 ω ( x , y ) \omega(x,y) ω(x,y) 表示 x x x 的子树 y y y 的子树 的双射方案。注意到这里我们已经要求它是一个有根树了——因为无根时没法进行子树 d p \tt dp dp 啊。

如果考虑换根的过程,你就会发现可能的 “子树” 其实只有 O ( m ) \mathcal O(m) O(m) 个,因为 x x x 的子树其实只有 deg ⁡ ( x ) \deg(x) deg(x) 个。再考虑到 x x x 为根的情况,也只有 3 m 3m 3m 个需要处理的子树。

那么 d p \tt dp dp 的过程就比较简单了,可以想到就是用状压,大力枚举 x x x 的子节点对应到哪个 y y y 的子节点。转移的复杂度是 deg ⁡ ( x ) ⋅ 2 deg ⁡ ( y ) ⋅ deg ⁡ ( y ) \deg(x)\cdot 2^{\deg(y)}\cdot \deg(y) deg(x)2deg(y)deg(y),最坏情况是 deg ⁡ ( y ) = m − 1 \deg(y)=m-1 deg(y)=m1 T 2 T2 T2 为菊花图,此时复杂度是 O ( n m 2 m ) \mathcal O(nm2^m) O(nm2m),再乘一个状态数 O ( m ) \mathcal O(m) O(m),总复杂度 O ( n m 2 2 m ) \mathcal O(nm^22^m) O(nm22m)

代码

其中 T 3 T3 T3 是抽象出来的 d p \tt dp dp 状态转移图。

#include <cstdio> // XJX yyds!!!
#include <iostream>
#include <vector>
#include <algorithm>
#include <cstring>
#include <cctype>
using namespace std;
# define rep(i,a,b) for(int i=(a); i<=(b); ++i)
# define drep(i,a,b) for(int i=(a); i>=(b); --i)
typedef long long int_;
inline int readint(){
	int a = 0, c = getchar(), f = 1;
	for(; !isdigit(c); c=getchar())
		if(c == '-') f = -f;
	for(; isdigit(c); c=getchar())
		a = (a<<3)+(a<<1)+(c^48);
	return a*f;
}
void writeint(int x){
	if(x < 0) putchar('-'), x = -x;
	if(x > 9) writeint(x/10);
	putchar((x-x/10*10)^48);
}

const int MAXN = 3005;
struct Graph{
	vector<int> G[MAXN];
	void addEdge(int a,int b){
		G[a].push_back(b);
	}
	void init(int){ }
};
# define adj(to,_x,_graph) for(int to : _graph.G[_x])

const int Mod = 998244353;
inline int qkpow(int_ b,int q){
	int_ a = 1;
	for(; q; q>>=1,b=b*b%Mod)
		(q&1) && (a = a*b%Mod);
	return static_cast<int>(a);
}

const int MAXM = 11;
inline void modAddUp(int &x,const int &y){
	(x += y) >= Mod ? (x -= Mod) : 0;
}
int dp[MAXN][MAXM*3];

int tmp[2][1<<MAXM];
void dfs(Graph &T1,int x1,int pre1,Graph &T3,int tot){
	adj(y1,x1,T1) (y1 != pre1) && (dfs(T1,y1,x1,T3,tot),0);
	rep(x3,1,tot){
		int siz = int(T3.G[x3].size());
		int *lst = tmp[0], *now = tmp[1];
		memset(lst,0,(1<<siz)<<2);
		lst[0] = 1; // nothing chosen
		adj(y1,x1,T1) if(y1 != pre1){
			memcpy(now,lst,(1<<siz)<<2);
			for(int S=0,id=0; S<(1<<siz); ++S,id=0)
				adj(y3,x3,T3){
					if(!(S>>id&1)){
						int &v = now[S^(1<<id)];
						v = int((v+int_(lst[S])*dp[y1][y3])%Mod);
					}
					++ id; // next successor
				}
			swap(now,lst); // lst = now
		}
		dp[x1][x3] = lst[(1<<siz)-1];
	}
}

int haxi[MAXM][MAXM];
int solve(Graph &T1,int n,Graph &T3,int m,int tot){
	dfs(T1,1,0,T3,tot); int res = 0;
	rep(i,1,n) rep(j,1,m) // sum up
		modAddUp(res,dp[i][haxi[j][0]]);
	return res; // how many different bijections
}

Graph T1, T2, T3;
int main(){
	int n = readint();
	T1.init(n);
	for(int a,b,i=1; i<n; ++i){
		a = readint(), b = readint();
		T1.addEdge(a,b), T1.addEdge(b,a);
	}
	int m = readint();
	T2.init(m);
	for(int a,b,i=1; i<m; ++i){
		a = readint(), b = readint();
		T2.addEdge(a,b), T2.addEdge(b,a);
	}
	int tot = 0;
	rep(x2,1,m){
		adj(y2,x2,T2) haxi[x2][y2] = ++ tot;
		haxi[x2][0] = ++ tot; // if it's root
	}
	T3.init(tot);
	rep(x2,1,m) adj(pre,x2,T2){
		adj(nxt,x2,T2) if(nxt != pre)
			T3.addEdge(haxi[x2][pre],haxi[nxt][x2]);
		T3.addEdge(haxi[x2][0],haxi[pre][x2]);
	}
	int ans = solve(T2,m,T3,m,tot);
	ans = int(int_(qkpow(ans,Mod-2))
		*solve(T1,n,T3,m,tot)%Mod);
	writeint(ans), putchar('\n');
	return 0;
}

后记

我在这里曾经说过,这类问题大多遵循这个套路。而这道题对于双射的性质的发掘,确实挺新奇的。

所以说,知道套路并不妨碍我做不出来这件事。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值