更简洁的做法。
这棵树的形态没给,考虑这棵树大概长什么样子。
这启示我们,选一个连通块作为父亲及以上,每一个点的子树大小都是确定的。不妨将最大的连通块作为父亲,发现这棵树的根就是重心。
观察样例,发现叶子节点是好确定的,就是 \(d_i=0\),数一下发现所有点中分出来连通块大小为 \(1\) 的数量恰好等于叶子数量(废话,不然怎么有解)。
可以发现,子树大小相同的点在树中互换位置是没有问题的,如果大小为 \(x\) 的子树有 \(c\) 个,这些子树对答案的贡献就是 \(c!\)。
但是这样可能算重,如果同一个点下面挂了若干个大小一样的子树,这些子树交换并不会对答案有贡献,要除掉。
到这里基本就做完了。还需要注意的一个点是重心可能有两个,这两个点交换是不能产生贡献的,详见第一个样例。
写得巨丑的赛时代码:
#include<bits/stdc++.h>
constexpr int rSiz=1<<21;
char rBuf[rSiz],*p1=rBuf,*p2=rBuf;
#define gc() (p1==p2&&(p2=(p1=rBuf)+fread(rBuf,1,rSiz,stdin),p1==p2)?EOF:*p1++)
template<class T>void rd(T&x){char ch=gc();for(;ch<'0'||ch>'9';ch=gc());for(x=0;ch>='0'&&ch<='9';ch=gc())x=(x<<1)+(x<<3)+(ch^48);
}
constexpr int _=3e5+5,mod=998244353;
int fac[_],inv[_];
int pw(int x,int y=mod-2){for(int v=1;;y>>=1){if(!y)return v;if(y&1)v=1ll*v*x%mod;x=1ll*x*x%mod;}
}
void init(int M){fac[0]=1;for(int i=1;i<=M;i++)fac[i]=1ll*fac[i-1]*i%mod;inv[M]=pw(fac[M]);for(int i=M;i;i--)inv[i-1]=1ll*inv[i]*i%mod;
}
int n,a[_],b[_],c[_],ans=1;
bool vis[_],you[_];
std::vector<int>d[_],g[_];
int main(){rd(n);init(n);for(int i=1;i<=n;i++){rd(a[i]);b[i]=n;for(int j=1,x;j<=a[i];j++){rd(x);d[i].push_back(x);b[i]=std::min(b[i],n-x);}g[b[i]].push_back(i);
// std::sort(d[i].begin(),d[i].end());}for(int sz=1,xianzai=0;sz<=n;sz++){int p=g[sz].size();if(!p)continue;ans=1ll*ans*fac[p]%mod;for(int i:g[sz]){for(int j:d[i])c[j]++,vis[j]=1;for(int j:d[i])if(vis[j]){ans=1ll*ans*inv[c[j]]%mod;c[j]=0,vis[j]=0;}}}printf("%d\n",ans);
}