orangejuice's blog

挂了请务必叫我

[NOI Online #3 提高组] 优秀子序列

这个题怎么不直接取名

集合幂级数 \(\text{exp}\)

优秀的子序列中任意两个元素01位无交,这是一个标准的子集卷积形式

\(\varphi\) 的计算显然与 \(a_i\) 的卷积独立,可以线性筛/埃氏筛

暴力

可以暴力 \(3^{18}\) 过,枚举时为了避免重复可以通过强制枚举的数包含最高位的1

注意 \(a_i=0\) 要特殊处理

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
bool Mbe;
const int N=1<<18,P=1e9+7;

int n,cnt0=1;
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;
}
int Phi[N+1],notpri[N+1];
int F[N],C[N],cnt[N];

int main(){
Phi[1]=1;
rep(i,2,N) if(!notpri[i]) {
Phi[i]=i-1;
for(int j=i+i;j<=N;j+=i) {
notpri[j]=1;
if(!Phi[j]) Phi[j]=j;
Phi[j]=Phi[j]/i*(i-1);
}
}
n=rd();
rep(i,1,N-1) cnt[i]=cnt[i&(i-1)]+1;
F[0]=1;
rep(i,1,n) {
int x=rd();
if(!x) F[0]*=2,Mod1(F[0]);
else C[x]++;
}
int ans=0;
rep(S,0,N-1) {
if(S) for(int T=S;_builtin_clz(S)==__builtin_clz(T);T=(T-1)&S) F[S]=(F[S]+1ll*F[S^T]*C[T])%P;
ans=(ans+1ll*F[S]*Phi[S+1])%P;
}
printf("%d\n",ans);
}

集合幂级数

就是直接套集合幂计数的 \(\text{exp}\)

同样要特殊处理 \(a_i=0\)

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
const int N=1<<18,P=1e9+7;
int F[N][19],Inv[20];
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
#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 buf[200000],*p1,*p2;
#define getchar() (((p1==p2)&&(p2=(p1=buf)+fread(buf,1,200000,stdin))),*p1++)
char IO;
int rd(){
int s=0; static char c;
while(c=getchar(),c<48);
do s=(s<<1)+(s<<3)+(c^'0');
while(c=getchar(),c>47);
return s;
}
bool Mbe;

int n,m,cnt0=1,U;
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;
}
int Phi[N+1],notpri[N+1],pri[N/4],pc;

void Exp(int *a){
static int b[N];
rep(i,1,n) b[i-1]=1ll*a[i]*i%P;
rep(i,0,n-1) {
int s=b[i];
rep(j,1,i) s=(s+1ll*a[j]*b[i-j])%P;
a[i+1]=1ll*s*Inv[i+1]%P;
}
}

int main(){
Inv[0]=Inv[1]=1;
rep(i,2,18) Inv[i]=1ll*(P-P/i)*Inv[P%i]%P;
n=rd();
rep(i,1,n) {
int x=rd(); cmax(U,x);
if(!x) cnt0*=2,Mod1(cnt0);
else F[x][__builtin_popcount(x)]++;
}
Phi[1]=1;
for(n=1;(1<<n)<=U;)n++;
m=1<<n;
rep(i,2,m) {
if(!notpri[i]) pri[++pc]=i,Phi[i]=i-1;
for(int j=1;j<=pc && 1ll*i*pri[j]<=m;++j){
notpri[i*pri[j]]=1;
if(i%pri[j]==0) {
Phi[i*pri[j]]=Phi[i]*pri[j];
break;
}
Phi[i*pri[j]]=Phi[i]*(pri[j]-1);
}
}
for(int i=1;i<m;i<<=1) for(int l=0;l<m;l+=i*2) for(int j=l;j<l+i;++j) rep(k,1,n) F[j+i][k]+=F[j][k],Mod1(F[j+i][k]);
rep(i,0,m-1) Exp(F[i]);
for(int i=1;i<m;i<<=1) for(int l=0;l<m;l+=i*2) for(int j=l;j<l+i;++j) rep(k,1,n) F[j+i][k]-=F[j][k],Mod2(F[j+i][k]);
int ans=0;
rep(S,1,m-1) ans=(ans+1ll*F[S][__builtin_popcount(S)]*Phi[S+1])%P;
ans=1ll*(ans+1)*cnt0%P;
printf("%d\n",ans);
}

[WC2019]数树(树形dp+多项式exp)

Part1

相同边连接的点同一颜色,直接模拟即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
namespace pt1{
int fa[N],sz[N];
map <int,int> M[N];
int Find(int x){ return fa[x]==x?x:fa[x]=Find(fa[x]); }
void Solve(){
rep(i,1,n) fa[i]=i;
rep(i,2,n){
int x=rd(),y=rd();
if(x>y) swap(x,y);
M[x][y]=1;
}
rep(i,2,n) {
int x=rd(),y=rd();
if(x>y) swap(x,y);
if(M[x][y]) fa[Find(x)]=Find(y);
}
int ans=1;
rep(i,1,n) if(Find(i)==i) ans=1ll*ans*y%P;
printf("%d\n",ans);
}
}

Part2

相同边连接的点同一颜色,即在相同边构成的树上形成了若干联通块

很容易想到可以强制一些边保留,设保留 \(i\) 条边的方案数是 \(F_i\) ,则答案就是 \(\sum_i F_i\cdot y^{n-i}\)

考虑 \(dp\) 那些边相同,但是不好直接计算剩下边不同的方案,所以考虑计算最多有 \(i\) 条边相同的方案数,即

\[G_i=\sum_{j=i}C(j,i)F_j\]

二项式反演得到 \(F_i=\sum_{j=i}(-1)^{j-i}C(j,i)G_j\)

设分成了 \(m\) 个联通块,大小分别为 \(size_i\) ,则这些联通块随意构成树的方案数就是 \(n^{m-2}\cdot\prod size_i\)

根据上述性质可以写出一个简单的 \(O(n^4)\) 树形dp求得 \(G_i\) ,即 \(dp[i][j][k]\) 表示在 \(i\) 的子树里,有 \(j\) 条边相同,当前还剩下一个大小为 \(k\) 的联通块,每多转移一条相同边,系数是 \(\frac{1}{ny}\)

考虑优化 \(dp\)

联通块大小的问题,可以转化为每次在联通块里选择一个关键点的方案数, \(dp\) 第三维 \(0/1\) 表示当前联通块里是否已经选出了关键点

每次断开一个联通块时必须已经存在关键点

答案是

\(\sum_i F_i\cdot y^{n-i}\)

\(=\sum_i y^{n-i} \sum_{j=i}(-1)^{j-i}C(j,i)G_j\)

\(=y^n G_j\sum_{i=0}^j(-1)^{j-i}C(j,i)y^{-i}\)

发现右边的式子 \(\sum_0^j(-1)^{j-i}C(j,i)y^{-i}=(\frac{1}{y}-1)^j\)

那么直接把 \(\frac{1}{y}-1\) 带入作为保留一条边的转移系数,消去了第二维

那么这个 \(\text{dp}\) 可以被优化到 \(O(n)\)

\[ \ \]

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
namespace pt2{
vector <int> G[N];
int dp[N][2],g[2],Inv;
void dfs(int u,int f){
dp[u][0]=dp[u][1]=1;
for(int v:G[u]) if(v!=f) {
dfs(v,u);
g[0]=g[1]=0;
rep(i,0,1) rep(j,0,1) {
if(!i||!j) g[i|j]=(g[i|j]+1ll*dp[u][i]*dp[v][j]%P*Inv)%P;
if(j) g[i]=(g[i]+1ll*dp[u][i]*dp[v][j])%P;
}
dp[u][0]=g[0],dp[u][1]=g[1];
}
}
void Solve() {
rep(i,2,n) {
int u=rd(),v=rd();
G[u].pb(v),G[v].pb(u);
}
Inv=(qpow(y)-1)*qpow(n)%P;
dfs(1,0);
ll res=dp[1][1]*qpow(y,n)%P*qpow(n,P+n-3)%P;
printf("%lld\n",res);
}
}

Part3

有了上面的 \(dp\) ,这一部分就简单多了,设分成了 \(m\) 个联通块,每个大小为 \(a_i\) ,则贡献为

\[\begin{aligned}\frac{n!\cdot a_i^{a_i-2}\cdot (n^{m-2})^2(\frac{1}{y}-1)^{n-m}(\frac{1}{n}^{n-m})^2\cdot a_i^2}{\prod a_i! m !}\end{aligned}\]

即枚举每个联通块生成树的数量,且需要考虑两棵树分别的联通块之间的连边数量,这一部分需要平方

很显然,可以直接对于 \([x^i]F(x)=\frac{1}{i!}\cdot (\frac{1}{n^2}\cdot (\frac{1}{y}-1))^{i-1} i^2i^{i-2}\) 这个多项式求exp得到

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
const int M=1<<18|10,K=17;
typedef vector <int> Poly;

int w[M],rev[M],Inv[M];
void Init(){
ll t=qpow(3,(P-1)>>K>>1);
w[1<<K]=1;
rep(i,(1<<K)+1,(1<<(K+1))-1) w[i]=w[i-1]*t%P;
drep(i,(1<<K)-1,1) w[i]=w[i<<1];
Inv[0]=Inv[1]=1;
rep(i,2,M-1) Inv[i]=1ll*(P-P/i)*Inv[P%i]%P;
}
int Init(int n){
int R=1,cc=-1;
while(R<n) R<<=1,cc++;
rep(i,1,R-1) rev[i]=(rev[i>>1]>>1)|((i&1)<<cc);
return R;
}

void NTT(int n,Poly &a,int f){
if((int)a.size()<n) a.resize(n);
rep(i,1,n-1) if(rev[i]<i) swap(a[i],a[rev[i]]);
for(int i=1;i<n;i<<=1) {
int *e=w+i;
for(int l=0;l<n;l+=i*2){
for(int j=l;j<l+i;++j){
int t=1ll*a[j+i]*e[j-l]%P;
a[j+i]=a[j]-t,Mod2(a[j+i]);
a[j]+=t,Mod1(a[j]);
}
}
}
if(f==-1) {
reverse(a.begin()+1,a.end());
rep(i,0,n-1) a[i]=1ll*a[i]*Inv[n]%P;
}
}

Poly operator * (Poly a,Poly b){
int n=a.size(),m=b.size();
int R=Init(n+m-1);
NTT(R,a,1),NTT(R,b,1);
rep(i,0,R-1) a[i]=1ll*a[i]*b[i]%P;
NTT(R,a,-1),a.resize(n+m-1);
return a;
}

Poly Poly_Inv(Poly a){
int n=a.size();
if(n==1) return {(int)qpow(a[0])};
Poly b=a; b.resize((n+1)/2),b=Poly_Inv(b);
int R=Init(n*2);
NTT(R,a,1),NTT(R,b,1);
rep(i,0,R-1) a[i]=1ll*b[i]*(2-1ll*a[i]*b[i]%P+P)%P;
NTT(R,a,-1); a.resize(n);
return a;
}

Poly Deri(Poly a){
rep(i,1,a.size()-1) a[i-1]=1ll*i*a[i]%P;
a.pop_back();
return a;
}
Poly IDeri(Poly a){
a.pb(0);
drep(i,a.size()-2,0) a[i+1]=1ll*a[i]*Inv[i+1]%P;
a[0]=0;
return a;
}

Poly Ln(Poly a){
int n=a.size();
a=Deri(a)*Poly_Inv(a),a.resize(n+1);
return IDeri(a);
}

Poly Exp(Poly a){
int n=a.size();
if(n==1) return Poly{1};
Poly b=a; b.resize((n+1)/2),b=Exp(b);
b.resize(n); Poly c=Ln(b);
rep(i,0,n-1) c[i]=a[i]-c[i],Mod2(c[i]);
c[0]++,c=c*b;
c.resize(n);
return c;
}

void Solve() {
int I=(qpow(y)-1)*qpow(1ll*n*n%P)%P;
Init();
Poly F(n+1);
for(int i=1,FInv=1;i<=n;FInv=1ll*FInv*Inv[++i]%P){
F[i]=qpow(I,(i-1)) * // 保留i-1条边
(i==1?1:qpow(i,i-2))%P // i个点生成树
* i%P * i%P //
* FInv%P; // 阶乘常数
}
F=Exp(F);
rep(i,1,n) F[n]=1ll*F[n]*i%P;
ll res=F[n]*qpow(y,n)%P*qpow(n,2*(P+n-3))%P;
printf("%lld\n",res);
}

[WC2020] 选课 (枚举+dp)

题面数据范围锅了导致枚举炸裂,写了正解却只有50分。。。。。。。

正确题面可以查看LOJ

记限制涉及到的不同的点个数为 \(P\)

首先是不同的会被限制的个数 \(\leq 12\) ,所以应该直接枚举这些点的状态,枚举部分的复杂度是 \(O(2^P)\)

(然后我枚举了 \(p\) ,实际 \(p\leq 66\) 啊啊啊啊啊啊)

对于没有被限制的点,可以优先预处理出每个类型的答案,注意到 \(L=T-\sum s_i\leq 40\) ,则可以每次只取出长度为 \(O(L)\) 的这一部分,类型之间合并为 \(O(ML^2)\) 的复杂度

或许比较难的点是在于每种类型内的合并,注意到 \(w\in\{1,2,3\}\)

把每种 \(w\) 排序后,令 \(dp_i\) 为总权值为 \(i\) 的最小花费,同时记录最小花费时选取的三种 \(w\) 的个数,最优决策肯定是取最小的几个

每次转移,可以直接枚举选取的 \(w\) ,在排序好的数组上找到下一个最小花费

ps:事实证明,这个做法显然是假的,但是为什么就是没卡掉呢?正确的做法是先dp,w=1或2,再和3的暴力合并求出最大的 \(L\) 个值,这样的复杂度为 \(O(NL)\)

ps2:后来测试,这个错误做法在值域只有200的情况下,随机情况下,整个值域中的错误率只有1/1001/1000左右,而答案需要用到的部分又奇少,只有L个,于是乎,嘿嘿嘿嘿~

如果用这种邪教写法,预处理的转移复杂度就是 \(O(N)\)

每次枚举之后,把被改变的几个类型答案重新计算,重新合并,这一部分复杂度就是 \(O(PL^2)\)

算上 \(2^P\) 次枚举,得到总复杂度是 \(O(N+2^PPL^2)\)

Code

[补]NOIP2020T4微信步数

题意:一个人在 \(k\) 维平面上,每一维范围是 \([1,W_i]\) 上的任意一个位置,初始可以在任何一个位置

这个人在空间上游走,每 \(n\) 步为一轮不断重复,每一步是一个方向上走-1或者1,求所有情况下 最后他离开空间范围的时间 之和

分析:

行走是循环的,每一维可以先看做独立,然后离开范围的时间就是每一维取 \(\min\)

一个简单的思路:

求出每一维每一个位置离开的时间,然后 \(k\) 路归并得到答案,复杂度为 \(O((\sum W_i)k+nk)\)

容易想到根据循环来优化计算,但是如果以每一个位置为元素进行考虑,难以处理不同长度循环之间的合并

\[ \ \]

更换思路:

简单的计数方法的转换:

从初始位置离开的时间之和 = 前 \(i(i\ge 0)\) 步还未离开空间的初始位置个数之和

\(F_{i,j}\)\(i\) 这一维 \(j\) 步还未离开的初始位置个数,则答案就是 \(\begin{aligned} \sum_{i\ge 0}\prod F_{i,j}\end{aligned}\)

此时观察发现除了第一轮需要特殊考虑以外,其它的 \(F_{i,j}\) 可以表示为 \(\max\lbrace0,F_{i,j-n}-D_i\rbrace\) (其中 \(D_i\) 为每一轮 \(i\) 这一维偏移的量)

对于前面 \(n\) (好像是 \(2n\) )个特殊考虑,后面对于每一个不同的 \(i\mod n\) 可以放在一起考虑,用一个统一的式子表示

然后计算就是类似 \(\begin{aligned} \sum_{i\ge 0}\prod (G_j-i D_j)\end{aligned}\) ,以 \(i\) 为元,所求的就是是一个 \(k\) 次多项式前缀和,也就是一个 \(k+1\) 次多项式的点值

暴力的方法就是 求出前面 \(k+2\) 项的值,然后用 拉格朗日插值/高斯消元 得到答案,复杂度为 \(O(nk^2-nk^3)\) (如果插值写好一点,复杂度主要受限于求值)

~~然后甚至可以无脑吸多项式做到 \(O(nk\log ^2k)\) ~~

求值时可以发现 对于 \(i\) 所求的 \(j\) 处点值的积式里面 最多只有一项和 \(i-1\) 不同,因而可以特殊考虑以优化求值复杂度

下面是 \(nk^2\) ,由于求值已经是 \(k^2\) 了,所以插值也没优化

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
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
#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)
char IO;
int rd(){
int s=0,f=0;
while(!isdigit(IO=getchar())) if(IO=='-') f=1;
do s=(s<<1)+(s<<3)+(IO^'0');
while(isdigit(IO=getchar()));
return f?-s:s;
}

const int N=5e5+10,P=1e9+7;
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;
}
int n,m,W[12],F[12][N*2];
int A[N],B[N],D[N];
int ans;
ll X[20],Y[20],I[20];
int Lag(int n,int x){
ll res=0;
rep(i,0,n) {
ll a=1,b=1;
rep(j,0,n) if(i!=j) a=a*(P+x-X[j])%P,b=b*(X[i]<X[j]?-I[X[j]-X[i]]:I[X[i]-X[j]])%P;
res=(res+a*b%P*Y[i])%P;
}
return res;
}
int main() {
I[0]=I[1]=1;
rep(i,2,19) I[i]=P-P/i*I[P%i]%P;
n=rd(),m=rd();
rep(i,1,m) W[i]=rd();
rep(i,1,n) A[i]=rd(),B[i]=rd();
int f=0;
rep(i,1,m) {
int c=F[i][0]=W[i],now=0,l=0,r=0;
rep(j,1,n*2) {
if(A[j<=n?j:j-n]==i) now+=B[j<=n?j:j-n];
if(now<l) l=now,c--;
if(now>r) r=now,c--;
c=max(c,0),F[i][j]=c;
}
D[i]=abs(now)/2;
if(now || c==0) f=1;
}
if(!f) return puts("-1"),0;
rep(i,0,n){
ll t=1;
rep(j,1,m) t=t*F[j][i]%P;
ans=(ans+t)%P;
}
rep(i,n+1,n*2) {
int n=1e9,f=1;
rep(j,1,m) {
if(D[j]) n=min(n,F[j][i]/D[j]);
f&=F[j][i]>0;
}
if(!f) continue;
int t=1;
rep(j,1,m) t=1ll*t*max(0,F[j][i]-D[j]*(n+1))%P;
ans=(ans+t)%P;
int s=0;
rep(k,0,m+1) {
int t=1;
rep(j,1,m) t=1ll*t*(F[j][i]-D[j]*k)%P;
X[k]=k,Y[k]=s=(s+t)%P;
}
ans=(ans+Lag(m+1,n))%P;
}
ans=(ans%P+P)%P,printf("%d\n",ans);
}

[NOI Online 2021 提高组] 积木小赛

题目大意:给定串 \(A\) , \(B\) ,求 \(B\) 中有多少本质不同的连续子段是 \(A\) 的子序列

\(n\leq 3000\)

暴力枚举 \(B\) 中的子段,同步维护与 \(A\) 的匹配指针 \(p\)

每次插入一个字符 \(c\) ,找到 \(A\)\(p+1\) 之后第一个字符 \(c\) ,令匹配指针跳过去

可以预处理出这样的下一个字符 \(nxt_{i,c}\) ,完成 \(O(1)\) 匹配

除此以外,我们还需要对于本质不同去重

如果用 \(\text{trie}\) 树去重,需要开一个 \(\frac{n^2}{2}\cdot 26\) 的数组,面临着内存不够的问题

你可以信仰不开这么大

也可以去学习一下 \(\text{DAT(Double Array Trie)}\) 算法

也可以用 \(\text{hash+set/map/hash table/sort unique}\)

也可以用链表暴力存储trie树的情况,每次暴力for过去找儿子

这样内存均为 \(O(n^2)\)

以下是链表trie树的版本

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
#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=3010,M=N*N/2,INF=1e9+10;
bool Mbe;

int n;
int nxt[N][26];
char A[N],B[N];
struct Edge{ int c,to,nxt; } e[M];
int head[M],cnt;

bool Med;
int main(){
//fprintf(stderr,"%.2lf\n",(&Med-&Mbe)/1024.0/1024.0);
freopen("block.in","r",stdin),freopen("block.out","w",stdout);
n=rd(),scanf("%s%s",A+1,B+1);
drep(i,n,1) {
rep(j,0,25) nxt[i][j]=nxt[i+1][j];
nxt[i][A[i]-'a']=i;
}
rep(i,1,n) {
int u=0,p=0;
rep(j,i,n) {
int c=B[j]-'a';
if(!(p=nxt[p+1][c])) break;
int v=-1;
for(int k=head[u];k;k=e[k].nxt) if(e[k].c==c) { v=e[k].to; break; }
if(~v) u=v;
else {
v=++cnt;
e[v]=(Edge){c,v,head[u]};
head[u]=v,u=v;
}
}
}
printf("%d\n",cnt);
}

[NOI Online 2021 提高组] 愤怒的小N

暴力

倍增维护 \([x,x+2^d)\) 内部所有 \(b\) 的权值和 以及 \(a\) 的,用多项式表示

具体的,维护两个多项式 \(F_0(x),F_1(x)\) ,每次倍增的转移如下

\(F_0(x)\leftarrow F_0(x)+F_1(x+d)\)

\(F_1(x)\leftarrow F_1(x)+F_0(x+d)\)

因此暴力倍增复杂度为 \(O(nk^2)\) ,实现上需要记录每次倍增之后多项式与答案的前面部分相拼接需要额外的偏移

\[ \ \]

优化

如果你输出多项式,就会发现,倍增 \(k\) 次之后,所有 \(a,b\) 位置对应的多项式就完全相同了

形式化地理解这个过程

一开始, \(F_0(x)=A(x),F_1(x)=0\) ,其中 \(A(x)\) 为读入的多项式

进行一次转移后, \(F_0(x),F_1(x)\) 的第 \(k-1\) 项只受到对方的 \(k-1\) 项和自己的 \(k-1\) 项影响

因此一次转移后 \([x^{k-1}]F_0(x)=[x^{k-1}]F_1(x)\)

下一次转移,第 \(k-2\) 项值只受到对方的 \(k-2\) 项,已经已经确定相同的 \(k-1\) 项影响

这个过程不断进行,第 \(i\) 次倍增会使得 \([k-i,k-1]\) 项相同

\[ \ \]

对于 \(k\) 次倍增之后,后面多出来的部分,可以直接求一个多项式前缀和,然后除2得到答案

多项式前缀和容易通过拉格朗日插值解决,复杂度为 \(O(k^2)\)

预处理前面的多项式复杂度为 \(O(k^3)\) ,求后面的式子为 \(O(k^2)\) ,预处理 \(n\) 的值复杂度为 \(O(\log n)\)

因此复杂度为 \(O(\log n+k^3)\)

\[ \ \]

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
#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=5e5+10,M=510,INF=1e9+10,P=1e9+7;

int n,m;
char s[N];
int D[N],T[N];
// D[i]预处理倍增求出的每项对于答案贡献时存在的偏移
// T[i]预处理每个位后面1的个数
int A[N],F[2][M],G[2][M],C[M][M];
int Pow[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;
}
int X[M],Y[M];
int Lagrange(int x,int n,int *X,int *Y){
int ans=0;
rep(i,0,n) {
ll s=1;
rep(j,0,n) if(i!=j) s=s*(X[i]-X[j])%P;
s=qpow(s);
rep(j,0,n) if(i!=j) s=s*(x-X[j])%P;
ans=(ans+s*Y[i])%P;
}
return ans;
}

int main(){
freopen("angry.in","r",stdin),freopen("angry.out","w",stdout);
scanf("%s",s),n=strlen(s),reverse(s,s+n);
m=rd();
rep(i,0,m-1) A[i]=F[0][i]=rd();
rep(i,0,m) rep(j,*C[i]=1,i) C[i][j]=(C[i-1][j]+C[i-1][j-1])%P;
int ans=0,x=2;
rep(i,1,n-1) D[i-1]=x*(s[i]=='1'),x=x*2%P,T[i-1]=(s[i]=='1');
drep(i,n-1,0) D[i]+=D[i+1],Mod1(D[i]),T[i]^=T[i+1];
x=1;
rep(i,0,min(m-1,n-1)) {
if(s[i]=='1') {
int t=1;
rep(j,0,m-1) {
ans=(ans+1ll*t*F[!T[i]][j])%P;
t=1ll*t*D[i]%P;
}
}
rep(d,0,1) rep(j,0,m-1) G[d][j]=F[d][j];
rep(j,*Pow=1,m-1) Pow[j]=1ll*Pow[j-1]*x%P;
rep(j,0,m-1) rep(k,0,j) {
F[0][k]=(F[0][k]+1ll*C[j][k]*Pow[j-k]%P*G[1][j])%P;
F[1][k]=(F[1][k]+1ll*C[j][k]*Pow[j-k]%P*G[0][j])%P;
}
x=x*2%P;
}
// 倍增到前k-1项
if(m>=n) return printf("%d\n",ans),0;
// 预处理拉格朗日插值
rep(i,0,m) {
X[i]=i,x=1;
rep(j,0,m-1) {
Y[i]=(Y[i]+1ll*A[j]*x)%P;
x=1ll*x*i%P;
}
if(i) Y[i]+=Y[i-1],Mod1(Y[i]);
}
ans=(ans+1ll*Lagrange(D[m-1]-1,m,X,Y)*(P+1)/2)%P;
printf("%d\n",ans);
}

[补]联合省选2021 图函数

考虑将所有加入 \(i-m\) 这些边的答案一起算出来

模拟删去点的过程容易发现,删去 \(u\) 时, \(u,v\) 在同一个强连通分量里的点满足:

存在仅包含 \(\ge u\) 的点的路径,使得 \(u,v\) 互相连通

\(A_{u,v}\) 表示最大的 \(i\) 使得 \(u\) 能仅通过 \(\ge u\) 的点到达 \(v\)

\(B_{u,v}\) 表示最大的 \(i\) 使得 \(v\) 能仅通过 \(\ge u\) 的点到达 \(u\)

计算 \(\min\{A_{u,v},B_{u,v}\}\) 即可确定一个点对能够贡献到的区间

考虑依次加入每一条边 \((u,v)\) ,在正反图上计算 \(A,B\) 中每个元素第一次被确定的时间

以计算 \(A\) 为例,每次会被更新的 \(A_{i,..}\) 一定满足

\(i\ge \min\{u,v\},i\rightarrow u,i\not \rightarrow v\)

可以暴力 \(\text{for}\) 这样的 \(i\) ,从 \([i,v]\) 开始,让 \(v\) 扩展,每次扩展找到未确定的 \([i,w]\)

每个元素只会被确定一次,复杂度为 \(O(n^2)\) ,暴力枚举起点为 \(O(nm)\)

可以用 \(\text{bitset}\) 优化到 \(O(\frac{nm}{64}+n^2)\) (扩展元素的部分复杂度可能是假的,但是没有关系)

Loj Submission

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
#include<cstdio>
#include<vector>
#include<cstring>
using namespace std;
using ull=unsigned long long;
#define pb push_back
#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)
int rd(){
int s=0; static char c;
while(c=getchar(),c<48);
do s=(s<<1)+(s<<3)+(c^'0');
while(c=getchar(),c>47);
return s;
}
enum{N=1010,M=200010};
int n,m,t;
int A[N][N],B[N][N],U[M],V[M],ans[M];
int Log(ull x){ return !x?-1:__builtin_ctzll(x); }
struct Bitset{
ull a[16];
void turn(int x){ a[x>>6]^=1ull<<x; }
} X[N],Y[N];
vector <int> G[N],E[N];
void dfs1(int st,int u) {
if(~A[st][u]) return;
A[st][u]=t,X[u].turn(st);
for(int v:G[u]) if(v>=st) dfs1(st,v);
}
void dfs2(int st,int u) {
if(~B[st][u]) return;
B[st][u]=t,Y[u].turn(st);
for(int v:E[u]) if(v>=st) dfs2(st,v);
}

int main(){
n=rd(),m=rd();
rep(i,0,m-1) U[i]=rd(),V[i]=rd();
memset(A,-1,sizeof A),memset(B,-1,sizeof B);
rep(i,1,n) A[i][i]=B[i][i]=m,X[i].turn(i),Y[i].turn(i);
for(t=m-1;~t;t--) {
G[U[t]].pb(V[t]),E[V[t]].pb(U[t]);
int L=min(U[t],V[t]);
rep(i,0,L>>6) {
// bitset 优化。
for(int j;~(j=Log(X[U[t]].a[i]&~X[V[t]].a[i])) && (i<<6|j)<=L;) dfs1(i<<6|j,V[t]);
for(int j;~(j=Log(Y[V[t]].a[i]&~Y[U[t]].a[i])) && (i<<6|j)<=L;) dfs2(i<<6|j,U[t]);
}
}
rep(i,1,n) rep(j,i,n) if(~A[i][j] && ~B[i][j]) ans[min(A[i][j],B[i][j])]++;
drep(i,m,0) ans[i]+=ans[i+1];
rep(i,0,m) printf("%d ",ans[i]);
}

LOJ3120「CTS2019 | CTSC2019」珍珠

只是想要祭奠做的时候死去的我

约定下文中的 \(d\) 为题目中的 \(D\)

Part1

先从最最暴力的定义转移(可以跳过这个)

\(S\) 表示个数为奇数的颜色集合

转移多项式(集合幂指数) \(F(x)=\sum_0^d x^{\{i\}}\)

\(\text{FWT}\) 得到

\(F'(x)=\sum (d-2|S|)x^S\)

\((F(x)^n)'=\sum (d-2|S|)^nx^S\)

\(\text{IFWT}\) 得到

\(2^d F(x)^n=\sum_S \sum_T (-1)^{|S\cap T|}(d-2|T|)^n x^{S}\)

答案就是

\(2^d Ans=\sum_S\sum_T(-1)^{|S\cap T|}(d-2|T|)^n[|S|\leq Lim]\)

如果枚举 \(|S|,|T|,|S\cap T|\) 进行统计,会出现3个元,无法直接优化到 \(O(n\log n)\)


Part2

设最后得到每一种颜色的个数为 \(c_i,\sum c_i=n\)

方案数就是 \(\frac{n!}{\Pi c_i!}\) ,很符合指数型生成函数吧

考虑最朴素的生成函数表示法,考虑每种颜色的选的个数

个数无限制的生成函数 \(F_0(x)=\sum \frac{x^i}{i!}=e^x\)

个数为奇数的生成函数 \(F_1(x)=\frac{x^{1}}{1!}+\frac{x^{3}}{3!}+\cdots=\frac{e^x-e^{-x}}{2}\)

个数为偶数的生成函数 \(F_2(x)=\frac{x^0}{0!}+\frac{x^2}{2!}+\cdots=\sum \frac{e^x+e^{-x}}{2}\)

\(F_1^i=2^{-i}\sum (-1)^jC(i,j)e^{(i-2j)x}\)

\(F_2^i=2^{-i}\sum C(i,j)e^{(i-2j)x}\)

设选中 \(i\) 个奇数的答案为 \(G_i\)

\(G_i=n!C(n,i)[x^n]F_1^iF_2^{d-i}\)

\(=n!C(n,i)[x^n]2^{-d}\sum_j\sum_k C(i,j)(-1)^je^{(i-2j)x} C(n-i,k)e^{(n-i-2k)x}\)

\(e^{ax}=\sum \frac{(ax)^i}{i!}\) ,约去 \(n!\)

\(=C(n,i)[x^n]2^{-d}\sum_j\sum_k C(i,j)C(n-i,k)(-1)^j (n-2k-2j)^n\)

诶好像和Part1一样。。。

暴毙结束。。。。


Part3

上面这两种方法都要枚举三个元,无法优化,所以考虑先重复计算,再容斥掉

计算 \(\ge i\) 个方案数,然后容斥回去

设最终答案序列为 \(H\)

我们可以快速求出选中 \(i\) 个奇数且带重复的方案数 \(G_i\)

\(G_i=\sum C(j,i)H_j=[x^n]C(d,i)F_1(x)^iF_0(x)^{d-i}\) (强制选 \(i\) 个剩下未知)

\(G_i=C(d,i) \cdot n! \cdot [x^n]\sum (-1)^je^{(d-2j)x}\cdot C(i,j)\cdot {2^{-i}}\)

\(e^{ax}=\sum \frac{(ax)^i}{i!}\) ,约去 \(n!\)

\(G_i=C(d,i) \cdot \sum (-1)^j(d-2j)^n\cdot C(i,j)\cdot {2^{-i}}\)

卷积一次可以得到 \(G\)

最后的容斥就是二项式反演

\(H_i=\sum_{j\ge i}(-1)^{j-i}G_jC(j,i)\)

再卷积求出 \(H\)

「CTS2019 | CTSC2019」重复(Kmp)


Part1

首先我们考虑对于一个已经确定的 \(t\) 串,如何检查是否合法

对于 \(s\) 串建立 \(\text{kmp}\) ( \(\text{kmp}\) 自动机当然可以),

如果当前 \(\text{kmp}\) 指针 \(j\)\(\text{fail}\) 树上的祖先所对应的所有下一个位置 \(s[ancestor+1]\) 中,存在一个字符, \(t\) 中当前位置的字符 \(t[i]<s[ancestor+1]\)

那么就是存在一个"有意义的串",并且这个串和s串第一个不同的位置就是 \(ancestor+1\)

所以可以预处理一个 \(fail\) 树上祖先最大的 \(s[ancestor+1],Max[state]\)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
rep(i,1,n) Max[i-1]=s[i];
Max[n]='a';//边界条件
cmax(Max[1],Max[0]);
rep(i,2,n) {
int j=nxt[i-1];
while(j && s[i]!=s[j+1]) j=nxt[j];
if(s[i]==s[j+1]) ++j;
nxt[i]=j;
cmax(Max[i],Max[j]);
}
//预处理Max
rep(i,0,n) {
if(i<n) Trans[i][s[i+1]-'a']=i+1;
rep(j,0,25) if(j!=s[i+1]-'a') Trans[i][j]=Trans[nxt[i]][j];
}//kmp自动机,不要再暴力了
rep(i,m+1,n+m) t[i]=t[i-m]; // 延长到n+m就够了
int j=0,f=0;
rep(i,1,n+m){
if(t[i]<Max[j]) { // 存在一个位置满足即可
f=1;
break;
}
j=Trans[j][t[i]-'a'];
}

配合dfs枚举,可以拿到pts10


Part2

设状态转移为 \(Trans[state][c]\)

把kmp状态压进dp状态里

如果把问题直接放到无穷意义下来看

那么跑够长度之后,后面的任意加入 \(m\) 次字符都会构成一个循环

枚举循环开始状态为 \(st\)\(\text{dp}\) 走了 \(m\) 步回到 \(st\) 的方案数

如果计算合法方案数,那么还要多一维,所以直接计算不合法方案,也就是

\(Trans[state][Max[state]..25]\) 这一段转移是不会出现合法情况的

最后减一下

复杂度 \(O(m\cdot n^2)\)

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
namespace pt2{
int dp[2][N];
void Solve(){
int cur=0,ans=0;
rep(st,0,n) { // 枚举初始状态
rep(i,0,n) dp[cur][i]=dp[!cur][i]=0;
dp[cur][st]=1;
rep(t,1,m) {
cur^=1;
rep(i,0,n) if(dp[!cur][i]){
rep(c,Max[i]-'a',25) {//只转移不合法的
dp[cur][Trans[i][c]]+=dp[!cur][i];
Mod1(dp[cur][Trans[i][c]]);
}
dp[!cur][i]=0;
}
}// 走m步回到st
ans+=dp[cur][st],Mod1(ans);
}
ans=(qpow(26,m)-ans+P)%P;//减一下
printf("%d\n",ans);
}
}



Part3

观察上面一档情况,合法的转移 \(Trans[state][Max[state]..25]\)

如果枚举下一个字符 \(c>Max[state]\) ,那么在 \(s\) 串中就找不到任何匹配了,下一个状态就是 \(0\)

否则,下一个状态就是 \(Trans[state][c]\)

也就是说,每个点出发其实只有两种情况,一种是一定会经过 \(0\)

所以对于这个环是否经过 \(0\) 进行讨论

如果不经过 \(0\) ,那么考虑直接从 \(st\) 出发走 \(m\) 步非0转移即可

经过 \(0\) 的,预处理一个从 \(0\) 出发,走了 \(i\) 步第一次回到 \(0\) 的方案数

\([st\rightarrow 0,0\rightarrow 0,0\rightarrow 0...,0\rightarrow st]\)

长成这个样子的转移

枚举第一个 \(st\rightarrow 0\) 的时间,后面可以预处理出来

复杂度 \(O(n\cdot m)\)

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
#include<bits/stdc++.h>
using namespace std;

#define pb push_back
#define reg register
typedef long long ll;
#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> void cmin(T &a, T b){ ((a>b)&&(a=b)); }
template <class T> 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())) if(IO=='-') f=1;
do s=(s<<1)+(s<<3)+(IO^'0');
while(isdigit(IO=getchar()));
return f?-s:s;
}

const int N=2010,P=998244353;

int n,m;
char s[N];
int Max[N],nxt[N],Trans[N][26];
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;
}

int st[N][N]; // 从i出发,走了j步第一次到0
int dp[N][N]; // 从0出发,走了i步,到了j,如果到0就不进行转移

int main(){
freopen("repeat.in","r",stdin),freopen("repeat.out","w",stdout);
m=rd();
scanf("%s",s+1),n=strlen(s+1);
rep(i,1,n) Max[i-1]=s[i];
Max[n]='a';
cmax(Max[1],Max[0]);
rep(i,2,n) {
int j=nxt[i-1];
while(j && s[i]!=s[j+1]) j=nxt[j];
if(s[i]==s[j+1]) ++j;
nxt[i]=j;
cmax(Max[i],Max[j]);
}
rep(i,0,n) {
if(i<n) Trans[i][s[i+1]-'a']=i+1;
rep(j,0,25) if(j!=s[i+1]-'a') Trans[i][j]=Trans[nxt[i]][j];
}

int ans=0;
st[0][0]++;
rep(i,1,n) {
int j=i,f=1;
rep(k,1,m) {
st[i][k]+=25-(Max[j]-'a'),j=Trans[j][Max[j]-'a'];
if(!j) {
st[i][k]++,f=0;
break;
}
}
if(j==i) ans+=f;
}

dp[0][0]=1;
rep(i,1,m) {
rep(j,0,n) if(dp[i-1][j]) {
dp[i][Trans[j][Max[j]-'a']]+=dp[i-1][j],Mod1(dp[i][Trans[j][Max[j]-'a']]);
dp[i][0]=(dp[i][0]+1ll*(25-(Max[j]-'a'))*dp[i-1][j])%P;
}
}

rep(i,0,n) { // 枚举初始状态
rep(j,0,m) if(st[i][j] && dp[m-j][i]) { // 枚举走了几步第一次到0
ans=(ans+1ll*st[i][j]*dp[m-j][i])%P;
}
}
ans=(qpow(26,m)-ans+P)%P;
printf("%d\n",ans);
}










「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);
}
0%