「JSOI2019」神经网络

考虑一个合法的哈密顿路可以表示为什么样子:

按照不同树的编号,分割为一段段,相邻两段属于不同树

同时,如果最后一段和第一段同编号,将最后一段移动到第一段前面

由此,一个哈密顿路可以由唯一表示:

1号点在第一个段中,此后每一段和上一个属于不同树,且最后一段不属于1树

由此,问题分解为两部分:

Part1 求解树路径分段

考虑树形$dp$求解,每个点记录$dp_{i,j,0/1}$表示当前$i$子树内已经产生$j$条路径,$i$自己是否可以向父亲连边

容易用类似树形背包的方式合并,每次决策儿子是否连接到自己上面

注意:一个长度$>1$的段,需要考虑正反方向的排放

复杂度为$O(\sum k_i^2)$

Part2 合并每棵树的段

相邻两段不同色,考虑容斥求解

枚举这棵树中的$i$个段自己生成了$j$个不合法的相邻,$i$个段合并生成$i-j$个段,且乘上容斥系数$(-1)^j$

$i$个并掉$j$个,方案数计算如下:

先把$i$个排好,乘上$i!$,然后选择$j$个间隔合并掉$\binom{i-1} {j}$,然后对于剩下的$i-j$个元素无序,需要除掉$(i-j)!$

背包合并容斥之后的结果,对于当前的$i$个元素,任意排列即可

然而上面是理想情况,还需要考虑$1$号元素不能被排列,要强制最后一个段不是1树的段

这一部分,在树1的容斥以及最终背包合并时特殊处理即可,即少排列一个元素,且最后合并时先选一个放在最后面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
#include<bits/stdc++.h>
using namespace std;

typedef long long ll;
typedef unsigned long long ull;
typedef double db;
typedef pair <int,int> Pii;
#define reg register
#define mp make_pair
#define pb push_back
#define Mod1(x) ((x>=P)&&(x-=P))
#define Mod2(x) ((x<0)&&(x+=P))
#define rep(i,a,b) for(int i=a,i##end=b;i<=i##end;++i)
#define drep(i,a,b) for(int i=a,i##end=b;i>=i##end;--i)
template <class T> inline void cmin(T &a,T b){ ((a>b)&&(a=b)); }
template <class T> inline void cmax(T &a,T b){ ((a<b)&&(a=b)); }

char IO;
template <class T=int> T rd(){
T s=0; int f=0;
while(!isdigit(IO=getchar())) f|=IO=='-';
do s=(s<<1)+(s<<3)+(IO^'0');
while(isdigit(IO=getchar()));
return f?-s:s;
}

const int N=5e3+10,P=998244353;

int n,m;
int I[N],J[N],C[N][N];
ll qpow(ll x,ll k=P-2) {
ll res=1;
for(;k;k>>=1,x=x*x%P) if(k&1) res=res*x%P;
return res;
}

struct Edge{
int to,nxt;
} e[N<<1];
int head[N],ecnt;
void AddEdge(int u,int v){
e[++ecnt]=(Edge){v,head[u]};
head[u]=ecnt;
}
int dp[N][N][2]; // 0,1 是否向上连
int G[N][3],H[N][3],sz[N];

void dfs(int u,int f){
sz[u]=0;
for(int i=head[u];i;i=e[i].nxt){
int v=e[i].to;
if(v==f) continue;
dfs(v,u);
}
G[0][0]=1,G[0][1]=G[0][2]=0;
for(int i=head[u];i;i=e[i].nxt){
int v=e[i].to;
if(v==f) continue;
rep(i,0,sz[u]+sz[v]) rep(j,0,2) H[i][j]=G[i][j],G[i][j]=0;
rep(i,0,sz[u]) rep(a,0,2) if(H[i][a]) rep(j,0,sz[v]) rep(b,0,min(1,2-a)) G[i+j][a+b]=(G[i+j][a+b]+1ll*H[i][a]*dp[v][j][b])%P;
sz[u]+=sz[v];
}
rep(i,0,sz[u]+1) dp[u][i][0]=dp[u][i][1]=0;
rep(i,0,sz[u]) {
dp[u][i+1][0]=(0ll+dp[u][i+1][0]+G[i][0]+2*G[i][1]+2*G[i][2])%P; // 长度>1的段可以翻转
dp[u][i][1]=(0ll+dp[u][i][1]+G[i][0]+G[i][1])%P; // 如果连了两个儿子,就无法向上连了
}
sz[u]++;
}

int F[N],T[N];
void Get(){
n=rd();
rep(i,1,n) head[i]=0;
ecnt=0;
rep(i,2,n) {
int u=rd(),v=rd();
AddEdge(u,v),AddEdge(v,u);
}
dfs(1,0);
rep(i,1,n) {
F[i]=dp[1][i][0],T[i]=0;
ll t=1ll*F[i]*J[i]%P;
rep(j,1,i) {
T[j]=(T[j]+((i-j)&1?P-1:1)*t%P*C[i-1][i-j]%P*I[j])%P;
}
}
}

int S[N],c;

int main(){
rep(i,J[0]=1,N-1) J[i]=1ll*J[i-1]*i%P;
I[N-1]=qpow(J[N-1]);
drep(i,N-1,1) I[i-1]=1ll*I[i]*i%P;
rep(i,0,N-1) rep(j,C[i][0]=1,i) C[i][j]=C[i-1][j]+C[i-1][j-1],Mod1(C[i][j]);
m=rd();
if(m==1) return n=rd(),printf("%d\n",n<=2),0;
S[0]=1;
rep(t,1,m-1) {
Get();
drep(i,n+c,0) {
S[i]=0;
rep(j,1,min(i,n)) S[i]=(S[i]+1ll*S[i-j]*T[j])%P;
}
c+=n;
}
Get();
rep(i,1,n) {
F[i]=dp[1][i][0],T[i]=0;
ll t=1ll*F[i]*J[i-1]%P;
// 特殊处理,不允许排列第一段
rep(j,1,i) T[j]=(T[j]+((i-j)&1?P-1:1)*t%P*C[i-1][i-j]%P*I[j-1])%P;
}
int ans=0;
// 不允许改变第一段的位置
// 且强制最后一段不能属于第一棵树
rep(i,1,c) if(S[i]) rep(j,1,n) if(T[j]) {
// 强制前面的最后一个在最后
int t=1ll*J[i]*J[j-1]%P*C[i-1+j-1][j-1]%P;
ans=(ans+1ll*t*S[i]%P*T[j])%P;
}
printf("%d\n",ans);
}