树形DP

题目一 包含每个点连通块计数

https://ac.nowcoder.com/acm/problem/19782

题目描述

修修去年种下了一棵树,现在它已经有n个结点了。
修修非常擅长数数,他很快就数出了包含每个点的连通点集的数量。
澜澜也想知道答案,但他不会数数,于是他把问题交给了你。

输入描述:

第一行一个整数n (1≤ n ≤ 106),接下来n-1行每行两个整数ai,bi表示一条边 (1≤ ai,bi≤ n)。

输出描述:

输出n行,每行一个非负整数。第i行表示包含第i个点的连通点集的数量对109+7取模的结果。
示例1
输入

6
1 2
1 3
2 4
4 5
4 6
输出

12
15
7
16
9
9

思路

f[i]:i的子树的连通点集的数量(一定包含i)。
往上转移时:f[x]=f[x](f[y]+1) y是x的子节点,+1是可以不选这棵子树
当然我们只统计的子树,我们没有统计父节点往上的方案。
换根时:fsz:代表父节点来的方案数。
ans[i]:i节点的方案数。ans[i]=f[u](fsz+1)
当去x子树时。往下传时:fsz=ans[i]/(f[x]+1)

因为有取mod,所以f[x]+1时,0是没有逆元的。我们要重新计算贡献。

#include <bits/stdc++.h>
#define LL long long
const int mod=1e9+7;
using namespace std;

vector<vector<int> > G(1000005);
LL f[1000005], ans[1000005];

LL ksm(LL a, LL b){
    LL ans=1;
    while(b){
        if(b&1) ans=ans*a%mod;
        a=a*a%mod;
        b>>=1;
    }
    return ans;
}

void DFS(int u, int fa){
    for(auto x: G[u]){
        if(x!=fa){
            DFS(x, u);
            f[u]=f[u]*(f[x]+1)%mod;
        }
    }
}

void dfs(int u, int fa, LL fsz){
    ans[u]=f[u]*(fsz+1)%mod;
    for(auto x: G[u]){
        if(x!=fa){
            if((f[x]+1)%mod){//f[x]+1!=0时
                dfs(x, u, ans[u]*ksm(f[x]+1, mod-2)%mod);
            }
            else{
                LL t=(fsz+1);
                for(auto y: G[u]){
                    if(x!=y){
                        t=t*(f[y]+1)%mod;
                    }
                }
                dfs(x, u, t);
            }
        }
    }
}

int main() {
    int n; scanf("%d", &n);
    for(int i=1; i<=n; i++) f[i]=1;
    for(int i=2; i<=n; i++){
        int x, y; scanf("%d%d", &x, &y);
        G[x].push_back(y);
        G[y].push_back(x);
    }
    DFS(1, 0);
    dfs(1, 0, 0);
    for(int i=1; i<=n; i++){
        printf("%lld\n", ans[i]);
    }

    return 0;
}

题目二 和树重心相同的连通块计数

https://ac.nowcoder.com/acm/problem/19953

题目描述

给定一个n个点的树,每个点的编号从1至n,问这个树有多少不同的连通子树,和这个树有相同的重心。
其中n个点的树指的是n个点的最小连通图,显然n个点的树有n-1条边,去掉这n-1条边中的任何一条,原图都不再联通,任意两个点之间由唯一一条路径相连。 对于一个树,树的重心定义为:
删掉某点i后,若剩余k个连通分量,那么定义d(i)为这些连通分量中点的个数的最大值,所谓重心,就是使得d(i)最小的点i。
基于以上定义,一个树的重心可能会有一个或者两个,题中所要求的联通子树,其重心编号和个数必须和原树的完全一样。
找出给定的树中有多少联通的子树和这个树有相同的重心。

输入描述:

第1行中给出正整数Q,表示该组数据中有多少组测试样例。
每组样例首先输入一个整数n (0 < n ≤ 200),表示该组样例中输入的树包含n个点,之后n-1行,每行输入两整数数x,y(1 ≤ x, y ≤ n),表示编号为x的点和编号为y的点之间存在一条边,所有点的编号从1-n

输出描述:

首先输出样例编号,之后输出满足条件的子树的个数,由于这个数字较大,你只需要输出这个数字对10007取模后的结果,即mod 10007,详见输出示例,请严格按照输出实例中的格式输出

示例1

输入
3
2
1 2
3
1 2
2 3
5
1 2
1 3
2 4
2 5

输出

Case 1: 1
Case 2: 2
Case 3: 6

思路

主要是重心性质:
根据重心的性质我们知道:如果重心的所有子树s1 s2 s3 ... sk,假设s1为最大
1.若2s1<n,那么重心唯一
2.若2s1=n,那么重心有两个且相邻

所以我们求出所有重心后,分情况讨论

1.如果重心是唯一的,我们把重心提根
现在我们需要选取子树使得重心唯一且为根节点
我们枚举一下选取子树大小,设为Size,那么需要根节点所有子树的大小s满足2*s1<Size,且s1+...+sk=Size-1
树上背包就可以了。

2.如果重心有两个,那么他们一定相邻,我们将两个重心之间边切去,那么变成两棵树

我们需要从两个树中选取大小相同的两颗子树(注意一定要连着重心)

对每个重心跑一个树上背包就可以了。

然后枚举两棵子树大小乘起来即可
时间复杂度O(N^3*Q)

#include <bits/stdc++.h>
#define LL long long
const int mod=10007;
using namespace std;

vector<vector<int> > G(205);
int sz[205], g[205], n;
int dfs(int u, int fa) {
    int res=1<<30;
    sz[u]=1, g[u]=0;
    for(auto x: G[u]) {
        if(x!=fa) {
            res=min(res, dfs(x, u));
            sz[u]+=sz[x], g[u]=max(g[u], sz[x]);
        }
    }
    g[u]=max(g[u], n-sz[u]);
    res=min(res, g[u]);
    return res;
}
vector<int> root;//保存重心

int f[205][205], f1[205][205];
void DP(int u, int fa, int n) {
    f[u][1]=1;
    sz[u]=1;
    for(auto x: G[u]) {
        if(x==fa) continue;
        DP(x, u, n);
        for(int p=sz[u]; p>=1; p--) {
            for(int q=min(sz[x], (n+1)/2-1); q>=1; q--) {
                f[u][p+q]+=(f[u][p]*f[x][q])%mod;
                f[u][p+q]%=mod;
            }
        }
        sz[u]+=sz[x];
    }
}

int main() {
    int t, cas=1; scanf("%d", &t);
    while(t--) {
        scanf("%d", &n);
        for(int i=1; i<=n; i++){
            G[i].clear(); g[i]=0, sz[i]=0;
        }
        root.clear();
        for(int i=2; i<=n; i++) {
            int x, y;
            scanf("%d%d", &x, &y);
            G[x].push_back(y);
            G[y].push_back(x);
        }
        int mi=dfs(1, 0);
        for(int i=1; i<=n; i++) {//得到所有的重心,最多只有2个
            if(g[i]==mi) {
                root.push_back(i);
            }
        }
        LL ans=0;
        if(root.size()==1) {
            memset(sz, 0, sizeof(sz));
            for(int sz=1; sz<=n; sz++) { //枚举siz的大小
                memset(f, 0, sizeof(f));
                DP(root[0], 0, sz);
                ans+=f[root[0]][sz];
                ans%=mod;
            }
            printf("Case %d: %lld\n", cas++, ans);
        } else if(root.size()==2) {
            memset(sz, 0, sizeof(sz));
            memset(f, 0, sizeof(f));
            DP(root[0], root[1], n);
            memcpy(f1, f, sizeof(f));

            memset(sz, 0, sizeof(sz));
            memset(f, 0, sizeof(f));
            DP(root[1], root[0], n);

            for(int i=1; i<=n; i++){
                ans=(ans+f[root[1]][i]*f1[root[0]][i]%mod)%mod;
            }
            printf("Case %d: %lld\n", cas++, ans);
        }
    }

    return 0;
}

题目三 树上点集染色求两两距离和

https://ac.nowcoder.com/acm/problem/19996

题目描述

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

输入描述:

第一行两个整数N,K。
接下来N-1行每行三个正整数fr,to,dis,表示该树中存在一条长度为dis的边(fr,to)。
输入保证所有点之间是联通的。N ≤ 2000,0 ≤ K ≤ N

输出描述:

输出一个正整数,表示收益的最大值。

示例1

输入
5 2
1 2 3
1 5 1
2 3 1
2 4 2
输出
17

说明

【样例解释】
将点1,2染黑就能获得最大收益。

思路

树上背包就可以了。计算每条边的经过次数
如果在x这棵子数染了q个黑点。那么黑色点之间经过u-x这条边的次数为q(kq)
那么x棵子树有siz[x]-q个白点,那么白色点之间经过u-x这条边的次数为(nk(siz[x]q))(siz[x]q)

#include <bits/stdc++.h>
#define LL long long
#define pii pair<int, int>
using namespace std;

vector<vector<pii> > G(2005);
LL f[2005][2005], siz[2005];
LL n, k;
void DFS(int u, int fa){
    f[u][1]=0; siz[u]=1;
    for(auto t: G[u]){
        int x=t.first, w=t.second;
        if(x!=fa){
            DFS(x, u);
            for(int p=min(siz[u], k); p>=0; p--){
                for(int q=min(siz[x], k-p); q>=0; q--){
                    f[u][p+q]=max(f[u][p+q], f[u][p]+f[x][q]+q*(k-q)*w+(siz[x]-q)*(n-k-(siz[x]-q))*w);
                }
            }
            siz[u]+=siz[x];
        }
    }
}

int main() {

    scanf("%lld%lld", &n, &k);
    for(int i=2; i<=n; i++){
        int x, y, z; scanf("%d%d%d", &x, &y, &z);
        G[x].push_back({y, z}); G[y].push_back({x, z});
    }
    DFS(1, 0);
    printf("%lld\n", f[1][k]);

    return 0;
}

题目四 树上有限制的染色

https://ac.nowcoder.com/acm/problem/19914

题目描述

给一棵m个结点的无根树,你可以选择一个度数大于1的结点作为根,然后给一些结点(根、内部结点和叶子均可)着以黑色或白色。你的着色方案应该保证根结点到每个叶子的简单路径上都至少包含一个有色结点(哪怕是这个叶子本身)。
对于每个叶结点u,定义c[u]为从根结点从U的简单路径上最后一个有色结点的颜色。给出每个c[u]的值,设计着色方案,使得着色结点的个数尽量少。
输入描述:
第一行包含两个正整数m, n,其中n是叶子的个数,m是结点总数。结点编号为1,2,…,m,其中编号1,2,… ,n是叶子。
以下n行每行一个0或1的整数(0表示黑色,1表示白色),依次为c[1],c[2],…,c[n]。
以下m-1行每行两个整数a,b(1 ≤ a < b ≤ m),表示结点a和b 有边相连。
输出描述:
仅一个数,即着色结点数的最小值。

示例1

输入
5 3
0
1
0
1 4
2 5
4 5
3 5

输出

2

思路

有个小结论:选不同的根节点没有影响
用f[i][0/1/2]:表示i节点染(白/黑/不染色)的方案数,并且满足子树所有的叶子要求的最小染色节点数
叶子节点:
f[u][c[u]]=1;
f[u][!c[u]]=1<<30;
f[u][2]=1<<30;
非叶子节点:
f[u][0]+=min(f[x][0]-1, min(f[x][1], f[x][2]));
f[u][1]+=min(f[x][1]-1, min(f[x][0], f[x][2]));
f[u][2]+=min(f[x][0], min(f[x][1], f[x][2]));

#include <bits/stdc++.h>
#define LL long long
using namespace std;

vector<vector<int> > G(10005);
int f[10005][3], c[10005];
int n, m;
void DFS(int u, int fa){
    f[u][0]=f[u][1]=1;
    for(auto x: G[u]){
        if(x!=fa){
            DFS(x, u);
            f[u][0]+=min(f[x][0]-1, min(f[x][1], f[x][2]));
            f[u][1]+=min(f[x][1]-1, min(f[x][0], f[x][2]));
            f[u][2]+=min(f[x][0], min(f[x][1], f[x][2]));
        }
    }
    if(G[u].size()==1){
        f[u][c[u]]=1;
        f[u][!c[u]]=1<<30;
        f[u][2]=1<<30;
    }
}

int main() {

    scanf("%d%d", &n, &m);
    for(int i=1; i<=m; i++){
        scanf("%d", &c[i]);
    }
    for(int i=2; i<=n; i++){
        int x, y; scanf("%d%d", &x, &y);
        G[x].push_back(y); G[y].push_back(x);
    }
    DFS(m+1, 0);
    printf("%d\n", min(f[m+1][0], min(f[m+1][1], f[m+1][2])));

    return 0;
}

题目五 树上贪心距离为2的覆盖

https://ac.nowcoder.com/acm/problem/20031

题目描述

2020年,人类在火星上建立了一个庞大的基地群,总共有n个基地。起初为了节约材料,人类只修建了n-1条道路来连接这些基地,并且每两个基地都能够通过道路到达,所以所有的基地形成了一个巨大的树状结构。如果基地A到基地B至少要经过d条道路的话,我们称基地A到基地B的距离为d。
由于火星上非常干燥,经常引发火灾,人类决定在火星上修建若干个消防局。消防局只能修建在基地里,每个消防局有能力扑灭与它距离不超过2的基地的火灾。你的任务是计算至少要修建多少个消防局才能够确保火星上所有的基地在发生火灾时,消防队有能力及时扑灭火灾。

输入描述:

输入文件的第一行为n,表示火星上基地的数目。
接下来的n-1行每行有一个正整数,其中文件第i行的正整数为a[i],表示从编号为i的基地到编号为a[i]的基地之间有一条道路,为了更加简洁的描述树状结构的基地群,有a[i]

输出描述:

输出文件仅有一个正整数,表示至少要设立多少个消防局才有能力及时扑灭任何基地发生的火灾。

示例1

输入
6
1
2
3
4
5
输出
2

思路

我们贪心的选择:先选择最深的节点,因为它不可以由子节点覆盖。那么他可以被兄弟节点,父节点,爷节点覆盖。肯定被爷节点覆盖是最优的。
如果我们把爷节点可以覆盖的节点全部覆盖,继续选择没有被覆盖的最深的节点。用优先队列就可以了。

#include <bits/stdc++.h>
#define LL long long
#define pii pair<int, int>
using namespace std;

vector<vector<int> > G(1005);
int d[1005], f[1005], vis[1005];
priority_queue<pii> q;
void DFS(int u, int fa){
    d[u]=d[fa]+1; f[u]=fa;
    q.push({d[u], u});
    for(auto x: G[u]){
        if(x!=fa){
            DFS(x, u);
        }
    }
}

void dfs(int u, int d){
    if(d<0) return ;
    vis[u]=1;
    for(auto x: G[u]){
        dfs(x, d-1);
    }
}

int main() {

    int n; scanf("%d", &n);
    for(int i=2; i<=n; i++){
        int x; scanf("%d", &x);
        G[i].push_back(x); G[x].push_back(i);
    }
    DFS(1, 0);
    int ans=0;
    while(!q.empty()){
        pii t=q.top(); q.pop();
        int x=t.second;
        if(vis[x]) continue;
        ans++;
        if(f[f[x]]==0) break;
        dfs(f[f[x]], 2);//把f[f[x]]可以覆盖到的点全部处理
    }
    printf("%d\n", ans);

    return 0;
}

题目六 D范围的覆盖最小代价

[JLOI2016]侦察守卫(https://ac.nowcoder.com/acm/problem/20562)

题目描述

小R和B神正在玩一款游戏。这款游戏的地图由N个点和N-1条无向边组成,每条无向边连接两个点,且地图是连通的 。换句话说,游戏的地图是一棵有N个节点的树。
游戏中有一种道具叫做侦查守卫,当一名玩家在一个点上放置侦 查守卫后,它可以监视这个点以及与这个点的距离在D以内的所有点。这里两个点之间的距离定义为它们在树上的 距离,也就是两个点之间唯一的简单路径上所经过边的条数。
在一个点上放置侦查守卫需要付出一定的代价,在不 同点放置守卫的代价可能不同。现在小R知道了所有B神可能会出现的位置,请你计算监视所有这些位置的最小代价 。

输入描述:

第一行包含两个正整数N和D,分别表示地图上的点数和侦查守卫的视野范围。约定地图上的点用1到N的整数编号。
第二行N个正整数,第i个正整数表示在编号为i的点放置侦查守卫的代价Wi。保证Wi ≤ 1000
第三行一个正整数M,表示B神可能出现的点的数量。保证M ≤ N。
第四行M个正整数,分别表示每个B神可能出现的点的编号,从小到大不重复地给出。
接下来N–1行,每行包含两个正整数U,V,表示在编号为U的点和编号为V的点之间有一条无向边。
N ≤ 500000,D ≤ 20

输出描述:

仅一行一个整数,表示监视所有B神可能出现的点所需要的最小代价

示例1

输入
12 2
8 9 12 6 1 1 5 1 4 8 10 6
10
1 2 3 5 6 7 8 9 10 11
1 3
2 3
3 4
4 5
4 6
4 7
7 8
8 9
9 10
10 11
11 12
输出
10

思路

状态设计

假设当前到了 x 的子树,现在该合并 x 的第 k 个子树。

f[x][j] 表示 x 的前 k-1 个子树该覆盖的完全覆盖,而且还能向四周覆盖 j 层(不含 x )的最小代价。

对于第 k 个子树的意义就是,兄弟子树放置的守卫可以帮 x 的第 k 个子树覆盖前 j 层(第 1 层为 x 的子节点),那么相应的就要有一个状态来表示这个 可以让兄弟子树帮忙覆盖 的前 j 层。

g[x][j] 表示还需要覆盖 x 的前 k 个子树中的前 j 层(含 x),且第 j 层以下该覆盖的完全覆盖的最小代价。

状态转移
要能向 x 的四周覆盖 j 层,只需要 x 的子节点中有一个子节点 z 能向上覆盖 j+1 层即可。所以

f[x][j] = min(f[x][j]+g[y][j] ,f[y][j+1]+g[x][j+1])

g[x][j] += g[y][j-1]

但是有可能更新后 x 能向上恰好覆盖 j 层的代价要小于能向上恰好覆盖 j-1 层的代价;也有可能 x 需要向下覆盖 j 层的代价小于再向下覆盖 j+1 层的代价。

所以将f的状态定义改为 向上覆盖至少j层的最小代价

同理,g的状态定义改为还需要覆盖至多j层的最小代价

所以要对f[x]做一个后缀最小值,g[x]做一个前缀最小值。

时间复杂度O(ND)。

#include <bits/stdc++.h>
#define LL long long
using namespace std;

vector<vector<int> > G(500005);
int w[500005], id[500005];
int f[500005][25], g[500005][25];
int n, d;
void dfs(int u, int fa) {
    for(int i=1; i<=d; i++) f[u][i]=w[u];
    if(id[u]) g[u][0]=f[u][0]=w[u];

    f[u][d+1]=1<<30;
    for(auto x: G[u]) {
        if(x!=fa) {
            dfs(x, u);
            for(int j=0; j<=d; j++){
                f[u][j]=min(f[u][j]+g[x][j], f[x][j+1]+g[u][j+1]);
            }
            for(int j=d; j>=0; j--){
                f[u][j]=min(f[u][j], f[u][j+1]);
            }
            g[u][0]=f[u][0];
            for(int j=1;j<=d;++j) g[u][j]+=g[x][j-1];
            for(int j=1;j<=d;++j) g[u][j]=min(g[u][j],g[u][j-1]);
        }
    }
}

int main() {

    scanf("%d%d", &n, &d);
    for(int i=1; i<=n; i++) {
        scanf("%d", &w[i]);
    }
    int m;
    scanf("%d", &m);
    for(int i=1; i<=m; i++) {
        int x;
        scanf("%d", &x);
        id[x]=1;
    }
    for(int i=2; i<=n; i++) {
        int x, y;
        scanf("%d%d", &x, &y);
        G[x].push_back(y);
        G[y].push_back(x);
    }
    dfs(1,0);
    printf("%d\n",g[1][0]);

    return 0;
}

题目七 树上概率+换根

[SHOI2014]概率充电器(https://ac.nowcoder.com/acm/problem/20589)

题目描述

著名的电子产品品牌 SHOI 刚刚发布了引领世界潮流的下一代电子产品——概率充电器: “采用全新纳米级加工技术,实现元件与导线能否通电完全由真随机数决定!SHOI 概率充电器,您生活不可或缺的必需品!能充上电吗?现在就试试看吧! ” SHOI 概率充电器由 n-1 条导线连通了 n 个充电元件。进行充电时,每条导线是否可以导电以概率决定,每一个充电元件自身是否直接进行充电也由概率决定。 随后电能可以从直接充电的元件经过通电的导线使得其他充电元件进行间接充电。 作为 SHOI 公司的忠实客户,你无法抑制自己购买 SHOI 产品的冲动。在排了一个星期的长队之后终于入手了最新型号的 SHOI 概率充电器。 你迫不及待地将 SHOI 概率充电器插入电源——这时你突然想知道,进入充电状态的元件个数的期望是多少呢?

输入描述:

第一行一个整数:n。概率充电器的充电元件个数。充电元件由 1-n 编号。
之后的 n-1 行每行三个整数 a, b, p,描述了一根导线连接了编号为 a 和 b 的 充电元件,通电概率为 p%。
第 n+2 行 n 个整数:qi。表示 i 号元件直接充电的概率为 qi%。

输出描述:

输出一行一个实数,为进入充电状态的元件个数的期望,四舍五入到六位小数

示例1

输入
3
1 2 50
1 3 50
50 0 0
输出
1.000000

思路

我们很好想到一个状态f[i]:i节点由子节点(包含自己直接亮)点亮的概率。但是这个状态很难转移。
因为不是独立事事件。例如i有x1,x2棵子树。那么可以f[i]+=P(x1)+P(x2)P(x1x2)
如果子树更多,情况就更复杂了。要各种容斥。
所以我们用f[i]:i节点由子节点(包含自己直接亮)不能点亮的概率。就可以直接乘。

换根,因为1节点的答案是正确的。我们往x子树转移时:
u节点由除x子树被点亮的概率:
那么
f[u]就是u节点不能被点亮的概率。

#include <bits/stdc++.h>
#define LL long long
#define pii pair<int, int>
using namespace std;

vector<vector<pii> > G(500005);
double p[500005], f[500005];

void dfs(int u, int fa) {
    f[u]=(1-p[u]);
    for(auto to: G[u]){
        int x=to.first;
        double w=to.second*0.01;
        if(x!=fa){
            dfs(x, u);
            f[u]=f[u]*(f[x]+(1-f[x])*(1-w));
        }
    }
}

void dfs2(int u, int fa, double fp, double ww){

    f[u]=f[u]*(fp+(1-fp)*(1-ww));
    for(auto to: G[u]){
        int x=to.first;
        double w=to.second*0.01;
        if(x!=fa){
            dfs2(x, u, f[u]/(f[x]+(1-f[x])*(1-w)) ,w);
        }
    }
}

int main() {

    int n; scanf("%d", &n);
    for(int i=2; i<=n; i++){
        int x, y, z; scanf("%d%d%d", &x, &y, &z);
        G[x].push_back({y, z}); G[y].push_back({x, z});
    }
    for(int i=1; i<=n; i++){
        scanf("%lf", &p[i]);
        p[i]/=100;
    }
    dfs(1, 0);
    dfs2(1, 0, 0, 0);
    double ans=0;
    for(int i=1; i<=n; i++){
        ans+=(1-f[i]);
    }
    printf("%.6f\n", ans);

    return 0;
}

题目八 树上半径

题目描述
从加里敦大学城市规划专业毕业的小明来到了一个地区城市规划局工作。这个地区一共有 nn 座城市,n-1n−1 条高速公路,保证了任意两运城市之间都可以通过高速公路相互可达,但是通过一条高速公路需要收取一定的交通费用。小明对这个地区深入研究后,觉得这个地区的交通费用太贵。

小明想彻底改造这个地区,但是由于上司给他的资源有限,因而小明现在只能对一条高速公路进行改造,改造的方式就是去掉一条高速公路,并且重新修建一条一样的高速公路(即交通费用一样),使得这个地区的两个城市之间的最大交通费用最小(即使得交通费用最大的两座城市之间的交通费用最小),并且保证修建完之后任意两座城市相互可达。如果你是小明,你怎么解决这个问题?

输入格式

输入数据的第一行为一个整数 nn,代表城市个数。

接下来的 n−1 行分别代表了最初的 n-1 条公路情况。每一行都有三个整数 u,v,d, u,v 代表这条公路的两端城市标号,d 代表这条公路的交通费用。

1≤u,v≤n, 1≤d≤2000。

输出格式

输出数据仅有一行,一个整数,表示进行了最优的改造之后,该地区两城市 之间最大交通费用。

输入

5
1 2 1
2 3 2
3 4 3
4 5 4

输出

7
说明/提示
对于 30% 的数据,1≤n≤500。

对于 100% 的数据,1≤n≤5000。

思路

可以贪心的想:断的肯定是直径。不然最远距离不会改变。断开之后形成两棵子树。
最大距离可能在(子树1的直径r1),(子树2的直径r2)。(或者子树1的半径d1+子树2的半径d2+w)。
半径:树上一点到任意点的最大距离最小。这个最小距离为半径。
半径用树形DP+换根就可以解决。

#include <bits/stdc++.h>
#define LL long long
#define pii pair<int, int>
using namespace std;

vector<vector<pii> > G(5005);
vector<pii> d;
//遍历两次求直径
pii dfsd(int u, int fa, int s) {
    int mx=0, root=u;
    for(auto to: G[u]) {
        int x=to.first, w=to.second;
        if(x!=fa) {
            pii ans=dfsd(x, u, s);
            if(ans.first+w>mx) {
                mx=ans.first+w;
                root=ans.second;
            }
        }
    }
    return {mx, root};
}

//得到直径上的所有点
int dfsd2(int u, int fa, int root) {
    if(u==root)
        return 1;
    for(auto to: G[u]) {
        int x=to.first, w=to.second;
        if(x!=fa) {
            if(dfsd2(x, u, root)) {
                d.push_back({x, w});
                return 1;
            }
        }
    }
    return 0;
}

//dp得到直径
int mx1[5005], mx2[5005];
int DP1(int u, int fa) {
    int res=0;
    for(auto to: G[u]) {
        int x=to.first, w=to.second;
        if(x!=fa) {
            res=max(DP1(x, u), res);
            if(mx1[x]+w>=mx1[u]) {
                mx2[u]=mx1[u];
                mx1[u]=mx1[x]+w;
            } else if(mx1[x]+w>mx2[u]){
                mx2[u]=mx1[x]+w;
            }
        }
    }
    return max(res, mx1[u]+mx2[u]);
}

//换根得到半径
int DP2(int u, int fa, int s) {
    int ret=1<<30;
    ret=max(s, mx1[u]);
    if(s>=mx1[u]) {
        mx2[u]=mx1[u];
        mx1[u]=s;
    } else if(s>mx2[u]){
        mx2[u]=s;
    }
    for(auto to: G[u]) {
        int x=to.first, w=to.second;
        if(x!=fa) {
            if(mx1[x]+w==mx1[u]) {
                ret=min(ret, DP2(x, u, mx2[u]+w));
            } else {
                ret=min(ret, DP2(x, u, mx1[u]+w));
            }
        }
    }
    return ret;
}

int main() {

    int n; scanf("%d", &n);
    for(int i=2; i<=n; i++) {
        int x, y, z;
        scanf("%d%d%d", &x, &y, &z);
        G[x].push_back({y, z});
        G[y].push_back({x, z});
    }
    pii ans1=dfsd(1, 0, 0);
    pii ans2=dfsd(ans1.second, 0, 0);

    dfsd2(ans1.second, 0, ans2.second);
    d.push_back({ans1.second, 0});

    DP1(1, 0);
    DP2(1, 0, 0);
    int ans=1<<30;
    for(int i=1; i<d.size(); i++) {
        memset(mx1, 0, sizeof(mx1));
        memset(mx2, 0, sizeof(mx2));
        int d1=DP1(d[i-1].first, d[i].first);
        int r1=DP2(d[i-1].first, d[i].first, 0);

        memset(mx1, 0, sizeof(mx1));
        memset(mx2, 0, sizeof(mx2));
        int d2=DP1(d[i].first, d[i-1].first);
        int r2=DP2(d[i].first, d[i-1].first, 0);

        d1=max(d1, d2);
        d1=max(d1, r1+r2+d[i-1].second);
        ans=min(ans, d1);

    }
    printf("%d\n", ans==(1<<30)?0:ans);

    return 0;
}
/*
5
1 3 2
2 3 2
3 4 1
4 5 3
*/

P1131 [ZJOI2007]时态同步 根到叶子节点的深度一样的最少代价

P1131 [ZJOI2007]时态同步(https://www.luogu.com.cn/problem/P1131)

题目描述

小QQ在电子工艺实习课上学习焊接电路板。一块电路板由若干个元件组成,我们不妨称之为节点,并将其用数字1,2,3…进行标号。电路板的各个节点由若干不相交的导线相连接,且对于电路板的任何两个节点,都存在且仅存在一条通路(通路指连接两个元件的导线序列)。

在电路板上存在一个特殊的元件称为“激发器”。当激发器工作后,产生一个激励电流,通过导线传向每一个它所连接的节点。而中间节点接收到激励电流后,得到信息,并将该激励电流传向与它连接并且尚未接收到激励电流的节点。最终,激烈电流将到达一些“终止节点”――接收激励电流之后不再转发的节点。

激励电流在导线上的传播是需要花费时间的,对于每条边e,激励电流通过它需要的时间为te
e

,而节点接收到激励电流后的转发可以认为是在瞬间完成的。现在这块电路板要求每一个“终止节点”同时得到激励电路――即保持时态同步。由于当前的构造并不符合时态同步的要求,故需要通过改变连接线的构造。目前小QQ有一个道具,使用一次该道具,可以使得激励电流通过某条连接导线的时间增加一个单位。请问小Q最少使用多少次道具才可使得所有的“终止节点”时态同步?

输入格式

第一行包含一个正整数N,表示电路板中节点的个数。

第二行包含一个整数S,为该电路板的激发器的编号。

接下来N-1行,每行三个整数a , b , t。表示该条导线连接节点a与节点b,且激励电流通过这条导线需要t个单位时间。

输出格式

仅包含一个整数V,为小Q最少使用的道具次数。

输入

3
1
1 2 1
1 3 3

输出

2

说明/提示

对于40%的数据,N≤1000

对于100%的数据,N≤500000

对于所有的数据,te≤1000000

思路

很容易想的树形DP:
f[i]:把节点i为根的所有叶子节点变的一样深的最小代价。我们肯定是把所有的叶子变的和最深的一样深。
所以再统计一个最大深的叶子节点就可以了。

#include <bits/stdc++.h>
#define LL long long
using namespace std;

struct Edge{
    int to, w, nxt;
}E[500005*2];
int head[500005], cut=0;
void AddEdge(int x, int y, int w){
    E[++cut]={y, w, head[x]}; head[x]=cut;
}
LL f[500005];
LL mx[500005];
void DFS(int u, int fa){
    for(int i=head[u]; i; i=E[i].nxt){
        int to=E[i].to, w=E[i].w;
        if(to==fa) continue;
        DFS(to, u); mx[u]=max(mx[u], mx[to]+w);
    }
    for(int i=head[u]; i; i=E[i].nxt){
        int to=E[i].to, w=E[i].w;
        if(to==fa) continue;
        f[u]+=(f[to]+mx[u]-(mx[to]+w));
    }
}

int main() {

    int n, root; scanf("%d%d", &n, &root);
    for(int i=1; i<n; i++){
        int a, b, w; scanf("%d%d%d", &a, &b, &w);
        AddEdge(a, b, w); AddEdge(b, a, w);
    }
    DFS(root, 0);
    printf("%lld\n", f[root]);

    return 0;
}
posted @   liweihang  阅读(341)  评论(0编辑  收藏  举报
编辑推荐:
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
阅读排行:
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)
Live2D
欢迎阅读『树形DP』
点击右上角即可分享
微信分享提示