树形DP练习

树的最长路径

题目链接:树的最长路径
分析:我们需要在树上DP,状态表示这一方面是比较困难的,我们要找最长的一条路径,那我们随意画一棵树
在这里插入图片描述
代码实现:

#include<iostream>
using namespace std;
const int N=1e4+10,M=2*N;
int n,h[N],w[M],ne[M],idx=1,e[M],ans;
void add(int a,int b,int c){
    e[idx]=b,ne[idx]=h[a],w[idx]=c,h[a]=idx++;
}
int dfs(int u,int father){
    int d1=0,d2=0;//d1记录最长的边,d2记录第2长的边
    for(int i=h[u];i;i=ne[i]){//遍历所有出边
        int j=e[i];
        if(j==father)   continue;//如果是它的父亲,就跳过,这里放止往回搜
        int d=dfs(j,u)+w[i];//搜出以j为根节点的子树中最长的加上w[i]
        if(d>=d1){//更新最长边和第二长边
            d2=d1;
            d1=d;
        }
        else if(d>d2){//更新第二长边
            d2=d;
        }
    }
    ans=max(ans,d1+d2);//更新答案
    return d1;//返回最长的边
}
int main(){
    scanf("%d",&n);
    for(int i=1;i<n;i++){
        int a,b,c;
        scanf("%d%d%d",&a,&b,&c);
        add(a,b,c);//加两条边
        add(b,a,c);
    }
    dfs(1,0);//第二个参数随意传一个没用到的结点,第一个结点随意,因为不管从哪个结点开始,都可以转化为一棵树
    printf("%d\n",ans);
    return 0;
}

树的中心

题目链接:树的中心
分析:这是一道很好的树形DP题,我们可以随便选取一个点,向上遍历所有点,再向下遍历所有点,遍历的途中更新答案,详细讲解见代码。
代码实现:
代码中的细节还是比较多的

#include<iostream>
using namespace std;
const int N=1e4+10,M=2*N,INF=1e9;
int n,h[N],e[M],ne[M],w[M],idx=1;
bool is_leaf[N];
int d1[N],d2[N],p[N],up[N];//d1[i]表示从第i点往下走的最长长度,d2表示第二长长度,p表示向下的最长路径经过了哪一点(记录最近的一个点即可),up表示向上走的最长路
void add(int a,int b,int c){
    e[idx]=b,ne[idx]=h[a],w[idx]=c,h[a]=idx++;
}
int dfs_d(int u,int father){//向下搜的话和上题差不多
    d1[u]=d2[u]=0;
    for(int i=h[u];i;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;
            p[u]=j;
        }
        else if(d>=d2[u]) d2[u]=d;
    }
    return d1[u];
}
int dfs_u(int u,int father){//向上走
    for(int i=h[u];i;i=ne[i]){
        int j=e[i];
        if(j==father)   continue;
        //这里当u是1时,up[u]=0,但这并不影响转移,因为一定去掉最长向下路后走拐向第二长一定是最长的
        if(p[u]==j) up[j]=max(d2[u],up[u])+w[i];//如果从u往下走的最长路经过了j,那么j点往上走的最长路就是向上或者拐弯走向第二长向下路的路径
        else up[j]=max(up[u],d1[u])+w[i];//否则只需从拐向最长向下路和向上走选一个
        dfs_u(j,u);
    }
}
int main(){
    int n;
    scanf("%d",&n);
    for(int i=1;i<n;i++){
        int a,b,c;
        scanf("%d%d%d",&a,&b,&c);
        add(a,b,c);
        add(b,a,c);
    }
    dfs_d(1,0);//向下
    dfs_u(1,0);//向上
    int res=d1[1];//注意,本题中1号结点遍历到所有点都相当于向下走了
    for(int i=2;i<=n;i++){//其它点要在向上和向下最长中取个最大值再和res取个最小值
        res=min(res,max(up[i],d1[i]));
    }
    cout<<res<<endl;
    return 0;
}

数字转换

题目链接:数字转换
分析:这道题还是比较有意思的,我们把所有能连的边都预处理出来并连上,然后就需要求最长的一条路径,回到了第一题。需要注意的是,本题可能会出现多棵树的情况,即不一定所有点都连通,因此我们对每个点都搜一遍。
代码实现:

#include<iostream>
using namespace std;
const int N=5e4+10;
int idx=1,h[N],ne[N],e[N],sum[N];
int d1[N],d2[N],res;
bool st[N];
void add(int a,int b){
    e[idx]=b,ne[idx]=h[a],h[a]=idx++;
}
void dfs(int u){
    if(d1[u])   return ;//如果搜过就直接返回最长的
    st[u]=1;//搜过了就标记一下
    for(int i=h[u];i;i=ne[i]){
        int j=e[i];
        dfs(j);
        if(d1[j]+1>d1[u])   d2[u]=d1[u],d1[u]=d1[j]+1;
        else if(d1[j]+1>d2[u])  d2[u]=d1[j]+1;
    }
    res=max(res,d1[u]+d2[u]);
}
int main(){
    int n;cin>>n;
    for(int i=1;i<=n;i++){//先处理出所有数的约数之和
        for(int j=2;j<=n/i;j++){//因为约数不包含自身,所有j从2开始
            sum[i*j]+=i;   
        }
    }
    for(int i=2;i<=n;i++){//加边从2开始,否则会加一条从0到1的边
        if(sum[i]<i){
            add(sum[i],i);//从小的数往大的树连一条边,我们不用反向搜,所以建立一条边即可
        }
    }
    for(int i=1;i<=n;i++){//从最小的点开始搜
        if(!st[i])//如果没搜过
         dfs(i);
    }
    cout<<res;
    return 0;
}

二叉苹果树

题目链接:二叉苹果树
分析:这道题还是比较easy的,感觉到一股01背包味?再品品,感觉到一股区间DP味?或许这就是算法的美妙之处吧(你中有我,我中有你)。
代码实现:

#include<iostream>
using namespace std;
int n,q;
const int N=110,M=2*N;
int dp[N][N];//dp[i][j]表示以i为根节点的子树,保留j根树枝剩余的最大苹果数
int h[N],ne[M],w[M],idx=1,e[M];
void add(int a,int b,int c){
    e[idx]=b,ne[idx]=h[a],w[idx]=c,h[a]=idx++;
}
void dfs(int u,int father){
    for(int i=h[u];i;i=ne[i]){
        if(e[i]==father)   continue;
        dfs(e[i],u);
        for(int j=q;j;j--){//枚举以u为根节点保留的树枝数量
            for(int k=0;k<j;k++){//枚举以该子节点为根节点的子树保留的树枝数量
                dp[u][j]=max(dp[u][j],dp[e[i]][k]+dp[u][j-k-1]+w[i]);
            }
        }
    }
}
int main(){
    cin>>n>>q;
    for(int i=1;i<n;i++){
        int a,b,c;
        cin>>a>>b>>c;
        add(a,b,c);//建双向边
        add(b,a,c);
    }
    dfs(1,0);
    cout<<dp[1][q];
    return 0;
}

战略游戏

题目链接:战略游戏
分析:一股子状态机模型味,就按照那个模型写吧,详见代码。
代码实现:

#include<iostream>
#include<cstring>
using namespace std;
const int N=1510;
int n;
int h[N],e[N],ne[N],idx=1;
int dp[N][2];//dp[i][0]表示在i处不放时的最小值,1时放置
bool vis[N];
void add(int a,int b){
    e[idx]=b,ne[idx]=h[a],h[a]=idx++;
}
void dfs(int u){
    dp[u][0]=0,dp[u][1]=1;
    for(int i=h[u];i;i=ne[i]){
        int j=e[i];
        dfs(j);
        dp[u][0]+=dp[j][1];//若u结点不放置,子节点必须放置
        dp[u][1]+=min(dp[j][0],dp[j][1]);//若u结点放置,子节点可以放可以不放
    }
}
int main(){
    while(scanf("%d",&n)!=-1){
        memset(h,0,sizeof h);
        idx=1;
        memset(vis,0,sizeof vis);
        for(int i=1;i<=n;i++){
            int a,b;
            scanf("%d:(%d)",&a,&b);
            while(b--){
                int c;
                scanf("%d",&c);
                add(a,c);
                vis[c]=true;//不是根节点的标记一下
            }
        }
        int root=0;
        while(vis[root])    root++;//找出根节点
        dfs(root);
        printf("%d\n",min(dp[root][0],dp[root][1]));
    }
    
    return 0;
}

皇宫看守

题目链接:皇宫看守
分析:注意,本题和上题不一样,上题是观察边,本题是观察点,但其实思路都差不多,详见代码。
代码实现:

#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1510;
int n;
int h[N], w[N], e[N], ne[N], idx;
int f[N][3];//f[i][0]表示第i点被它的父节点观察到,f[i][1]表示被它的子节点看住,f[i][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][2] = w[u];
    int sum = 0;
    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]);//如果此节点自己看着,那么它的子节点可以随意
        sum += min(f[j][1], f[j][2]);//sum求f[u][1]时会用,此状态下所有子节点都必须依靠自己或儿子,累加起来
    }
    f[u][1] = 1e9;//对于f[u][1],我们要选一个最好的子节点,因此初始化为很大的数,然后取最小的情况的值
    for (int i = h[u]; ~i; i = ne[i]){//枚举所有子节点,该子节点放置上守卫,则总价钱要减去min(f[j][1], f[j][2])再加上f[j][2]
        int j = e[i];
        f[u][1] = min(f[u][1], sum - min(f[j][1], f[j][2]) + 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;
    return 0;
}

树形DP的题感觉还都是很有意思的,还要在洛谷上多刷刷题!

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

_bxzzy_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值