树形dp的简单应用

简介

树形DP,字面意思,在树结构上的DP,通常根据比较子节点的最优得到父节点的状态或根据父节点预处理后再遍历得到子节点的解。
由于是入门篇,我们暂且先讨论前者。

原理

树一大特点就具有子结构,而DP也是要求最优子结构,并且两者都有相同的基本操作—“递归”,那么可见这两者就可以非常和谐的结合在一起了。
下面先看一个最简单的例子

一.NC15033 小G有一个大树
小G想要把自己家院子里的橘子树搬到家门口(QAQ。。就当小G是大力水手吧)
可是小G是个平衡性灰常灰常差的人,他想找到一个这个橘子树的平衡点。
怎么描述这棵树呢。。。就把它看成由一个个节点构成的树吧。结点数就
代表树重。
输入描述:
多组数据输入输出,
第一行包含一个整数n(3<=n<=1000)代表树的结点的个数
以下n-1行描述(1-n)节点间的连接关系。
输出描述:
输出两个个整数 x,num 分别代表树的平衡点,和删除平衡点后最大子树的结点数(如果结点数相同输出编号小的)。
题目意思就是给定一个任意分布没有给出根节点的树,问在那个点分开时,可以保证两端的节点数尽可能相等,
处理这个问题时,我们只需先用vector双向存边,在从任意一个边开始进行dfs,在dfs过程中找到以该节点断开时的重量分配情况,需要注意的是,为了避免重复访问父节点,我们需要在参数中加入fa,并且手动限制不会回到父节点,保证遍历能顺利结束,在dfs过程中,实际上做到了由浅入深,完成了dp的积累过程全部点的重量减去它的一个方向的重量就是另一个方向,取这两个值中更大的一个,使其最小即为所求。

#include<bits/stdc++.h>
using namespace std;
const int N=1e5+1000;
vector<int> G[N];
int f[N];
int n,a,b;
int maxnode,maxsum=0x3f3f3f3f;
void dfs(int q,int fa)
{f[q]=1;int maxn=0;
    for(int i=0;i<G[q].size();i++)
    {int t=G[q][i];
if(t==fa)continue;
        dfs(t,q);
     f[q]+=f[t];
maxn=max(maxn,f[t]);
    }
    maxn=max(maxn,n-f[q]);  
    if(maxn<maxsum)
    {
        maxnode=q;
        maxsum=maxn;
    }
}
int main()
{
    scanf("%d",&n);
    for(int i=1;i<=n-1;i++)
    {
        scanf("%d %d",&a,&b);
        G[a].push_back(b);
        G[b].push_back(a);
    }
    dfs(1,0);
    cout<<maxnode<<" "<<maxsum<<endl;
}

这就是最简单的树形dp的例子
下面在介绍2种变形
分别是最大独立集,和最小支配集
NC51178最大独立集 没有上司的舞会
题目描述
Ural大学有N名职员,编号为1~N。
他们的关系就像一棵以校长为根的树,父节点就是子节点的直接上司。
每个职员有一个快乐指数,用整数 Hi给出,其中 1≤i≤N。
现在要召开一场周年庆宴会,不过,没有职员愿意和直接上司一起参会。
在满足这个条件的前提下,主办方希望邀请一部分职员参会,使得所有参会职员的快乐指数总和最大,求这个最大值。
输入描述:
第一行一个整数N。
接下来N行,第 i 行表示 i 号职员的快乐指数Hi。
接下来N-1行,每行输入一对整数L, K,表示K是L的直接上司。
最后一行输入0。
输出描述:
输出最大的快乐指数。
这道题和上道题的最大的区别在于,这道题规定了谁是谁的父节点,所以不用双向存边,只需开一个父亲数组记录所有数的情况,保证可以找到唯一的根节点,从根节点开始dfs,
在dfs之前,我们先分析这个问题的本质, 也就是对基于第一层的选择,影响后续的决策过程,我们可以这样定义状态方程,用f[i][0]表示不选择i点时,i点及其子树能选出的最多人数,f[i][1]表示选择i点时,i点及其 子树的最多人数。
之后我们分析发现如果不选择i点本身,那么上一层的结果不受限制所以有• f[i][0] = Σ(max (f[j][0], f[j][1])) ,但是如果选择了i点,那么上一层的状态一定是没有选的状态,即• f[i][1] = Hi+ Σf[j][0] ,
关于边界和结果的处理就是这样
f[i][0] = 0, f[i][1] =Hi--------i是叶子节点
结果为max(f[root][0], f[root][1])
,分析完之后,我们首先要找到唯一的根节点,从头开始搜索,

while(fa[i]!=0)i++;//找父亲节点的位置

可以用简单的while循环寻找,需要注意的是,fa数组的值在读入数据时一并处理,只要当过儿子节点的,都使其值等于1,按序寻找即可找到父节点,

#include<bits/stdc++.h>
using namespace std;
int h[10000],fa[10000];
int dp[10000][2];
vector<int> v[10000];
void dfs(int n)
{
    dp[n][1]=h[n];
for(int i=0;i<v[n].size();i++)
{int w=v[n][i];
dfs(w);
 dp[n][1]+=dp[w][0];
 dp[n][0]+=max(dp[w][0],dp[w][1]);
}
}
int main()
{int n;
cin>>n;
    for(int i=1;i<=n;i++)
    scanf("%d",&h[i]);
    for(int i=1;i<n;i++)
    {
        int x,y;
        scanf("%d%d",&x,&y);
        v[y].push_back(x);
        fa[x]++;
    }int i=1;
while(fa[i]!=0)i++;//找父亲节点的位置
 dfs(i);
    printf("%d\n",max(dp[i][0],dp[i][1]));
    return 0;
return 0;
}

这样就处理了一个局部的最大独立集问题

NC24953CellPhoneNetwork最小支配集

题目描述
Farmer John has decided to give each of his cows a cell phone in hopes to encourage their social interaction. This, however, requires him to set up cell phone towers on his N (1 ≤ N ≤ 10,000) pastures (conveniently numbered 1…N) so they can all communicate.
Exactly N-1 pairs of pastures are adjacent, and for any two pastures A and B (1 ≤ A ≤ N; 1 ≤ B ≤ N; A ≠ B) there is a sequence of adjacent pastures such that A is the first pasture in the sequence and B is the last. Farmer John can only place cell phone towers in the pastures, and each tower has enough range to provide service to the pasture it is on and all pastures adjacent to the pasture with the cell tower.
Help him determine the minimum number of towers he must install to provide cell phone service to each pasture.
输入描述:

  • Line 1: A single integer: N
  • Lines 2…N: Each line specifies a pair of adjacent pastures with two space-separated integers: A and B
    输出描述:
  • Line 1: A single integer indicating the minimum number of towers to install
  • • 本题的大意是给你一棵无向树,问你最少用多少个点可以覆盖掉所有其他的点。 • (一个点被盖,它自己和与它相邻的点都算被覆盖),关于这道题,一个很重要的点就是他是一个无向图,所以每个点被覆盖的可能性存在三种,1被他上面覆盖,2被他儿子覆盖,三被他自己覆盖
  • 我们继续设计状态转移方程
  • • dp[i][0]:选点i,并且以点i为根的子树都被覆盖了。
  • • dp[i][1]:不选点i,i被其儿子覆盖
  • • dp[i][2]:不选点i,被其父亲覆盖(儿子可选可不选)
  • • 确定状态转移方程
  • • dp[i][0]=1+Σmin(dp[u][0],dp[u][1],dp[u][2]) (u是i的儿子)
  • • dp[i][2]=Σ(dp[u][1],dp[u][0])
  • • 对于dp[i][1]的讨论稍微复杂一点——他的所有儿子里面必须有一个取dp[u][1]
  • • 那么:if(i没有子节点)dp[i][1]=INF else dp[i][1]=Σmin(dp[u][0],dp[u][1])+inc
  • • 其中对于inc有:
  • • if(上面式子中的Σmin(dp[u][0],dp[u][1])中包含某个dp[u][0])inc=0; • else inc=min(dp[u][0]-dp[u][1])。
  • 也就是说,因为有多个儿子节点,所以如果儿子中全部都为儿子被儿子覆盖,则会导致情况不成立,所以我们需要用inc来存储差值最小的一个值代替这个选择。
  • ,首先我们任选一个点作为根进行dfs处理在dfs实现的细节上需要注意加入fa参数,控制不会访问父亲节点,在这个问题中如果我们使用邻接矩阵存储边权,那么我们需要进行两个特判,一是如果我是出发的节点那么我本身不可以被父节点覆盖需要设置dp[q][2]=INF;还有一个就是如果该节点只有父节点,那么dp[q][1]=INF;其余情况就按状态方程进行转移

下面试试邻接矩阵的写法

#include <bits/stdc++.h>
using namespace std;
const int INF=0x3f3f3f3f;
vector <int> v[10001];
int dp[10001][3];//0表示该点覆盖 1表示该点儿子覆盖 2表示该点父亲覆盖
void dfs(int q,int fa)
{int tmp=INF;
 int flag=1;
 int hp=0;
   dp[q][0]=1;
 for(int i=0;i<v[q].size();i++)
 {
     int t=v[q][i];
     if(fa==t)continue;
     hp=1;
     dfs(t,q);
     dp[q][0]+=min(dp[t][0],min(dp[t][1],dp[t][2]));
     if(q==1)dp[q][2]=INF;
     else dp[q][2]+=min(dp[t][1],dp[t][0]);
     if(dp[t][0]<=dp[t][1])
     {
         flag=0;
         dp[q][1]+=dp[t][0];
     }     
     else {
          dp[q][1]+=dp[t][1];
         tmp=min(tmp,dp[t][0]-dp[t][1]);
     }
     
 }
 if(flag)dp[q][1]+=tmp;
 if(hp==0)dp[q][1]=INF;
}
int main()
{
    int n;
    cin>>n;
    for(int i=1;i<n;i++)
    {
    int x,y;
        cin>>x>>y;
        v[x].push_back(y);
        v[y].push_back(x);
    }
    dfs(1,0);
    printf("%d\n",min(dp[1][0],min(dp[1][1],dp[1][2])));
}

如果换成链式前项星,那么代码细节会少一点,也就是对于父亲节点的判断手段发生变换,减少2个特判处理

void add(int u,int v)
{
    edge[++tot].to=v;
    edge[tot].nex=head[u];
    head[u]=tot;
}
void dfs(int u)
{
    vis[u]=1;
    dp[u][0]=1;dp[u][1]=dp[u][2]=0;
    int flag=0,res=inf;
    //int len=G[u].size();
    for(int i=head[u];~i;i=edge[i].nex)
    {
        int v=edge[i].to;
        if(!vis[v])
        {
            dfs(v);
            dp[u][2]+=min(dp[v][1],dp[v][0]);
            dp[u][0]+=min(dp[v][1],min(dp[v][0],dp[v][2]));

            if(dp[v][0]<=dp[v][1])
            {
                flag=1;dp[u][1]+=dp[v][0];
            }
            else
            {
                dp[u][1]+=dp[v][1];
                res=min(res,dp[v][0]-dp[v][1]);
            }
        }
    }
    if(!flag) dp[u][1]+=res;


}

这就是最基础的树形dp的问题,及通过树的性质,一层层的进行状态转移,结合dfs用递归,从根出发扫到最后,实际上也就是递推中的从0出发。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值