ICPC 2022济南 C:DFS Order 2 题解(回退背包)

C:

题意:给你一棵以1为根的树,输出一个n方的矩阵,即:第 i 行第 j 列表示在所有的DFS序中,第 i 个点出现在第 j 个位置的次数。(n<=500)


Solution:

透过样例我们可以看出父亲结点的那一行要比子结点的数字靠前,因为所有的DFS序都是访问完父亲再访问儿子,于是每棵子树其实可以看做一个子问题:我们只需要求出,每个子结点在它父亲这棵子树内DFS序的位置,然后再从父亲那一行的答案推导出自己的答案。

具体的,设 f[u][i] 表示 u 在它父亲这棵子树内的DFS序为 i 的情况数。想要求出最后的答案 ans[][] 矩阵,就让父亲那一行的答案 ans[fa][] 平移乘上 f[u] 数组,递归地向下求。

ans[1][1] 等于多少,即所有分叉处 siz 的阶乘。无论什么位置,不同的DFS序种数都是以阶乘计算。

接下来我们只需要求每个点的 f 数组,即每个点在父亲这棵树内排第几。

很容易想到一个背包的解法:

在DFS序中,每个子树都要先访问完才能跳出,若 u 排名为 i ,说明它前面先访问了大小和为 i-1 的兄弟子树,将每个兄弟树看做物品,背包求出装满 i-1 的空间有多少种方案,由于背包内的物品还可以交换位置,因此还要记录装了多少个物品,答案最终还要乘以物品数量的阶乘。

dp[i][j] 表示装了 i 棵子树,总大小为 j 的方案数,这样求出每个儿子的 f[u] 都是 n^3 的复杂度,因为物品是 “所有的兄弟树”,自己这棵子树是不能算入物品的。

但是发现这题满足回退背包的科技前提:求的是方案数不是最值,而且所有儿子的转移是平等的。

每棵子树对 dp 数组的贡献无非就是加上自己这棵树大小的平移量,那么按原路再减回去,相当于没有加过自己这棵树。

这时所有的儿子只需要求一遍背包,计算 u 的 f 数组时再把 u 这棵子树的贡献从背包中回退,总复杂度变成 n^3。


至初学者:刚开始理解回退背包会比较发懵,自己放几个物品当例子手动写一下 dp 数组就会了。

然后写代码的时候还会发懵,这个回退过程不仅要把加号变成减号这么简单,还需要完全原路返回,包括滚动优化时的枚举顺序,以及贡献传递的方向,才能保证回退不出问题。

#include <algorithm> #include <iostream> #include <cstring> #include <cstdio> #include <cmath> #define FOR() ll le=e[u].size();for(ll i=0;i<le;i++) #define QWQ cout<<"QwQ\n"; #define ll long long #include <vector> #include <queue> #include <map> using namespace std; const ll N=555; const ll qwq=303030; const ll inf=0x3f3f3f3f; const ll p=998244353; ll T; ll n,m; vector <ll> d[N]; vector <ll> e[N]; ll siz[N]; ll fang[N]; ll f[N]; ll g[N][N]; ll dp[N][N]; ll dp1[N][N]; ll ans[N][N]; inline ll read() { ll sum = 0, ff = 1; char c = getchar(); while(c<'0' || c>'9') { if(c=='-') ff = -1; c = getchar(); } while(c>='0'&&c<='9') { sum = sum * 10 + c - '0'; c = getchar(); } return sum * ff; } void qiu() { f[0] = 1; for(ll i=1;i<=N-10;i++) f[i] = f[i-1] * i %p; } inline ll ksm(ll aa,ll bb) { ll sum = 1; while(bb) { if(bb&1) sum = sum * aa %p; bb >>= 1; aa = aa * aa %p; } return sum; } void DFS(ll u,ll fa) { for(ll i=0;i<d[u].size();i++) if(d[u][i]!=fa) e[u].push_back(d[u][i]); siz[u] = 1; fang[u] = f[e[u].size()]; FOR() { ll v = e[u][i]; DFS(v,u); siz[u] += siz[v]; (fang[u] *= fang[v]) %= p; } } void TREE(ll u,ll fa) { memset(dp,0,sizeof(dp)); dp[0][0] = 1; ll qian = 0; ll le = e[u].size(); for(int i=1;i<=le;i++) { ll v = e[u][i-1]; for(ll j=i-1;j>=0;j--) { for(ll k=0;k<=qian;k++) { (dp[j+1][k+siz[v]] += dp[j][k]) %= p; } } qian += siz[v]; } ll zong = fang[u] * ksm(f[le],p-2) %p; ll wo = ksm(fang[u],p-2); for(ll i=0;i<le;i++) { ll v = e[u][i]; for(int j=0;j<=le;j++) for(int k=0;k<=siz[u];k++) dp1[j][k] = dp[j][k]; for(ll j=1;j<=le;j++) { for(ll k=siz[v];k<=qian;k++) { (dp1[j][k] -= dp1[j-1][k-siz[v]] - p) %= p; } } for(ll j=le-1;j>=0;j--) { for(ll k=1;k<=siz[u]-1;k++) (g[v][k] += dp1[j][k-1] * f[j] %p * f[le-1-j] %p * zong %p) %= p; } for(ll j=1;j<=n;j++){ for(ll k=1;k<=siz[u]-1;k++) { if(j+k>n) break; (ans[v][j+k] += ans[u][j] * wo %p * g[v][k] %p) %= p; } } } for(int i=0;i<le;i++) TREE(e[u][i],u); } int main() { qiu(); ll x,y; n = read(); for(ll i=1;i<n;i++) { x = read(); y = read(); d[x].push_back(y); d[y].push_back(x); } DFS(1,1); ans[1][1] = fang[1]; TREE(1,1); for(ll i=1;i<=n;i++) { for(ll j=1;j<=n;j++) { cout<<ans[i][j]<<" "; } cout<<endl; } return 0; }

__EOF__

本文作者枫叶晴
本文链接https://www.cnblogs.com/maple276/p/18006504.html
关于博主:菜菜菜
版权声明:呃呃呃
声援博主:呐呐呐
posted @   maple276  阅读(182)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· winform 绘制太阳,地球,月球 运作规律
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· AI 智能体引爆开源社区「GitHub 热点速览」
· Manus的开源复刻OpenManus初探
· 写一个简单的SQL生成工具
点击右上角即可分享
微信分享提示