树上背包的上下界优化

最近做了几道树上背包的题目,很多题目的数据范围都很小,但实际上树上背包有多种方式可以优化到 O(nm)n 为节点数,m 为体积的值域),比如先序遍历优化(何森《先序遍历用于优化树形背包问题》),求泛化物品的并(徐持衡《浅谈几类背包题》)……经过一番学习,觉得还是上下界优化理解起来最简单,也比较好写,适用范围广,唯一比其它做法复杂的地方就是复杂度分析。

例题讲解

这里以一道经典的树上背包作为例题:【数据加强版】选课

直接把我出的数据加强版放上来了..反正题面里有原题链接QAQ

注:本文中用 ai 代指题面中的 si

O(nm2) 做法

fu,i 表示以 u 为根的子树中选 i 门课的最大得分,那么 fu,i=minfa[vj]=u,kj=i1(f[vj][kj])+au,而这个转移可以通过背包实现,依次合并每棵子树,每次合并时枚举 ikjfu,i=max(fu,i,fu,ikj+fvj,kj)

需要倒序枚举 i 防止状态在转移前被覆盖。否则的话dp数组要多一维。

由于可能是森林,所有没有直接先修课的节点,父亲视为节点 0,实际上就要选 m+1 个节点。

参考代码:

void dfs(int u)
{
    f[u][1]=a[u];
    int i,j,k,v;
    for (i=head[u];i;i=nxt[i])
    {
        v=to[i];
        dfs(v);
        for (j=m+1;j>=1;--j)
        {
            for (k=1;k<j;++k)
            {
                f[u][j]=max(f[u][j],f[u][j-k]+f[v][k]);
            }
        }
    }
}

上下界优化

注意背包转移的这部分:

        for (j=m+1;j>=1;--j)
        {
            for (k=1;k<j;++k)
            {
                f[u][j]=max(f[u][j],f[u][k]+f[v][j-k]);
            }
        }

实际上,这里面有很多状态都是没有意义的:

  1. 转移时已经合并了大小之和为 s 的一些子树,那么 fu,i(i>s) 实际上是没有意义的。

  2. fv,i(i>siz[v]) 也是没有意义的。

  3. fu,i(i>m) 是没有作用的。

所以,可以对 jk 的枚举范围进行优化:

void dfs(int u)
{
    siz[u]=1;
    f[u][1]=a[u];
    int i,j,k,v;
    for (i=head[u];i;i=nxt[i])
    {
        v=to[i];
        dfs(v);
        for (j=min(m+1,siz[u]+siz[v]);j>=1;--j)
        {
            for (k=max(1,j-siz[u]);k<=siz[v]&&k<j;++k)
            {
                f[u][j]=max(f[u][j],f[u][j-k]+f[v][k]);
            }
        }
        siz[u]+=siz[v];
    }
}

复杂度分析

可以参考这篇博客

形象的解释

每个点对都只会在 lca 处合并一次,所以总的复杂度是 O(n2) 的。

这个解释很简洁,需要自己意会一下..

严格?证明

Tu 为处理子树 u 的总用时,那么:

Tu=(fa[vi]=uTvi)+tutu=1+(1+siz[v1])×siz[v1]+(1+siz[v1]+siz[v2])×siz[v2]++siz[u]×siz[vk]=1+fa[vi]=usiz[vi]×(siz[u]+1)=siz[u]2

对于叶子节点 uT(u)=1 ,是 O(siz[u]2) 的。

对于儿子都是叶子节点的节点 u,由于平方和小于和平方,fa[vi]=uTvi 也是 O(siz[u]2) 的。

可以这样递归地说明,对于任意节点 ufa[vi]=uTvi 都是 O(siz[u]2) 的。

又因为 t(u)O(siz[u]2) 的,T(u) 就是 O(siz[u]2) 的。

所以解决整个问题就是 O(n2) 的。

严格!证明

枚举过程中还要对 m 取 min ,所以应该是这样的:

tu=1+min(m,1+siz[v1])×min(m,siz[v1])+min(m,1+siz[v1]+siz[v2])×min(m,siz[v2])++min(m,siz[u])×min(m,siz[vk])m×siz[u]

所以,t(u)O(min(siz[u],m)×siz[u]) 的。

对于 siz[u]mT(u)O(siz[u]2) 的。

对于 siz[u]>mfa[vi]=u,siz[vi]mTviO((fa[vi]=u,siz[vi]msiz[vi])2) 的;fa[vi]=u,siz[vi]>mTviO(m×fa[vi]=u,siz[vi]>msiz[vi]) 的;所以,T(u)O(m×siz[u]) 的。

所以,解决整个问题是 O(nm) 的。

其它例题

【数据加强版】道路重建

dl代码

我出的那两道数据加强版略有些毒瘤..(n×m108

大约需要这样写:

#include <iostream>
#include <cstdio>
#include <algorithm>

using namespace std;

void dfs(int u);
void add(int u,int v);

const int N=100010;

int head[N],nxt[N],to[N],cnt;
int n,m,a[N],f[100000010],siz[N];

int main()
{
    int i,k;

    scanf("%d%d",&n,&m);

    for (i=1;i<=n;++i)
    {
        scanf("%d%d",&k,a+i);
        add(k,i);
    }

    dfs(0);

    printf("%d",f[m+1]);

    return 0;
}

void add(int u,int v)
{
    nxt[++cnt]=head[u];
    head[u]=cnt;
    to[cnt]=v;
}

void dfs(int u)
{
    siz[u]=1;
    f[u*(m+2)+1]=a[u];
    int i,j,k,v;
    for (i=head[u];i;i=nxt[i])
    {
        v=to[i];
        dfs(v);
        for (j=min(m+1,siz[u]+siz[v]);j>=1;--j)
        {
            for (k=max(1,j-siz[u]);k<=siz[v]&&k<j;++k)
            {
                f[u*(m+2)+j]=max(f[u*(m+2)+j],f[u*(m+2)+j-k]+f[v*(m+2)+k]);
            }
        }
        siz[u]+=siz[v];
    }
}

关于另一种 O(nm) 做法

一开始我在洛谷发了篇选课的题解,然后没过...

那篇题解用的是求泛化物品的并(徐持衡《浅谈几类背包题》)

虽然说洛谷好像还没有上下界优化的题解..但最近好几篇题解没过审,都不太想在洛谷发题解了...

posted @   ouuan  阅读(3348)  评论(8编辑  收藏  举报
编辑推荐:
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
阅读排行:
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】
点击右上角即可分享
微信分享提示