题意

$n$ 个点的树 每个点有一个体积 $v_i$ 和收益 $w_i$。

只能选择不相邻的物品 要求你输出 $\forall i \in [1,m]$,容量为 $i$ 的背包收益最大的方案数

$n \leq 50,m \leq 5000$

题解

很 naive 的 dp 大概是 $f[i][j][0/1] $ 表示处理 $i$ 号点子树 已经用了 $j$ 容量,是否选择这个点的方案数 转移的时候搞个树形背包 复杂度 $O(nm^2)$

官方题解看起来很强大。。。

我们考虑沿用树形背包的一贯做法:按照 dfs 序来 dp。发现在 dp 过程中 一个点能不能选只取决于它的 father 是否选了 但是如果只记录 father 的值的话根本转移不了($i \to i+1$的时候你不知道dfs序是 $i+1$ 的点上面的情况)、暴力的记录整个树复杂度就爆炸了,一个稍微好一点的想法是注意到 dfn 相邻的点的移动轨迹一定是往上跳若干步然后往下走一步(这个性质很有用),所以我们只需要记录这个点到根的路径上是否被选择就好了。

但是这样显然还是会被链卡没。

官方题解告诉我们先进行轻重链剖分 然后划定 dfs 序的时候最后遍历重儿子 这样你在 $i \to i+1$ 的时候会直接跳过上面的一段重链,所以我们发现对于每个点只需要记录每个重链的链底就好了 根据轻重链剖分的性质 这个数量就是一个点到根轻边的数量 是 $O(\log n)$ 的。

于是设 $f[i][S][j]$ 表示考虑到了 dfn 为 $i$ 的点 当前到根的链底的点的状态是 $S$ ,已经用了容量 $j$ ,$S$的状态数是 $O(2^{\log n}) = O(n)$ 的,转移可以写成 $O(1)$ 所以复杂度变成了 $O(n^2m)$

转移不会写 看了个代码 于是决定加一加注释加深理解

Code:

#include<bits/stdc++.h>

#define fi first
#define se second
#define U unsigned
#define P std::pair<int,int>
#define LL long long
#define pb push_back
#define MP std::make_pair
#define all(x) x.begin(),x.end()
#define CLR(i,a) memset(i,a,sizeof(i))
#define FOR(i,a,b) for(int i = a;i <= b;++i)
#define ROF(i,a,b) for(int i = a;i >= b;--i)
#define DEBUG(x) std::cerr << #x << '=' << x << std::endl

const int MAXN = 100+5;
const int MAXM = 5000+5;

int n,m;
int a[MAXN],b[MAXN];// 重量 收益

struct Edge{
    int to,nxt;
}e[MAXN<<1];
int head[MAXN],cnt;

inline void add(int u,int v){
    e[++cnt] = (Edge){v,head[u]};head[u] = cnt;
    e[++cnt] = (Edge){u,head[v]};head[v] = cnt;
}

int sz[MAXN],son[MAXN];

inline void dfs1(int v,int fa=0){
    sz[v] = 1;son[v] = 0;
    for(int i = head[v];i;i = e[i].nxt){
        if(e[i].to == fa) continue;
        dfs1(e[i].to,v);sz[v] += sz[e[i].to];
        if(sz[son[v]] < sz[e[i].to]) son[v] = e[i].to;
    }
}

int ts;
int dfn[MAXN],nfd[MAXN],tp[MAXN],fa[MAXN];

inline void dfs2(int v,int ntp,int fa=0){
    dfn[v] = ++ts;nfd[dfn[v]] = v;tp[v] = ntp;
    ::fa[v] = fa;
    for(int i = head[v];i;i = e[i].nxt){
        if(e[i].to != fa && e[i].to != son[v]){
            dfs2(e[i].to,e[i].to,v);
        }
    }
    if(son[v]) dfs2(son[v],ntp,v);
}

struct Node{
    int f;LL g;
    Node(int f=0,LL g=0) : f(f),g(g) {}
}f[2][MAXN][MAXM],ans[MAXM];

inline void update(Node &x,const Node &y){
    if(x.f > y.f) return;
    if(x.f < y.f) x = y;
    else x.g += y.g;
}

int now;
std::vector<int> cha[MAXN];
int t1[MAXN];

inline void Solve(){
    scanf("%d%d",&n,&m);
    FOR(i,1,n) scanf("%d%d",a+i,b+i);
    FOR(i,2,n){
        int u,v;scanf("%d%d",&u,&v);
        add(u,v);
    }
    dfs1(1);dfs2(1,1);
    FOR(i,1,n){
        cha[i].clear();
        int t = i;
        while(t) cha[i].pb(t),t = fa[tp[t]];
    }
    CLR(f,0);
    f[now][0][0] = Node(0,1);
    FOR(i,1,n){
        int t = 0;
        int fat = -1;
        FOR(S,0,(1<<cha[nfd[i]].size())-1) FOR(k,0,m) f[now^1][S][k] = Node(0,0);
        for(auto x:cha[nfd[i-1]]){
            if(x == fa[nfd[i]]) fat = t;
            t1[t] = -1;
            FOR(j,0,(int)cha[nfd[i]].size()-1){
                if(cha[nfd[i]][j] == x){
                    t1[t] = j;
                    break;
                }
            }
            ++t;
        }
        // 转移时主要目的是得到 S 的后继状态 nxt 
        // 首先处理出任意一个点需要记录的点的集合 记做 cha[i] 那么cha[i-1]和cha[i]的交的状态就一定确定好了 现在就是确定当前点的状态和 father 的状态(判断是否转移)
        // 于是 t1[i] 存 nfd[i-1] 的第 i 位是 nfd[i] 的哪一位(或者不存在)转移即可
        FOR(S,0,(1<<cha[nfd[i-1]].size())-1){
            int nxt = 0;
            FOR(j,0,(int)cha[nfd[i-1]].size()-1)
                if((S>>j)&1 && t1[j] != -1) nxt |= (1<<t1[j]);
            FOR(k,0,m){
                if(f[now][S][k].g){
                    update(f[now^1][nxt][k],f[now][S][k]);
                    if(!((S>>fat)&1) && k+a[nfd[i]] <= m){
                        update(f[now^1][nxt|1][k+a[nfd[i]]],Node(f[now][S][k].f+b[nfd[i]],f[now][S][k].g));
                    }
                }
            }
        }
        now ^= 1;
    }
    FOR(i,0,m) ans[i] = Node(0,0);
    FOR(S,0,(1<<cha[nfd[n]].size())-1){
        FOR(i,0,m){
            update(ans[i],f[now][S][i]);
        }
    }
    FOR(i,1,m) printf("%lld%c",ans[i].g," \n"[i==m]);
    FOR(i,0,n) head[i] = dfn[i] = nfd[i] = 0;cnt = 0;ts = 0;
}

int main(){
    int T;scanf("%d",&T);
    FOR(i,1,T) printf("Case %d:\n",i),Solve();
    return 0;
}

More Things

我看到知乎上出题人说希望这个题成为以后题目的一种套路。。

这类套路大概就是将树 dp 转移到 dfn 上去做 如果只和父亲有关的话就可以用这个优化

所以以后做树 dp 的时候还是要考虑 dfn

Last modification:April 26th, 2020 at 12:03 pm
如果觉得我的文章对你有用,请随意赞赏