树形dp题目讲解

题目链接1

题目思路

显然是一个树形dp,设dp[i][j]为第i棵树下有j个子节点的最大值,再利用背包来更新。注意的细节有点多,还有要逆向枚举。

代码

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int maxn=2e2+5;
int m,n,head[maxn],cnt,dp[maxn][maxn],v[maxn],ans,sz[maxn];
struct node{
    int to,next;
}e[maxn];
void add(int u,int v){
    e[++cnt].to=v;
    e[cnt].next=head[u];
    head[u]=cnt;
}
void dfs(int son,int fa){
    for(int i=head[son];i;i=e[i].next){
        if(e[i].to!=fa){
            sz[e[i].to]=1;
            dfs(e[i].to,son);
            sz[son]+=sz[e[i].to];
            for(int j=min(m,sz[son]);j>=1;j--){//注意要逆向枚举,要不然自己会更新自己
                for(int k=1;k<=min(j,sz[e[i].to])&&(j-k>=1||son==0);k++){//判断条件有点多,要仔细思考
                    dp[son][j]=max(dp[son][j],dp[e[i].to][k]+dp[son][j-k]);
                }
            }
        }
    }
}
void init(){
    ans=0,cnt=0;
    memset(dp,0,sizeof(dp));
    memset(head,0,sizeof(head));
    memset(v,0,sizeof(v));
    memset(e,0,sizeof(e));
    memset(sz,0,sizeof(sz));
}
signed main(){
    while(scanf("%d %d",&n,&m)!=-1&&n){
        init();
        for(int i=1,to;i<=n;i++){
            scanf("%d %d",&to,&v[i]);
            add(to,i);
            dp[i][1]=v[i];//i选一个节点,即是自己
        }
        dfs(0,0);
        printf("%d\n",dp[0][m]);
    }
    return 0;
}

题目链接2

题目大意

给你一棵树,定义了结点距离为任意结点对(i,j)在最短路径上的边的条数。让你求出每个结点的结点距离小于等于K的结点个数,然后求异或值。

题目思路

首先,这是树结构。
然后,我们尝试简化问题。
如果求的,不是对于一个节点,所有距离在[1,K]的节点数,而是限制在子树内的距离在[1,K]的节点数。
那么,这道题,我们直接一个dfs就可以搞定。
就是从叶子节点开始,距离这个节点距离为[1,K]的节点数。
然后f[x][i]=∑f[son][i-1],i∈[1,K]
f[x][0]=1
然而,我们还要求与非子树内的节点,怎么办呢?
我们做完之前的预处理之后,只需要从父节点寻求转移即可。
我们假设,我们已经知道了距离父节点x距离为0~K的所有点的点数。我们现在要向子节点y转移。
显然,f[y][i]+=f[x][i-1]-f[y][i-2],2<=i<=K。
++f[y][1];
意思是,距离父节点x为i-1的节点,转移到节点y的时候,距离就变成了i。
然而, 并非所有的节点都能做转移。y子树内的,距离为y为i-2的节点,距离父节点的距离也是i-1,但是距离y的距离并非为i。
所以我们把这些节点剔除。
一个转移从子节点转移而来,另外一个转移从父节点转移而来。

参考链接:https://blog.csdn.net/snowy_smile/article/details/50214429

代码

#include<cstdio>
#include<cstring>
using namespace std;
const int maxn=5e5+5;
int t,n,k,a,b,cnt,head[maxn],ans,f[maxn][20];
struct node{
    int to,next;
}e[maxn<<1];//双向边
void add(int u,int v){
    e[++cnt].to=v;
    e[cnt].next=head[u];
    head[u]=cnt;
}
void init(){
    cnt=0,ans=0;
    memset(head,0,sizeof(head));
    memset(f,0,sizeof(f));
    memset(e,0,sizeof(e));
}
void dfs1(int son,int fa){
    f[son][0]=1;//初始化,就是自己
    for(int i=head[son];i;i=e[i].next){
        if(e[i].to!=fa){
            dfs1(e[i].to,son);
            for(int j=1;j<=k;j++){
                f[son][j]+=f[e[i].to][j-1];
            }
        }
    }
}
void dfs2(int son,int fa){
    for(int i=head[son];i;i=e[i].next){
        if(e[i].to!=fa){
            for(int j=k;j>=2;j--){//逆向枚举!!!!!!!!!!!!!!
                f[e[i].to][j]+=f[son][j-1]-f[e[i].to][j-2];
            }
            f[e[i].to][1]++;//e[i].to的父亲
            dfs2(e[i].to,son);
        }
    }
    int sum=0;
    for(int i=0;i<=k;i++){//注意从0开始
        sum+=f[son][i];
    }
    ans=ans^sum;
}
int main(){
    scanf("%d",&t);
    while(t--){
        init();
        scanf("%d %d %d %d",&n,&k,&a,&b);
        for(int i=2;i<=n;i++){
            int to=(1ll*a*i+b)%(i-1)+1;//注意ll
            add(i,to),add(to,i);
        }
        dfs1(1,1);//两次dfs
        dfs2(1,1);
        printf("%d\n",ans);
    }
    return 0;
}

题目链接3

题目大意

给出n个节点的一棵树,每条边有权值。现在要求切段其中的一些边,使得任意一个叶子没有走到祖先的路。
要求切断的边的总权值不能超过m。求所有方案中切断的最大的边权的最小值。

题目思路

看到这个要求,显然是二分。

两种情况讨论一下
枚举i的子节点j。
第一种:直接切掉(i, j)这条边,花费是w(i, j)
第二种:子树j中的叶节点已经与j断开,花费是f[j]

代码

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
typedef long long ll;
const int maxn=1e3+5;
const int inf=0x3f3f3f3f;
int n,m,cnt,head[maxn];
ll dp[maxn];//这里要用ll
struct node{
    int to,next,w;
}e[maxn<<1];//双向边
void add(int u,int v,int w){
    e[++cnt].to=v;
    e[cnt].next=head[u];
    e[cnt].w=w;
    head[u]=cnt;
}
void init(){
    cnt=0;
    memset(e,0,sizeof(e));
    memset(head,0,sizeof(head));
}
void dfs(int son,int fa,int ma){
    bool flag=0;
    for(int i=head[son];i;i=e[i].next){
        if(e[i].to!=fa){
            flag=1;
            dfs(e[i].to,son,ma);
            if(e[i].w<=ma){
                dp[son]+=min(dp[e[i].to],1ll*e[i].w);
            }else{
                dp[son]+=dp[e[i].to];
            }
        }
    }
    if(!flag){//表明这是叶子节点
        dp[son]=inf;
    }
}
int main(){
    while(scanf("%d%d",&n,&m)!=-1&&n+m){
        init();
        for(int i=1,u,v,w;i<=n-1;i++){
            scanf("%d %d %d",&u,&v,&w);
            add(u,v,w),add(v,u,w);
        }
        int l=1,r=1000,ans=-1;
        while(l<=r){
            int mid=(l+r)/2;
            memset(dp,0,sizeof(dp));
            dfs(1,1,mid);
            if(dp[1]<=m){
                ans=mid;
                r=mid-1;
            }else{
                l=mid+1;
            }
        }
        printf("%d\n",ans);
    }
    return 0;
}

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值