题目
题目描述
给出两棵树
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(x∈S⫅T1,y∈T2) 使得边
⟨
a
,
b
⟩
∈
S
\langle a,b\rangle\in S
⟨a,b⟩∈S 当且仅当
⟨
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
∣T1∣⩽2×103 而
∣
T
2
∣
⩽
10
|T2|\leqslant 10
∣T2∣⩽10 。
思路
考虑求双射 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}| ∣fS→T2∣÷∣fT2→T2∣ 。
而求出 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)=m−1 即 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;
}
后记
我在这里曾经说过,这类问题大多遵循这个套路。而这道题对于双射的性质的发掘,确实挺新奇的。
所以说,知道套路并不妨碍我做不出来这件事。