「REMAKE系列」树形dp篇

总结

树上背包

树上背包合并复杂度证明

基环树上dp

树上路径

  • \(n,m\leq2000\) ,要求选择一些路径,使得每个点至多在一条路径上,并且路径的权值和最大。代码源树上路径1

换根dp

  • 简单换根。CF219D
  • 不明显换根,模拟后发现是换根。CF1187E

基础树形dp

习题

一些做对了一半的题

CF533B. Work Group

*2000

状态设计的差不多,自己的转移太复杂而且玄学,应该重新想想是不是可以简化。

const int N = 2e5 + 10;
vector<int> edge[N];
ll n, a[N], f[N][2];
// f[u][0 / 1] 从 u 的子树中选 偶数/奇数 个点。

void dfs(int u) {
    f[u][1] = -2e18;
    for (auto v: edge[u]) {
        dfs(v);
        ll x = f[u][0], y = f[u][1];
        f[u][0] = max(y + f[v][1], x + f[v][0]);
        f[u][1] = max(y + f[v][0], x + f[v][1]);
    }
    f[u][1] = max(f[u][1], f[u][0] + a[u]);
}

int main() {
    re(n);
    for (int i = 1; i <= n; i++) {
        int p;
        re(p), re(a[i]);
        if (p != -1) edge[p].pb(i);
    }
    dfs(1);
    printf("%lld\n", max(f[1][0], f[1][1]));
    return 0;
}

洛谷P2899 [USACO08JAN]Cell Phone Network G

提高+/省选- 基础树形dp

评测记录

题意

John 想让他的所有牛用上手机以便相互交流,他需要建立几座信号塔在 \(N\) 块草地中。已知与信号塔相邻的草地能收到信号。给你 \(N-1\) 个草地(A,B)的相邻关系,问:最少需要建多少个信号塔能实现所有草地都有信号。

思路

  • 写的时候少加了一个状态,然后加上原来的思路就ac了。
  • 看起来和没有上司的舞会有点像其实有一点区别,对于每个节点被儿子染色时,儿子节点可以放也可以不放,每个节点还可以被父亲染色。
  • 设计状态 \(f_{u,0}\) 表示节点被儿子染色自己不放,\(f_{u,1}\) 表示结点自己放来染色自己,\(f_{u,2}\) 表示节点自己不放被父亲染色。
  • 转移写在代码里,注意的点是,节点被儿子染色,只需要大于等于 1 个儿子放就可以了,所以需要记录最优的情况。
    • 实现:如果存在一个儿子放比不放其值更优,就简单了,如果不存在就找一个 放与不放 差值最小的来替代。
const int N = 1e4 + 10, INF = 1e9;
vector<int> edge[N];
int n;
int f[N][3], leaf[N]; // 0, 儿子放 自己不放,1 自己放, 2 父亲放 儿子不放。

void dfs(int u, int p) {
    f[u][1] = 1;
    leaf[u] = true;
    int sum = 0, d = INF;
    vector<int> tmp;
    bool find = false;
    for (auto v: edge[u]) {
        if (v == p) continue;
        dfs(v, u);
        leaf[u] = false;
        if (f[v][1] <= f[v][0]) {
            find = true;
            f[u][0] += f[v][1];
        }
        else {
            f[u][0] += f[v][0];
            d = min(d, f[v][1] - f[v][0]);
        }
        f[u][1] += min({f[v][0], f[v][1], f[v][2]});
        f[u][2] += min(f[v][1], f[v][0]);
    }
    if (!find)
        f[u][0] += d;
}
/*
1
|
3
|\
5 4
|
2
*/
int main() {
    re(n);
    for (int i = 1; i < n; i++) {
        int a, b;
        re(a), re(b);
        edge[a].pb(b), edge[b].pb(a);
    }
    dfs(1, 0);
    printf("%d\n", min({f[1][0], f[1][1]}));
    return 0;
}

dls动态规划中级

树上路径1

link树上路径1

  • \(dp[u]\) 表示当前 u 点对应路径最高点的路径选或不选的最大值。
  • u 点没有路径,则最大值为儿子节点 dp 权值和。
  • u 点有路径,要与路径上节点儿子权值加和后取 \(\max\) ,路径上的点 dp 权值和不计入。
  • 所以有一个巧妙办法,选取了一段路径,对于路径上的点可以等价于 \(sdp_i-dp_i\) 。节点儿子dp权值和减去节点dp权值和。
  • 以上可以用 BIT + DFS序优化到 nlogn 级别。
const int N = 2e3 + 10;
vector<int> edge[N];
vector<array<int, 3>> path[N];
int fa[N], n, m, dep[N];
ll dp[N], sdp[N];
void dfs(int u) {
    for (auto v: edge[u]) {
        dfs(v);
        sdp[u] += dp[v];
    }
    dp[u] = sdp[u];
    ll t = 0;
    for (auto p: path[u]) {
        ll tmp = 0;
        int x = p[0];
        while (x != u) {
            tmp += sdp[x] - dp[x];
            x = fa[x];
        }
        x = p[1];
        while (x != u) {
            tmp += sdp[x] - dp[x];
            x = fa[x];
        }
        tmp += p[2];
        t = max(t, tmp);
    }
    dp[u] += t;
}
int main() {
    scanf("%d%d", &n, &m);
    for (int i = 2; i <= n; i++) {
        scanf("%d", &fa[i]);
        edge[fa[i]].pb(i);
        dep[i] = dep[fa[i]] + 1;
    }
    for (int i = 0, u, v, a; i < m; i++) {
        scanf("%d%d%d", &u, &v, &a);
        int x = u, y = v;
        while (x != y) {
            if (dep[x] > dep[y]) x = fa[x];
            else y = fa[y];
        }
        path[x].pb({u, v, a});
    }
    dfs(1);
    printf("%lld\n", dp[1]);
    return 0;
}

洛谷——「能力综合提升题单-树形DP篇」

P3047 [USACO12FEB]Nearby Cows G

提高+/省选- , 简单容斥

给你一棵 \(n\) 个点的树,点带权,对于每个节点求出距离它不超过 \(k\) 的所有节点权值和 \(m_i\)

思路

  • 定义状态 \(f[u][j]\) 距离点 u 距离不超过 j 的点权和。
  • 第一次 dfs 转移:f[u][j] += f[v][j - 1]
  • 第二次 dfs 转移,由父节点更新子节点有点类似换根dp,但需要减去父节点和子节点的子树中重合的部分。
    • j:n->2, f[v][j] -= f[v][j - 2]
    • f[v][j] += f[u][j - 1]
ll f[N][25];
void dfs1(int u, int p) {
    f[u][0] = c[u];
    for (int i = h[u]; ~i; i = ne[i]) {
        int v = e[i];
        if (v == p) continue;
        dfs1(v, u);
        for (int j = 1; j <= k; j++) {
            f[u][j] += f[v][j - 1];
        }
    }
}
void dfs2(int u, int p) {
    for (int i = h[u]; ~i; i = ne[i]) {
        int v = e[i]; 
        if (v == p) continue;
        for (int j = k; j >= 2; j--)
            f[v][j] -= f[v][j - 2];
        for (int j = 1; j <= k; j++)
            f[v][j] += f[u][j - 1];
        dfs2(v, u);
    }
}
int main() {
    init();
    re(n), re(k);
    for (int i = 1; i < n; i++) {
        int a, b;
        re(a), re(b);
        add(a, b), add(b, a);
    }
    for (int i = 1; i <= n; i++)
        re(c[i]);
    dfs1(1, 0);
    dfs2(1, 0);
    for (int i = 1; i <= n; i++) {
        ll ans = 0;
        for (int j = 0; j <= k; j++)
            ans += f[i][j];
        printf("%lld\n", ans);
    }
    return 0;
}

P3698 [CQOI2017]小Q的棋盘

提高+/省选- ,简单树上背包

对于一颗树,现在想知道,当棋子从格点 0 出发,移动 N 步最多能经过多少格点。格点可以重复经过多次,但不重复计数。

const int N = 110;
vector<int> edge[N];
int n, m;
// f[u][j] 从 u 点选 j 个点的花费,并回到 u 点
// g[u][j] 从 u 点选 j 个点的花费,不回到 u 点。
// j: n->1 : f[u][j] = f[u][j - k] + f[v][k] + 1
// g[u][j] = min(g[u][j - k] + f[v][k], f[u][j - k] + g[v][k]) + 1;
int f[N][N], g[N][N], tf[N], tg[N];
void dfs(int u, int p) {
   f[u][1] = g[u][1] = 0;
   for (auto v: edge[u]) {
       if (v == p) continue;
       dfs(v, u);
       for (int i = 1; i <= n; i++) {
           tf[i] = f[u][i];
           tg[i] = g[u][i];
       }
       for (int j = n; j >= 1; j--) {
           for (int k = 1; k <= j - 1; k++) {
               f[u][j] = min(f[u][j], tf[j - k] + f[v][k] + 2);
               g[u][j] = min({g[u][j], tg[j - k] + f[v][k] + 2, g[v][k] + tf[j - k] + 1});
           }
       }
   }
}

int main() {
   re(n), re(m);
   memset(f, 0x3f, sizeof f);
   memset(g, 0x3f, sizeof g);
   for (int i = 1, a, b; i < n; i++) {
       re(a), re(b);
       edge[a].pb(b), edge[b].pb(a);
   }
   dfs(0, -1);
   for (int i = n; i >= 1; i--) {
       if (g[0][i] <= m) {
           printf("%d\n", i);
           break;
       }
   }
   return 0;
}

P3177 [HAOI2015] 树上染色

提高+/省选- 树上背包、细节题、贡献考虑

有一棵点数为 \(n\) 的树,树边有边权。给你一个在 \(0 \sim n\) 之内的正整数 \(k\) ,你要在这棵树中选择 \(k\) 个点,将其染成黑色,并将其他 的 \(n-k\) 个点染成白色。将所有点染色后,你会获得黑点两两之间的距离加上白点两两之间的距离的和的受益。问受益最大值是多少。

对于 \(100\%\) 的数据,\(0 \leq n,k \leq 2000\)

思路

  • 直接设状态不好求,考虑按贡献求解,求出对于某一条边被经过了多少次。
  • 显然是边左右两侧同色点的对数乘积。有一些细节,看代码实现就行。
const int N = 2e3 + 10;
vector<PLL> edge[N];
ll f[N][N];
int n, K;
int sz[N];
// f[u][j] u 从选取 j 个黑点
void dfs(int u, int p) {
    sz[u] = 1;
    f[u][0] = f[u][1] = 0;
    for (auto [v, w]: edge[u]) {
        if (v == p) continue;
        dfs(v, u);
        sz[u] += sz[v];
        for (int j = min(sz[u], K); j >= 0; j--) {
            if (f[u][j] != -1) {
                ll tot = sz[v] * (n - K - sz[v]);
                f[u][j] += f[v][0] + tot * w;
            }
            for (int k = min(j, sz[v]); k > 0; k--) {
                if (f[u][j - k] == -1) continue;
                ll tot = k * (K - k) + (sz[v] - k) * (n - K - (sz[v] - k));
                f[u][j] = max(f[u][j], f[u][j - k] + f[v][k] + tot * w);
            }
        }
    }
}
int main() {
    re(n), re(K);
    memset(f, -1, sizeof f);
    for (int i = 1; i < n; i++) {
        int a, b, c;
        re(a), re(b), re(c);
        edge[a].pb({b, c}), edge[b].pb({a, c});
    }
    dfs(1, 0);
    printf("%lld\n", f[1][K]);
    return 0;
}

P2607 [ZJOI2008] 骑士

省选/NOI-基环树dp

const int N = 1e6 + 10;
const ll INF = 2e18;
ll n, a[N], fa[N], dp[N][2], dp2[N][2];
bool vis[N], oncyc[N];
vector<int> edge[N];

void dfs(int u) {
    vis[u] = true;
    dp[u][1] = a[u];
    for (auto v: edge[u]) {
        if (oncyc[v]) continue;     // 在环上跳过
        dfs(v);
        dp[u][0] += max(dp[v][0], dp[v][1]);
        dp[u][1] += dp[v][0];
    }
}

int main() {
    re(n);
    for (int i = 1, p; i <= n; i++) {
        re(a[i]), re(p);
        edge[p].pb(i);
        fa[i] = p;
    }
    ll ans = 0;
    // 对于每个连通分量求解
    for (int i = 1; i <= n; i++) {
        if (vis[i]) continue;
        // 找出每个环
        int now = i;
        while (!vis[now]) {
            vis[now] = true;
            now = fa[now];
        }
        vector<int> cyc;
        while (!oncyc[now]) {
            oncyc[now] = true;
            cyc.pb(now);
            now = fa[now];
        }
        // 对非环节点树形 dp
        for (auto u: cyc)
            dfs(u);
        // 环上dp
        int m = SZ(cyc);
        ll res = -INF;
        for (int t = 0; t < 2; t++) {
            for (int j = 0; j < 2; j++) {
                if (t == j) dp2[0][j] = dp[cyc[0]][t];
                else dp2[0][j] = -INF;
            }
            for (int i = 1; i < m; i++) {
                dp2[i][0] = max(dp2[i - 1][0], dp2[i - 1][1]) + dp[cyc[i]][0];
                dp2[i][1] = dp2[i - 1][0] + dp[cyc[i]][1];
            }
            if (t == 0) res = max(dp2[m - 1][0], dp2[m - 1][1]);
            else res = max(res, dp2[m - 1][0]);
        }
        ans += res;
    }
    printf("%lld\n", ans);
    return 0;
}

P4516 [JSOI2018] 潜入行动

省选/NOI-树上背包计数

题意略

  • 设状态为 \(dp[u][j][0/1][0/1]\) ,u 点子树放了 j 个装置,u 点有没有放装置,u 点有没有被监听的方案数。
  • 对于转移时两点 u, v,考虑 u 点的情况
  • 如果 u 没有放装置也没有被监听,v 一定不能放装置但 v 要被监听(否则 u 被监听)。
    • \[dp[u][i+j][0][0] = \sum dp[u][i][0][0] \times dp[v][j][0][1] \]

  • 如果 u 没有放装置但被监听,v 的状态为被监听。对于 dp[u][k][0][1] 放不放装置无所谓, 对于 dp[u][k][0][0] v 必须放装置。
    • \[dp[u][i+j][0][1] = \sum dp[u][i][0][1] \times (dp[v][j][0][1] + dp[v][j][1][1]) + \sum dp[u][i][0][0] \times dp[v][j][1][1] \]

  • 如果 u 放了装置但没有被监听,v 有没有被监听无所谓,一定没有放装置。
    • \[dp[u][i+j][1][0] = \sum dp[u][i][1][0]\times (dp[v][j][0][1] + dp[v][j][0][0]) \]

  • 如果 u 放了装置且被监听,对于 dp[u][k][1][0], v 一定放装置,有没有被监听无所谓。对于 dp[u][k][1][1] v 放不放装置,被不被监听都无所谓。
    • \[dp[u][i+j][1][0]=\sum dp[u][i][1][0]\times (dp[v][j][1][0] + dp[v][j][1][1]) \]

    • \[dp[u][i+j][1][1]=\sum dp[u][i][1][1]\times (dp[v][j][0][0]+dp[v][j][0][1]+dp[v][j][1][0]+dp[v][j][1][1]) \]

  • 转移复杂度是 \(O(nk)\) ,数组不能开 long long ,转以后再更新子树大小。
const int N = 1e5 + 10, mod = 1e9 + 7;
int n, K;
vector<int> edge[N];
int dp[N][110][2][2], tmp[110][2][2], sz[N];

void dfs(int u, int p) {
    sz[u] = 1;
    dp[u][0][0][0] = dp[u][1][1][0] = 1;
    for (auto v: edge[u]) {
        if (v == p) continue;
        dfs(v, u);
        memcpy(tmp, dp[u], sizeof tmp);
        memset(dp[u], 0, sizeof tmp);
        for (int i = 0; i <= min(sz[u], K); i++) {
            for (int j = 0; j <= min(sz[v], K) && i + j <= K; j++) {
                dp[u][i + j][0][0] += 1ll * tmp[i][0][0] * dp[v][j][0][1] % mod;
                dp[u][i + j][0][1] += (1ll * tmp[i][0][1] * (dp[v][j][0][1] + dp[v][j][1][1]) + 1ll * tmp[i][0][0] * dp[v][j][1][1]) % mod; 
                dp[u][i + j][1][0] += 1ll * tmp[i][1][0] * (dp[v][j][0][1] + dp[v][j][0][0]) % mod;
                dp[u][i + j][1][1] += (1ll * tmp[i][1][0] * (dp[v][j][1][0] + dp[v][j][1][1]) +
                    1ll * tmp[i][1][1] * (1ll * dp[v][j][0][0] + dp[v][j][0][1] + dp[v][j][1][0] + dp[v][j][1][1])) % mod;
                if (dp[u][i + j][0][0] >= mod) dp[u][i + j][0][0] -= mod;
                if (dp[u][i + j][0][1] >= mod) dp[u][i + j][0][1] -= mod;
                if (dp[u][i + j][1][0] >= mod) dp[u][i + j][1][0] -= mod;
                if (dp[u][i + j][1][1] >= mod) dp[u][i + j][1][1] -= mod;
            }
        }
        sz[u] += sz[v];     // 转移后再更新子树大小
    }
    return ;
}

int main() {
    re(n), re(K);
    for (int i = 1; i < n; i++) {
        int a, b;
        re(a), re(b);
        edge[a].pb(b), edge[b].pb(a);
    }
    dfs(1, -1);
    ll ans = (dp[1][K][0][1] + dp[1][K][1][1]) % mod;
    printf("%lld\n", ans);
    return 0;
}

CodeForces

CF219D. Choosing Capital for Treeland

*1700 换根dp

评测记录

题意

Treeland国有n个城市,这n个城市连成了一颗树,有n-1条道路连接了所有城市。每条道路只能单向通行。现在政府需要决定选择哪个城市为首都。假如城市i成为了首都,那么为了使首都能到达任意一个城市,不得不将一些道路翻转方向,记翻转道路的条数为k。你的任务是找到所有满足k最小的首都。

思路

  • 定义 \(f_u\) 表示到达子树所有点最小翻转次数, \(g_u\) 表示到达父节点以上的所有点最小翻转次数。
  • \(f_u = \sum_v f[v] + w_{u,v}\), \(g_v = g_u + (f_u - f_v - w) + (w\; xor\; 1);\)

CF1187E. Tree Painting

*2100 换根dp

评测记录

题意

给定一棵n个点的树 初始全是白点

要求你做n步操作,每一次选定一个与一个黑点相隔一条边的白点,将它染成黑点,然后获得该白点被染色前所在的白色联通块大小的权值。

第一次操作可以任意选点。求可获得的最大权值

思路

  • 发现第一次选择一个点后方案固定。
  • 再画一下有点类似换根dp,需要求出父节点以上连通块和自己子树的答案。
  • \(f_u\) 表示 u 子树的贡献,\(g_u\) 表示 u 父节点以上的贡献。
    • \[f_u=size_u + \sum_v f_v \]

    • \[g_v=f_u - size_u - f_v + g_u + n - size_v \]

  • 对于每个节点答案为 \(f_i+g_i + n - size_i\) ,加的部分补偿 i 作为第一个选的得分。
posted @ 2022-09-12 22:26  Roshin  阅读(54)  评论(0编辑  收藏  举报
-->