树形dp(更新)

树形dp

emmmm树形dp应该是由大牛《背包九讲》的第七讲依赖背包演化而来,

P07: 有依赖的背包问题
简化的问题
这种背包问题的物品间存在某种“依赖”的关系。也就是说,i依赖于j,表示若选物品i,则必须选物品j。为了简化起见,我们先设没有某个物品既依赖于别的物品,又被别的物品所依赖;另外,没有某件物品同时依赖多件物品。
算法
这个问题由NOIP2006金明的预算方案一题扩展而来。遵从该题的提法,将不依赖于别的物品的物品称为“主件”,依赖于某主件的物品称为“附件”。由这个问题的简化条件可知所有的物品由若干主件和依赖于每个主件的一个附件集合组成。
按照背包问题的一般思路,仅考虑一个主件和它的附件集合。可是,可用的策略非常多,包括:一个也不选,仅选择主件,选择主件后再选择一个附件,选择主件后再选择两个附件……无法用状态转移方程来表示如此多的策略。(事实上,设有n个附件,则策略有2^n+1个,为指数级。)
考虑到所有这些策略都是互斥的(也就是说,你只能选择一种策略),所以一个主件和它的附件集合实际上对应于P06中的一个物品组,每个选择了主件又选择了若干个附件的策略对应于这个物品组中的一个物品,其费用和价值都是这个策略中的物品的值的和。但仅仅是这一步转化并不能给出一个好的算法,因为物品组中的物品还是像原问题的策略一样多。
再考虑P06中的一句话: 可以对每组中的物品应用P02中“一个简单有效的优化”。 这提示我们,对于一个物品组中的物品,所有费用相同的物品只留一个价值最大的,不影响结果。所以,我们可以对主件i的“附件集合”先进行一次01背包,得到费用依次为0..V-c[i]所有这些值时相应的最大价值f’[0..V-c[i]]。那么这个主件及它的附件集合相当于V-c[i]+1个物品的物品组,其中费用为c[i]+k的物品的价值为f’[k]+w[i]。也就是说原来指数级的策略中有很多策略都是冗余的,通过一次01背包后,将主件i转化为V-c[i]+1个物品的物品组,就可以直接应用P06的算法解决问题了。
较一般的问题
更一般的问题是:依赖关系以图论中“森林”的形式给出(森林即多叉树的集合),也就是说,主件的附件仍然可以具有自己的附件集合,限制只是每个物品最多只依赖于一个物品(只有一个主件)且不出现循环依赖。
解决这个问题仍然可以用将每个主件及其附件集合转化为物品组的方式。唯一不同的是,由于附件可能还有附件,就不能将每个附件都看作一个一般的01背包中的物品了。若这个附件也有附件集合,则它必定要被先转化为物品组,然后用分组的背包问题解出主件及其附件集合所对应的附件组中各个费用的附件所对应的价值。
事实上,这是一种树形DP,其特点是每个父节点都需要对它的各个儿子的属性进行一次DP以求得自己的相关属性。这已经触及到了“泛化物品”的思想。看完P08后,你会发现这个“依赖关系树”每一个子树都等价于一件泛化物品,求某节点为根的子树对应的泛化物品相当于求其所有儿子的对应的泛化物品之和。
小结
NOIP2006的那道背包问题我做得很失败,写了上百行的代码,却一分未得。后来我通过思考发现通过引入“物品组”和“依赖”的概念可以加深对这题的理解,还可以解决它的推广问题。用物品组的思想考虑那题中极其特殊的依赖关系:物品不能既作主件又作附件,每个主件最多有两个附件,可以发现一个主件和它的两个附件等价于一个由四个物品组成的物品组,这便揭示了问题的某种本质。
我想说:失败不是什么丢人的事情,从失败中全无收获才是。

emmm大牛说的2006那道题 应该是金明的预算方案进化版,那道题只有一层依赖关系,而且最多只依赖两个,所以是分类的01背包。
而树形dp则是有多层依赖关系,可以依赖多个的dp。废话不多说。从题目中理解。先以上一些水题。

P1040加分二叉树
很经典的一道树形dp,
无论如何,你都可以得知在中序遍历中,左子在父亲的左边,右子在右边,所以你去枚举每一个当前形成的这个父亲节点与左子与右子,即在某一段[l,r]中以i为根的最优解。
这代码使用记忆化搜索写的。
设f[i, j]为顶点i . . 顶点j所组成的子树的最大分值。若f[i, j] = -1,则表明最大分值尚未计算出。

f(i,j)={1 ; 顶点i的分数 ; max(f{i,k-1}*f{k+1,j}+顶点i的分数『k取i~j』)
i > > j 1
i=j 作为父亲的i的分数
i<j时 以某点分割好的目前根的左子树右子树的最优解。

#include<iostream>
#include<cstdio>
using namespace std;
int f[40][40],root[40][40];
bool firstwrite;
long long search(int L,int r)
{
  int k;
  long long now,ans;    
  if (L>r) return 1;
  if (f[L][r]==-1)    
     for(k=L; k<=r; k++) 
      { 
        now=search(L,k-1)*search(k+1,r)+f[k][k];  
        if(now>f[L][r])  
        {
          f[L][r]=now; 
          root[L][r]=k;
        }
      }
  return f[L][r]; 
}
void  preorder(int L, int r)
{
  if (L>r) return;
  if (firstwrite) firstwrite=false;  
  cout<<root[L][r]<<" ";   
  preorder(L,root[L][r]-1);  
  preorder(root[L][r]+1,r); 
}
int main()
{
  int n,i;
  bool firstwrite;
  cin>>n; 
  for(i=1; i<=n; i++)  
    for(int j=i; j<=n; j++)
      f[i][j] = -1;
  for (i=1; i<=n; i++)
    {
      cin>>f[i][i];  
      root[i][i] = i;  
    }
  cout << search(1, n) << endl;    
  firstwrite=true;                  
  preorder(1,n);  
  cin>>n;           
  return 0;
}

P1352 没有上司的舞会
这道题虽然水到一种境界,但是这非常树形dp的经典。
上方程

f[i][0]=a[i]+max(f[son[i][j]][1],f[son[i][j]][0]);
f[i][1]=a[i]+f[son[i][j]][0];

f[i][1]代表i这个节点不取,那么他的儿子必然取不了
f[i][1]代表i这个节点取,那么他的儿子可取可不取

#include<bits/stdc++.h>
using namespace std;
const int maxn=6005;
int n,a[maxn],vis[maxn],root,f[maxn][3];
vector <int > son[maxn];
void dp(int x)
{
    f[x][0]=0;
    f[x][1]=a[x];
    for (int i=0; i<son[x].size(); i++)
      {
        int y=son[x][i];
        dp(y);
        f[x][0]+=max(f[y][1],f[y][0]);
        f[x][1]+=f[y][0];
      }
}
int main()
{
  scanf("%d",&n);
  for (int i=1; i<=n; i++)  scanf("%d",&a[i]); 
  for (int i=1; i<n; i++)
    {
        int x,y;
        scanf("%d%d",&x,&y);
        son[y].push_back(x);
        vis[x]=1;
    }
  for (int i=1;i<=n;i++)
    if (!vis[i])  
      {
        root=i;
        break;
      }
  dp(root);
  int ans=max(f[root][0],f[root][1]);
  cout<<ans;
}

还有一道斯波题
p2014选课
这道题树形dp以前做过一次不会做,订正以后。。现在还是错的。。。60分,不知道哪里出问题了。。。。。。。。。。。

树形dp程序都是dfs加dp,模板差不多,一通百通大概这样子(其实大多知识都是这样)这道题的代码我是从上一题改过来的。
方程就是f[i][j]表示分配给i这个节点j个名额的最优方案 。
(剩下的以及不水的dp明天讲)

#include<bits/stdc++.h>
using namespace std;
const int maxn=310;
int n,a[maxn],root,f[maxn][maxn],m;
vector <int > son[maxn];
void dp(int x)
{
    for (int i=0; i<son[x].size(); i++)
      {
        int y=son[x][i];
        dp(y);
        for (int j=m; j>=0; j--)
          for (int k=j; k>=0; k--)
            f[x][j]=max(f[x][j],f[x][j-k]+f[y][k]);
      }
    if (x!=0)
      {
        for (int i=m; i>=1; i--)
          f[x][i]=f[x][i-1]+a[x];
      }
}
int main()
{
  scanf("%d%d",&n,&m);
  for (int i=1; i<n; i++)
    {
        int x,y;
        scanf("%d%d",&x,&y);
        a[i]=y;
        son[x].push_back(i);
    }
  dp(0);
  cout<<f[0][m];
}

骑士

题目描述
Z国的骑士团是一个很有势力的组织,帮会中汇聚了来自各地的精英。他们劫富济贫,惩恶扬善,受到社会各界的赞扬。

最近发生了一件可怕的事情,邪恶的Y国发动了一场针对Z国的侵略战争。战火绵延五百里,在和平环境中安逸了数百年的Z国又怎能抵挡的住Y国的军队。于是人们把所有的希望都寄托在了骑士团的身上,就像期待有一个真龙天子的降生,带领正义打败邪恶。

骑士团是肯定具有打败邪恶势力的能力的,但是骑士们互相之间往往有一些矛盾。每个骑士都有且仅有一个自己最厌恶的骑士(当然不是他自己),他是绝对不会与自己最厌恶的人一同出征的。

战火绵延,人民生灵涂炭,组织起一个骑士军团加入战斗刻不容缓!国王交给了你一个艰巨的任务,从所有的骑士中选出一个骑士军团,使得军团内没有矛盾的两人(不存在一个骑士与他最痛恨的人一同被选入骑士军团的情况),并且,使得这支骑士军团最具有战斗力。

为了描述战斗力,我们将骑士按照1至N编号,给每名骑士一个战斗力的估计,一个军团的战斗力为所有骑士的战斗力总和。

输入输出格式
输入格式:
输入文件knight.in第一行包含一个正整数N,描述骑士团的人数。

接下来N行,每行两个正整数,按顺序描述每一名骑士的战斗力和他最痛恨的骑士。

输出格式:
输出文件knight.out应包含一行,包含一个整数,表示你所选出的骑士军团的战斗力。

题目链接

难度看起来是省选+/NOI-
虽然看起来好像也只是一道树形DP……很普通吧
仔细想想的话,
这道题和没有上司的聚会的dp没有本质上的区别
f[i][1]=max(f[j][1],f[j][0]) f [ i ] [ 1 ] = m a x ( f [ j ] [ 1 ] , f [ j ] [ 0 ] )
f[i][0]=f[j][1] f [ i ] [ 0 ] = f [ j ] [ 1 ]
ji j 是 i 的 儿 子

具体看没有上司的聚会是怎么样的
这道题主要的问题是,因为每一个骑士有且只有一个仇视的骑士,所以没有固定的树根,那么需要你去造。
因为本身这里有n个点,n条边所以,必然是有一些联通块的构成,因为一个点只含有一条出边一条入边,这个不懂的话小学再学学。
每个连通块内有且只有一个简单环
这样 我们考虑把每个联连通块的环上删一条边
这样它必然构成树
然后要注意
删掉的边所连接的两点x,y
是不同时选的
所以我们分别强制x,y其中一个点不选
对新树跑DP
显然相邻的点是不能选的
而且我们保证了每条边都会一一枚举过去,看程序自己想为什么
所以得状态转移方程:
f[i][0/1] f [ i ] [ 0 / 1 ] 表示以i为根节点的子树选i/不选i所能获得的最大价值

#include<bits/stdc++.h>
using namespace std;
const int maxn=2000010;
long long n,a[maxn],vis[maxn],root,fa[maxn],f[maxn][3],head[maxn];
int cnt;
long long ans;
struct node
{
    int v,nxt;
}e[maxn<<1];
void add(int u,int v)
{
    ++cnt;
    e[cnt].nxt=head[u];
    e[cnt].v=v;
    head[u]=cnt;
}
void dp(int x)
{
    vis[x]=1;
    f[x][0]=0;
    f[x][1]=a[x];
    for (int i=head[x]; i; i=e[i].nxt)
      {
        int y=e[i].v;
        if (y!=root)
        {
          dp(y);
          f[x][0]+=max(f[y][1],f[y][0]);
          f[x][1]+=f[y][0];
        }
        else
        f[y][1]=-maxn;
      }
}
void find(int x)
{
    vis[x]=1;
    root=x;
    while (!vis[fa[root]])
      {
        root=fa[root];
        vis[root]=1;
      }
    dp(root);
    long long t=max(f[root][0],f[root][1]);
    vis[root]=1;
    root=fa[root];
    dp(root);
    ans+=max(t,(long long)max(f[root][0],f[root][1]));
    return;
}
int main()
{
  scanf("%d",&n);
  for (int i=1; i<=n; i++)
    {
        int x,y;
        scanf("%d%d",&a[i],&y);
        add(y,i);       
        fa[i]=y;
    }
  for (int i=1;i<=n;i++)
    if (!vis[i])  find(i);
  cout<<ans;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值