[NOIP 2024] 树的遍历
题目传送门
题解
这道题是笔者第一次自己切掉一道非类模板的紫,虽然耗费一周在考场上根本写不出来(还是太菜了
这道题在考场尝试暴力写第一档部分分,但是暴力并不好写,挂完了。
上文是吐槽,下文开始解题。
观察到这是一道计数题,不是直接组合就是 dp,发现对于不同的关键边答案不同,似乎不能直接组合,图又是一棵树,所以考虑树形 DP。根据部分分分析。
Solution \(0\)
先考虑链性质和菊花图。
显然,一条链的从一个关键边出发形成的树也一定是这条链,所以答案是 \(1\)。
菊花图从一个关键边出发,起点固定,则答案就是剩下的 \(n-2\) 条边的排列个数,即 \((n-2)!\)。但不同的关键边可能会有重叠的部分,需要减去重叠的部分。
性质 \(1\):根的所有儿子中,任意三条关键边没有重叠部分。
证明:因为一条关键边可以作为起点或终点,所以两条关键边才可能一个起点一个终点从而重叠。
两条关键边重叠的部分即为剩下的 \(n-3\) 条边形成的排列,即 \((n-3)!\)。所以菊花图的答案即为:\(k \times (n-2)! - C_{k}^2 \times (n-3)!\)。给出化简后式子的代码:
void solve1(){
int ans=k*fact[n-3]%mod*(2*n-k-3)%mod*ksm(2,mod-2)%mod;
cout<<ans<<"\n";
}
Solution \(1\)
考虑 \(k=1\) 的时候怎么做。
由于 \(k=1\),所以此时没有重叠的部分,观察一条边相邻的边只有其父亲儿子与兄弟(即同一个父亲的点),所以走到儿子后仍然会回到原边,所以其儿子是独立的,可以直接计算到答案中。而由于只有一个父亲边,所以只能走一次父亲,所以走到父亲的时候一定会将其子树外的所有点走遍,也可以直接记录的答案中。设走到父亲前一条边为 \(x\),父亲走完下来走的一条边为 \(y\),发现连接 \((x,fa),(fa,y)\) 与直接连接 \((x,y)\) 没有什么区别,所以兄弟之间的答案也可以直接计算到答案中。
至此,我们将问题分成了三步解决:
- 儿子们的方案数乘积
- 父亲即以上的方案数乘积
- 兄弟之间的方案数乘积
我们设 \(dp1_x\) 为以 \(x\) 点为儿子的边为根的子树内答案数量。
先考虑兄弟之间的方案数乘积,同菊花图,固定了起点,答案即为 \((son_{fa}-1)!\)。
然后儿子的方案数如何求解呢?目前固定了父亲节点,则将所有儿子进行排列,然后从父亲依次连接,答案即为排列个数 \(dp1_x=\prod dp1_v \times son_x!\)。
而父亲以上的方案数乘积,相当于固定了一个儿子节点求解,也相当于固定了起点,但是可以选择任意一个儿子连接父亲,所以答案应该乘上 \(son_y\),\(y\) 是其包含关键边的儿子,则答案即为 \(dp1_x=\prod dp1_v \times son_y \times (son_{x}-1)!\)。注意,当 \(x=1\) 的时候,由于没有父亲了,所以不能乘上 \(son_y\) 了。
我们令 \(dp2_x=\prod dp2_v \times (son_x)!\),我们发现关键边的父亲 \(fa\) 的答案 \(ans_{fa}=\prod dp2_v \times (son_{fa}-1) \times son_fa=dp2_x\),发现除了根节点,其他点 \(x\) 的 \(ans_x=dp2_x\),而根节点由于没有父亲所以 \(ans_1=\dfrac{dp2_1}{son_1}\)。所以答案即为 \(ans_1\),这个东西显然可以 \(O(n)\) 预处理。
void dfs(int u,int v){
dp3[u]=1,dep[u]=dep[v]+1,fa[u]=v,bei[0][u]=v;
for(int i=1;i<=18;i++) bei[i][u]=bei[i-1][bei[i-1][u]];
for(int i=head[u];i;i=nxt[i]){
int dao=ver[i];
if(dao==v) continue;
dfs(dao,u);
son[u]++,dp3[u]=dp3[u]*son[u]%mod*dp3[dao]%mod;
}
}
void solve3(){
dfs(1,0);
cout<<(dp3[1]*ksm(son[1],mod-2)%mod)<<"\n";
}
Solution \(2\)
考虑 \(k=2\) 该怎么做。
考虑容斥,显然我们可以先将答案变为 \(2 \times ans_1\),然后减去重复的部分。可以固定一个点进行求解,算能包含另外一个点为初始边的方案数。这里分两种情况进行讨论:
设 \(dp3_x\) 为固定两个点后以 \(x\) 为儿子的边的重叠部分方案数。
- 若两个点形成祖先后代关系,假设固定祖先节点,可以只考虑后代的子树。目前固定了父亲节点,问以 \(x\) 为根的方案数有多少。很明显,若以 \(x\) 为根,则 \(x\) 除去连接儿子的度数只能为 \(1\),所以该点一定要在与兄弟排列的尾端。故该子树的答案即为 \(dp3_x=\prod dp2_v \times (son_{x}-1)!=\dfrac{dp2_x}{son_x}\)。
考虑该后代的父亲,很明显也是相当于固定父亲节点求以 \(fa\) 为根的方案数是多少,所以答案也为 \(dp3_{fa}=\prod dp3_v \times (son_{fa}-1)!=\dfrac{dp2_{fa}}{son_{fa} \times son_x}\)。
发现除以的数等于后代的父亲直到祖先的儿子的 \(son\) 之积,因为到达祖先之后,上面的方案数就相同了。
- 若两个点不是祖先后代关系,由于 \(lca\) 以上的部分方案相同,所以从 \(lca\) 向下考虑什么情况会重叠,此时求解儿子的时候就可化为两个祖先后代关系。由于两个点要同时为根,所以一个为 \(lca\) 儿子排列的起点,一个为终点。
设 \(x=\prod_{v_1=fa_{v_1}} son_{v_1} \times \prod_{v_2=fa_{v_2}} son_{v_2}\)。
所以此时答案为 \(dp3_{lca}=\prod dp3_v \times (son_{lca}-2)!=\dfrac{dp2_{lca}}{x \times son_{lca} \times (son_{lca}-1)}\)。
若 \(lca \ne 1\),那么可以选择除终点外的任意一个点连接父亲,答案乘上 \(son_{lca}-1\),即 \(dp3_{lca}=\dfrac{dp2_{lca}}{x \times son_{lca}}\),答案为 \(\dfrac{dp2_{1}}{x \times son_{lca} \times son_{1}}\)。
若 \(lca = 1\),由于我们原本的答案 \(\dfrac{dp2_1}{son_1}\) 已经考虑了 \(1\) 没有父亲的情况,所以只用再除以 \(son_1-1\) 就可以了。
int Solve4(){
int ans1=1,ans=0,last=0;bool flag=false;
q[3]=get_fa(ver[2*q[1]],ver[2*q[1]-1]);
q[4]=get_fa(ver[2*q[2]],ver[2*q[2]-1]);
if(dep[q[3]]<dep[q[4]]) q[5]=get_son(ver[2*q[1]],ver[2*q[1]-1]),swap(q[3],q[4]);
else q[5]=get_son(ver[2*q[2]],ver[2*q[2]-1]);
q[1]=q[3],q[2]=q[4];
//若形成祖先-后代关系则 q1 q3 为后代边的父亲,q2 q4 为祖先边的父亲,q5 为祖先边的儿子
int Lca=lca(q[3],q[4]),Lca1=lca(q[3],q[5]);
while(q[1]!=Lca) ans1=ans1*son[q[1]]%mod,q[1]=fa[q[1]];
while(q[2]!=Lca) ans1=ans1*son[q[2]]%mod,q[2]=fa[q[2]];
//乘上到 Lca 的所有 son_x
ans1=dp3[1]*ksm(son[1],mod-2)%mod*ksm(ans1,mod-2)%mod;
if(Lca1==q[5]) ans1=ans1;//形成祖先-后代关系
else if(Lca!=1) ans1=ans1*ksm(son[Lca],mod-2)%mod;//lca != 1
else ans1=ans1*ksm(son[1]-1,mod-2)%mod;//lca = 1
return ans1;
}
void solve4(){
dfs(1,0);int ans=dp3[1]*ksm(son[1],mod-2)%mod*2%mod;
int ans1=Solve4();
cout<<((ans-ans1)%mod+mod)%mod<<"\n";
//两倍答案减去重叠方案数
}
Solution \(3\)
考虑 \(k \le 8\) 的情况怎么做。
我的第一眼想法是进行容斥,答案加上奇数个点的答案减去偶数个点的答案。但是问题来到了如何求多个关键边的重叠部分。受 Solution \(2\) 的启发,考虑枚举 \(lca\)。
- 先考虑这若干条关键边未形成祖先后代关系。由于固定了 \(lca\),所以 \(lca\) 以上的方案一定一样,只考虑 \(lca\) 内的子树。由于性质 \(1\),所以其应该是两条链。此时发现答案只与两条链最下面的点相关。因为根据 Solution \(2\),这两个点会一直沿着各自链向上,所以一定能包含各自链上的关键边作为根的情况。
那考虑之前的容斥,奇数个点加,偶数个点减。那设一条关键边儿子之前的答案为 \(ans\),发现如果加入这条边且该边不是该链最后一个节点,那么 \(newans=-ans\);如果是最后一个节点,则 \(newans=newans+1\);不加入的话,\(newans=newans+ans\),神奇的是这样变完发现 \(newans=1\)。所以新遇到一个边的儿子,只需让答案变为 \(1\),向上走的时候乘上 \(son_x\) 即可。
所以现在一条链的答案只与最上面的节点有关,显然,现在可以预处理出一个链的答案了。
每遇到一条新的边的儿子,发现此时 \(ans\) 就是这些关键边形成祖先后代关系的答案,祖先即为这个新加入的边。记录下来答案之后将 \(ans=1\)。
设 \(dp4_x\) 为以 \(x\) 为儿子的关键边子树的链答案总和。
对于一个 \(lca \ne 1\),答案即为 \(\sum_{i=1}^{son_{lca}} \sum_{j=i+1}^{son_{lca}} \dfrac{dp3_1}{son_1 \times dp4_i \times dp4_j \times son_{lca}}\)。
对于 \(lca=1\),答案即为 \(\sum_{i=1}^{son_{lca}} \sum_{j=i+1}^{son_{lca}} \dfrac{dp3_1}{son_1 \times dp4_i \times dp4_j \times (son_{lca}-1)}\)。
先不管 \(son_{lca}\),把 \(\dfrac{dp3_1}{son_1}\) 提出来,设 \(y=\sum_v \dfrac{1}{dp4_v}\),其中 \(v\) 比 \(i\) 先加入,发现答案即为 \(\sum_{i=1}^{son_{lca}} y \times \dfrac{1}{dp4_i}\)。
所以统计答案的时候,我们将 \(dp4\) 取倒数,遇到多个 \(dp4\) 值合并时直接相加即可。
最后枚举 \(lca\) 乘上 \(son_{lca}\) 或 \(son_{lca}-1\),加上以该点位祖先形成祖先后代关系的点的答案,得到的值乘上 \(\dfrac{dp3_1}{son_1}\) 就做完了。
时间复杂度 \(O(n)\)。
void dfs(int u,int v){
dp3[u]=1,dep[u]=dep[v]+1,fa[u]=v,bei[0][u]=v;
for(int i=1;i<=18;i++) bei[i][u]=bei[i-1][bei[i-1][u]];
for(int i=head[u];i;i=nxt[i]){
int dao=ver[i];
if(dao==v) continue;
dfs(dao,u);
son[u]++,dp3[u]=dp3[u]*son[u]%mod*dp3[dao]%mod;
}
}
int Mul;
int ans[N],ans1[N],vis[N];
void dfs1(int u,int v){
for(int i=head[u];i;i=nxt[i]){
int dao=ver[i];
if(dao==v) continue;
dfs1(dao,u);
ans[u]=(ans[u]+dp1[dao]*dp1[u]%mod)%mod,dp1[u]+=dp1[dao];
}
dp1[u]=dp1[u]*ksm(son[u],mod-2)%mod;
if(vis[u]) ans1[u]=dp1[u],dp1[u]=1;
}
void solve5(){
dfs(1,0);
for(int i=1;i<=k;i++)
vis[get_son(ver[2*q[i]],ver[2*q[i]-1])]=true;
dfs1(1,0);
Mul=dp3[1]*ksm(son[1],mod-2)%mod;
int ans2=Mul*k%mod,ans3=0;
for(int i=1;i<=n;i++)
ans3=(ans3+ans[i]*ksm(i==1?son[i]-1:son[i],mod-2)%mod+ans1[i])%mod;
ans3=ans3*Mul%mod;
ans2=((ans2-ans3)%mod+mod)%mod;
cout<<ans2<<"\n";
}
撒花~~

浙公网安备 33010602011771号