树形DP入门(持续更新中

<code class="language-null">以前在暑假集训的时候搞得最多的还是图论相关,如果碰到树形结果很多时候就直接用搜……当然绝大多数时候也都T了……(当时也不喜欢分析复杂度,真是个坏习惯……

树形DP,简单说还是在树上做决策,只要把基础DP掌握好了我想应该不会是什么难事,只是换个场地罢了……

0. 入门题 POJ 2342

题意:
某公司要举办一次晚会,但是为了使得晚会的气氛更加活跃,每个参加晚会的人都不希望在晚会中见到他的直接上司,现在已知每个人的活跃指数和上司关系(当然不可能存在环),求邀请哪些人(多少人)来能使得晚会的总活跃指数最大。

思路:
定义数组dp[maxn][2],dp[k][0]表示第 k 人不去,dp[k][1] 表示第 k 人去。定义 son 为第 K 人的某个下属
我们从下往上分析,对于第 K 个人有以下去与不去两种情况

  • 假如 K 参加,那么对于他的每一个下属都不会去,那么就只能加上这个下属不去的这棵子树的值 dp[son][0]
  • 假如 K 不参加,那么他的下属可以选择去或者不去,当然我们要选择子树权值最大的 max(dp[son][0],dp[son][1])
#include <algorithm>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <vector>
#define ll long long

using namespace std;

const int maxn = 10000;

vector<int> tree[maxn];
int n;
int dp[maxn][2];

void tree_dp(int cur, int pre)
{
    int sz = tree[cur].size();
    for (int i = 0; i < sz; i++) {
        int son = tree[cur][i];
        if (son != pre) {
            tree_dp(son, cur);
            dp[cur][1] += dp[son][0];
            dp[cur][0] += max(dp[son][0], dp[son][1]);
        }
    }
}

int main()
{
    ios_base::sync_with_stdio(false);
    cin.tie(0);

    int u, v;
    while (cin >> n) {
        for (int i = 1; i <= n; i++) {
            cin >> dp[i][1];
            tree[i].clear();
            dp[i][0] = 0;
        }
        while (cin >> u >> v) {
            if (u == 0 && v == 0)
                break;
            tree[u].push_back(v);
            tree[v].push_back(u);
        }
        tree_dp(1, -1);
        printf("%d\n", max(dp[1][0], dp[1][1]));
    }
    return 0;
}

顺便值得一提的是:
我看了很多网上的题解,无一都选择先找到树的根节点,然后从根节点进行向下dp。这样或许可以降低复杂度
因为出数据都习惯把初始树建的平衡一些……
但对于一棵树来说,无论你把哪一个节点定为根节点,它都能转化成另外一棵树,在这里是没有什么影响的。更何况,极端数据不排除会有极度不平衡的情况,其结果是一样的,该A的A,该WA的WA……还不如偷懒一点,提升一点码速。

1.1 向下递推 POJ 1947 Rebuilding Roads

题意:
给一棵树,问最少删掉几条边.使得剩下的子树中有节点个数为p个。

思路:
定义数组 dp[root][n] 表示以 当前节点为 root 节点,含有 n 个点所需删除的最小边数。
显然 dp[root][1]=root节点的所有孩子的个数+1(一个父亲节点),但是对于一棵树的根节点是没有父亲节点的。我这里先不考虑父亲节点,到最后再加上去。

初始状态已经确定,关键的还是 转移方程

dp[root][n] = min(dp[root][n],dp[root][n-k]+dp[root.son][k]-1);

#include <algorithm>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <vector>
#define ll long long

using namespace std;

const int maxn = 160;
int n, p, ans;
vector<int> key[maxn];
int dp[maxn][maxn];

int tree_dp(int root)
{
    int total = 1, sz = key[root].size();
    for (int i = 0; i < sz; i++)
        total += tree_dp(key[root][i]);
    dp[root][1] = sz;
    for (int j = 0; j < sz; j++) {
        int son = key[root][j];
        for (int i = min(p, total); i > 1; i--) {
            for (int k = 1; k < i; k++) {
                dp[root][i] = min(dp[root][i], dp[root][i - k] + dp[son][k] - 1);
            }
        }
    }
    ans = min(ans, dp[root][p] + (root != 1));
    return total;
}

int main()
{
    ios_base::sync_with_stdio(false);
    cin.tie(0);

    int u, v;
    while (cin >> n >> p) {
        for (int i = 1; i <= n; i++)
            key[i].clear();
        memset(dp, 0x3f, sizeof dp);
        for (int i = 1; i < n; i++) {
            cin >> u >> v;
            key[u].push_back(v);
        }
        ans = 0x3f3f3f3f;
        tree_dp(1);
        cout << ans << endl;
    }
    return 0;
}
1.2 树上的01背包 HDU 1561

题意:
给你一个森林,每个节点都赋予一个权值。让你取 m 个节点,使得节点和最大。
当然,取节点也是有条件的: 如果你要取走这个节点,那你必须先取走它的父亲节点。

思路:
额外增加一个超级根节点,让这片森林里的每棵树的根节点都成为这个超级根节点的孩子,以形成一棵树。
再与上题一样,在树上不断向下递推即可

转移方程: dp[root][i] = max( dp[root][i] , dp[root][i-k] + dp[root.son][k])

#include <algorithm>
#include <cmath>
#include <cstdio>
#include <cstring>
#include <iostream>
#define ll long long

using namespace std;

const int maxn = 205;
int n, idx, ans, m;
int head[maxn], dp[maxn][maxn], val[maxn];

struct edge {
    int to;
    int nxt;
} edges[maxn << 1];

void addEdge(int u, int v)
{
    edges[idx].to = v;
    edges[idx].nxt = head[u];
    head[u] = idx++;
}

void init()
{
    for (int i = 0; i <= n; i++) {
        head[i] = -1;
        for (int j = 0; j <= n; j++)
            dp[i][j] = 0;
    }
    idx = val[0] = 0;
    m++;
}

void treeDP(int root)
{
    dp[root][1] = val[root];
    for (int it = head[root]; ~it; it = edges[it].nxt) {
        int v = edges[it].to;
        treeDP(v);
        for (int i = m; i >= 1; i--)
            for (int k = 1; k < i; k++)
                dp[root][i] = max(dp[root][i], dp[root][i - k] + dp[v][k]);
    }
}

int main()
{
    ios_base::sync_with_stdio(false);
    cin.tie(0);

    int u;
    while (cin >> n >> m) {
        if (n == 0 && m == 0)
            break;
        init();
        for (int i = 1; i <= n; i++) {
            cin >> u >> val[i];
            addEdge(u, i);
        }
        treeDP(0);
        cout << dp[0][m] << endl;
    }
    return 0;
}
1.3 树上的最大连续序列 POJ 1192

题意:
题目虽然是中文,但说的非常繁琐。
简单概括就是 给定一个平面整点集,点与点间在|x1-x2| + |y1-y2| = 1 时两点连通,且形成的图没有回路其实就是一棵树啦,每个点有一个可正可负的权值,求最大权和连通子图。

思路:
和一维的 求连续最大权值序列 是一个思路

转移方程: dp = max(dp, dp + son )

//好激动呀,这是第一道我自己独立写出来的树形DP

#include <algorithm>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <stack>
#include <vector>
#define ll long long

using namespace std;

const int maxn = 1005;
int n, idx, ans;
int head[maxn];

struct node {
    int x, y, w;
} nodes[maxn];

struct edge {
    int to;
    int nxt;
} edges[maxn << 1];

void addEdge(int u, int v)
{
    edges[idx].to = v;
    edges[idx].nxt = head[u];
    head[u] = idx++;
}

bool judgeAdj(int u, int v)
{
    return abs(nodes[u].x - nodes[v].x) + abs(nodes[u].y - nodes[v].y) == 1;
}

void init()
{
    for (int i = 0; i <= n; i++)
        head[i] = -1;
    idx = ans = 0;
}

int treeDP(int cur, int pre)
{
    int dp = nodes[cur].w;
    for (int it = head[cur]; it != -1; it = edges[it].nxt)
        if (edges[it].to != pre) 
            dp = max(dp, dp + treeDP(edges[it].to, cur));
    return dp;
}

int main()
{
    ios_base::sync_with_stdio(false);
    cin.tie(0);

    while (cin >> n) {
        init();
        for (int i = 0; i < n; i++)
            cin >> nodes[i].x >> nodes[i].y >> nodes[i].w;
        for (int i = 0; i < n; i++)
            for (int j = i + 1; j < n; j++) {
                if (judgeAdj(i, j)) {
                    addEdge(i, j);
                    addEdge(j, i);
                }
            }
        ans = treeDP(1, -1);
        cout << ans << endl;
    }
    return 0;
}