树上背包

书上背包 -> 树型dp 和 背包 结合的问题,对于背包问题如果熟悉的话其实是很简单的一个算法。

树上背包模板

P2014 [CTSC1997] 选课

题目大意:

在大学里每个学生,为了达到一定的学分,必须从很多课程里选择一些课程来学习,在课程里有些课程必须在某些课程之前学习,如高等数学总是在其它课程之前学习。现在有 N 门功课,每门课有若干学分,分别记作 s1,s2,,s__N,每门课有一门或没有直接先修课(若课程 a 是课程 b 的先修课即只有学完了课程 a,才能学习课程 b)。一个学生要从这些课程里选择 M 门课程学习,问他能获得的最大学分是多少?

题目保证课程安排无冲突。(即不会有 ab 的先修课,b 也是 a 的先修课这类情况存在。)

1≤N≤300 , 1≤M≤300, ki=0 表示没有直接先修课 (0≤kiN,1≤si≤20)

解题:树上背包

背包的题一般就有很模式化的状态表示,都是对于一个物品的选与不选的问题;而对于树形dp,对于一个节点的信息需要到其子节点的相应信息。

在本题中对于父节点的选课情况,根据当前节点的选与不选来分情况,然后从其子节点中获取相应的信息,所以本题的解法是树上背包。

定义状态表示:dp[x][i][j]: 从x的前i个子树中选j门课程的最大学分。

初始化:本题中课程的选择依据拓扑序,因此如果想要获取子节点的信息,根节点必选,dp[x][][1] = a[x]

状态转移方程:从x的孩子节点中选,显然是一个完全背包的问题,而从孩子节点中选择的节点的个数就是常规背包中的背包容量,需要额外枚举一个维度k,用来表示从子结点中选择的课程数量。

因此对于一个孩子节点的转移:

  • \(dp[x][i][j] = \max_{k=0}^{j-1}(dp[x][i - 1][j - k] + dp[y][edges[y].size()][k])\),由于根节点必选,所有k最多到j -1.

对于所有的孩子节点,把信息并入到x节点,仅需循环x的所有子节点一遍即可。

答案:dp[0][edgse[0].size()][m]

code:

#include <iostream>
#include <vector>

using namespace std;
const int N = 3e2 + 10;
// dp[i][j][k]: 表示从i的子树中选,[j][k]构成一个分组背包,表示从前j个孩子中选,一共选k个结点,整体表示贡献的最大值
// 注意i号结点必选
int a[N], dp[N][N][N];
int n, m;
vector<int> edges[N];
// 时间复杂度O(n^3)
void dfs(int x)
{
    // 从x的子树中选, 由于x必选,所有从前i个子树中选1个的情况总是a[x]
    for (int i = 0; i <= m; i++) dp[x][i][1] = a[x];
    for (int i = 0; i < edges[x].size(); i++)
    {
        int y = edges[x][i];
        dfs(y);
        for (int j = 2; j <= m; j++)
        {
            for (int k = 0; k < j; k++)
            {
                // 这里要取max,因为dp[x][i + 1][j]这个状态会进来多次
                // 然后完全背包的val就是子树的dp值,weight就是在当前子树中选择的结点个数
                dp[x][i + 1][j] = max(dp[x][i][j], dp[x][i + 1][j]);
                dp[x][i + 1][j] = max(dp[x][i + 1][j], dp[x][i][j - k] + dp[y][edges[y].size()][k]);
            }
        }
    }
}

int main()
{
    cin >> n >> m;
    m++;
    for (int i = 1; i <= n; i++)
    {
        int x; cin >> x;
        edges[x].push_back(i);
        cin >> a[i];
    }
    dfs(0);
    cout << dp[0][edges[0].size()][m] << endl;
    return 0;
}

时间复杂度:O(n^3).

到这里来用背包的角度总结一下:

对于x的所有的子节点y。

  • 子节点y,每个节点对应一个组,整个x节点状态转移的过程是一个完全背包的过程。
  • 考虑y中的单一节点,我们枚举的是从中选择几个节点k,k即是物品的体积,背包的容量就是选择课程的数量。
  • 然后子节点我们考虑的仅仅是它选完自己所有的子节点之后的dp值,这个值就是背包中的价值。
  • 至于子节点的第二维选择的过程我们是不关心的,而且后两位组成的是完全背包,所以可以优化掉这一维。

状态表示:dp[x][j]:从x的子树中选择j个课程的最大学分。

状态转移:$dp[x][j] = \max_{k=0}^{j - 1}(dp[x][j-k]+dp[y][k])

$

初始化:dp[x][1] = a[x]

答案:dp[0][m]

转移循序:由于优化掉了第二维,分组背包的转移逻辑逆序遍历。

code:

#include <iostream>
#include <vector>

using namespace std;
const int N = 310;
// dp[i][j]: 从i的子树中选,更新了i的子树的size次之后,选择j个的最大贡献
// 实际上i总数需要子树的更新size轮之后的值,因此空间优化是可行的
int dp[N][N], a[N];
int n, m;
vector<int> edges[N];

void dfs(int x)
{
    // 从x的子树中选,x必选,选择一种的答案总是a[x]
    dp[x][1] = a[x];
    // 由于不关心孩子结点的编号,这里直接走正常dfs的逻辑即可
    // 其实这里枚举的就是i
    for(auto& y : edges[x])
    {
        dfs(y);
        // 把子树y并入到x的dp值中
        // 注意空间优化之后要改变遍历方向
        for(int j = m; j >= 2; j--)
            for(int k = 0; k < j; k++) 
                dp[x][j] = max(dp[x][j], dp[x][j - k] + dp[y][k]);
    }
}

int main()
{
    cin >> n >> m;
    for(int i = 1; i <= n; i++)
    {
        int k; cin >> k;
        edges[k].push_back(i);
        cin >> a[i];
    }
    m++;
    dfs(0);
    cout << dp[0][m] << endl;
    return 0;
}

优化一:上下界优化

令当前遍历到的x节点的个数为sz[x], 子树y的个数为sz[y]。

  • 对于 j 这一维度,上界不会超过sz[x] + sz[y]。
  • 对于k 这一维度,上界为min(j - 1, sz[y]), 下界为 max(1, j - sz[x])。

优化后的时间复杂度近似O(n^2), 某些情况下会被卡大

code:

#include <iostream>
#include <vector>

using namespace std;
const int N = 1e5 + 10;
vector<vector<int>> dp;
int n, m;
int a[N], sz[N];
vector<int> edges[N];

// 这个是上下界优化的版本,时间复杂度近似O(n^2),也会被卡大
void dfs(int x)
{

    dp[x][1] = a[x];
    sz[x] = 1;
    for (auto &y : edges[x])
    {
        dfs(y);
        for (int j = min(m, sz[x] + sz[y]); j >= 2; j--)
            for (int k = max(1, j - sz[x]); k <= min(sz[y], j - 1); k++)
                dp[x][j] = max(dp[x][j], dp[x][j - k] + dp[y][k]);
        sz[x] += sz[y];
    }
}

int main()
{
    cin >> n >> m;
    m++;
    dp.resize(n + 1, vector<int>(m + 1));
    for (int i = 1; i <= n; i++)
    {
        int k;
        cin >> k;
        edges[k].push_back(i);
        cin >> a[i];
    }
    dfs(0);
    cout << dp[0][m] << endl;
    return 0;
}

优化二:dfn序

在逆序遍历dfn序时,会逐渐还原出整棵树。

状态表示:dp[i][j]: 表示dfn序为 [i, n] 的节点中选 j 个课程的最大学分。

状态转移:

令dfn序为i的节点为x

  • 由于如果节点 x 不选的话,其子树都不能选,dp[i][j] = dp[i + sz[x]][j].
  • 如果节点x选了,那么从其子树中选,dp[i][j] = dp[i + 1][j - 1] + a[x].
  • \(dp[i][j] = max(dp[i + sz[x]][j], dp[i + 1][j - 1] + a[x])\).

code:

#include <iostream>
#include <vector>

using namespace std;
const int N = 1e5 + 10;
vector<int> edges[N];
int a[N], n, m;
// dp[i][j]: 从dfn序[i, n] 中选则j个节点的最大价值
// 由于dfn序的特点,当遍历到i的时候后面的结点及其子树已经遍历完成了
vector<vector<int>> dp;
int sz[N], dfn[N], seg[N], idx;

// dfn序优化树上背包,时间复杂度O(nm)

void dfs(int x)
{
    dfn[x] = ++idx;
    seg[idx] = x;
    sz[x] = 1;
    for (auto &y : edges[x])
    {
        dfs(y);
        sz[x] += sz[y];
    }
}

int main()
{
    cin >> n >> m;
    for (int i = 1; i <= n; i++)
    {
        int k;
        cin >> k;
        edges[k].push_back(i);
        cin >> a[i];
    }
    m++;
    dfs(0);
    dp.resize(n + 10, vector<int>(m + 10));
    for (int i = n + 1; i >= 1; i--)
    {
        int x = seg[i];
        for (int j = 1; j <= m; j++)
        {
            // i 不选 -> i 的子树都不能选,[i + sz[x] + 1, n]
            // i 选 -> [i + 1, n] + a[x] 
            dp[i][j] = max(dp[i + sz[x]][j], dp[i + 1][j - 1] + a[x]);
        }
    }
    cout << dp[1][m] << endl;
    return 0;
}

练习题

二叉苹果树

#include <iostream>
#include <vector>

using namespace std;
typedef long long LL;
typedef pair<int, LL> PII;
const int N = 110, M = 3e4 + 10;
// dp[i][j]: 从dfn序[i, n] 中选j个点的最大价值
LL dp[N][M];
int n, m;
LL w[N];
vector<PII> edges[N];
int sz[N], idx, dfn[N], seg[N];

void dfs(int x, int fa)
{
    dfn[x] = ++idx;
    seg[idx] = x;
    sz[x] = 1;
    for(auto& [y, z] : edges[x])
    {
        if(y == fa) continue;
        dfs(y, x);
        sz[x] += sz[y];
        w[y] = z;
    }
}


int main()
{
    cin >> n >> m;
    for(int i = 1; i <= n - 1; i++)
    {
        int x, y; LL z;
        cin >> x >> y >> z;
        edges[x].push_back({y, z});
        edges[y].push_back({x, z});
    }
    dfs(1, 0);
    for(int i = n; i >= 2; i--)
    {
        int x = seg[i];
        for(int j = 1; j <= m; j++)
        {
            dp[i][j] = dp[i + sz[x]][j];
            if(j - 1 >= 0) dp[i][j] = max(dp[i][j], dp[i + 1][j - 1] + w[x]);
        }
    }

    LL ans = 0;
    // for(auto& [y, z] : edges[1]) ans = max(ans, dp[dfn[y]][m]);
       
    // cout << ans << endl;
    cout << dp[2][m] << endl;
    return 0;
}


树形背包

#include <iostream>
#include <vector>

using namespace std;
const int N = 5e4 + 10;
typedef long long LL;
vector<int> edges[N];
vector<vector<LL>> dp;
int n, m;
int sz[N], idx, dfn[N], seg[N];
LL w[N], v[N];

void dfs(int x)
{
    dfn[x] = ++idx;
    seg[idx] = x; 
    sz[x] = 1;
    for(auto& y : edges[x])
    {
        dfs(y);
        sz[x] += sz[y];
    }
}

// 本题是树上01背包,采用的dfn序优化背包转移
// dp[i][j]: 从dfn序[i, n]中选j个物品的最大价值
// 转移的方式和选课几乎一摸一样
// 上一题是对于子树的分组背包,即可以从x的子树中选择1-sz个结点
// 本题其实也可以理解成分组背包,即从子树中选择不超过1-m的重量的最大价值
// 但本质都是01背包,都是单一结点的选于不选的问题,dfn序优化比价容易体现这个问题


int main()
{
    cin.tie(0); cout.tie(0);
    ios::sync_with_stdio(false);
    cin >> n >> m;
    for(int i = 1; i <= n; i++)
    {
        int x; cin >> x;
        edges[x].push_back(i);
    }
    for(int i = 1; i <= n; i++) cin >> w[i];
    for(int i = 1; i <= n; i++) cin >> v[i];
    dfs(0);
    dp.resize(n + 10, vector<LL> (m + 10));
    for(int i = n + 1; i >= 1; i--)
    {
        int x = seg[i];
        for(int j = 0; j <= m; j++)
        {
            dp[i][j] = dp[i + sz[x]][j];
            if(j - w[x] >= 0) dp[i][j] = max(dp[i][j], dp[i + 1][j - w[x]] + v[x]);
        }
    }
    cout << dp[1][m] << endl;
    return 0;
}

重建道路

#include <iostream>
#include <vector>
#include <cstring>

using namespace std;
const int N = 200;
const int INF = 0x3f3f3f3f;
int dp[N][N];
int n, p;
int d[N];
vector<int> edges[N];

void dfs(int x)
{
    dp[x][1] = d[x];
    for(auto& y : edges[x])
    {
        dfs(y);
        for(int j = p; j >= 2; j--)
        {
            for(int k = 0; k < j; k++)
            {
                dp[x][j] = min(dp[x][j], dp[x][j - k] + dp[y][k] - 2);
            }
        }
    }
}

int main()
{
    cin >> n >> p;
    for(int i = 1; i <= n - 1; i++)
    {
        int x, y; cin >> x >> y;
        edges[x].push_back(y);
        d[x]++; d[y]++;
    }
    int ret = INF;
    memset(dp, 0x3f, sizeof dp);
    dfs(1);
    for(int i = 1; i <= n; i++) ret = min(ret, dp[i][p]);
    cout << ret << endl;
    return 0;
}

有线电视网

#include <iostream>
#include <vector>
#include <cstring>

using namespace std;

const int N = 3010;
int n, m, w[N];
vector<pair<int, int>> edges[N];
int f[N][N], cnt[N];

void dfs(int x)
{
    f[x][0] = 0;
    if (x > n - m)
    {
        f[x][1] = w[x];
        cnt[x] = 1;
        return;
    }
    for (auto &t : edges[x])
    {
        int y = t.first, z = t.second;
        dfs(y);
        for (int j = cnt[x] + cnt[y]; j >= 1; j--)
            for (int k = 1; k <= min(j, cnt[y]); k++)
                f[x][j] = max(f[x][j], f[x][j - k] + f[y][k] - z);
        cnt[x] += cnt[y];
    }
}

int main()
{
    cin >> n >> m;
    for (int i = 1; i <= n - m; i++)
    {
        int k;
        cin >> k;
        while (k--)
        {
            int a, c;
            cin >> a >> c;
            edges[i].push_back({a, c});
        }
    }
    for (int i = n - m + 1; i <= n; i++)
        cin >> w[i];

    memset(f, -0x3f, sizeof f);

    dfs(1);
    for (int i = m; i >= 0; i--)
    {
        if (f[1][i] >= 0)
        {
            cout << i;
            break;
        }
    }
    return 0;
}

posted on 2026-06-29 10:12  我不爱吃汉堡  阅读(2)  评论(0)    收藏  举报

导航