很有意思的一道题。
考虑树形 DP。首先考虑的是 表示 为根的子树内合法的拓扑序数量,但是这样合并子树的时候是无法计算的,如下图:
假设 当前合并了 这棵子树,接下来要合并红色和蓝色的部分,此时 必须在 之后挑战,但是方案数并不是简单地两个 值相乘,观察可以发现 号点的合并顺序是任意的,并且 都挑战了之后 的挑战顺序也不是一种,所以我们合并的时候会乘两个神秘系数。
观察性质,合并 两棵树时,问题可以抽象为两个序列,合并之后 需要在 前面并且保持原先相对顺序不变,而上面所说的的神秘系数其实完全由在 之前与之后的数的个数决定。
注意到我们并不关心前面的数的顺序,因为这个东西显然满足乘法原理,只需要把顺序数存在 里直接乘起来就是对的。
很自然的,想到再加一维状态 表示 在它当前子树内排名为 的方案数(本质上还是记录的在它之前的数的个数 ,而在它之后的数就是 ),接下来是愉快的推式子时间:
设 为当前合并的两棵树,其中 在它所在的子树内排在第 位, 在它所在的子树内排在第 位。
若 指向 ,即 在 之前挑战,则合并大概是这样的:
橙圈表示合并后的序列在 之前的数字数,枚举 表示合并后 的位置在第 位,则红圈一定是第 个,橙圈中应当有 个数,其中 个为序列 中的元素,合并顺序随意,所以第一个系数就是 。
新序列中第 个为数字是 ,接下来 个数中有 个是序列 中的元素,所以第二个系数就是 。
对于 的取值,最少取 ,最多取 ,因为 一定在 之后,如果 则一定是合法。
整理一下,在外层枚举 ,求出 的取值范围 就得出转移方程:
观察一下,其中只有 一项中包含 ,并且是连续的一段,显然可以用前缀和优化。
对于 指向 的情况也是同理,转移方程甚至也是一样的,只不过取值变了一些。
最后讨论一下时间复杂度,对于每一条边连接 和 ,它会被计算 ,相当于是把它两边的点互相枚举了一遍,这样每个点对只会合并一次,所以时间复杂度是优秀的 。
本题无论是状态的设计,方程的推导,范围的计算,转移的优化都需要细致的思考,是一道不可多得的好题。
int T,n,cnt,fr[1001],inv[1001],head[1001],to[2001],nex[2001],v[2001],tmp[1001],f[1001][1001],siz[1001];
inline void add(int x,int y,int z){to[++cnt]=y,v[cnt]=z,nex[cnt]=head[x],head[x]=cnt;}
inline int C(int n,int m){return fr[n]*inv[m]%MOD*inv[n-m]%MOD;}
inline int power(int x,int y)
{
int s=1;
for(;y;y>>=1,x=x*x%MOD)if(y&1)s=s*x%MOD;
return s;
}
void dfs(int x,int fa)
{
siz[x]=1,f[x][1]=1;
for(int i=head[x];i;i=nex[i])
{
if(to[i]==fa)continue;
dfs(to[i],x),memcpy(tmp,f[x],sizeof(tmp)),memset(f[x],0,sizeof(f[x]));
if(v[i]==1)
{
for(int j=1;j<=siz[x];++j)
{
for(int t=j;t<=j+siz[to[i]]-1;++t)
f[x][t]=(f[x][t]+tmp[j]*((f[to[i]][siz[to[i]]]-f[to[i]][t-j])%MOD+MOD)%MOD*C(t-1,j-1)%MOD*C(siz[x]+siz[to[i]]-t,siz[x]-j))%MOD;
}
}
else
{
for(int j=1;j<=siz[x];++j)
{
for(int t=j+1;t<=siz[to[i]]+j;++t)
f[x][t]=(f[x][t]+tmp[j]*f[to[i]][t-j]%MOD*C(t-1,j-1)%MOD*C(siz[x]+siz[to[i]]-t,siz[x]-j))%MOD;
}
}
siz[x]+=siz[to[i]];
}
for(int i=2;i<=siz[x];++i)f[x][i]=(f[x][i]+f[x][i-1])%MOD;
}
inline void mian()
{
fr[0]=inv[0]=1,read(T);int x,y;char ch;
for(int i=1;i<=1000;++i)fr[i]=fr[i-1]*i%MOD;
inv[1000]=power(fr[1000],MOD-2);
for(int i=999;i>=1;--i)inv[i]=inv[i+1]*(i+1)%MOD;
while(T--)
{
read(n),memset(head,0,sizeof(head)),memset(f,0,sizeof(f)),cnt=0;
for(int i=1;i<n;++i)read(x),ch=getchar(),read(y),++x,++y,add(x,y,ch=='<'),add(y,x,ch=='>');
dfs(1,0),write(f[1][n],'\n');
}
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!