树形DP初探

声明:

一下学习内容摘自–树型动态规划 JSOI2010冬令营


关于DP:

DP的赘述就不多说了,强调两点:
最优子结构:一个最优化策略的子策略总是最优的。
无后效性:当前决策与过去状态无关。


再加上树:

有n个点,n-1条边的无向图,任意两顶点间可达
无向图中任意两个点间有且只有一条路
一个点至多有一个前趋,但可以有多个后继
无向图中没有环


引例、问题描述

给定一棵树,树的每个结点有一个权值,要求从中选出一些不相邻的点,使选出的结点权值和最大。

引例、问题分析

首先要给这棵树选一个根,明确了父子关系才有动规的顺序。本题没有特殊要求,只要任意选择一个点作根就可以了。

引例、确定状态

用f[i][0]表示不选i时,以i为根子树的最大权值;用f[i][1]表示选择i时,以i为根子树的最大值。

引例、状态转移

f[i][0]=sum(max(f[j][0],f[j][1]))
f[i][1]=sum(f[j][0])+v[i]

引例、两种实现方式

记忆划搜索:易于实现,但可能会爆栈
拓扑排序+动规:实现起来比较麻烦

引例、两种实现方式

实现方式的选择因题而异,对于本题,首先要保证程序不会出错。但一般来说,在保证正确的前提下,记忆划搜索更加易于实现,且在对于复杂的题目,记忆划搜索更加直观,便于思考。

引例、小结

动态规划都需要一个决策序列,而许多题目的树是无序给出的,要做动规,我们要选择好合适的根。
对于大多数树型动态规划问题,都是用一棵子树的根结点编号来作为代表这棵子树的第一维状态,然后再根据需要加维。
因为树的特殊结构,任何两个点只有唯一通路,所以很容易满足无后效性。假如本题给定的是图而不是树,那么显然就无法用动规解决了。


对于此题代码…觉得贴出来实在是亵渎原作者的原意,个人也觉得不应该看代码,而进行自己打,无非就是一个递推套了个DFS嘛。
赠送案例:

输入:
6
1 2 1 2 4 1
1 2
1 3
1 6
2 5
5 4
输出:
8

感觉到了原文中的第二题深深的恶意,所以还是先写第三题吧

例三 问题描述(ural1018)

有一棵苹果树,如果树枝有分叉,一定是分 2 叉(就是说没有只有 1 个儿子的结点)。这棵树共有 N(1<=N<=100) 个结点(叶子点或者树枝分叉点),编号为 1-N, 树根编号一定是 1。
现在这颗树枝条太多了,需要剪枝。但是一些树枝上长有苹果。 给定需要保留的树枝数量P,求出最多能留住多少苹果。

例三、问题分析

这题的权值在边上,这在思考时有些别扭,其实只要把边的权值转移到儿子结点上,问题性质不变。
//其实就是按照树+DP的特性,父节点与子节点的关系联系于状态转移
这样状态就应该容易想到了,dp[i][j]表示以i结点为根的子树保留j个结点所得的最大值。因为根结点没有权值,所以我们要保留p+1个点。
/*******/
//定义:
//dp[i][j]表示以i结点为根的子树保留j个结点所得的最大值。
/*******/
//状态转移:
//对于根节点为 i 保留 j 个节点的子树,枚举左子树保留的节点个数k,
//dp[i][j]=max{dp[i_left][k]+dp[i_right][j-k-1],k [0, j-1] };
//对于边界 dp[i][0] = 0; dp[i][1] = value[i];

具体代码实现:
先按照边输入,然后预处理每个节点为根时当前子树的节点数量。
然后记忆化搜索,对当前的节点 i ,当前保留的节点数 j ,
先判断掉节点是否超过当前节点的范围。
然后对它的子节点枚举子节点数进行DFS。

const int N=1e2+10;
const int M=2e2+10;

struct Edge{
    int val;
    int v;
    int next;
}edge[M];
int head[N],tol,n,p;
int cnt[N];
int w[N];
void init(){
    tol=0;
    memset(head,-1,sizeof(head));
}
void add(int u,int v,int val){
    edge[tol].val=val;
    edge[tol].v=v;
    edge[tol].next=head[u];
    head[u]=tol++;
}

bool vis[N];
int dp[N][N];
int DFS(int u,int num,int pre){
    int v,temp;
    if(num>cnt[u]) return 0;
    if(dp[u][num]!=-1) return dp[u][num];
    int node1=-1,node2=-1;
    for(int i=head[u];~i;i=edge[i].next){
        v = edge[i].v;
        if(v==pre) continue;
        if(node1 == -1)
            node1 = v;
        else
            node2 = v;
    }
    temp = 0;
    for(int i=0;i<num;i++){
        temp = max(DFS(node1,i,u)+DFS(node2,num-1-i,u),temp);
    }
    dp[u][num] = temp + w[u];
    return dp[u][num];
}

void solve()
{
    memset(vis,false,sizeof(vis));
    memset(dp,-1,sizeof(dp));
    for(int i=1;i<=n;i++){
        dp[i][0] = 0;
        dp[i][1] = w[i];
    }
    vis[1] = true;
    printf("%d\n",DFS(1,p+1,1));
}

int DFS1(int u){
    int v,res;
    if(cnt[u]) return cnt[u];
    res = 1;
    for(int i=head[u];~i;i=edge[i].next){
        v = edge[i].v;
        if(vis[v]) continue;
        vis[v] = true;
        w[v] = edge[i].val;
        res += DFS1(v);
    }
    cnt[u] = res;
    return  res;
}

int main(){
    int u,v,val;
    scanf("%d%d",&n,&p);
    init();
    for(int i=1;i<n;i++){
        scanf("%d%d%d",&u,&v,&val);
        add(u,v,val);
        add(v,u,val);
    }
    memset(vis,false,sizeof(vis));
    vis[1] = true;
    memset(cnt,0,sizeof(cnt));
    DFS1(1);
    solve();
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值