1. 程式人生 > >樹上揹包的上下界優化

樹上揹包的上下界優化

最近做了幾道樹上揹包的題目,很多題目的資料範圍都很小,但實際上樹上揹包有多種方式可以優化到 \(O(nm)\)\(n\) 為節點數,\(m\) 為體積的值域),比如先序遍歷優化(何森《先序遍歷用於優化樹形揹包問題》),求泛化物品的並(徐持衡《淺談幾類揹包題》)……經過一番學習,覺得還是上下界優化理解起來最簡單,也比較好寫,適用範圍廣,唯一比其它做法複雜的地方就是複雜度分析。

例題講解

這裡以一道經典的樹上揹包作為例題:【資料加強版】選課

直接把我出的資料加強版放上來了..反正題面裡有原題連結QAQ

注:本文中用 \(a_i\) 代指題面中的 \(s_i\)

\(O(nm^2)\)
做法

\(f_{u,i}\) 表示以 \(u\) 為根的子樹中選 \(i\) 門課的最大得分,那麼 \(f_{u,i}=\min\limits_{\forall fa[v_j]=u,\sum k_j=i-1}(\sum f[v_j][k_j])+a_u\),而這個轉移可以通過揹包實現,依次合併每棵子樹,每次合併時列舉 \(i\)\(k_j\)\(f_{u,i}=\max(f_{u,i},f_{u,i-k_j}+f_{v_j,k_j})\)

需要倒序列舉 \(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\) 的一些子樹,那麼 \(f_{u,i}(i>s)\) 實際上是沒有意義的。

  2. \(f_{v,i}(i>siz[v])\) 也是沒有意義的。

  3. \(f_{u,i}(i>m)\) 是沒有作用的。

所以,可以對 \(j\)\(k\) 的列舉範圍進行優化:

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

using namespace std;

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

const int N=2510;

int head[N],nxt[N],to[N],cnt;
int n,m,a[N],f[N][N],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[0][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][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(n^2)\) 的。

這個解釋很簡潔,需要自己意會一下..

嚴格?證明

\(T_u\) 為處理子樹 \(u\) 的總用時,那麼:

\(\begin{aligned}T_u&=\left(\sum\limits_{\forall fa[v_i]=u}T_{v_i}\right)+t_u\\\\t_u&=1+(1+siz[v_1])\times siz[v_1]+(1+siz[v_1]+siz[v_2])\times siz[v_2]+\cdots+siz[u]\times siz[v_k]\\&=1+\sum\limits_{\forall fa[v_i]=u}siz[v_i]\times(siz[u]+1)\\&=siz[u]^2\end{aligned}\)

對於葉子節點 \(u\)\(T(u)=1\) ,是 \(O(siz[u]^2)\) 的。

對於兒子都是葉子節點的節點 \(u\),由於平方和小於和平方,\(\sum\limits_{\forall fa[v_i]=u}T_{v_i}\) 也是 \(O(siz[u]^2)\) 的。

可以這樣遞迴地說明,對於任意節點 \(u\)\(\sum\limits_{\forall fa[v_i]=u}T_{v_i}\) 都是 \(O(siz[u]^2)\) 的。

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

所以解決整個問題就是 \(O(n^2)\) 的。

嚴格!證明

列舉過程中還要對 \(m\) 取 min ,所以應該是這樣的:

\(\begin{aligned}t_u&=1+\min(m,1+siz[v_1])\times \min(m,siz[v_1])+\min(m,1+siz[v_1]+siz[v_2])\times \min(m,siz[v_2])+\cdots+\min(m,siz[u])\times \min(m,siz[v_k])\\&\le m\times siz[u]\end{aligned}\)

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

對於 \(siz[u]\le m\)\(T(u)\)\(O(siz[u]^2)\) 的。

對於 \(siz[u]>m\)\(\sum\limits_{\forall fa[v_i]=u,siz[v_i]\le m}T_{v_i}\)\(O\left(\left(\sum\limits_{\forall fa[v_i]=u,siz[v_i]\le m}siz[v_i]\right)^2\right)\) 的;\(\sum\limits_{\forall fa[v_i]=u,siz[v_i]>m}T_{v_i}\)\(O\left(m\times\sum\limits_{\forall fa[v_i]=u,siz[v_i]>m}siz[v_i]\right)\) 的;所以,\(T(u)\)\(O(m\times siz[u])\) 的。

所以,解決整個問題是 \(O(nm)\) 的。

其它例題

【資料加強版】道路重建