某些比较烦的DP题。
实际却都是在瞎搞,正经的DP题没有几个。
T1 Parenthese sequence
然后这题我是贪心过的。
把可以不用问号匹配的直接用栈匹配掉。
然后贪心地用问号去匹配那些未被匹配的。
然后看最后剩下的问号是否在2个以上。
代码
根本找不到类似这种的写法= =
另外,这似乎感觉是错的,请勿参考
#include<bits/stdc++.h>
using namespace std;
#define Komachi is retarded
#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--)
#define chkmin(a,b) a=min(a,b)
#define chkmax(a,b) a=max(a,b)
#define LL long long
void Rd(int &res){
char c;res=0;
while((c=getchar())<48);
do res=(res<<3)+(res<<1)+(c^48);
while((c=getchar())>47);
}
#define M 1000004
char C[M];
int n,With[M],Stk[M],Top,Cnt,Num;
int main(){
while(scanf("%s",C)!=EOF){
n=strlen(C);
if(n&1){
puts("None");
continue;
}
Top=0;
REP(i,0,n){
if(C[i]=='(')Stk[++Top]=i;
else if(C[i]==')'){
if(Top)With[Stk[Top]]=i,With[i]=Stk[Top--];
else With[i]=-1;
}
}
while(Top)With[Stk[Top--]]=-1;
Cnt=Num=0;
REP(i,0,n){
if(C[i]=='?'){
if(Cnt)Cnt--;
else Num++;
}
else if(With[i]==-1){
if(C[i]=='(')Cnt++;
else {
Num--;
if(Num<0)break;
}
}
}
if(Cnt || Num<0)puts("None");
else if(Num>2)puts("Many");
else puts("Unique");
}
return 0;
}
T2 Rating
分析
高斯消元可以强行写掉= =
然后,先考虑只有一个账号的情况。
定义
DPi
D
P
i
为i分到20分所需的期望比赛场数。
DP0=1+(1−p)∗DP0+p∗DP1
D
P
0
=
1
+
(
1
−
p
)
∗
D
P
0
+
p
∗
D
P
1
DP1=1+(1−p)∗DP0+p∗DP2
D
P
1
=
1
+
(
1
−
p
)
∗
D
P
0
+
p
∗
D
P
2
整理并化简可得:
DP0=1/p+DP1
D
P
0
=
1
/
p
+
D
P
1
DP1=1/p+1/p2+DP2
D
P
1
=
1
/
p
+
1
/
p
2
+
D
P
2
然后定义
ti=DPi−DP0
t
i
=
D
P
i
−
D
P
0
那么
t1=1/p,t2=1/p+1/p2
t
1
=
1
/
p
,
t
2
=
1
/
p
+
1
/
p
2
又由
DPi=1+(1−p)∗DPi−2+p∗DPi+1(i≥2)
D
P
i
=
1
+
(
1
−
p
)
∗
D
P
i
−
2
+
p
∗
D
P
i
+
1
(
i
≥
2
)
,代入
得
DP0=1/p+1/p∗ti−(1−p)/p∗ti−2+DPi+1
D
P
0
=
1
/
p
+
1
/
p
∗
t
i
−
(
1
−
p
)
/
p
∗
t
i
−
2
+
D
P
i
+
1
则
ti=1/p+1/p∗ti−1+(1−p)/p∗ti−3
t
i
=
1
/
p
+
1
/
p
∗
t
i
−
1
+
(
1
−
p
)
/
p
∗
t
i
−
3
然后可知最后一定会到达
(19,20)
(
19
,
20
)
的分数。
所以答案就是
t19+t20
t
19
+
t
20
代码
#include<bits/stdc++.h>
using namespace std;
#define Komachi is retarded
#define REP(i,a,b) for(int i=(a),i##_end_=(b);i<i##_end_;i++)
#define DB double
DB P,t[24];
int main(){
while(scanf("%lf",&P)!=EOF){
t[1]=1/P;
t[2]=1/P+1/P/P;
REP(i,3,21)
t[i]=1/P+1/P*t[i-1]-(1-P)/P*t[i-3];
printf("%.6lf\n",t[19]+t[20]);
}
return 0;
}
T3 Peter’s Hobby
分析
经典的概率DP模型。
一套状态的成立概率即为所有概率的乘积。
用类似最长路的转移求最大概率的可能性即可。
代码
#include<bits/stdc++.h>
using namespace std;
#define Komachi is retarded
#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--)
#define chkmin(a,b) a=min(a,b)
#define chkmax(a,b) a=max(a,b)
#define LL long long
void Rd(int &res){
char c;res=0;
while((c=getchar())<48);
do res=(res<<3)+(res<<1)+(c^48);
while((c=getchar())>47);
}
#define M 54
double P1[4][4]={
{0.500,0.375,0.125},
{0.250,0.125,0.625},
{0.250,0.375,0.375}
};
double P2[4][4]={
{0.60,0.20,0.15,0.05},
{0.25,0.30,0.20,0.25},
{0.05,0.10,0.35,0.50}
};
int T,n,A[M],Case;
char C[14];
double DP[M][4];
int Pre[M][4];
int Ans[M];
void Solve(){
memset(DP,0,sizeof(DP));
DP[0][0]=0.63*P2[0][A[0]];
DP[0][1]=0.17*P2[1][A[0]];
DP[0][2]=0.20*P2[2][A[0]];
REP(i,0,n-1) REP(j,0,3) REP(k,0,3){
double Val=DP[i][j]*P1[j][k]*P2[k][A[i+1]];
if(Val>DP[i+1][k]){
DP[i+1][k]=Val;
Pre[i+1][k]=j;
}
}
int p=0,Pos=n-1;
REP(i,1,3)if(DP[Pos][i]>DP[Pos][p])p=i;
while(Pos>=0){
Ans[Pos]=p;
p=Pre[Pos--][p];
}
}
int main(){
Rd(T);
while(T--){
Rd(n);
REP(i,0,n){
scanf("%s",C);
A[i]=strlen(C);
if(A[i]==3)A[i]=0;
else if(A[i]==6)A[i]=1;
else if(A[i]==4)A[i]=2;
else A[i]=3;
}
Solve();
printf("Case #%d:\n",++Case);
REP(i,0,n){
if(Ans[i]==0)puts("Sunny");
else if(Ans[i]==1)puts("Cloudy");
else puts("Rainy");
}
}
return 0;
}
T4 A simple greedy problem
分析
正如题目标题所说的,
这题需要贪心一部分才能DP。
注意到在每个血量只能补一刀。
定义
Wi
W
i
为在
i
i
这个血量补到一个兵的最小消耗值。
然后用一个栈维护之前为
0
0
的位置,
贪心地以栈顶来作为最小位置。
最后的转移是一个普通的背包,注意不能超出当前背包容量。
代码
#include<bits/stdc++.h>
using namespace std;
#define Komachi is retarded
#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--)
#define chkmin(a,b) a=min(a,b)
#define chkmax(a,b) a=max(a,b)
#define LL long long
void Rd(int &res){
char c;res=0;
while((c=getchar())<48);
do res=(res<<3)+(res<<1)+(c^48);
while((c=getchar())>47);
}
#define M 1004
#define INF 0x3f3f3f3f
int T,n,Case,A[M],Cnt[M];
int W[M],Stk[M],Top,DP[M];
int main(){
Rd(T);
while(T--){
memset(Cnt,0,sizeof(Cnt));
Rd(n);
REP(i,0,n)Rd(A[i]),Cnt[A[i]]++;
memset(W,63,sizeof(W));
Top=0;
REP(i,1,M){
if(!Cnt[i])Stk[++Top]=i;
else{
while(Top && Cnt[i]>1){
int Pos=Stk[Top--];
W[Pos]=i-Pos;
Cnt[i]--;
}
W[i]=0;
}
}
memset(DP,0,sizeof(DP));
REP(i,1,M) if(W[i]!=INF)
DREP(j,i,W[i]) chkmax(DP[j],DP[j-W[i]-1]+1);
int Ans=0;
REP(i,0,M)chkmax(Ans,DP[i]);
printf("Case #%d: %d\n",++Case,Ans);
}
return 0;
}
T5 Centroid of a Tree
分析
这里牵扯到树的重心的一个判定性质。
树的重心的最大子树必然不超过其他子节点数目之和。
这实际上是很容易证明的。
利用这个性质,
定义为以i为根子树有j个节点的方案数。
然后求不满足的总个数即可。
对于有两个重心的情况,特判并特殊处理。
代码
#include<bits/stdc++.h>
using namespace std;
#define Komachi is retarded
#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--)
#define chkmin(a,b) a=min(a,b)
#define chkmax(a,b) a=max(a,b)
#define LL long long
void Rd(int &res){
char c;res=0;
while((c=getchar())<48);
do res=(res<<3)+(res<<1)+(c^48);
while((c=getchar())>47);
}
#define M 204
#define Mod 10007
#define Add(a,b) ((a+=b)%=Mod)
int T,Case,n;
int Next[M<<1],V[M<<1],Head[M],tot;
void Add_Edge(int u,int v){
Next[++tot]=Head[u],V[Head[u]=tot]=v;
}
#define LREP(i,A) for(int i=Head[A];i;i=Next[i])
bool Two;
int Sz[M],Fa[M],Mx[M];
void DFS(int A,int f){
int B;
Sz[A]=1;
Fa[A]=f;
Mx[A]=0;
LREP(i,A) if((B=V[i])!=f){
DFS(B,A);
Sz[A]+=Sz[B];
chkmax(Mx[A],Sz[B]);
}
chkmax(Mx[A],n-Sz[A]);
}
int FindRT(){
DFS(1,0);
int Mn=n,RT=0;
REP(i,1,n+1) if(Mx[i]<Mn) RT=i,Mn=Mx[i];
else if(Mx[i]==Mn && Fa[i]==RT) RT=i;
if(!(n&1) && Mn==n/2 && Mx[Fa[RT]]==n/2)Two=1;
return RT;
}
int DP[M][M];
int GetDP(int A,int f){
DP[A][0]=DP[A][1]=1;
int B;
LREP(i,A) if((B=V[i])!=f){
GetDP(B,A);
DREP(i,n-1,0) if(DP[A][i]) DREP(j,n-i,0)
Add(DP[A][i+j],DP[A][i]*DP[B][j]);
}
}
int F[M];
void Solve(int RT){
GetDP(RT,0);
int Res=0,Ans=0;
LREP(i,RT){
memset(F,0,sizeof(F));
F[0]=1;
int A=V[i],B;
LREP(j,RT)if((B=V[j])!=A){
DREP(i,n-1,-1) if(F[i]) DREP(j,n-i,0)
Add(F[i+j],F[i]*DP[B][j]);
}
REP(x,0,n+1) REP(y,0,x)
Add(Res,DP[A][x]*F[y]);
}
REP(i,1,n+1)
Add(Ans,DP[RT][i]);
Add(Ans,Mod-Res);
printf("Case %d: %d\n",++Case,Ans);
}
void SolveT(int RT1){
int RT2=Fa[RT1];
GetDP(RT1,RT2);
GetDP(RT2,RT1);
int Ans=0;
REP(i,1,n+1)
Add(Ans,DP[RT1][i]*DP[RT2][i]);
printf("Case %d: %d\n",++Case,Ans);
}
int main(){
Rd(T);
while(T--){
Rd(n);
memset(Head,tot=0,sizeof(Head));
REP(i,1,n){
int u,v;
Rd(u),Rd(v);
Add_Edge(u,v);
Add_Edge(v,u);
}
memset(DP,0,sizeof(DP));
Two=0;
int RT=FindRT();
if(Two)SolveT(RT);
else Solve(RT);
}
return 0;
}
T6 A simple graph problem
分析
状态定义很容易出来:
定义
DPi,j
D
P
i
,
j
为根节点为
i
i
的子树有条向上路径的最小花费。
然后转移方程可以轻易地写出
O(n3)
O
(
n
3
)
,总时间复杂度
O(n4)
O
(
n
4
)
其实利用子树大小来进行一个小剪枝就可以卡过。
如果定义
Fi,j
F
i
,
j
为在子树A中取i条路径,在子树B中取j条路径。
令W为当前访问到的边权值。
有
Fi,j=min(Fi+1,j+1,DPA,i+DPB,j+W∗j)
F
i
,
j
=
m
i
n
(
F
i
+
1
,
j
+
1
,
D
P
A
,
i
+
D
P
B
,
j
+
W
∗
j
)
然后
DPA,i=min(Fi−j,j)(j≤i)
D
P
A
,
i
=
m
i
n
(
F
i
−
j
,
j
)
(
j
≤
i
)
。
这两个转移方程都是
O(n2)
O
(
n
2
)
的,加上子树大小的剪枝可以轻松通过。
然后还有更好的性质没有被利用。
实际上,
DPA,j
D
P
A
,
j
中的j不会到达3及以上。
如果这个j到达3及以上,
则说明在某棵子树B中同样需要3及以上的路径数与其匹配。
然后这花费的路径权值为
j∗Lenth(A,B)
j
∗
L
e
n
t
h
(
A
,
B
)
。
将A子树中两条路径与B子树中若干个两条路径首尾相接,
可以保证投下的人数不变,路径权值和减小。
因此DP就变成了
O(32∗n)
O
(
3
2
∗
n
)
的。
代码
其实这个没有 O(33∗n) O ( 3 3 ∗ n ) 的跑得快= =。
#include<bits/stdc++.h>
using namespace std;
#define Komachi is retarded
#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--)
#define chkmin(a,b) a=min(a,b)
#define chkmax(a,b) a=max(a,b)
#define LL long long
#define INF 0x3f3f3f3f
void Rd(int &res){
char c;res=0;
while((c=getchar())<48);
do res=(res<<3)+(res<<1)+(c^48);
while((c=getchar())>47);
}
#define M 544
int T,Case;
int n,K;
int Next[M],V[M],W[M],Head[M],tot;
void Add_Edge(int u,int v,int w){
Next[++tot]=Head[u],V[Head[u]=tot]=v,W[tot]=w;
}
#define LREP(i,A) for(int i=Head[A];i;i=Next[i])
int DP[M][3],F[4][4];
int Len(int P[M<<1]){
int x=n;
while(x>0 && P[x]==INF)x--;
return x;
}
void DFS(int A,int f){
int B,w;
DP[A][0]=DP[A][1]=0;
LREP(i,A)if((B=V[i])!=f){
DFS(B,A);
w=W[i];
REP(p,0,3) F[p+1][3]=INF;
REP(q,0,3) F[3][q+1]=INF;
DREP(p,2,-1) DREP(q,2,0)
F[p][q]=min(F[p+1][q+1]+K,DP[A][p]+DP[B][q]+q*w);
DREP(p,2,-1)
F[p][0]=F[p+1][1]+K;
memset(DP[A],63,sizeof(DP[A]));
REP(j,0,3) REP(k,0,j+1)
chkmin(DP[A][j],F[k][j-k]);
}
}
int main(){
Rd(T);
while(T--){
memset(Head,tot=0,sizeof(Head));
Rd(n),Rd(K);
REP(i,1,n){
int u,v,w;
Rd(u),Rd(v),Rd(w);
Add_Edge(u,v,w);
}
memset(DP,63,sizeof(DP));
DFS(0,-1);
int Ans=INF;
REP(i,0,3)chkmin(Ans,DP[0][i]+i*K);
printf("Case #%d: %d\n",++Case,Ans);
}
return 0;
}
T7 Tree
分析
由于“没有自环”这个条件,
可知某些集合不能为某些点。
考虑暴力,枚举排列来计算节点的归属。
可以发现只有满足所有归属均不属于那个集合时的方案才是成立的。
每个节点的出度为1,即每个节点均在这些集合中只出现了一次。
所以节点的编号没有意义,只有集合的大小有意义。
那么就能容斥来解决这个问题。
定义
Fi
F
i
为有i个集合不满足要求的方案数,
F0=1
F
0
=
1
。
令
Aj
A
j
为第j个集合的大小
状态转移方程为
Fi=Fi+Fi−1∗Aj
F
i
=
F
i
+
F
i
−
1
∗
A
j
,用类似背包的转移即可。
容斥计算,有
Ans=∑ni=0(i%2?−1:1)∗Fi∗(n−i)!
A
n
s
=
∑
i
=
0
n
(
i
%
2
?
−
1
:
1
)
∗
F
i
∗
(
n
−
i
)
!
最后还有一个问题,这是一个有向图,因此当某个集合中两个元素的集合大小均为0时,交换是不会产生新的结果的。
所以令p为
Aj==0
A
j
==
0
的数目,将答案除以
p!
p
!
即可。
代码
#include<bits/stdc++.h>
using namespace std;
#define Komachi is retarded
#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--)
#define chkmin(a,b) a=min(a,b)
#define chkmax(a,b) a=max(a,b)
#define LL long long
void Rd(int &res){
char c;res=0;
while((c=getchar())<48);
do res=(res<<3)+(res<<1)+(c^48);
while((c=getchar())>47);
}
#define M 1004
#define Mod 1000000007
int n,A[M],F[M],Fac[M],Ans,Case;
int Pow(int x,int p){
int Res=1,k=x;
while(p){
if(p&1)Res=1ll*Res*k%Mod;
k=1ll*k*k%Mod,p>>=1;
}
return Res;
}
int main(){
Fac[0]=1;
REP(i,1,M)Fac[i]=1ll*Fac[i-1]*i%Mod;
while(1){
Rd(n);
if(!n)break;
int p=0;
REP(i,0,n){
int x;
Rd(A[i]);
REP(j,0,A[i])Rd(x);
p+=!A[i];
}
memset(F,0,sizeof(F));
F[0]=1;
REP(i,0,n) if(A[i]) DREP(j,n,0)
F[j]=(F[j]+1ll*F[j-1]*A[i])%Mod;
Ans=Fac[n];
REP(i,1,n+1)
Ans=(Ans+(i&1?-1ll:1ll)*Fac[n-i]*F[i])%Mod;
(Ans+=Mod)%=Mod;
Ans=1ll*Ans*Pow(Fac[p],Mod-2)%Mod;
printf("Case #%d: %d\n",++Case,Ans);
}
return 0;
}
然后这好像还是个卷积式,用NTT或许能写更大的数据。
模数比较烦,然后常数也是个问题,
将模数改为
998244353
998244353
后,NTT还是比暴力要慢= =
代码
如果有谁想卡常可以试试233
当前这份代码跑
n=104
n
=
10
4
要10s
然而暴力只用0.5s
#include<bits/stdc++.h>
using namespace std;
#define Komachi is retarded
#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--)
#define chkmin(a,b) a=min(a,b)
#define chkmax(a,b) a=max(a,b)
#define LL long long
#define DB double
void Rd(int &res){
char c;res=0;
while((c=getchar())<48);
do res=(res<<3)+(res<<1)+(c^48);
while((c=getchar())>47);
}
#define M 66444
#define Mm 134444
#define Mod 998244353
int Pow(int x,int p){
int Res=1;
for(;p;x=1ll*x*x%Mod,p>>=1)if(p&1)Res=1ll*Res*x%Mod;
return Res;
}
int Rev[20][Mm];
void Init_Rev(int Bit){
REP(i,0,1<<Bit)
Rev[Bit][i]=(Rev[Bit][i>>1]>>1)|((i&1)<<(Bit-1));
}
struct ntt{
int X[Mm],Y[Mm];
int n,m,Bit,s;
void NTT(int *A,int n,int p){
REP(i,0,n)if(i<Rev[Bit][i])swap(A[i],A[Rev[Bit][i]]);
for(int i=1;i<n;i<<=1){
int wn=Pow(3,(Mod-1)/i/2);
if(p)wn=Pow(wn,Mod-2);
for(int j=0;j<n;j+=i<<1){
int w=1;
REP(k,j,j+i){
int x=A[k],y=1ll*A[k+i]*w%Mod;
A[k]=(x+y)%Mod,A[k+i]=(x+Mod-y)%Mod;
w=1ll*wn*w%Mod;
}
}
}
if(p){
int Val=Pow(n,Mod-2);
REP(i,0,n)A[i]=1ll*A[i]*Val%Mod;
}
}
void Solve(){
while((1<<Bit)<n+m-1)Bit++;
s=1<<Bit;
REP(i,n,s)X[i]=0;
REP(i,m,s)Y[i]=0;
NTT(X,s,0);
NTT(Y,s,0);
REP(i,0,s)X[i]=1ll*X[i]*Y[i]%Mod;
NTT(X,s,1);
}
}ntt;
int n,Ans,Fac[M],A[M];
vector<int> F;
vector<int>Find(int l,int r){
vector<int>Res,Left,Right;
if(l==r){
Res.resize(4);
Res[0]=1;
Res[1]=A[l];
return Res;
}
int mid=l+r>>1;
Left=Find(l,mid);
Right=Find(mid+1,r);
ntt.n=mid-l+2;
ntt.m=r-mid+1;
REP(i,0,ntt.n)ntt.X[i]=Left[i];
REP(i,0,ntt.m)ntt.Y[i]=Right[i];
ntt.Solve();
Res.resize(ntt.s+4);
REP(i,0,ntt.s)Res[i]=ntt.X[i];
return Res;
}
int main(){
Fac[0]=1;
REP(i,1,M)Fac[i]=1ll*Fac[i-1]*i%Mod;
REP(i,1,18)Init_Rev(i);
Rd(n);
int p=0;
REP(i,0,n){
int x;
Rd(A[i]);
REP(j,0,A[i])Rd(x);
p+=!A[i];
}
F=Find(0,n-1);
Ans=Fac[n];
REP(i,1,n+1)
Ans=(Ans+(i&1?-1ll:1ll)*Fac[n-i]*F[i])%Mod;
(Ans+=Mod)%=Mod;
Ans=1ll*Ans*Pow(Fac[p],Mod-2)%Mod;
printf("%d\n",Ans);
return 0;
}
2018/9/15 updata
事实上并不是非常需要卡常…这个写法改改就可以用了。
New Code
#include<bits/stdc++.h>
using namespace std;
#define Uni All Right
#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)
#define LREP(i,a) for(int i=Head[a];i;i=Next[i])
#define LL long long
#define Mod 1004535809
void Rd(int &x){
static char c;x=0;
while((c=getchar())<48);
do x=(x<<3)+(x<<1)+(c^48);
while((c=getchar())>47);
}
static const int M=160004;
int n,m;
int Rev[M],W[M];
int Pool[M*40],*Allc=Pool;
int Pow(int k,int p){
int x=1;
for(;p;k=(LL)k*k%Mod,p>>=1)if(p&1)x=(LL)x*k%Mod;
return x;
}
void DFT(int *A,int s,int p){
REP(i,0,s)if(i<Rev[i])swap(A[i],A[Rev[i]]);
W[0]=1;
for(int i=1,pi=2;i<s;i<<=1,pi<<=1){
int w=Pow(3,(Mod-1)/pi);
if(p)w=Pow(w,Mod-2);
for(int j=i-2;j>=0;j-=2)W[j+1]=(LL)(W[j]=W[j>>1])*w%Mod;
for(int j=0;j<s;j+=pi){
int *l=A+j,*r=A+j+i;
REP(k,0,i){
LL Tmp=(LL)r[k]*W[k];
r[k]=(l[k]-Tmp)%Mod;
l[k]=(l[k]+Tmp)%Mod;
}
}
}
if(p){
LL inv=Pow(s,Mod-2);
REP(i,0,s)A[i]=A[i]*inv%Mod;
}
}
int *GetNew(int n){
int *p=Allc;Allc+=n;
return p;
}
struct Poly{
int *V,n;
Poly operator *(const Poly &_){
Poly __;
__.V=GetNew(__.n=n+_.n-1);
static int a[M],b[M],s,t;
for(t=0,s=1;s<n+_.n-1;++t,s<<=1);
REP(i,0,s){
a[i]=i<n?V[i]:0;
b[i]=i<_.n?_.V[i]:0;
Rev[i]=(Rev[i>>1]>>1)|((i&1)<<t-1);
}
DFT(a,s,0),DFT(b,s,0);
REP(i,0,s)a[i]=(LL)a[i]*b[i]%Mod;
DFT(a,s,1);
REP(i,0,n+_.n-1)__.V[i]=a[i];
return __;
}
};
int Q[M],A[M];
Poly Gets(int l,int r){
if(l==r){
Poly _;
_.V=GetNew(_.n=2);
_.V[0]=1,_.V[1]=Q[l];
return _;
}
int mid=l+r>>1;
return Gets(l,mid)*Gets(mid+1,r);
}
int Fac[M];
int main(){
Fac[0]=1;
REP(i,1,M)Fac[i]=1ll*Fac[i-1]*i%Mod;
Rd(n);
int p=0;
REP(i,0,n){
int x;
Rd(A[i]);
REP(j,0,A[i])Rd(x);
if(A[i])Q[++p]=A[i];
}
Poly Tmp=Gets(1,p);
int *F=Tmp.V;
int Ans=Fac[n];
REP(i,1,Tmp.n)
Ans=(Ans+(i&1?-1ll:1ll)*Fac[n-i]*F[i])%Mod;
Ans=(LL)(Ans+Mod)*Pow(Fac[n-p],Mod-2)%Mod;
printf("%d\n",Ans);
return 0;
}