标签 树形DP 下的文章

url: https://www.luogu.com.cn/problem/P1453

tag:
动态规划,图论,基环树,树形DP

思路:
基环树是一种特殊的图结构,可以看作是在一棵树的基础上加入一条额外的边,从而使图中恰好出现一个环,也称作单环图唯一环图(unicyclic graph)。其主要特点包括:

  1. 连通性:基环树是连通图,任意两个顶点之间都有路径相连。
  2. 唯一的环:整个图中仅存在一个简单环,其他部分均保持树的结构(即无环)。
  3. 分解思想:可以将基环树看作由一个环和附着在环上各个顶点上的若干棵树构成。对于很多算法(例如树形 DP),通常先对环进行特殊处理(比如“断环”),再处理附着的树结构。

这种结构在算法竞赛中经常出现,处理时通常需要先找出环,然后考虑两种或多种情况(例如环上某些节点选或不选),从而在保证整体方案最优的前提下完成状态转移和求解。

这道题目其实和求树的最小覆盖问题类似,我们用 f[u][0/1] 表示节点u为根且选或者不选时的总共人流量。只是因为有基环树,所以需要先断边,然后分别用那一边的两个点为根且不选(方便另外一个点可选)时各跑一遍dfs,求答案,最后两个答案求一个最大值并乘上k,注意,对于不同的情况来说需要先将f数组清空(因为可选的点不同了)。

代码:

#include <iostream>
#include <cstring>
#include <cstdio>
#include <algorithm>
using namespace std;
const int N = 1e5 + 10;
int h[N], e[N * 2], ne[N * 2], idx;
int p[N];
bool st[N];
double k;
int n, s1, s2, flag;
int f[N][2];
void add(int a, int b)
{
    e[idx] = b;
    ne[idx] = h[a];
    h[a] = idx ++;
}
void dfs1(int u, int fa)
{
    st[u] = 1;
    for (int i = h[u]; ~i; i = ne[i])
    {
        int v = e[i];
        if (v == fa) continue;
        if (st[v])
        {
            s1 = u, s2 = v;
            flag = true;
            return;
        }
        dfs1(v, u);
        if (flag) return ;
    }
    st[u] = 0;
}
void dfs2(int u, int fa)
{
    f[u][1] = p[u];
    for (int i = h[u]; ~i; i = ne[i])
    {
        int v = e[i];
        if (v == fa) continue;
        if ((u == s1 && v == s2) || (u == s2 && v == s1)) continue;
        dfs2(v, u);
        f[u][1] += f[v][0];
        f[u][0] += max(f[v][0], f[v][1]);
    }
}
int main()
{
    memset(h, -1, sizeof h);
    cin >> n;
    for (int i = 0; i < n; i ++) cin >> p[i];
    int a, b;
    for (int i = 0; i < n; i ++)
    {
        cin >> a >> b;
        add(a, b), add(b, a);
    }
    cin >> k;
    dfs1(0, 0);
    dfs2(s1, s1);
    int res1 = f[s1][0];
    memset(f, 0, sizeof f);
    dfs2(s2, s2);
    int res2 = f[s2][0];
    printf("%.1f", max(res1, res2) * k);
    return 0;
}

url: https://www.luogu.com.cn/problem/P1272

tag:
动态规划,树形DP

思路:
简单的树形DP和背包结合。令 f[u][j] 表示以u节点为根的子树在需要取出j个节点时最少需要断的边。最后在输出答案的时候在所有节点中选择一个答案最小的,注意选择其他节点为根节点时需要断去父节点和其之间的一条边。最后输出答案即可。

代码:

#include <iostream>  
#include <cstdio>  
#include <cstring>  
#include <algorithm>  
using namespace std;  
const int N = 200;  
int h[N], e[N * 2], ne[N * 2], idx;  
int s[N];  
int n, p;  
int f[N][N];  
void add(int a, int b)  
{  
    e[idx] = b;  
    ne[idx] = h[a];  
    h[a] = idx ++;  
}  
void dfs(int u, int fa)  
{  
    s[u] = 1;  
    f[u][1] = 0;  
    for (int i = h[u]; ~i; i = ne[i])  
    {  
        int v = e[i];  
        if (v == fa) continue;  
        dfs(v, u);  
        s[u] += s[v];  
        for (int j = min(s[u], p); j >= 0; j --)  
        {  
            f[u][j] += 1;  
            for (int k = 0; k <= min(j, s[v]); k++)  
                f[u][j] = min(f[u][j], f[u][j - k] + f[v][k]);  
        }  
    }  
}  
int main()  
{  
    memset(h, -1, sizeof h);  
    memset(f, 0x3f, sizeof f);  
    cin >> n >> p;  
    for (int i = 0; i < n - 1; i ++)  
    {  
        int a, b;  
        cin >> a >> b;  
        add(a, b), add(b, a);  
    }  
    dfs(1, 0);  
    int res = f[1][p];  
    for (int i = 2; i <= n; i ++) res = min(res, f[i][p] + 1);  
    cout << res << endl;  
    return 0;  
}

url: https://www.luogu.com.cn/problem/P3177

tag:
动态规划,树形DP

思路:
f[u][j] 表示以u为根节点的子树中,恰好有j个黑点时的收益。之后就是和分组背包类似的思路,对于每一个相连的节点,由当那个节点取k个黑点时更新过来,在不同的k值之间取一个最大值。使用 (k * (m - k) + (s[v] - k) * (n - m - s[v] + k)) * w[i] 来计算u和v之间边对答案的贡献。最后记得开long long。

代码:

#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 2020;
int h[N], e[N * 2], ne[N * 2], w[N * 2], idx;
long long n, m;
long long s[N];
long long f[N][N];
void add(int a, int b, int c)
{
    e[idx] = b;
    w[idx] = c;
    ne[idx] = h[a];
    h[a] = idx ++;
}
void dfs(int u, int fa)
{
    s[u] = 1;f[u][0] = f[u][1] = 0;
    for (int i = h[u]; ~i; i = ne[i])
    {
        int v = e[i];
        if (v == fa) continue;
        dfs(v, u);
        s[u] += s[v];
        for (int j = min(s[u], m); j >= 0; j --)
            for (int k = 0; k <= min((long long)j, s[v]); k ++)
                if (f[u][j - k] != -1)
                {
                    long long val = (k * (m - k) + (s[v] - k) * (n - m - s[v] + k)) * w[i];
                    f[u][j] = max(f[u][j], f[u][j - k] + f[v][k] + val);
                }
    }
}
int main()
{
    memset(h, -1, sizeof h);
    memset(f, -1, sizeof f);
    cin >> n >> m;
    for (int i = 0; i < n - 1; i ++)
    {
        int a, b, c;
        cin >> a >> b >> c;
        add(a, b, c), add(b, a, c);
    }
    dfs(1, 0);
    cout << f[1][m] << endl;
    return 0;
}

url: https://www.luogu.com.cn/problem/P3621

tag:
动态规划,树形DP

思路:
tr[u][0/1] 来存二叉树。做两次dfs第一次求出最大的深度和最小的深度,如果相差大于1直接输出-1。第二次dfs从下到上递归,设计返回值为0/1/2分别表示都是浅层,都是深层和混合。当左边为浅层,右边为深层或者左边为混合,右边为深层时交换答案加1,如果存在某个节点两边都是混合的情况时说明无论怎么交换都不会改变当前的情况,则输出-1程序结束。最后如果存在一个方案使得可以交换成功就输出答案ans。

代码:

#include <iostream>
#include <algorithm>
#include <cstdio>
#include <cstring>
using namespace std;
const int N = 1e5 + 10;
int tr[N][2],ans, mi = 0x3f3f3f3f, mx;
int n;
void dfs1(int u, int dep)
{
    if (u == -1)
    {
        mi = min(mi, dep);
        mx = max(mx, dep);
        return;
    }
    dfs1(tr[u][0], dep + 1), dfs1(tr[u][1], dep + 1);
}
int dfs2(int u, int dep)
{
    if (u == -1) return (dep != mi);
    int x = dfs2(tr[u][0], dep + 1);
    int y = dfs2(tr[u][1], dep + 1);
    ans += (!x && y) || (x == 2 && y == 1);
    if (x == 2 && y == 2)
    {
        cout << -1;
        exit(0);
    }
    if (x + y == 1 || x == 2 || y == 2) return 2;
    if (!(x + y)) return 0;
    return 1;
}
int main()
{
    cin >> n;
    for (int i = 1; i <= n; i ++) cin >> tr[i][0] >> tr[i][1];
    dfs1(1, 0);
    if (mx - mi > 1) cout << -1 << endl;
    else if (mi == mx) cout << 0 << endl;
    else
    {
        int _ = dfs2(1, 0);
        cout << ans << endl;
    }
    return 0;
}

url: https://www.luogu.com.cn/problem/P1273

tag:
动态规划,树形DP,背包DP

思路:
f[u][j] 表示第u个节点选择j个用户时的盈利数。使用分组背包的思想。状态转移方程为
f[u][k] = max(f[u][k], f[u][k - l] + f[j][l] - w[i]) 其中w表示如果将节点j接入时的成本。最后因为是求不亏本时最多可以观看转播的用户数,所以从m开始向下循环,直到找到第一个盈利大于等于0的然后输出用户数即可。

代码:

#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 3030;
int h[N], e[N * 2], ne[N * 2], w[N * 2], idx;
int n, m;
int p[N];
int s[N];
int f[N][N];
void dfs(int u, int fa)
{
    if (u > n - m) f[u][1] = p[u], s[u] = 1;
    for (int i = h[u]; ~i; i = ne[i])
    {
        int j = e[i];
        if (j == fa) continue;
        dfs(j, u);
        s[u] += s[j];
        for (int k = m; k >= 0; k --)
        {
            for (int l = 1; l <= min(k, s[j]); l ++)
            {
                f[u][k] = max(f[u][k], f[u][k - l] + f[j][l] - w[i]);
            }
        }
    }
}
void add(int a, int b, int c)
{
    e[idx] = b;
    ne[idx] = h[a];
    w[idx] = c;
    h[a] = idx ++;
}
int main()
{
    memset(h, -1, sizeof h);
    memset(f, -0x3f, sizeof f);
    cin >> n >> m;
    for (int i = 1; i <= n - m; i ++)
    {
        int k;
        cin >> k;
        for (int j = 0; j < k; j ++)
        {
            int a, b;
            cin >> a >> b;
            add(i, a, b), add(a, i, b);
        }
    }
    for (int i = n - m + 1; i <= n; i ++) cin >> p[i];
    for (int i = 1; i <= n; i ++) f[i][0] = 0;
    dfs(1, 0);
    for (int i = m; i >= 0; i --)
    {
        if (f[1][i] >= 0)
        {
            cout << i;
            break;
        }
    }
    return 0;
}