树形dp问题讲解

树形DP

1.统计人数

1.题目

一家公司里有 n n n 个员工,他们的编号分别是 1 1 1 n n n,其中 1 1 1 号员工是公司 CEO,CEO 在公司里没有上司。除了 CEO 外,每个人都有一个直接上司。我们想知道,每个人的团队(包括他/她自己、他/她的直接下属和间接下属)一共有多少人?

输入格式

第一行一个整数 n。

接下来一行 n−1个整数 f2,f3,…,fn,其中 fi (1≤fi<i) 表示 i号员工的上司的编号。

输出格式

输出一行 n个整数,分别表示 1∼n 号员工的团队人数。

2.分析

i i i 号员工的团队人数等于他/她的每个直接下级的团队人数的总和 + 1 +1 +1

d f s dfs dfs b f s bfs bfs 两种选法。

3.代码

dfs写法

#include <bits/stdc++.h>
#define ll long long
using namespace std;
struct node{
    node *next;
    int where;
} *first[100001], a[100001];
int n, l, f[100001];
inline void makelist(int x,int y){//连一条从x到y的边
    a[++l].where = y;
    a[l].next = first[x];
    first[x] = &a[l];
}
inline void solve(int i){
    f[i] = 1;//一开始就自己一个人
    for (node *x = first[i]; x; x = x->next)//找所有的儿子
    {
        solve(x->where);
        f[i] += f[x->where];//加上儿子子树的个数
    }
}
int main()
{
    scanf("%d", &n);
    memset(first, 0, sizeof(first));
    l = 0;
    for (int i = 2; i <= n;i++){
        int x;
        scanf("%d", &x);
        makelist(x, i);
    }
    solve(1);//解决以1号点为子树的情况
    for (int i = 1; i <= n;i++)
        printf("%d ", f[i]);
}

bfs

#include <bits/stdc++.h>
#define ll long long
using namespace std;
struct node
{
    node *next;
    int where;
} *first[100001], a[100001];
int n, l, f[100001], c[100001];
inline void makelist(int x, int y)
{ // 连一条从x到y的边
    a[++l].where = y;
    a[l].next = first[x];
    first[x] = &a[l];
}
int main()
{
    scanf("%d", &n);
    memset(first, 0, sizeof(first));
    l = 0;
    for (int i = 2; i <= n; i++)
    {
        int x;
        scanf("%d", &x);
        makelist(x, i);
    }
    c[1] = 1;//队列,找出bfs序
    for (int k = 1, l = 1; l <= k; l++)
    {
        int m = c[l];//头元素
        for (node *x = first[m]; x; x = x->next)//下属点
            c[++k] = x->where;
    }
    for (int i = n; i; i--)//倒着算的,算到前面的时候后面都计算过了
    {
        int m = c[i];
        f[m] = 1;
        for (node *x = first[m]; x; x = x->next)
        {
            f[m] += f[x->where];
        }
    }
    for (int i = 1; i <= n; i++)
        printf("%d ", f[i]);
}

2.没有上司的舞会

1.题目

一家公司里有 n n n 个员工,他们的编号分别是 1 1 1 n n n,其中 1 1 1 号员工是公司 CEO,CEO 在公司里没有上司。除了 CEO 外,每个人都有一个直接上司。今天公司要办一个舞会,为了大家玩得尽兴,如果某个员工的直接上司来了,他/她就不想来了。 i i i 号员工来参加舞会会为大家带来 a i a_i ai 点快乐值。现在我们想要确定一组员工参加舞会的方案,使得快乐值总和最大。请求出快乐值总和最大是多少。

输入格式

第一行一个整数 n。

接下来一行 n−1个整数 f2,f3,…,fn,其中 fi (1≤fi<i) 表示 i号员工的上司的编号。

接下来一行 n个整数 a1,a2,…,an

表示每个员工参加舞会带来的快乐值。

输出格式

一行一个整数表示答案。

2.分析

考虑 i i i 号员工的团队,有参加和不参加两种情况,分别求最大值。

如果他参加,他的直接下属都不参加;如果他不参加,考虑他直接下属参加和不参加。

f [ i ] [ 0 ] f[i][0] f[i][0] f [ i ] [ 1 ] f[i][1] f[i][1] 分别表示考虑 i i i 号员工的团队,他不参加/参加的最大快乐值。

3.代码

#include <bits/stdc++.h>
#define ll long long
using namespace std;
struct node
{
    node *next;
    int where;
} *first[100001], a[100001];
int n, l ,c[100001],v[100001];
ll f[100001][2];
inline void makelist(int x, int y)
{ // 连一条从x到y的边
    a[++l].where = y;
    a[l].next = first[x];
    first[x] = &a[l];
}
inline void solve(int i){
    f[i][1] = v[i];
    for (node *x = first[i]; x;x=x->next){
        solve(x->where);
        f[i][0] += max(f[x->where][0], f[x->where][1]);
        f[i][1] += f[x->where][0];
    }
}
int main()
{
    scanf("%d", &n);
    memset(first, 0, sizeof(first));
    l = 0;
    for (int i = 2; i <= n;i++){
        int x;
        scanf("%d", &x);
        makelist(x, i);
    }
    for (int i = 1; i <= n;i++)
        scanf("%d", &v[i]);
    solve(1);
    printf("%lld\n", max(f[1][0], f[1][1]));
}

3.新的背包

1.题目

n n n 种物品要放到一个袋子里,袋子的总容量为 m m m,每种物品都有 m m m 个,单个物品的体积都是 1 1 1。对于第 i i i 种物品,如果我们一共取了 j ( j ≥ 1 ) j (j≥1) j(j1) 个,会获得 w i , j w_{i,j} wi,j 的收益。请问如何选择物品,使得在物品的总体积不超过 m m m 的情况下,获得的总收益最大?请求出最大总收益。

输入格式

第一行两个整数 n,m。

接下来 n行,每行 m 个整数 wi,1,…,wi,m。

输出格式

一行一个整数表示答案。

2.分析

f [ i ] [ j ] f[i][j] f[i][j] 表示前 i i i 种物品,总体积不超过 j j j。然后枚举第 i i i 种物品取了 k k k 个。

3.代码

//二维
// #include <bits/stdc++.h>
// #define ll long long
// using namespace std;
// int n, m, f[501][501], w[501][501];
// int main()
// {
//     scanf("%d%d", &n, &m);
//     for (int i = 1; i <= n;i++)
//         for (int j = 1; j <= m;j++)
//             scanf("%d", &w[i][j]);
//     for (int i = 1; i <= n;i++)
//         for (int j = 0; j <= m;j++)
//             for (int k = 0; k <= j;k++)
//                 f[i][j] = max(f[i][j], f[i - 1][j - k] + w[i][k]);
//     printf("%d\n", f[n][m]);
// }
//一维
#include <bits/stdc++.h>
#define ll long long
using namespace std;
int n, m, f[501], w[501][501];
int main()
{
    scanf("%d%d", &n, &m);
    for (int i = 1; i <= n; i++)
        for (int j = 1; j <= m; j++)
            scanf("%d", &w[i][j]);
    for (int i = 1; i <= n; i++)
        for (int j = m; j >= 0; j--)
            for (int k = 0; k <= j; k++)
                f[j] = max(f[j], f[j - k] + w[i][k]);
    printf("%d\n", f[m]);
}

4.没有上司的舞会2(书上背包)

1.题目

一家公司里有 n n n 个员工,他们的编号分别是 1 1 1 n n n,其中 1 1 1 号员工是公司 CEO,CEO 在公司里没有上司。除了 CEO 外,每个人都有一个直接上司。今天公司要办一个舞会,为了大家玩得尽兴,如果某个员工的直接上司来了,他/她就不想来了。 i i i 号员工来参加舞会会为大家带来 a i a_i ai 点快乐值。由于场地有大小限制,场地最多只能容纳 m m m 个人。现在我们想要确定一组员工参加舞会的方案,使得快乐值总和最大。请求出快乐值总和最大是多少。

输入格式

第一行两个整数 n,m。

接下来一行 n−1个整数 f2,f3,…,fn,其中 fi (1≤fi<i) 表示 i号员工的上司的编号。

接下来一行 n个整数 a1,a2,…,an

表示每个员工参加舞会带来的快乐值。

输出格式

一行一个整数表示答案。

2.分析

有人数限制,考虑 i i i 号员工的团队,除了考虑参加与否,还要考虑人数。

f [ i ] [ j ] [ 0 ] , f [ i ] [ j ] [ 1 ] f[i][j][0],f[i][j][1] f[i][j][0],f[i][j][1] 分别考虑 i i i 号员工的团队,团队中一共有不超过 j j j 个人参加,他不参加和参加的最大快乐值。

i i i 确定的时候,就转换成了求上一题。

3.代码

#include <bits/stdc++.h>
#define ll long long
using namespace std;
struct node
{
    node *next;
    int where;
} *first[100001], a[100001];
int n, m, l, c[100001], v[100001];
ll f[501][501][2];
inline void makelist(int x, int y)
{ // 连一条从x到y的边
    a[++l].where = y;
    a[l].next = first[x];
    first[x] = &a[l];
}
inline void solve(int i)
{
    for (node *x = first[i]; x; x = x->next)
    {
        solve(x->where);//先遍历所有的子树
        for (int j = m; j >= 0; j--)
            for (int k = 0; k <= j; k++)//枚举来了多少人
            {
                f[i][j][0] = max(f[i][j][0], f[i][j - k][0] + max(f[x->where][k][0], f[x->where][k][1]));
                f[i][j][1] = max(f[i][j][1], f[i][j - k][1] + f[x->where][k][0]);
            }
    }
    for (int j = m; j; --j)
        f[i][j][1] = f[i][j - 1][1] + v[i];//因为之前都是只考虑员工,所以在这里加上,子树来了j-1个人,然后加上现在这个
    f[i][0][1] = 0;
}
int main()
{
    scanf("%d%d", &n, &m);
    memset(first, 0, sizeof(first));
    l = 0;
    for (int i = 2; i <= n; i++)
    {
        int x;
        scanf("%d", &x);
        makelist(x, i);
    }
    for (int i = 1; i <= n; i++)
        scanf("%d", &v[i]);
    solve(1);
    printf("%lld\n", max(f[1][m][0], f[1][m][1]));
}

5.树的最长路径

1.题目

给定一棵树,树中包含 n n n 个结点(编号 1   n 1~n 1 n)和 n − 1 n−1 n1 条无向边,每条边都有一个权值。

现在请你找到树中的一条最长路径。

换句话说,要找到一条路径,使得使得路径两端的点的距离最远。

注意:路径中可以只包含一个点。

输入格式:
第一行包含整数 n 。接下来 n − 1行,每行包含三个整数 $ a_i,b_i,c_i $,表示点 $ a_i 和 和 b_i$ 之间存在一条权值为 c i c_i ci的边。

输出格式:
输出一个整数,表示树的最长路径的长度。

2.分析

1.任取一点作为起点,找到距离最远的一个点 u u u
2.再找距离 u u u 最远的一点 v v v。能 B F S BFS BFS B F S BFS BFS

那么 u u u v v v 之间的路径就是一条直径。

我们对每个路径进行分类,每次找到高度最高的点,然后将通过它的路径都归到这个点上。
到时候枚举所有的点即可,求出每个点挂的路径的最长的即可。

如何去求挂在它上的最大值,按它儿子方向找,找到从它往下走的最长边和次长边。

3.代码

#include <bits/stdc++.h>
#define ll long long
using namespace std;
const int N = 10010, M = N * 2;
int n;
int h[N], e[M], w[M], ne[M], idx;
int ans;
void add(int a, int b, int c)
{
    e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++;
}
int dfs(int u, int father) // father保证每次都是往下找
{
    int dist = 0;       // 表示从当前带你往下走的最大长度
    int d1 = 0, d2 = 0; // 存最大值和次大值
    for (int i = h[u]; i != -1; i = ne[i])
    {
        int j = e[i];
        if (j == father)
            continue;
        int d = dfs(j, u) + w[i];
        dist = max(dist, d);
        if (d >= d1)
            d2 = d1, d1 = d;
        else if (d > d2)
            d2 = d;
    }
    ans = max(ans, d1 + d2);
    return dist;
}
int main()
{
    cin >> n;
    memset(h, -1, sizeof h);
    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, -1);
    cout << ans << endl;
}

6.树的中心

1.题目

给定一棵树,树中包含 n n n 个结点(编号 1   n 1~n 1 n)和 n − 1 n−1 n1 条无向边,每条边都有一个权值。

请你在树中找到一个点,使得该点到树中其他结点的最远距离最近。

输入格式:
第一行包含整数 n 。接下来 n−1行,每行包含三个整数 $ a_i,b_i,c_i$,表示点 $ a_i$和 $ b_i$ 之间存在一条权值为 $ c_i$的边。

输出格式:
输出一个整数,表示所求点到树中其他结点的最远距离。

2.分析

对于每个节点,都有往下走和往上走。

往上走,先走到父节点,可以分为从父节点往上走,从父节点往下走不经过这个点,从父节点往下走经过这个点,如果第三种情况最长的话,我们就要记录一个次长值用来判断,因为第三种肯定不能要。

往下走,就直接走,找最长的。

3.代码

#include <bits/stdc++.h>
#define ll long long
using namespace std;
const int N = 10010, M = N * 2, INF = 0x3f3f3f3f;
int n;
int h[N], e[M], w[M], ne[M], idx;
int d1[N], d2[N], up[N],p1[N],p2[N]; // d1表示往下走最长,d2是次长,up是往上最长
int dfs_d(int u,int father)
{
    d1[u] = d2[u] = -INF;
    for (int i = h[u]; i != -1;i=ne[i])
    {
        int j = e[i];
        if(j==father)
            continue;
        int d = dfs_d(j, u) + w[i];
        if(d>=d1[u])
        {
            d2[u] = d1[u], d1[u] = d;
            p2[u] = p1[u], p1[u] = j;
        }
        else if(d>d2[u])
            d2[u] = d, p2[u] = j;
    }
    if(d1[u]==-INF)
        d1[u] = d2[u] = 0;
    return d1[u];
}
void dfs_u(int u,int father)
{
    for (int i = h[u]; i != -1;i=ne[i])
    {
        int j = e[i];
        if(j==father)
            continue;
        if(p1[u]==j)
            up[j] = max(up[u] , d2[u]) + w[i];
        else
            up[j] = max(up[u], d1[u]) + w[i];
        dfs_u(j, u);
    }
}
void add(int a,int b,int c)
{
    e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++;
}
int main()
{
    cin >> n;
    memset(h, -1, sizeof h);
    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_d(1, -1);
    dfs_u(1, -1);
    int res = INF;
    for (int i = 1; i <= n;i++)
        res = min(res, max(d1[i], up[i]));
}

7.数字转换

1.题目

如果一个数 x x x 的约数之和 y y y(不包括他本身)比他本身小,那么 x x x 可以变成 y y y y y y 也可以变成 x x x

例如, 4 4 4 可以变为 3 3 3 1 1 1 可以变为 7 7 7

限定所有数字变换在不超过 n n n 的正整数范围内进行,求不断进行数字变换且不出现重复数字的最多变换步数。

输入格式:

输入一个正整数 n。

输出格式:

输出不断进行数字变换且不出现重复数字的最多变换步数。

2.分析

以约数和作为父节点,能构成一堆树,相当于在这些树中找长度最长的路径。

快速求约数,可以用筛法,去找这个约数的倍数。

3.代码

#include <bits/stdc++.h>
#define ll long long
using namespace std;
const int N = 50010;
int n;
int h[N], e[N], ne[N], idx;
int sum[N];
bool st[N];
int ans;
void add(int a, int b)
{
    e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}
int dfs(int u)
{
    int d1 = 0, d2 = 0;
    for (int i = h[u]; i != -1; i = ne[i])
    {
        int j = e[i];
        int d = dfs(j) + 1;
        if (d >= d1)
            d2 = d1, d1 = d;
        else if (d > d2)
            d2 = d;
    }
    ans = max(ans, d1 + d2);
    return d1;
}
int main()
{
    cin >> n;
    for (int i = 1; i <= n; i++)
        for (int j = 2; j <= n / i; j++) // 怕溢出
            sum[i * j] += i;
    memset(h, -1, sizeof h);
    for (int i = 2; i <= n; i++)
    {
        if (i > sum[i])
        {
            add(sum[i], i);
            st[i] = true;
        }
    }
    for (int i = 1; i <= n; i++)
        if (!st[i])
            dfs(i);
    cout << ans << endl;
}

8.二叉苹果树

1.题目

一棵二叉苹果树,如果数字有分叉,一定是分两叉,即没有只有一个儿子的节点。这棵树共 N N N 个节点,标号 1 1 1 N N N,树根编号一定为 1 1 1

我们用一根树枝两端连接的节点编号描述一根树枝的位置。一棵苹果树因为树枝太多了,需要剪枝。但是一些树枝上长有苹果,给定需要保留的树枝数量,求最多能留住多少苹果。

这里的保留是指最终与 1 1 1 号点连通。

输入格式:

第一行包含两个整数 N和 Q,分别表示树的节点数以及要保留的树枝数量。接下来 N−1行描述树枝信息,每行三个整数,前两个是它连接的节点的编号,第三个数是这根树枝上苹果数量。

输出格式:

输出仅一行,表示最多能留住的苹果的数量。

2.分析

f ( i , j ) f(i,j) f(i,j) 表示以 i i i 为根的子树中选 j j j个树枝的最大价值。类似有依赖的背包问题。

3.代码

#include <bits/stdc++.h>
#define ll long long
using namespace std;
const int N = 110, M = N * 2;
int n, m;
int h[N], e[M], w[M], ne[M], idx;
int 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 father)
{
    for (int i = h[u]; ~i;i=ne[i])
    {
        int j = e[i];
        if(j==father)
            continue;
        dfs(e[i],u);
        for (int j = m; j >= 0;j--)
        {
            for (int k = 0; k < j;k++)
                f[u][j] = max(f[u][j], f[u][j - k - 1] + f[e[i]][k] + w[i]);
        }
    }
}
int main()
{
    cin >> n >> m;
    memset(h, -1, sizeof(h));
    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,-1);
    cout << f[1][m] << endl;
}

9.战略游戏

1.题目

Bob喜欢玩电脑游戏,特别是战略游戏。但是他经常无法找到快速玩过游戏的办法。现在他有个问题。他要建立一个古城堡,城堡中的路形成一棵树。他要在这棵树的结点上放置最少数目的士兵,使得这些士兵能了望到所有的路。注意,某个士兵在一个结点上时,与该结点相连的所有边将都可以被了望到。

请你编一程序,给定一树,帮Bob计算出他需要放置最少的士兵。

输入格式

输入包含多组测试数据,每组测试数据用以描述一棵树。

对于每组测试数据,第一行包含整数N,表示树的节点数目。

接下来N行,每行按如下方法描述一个节点。

节点编号:(子节点数目) 子节点 子节点 …

节点编号从0到N-1,每个节点的子节点数量均不超过10,每个边在输入数据中只出现一次。

输出格式

对于每组测试数据,输出一个占据一行的结果,表示最少需要的士兵数。

2.分析

每条边上最少选择一个点,求最小的权值。跟上司的误会对称。

f [ i ] [ j ] f[i][j] f[i][j] 表示所有以 i i i 为根的子树中选,且点 i i i 的状态是 j j j 的所有选法。

3.代码

#include <bits/stdc++.h>
#define ll long long
using namespace std;
const int N = 1510;
int n;
int h[N], e[N], ne[N], idx;
int f[N][2];
bool st[N];
void add(int a,int b)
{
    e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}
void dfs(int u)
{
    f[u][0] = 0;
    f[u][1] = 1;
    for (int i = h[u]; ~i;i=ne[i])
    {
        int j = e[i];
        dfs(j);
        f[u][0] += f[j][1];
        f[u][1] += min(f[j][0], f[j][1]);
    }
}
int main()
{
    while(scanf("%d",&n)==1)
    {
        memset(h, -1, sizeof h);
        idx = 0;
        memset(st, 0, sizeof st);
        for (int i = 0; i < n;i++)
        {
            int id, cnt;
            scanf("%d:(%d)", &id, &cnt);
            while(cnt--)
            {
                int ver;
                scanf("%d", &ver);
                add(id, ver);
                st[ver] = true;
            }
        }
        int root = 0;
        while(st[root])
            root++;
        dfs(root);
        printf("%d\n",min(f[root][0], f[root][1]));
    }
}

10.皇宫看守

1.题目

太平王世子事件后,陆小凤成了皇上特聘的御前一品侍卫。

皇宫各个宫殿的分布,呈一棵树的形状,宫殿可视为树中结点,两个宫殿之间如果存在道路直接相连,则该道路视为树中的一条边。

已知,在一个宫殿镇守的守卫不仅能够观察到本宫殿的状况,还能观察到与该宫殿直接存在道路相连的其他宫殿的状况。

大内保卫森严,三步一岗,五步一哨,每个宫殿都要有人全天候看守,在不同的宫殿安排看守所需的费用不同。

可是陆小凤手上的经费不足,无论如何也没法在每个宫殿都安置留守侍卫。

帮助陆小凤布置侍卫,在看守全部宫殿的前提下,使得花费的经费最少。

输入格式

输入中数据描述一棵树,描述如下:

第一行 n,表示树中结点的数目。

第二行至第 n+1 行,每行描述每个宫殿结点信息,依次为:该宫殿结点标号 i,在该宫殿安置侍卫所需的经费 k,该结点的子结点数 m,接下来 m 个数,分别是这个结点的 m 个子结点的标号 r1,r2,…,rm。

对于一个 n 个结点的树,结点标号在 1 到 n 之间,且标号不重复。

输出格式

输出一个整数,表示最少的经费。

2.分析

这道题是每个点以及周围的临点至少有一个点。

f ( i , 0 ) f(i,0) f(i,0) 表示点 i i i 被父节点看到的最小花费, f ( i , 1 ) f(i,1) f(i,1) 表示点 i i i 被子节点看到, f ( i , 2 ) f(i,2) f(i,2) 表示点 i i i 自己放一个。

3.代码

#include <bits/stdc++.h>
#define ll long long
using namespace std;
const int N = 1510;
int n;
int h[N], e[N], w[N], ne[N], idx;
int f[N][3];
bool st[N];
void add(int a, int b)
{
    e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}
void dfs(int u)
{
    f[u][2] = w[u];
    for (int i = h[u]; ~i; i = ne[i])
    {
        int j = e[i];
        dfs(j);
        f[u][0] += min(f[j][1], f[j][2]);
        f[u][2] += min(min(f[j][0], f[j][1]), f[j][2]);
    }
    f[u][1] = 1e9;
    for (int i = h[u]; ~i; i = ne[i])
    {
        int j = e[i];
        f[u][1] = min(f[u][1], f[j][2] + f[u][0] - min(f[j][1], f[j][2]));
    }
}
int main()
{
    cin >> n;
    memset(h, -1, sizeof h);
    for (int i = 1; i <= n; i++)
    {
        int id, cost, cnt;
        cin >> id >> cost >> cnt;
        w[id] = cost;
        while (cnt--)
        {
            int ver;
            cin >> ver;
            add(id, ver);
            st[ver] = true;
        }
    }
    int root = 1;
    while (st[root])
        root++;
    dfs(root);
    cout << min(f[root][1], f[root][2]) << endl;
}

11.距离和

1.题目

有一棵 n 个节点的树,节点编号从 1 到 n。请求出每个节点到其他所有节点的距离的和。

定义两个节点的距离为它们之间的简单路径上经过了多少条边。

输入格式

第一行一个整数 n表示节点数。

接下来 n−1行,每行两个整数 x,y 表示 x 号节点和 y号节点之间有一条边。

数据保证读入的是一棵树。

输出格式

输出共 n行,第 i 行一个整数表示 i 号节点到其他所有节点的距离的和。

2.分析

先考虑以1为根的有根树的情况,用size【i】表示以i为根的子树中有多少个点,f【i】表示考虑以i为根的子树,i到子树中其他所有点的距离的和。

假设j是i的儿子,以j为根的子树对f【i】的贡献为f【j】+size【j】

所以f【i】就是所有子树的f【j】的和+size【i】-1

我们用v【i】表示一个点的父亲作为它的子树时的贡献

当根从1变成它儿子i的时候
v【i】=f【1】-f【i】-size【i】+n-size【i】

当根从x变成它儿子y的时候
v【y】=v【x】+f【x】-f【y】-size【y】+n-size【y】

对于y,以它为根的答案为v【y】+f【y】

3.代码

#include <bits/stdc++.h>
#define ll long long
using namespace std;
struct node
{
    node *next;
    int where;
} *first[100001], a[200001];
int n, l;
int sze[100001];
long long f[100001], v[100001];
bool b[100001];
inline void makelist(int x, int y)
{
    a[++l].where = y;
    a[l].next = first[x];
    first[x] = &a[l];
}
inline void up(int i)//求f数组
{
    sze[i] = 1;
    b[i] = true;
    for (node *x = first[i]; x; x = x->next)
        if (!b[x->where])
        {
            up(x->where);
            sze[i] += sze[x->where];
            f[i] += f[x->where];
        }
    f[i] += sze[i] - 1;
}
inline void down(int i)//求z数组
{
    b[i] = true;
    for (node *x = first[i]; x; x = x->next)
    {
        if (!b[x->where])
        {
            v[x->where] = v[i] + f[i] - f[x->where] - sze[x->where] + n - sze[x->where];
            down(x->where);
        }
    }
}
int main()
{
    memset(first, 0, sizeof(first));
    l = 0;
    scanf("%d", &n);
    for (int i = 1; i < n; i++)
    {
        int x, y;
        scanf("%d%d", &x, &y);
        makelist(x, y);
        makelist(y, x);
    }
    memset(b, false, sizeof b);
    up(1);
    memset(b, false, sizeof b);
    down(1);
    for (int i = 1; i <= n; i++)
        printf("%lld\n", f[i] + v[i]);
}

12.流

1.题目

有一棵 n 个节点的树,节点编号从 1 到 n,树上的每条边都有流量限制。令某一个节点为根节点,向这个节点灌水,最终从叶子节点流出的水量之和为这个节点的最大流量。请求出每个节点的最大流量。

输入格式

第一行一个整数 n

表示节点数。

接下来 n−1行,每行三个整数 x,y,z 表示 x 号节点和 y 号节点之间有一条流量限制为 z的边。数据保证读入的是一棵树。

输出格式

输出共 n行,第 i 行一个整数表示 i 号节点的最大流量。

2.分析

先考虑以1为根的有根树的情况,f[i] 表示考虑以i为根的子树,最多可以承接多少上面下来的流量。

假设j是i的儿子,以j为根的子树对f【i】的贡献为min(f[i],c[j])

当根从1变成它的儿子i的时候。我们用v[i]表示一个点的父亲作为它的子树时的贡献,v[1]=0

v[i]=min(f[1]-min(c[i],f[i]),c[i])

当根从x变成它的儿子y的时候,v[y]=min(v[x]+f[x]-min(c[y],f[y]),c[y])

3.代码

#include <bits/stdc++.h>

using namespace std;

struct node
{
    node *next;
    int where, v;
} *first[100001], a[200001];

int n, l;
long long f[100001], v[100001];
bool b[100001];

inline void makelist(int x, int y, int z)
{
    a[++l].where = y;
    a[l].v = z;
    a[l].next = first[x];
    first[x] = &a[l];
}

inline void up(int i)
{
    b[i] = true;
    bool ok = true;
    for (node *x = first[i]; x; x = x->next)
        if (!b[x->where])
        {
            up(x->where);
            f[i] += min(1LL * x->v, f[x->where]);
            ok = false;
        }
    if (ok)
        f[i] = 1 << 30;
}

inline void down(int i)
{
    b[i] = true;
    for (node *x = first[i]; x; x = x->next)
        if (!b[x->where])
        {
            if (v[i] + f[i] - min(1LL * x->v, f[x->where]))
                v[x->where] = min(v[i] + f[i] - min(1LL * x->v, f[x->where]), 1LL * x->v);
            else
                v[x->where] = x->v;
            down(x->where);
        }
}

int main()
{
    memset(first, 0, sizeof(first));
    l = 0;
    scanf("%d", &n);
    for (int i = 1; i < n; i++)
    {
        int x, y, z;
        scanf("%d%d%d", &x, &y, &z);
        makelist(x, y, z);
        makelist(y, x, z);
    }
    memset(b, false, sizeof(b));
    up(1);
    memset(b, false, sizeof(b));
    down(1);
    for (int i = 1; i <= n; i++)
        if (f[i] != 1 << 30)
            printf("%lld\n", f[i] + v[i]);
        else
            printf("%lld\n", v[i]);
}

13.最长路径

1.题目

有一棵 n 个节点的树,节点编号从 1 到 n。对于每个节点,请求出经过它的长度最长的简单路径有多长。

定义一条路径的长度为这条路径上经过了多少条边。

输入格式

第一行一个整数 n表示节点数。

接下来 n−1行,每行两个整数 x,y 表示 x 号节点和 y号节点之间有一条边。数据保证读入的是一棵树。

输出格式

输出共 n行,第 i 行一个整数表示经过 i 号节点的长度最长的简单路径有多长。

2.分析

先考虑以1为根的有根树的情况,我们用f[i]记录考虑以i为根的子树,从i的不同子树里连上来的到i的最长的两条路径。

假设j是i的儿子,以j为根的子树连上来的路径是连接到j的最长的路径加上i

当根从1变成它的儿子i的时候,用v[i]表示一个点的父亲作为它的子树时从里面练上来的到i的最长路径有多长

3.代码

#include <bits/stdc++.h>

using namespace std;

struct node {
    node *next;
    int where;
} *first[100001], a[200001];

int n, l, f[100001][2][2], v[100001];
bool b[100001];

inline void makelist(int x, int y) {
    a[++l].where = y;
    a[l].next = first[x];
    first[x] = &a[l];
}

inline void up(int i) {
    b[i] = true;
    for (node *x = first[i]; x; x = x->next)
        if (!b[x->where]) {
            up(x->where);
            if (f[x->where][0][0] + 1 > f[i][0][0])
                f[i][1][0] = f[i][0][0], f[i][1][1] = f[i][0][1], f[i][0][0] = f[x->where][0][0] + 1, f[i][0][1] = x->where;
            else
                if (f[x->where][0][0] + 1 > f[i][1][0])
                    f[i][1][0] = f[x->where][0][0] + 1, f[i][1][1] = x->where;
        }
}

inline void down(int i) {
    b[i] = true;
    for (node *x = first[i]; x; x = x->next)
        if (!b[x->where]) {
            if (f[i][0][1] == x->where)
                if (v[i] > f[i][1][0])
                    v[x->where] = v[i] + 1;
                else
                    v[x->where] = f[i][1][0] + 1;
            else
                v[x->where] = max(f[i][0][0], v[i]) + 1;
            down(x->where);
        }
}

int main() {
    memset(first, 0, sizeof(first));
    l = 0;
    scanf("%d", &n);
    for (int i = 1; i < n; i++) {
        int x, y;
        scanf("%d%d", &x, &y);
        makelist(x, y);
        makelist(y, x);
    }
    memset(b, false, sizeof(b));
    up(1);
    memset(b, false, sizeof(b));
    down(1);
    for (int i = 1; i <= n; i++)
        printf("%d\n", f[i][0][0] + f[i][1][0] + v[i] - min(min(f[i][0][0], f[i][1][0]), v[i]));
}

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值