dp专题3--树形dp入门

一、

树形dp顾名思义是是建立在树上的动态规划。所以实现树形dp的第一步是建树。下面是两种比较常见的建树方法。

1、用vector来建树,优点是比较简单直观,方便调试,缺点是运行时间较长,有些题目会TLE(如 POJ - 3107 Godfather)。

vector<int> e[maxn];
for(int i=0;i<m;i++)
{
    int x,y;
    scanf("%d%d",&x,&y)
    e[y].push_back(x);
}
void dfs(int u)
{
    if(e[u].size()==0) return;
    int l=e[u].size();
    for(int i=0;i<l;i++)
    {
        dfs(e[u][i]);
        //具体操作
    }
}

2、用邻接表来建树,优点是运行时间较短,缺点是较为不直观,调试不方便(大神别打我..)。

以POJ - 3107 Godfather这题的样例为例,题目的树是一颗无向树,所以我们要把每条边的端点正着加进去,然后反着再加进去(例如下图左上角的1 2,我们先add(1,2),然后再add(2,1)),把所有边加进数组后,head数组与edge数组如图:


如果是有向树,则将每条边按方向加进去即可(如下图左上角的1 2,我们只要add(1,2)),把所有边加进数组后,head数组与edge数组如图:


struct edge
{
    int v,next;
} edge[maxn<<1];
int head[maxn],cnt;
void init()
{
    memset(head,-1,sizeof(head));
    cnt=0;
}
void add(int u,int v)
{
    edge[cnt].v=v;
    edge[cnt].next=head[u];
    head[u]=cnt++;
}
for(int i=1; i<n; i++)
{
    int u,v;
    scanf("%d%d",&u,&v);
    add(u,v);
    add(v,u);
}
void dfs(int u,int fa)
{
    for(int i=head[u];i!=-1;i=edge[i].next)
    {
        int v=edge[i].v;
        if(v==fa) continue;
        dfs(v,u);
        //具体操作
    }
    //具体操作
}


二、树形dp的状态一般与节点有关(废话..),它的转移方程一般有两个方向:根节点->叶子节点、叶子节点->根节点

       其中叶子节点->根节点一般是在dfs的回溯时更新父亲节点的值(如POJ - 2342 Anniversary party),而根节点->叶子节点则比较复杂,先dfs用子节点处理用父节点,再一次dfs用父节点处理子节点(如HDU - 2196 Computer)。


三、具体题目

1、点击打开链接

      

Anniversary party
Time Limit: 1000MS Memory Limit: 65536K
Total Submissions: 8906 Accepted: 5122

Description

There is going to be a party to celebrate the 80-th Anniversary of the Ural State University. The University has a hierarchical structure of employees. It means that the supervisor relation forms a tree rooted at the rector V. E. Tretyakov. In order to make the party funny for every one, the rector does not want both an employee and his or her immediate supervisor to be present. The personnel office has evaluated conviviality of each employee, so everyone has some number (rating) attached to him or her. Your task is to make a list of guests with the maximal possible sum of guests' conviviality ratings.

Input

Employees are numbered from 1 to N. A first line of input contains a number N. 1 <= N <= 6 000. Each of the subsequent N lines contains the conviviality rating of the corresponding employee. Conviviality rating is an integer number in a range from -128 to 127. After that go N – 1 lines that describe a supervisor relation tree. Each line of the tree specification has the form: 
L K 
It means that the K-th employee is an immediate supervisor of the L-th employee. Input is ended with the line 
0 0 

Output

Output should contain the maximal sum of guests' ratings.

Sample Input

7
1
1
1
1
1
1
1
1 3
2 3
6 4
7 4
4 5
3 5
0 0

Sample Output

5

题意:每个节点都有一个权值,问选择哪些点使得权值和最大且被选择的点之间没有父子关系,输出最大权值和。

分析:树形dp的裸题,对于每个节点我们可以选择选or不选,所以我们用dp[i][0/1]表示第i个节点选or不选的最大权值和。因为每个节点选or不选影响的只有它的儿子节点或父亲节点,所以,我们可以写出状态转移方程为:

       dp[u][1]=sum(dp[v][0])
       dp[u][0]=sum(max(dp[v][1],dp[v][0]))    (v表示u的儿子节点)

#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <iostream>
#include <cmath>
#include <algorithm>
#include <vector>
#include <set>
#include <queue>
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
const int maxn=6e3+11;
const int inf=0x7fffffff;
int n;
int p[maxn],dp[maxn][2];
vector<int> e[maxn];

void dfs(int u)
{
    if(e[u].size()==0) return;
    int l=e[u].size();
    for(int i=0;i<l;i++)
    {
        dfs(e[u][i]);
        dp[u][0]+=(max(dp[e[u][i]][1],dp[e[u][i]][0]));
        dp[u][1]+=(dp[e[u][i]][0]);
    }
}
bool belead[maxn];
void work()
{
    memset(belead,0,sizeof(belead));
    memset(dp,0,sizeof(dp));
    for(int i=1;i<=n;i++)
    {
        scanf("%d",&p[i]);
        dp[i][1]=p[i];
    }
    int x,y;
    while(scanf("%d%d",&x,&y))
    {
        if(x==0&&y==0) break;
        e[y].push_back(x);
        belead[x]=1;
    }
    int k=0;
    for(int i=1;i<=n;i++)
    {
        if(belead[i]==0)
        {
            k=i;
            break;
        }
    }
    dfs(k);
//    for(int i=1;i<=n;i++)
//        cout<<dp[i][0]<<" "<<dp[i][1]<<endl;
    int ans=0;
    for(int i=1;i<=n;i++)
    {
        ans=max(ans,max(dp[i][1],dp[i][0]));
    }
    printf("%d\n",ans);
}

int main()
{
#ifdef LOCAL
    freopen("input.in","r",stdin);//dwarf.in / dwarf.out
//    freopen("output.in","w",stdout);
//#else
//    freopen("dwarf.in","r",stdin);
//    freopen("dwarf.out","w",stdout);
#endif // LOCAL
    while(~scanf("%d",&n))
        work();
    return 0;
}

2、 点击打开链接

Godfather
Time Limit: 2000MS Memory Limit: 65536K
Total Submissions: 7244 Accepted: 2547

Description

Last years Chicago was full of gangster fights and strange murders. The chief of the police got really tired of all these crimes, and decided to arrest the mafia leaders.

Unfortunately, the structure of Chicago mafia is rather complicated. There are n persons known to be related to mafia. The police have traced their activity for some time, and know that some of them are communicating with each other. Based on the data collected, the chief of the police suggests that the mafia hierarchy can be represented as a tree. The head of the mafia, Godfather, is the root of the tree, and if some person is represented by a node in the tree, its direct subordinates are represented by the children of that node. For the purpose of conspiracy the gangsters only communicate with their direct subordinates and their direct master.

Unfortunately, though the police know gangsters’ communications, they do not know who is a master in any pair of communicating persons. Thus they only have an undirected tree of communications, and do not know who Godfather is.

Based on the idea that Godfather wants to have the most possible control over mafia, the chief of the police has made a suggestion that Godfather is such a person that after deleting it from the communications tree the size of the largest remaining connected component is as small as possible. Help the police to find all potential Godfathers and they will arrest them.

Input

The first line of the input file contains n — the number of persons suspected to belong to mafia (2 ≤ n ≤ 50 000). Let them be numbered from 1 to n.

The following n − 1 lines contain two integer numbers each. The pair aibi means that the gangster ai has communicated with the gangster bi. It is guaranteed that the gangsters’ communications form a tree.

Output

Print the numbers of all persons that are suspected to be Godfather. The numbers must be printed in the increasing order, separated by spaces.

Sample Input

6
1 2
2 3
2 5
3 4
3 6

Sample Output

2 3
题意:输出无向树的所有重心。 树的重心:树的重心也叫树的质心。对于一棵树n个节点的无根树,找到一个点,使得把树变成以该点为根的有根树时,最大子树的结点树最小。换句话说,删除这个点后最大连通块(一定是树)的结点数最小。

分析:我们要记录以每个节点为根所生成的树的每个子树上的节点数。

观察上图我们可以知道对于某个节点的其中一个子树的节点数x等于所有节点数n减该节点其他子树的总节点数。所以,dp[u]表示第u个节点的子树的最大节点数,我们写出转移方程:dp[u]=max(dp[u],sum[v],n-sum[u])   sum[u]表示第u个节点所有子树的总节点数。

这题的状态和转移方程不难找,但是这题用vector来建树会TLE,所以要用邻接表来建树。

#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <iostream>
#include <cmath>
#include <algorithm>
#include <vector>
using namespace std;
typedef long long ll;
const int maxn=5e4+11;
const int inf=1e9+7;
const int mod=1e9+7;

struct edge
{
    int v,next;
}edge[maxn<<1];
int head[maxn],sum[maxn],ans[maxn];
int cnt,n,minl,num;

void add(int u,int v)
{
    edge[cnt].v=v;
    edge[cnt].next=head[u];
    head[u]=cnt++;
}

void dfs(int u,int fa)
{
    int maxl=0,tot=0;
    sum[u]=1;
    for(int i=head[u];i!=-1;i=edge[i].next)
    {
        int v=edge[i].v;
        if(v==fa) continue;
        dfs(v,u);
        sum[u]+=sum[v];
        maxl=max(sum[v],maxl);
        tot+=sum[v];
    }
    maxl=max(maxl,n-tot-1);
    if(minl>maxl)
    {
        minl=maxl;
        num=0;
        ans[num++]=u;
    }
    else if(minl==maxl)
    {
        ans[num++]=u;
    }
}

void work()
{
    memset(head,-1,sizeof(head));
    cnt=0; minl=inf;
    for(int i=1;i<n;i++)
    {
        int u,v;
        scanf("%d%d",&u,&v);
        add(u,v);
        add(v,u);
    }
    dfs(1,-1);
    sort(ans,ans+num);
    for(int i=0;i<num;i++)
    {
        printf("%d",ans[i]);
        if(i!=num-1) printf(" ");
    }
    printf("\n");
}

int main()
{
#ifdef LOCAL
    freopen("input.in","r",stdin);
//    freopen("output.in","w",stdout);
#endif // LOCAL
    while(~scanf("%d",&n))
        work();
    return 0;
}

3、 点击打开链接

Computer

Time Limit: 1000/1000 MS (Java/Others)    Memory Limit: 32768/32768 K (Java/Others)
Total Submission(s): 7347    Accepted Submission(s): 3638


Problem Description
A school bought the first computer some time ago(so this computer's id is 1). During the recent years the school bought N-1 new computers. Each new computer was connected to one of settled earlier. Managers of school are anxious about slow functioning of the net and want to know the maximum distance Si for which i-th computer needs to send signal (i.e. length of cable to the most distant computer). You need to provide this information. 


Hint: the example input is corresponding to this graph. And from the graph, you can see that the computer 4 is farthest one from 1, so S1 = 3. Computer 4 and 5 are the farthest ones from 2, so S2 = 2. Computer 5 is the farthest one from 3, so S3 = 3. we also get S4 = 4, S5 = 4.
 

Input
Input file contains multiple test cases.In each case there is natural number N (N<=10000) in the first line, followed by (N-1) lines with descriptions of computers. i-th line contains two natural numbers - number of computer, to which i-th computer is connected and length of cable used for connection. Total length of cable does not exceed 10^9. Numbers in lines of input are separated by a space.
 

Output
For each case output N lines. i-th line must contain number Si for i-th computer (1<=i<=N).
 

Sample Input
  
  
5 1 1 2 1 3 1 1 1
 

Sample Output
  
  
3 2 3 4 4

题意: 求树上每个点能到达的最远距离。

分析:在这题中,我们发现父节点无法通过子节点得到能到达该父节点的最远距离,即我们无法从叶子节点向根节点进行dp,所以我们现在思考从根节点向叶子节点进行dp。


通过观察图,我们可以知道对于一个节点来说,离它最远的节点要么是该节点的子节点,要么是该节点的父节点的其中一个子节点(且这个子节点是离该父节点最远的节点或次远的节点)。所以,我们先dfs(1)遍历整棵树,记录以节点u为根的子树中离u最远的距离和次远的距离(注意此时我们并没有求出以u为根的新树中离u最远的距离和次远的距离,此时树的根仍为1)。然后,我们再dfs(1,-1)遍历整棵树,更新以u为根的新树中离u最远的距离和次远的距离,并求出u能到达的最远距离。

#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <iostream>
#include <cmath>
#include <algorithm>
#include <vector>
using namespace std;
typedef long long ll;
const int maxn=1e4+11;
const int inf=1e9+7;
const int mod=1e9+7;

typedef struct node
{
    int v,l;
} node;
vector<node> e[maxn];
int dp[maxn][3];
int n;

void dfs1(int u)
{
    int len=e[u].size();
    if(len==0) return;
    int a=0,b=0;
    for(int i=0; i<len; i++)
    {
        int v=e[u][i].v;
        dfs1(v);
        int tmp=dp[v][0]+e[u][i].l;
        if(a<tmp)
        {
            b=a;
            a=tmp;
        }
        else if(a==tmp)
        {
            b=tmp;
        }
        else if(b<tmp)
        {
            b=tmp;
        }
    }
    dp[u][0]=a;
    dp[u][1]=b;
}

void dfs2(int u,int fa)
{
//    for(int i=1;i<=n;i++) cout<<dp[i][0]<<" "; cout<<endl;
//    for(int i=1;i<=n;i++) cout<<dp[i][1]<<" "; cout<<endl; cout<<endl;
    int len=e[fa].size();
    int w=0;
    for(int i=0; i<len; i++)
    {
        if(e[fa][i].v==u)
        {
            w=e[fa][i].l;
            break;
        }
    }
    int tmp=dp[u][0]+w==dp[fa][0]?(dp[fa][1]+w):(dp[fa][0]+w);
    if(tmp>dp[u][0])
    {
        dp[u][2]=tmp;
        dp[u][1]=max(dp[u][1],dp[u][0]);
        dp[u][0]=tmp;
    }
    else
    {
        dp[u][2]=dp[u][0];
        dp[u][1]=max(dp[u][1],tmp);
    }

    len=e[u].size();
    for(int i=0; i<len; i++)
    {
        int v=e[u][i].v;
        dfs2(v,u);
    }
}

void work()
{
    memset(dp,0,sizeof(dp));
    for(int i=1; i<=n; i++)
        e[i].clear();
    for(int i=2; i<=n; i++)
    {
        int x,y;
        scanf("%d%d",&x,&y);
        node tmp;
        tmp.v=i;
        tmp.l=y;
        e[x].push_back(tmp);
    }
    dfs1(1);
    dfs2(1,0);
    for(int i=1; i<=n; i++)
        printf("%d\n",dp[i][2]);
}

int main()
{
#ifdef LOCAL
    freopen("input.in","r",stdin);
//    freopen("output.in","w",stdout);
#endif // LOCAL
    while(~scanf("%d",&n))
        work();
    return 0;
}

4、点击打开链接

以上都是树形dp的经典入门题,下面的是个人排位的一道题,是第4场的B题《The more, The Better》

The more, The Better

Time Limit: 6000/2000 MS (Java/Others)    Memory Limit: 32768/32768 K (Java/Others)
Total Submission(s): 8409    Accepted Submission(s): 4912


Problem Description
ACboy很喜欢玩一种战略游戏,在一个地图上,有N座城堡,每座城堡都有一定的宝物,在每次游戏中ACboy允许攻克M个城堡并获得里面的宝物。但由于地理位置原因,有些城堡不能直接攻克,要攻克这些城堡必须先攻克其他某一个特定的城堡。你能帮ACboy算出要获得尽量多的宝物应该攻克哪M个城堡吗?
 

Input
每个测试实例首先包括2个整数,N,M.(1 <= M <= N <= 200);在接下来的N行里,每行包括2个整数,a,b. 在第 i 行,a 代表要攻克第 i 个城堡必须先攻克第 a 个城堡,如果 a = 0 则代表可以直接攻克第 i 个城堡。b 代表第 i 个城堡的宝物数量, b >= 0。当N = 0, M = 0输入结束。
 

Output
对于每个测试实例,输出一个整数,代表ACboy攻克M个城堡所获得的最多宝物的数量。
 

Sample Input
  
  
3 2 0 1 0 2 0 3 7 4 2 2 0 1 0 4 2 1 7 1 7 6 2 2 0 0
 

Sample Output
  
  
5 13
题意:给一个树形结构,问最多拿m个城市 ,能获得的最大价值多少,拿下面的一定要先拿上面的。

分析:对于每个节点我们都有选or不选两个选择,所以这是个背包问题,且题目限制,对于节点v,如果我们选了v,则v的父节点u一定被选了。类似于背包问题,我们定义状态为dp[u][m]表示选取节点u及其子树(不包括节点u)上拿m个城市的最大价值。所以转移方程为:

for(int j=m;j>=0;j--)
{
       for(int k=0;k<=j;k++)
      {
            dp[u][j]=max(dp[u][j],dp[u][j-k]+dp[v][k]);
       }
}

#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <iostream>
#include <cmath>
#include <algorithm>
#include <vector>
using namespace std;
typedef long long ll;
const int maxn=211;
const int inf=1e9+7;
const int mod=1e9+7;

int n,m;
vector<int> e[maxn];
int dp[maxn][maxn];
int p[maxn];

void dfs(int u)
{
    int len=e[u].size();
    for(int i=0;i<len;i++)
    {
        int v=e[u][i];
        dfs(v);
        for(int j=m;j>=0;j--)
        {
            for(int k=0;k<=j;k++)
            {
                dp[u][j]=max(dp[u][j],dp[u][j-k]+dp[v][k]);
            }
        }
    }
//    printf("%d:",u);
//    for(int i=0;i<=m;i++) cout<<dp[u][i]<<" ";
//    cout<<endl;
    for(int i=m;i>=0;i--)
    {
        dp[u][i+1]=dp[u][i]+p[u];
    }
}

void work()
{
    for(int i=0;i<=n;i++)
        e[i].clear();
    memset(dp,0,sizeof(dp));
    memset(p,0,sizeof(p));
    for(int i=1;i<=n;i++)
    {
        int x,y;
        scanf("%d%d",&x,&y);
        e[x].push_back(i);
        p[i]=y;
    }
    dfs(0);
//    for(int i=0;i<=n;i++)
//    {
//        for(int j=0;j<=m;j++)
//            cout<<dp[i][j]<<" ";
//        cout<<endl;
//    }
    printf("%d\n",dp[0][m+1]);
}

int main()
{
#ifdef LOCAL
    freopen("input.in","r",stdin);
//    freopen("output.in","w",stdout);
#endif // LOCAL
    while(~scanf("%d%d",&n,&m))
    {
        if(n==0&&m==0) break;
        work();
    }
    return 0;
}

四、感想

学了树形dp后,对树的理解加深了,掌握了建树的基础知识。同时也对dp进一步加深理解。dp一般考虑当前状态与下一状态的转移,而树形dp则状态一般与节点有关,一般考虑父节点与子节点的状态转移,有时要从父节点到子节点,有时要子节点到父节点,但总体来说大概就是这两个方向。



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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值