Luogu4500 [ZJOI2018]树
Luogu4500 [ZJOI2018]树
\(dp,\)群论
题意简述:求\(k(k \le 10^9)\)棵大小为\(n(n \le 2000)\)的随机有根树(根为\(1\),生成方式为每个节点\(i(i>1)\)随机认\([1,i)\)中一个节点作为祖先)两两同构的概率。
\(Part 1\)
我们先考虑答案是什么?
先计算总方案数,根据树的生成方式,总共有\((n-1)!\)种不同的树,那么总方案数自然而然就是\([(n-1)!]^k\)。
我们将大小为\(n\)的树按照同构的原则划分成一些等价类,对于一种等价类,我们设其大小为\(sz_i\)。
最终树的形态属于其中一个等价类。
那么本题的答案就是:
\(Part 2\)
设计一个\(DP\),令\(dp(i,j)\)表示一个大小为\(i\)的树,其所有等价类大小的\(jk\)次方的和(没看错,\(jk\)就是\(j\)乘\(k\))。
那么答案就是\(dp(n,1)\)。
为了维护转移,我们必然需要让\(dp(i,j)\)值从更小的树转移过来。
将根节点去掉,其子树构成一片大小为\(i-1\)的森林。
然而我们极度缺乏信息,所有子树构成一个集合,而元素间没有顺序关系。我们根本不知道有多少树,哪些树是同构的,难以转移。
不过我们可以分析的一点是,两两同构的子树容易导致重复的计算。
首先,同构的子树,大小必然相等。我们根据子树大小把原问题继续拆分,设\(tmp(p,j,t)\)表示大小为\(t\)的森林,每棵树大小不超过\(p\),其等价类大小的\(jk\)次方和,边界条件\(tmp(1,j,t)=1\)。
设\(f(p,i,j)\)表示\(i\)棵大小为\(p\)的树组成的森林的等价类大小的\(jk\)次方和。
枚举大小为\(i\)的树的个数进行转移。
上面的式子应该不必多解释了吧。
现在问题的核心转移到了求解\(f(p,i,j)\)。
怎么办呢?
\(Part 3\)
重新回顾\(f(p,i,j)\)的定义:\(f(p,i,j)\)表示\(i\)棵大小为\(p\)的树组成的森林的等价类大小的\(jk\)次方和。
大小为\(p\)的树可以划分为一些等价类,我们为这些等价类标号,那么对于这一片森林来讲,我们先定义一个\(\{ x \}\),表示总共\(i\)棵树,它们属于的等价类的标号组成的可重集,它可以转化为一个数列\(\{ a_1,a_2,\cdots,a_m\}\)(总共有\(m\)个等价类),\(a_i\)表示第\(i\)种树的个数,数列\(\{ x \}\)或\(\{ a \}\)可确定一个森林类型。
由于\(f(p,i,j)\)维护的是森林,所以它是无序的,这里的无序是指树之间没有偏序关系,而点之间依然是有的(也就是有标号)。
现在我们再次给出一个定义\(f(\{ a \})\),表示在有序(各棵树之间的顺序)的情况下,类型为\(\{ a \}\)的森林的方案数。
得到等式:
到现在为止,\(\{ a \}\)对我们来说仍然是一个确定的数列,我们下一步就是让数列\(\{ a \}\)抽象化,使其适用的范围扩大。
其实上面的式子已经为我们指出了一条路径\(\prod_t \frac{1}{a_t!}\)明显与数组内数的顺序无关,于是我们得到一个想法:把数列\(\{ a \}\)变为可重集\(\{ c \}\)。
本质上,\(\{ c \}\)就是一个元素个数的可重集,也就是对原森林计算每个等价类的个数,这些数组成的可重集。在这个可重集中,每个数对应的树的类型都是不确定的。当然,也可以认为\(\{ c \}\)是一个有序序列,其中的数代表一个类型树的个数,而到底是什么类型的树我们还没有确定,上面的\(\{ x \}\)也可以当成有序序列来看(注:以下\(\{ x \},\{ a \},\{ c \}\)同时出现均代表同一片森林)。
同样给出定义,\(h(\{ c \})\)表示在有序的情况下,元素个数的可重集为\(\{ c \}\)的树的方案数。
再来一个定义:
由于这里所有的一切都是在\(f(p,i,j)\)的计算这个子问题下进行的,所以上式中的\(p,j\)与\(f(p,i,j)\)中的\(p,j\)意义相同。
也就是我们为可重集\(\{ c \}\)中每一种元素钦定一棵树计算答案,但是\(h(\{ c \}) \ne w(\{ c \})\),原因在于\(h\)的计算中必须满足每个树对应的树的类型互不相同,而\(w\)的计算中没有这个限制。
我们需要利用群论利用\(w(\{ c \})\)解决\(h(\{ c \})\)的计算。
\(Part 4\)
我们首先必须明确的是,这里的置换群是什么,还有一个重要的点就是判断两个可重集\(\{ x \}\)本质不同的标准。需要注意的一点是,这里的群,并没有直接作用在可重集\(\{ x \}\)上,它只是一个计数工具。
\(1.\)群、置换:这里的置换就是普通的置换,即从一个排列置换到另一个排列,排列大小为树的个数\(i\),想必大家对这个群已经十分熟悉了。
\(2.\)等价标准:每个\(\{ x \}\)都会对应一个\(\{ c \}\)(经过上面\(\{ x \} \rightarrow \{ a \} \rightarrow \{ c \}\)的转换),如果\(\{ x_1 \},\{ x_2 \}\)对应\(\{ c_1 \},\{ c_2 \}\)相等,那么\(\{ x_1 \},\{ x_2 \}\)等价。这里的等价类与树的等价类是两种不同的体系,不要搞混。
设元素\(\{ x \}\)的等价类大小为\(Tr(\{ x \})\)。
相当于每种等价类中的\(\{ x \}\)都计算一遍,每次的权重都是\(\frac{1}{|Tr(\{ x \})|}\),所以上式成立。
我们试图将\(\{ x \}\)与置换建立联系,对于\(s\)个相同的元素(把一堆相同元素拆成好几份也是合法的),我们对应到一个排列上的一个大小为\(s\)的循环,如果\(\{ x \}\)可以通过上述的对应关系对应到一个置换\(G_0\),那么我们称\(\{ x \}\)为置换\(G_0\)上的不动点,\(G_0\)的不动点集合为\(fix(G_0)\)。
按照\(\operatorname{Pólya}\) 定理的方式,我们把置换\(T\)拆分成循环的形式,用数列\(\{ D_T \}\)表示,其中的数代表一个循环的大小。
考虑\(fix(T)\)中的元素,举个例子:
\(T\)中有一个循环的大小为\(3\),那么\(\{ x \}\)中对应的可以是\(3\)个相同的元素,也可以是\(5\)个相同元素中拆出\(3+2\)进行匹配。
也就是\(fix(T)\)可以兼容两堆树相同的情况,于是:
\(Part 5\)
一些奇怪的操作来了。
让置换带上权\(V\),试图去拟合求解的值。
为方便运算,添加一个常数\(C\)。
等式右边存在连乘式,设:
对等式两边分开来推导。
把左边的\(\{ x \} \in fix(T)\)拆开,我们主要考虑满足条件的\(T\)到底是什么。由置换圈定\(\{ x \}\),与从\(\{ x \}\)圈定置换的方式恰好相反,也就是\(\{ x \}\)中的\(3\)可以用可以用置换中的\(1+2\)对应,本质上是对\(\{ x \}\)中每个数的整数拆分,但是拆分之后还需要对于每个拆分的部分进行圆排列。
经典的集合划分问题。
设:
令\(C=i!\)。
设\(F_0(x)=\sum_{i=0}^{\infty} \frac{1}{i^{jk}}\)。
于是我们直接把所有\(Q(t)\)的值算出来了。
回到原问题,求解\(\sum_{\{ x \} \in X} \left ( \prod_t \frac{1}{c_t!^{jk}} \right) \frac{h(\{ c \})}{|Tr(\{ x \})|}\)。
把\(T \in G\)拆开,把置换拆成多个循环的过程,也是整数拆分和圆排列的结合。
设\(G(x)=\sum_{i=1}^{\infty} \frac{Q(i)dp(p,ij)}{i} x^i\)。
\(f(p,i,j)\)终于算完了。。。
\(Part 6\)
模数不一定是\(NTT\)模数,\(\ln\)和\(\exp\)总不能\(MTT\)吧(害怕)。
\(\ln\):
\(\exp\):
\(Part 7\)
\(DP\)的过程为互相更新,更新顺序为\(dp \rightarrow f \rightarrow tmp \rightarrow dp \rightarrow \cdots\)。
\(DP\)时间复杂度:
枚举\(j\),可以在\(\sum_{i=1}^n \frac{n^2}{i^2}=O(n^2)\)时间完成所有\(\ln\)的预处理。
\(\exp\)的复杂度:\(\sum_{i=1}^n \sum_{j=1}^{\frac{n}{i}} \frac{n^2}{(ij)^2}=O(n^2)\)。
总时间复杂度:\(O(n^2 \log n)\)。
\(Code:\)
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#define N 2005
#define ll long long
using namespace std;
int n,k,mp;
int inv[N],fac[N],infac[N],kfac[N][N],kinfac[N][N];
int L[N][N],dp[N][N],f[N][N],tmp[N][N];
int ans,Q[N],g[N];
int r[N],cr[N];
void Add(int &x,int y)
{
x=(x+y)%mp;
}
void Del(int &x,int y)
{
x=(x-y)%mp;
}
void Mul(int &x,int y)
{
x=(ll)x*y%mp;
}
int add(int x,int y)
{
return (x+y)%mp;
}
int del(int x,int y)
{
return (x-y)%mp;
}
int mul(int x,int y)
{
return (ll)x*y%mp;
}
int ksm(int x,int y)
{
int ans=1;
while (y)
{
if (y & 1)
Mul(ans,x);
Mul(x,x);
y >>=1;
}
return ans;
}
int C_j(int n,int k,int j)
{
if (n<k)
return 0;
return mul(kfac[n][j],mul(kinfac[k][j],kinfac[n-k][j]));
}
void InvDev(int *a,int *b,int n)
{
b[0]=0;
for (int i=1;i<=n;++i)
b[i]=mul(inv[i],a[i-1]);
b[n+1]=0;
}
void GetLn(int *f,int *g,int n)
{
f[n+1]=0;
for (int i=0;i<n;++i)
{
cr[i]=mul(i+1,f[i+1]);
for (int j=0;j<i;++j)
Del(cr[i],mul(f[i-j],cr[j]));
}
InvDev(cr,g,n);
}
void GetExp(int *f,int *g,int n)
{
g[0]=1;
for (int i=1;i<=n;++i)
{
g[i]=0;
for (int j=1;j<=i;++j)
Add(g[i],mul(mul(j,f[j]),g[i-j]));
Mul(g[i],inv[i]);
}
}
int main()
{
scanf("%d%d%d",&n,&k,&mp);
inv[1]=fac[0]=fac[1]=infac[0]=infac[1]=1;
for (int i=2;i<=n+2;++i)
inv[i]=mul(mp-mp/i,inv[mp%i]),fac[i]=mul(fac[i-1],i),infac[i]=mul(infac[i-1],inv[i]);
for (int i=0;i<=n+2;++i)
kfac[i][1]=ksm(fac[i],k),kinfac[i][1]=ksm(infac[i],k);
for (int i=0;i<=n+2;++i)
for (int j=2;j<=n;++j)
kfac[i][j]=mul(kfac[i][j-1],kfac[i][1]),kinfac[i][j]=mul(kinfac[i][j-1],kinfac[i][1]);
for (int j=1;j<=n;++j)
{
for (int i=0;i<=n/j;++i)
r[i]=kinfac[i][j];
GetLn(r,L[j],n/j);
}
for (int i=1;i<=n;++i)
dp[1][i]=1;
for (int j=1;j<=n;++j)
for (int i=0;i<=n/j;++i)
tmp[i][j]=1;
for (int p=2;p<=n;++p)
{
for (int j=1;j<=n/p;++j)
dp[p][j]=tmp[p-1][j];
for (int j=1;j<=n/p;++j)
{
int cnt=n/p/j,sz=n/j;
for (int i=0;i<=cnt;++i)
Q[i]=L[j][i];
for (int i=1;i<=cnt;++i)
g[i]=mul(Q[i],dp[p][i*j]);
GetExp(g,r,cnt);
for (int i=1;i<=cnt;++i)
f[i][j]=mul(mul(kfac[i*p][j],kinfac[p][i*j]),r[i]);
for (int i=0;i<=sz;++i)
r[i]=tmp[i][j];
for (int i=1;i<=sz;++i)
for (int z=1;z<=i/p;++z)
Add(r[i],mul(mul(tmp[i-z*p][j],f[z][j]),C_j(i,z*p,j)));
for (int i=0;i<=sz;++i)
tmp[i][j]=r[i];
}
}
ans=mul(dp[n][1],kinfac[n-1][1]);
ans=(ans%mp+mp)%mp;
printf("%d\n",ans);
return 0;
}