树状DP个人总结

树状DP是一种适用于树形结构的动态规划方法,它无环、避免重复,并具有明确的层次关系。适用条件包括满足动态规划问题的特征:子问题最优解也是全局最优解,无后效性。典型问题如Anniversary party和Strategic game,通过状态转移方程求解最大值或最小值。树的存储方式通常依据节点数量选择邻接矩阵或邻接表,动态规划方程分为根到叶子和叶子到根两种情况。在解决实际问题时,需要判断数据结构是否为树,并考虑动态规划的状态和决策。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

树状dp就是在树上的动态规划,树状dp的特殊性:无环,dfs不会重复,具有明显严格的层数关系

大神的好文:https://blog.youkuaiyun.com/txl199106/article/details/45373507

大神的好文:https://blog.youkuaiyun.com/txl199106/article/details/45372337

什么时候用树状dp?

  • 判断题目描述的数据结构是否是一棵树,是否满足动态规划的要求。是的话就开始入手,不是的话换方法

         什么题目满足动态规划的要求?

               子问题最优解也是全局最优解,满足无后效性,多阶段决策问题。把问题首先分解为若干个子问题,在每一个子问题做                 出决 策,动态规划就是解决多阶段决策最优解的一种方法。

        阶段:
           将所给问题的过程,按时间或空间(树归中是空间,即层数)特征分解成若干相互联系的阶段,以便按次序去求每阶                    段的解。
       状态:
           各阶段开始时的客观条件叫做状态。
       决策:
           当各段的状态取定以后,就可以做出不同的决定,从而确定下一阶段的状态,这种决定称为决策。 (即孩子节点和父亲节点的关系)
       策略:由开始到终点的全过程中,由每段决策组成的决策序列称为全过程策略,简称策略。
       状态转移方程:
          前一阶段的终点就是后一阶段的起点,前一阶段的决策选择导出了后一阶段的状态,这种关系描述了由k阶段到k+1阶  段 (在树中是孩子节点和父亲节点)状态的演变规律,称为状态转移方程。
       目标函数与最优化概念:
          目标函数是衡量多阶段决策过程优劣的准则。最优化概念是在一定条件下找到一个途径,经过按题目具体性质所确定的运算以后,使全过程的总效益达到最优。

  • 树的存储方式:如果节点数目小于5000,用邻接矩阵存储树,如果更大,则用邻接表来存储,邻接表的存储一般情况下使用vector数组来存储。如果是二叉树或者是需要多叉转二叉,可以用两个一维数组来存储。
  • 写出状态dp方程:两种:a.根到叶子 b.叶子到根,根的子节点传递有用的信息给根,得出最优解的一个过程。

树的特点及性质:

  1. 有n个点,n-1条边的无向图,任意两个顶点之间可达。
  2. 无向图中任意两个点间有且只有一条路。
  3. 一个点至多有一个前驱,但可以有多个后继。
  4. 无向图无环。

Anniversary party

 POJ - 2342 

首先理解题意: 如何选择使得最终得到的欢乐值最大?题目要求:下属和他的直接领导不能被一起选择。下面看一下样例:n,下面输入n行,每行代表一个人对应的欢乐值,再下面输入n-1行代表对应的关系,并且以0 0输入结束。

思路:按照大神给的步骤:首先这道题是否是数据结构对应树的关系,根据主管关系可以判断满足对应树的关系。是否是动态规划问题?

满足动态规划要求,最大欢乐值,无后效性,最优解,有子问题。接下来分析状态,每个状态都有选择和不选择两种状态,任何一个点的取舍都有两种状态,dp[i][0]表示第i个人不选择所获得的最大欢乐值,dp[i][1]表示第i个人选择所获得的最大欢乐值,

dp[node][0]+=max(dp[i][0],dp[i][1]),dp[node][1]+=dp[i][0];存储方式:节点数目多余5000个,最好采用邻接表存储,建议使用vector存储。

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
using namespace std;
const int maxn=6001;
int n;
int dp[maxn][3];
//dp[i][0]表示第i个人不选择所获得的最大欢乐值
//dp[i][1]表示第i个人选择所获得的最大欢乐值
int father[maxn];
int vis[maxn];
void dfs(int node)
{
  vis[node]=1;
  for(int i=1;i<=n;i++)
  {
    if(!vis[i]&&father[i]==node)//i是node的儿子
    {
      dfs(i);
      dp[node][0]+=max(dp[i][0],dp[i][1]);
      dp[node][1]+=dp[i][0];
    }
  }
}
int main()
{
  while(cin>>n)
  {
    memset(dp,0,sizeof(dp));
    memset(vis,0,sizeof(vis));
    for(int i=1;i<=n;i++)
    {
      cin>>dp[i][1];
      dp[i][0]=0;
    }
    int son,fath,root;
    bool beg=1;
    root=0;
    while(cin>>son>>fath,son||fath)
    {
      father[son]=fath;
      if(root==beg||son)
      {
        root=fath;
      }
    }
    while(father[root])//查找父亲节点
        root=father[root];
    dfs(root);
    int ans=max(dp[root][0],dp[root][1]);
    printf("%d\n",ans);
  }
  return 0;
}

Strategic game  POJ - 1463 

 其实这道题和上一道题目类型很像,动态规划方程都很像,原理也基本一样,只是方法,用的是vector方法。

思路,是否满足动态规划的要求,是否是树状结构,接下来考虑存储方式,不论是vector类似于邻接表还是用一维数组代替都可以。接下来考虑动态规划方程,同样的也是有两种状态,dp[node][0]表示以node为根在node上不放置士兵所需的最小士兵数目,dp[node][1]表示以node为根在node上放置士兵所需的最小士兵数目。i是node的儿子,dp[node][0]+=dp[i][1],父亲上不放置,儿子上一定要放置。注意看清题意,是树的点覆盖问题。dp[node][1]+=min(dp[i][0],dp[i][1])。我觉得主要难的是处理结点以及边的一些细节问题。

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<vector>
using namespace std;
typedef long long ll;
const int maxn=1501;
vector<int>mp[maxn];
ll dp[maxn][3];
ll father[maxn];
ll vis[maxn];
ll n;
void dfs(ll node)
{
  dp[node][0]=0;
  dp[node][1]=1;
  //vis[node]=1;
//  cout<<1<<endl;
  for(ll i=0;i<mp[node].size();i++)
  {
    int v=mp[node][i];//node是i的父亲
    //if(!vis[i])
    //{
      dfs(v);
      //不放置的话儿子一定要放
      dp[node][0]+=dp[v][1];
      dp[node][1]+=min(dp[v][0],dp[v][1]);
      //以node为根在node上放置士兵所需的最小士兵数目
    //}
  }
}
int main()
{
  int n;
  while(cin>>n)
  {
    memset(dp,0,sizeof(dp));
    memset(vis,0,sizeof(vis));
    memset(father,-1,sizeof(father));
    int fa,son,edge;
    for(int i=0;i<n;i++)
    {
      scanf("%d:(%d)",&fa,&edge);
      mp[fa].clear();
      while(edge--)
      {
        scanf("%d",&son);
        mp[fa].push_back(son);
        father[son]=fa;
      }
    }
  //  cout<<1<<endl;
    int root=1;
    while(father[root]!=-1)
        root=father[root];
    //cout<<root<<endl;
    dfs(root);
  //  cout<<root<<endl;
    ll ans=min(dp[root][0],dp[root][1]);
  //  cout<<dp[root][0]<<"\t"<<dp[root][1];
    cout<<ans<<endl;
  }
  return 0;
}

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值