本周主要学习的是树形DP,比起敲代码,更多的是看他人写的博客,在此对树形DP做一点总结,题目与素材主要来自kuangbin和ZeroClock两位大牛的博客,在此感谢两位的技术支持。每道题后我都自己对其进行了自我分析。

这篇总结只能算是刚看树形DP的简单概括,具体内容今后还会补充。

常规树形DP

Problem 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 T 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

 

题意:话说一个公司的一些然要去参加一个party,每个人有一个愉悦值,而如果某个人的直接上司在场的话会非常扫兴,所以避免这样的安排,问给出n个人,每个人的愉悦值以及他们的上司所属关系,问你让那些人去可以让总的愉悦值最大,并求出这个值。

 

分析:树形dp入门题目,这个公司的人事关系可以根据给出的数据得到一个树,最上面的是最高层,往下依次,我们要做的就是在树的节点处进行dp。

用dp【i】【0】表示当前i这个人不选,dp【i】【1】表示当前i这个人安排去参加。

那么dp【st】【1】+=dp【i】【0】  ///因为当前这个要选,那么他的前一个一定不能选,否则不满足题目要求

 而  dp【st】【0】+=max(dp【i】【0】,dp【i】【1】)  而在树中 st 是 i 的父节点。

法一:
Dfs+dp(超时)
1. #include <iostream>
2. #include <cstdio>
3. #include <cstdlib>
4. #include <cstring>
5. #include <algorithm>
6. #include <queue>
7. #include <vector>
8. using namespace std;  
9. #define Del(a,b) memset(a,b,sizeof(a))
10. const int N = 10000;  
11. int dp[N][3];  ///dp[i][0]表示当前i点不选 1表示选
12. int father[N],vis[N];  
13. int n;  
14. void creat(int o)  
15. {  
16.     vis[o]=1;  
17.     for(int i=1;i<=n;i++)  //超时原因,每次都循环n次,//时间复杂度o(n^2)
18.     {  
19.         if(vis[i]==0 && father[i]==o)  
20.         {  
21.             creat(i);  
22.             dp[o][0]+=max(dp[i][0],dp[i][1]);  
23.             dp[o][1]+=dp[i][0];  
24.         }  
25.     }  
26. }  
27.   
28. int main()  
29. {  
30.     int i;  
31.   
32.     while(~scanf("%d",&n))  
33.     {  
34.         Del(dp,0);Del(father,0);  
35.         Del(vis,0);  
36.         for(i=1; i<=n; i++)  
37.         {  
38.             scanf("%d",&dp[i][1]);  
39.         }  
40.         int f,c,root;  
41.         root = 0;//记录父结点
42.         bool beg = 1;  
43.         while (scanf("%d %d",&c,&f),c||f)  
44.         {  
45.             father[c] = f;  
46.             if( root == c || beg )  
47.             {  
48.                 root = f;  
49.             }  
50.         }  
51.         while(father[root])//查找父结点
52.             root=father[root];  
53.         creat(root);  
54.         int imax=max(dp[root][0],dp[root][1]);  
55.         printf("%d\n",imax);  
56.     }  
57.     return 0;  
58.   
59. }  
优化:
1:STL中vector实现链表
#include<stdio.h>
#include<string.h>
#include<iostream>
#include<vector>
#include<algorithm>using namespace std;
const int MAXN=6050;
vector<int>vec[MAXN];int f[MAXN];int hap[MAXN];
int dp[MAXN][2];
void dfs(int root)
{
    int len=vec[root].size();
    dp[root][1]=hap[root];
    for(int i=0;i<len;i++)
       dfs(vec[root][i]);//记录了每个节点的子节点,时间复杂度/o(nlogn)
    for(int i=0;i<len;i++)
    {
        dp[root][0]+=max(dp[vec[root][i]][1],dp[vec[root][i]][0]);
        dp[root][1]+=dp[vec[root][i]][0];
    }
}int main()
{
    //freopen("in.txt","r",stdin);
    //freopen("out.txt","w",stdout);
    int n;
    int a,b;
    while(scanf("%d",&n)!=EOF)
    {
        for(int i=1;i<=n;i++)
        {
            scanf("%d",&hap[i]);
            vec[i].clear();
            f[i]=-1;//树根标记
            dp[i][0]=dp[i][1]=0;
        }
        while(scanf("%d%d",&a,&b))
        {
            if(a==0&&b==0)break;
            f[a]=b;
            vec[b].push_back(a);
        }
        a=1;
        while(f[a]!=-1)a=f[a];//找到树根       
 dfs(a);
        printf("%d\n",max(dp[a][1],dp[a][0]));
 
 
    }
    return 0;
}
法二:
用结构体实现链表//不懂。。。。。。。。->略懂。。。
#include<stdio.h>
#include<string.h>
#include<algorithm>using namespace std;
const int MAXN=6010;
struct Node
{
    int v;
    Node *next;
};
Node *head[MAXN];//头指针
Node edge[MAXN*2];//这个要大一点
int tol;//边的总数,也就是edge数组
int dp[MAXN][2];
int hap[MAXN];
bool vis[MAXN];
void init()
{
    tol=0;
    memset(dp,0,sizeof(dp));
    memset(head,NULL,sizeof(head));//初始化头指针
    memset(vis,false,sizeof(vis));
}
void add_edge(int a,int b)//加一条a与b的无向边{
    edge[tol].v=b;
    edge[tol].next=head[a];
    head[a]=&edge[tol++];
 
    edge[tol].v=a;
    edge[tol].next=head[b];
    head[b]=&edge[tol++];
}
void dfs(int v)
{
    if(vis[v])return;
    vis[v]=true;
    Node *p=head[v];
    dp[v][1]=hap[v];
    while(p!=NULL)
    {
        if(!vis[p->v])
        {
            dfs(p->v);
            dp[v][0]+=max(dp[p->v][0],dp[p->v][1]);
            dp[v][1]+=dp[p->v][0];
        }
        p=p->next;
    }
}
int main()
{
   // freopen("in.txt","r",stdin);
   // freopen("out.txt","w",stdout);
    int n,a,b;
    while(scanf("%d",&n)!=EOF)
    {
        init();
        for(int i=1;i<=n;i++)
           scanf("%d",&hap[i]);
        while(scanf("%d%d",&a,&b))
        {
            if(a==0&&b==0)break;
            add_edge(a,b);
        }
        //由于建的是无向图,可以任意找个点当树根进行DP
        //但是在搜索中要判重,加个vis数组
        dfs(1);
        printf("%d\n",max(dp[1][0],dp[1][1]));
    }
    return 0;
}

题目链接:http://acm.hdu.edu.cn/showproblem.php?pid=2196

 

题目大意:给一棵树,每条树边都有权值,问从每个顶点出发,经过的路径权值之和最大为多少?每条树边都只能走一次,n <= 10000,权值<=10^9

 

解题思路:比较复杂的树形DP,刚看到题目一点思路都没有,但一想金华赛区的那道B题,似乎有点相似,那题是先深搜一次看每个节点拥有多少子孙节点和子孙节点的权值和。这题要记录的信息比较多,状态转移也比较复杂。设定1为树根(其实每个节点当根都是一样的),这样树变成了有向树,每个节点都有若干个子分支,用一次深搜从叶子节点往上更新算出每个分支到叶子节点的最大权值和。此时得出了根节点到其他分支的最大权值和。得出上面的信息之后,怎么得到答案呢?其他节点的计算类似于根节点。从根节点的孩子出发,先找到根节点除当前分支(根->当前节点为一个分支)外其他分支的最大权值和,然后往上更新选择的这条分支权值和,也就是父亲节点找到的其他分支最大权值和+树边的权值,见Tree_DP()的第二个While循环。不断递归重复上述过程即可得解。

状态转移方程:  dp[i] = max(dp[i],tree[i][j].sum)(j为i的子节点,这个tree[i][j].len是分支的权值和)

 

测试数据:

2  //两个点
 1 2//第二个点直接连到第一个点,边的权值为2

 3
 1 2
 1 3

 5
 1 1
 2 1
 3 1
 1 1//用结构体指针构造链表
#include<iostream>
#include<cstring>
#include<cstdio>
#include<algorithm>
#include<cmath>
#define N 10005
using namespace std;
struct node{
    int v,len;
    long long sum;
    node* next;
}*head[N],tree[N*2];
bool vis[N];
long long dp[N];
int total;
void init(){
    total=0;
    memset(head,NULL,sizeof(head));
    memset(dp,0,sizeof(dp));
    memset(tree,NULL,sizeof(tree));
    memset(vis,0,sizeof(vis));
}
void add(int i,int a,int b){
    tree[total].v=i;
    tree[total].len=b;
    tree[total].next=head[a];
    head[a]=&tree[total++];
    tree[total].v=a;
    tree[total].len=b;
    tree[total].next=head[i];
    head[i]=&tree[total++];
}
void dfs(int v){
    //if(vis[v])return ;
    vis[v]=1;
    node *p=head[v];
    while(p!=NULL){
        if(!vis[p->v]){
            dfs(p->v);
            dp[v]=max(dp[v],dp[p->v]+p->len);
            p->sum=dp[p->v]+p->len;
        }
        p=p->next;
    }
}
void tree_dfs(int pa,int son){
    if(vis[son])return;
    vis[son]=1;
    node *p=head[pa];
    long long mmax=0;
    while(p!=NULL){
        if(p->v!=son){
            mmax=max(mmax,p->sum);
        }
        p=p->next;
    }
    p=head[son];
    while(p!=NULL){
        if(p->v==pa){
            p->sum=mmax+p->len;
            break;
        }
        p=p->next;
    }
再次赋值,因为p已变化
    while(p!=NULL){
        tree_dfs(son,p->v);
        dp[son]=max(dp[son],p->sum);
        p=p->next;
    }
}
int main(){
    int n;
    while(cin>>n&&n!=EOF){
        init();
        int a,b;
从2开始
            cin>>a>>b;
            add(i,a,b);
        }
        dfs(1);
        node* p=head[1];
        memset(vis,0,sizeof(vis));
        while(p!=NULL){
            tree_dfs(1,p->v);
            p=p->next;
        }
        for(int i=1;i<=n;i++)
            cout<<dp[i]<<endl;
    }
    return 0;
}

自我分析:

本题方法:

先看好,这是树!因为每个新电脑只能与一台旧电脑相连,不可能出现环。

由于求的是每个点到另一点的最大权值,直接硬怼,每个点都dfs一次,重复性太高,会超时。所以,先任取一点作为有向树的根(这里假设为1),对其进行dfs,用node中的sum储存每个节点到另一节点的最大权值。然后从根开始与其子节点进行dp(1:这个是爹,*head[1]->v:这个是儿子),先求出除子节点以外最大的权值,再将根与子节点间的权值相加,之后以子节点(*head[1]->v)为新的父节点,与其子节点(*head[1]->next)做同样的事情,进行dfs,遍历所有点,求出结果,用dp存储,dp[i]代表i点到另一点的最大权值。

Tips:

1,在函数dfs中,dp[v]与p->sum结果相同函数dfs的目的:1、算出第一个点到另一点的最大权值;2、算出每个点到其子孙节点的最大权值(因为dfs是有方向的)。

 

2,函数tree_dfs中,用p->sum做运算,因为dp最终表达的为结果,p->sum为可变的。

 

3,将数据弄成无向边,因为每个点都要做tree_dfs。

 

目前已解锁的套路:

1,结构体建立链表的方法:
Int total=0;
Struct node{
Int n;//放数据的
Node *next;//放下一个点的
}*head[N],edge[N*2];
Void add(int a,int b){
Edge[total].n=a;
edge[total].next=head[b];
Head[b]=&edge[total++];
Edge[total].n=b;
edge[total].next=head[a];
Head[a]=&edge[total++];
}
2,循环的套路:
Node* p=head[1];//设1为起始
While(p!=NULL){
If(dp/dfs条件){
Dfs(p->v);
一些不可描述的事情;
一些不可描述的事情;
一些不可描述的事情;
状态转移方程;
}
P=p->next;
}

 

Pre:树的分治

题目:http://poj.org/problem?id=1655

 

题意:给定一棵树,求树的重心的编号以及重心删除后得到的最大子树的节点个数size,如果size相同就选取编号最小的.

 

分析:首先要知道什么是树的重心,树的重心定义为:找到一个点,其所有的子树中最大的子树节点数最少,那么这个点就是这棵树的重心,删去重

心后,生成的多棵树尽可能平衡.  实际上树的重心在树的点分治中有重要的作用, 可以避免N^2的极端复杂度(从退化链的一端出发),保证

NlogN的复杂度, 利用树型dp可以很好地求树的重心.

#include<iostream>
#include<cstring>
#include<cstdio>
#include<algorithm>
#define N 20005
#define INF 1<<30
using namespace std;
bool vis[N];
int ans,tot,size,n;
int dp[N];
struct node{
    int v;
    node *next;
}*head[N],edge[N*2];
void init(){
    tot=0;
    //ans=INF;
    size=INF;
    memset(dp,0,sizeof(dp));
    memset(vis,0,sizeof(vis));
    memset(head,NULL,sizeof(head));
}
void addtree(int a,int b){
    edge[tot].v=b;
    edge[tot].next=head[a];
    head[a]=&edge[tot++];
}
void dfs(int v){
    vis[v]=1;
    node *p=head[v];
每次dfs都要更新
    while(p!=NULL){
        if(!vis[p->v]){
            dfs(p->v);
记录根的最大子树点数
记录每个节点的最大子树的点数
        }
        p=p->next;
    }
    tmp=max(tmp,n-dp[v]-1);
相同时,哪个接近根,哪个是重心
        size=tmp;
        ans=v;
    }
}
int main(){
    int t;
    cin>>t;
    while(t--){
        init();
        cin>>n;
        int a,b;
        for(int i=2;i<=n;i++){
            cin>>a>>b;
            addtree(a,b);
            addtree(b,a);
        }
        dfs(1);
        cout<<ans<<" "<<size<<endl;
    }
    return 0;
}

思路很简单,但将局部变量 tmp定义为全局变量。呃。。。。。虽然更新了它的数据,但传过来的时候tmp=max(tmp,dp[p->v]+1),tmp已不再是0,神坑。。。。

状态转移方程

dp[v]+=dp[p->v]+1;

树的分治:

 


 

题目链接:http://poj.org/problem?id=1741

 

测试数据:

5 4
1 2 3
1 3 1
1 4 2
3 5 1
0 0

 

 

具体代码:

///终于懂了QAQ
#include<iostream>
#include<cstring>
#include<cstdio>
#include<algorithm>
#include<cmath>
#define N 21000
#define INF 2147483647
using namespace std;
struct node{
    int v,len;
    int sum,mark;
    node *next;
}*head[N],edge[N],tmp[N];
int dp[N],dis[N];
int size[N],pos[N];
bool vis[N];
int tot,cnt,k,n,ans,root;
void init(){
    cnt=ans=0;
    for (int i = 0; i < N; ++i)
        vis[i] = 0,head[i] = NULL;
}
void addtree(int a,int b,int c){
    edge[cnt].v=b;
    edge[cnt].len=c;
    edge[cnt].next=head[a];
    head[a]=&edge[cnt++];
}
void dfs(int v){///计算出各个点与子孙节点的权值
    tmp[v].sum=0;tmp[v].mark=0;
    node *p=head[v];
    while(p!=NULL){
第二个条件为了防止只有一个点时的误操作
            dfs(p->v);
            tmp[v].sum+=tmp[p->v].sum;
            tmp[v].mark=max(tmp[v].mark,tmp[p->v].sum);
        }
        p=p->next;
    }
    tmp[v].sum++;
    pos[tot]=v;
    size[tot++]=tmp[v].mark;
}
int getroot(int v){
    tot=0;
    dfs(v);
    int i,mmax,ssum=INF;
    int b=tmp[v].mark;
    for(i=0;i<tot;i++){
与以v为根的另外子树点数比较,不要漏下!
        if(ssum>size[i]){
            mmax=pos[i];
            ssum=size[i];
        }
    }
    return mmax;
}
上面是找重心的
void getlength(int v,int d){///求每个点到当前根节点的距离
    node *p=head[v];
    dis[tot++]=d;
    while(p!=NULL){
        if(!vis[p->v]&&(d+p->len<=k)&&p->v!=v)
            getlength(p->v,d+p->len);
        p=p->next;
    }
}
void count1(int v){
    sort(dis,dis+tot);
    int left=0,right=tot-1;
    while(left<right){
        if(dis[left]+dis[right]<=k){
            ans+=right-left;
            left++;
        }else
            right--;
    }
}
void count2(int v){///ans+=count1-count2
    vis[v]=1;
    node *p=head[v];
    while(p!=NULL){
        if(!vis[p->v]){
            tot=0;
            getlength(p->v,p->len);
            sort(dis,dis+tot);
            int left=0,right=tot-1;
            while(left<right){
                if(dis[left]+dis[right]<=k){
                    ans-=right-left;
                    left++;
                }else
                    right--;
            }
        }
        p=p->next;
    }
}
void solve(int v){///在此分治,找每个子树的重心
    root=getroot(v);
    tot=0;
    getlength(root,0);
    count1(root);
    count2(root);
    node *p=head[root];
    while(p!=NULL){
        if(!vis[p->v]&&v!=p->v){
            solve(p->v);
        }
        p=p->next;
    }
}
int main(){
    while(scanf("%d%d", &n, &k),n + k){
        init();
        for(int i=1;i<n;i++){
            int a,b,c;
            scanf("%d%d%d",&a,&b,&c);
            addtree(a,b,c);
            addtree(b,a,c);
        }
        solve(1);
        printf("%d\n",ans);
    }
    return 0;
}为何RE;黑人问号.jpg

 

具体思路:

1,用dfs算出每个节点到当前根的权值,第i点到根的距离记作dis[i],所要求的就是dis[i]+dis[j]<k的值。但如果i,j两点在同一子树下,就会重复计算,而且不合逻辑(i到根的距离j到根的距离和小于k,按判断条件正确,但这并不是i到j的距离小于k),所以在以后的递归中要将多余的删去。

2,有了dis后,在判断是否满足条件时,如果用两个for循环会超时,所以,用到了二分法来判断,只有一个循环(涨姿势了)。

3,用到了树的分治,如果直接dfs会超时,所以要用到分治,在漆子超的论文中有详细证明。

tips;

1,两组数组做加减等条件判断时,运用二分找,省时。

2,目前只了解了分治的想法与其作用,但具体问题完全懵B,今后再补。

 

 

树形dp+背包

在树上进行(分组)背包背包问题。要点有三个:预处理,初始化与状态转移方程。

状态转移方程,多与背包的方程相仿。Etc:dp[1]=cost[i];dp[i]=max/min(dp[i],dp[i-k]+cost[j])

每个子树为一组背包。

for(i=m--------1)
for(j=0--------i)

条件+方程;

初始化与预处理,每一种题目类型不同。

活用临时变量tot,通过其自增在计算关于点与边数目的数据。

具体题目可以参考ZeroClock的博客,他树形DP+背包有一个专题。

 

目前对树形DP有了大概的了解。下一步,就是通过刷题来巩固了。

以上。