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;
}