关于树上背包

198 阅读2分钟

关于树上背包

在背包问题中有这么一类问题蛮特殊--树上背包

树上背包的问题就特殊在会有依赖性,然后就是一整个时间复杂度的小trick。

ex-1:

给定n(2000\leq2000)个点的有根树, 每个点有权值,权值可能为负数。

多组询问,询问以u为根的子树,个数恰好为m(n\leq n)的,包含u点的连通块的最大权值和。

很明显的树上背包问题

状态数量是onno(n * n)量级的,转移是ono(n)的,所以很多人可能会认为这样的复杂度无法通过本题。

其实不然,这样的题目就是树上背包的典型trick。

 void dfs(int u)
 {
     for (int i = h[u]; ~i; i = ne[i])
     {
         int j = e[i];
 ​
         dfs(j);
 ​
         for (int k = 0; k <= sz[u] + sz[j]; k ++ ) tmp[k] = -INF;
 ​
         for (int _i = 0; _i <= sz[u]; _i ++ )
             for (int _j = 0; _j <= sz[j]; _j ++ )
                 tmp[_i + _j] = max(tmp[_i + _j], f[u][_i] + f[j][_j]);
 ​
         for (int k = 0; k <= sz[u] + sz[j]; k ++ ) f[u][k] = tmp[k];
         sz[u] += sz[j];
     }
 ​
     sz[u] ++ ;
 ​
     for (int i = sz[u]; i > 0; i -- )
         f[u][i] = f[u][i - 1] + a[u];   
 ​
     f[u][0] = 0;
 }

只列出了重要的函数,可以发现是三个for循环嵌套的。

通过势能的一些分析最终可以得出,这个函数的时间复杂度就是n2n^2

所以其实这样的题目是可以通过这样比较暴力的思路通过的。

ex-2:

其他的都和上面一样,(n50000n\leq50000)(m100m\leq100)

 void dfs(int u)
 {
 ​
     f[u][0] = 0;
 ​
     for (int i = h[u]; ~i; i = ne[i])
     {
         int j = e[i];
 ​
         dfs(j);
 ​
         for (int k = 0; k <= min(V, sz[u] + sz[j]); k ++ ) tmp[k] = -INF;
 ​
         for (int _i = 0; _i <= min(sz[u], V); _i ++ )
             for (int _j = 0; _j <= min(V - _i, sz[j]); _j ++ )
                 if (_i + _j <= V) tmp[_i + _j] = max(tmp[_i + _j], f[u][_i] + f[j][_j]);
 ​
         for (int k = 0; k <= min(sz[u] + sz[j], V); k ++ ) f[u][k] = tmp[k];
         sz[u] += sz[j];
     }
 ​
     sz[u] ++ ;
 ​
     for (int i = min(sz[u], V); i > 0; i -- )
         f[u][i] = f[u][i - 1] + a[u];   
 ​
     f[u][0] = 0;
 }

这样的代码又很像o(nm2n * m^2)

但其实又不然,这样和每个size取完min后,可以证明出这个函数的时间复杂度为o(nm)o(n * m)

至此这个时间复杂度就足以通过本题

ex-3:

其他的都和上面一样,但是物品有重量(n\leq1000)(m\leq10000)

这时好像那种分析时间复杂度的方法就不太适用了。

因为毕竟很多地方没办法剪掉,而是要把所有体积都枚举到,时间复杂度是妥妥的o(nm2)o(n * m^2)

在这种情况下,这个题目的解法就有些诡异了,而且非常的没有拓展性,也纯属是因为在分享树上背包把这个数据范围拿出来。

正解是使用树的欧拉序来DP(给出代码:)

 void solve()
 {
     init();
 ​
     memset(h, -1, sizeof h);
 ​
     cin >> n >> m;
 ​
     for (int i = 2; i <= n; i ++ )
     {
         int fa;
         cin >> fa;
 ​
         add(fa, i, 1);
     }
 ​
     for (int i = 1; i <= n; i ++ ) cin >> a[i];
     for (int i = 1; i <= n; i ++ ) cin >> b[i];
 ​
     dfs(1);
     
     memset(f[n + 1] + 1, -0x3f, 4 * (m));
 ​
     for (int i = tot; i >= 1; i -- )
     {
         int u = id[i];
 ​
         for (int j = 0; j <= m; j ++ )
         {
             f[i][j] = f[r[u] + 1][j];
 ​
             if (j >= b[u])
                 f[i][j] = max(f[i][j], f[i + 1][j - b[u]] + a[u]);
         }
     }
 ​
     for (int i = 0; i <= m; i ++ )
         if (f[1][i] >= 0) cout << f[1][i] << endl;
         else cout << 0 << endl;
 }

这样的时间复杂度就来到了o(nm)o(n * m)

了解一下就可吧,感觉这个思想没办法扩展了。


最后总结一下就是,遇到这样的树上背包,尤其要注意时间复杂度的trick。

如果你发现不了这个trick,应该还是很难通过的。

✅✅✅

树上背包就说到这吧~