详解树形DP

 更新时间:2021年05月31日 14:24:33   作者:zhxmdefj  
树形DP是什么?跟其他DP有什么区别?相信很多初学者在刚刚接触一种新思想的时候都会有这种问题。没错,树形DP准确的说是一种DP的思想,将DP建立在树状结构的基础上。所以我们结合具体题目进行讲解,希望大家可以在题目中领悟这种思想。

前言

给定一颗有N个节点的树(一般是无根树,就有N-1条无向边),可以任选一个节点作为根节点

一般以节点从深到浅(子树从小到大)的顺序作为dp阶段顺序

dp的状态表示中,第一维通常是节点编号(节点编号代表了以该节点为根的子树)

对于每个节点x,先递归在它的每个子节点上进行dp,回溯时,从子节点向x进行状态转移

A - Anniversary part

N个员工,编号为1~N

他们之间有从属关系,也就是说他们的关系就像一棵以校长为根的树,父结点就是子结点的直属上司。现在有个宴会,宴会每邀请来一个员工 i 都会增加一定的快乐指数 Ri,但如果某个员工的直属上司来了,那么这个员工就不会来。计算邀请哪些员工可以使快乐指数最大,输出最大的快乐指数

设 dp [x] [0] 表示在 x 为根的子树中邀请部分员工,并且 x 不参加时,快乐指数总和的最大值,此时 x 子节点(直接下属)可以参加也可以不参加 (s表示x子节点)

dp [x] [1] 表示在 x 为根的子树中邀请部分员工,并且 x 参加时,快乐指数总和的最大值,此时 x 子节点(直接下属)都不能参加,H[x] 表示当前节点(x)的快乐指数 (s表示x子节点)

这个题给的是有根树,就可以从根节点开始,dp目标就是 max(F[root,0] , F[root,1]) 时间复杂度ON

#include <cstdio>
#include <iostream>
#include <cstring>
#include <algorithm>
#include <vector>
using namespace std;

vector<int> son[10010];
int dp[10010][2];//0不参加,1参加
int v[10010];//记录有没有父节点
int h[10010];//记录快乐指数
int n;

void DFS(int x){
    dp[x][0] = 0;
    dp[x][1] = h[x];
    for (int i = 0; i < son[x].size(); i++)
    {
        int y = son[x][i];
        DP(y);
        dp[x][0] += max(dp[y][0],dp[y][1]);
        dp[x][1] += dp[y][0];
    }
}

int main(){
    cin>>n;
    for (int i = 1; i <=n ; i++)
        scanf("%d",&h[i]);
    for (int i = 1; i <n ; i++)
    {
        int x,y;
        cin>>x>>y;
        v[x] = 1;//x有爸爸
        son[y].push_back(x);//x是y的子节点
    }
    int root;
    for (int i = 1; i <= n; i++)
        if(!v[i]){ //i没有爸爸
            root = i;
            break;
        }
    DFS(root);
    cout << max(dp[root][0],dp[root][1]) << endl;
    return 0;
}

B - Strategic game

有n个点,在某些点上放置哨兵,每个哨兵可以监控和它有边相连的点,问监视所有的点需要的最少的哨兵数

也就是说一颗n个结点的树,要求选出其中的一些顶点,使得对于树中的每条边(u, v),u和v至少有一个被选中,要求给出选中顶点数最少的方案

设 dp [x] [0] 表示在不选择节点 x 的情况下,以 x 为根节点的子树,最少需要选择的节点数

​当i为叶子节点时

​当i不为叶子节点时 (s表示x子节点)

dp [x] [1] 表示在选择节点 x 的情况下,以 x 为根节点的子树,最少需要选择的节点数
​当i为叶子节点时

​当i不为叶子节点时 (s表示x子节点)

#include <iostream>
#include <cstring>
#include <cstdio>
#include <cmath>
#define maxn 1508
using namespace std;

int dp[maxn][2];
int soncnt[maxn];
int parent[maxn];
int n;

void DFS(int x) {
    int i, d1=0, d0=0;
    if (soncnt[x] == 0) {
        dp[x][0] = 0;
        dp[x][1] = 1;
        return;
    }
    for (i=0; i < n; i++) {
        if (parent[i] == x) {
            DFS(i);
            d1 += min(dp[i][0], dp[i][1]);
            d0 += dp[i][1];
        }
    }
    dp[x][1] = d1 + 1;
    dp[x][0] = d0;
}

int main() {
    int dad, son, m;
    while (cin >> n) {
        memset(soncnt, 0, sizeof(soncnt));
        memset(parent, -1, sizeof(parent));
        int root = -1;
        for (int i = 0; i < n; i++) {
            scanf("%d:(%d)", &dad, &m);
            soncnt[dad] = m;
            if (root == -1) {
                root = dad;
            }
            while (m--) {
                scanf("%d", &son);
                parent[son] = dad;
            }
        }
        DFS(root);
        cout << min(dp[root][0], dp[root][1]) << endl;
    }
    return 0;
}

C - Tree Cutting

给一颗n个结点的树,节点编号为1~n

问:删除哪些结点后,剩余各个子树的大小均小于原总结点数的一半

拆除一个节点后,剩余部分为其若干儿子的子树以及该节点上层所连其余部分(n-size[i]),只要这些连接块大小都不超过n/2,该节点就满足条件。因而我们可以先求出每个节点所管辖的那棵树的大小,自下而上地为每个节点求出其子树规模(该点规模=其儿子的规模和+1)。

在建树的时候可以直接用连接表(vector)储存无向边,这时由于无法区分与每个点相连的是其父节点还是子节点,会引发问题:在dfs的时候把父节点误认作儿子节点,解决方法就是在递归的时候传入父节点编号,然后在递归,计算规模,比较大小的时候避开它就可以了

#include<iostream>
#include<cstring>
#include<vector>
#include<cstdio>
#include<algorithm>
using namespace std;
 
int n;
int root;
vector<int>G[10000+400];
vector<int>ans;
int sz[10000+400];
 
void dfs(int par,int u){
    sz[u]=1;
    for(int i=0;i<(int)G[u].size();i++){
        if(G[u][i]!=par)
            dfs(u,G[u][i]);
    }
    int piece=0;
    for(int i=0;i<(int)G[u].size();i++)
        if(par!=G[u][i]){
            sz[u]+=sz[G[u][i]];
            piece=max(piece,sz[G[u][i]]);
        }
    piece=max(piece,n-sz[u]);
    if(piece<=n/2)
        ans.push_back(u);
}
 
int main(){
    while(cin>>n){
        memset(sz,0,sizeof(sz));
        int x,y;
        for(int i=0;i<n-1;i++){
            scanf("%d%d",&x,&y);
            G[x].push_back(y);
            G[y].push_back(x);
        }
        dfs(0,1);
        sort(ans.begin(),ans.end());
        for(int i=0;i<(int)ans.size();i++)
            cout<<ans[i]<<endl;
    }
    return 0;
}

LCA 最近公共祖先

倍增(基于二分的方法)

假如树上的两个点,处于同一深度时,对于往上跳mid步,显然有单调性:

如果它们往上跳mid步是同一个点,则这个点是它们的共同祖先,但不一定是最近公共祖先

那么如果我们可以快速得到每个点往上跳若干步是谁,显然我们可以利用二分来求LCA

这个可以用倍增来做。

倍增的思想其实非常简单,就是利用二进制的思想。一个显然的结论有:

节点u往上跳2^(k+1)的布的祖先 = (节点u往上跳2k布的祖先)往上跳2k布的祖先

用代码表示就是:

fa[u][k + 1] = fa[ fa[u][k] ][k];

这个的预处理也非常简单,我们只需要初始化每个fa[u][0]就可以了就可以了,因为接下来的部分都可以根据上面的公式递推。

然后预处理好这个之后,我们就可以根据这个进行二分了,代码如下:

int fa[maxn][20], dep[maxn];

void dfs(int u, int f, int d)
{
    dep[u] = d;
    fa[u][0] = f;
    for(int i = 1; i < 20; ++i)
    {
        fa[u][i] = fa[fa[u][i -  1]][i] - 1;
    }
    for(int i = head[u]; ~i; i = nxt[i])
    {
        int t = to[i];
        if(t != f)
        {
            dfs(t, u, d + 1);
        }
    }
}

int lca(int u, int v)
{
    if(dep[u] < dep[v]) swap(u, v);
    int k = dep[u] - dep[v];
    for(int i = 0; i < k; ++i)
    {
        if((1 << i) & k) u = fa[u][i];
    }
    if(u == v) return u;
    for(int i = 19; i >= 0; --i)
    {
        if(fa[u][i] != fa[v][i])
        {
            u = fa[u][i];
            v = fa[v][i];
        }
    }
    return fa[u][0];
}

以上就是详解树形DP的详细内容,更多关于树形DP的资料请关注脚本之家其它相关文章!

相关文章

  • 纯C语言:分治快速排序源码分享

    纯C语言:分治快速排序源码分享

    这篇文章主要介绍了分治快速排序源码,有需要的朋友可以参考一下
    2014-01-01
  • C++详解如何通过模板实现元素的反序

    C++详解如何通过模板实现元素的反序

    这篇文章主要介绍了C++中模板(Template)实现元素的反序,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2022-06-06
  • Clion下载安装使用的详细教程(Win+MinGW)

    Clion下载安装使用的详细教程(Win+MinGW)

    这篇文章主要介绍了Clion下载安装使用教程(Win+MinGW),本文通过图文并茂的形式给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2020-08-08
  • C语言实现图的邻接矩阵存储操作

    C语言实现图的邻接矩阵存储操作

    这篇文章主要为大家详细介绍了C语言实现图的邻接矩阵存储操作,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2018-08-08
  • 详析C++中的auto

    详析C++中的auto

    这篇文章主要介绍了详析C++中的auto,auto是具有自动存储器的局部变量,C++11中,标准委员会赋予了auto全新的含义即:auto不再是一个存储类型指示符,而作为一个新的类型指示符来指示编译器,下面来看看文章的详细介绍吧
    2022-01-01
  • 判断整数序列是否为二元查找树的后序遍历结果的解决方法

    判断整数序列是否为二元查找树的后序遍历结果的解决方法

    本篇文章是对判断整数序列是否为二元查找树的后序遍历结果的解决方法进行了详细的分析介绍,需要的朋友参考下
    2013-05-05
  • C++实现航空订票系统课程设计

    C++实现航空订票系统课程设计

    这篇文章主要为大家详细介绍了C++实现航空订票系统课程设计,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2022-03-03
  • C语言编程数据结构带头双向循环链表全面详解

    C语言编程数据结构带头双向循环链表全面详解

    这篇文章主要为大家介绍了C语言编程的数据结构中带头双向循环链表全面详解,有需要的朋友可以借鉴参考下,希望能够有所帮助祝大家多多进步,早日升职加薪
    2021-10-10
  • C语言实现3个数从小到大排序/输出的方法示例

    C语言实现3个数从小到大排序/输出的方法示例

    这篇文章主要给大家介绍了关于如何利用C语言实现3个数从小到大排序/输出的相关资料,文中通过示例代码介绍的非常详细,对大家学习或者使用C语言具有一定的参考学习价值,需要的朋友们下面来一起学习学习吧
    2019-05-05
  • C++ Date类的具体使用(构建,重载等)

    C++ Date类的具体使用(构建,重载等)

    本文主要介绍了C++ Date类的具体使用(构建,重载等),文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2022-07-07

最新评论