码迷,mamicode.com
首页 > 其他好文 > 详细

树形dp

时间:2020-02-02 23:21:09      阅读:90      评论:0      收藏:0      [点我收藏+]

标签:cstring   +=   sync   toc   continue   ++   数组   size   双向   

树形dp

树形dp的性质:没有环,dfs不会重复,而且具有明显而又严格的层数关系。

判断一道题是否是树形dp:首先判断数据结构是否是一棵树,然后是否符合动态规划的要求。如果都符合,那么是一道树形dp的问题。我们需要通过下面几个步骤来解题。

建树

建树过程中,我们需要通过数据量和题目的要求,选择合适的树的存储方式。

一般来说,如果节点数小于5000,我们一般可以用邻接矩阵来存储。如果节点数更多,我们一般采用邻接表来存储。(**注意,采用邻接表时,我们需将边开到2*n,因为边是双向**)。如果是二叉树或则多叉树转二叉树,我们可以用一个brother[]child[]来存储。

写出树规方程

通过观察孩子与父亲之间的关系建立方程。通常我们认为,树形DP的写法有两种:

1:根到叶子:不过这种动态规划在实际问题中运用的不多。

2:叶子到根:即根的子节点传递有用的信息给根,之后根得出最优解。

注意:上面两种方法一般来说是不能相互转换的(个别情况是个例),且第二种方法适用的普遍性广泛很多。

入门例题

1:poj 2342

问题描述:对于一棵树,每个节点有自己的权值,取了一个节点其相邻的父节点和子节点就没有价值了。求最大价值。

分析可得状态转移方程:当取 i 节点时:\(dp[i][1]+=dp[j][0]\);当不取 i 节点时:\(dp[[i][0]+=max(dp[j][1],dp[j][0])\);上面的 j 都表示为节点 i 的下属。

show code:

#include<bits/stdc++.h>

using namespace std;
const int maxn=1e4;
int fa[maxn],dp[maxn][maxn],val[maxn];
bool v[maxn];
int n,root,res;             //root为根节点
void Tree_dp(int x)
{
    v[x]=true;
    for(int i=1;i<=n;++i)
    {
        if(!v[i]&&fa[i]==x)      //i是x的子节点
        {
            Tree_dp(i);       //O(n^2)的时间复杂度
            dp[x][1]+=dp[i][0];
            dp[x][0]+=max(dp[i][1],dp[i][0]);
        }   
    }
    res=max(dp[x][0],dp[x][1]);
}

int main()
{
    ios::sync_with_stdio(false);

    cin>>n;
    memset(dp,0,sizeof(dp));
    memset(fa,0,sizeof(fa));
    memset(v,false,sizeof(v));
    for(int i=1;i<=n;++i){
        cin>>val[i];
        dp[i][1]=val[i];
    }
    root=0;
    int a,b;
    while(cin>>a>>b)
    {
        if(a==0&&b==0)  break;
        fa[a]=b;          //a的父节点是b
        root=b;           //先随便更新一下根节点,下面还会更新
    }
    while(fa[root])
        root=fa[root];    //找到这棵树真正的根节点
    Tree_dp(root);
    cout<<res<<endl;

    system("pause");
    return 0;
}

2:POJ 1947

题意:有n个点组成的树,问至少删除多少条边才能获得一颗有p个节点的子树。

我们定义状态dp[i][j]表示以 i 为根节点生成节点数为 j 的最少删的边的个数,所以我们可以得到状态转移方程:
\[ dp[i][j]=min(dp[i][j],dp[i][j-k]+dp[i.son][k]-2) \]

#include<iostream>
#include<cstdlib>
#include<cstring>
#include<algorithm>
#include<vector>

using namespace std;
const int maxn=200;
const int inf=0x3f3f3f3f;
int dp[maxn][maxn];     //dp[i][j]代表以i为根节点,生成节点数为k所删掉的边
int a,b,n,p,root;       //分别代表边的两个端点,边的总数,最后要得到的边的数量,根节点
int fa[maxn];
vector<int> tree[maxn];
void dfs(int x)
{
    int len=tree[x].size();
    for(int i=0;i<len;++i)      //不能从1到Len,因为vector数组下标是从0开始的
    {
        dfs(tree[x][i]);     //递归子节点
        for(int j=p;j>1;j--)    //j=1情况下面已近初始化过了
            for(int k=1;k<j;++k)
                dp[x][j]=min(dp[x][j],dp[x][j-k]+dp[tree[x][i]][k]-2);
    }
}

int main()
{
    ios::sync_with_stdio(false);

    memset(fa,false,sizeof(fa));
    cin>>n>>p;
    for(int i=1;i<n;++i){
        cin>>a>>b;
        tree[a].push_back(b);
        fa[b]=a;
    }
    root=1;
    while(fa[root])     //找根节点
        root=fa[root];
    for(int i=1;i<=n;++i)   //初始化dp
    {
        //以i为根,生成节点数为1的子树所需删掉的根,每个节点都有个父节点+1,根节点也有个虚拟父节点,方便处理
        dp[i][1]=tree[i].size()+1;          //这里也是为什么下面状态转移时要-2的原因了
        for(int j=2;j<=p;++j)
            dp[i][j]=inf;
    }
    dfs(root);
    dp[root][p]--;              //将根节点的虚拟节点删去
    int res=inf;
    for(int i=1;i<=n;++i)
        res=min(res,dp[i][p]);
    cout<<res<<endl;

    system("pause");
    return 0;
}

3.换根树形dp poj-3585

题意:树的每条边有自己的权值,以哪个结点作为源点使得总权值最大(如果一个结点有多个节点,只能取最小的)

1:O(n^2)的做法

直接上状态转移方程:
\[ g[root]=\sum_{son∈son(root)}\left\{ \begin{aligned} v(root,son)\ \ size(son)=1\min(g[son],v)\ \ size(son)>1\\end{aligned} \right. \]
每一个点按照上面方法做一遍树形dp即可。

2:O(n)的做法

由于这是一颗无根树,不同的根会产生不同的答案,故我们只需思考一下如何进行换根。而换根的主要思路就是如何处理根与根的转化。

f[i]表示以 i 为根的子树中,答案的最大值,g[i]表示以 i 为根节点的到子端点的最大流量。所以有状态转移方程:
\[ f[i]\ =\left\{ \begin{aligned} v(i,j)\ \ \ \ \ \ \ size(j)=1\g[j]+min(v(i,j),f[i]-min(g[j],v(i,j)))\ size(j)>1\\end{aligned} \right. \]
show code:

#include<iostream>
#include<cstdlib>
#include<cstring>
#include<vector>

using namespace std;
const int maxn=2e5+10;
int T,n,g[maxn],f[maxn];      //g[i]表示以i为根节点到子节点的最大流量,f[i]表示i到所有结点的流量和
struct node
{
    int to;                  //边的邻接点
    int val;                 //边的权值
    node(){}
    node(int a,int b){
        to=a;
        val=b;
    }
};
vector<node> G[maxn];
void gdfs(int u,int fa)             //自下而上求g[i]
{
    for(int i=0;i<G[u].size();i++)
    {
        node e=G[u][i];             //e为u的一个子节点
        if(e.to==fa)    continue;
        gdfs(e.to,u);   
        if(G[e.to].size()==1)       //特判,子节点只有1个
            g[u]+=e.val;            //自下而上的原因是先dfs在求值
        else
            g[u]+=min(e.val,g[e.to]);
    }
}
void fdfs(int u,int fa)         //自上而下求f[i]
{
    for(int i=0;i<G[u].size();i++)
    {
        node e=G[u][i];
        if(e.to==fa)    continue;
        if(G[u].size()==1)  f[e.to]=g[e.to]+e.val;
        else{
            f[e.to]=g[e.to]+min(e.val,f[u]-min(e.val,g[e.to]));
        }
        fdfs(e.to,u);               //自上而下的原因是先求值在dfs
    }
}

int main()
{
    ios::sync_with_stdio(false);

    cin>>T;
    while(T--)
    {
        cin>>n;
        memset(f,0,sizeof(f));
        memset(g,0,sizeof(g));
        for(int i=1;i<=n;++i)   G[i].clear();
        for(int i=1;i<n;++i){
            int u,v,c;
            cin>>u>>v>>c;
            G[u].push_back(node(v,c));      //注意双向边
            G[v].push_back(node(u,c));
        }
        gdfs(1,0);
        f[1]=g[1];
        fdfs(1,0);
        int ans=-0x3f3f3f3f;
        for(int i=1;i<=n;++i)
            ans=max(ans,f[i]);
        cout<<ans<<endl;
    }

    system("pause");
    return 0;
}

树形dp

标签:cstring   +=   sync   toc   continue   ++   数组   size   双向   

原文地址:https://www.cnblogs.com/StungYep/p/12254143.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!