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

posted @ 2024-02-04 16:47  maple276  阅读(172)  评论(0编辑  收藏  举报