关于树上背包的复杂度证明

关于树上背包及其正确性证明

前言

2024.08.20:由于今天模拟赛 T1 想到了正解,然而我以为树上背包的复杂度是 \(O(n^3)\),然后没有继续调试我的代码,然后也没有提交,后来发现树上背包的复杂度是 \(O(n^2)\) 的,痛失 100 pts。发现我从来都没有写过树上背包,于是写下此文。

树上背包

树上背包解决这样一类问题:

给你一棵树,每个点有权值,要求你选一些点,每个子树选的个数有限制(一个范围),还有一些别的限制(如带有颜色、点与点之间要满足一定的关系等等),你要使得最终选的价值和最大(或者求选点的合法方案数)。

一般来说我们可以设状态 \(f_{i,j}\) 表示点 \(i\) 选了 \(j\) 个物品的最大价值(或是方案数之类的),也许后面还有一些状态表示,我们这里只讨论最简单的情况,即限制选的物品个数。

考虑背包的转移,我们已经把当前点的儿子的背包数组处理好了,考虑求当前点的背包数组。

那么,我们把当前点当做包,把儿子的背包数组往这个包里塞,也就是枚举一个儿子,之后枚举它的选的数量 \(k\),然后就有转移:

\[f_{i,j}\leftarrow f_{son_i,k}+f_{i,j-k} \]

中间的加法可以看做任何运算。

注意要加上选或不选当前点的贡献。

考虑这个的复杂度。我们枚举了 \(i,j,k\) 对吧,然后 \(son_i\) 的总数量也是 \(O(n)\),所以看起来这个的复杂度是 \(O(n^3)\)

但实际上,它是 \(O(n^2)\) 的!!!

为什么?

树上背包的时间复杂度

其实你直接做时间并不正确,你需要限制枚举范围,去掉无用状态,此时时间复杂度可证明是 \(O(n^2)\) 的。

考虑一个状态 \(f_{i,j}\),它肯定要满足 \(j\le sz_i\) 才有意义。

注意上面的转移式子,它实际上是每次处理完 \(son_i\) 的一个背包后,把 \(son_i\) 的背包和 \(i\) 的背包合并。

合并的时间复杂度是 \(sz_i\times sz_{son_i}\),注意这里 \(sz_i\) 是一直变化的,每合并完一个 \(son_i\)\(sz_i\) 就会加 \(sz_{son_i}\),初始时 \(sz_i=1\)

考虑这两个 \(sz\) 相乘 \(sz_x\times sz_y\) 的意义,实际可以看做在 \(x,y\) 集合内各选一个点,组成的点对的数量。

我们把两个点组成点对称作合并,那么在树上背包的过程中,两个点有且仅有在它们的 LCA 处会发生一次合并,也就是两个点只会合并一次。

而点对数目是 \(O(n^2)\) 的,所以树上背包的基础时间复杂度是 \(O(n^2)\) 的。

实现

基于 dfs 的过程进行 DP 即可。

伪代码如下:

void dfs(int x){
    sz[x]=1;
    for(each v in son[x]){
        dfs(v);
        for(int i=sz[x];i>=0;i--)
            for(int j=sz[y];j>=0;j--)
                update dp[x][i+j];
    	sz[x]+=sz[v];
    }
}

注意这里 \(sz_x\) 是实时变化的,而且要先做 DP,再把 \(sz\) 加上。

否则若先把 \(sz\) 加上,其中一部分时间就用来把 \(v\) 内部的点两两合并,但是 \(v\) 内的点我们已经合并过了,复杂度就变成了 \(O(n^3)\)

例题

JZOJ 8167 树上选点

P4516 [JSOI2018] 潜入行动

posted @ 2024-08-20 20:19  dengchengyu  阅读(64)  评论(0编辑  收藏  举报