树形DP学习笔记

概述

在这里插入图片描述

引自以下文章

通常情况下,树形DP需借助DFS/BFS完成。

应用:

1.统计每个节点的子孙节点个数(包含自身)

显然状态方程为:
d p [ f a ] = ∑ i ∈ s o n s ( x ) d p [ i ] dp[fa]=\sum \limits_{i \in sons(x)}dp[i] dp[fa]=isons(x)dp[i]
其中sons(x)为fa对应的子孙集合

void make_son(int st,int fa=0) {
    vis[st]=1;
    a[st].son=1;
    for(int i=a[st].fir; ~i; i=T[i].next) {
        int to=T[i].to;
        if(!vis[to]) {
            make_son_dep(to,st);
            a[st].son+=a[to].son;
        }
    }
    vis[st]=0;
}
2.换根DP

例题1

这题可以通过DFS/BFS直接模拟,采用图的向前星方式存储树,则复杂度为O(n2)且常数较大。
我们可以先利用上面的方法求出每个节点的子孙节点个数(包含自身)。可以假设一个点(一般是1)为根,对于任意一点x,假设其父节点为y,可以分为两部分讨论(以下 d p 1 [ y ] + d p 2 [ y ] = d p [ y ] dp_1[y]+dp_2[y]=dp[y] dp1[y]+dp2[y]=dp[y]):

1.在x的子孙集合外:为所有子孙外的节点到y的距离之和 加上子孙外的节点的个数乘以x到y的边权(此题均为1,下同),因此:
d p [ x ] = d p 1 [ y ] + ( n − a [ x ] . s o n ) dp[x]=dp_1[y]+(n-a[x].son) dp[x]=dp1[y]+(na[x].son)
其中 d p 1 [ x ] dp_1[x] dp1[x]表示所有子孙外的节点到y的距离之和。
2.在x的子孙集合中:为该集合中的节点到y的距离之和 减去它们的个数乘以x到y的边权,因此:
d p [ x ] = d p 2 [ y ] − a [ x ] . s o n dp[x]=dp_2[y]-a[x].son dp[x]=dp2[y]a[x].son
因此
dp[x]=dp[y]+(n-a[x].son)-a[x].son=dp[y]+n-2 * a[x].son
最后只要计算dp[]中的最小者即可。

但我们也要提前处理dp[1]否则一开始就会出错。不难发现dp[1]即为除1外各个节点的深度之和且a[1].son=n,利用上面的公式计算出dp[0]=dp[1]+a[1].son。
由于后续还要DFS求解,因而这样会导致dp[1]被算两次,不过复杂度仍为O(n),足以解决问题。

#include <cstdio>
#include <algorithm>
#include <queue>
const int M=50001;
using namespace std;
struct graph {
    int next,to;
    graph():next(-1),to(-1) {}
    graph(int n,int t):next(n),to(t) {}
};
vector<graph> T;
struct Node {
    int fir,son,dep;
    Node():fir(-1),son(0),dep(-1) {}
};
Node a[M];
int dis[M];
bool vis[M];
inline void add(int u,int v,int &i) {
    T.push_back(graph(a[u].fir,v));
    a[u].fir=i++;
}

void make_son(int st,int fa=0) {
    vis[st]=1;
    a[st].son=1;
    a[st].dep=a[fa].dep+1;
    for(int i=a[st].fir; ~i; i=T[i].next) {
        int to=T[i].to;
        if(!vis[to]) {
            make_son(to,st);
            a[st].son+=a[to].son;
        }
    }
    vis[st]=0;
}

void dfs(int st,int fa=0) {
    dis[st]=dis[fa]+(a[1].son-a[st].son)-a[st].son;
    vis[st]=1;
    for(int i=a[st].fir; ~i; i=T[i].next) {
        int to=T[i].to;
        if(!vis[to])    dfs(to,tot,st);
    }
    vis[st]=0;
}

int main(){
    int k=0,n;
    scanf("%d",&n);
    for(int i=1; i<n; i++) {
         int u,v;
         scanf("%d%d",&u,&v);
         add(u,v,k);
         add(v,u,k);
    }
    make_son(1);
    for(int i=2; i<=n; i++) {
        dis[1]+=a[i].dep;
        vis[i]=0;
    }
    dis[0]=a[1].son+dis[1];  //为防止一进去就出错,这里先将dis[1]求好,这样dis[1]会被求两遍
    dfs(1);
    int *mini=min_element(dis+1,dis+n+1);
    printf("%d %d",mini-dis,*mini);
    return 0;
}

例题2
这题边和点上都带权,因此需要更新状态方程。
如果理解上一题状态方程是怎么得出的,那此题也就不难完成了。
注意点上也带权,因此son的定义应该为子孙节点(包括自身)的牛的总数。

#include <cstdio>
#include <algorithm>
#include <vector>
const int M=100001; 
typedef long long llong;
using namespace std;
struct Edge {
    int next,to,wei;
    Edge():next(-1),to(-1),wei(-1) {}
    Edge(int n,int t,int w):next(n),to(t),wei(w) {}
};
vector<Edge> T;
struct Node {
    llong len,son;
    int wei;
    Node():len(0),son(0),wei(0) {}
    Node(int d,int s,int w):len(d),son(s),wei(w) {}
};
Node a[M];
int fir[M];
llong dis[M];
bool vis[M];
inline void add(int u,int v,int w,int &i) {
    T.push_back(Edge(fir[u],v,w));
    fir[u]=i++;
}

void preDFS(int now,int fa=0,int path=0) {
    vis[now]=1;
    a[now].son=a[now].wei;
    a[now].len=a[fa].len+path;
    for(int i=fir[now]; ~i; i=T[i].next) {
        int to=T[i].to;
        if(!vis[to]) {
            preDFS(to,now,T[i].wei);
            a[now].son+=(llong)a[to].son;
        }
    }
    vis[now]=0;
}

void dfs(int now,int fa=0,llong path=1) {
    vis[now]=1;
    dis[now]=dis[fa]-path*a[now].son+path*(a[1].son-a[now].son);
    for(int i=fir[now]; ~i; i=T[i].next) {
        int to=T[i].to;
        if(!vis[to])    dfs(to,now,(llong)T[i].wei);
    }
    vis[now]=0;
}

int main(){
    int n,k=0;
    scanf("%d",&n);
    for(int i=1; i<=n; i++) {
        scanf("%d",&a[i].wei);
        fir[i]=-1;
    }
    for(int i=0; i<n-1; i++) {
        int u,v,w;
        scanf("%d%d%d",&u,&v,&w);
        add(u,v,w,k);
        add(v,u,w,k);
    }
    preDFS(1);
    for(int i=2; i<=n; i++) {
        dis[1]+=a[i].len*a[i].wei;
        //printf("%lld ",a[i].son);
    }
    dis[0]=a[1].son+dis[1];
    dfs(1);
    llong minn=*min_element(dis+1,dis+n+1);
    printf("%lld",minn);
    return 0;
}
树的直径

待更

树上“背包”

例题链接
这题是一个“分配子树”问题,即在n-1条边中选m条边(m<n),求解满足某一(些)条件的结果。
考虑某个节点u,假设它共能分配j条边,它的一个直接子节点为v,那么若以v为根,且为v及其子树分配了k条边,那么还要在u到v之间分配1条边,u到其他子节点分配j-k-1条边。
如果不为u分配边,那么它的值不应当被更新。

代码:

#include <cstdio>
#include <algorithm>
#include <vector>
const int M=10001;
using namespace std;
struct edge {
    int next,to,wei;
    edge():next(-1),to(-1),wei(0) {}
    edge(int n,int t,int w):next(n),to(t),wei(w) {}
};
vector<edge> T;
int fir[M],dp[M][M];
int m,son[M];  //有多少子节点
bool vis[M];
inline void add(int u,int v,int w,int &i) {
    T.push_back(edge(fir[u],v,w));
    fir[u]=i++;
}

void preDFS(int u) {
    vis[u]=1;
    for(int i=fir[u]; ~i; i=T[i].next) {
        int to=T[i].to;
        if(!vis[to]) {
            preDFS(to);
            son[u]+=son[to]+1;
        }
    }
    vis[u]=0;
}

void dfs(int u) {
    vis[u]=1;
    for(int i=fir[u]; ~i; i=T[i].next) {
        int to=T[i].to;
        if(!vis[to]) {
            dfs(to);
            //一共可能没有m个
            for(int j=min(m,son[u]); j>0; j--) {
                for(int k=j-1; k>=0; k--) {
                    dp[u][j]=max(dp[u][j],dp[u][j-k-1]+dp[to][k]+T[i].wei);
                    //注意自己也占有一条边
                }
            }
        }
    }
}

int main(){
    int n,k=0;
    scanf("%d%d",&n,&m);
    for(int i=1; i<=n; i++) {
        fir[i]=-1;
    }
    for(int i=0; i<n-1; i++) {
        int u,v,w;
        scanf("%d%d%d",&u,&v,&w);
        add(u,v,w,k);
        add(v,u,w,k);
    }
    preDFS(1);
    dfs(1);
    //for(int i=1; i<=n; i++) {
     //   printf("%d ",son[i]);
    //}
    printf("%d",dp[1][m]);
    return 0;
}

例题2链接
这题和上面的极其相似,但要注意是单向边,而且由于可能有多个入度为0的点,所以应当设一个“虚拟点”0,初始化及求解时均应当从0开始。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值