树形dp

Posted 探险家Mr.H

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了树形dp相关的知识,希望对你有一定的参考价值。

高老师让我更我就更了...一道一道看吧

1.ZJU 3201 Tree of Tree

给出一棵N个点的无根树,每个节点都有对应的权值
现要求你找出一棵K个节点的子树,使得这个子树上的权值和最大

题解:dp[v][i]表示以v为根下的子树选i个点的最大权值和,做一下背包就可以了

代码源网侵删

(ZOJ密码忘了我真是...)

技术分享图片
#include<cstdio>
#include<iostream>
#include<cstdlib>
#include<algorithm>
#include<cmath>
#include<cstring>
#include<vector>
#include<queue>
using namespace std;
int head[maxn],dp[maxn][maxn],tol,num[maxn];  
struct node{  
    int next,to;  
}edge[maxn];  
void add(int u,int v){  
    edge[tol].to=v;  
    edge[tol].next=head[u];  
    head[u]=tol++;  
}  
int m;  
void dfs(int u,int fa)  
{  
    for(int i=head[u];i!=-1;i=edge[i].next){  
        int v=edge[i].to;  
        if(v==fa)continue;  
        dfs(v,u);  
        for(int j=m;j>1;j--)  
            for(int k=1;k<j;k++)  
                dp[u][j]=max(dp[u][j],dp[u][j-k]+dp[v][k]);  
    }  
}  
int main()  
{  
     int i,j,k,n;  
     while(cin>>n>>m){   
         memset(head,-1,sizeof(head));  
         tol=0;  
         memset(dp,0,sizeof(dp));  
         for(i=0;i<n;i++)  
              cin>>j,dp[i][1]=j;  
         for(i=1;i<n;i++){  
             scanf("%d%d",&j,&k);  
             add(j,k);  
             add(k,j);  
         }  
         dfs(0,-1);  
         int ans=0;  
         for(i=0;i<n;i++)  
             ans=max(ans,dp[i][m]);  
         cout<<ans<<endl;  
     }  
     return 0;  
}  
View Code

2.POJ1155 TELE

给定一棵树,1为根结点表示电视台,有m个叶子节点表示客户,有n-m-1个中间节点表示中转站,每条树边有权值。现在要在电视台播放一场比赛,每个客户愿意花费cost[i]的钱观看,而从电视台到每个客户也都有个费用,并且经过一条边只会产生一个费用。问电视台不亏损的情况最多有几个客户可以看到比赛

题解:也是一个背包,dp[v][i]表示以v为根的子树下连i个用户最多剩多少钱,这样答案就是dp[1][i]中满足dp[1][i]>0且最大的那个i

写一下转移方程吧,家里上不去poj

for(int i=size[v];i>=0;i--)
            for(int j=0;j<=i;j++)
                if(dp[vson][j]!=-INF&&dp[v][i-j]!=-INF)
                    dp[v][i]=max(dp[v][i],dp[v][i-j]+dp[vson][j]-w);
//w为连接v和vson的费用

3.NOI2002 贪吃的九头龙

传说中的九头龙是一种特别贪吃的动物。虽然名字叫“九头龙”,但这只是说它出生的时候有九个头,而在成长的过程中,它有时会长出很多的新头,头的总数会远大于九,当然也会有旧头因衰老而自己脱落。
有一天,有M个脑袋的九头龙看到一棵长有N个果子的果树,喜出望外,恨不得一口把它全部吃掉。可是必须照顾到每个头,因此它需要把N个果子分成M组,每组至少有一个果子,让每个头吃一组。
这M个脑袋中有一个最大,称为“大头”,是众头之首,它要吃掉恰好K个果子,而且K个果子中理所当然地应该包括唯一的一个最大的果子。果子由N-1根树枝连接起来,由于果树是一个整体,因此可以从任意一个果子
出发沿着树枝“走到”任何一个其他的果子。 对于每段树枝,如果它所连接的两个果子需要由不同的头来吃掉,那么两个头会共同把树枝弄断而把果子分开;如果这两个果子是由同一个头来吃掉,那么这个头会懒得把它弄断而直接把果子连同树枝一起吃掉。
当然,吃树枝并不是很舒服的,因此每段树枝都有一个吃下去的“难受值”,而九头龙的难受值就是所有头吃掉的树枝的“难受值”之和。 九头龙希望它的“难受值”尽量小,你能帮它算算吗? 【输入】 输入的第1行包含三个整数N(
1<=N<=300),M(2<=M<=N),K(1<=K<=N)。N个果子依次编号1,2,...,N,且最大的果子的编号总是1。第2行到第N行描述了果树的形态,每行包含三个整数a(1<=a<=N),
b(1<=b<=N),c(0<=c<=10^5),表示存在一段难受值为c的树枝连接果子a和果子b。 【输出】 输出仅有一行,包含一个整数,表示在满足“大头”的要求的前提下,九头龙的难受值的最小值。如果无法满足要求,输出-1

题解:远古NOI的题还是有一点难度的呀,这题也是思考30min无果膜了别人的题解才做出来的

-1的情况只有一种:大头吃完了小头不够每人一个

首先我们会发现,如果有2个或2个以上的小头,是可以经过安排使小头不会加"难受"值的,

于是只用考虑大头和小头之间的关系,部分分告诉我们要多叉转二叉,于是可做

状态:dp[v][i][isbig]表示以v为根的子树中分i个给大头吃,根是否被大头吃(isbig=1 是 isbig=0 不是)的最小代价

因为只有二叉,很好转移

4.HDU1561 The More The Better

给一个森林,有点权,选一个点必须选它父亲,最多选k个点,求点权和最大值

题解:上课讲了啊OuO就是一个01背包,枚举的时候套一层枚举子树就好了

5.NOI2008道路设计

  Z国坐落于遥远而又神奇的东方半岛上,在小Z的统治时代公路成为这里主要的交通手段。Z国共有n座城市,一
些城市之间由双向的公路所连接。非常神奇的是Z国的每个城市所处的经度都不相同,并且最多只和一个位于它东
边的城市直接通过公路相连。Z国的首都是Z国政治经济文化旅游的中心,每天都有成千上万的人从Z国的其他城市
涌向首都。为了使Z国的交通更加便利顺畅,小Z决定在Z国的公路系统中确定若干条规划路线,将其中的公路全部
x改建为铁路。我们定义每条规划路线为一个长度大于1的城市序列,每个城市在该序列中最多出现一次,序列中相
邻的城市之间由公路直接相连(待改建为铁路)。并且,每个城市最多只能出现在一条规划路线中,也就是说,任意
两条规划路线不能有公共部分。当然在一般情况下是不可能将所有的公路修建为铁路的,因此从有些城市出发去往
首都依然需要通过乘坐长途汽车,而长途汽车只往返于公路连接的相邻的城市之间,因此从某个城市出发可能需要
不断地换乘长途汽车和火车才能到达首都。我们定义一个城市的“不便利值”为从它出发到首都需要乘坐的长途汽
车的次数,而Z国的交通系统的“不便利值”为所有城市的不便利值的最大值,很明显首都的“不便利值”为0。小
Z想知道如何确定规划路线修建铁路使得Z国的交通系统的“不便利值”最小,以及有多少种不同的规划路线的选择
方案使得“不便利值”达到最小。当然方案总数可能非常大,小Z只关心这个天文数字modQ后的值。注意:规划路
线1-2-3和规划路线3-2-1是等价的,即将一条规划路线翻转依然认为是等价的。两个方案不同当且仅当其中一个方
案中存在一条规划路线不属于另一个方案。

题解:题解不重要,重要的是:为什么可以这么做

第一问直接dp[x][i]表示x点往下连i条链(i=0,1,2)就好了

第二问比较Tricky

“我们就算按树链剖分的方式搞这棵树,两个点也不会超过O(logn)条轻边”

所以我们给dp方程加一维:答案为j,然后让这个dp方程记方案数

dp[x][j][i]表示节点x往下连i条链,此时不方便值为j的方案数

因为j不超过O(logn)所以整体不超过O(nlogn)

小技巧:因为要取模还要判断可不可行,所以在计算中间结果的时候让%P=0的数%P=P

这样就少用了一个记录可不可行的数组

技术分享图片
#include<cstdio>
#include<cstring>
#include<algorithm>
const int maxn=200010,lim=10;
#define ll long long
using namespace std;
int next[maxn],first[maxn],to[maxn],Cnt;
int n,m,Q;
ll f[maxn][12][3];
void add(int a,int b){next[++Cnt]=first[a],first[a]=Cnt,to[Cnt]=b;}
int mo(ll t){return !t?0:t%Q?t%Q:Q;}
 
void Trdp(int x,int fa)
{
    int cnt=0;
    for(int i=first[x];i;i=next[i])if(to[i]!=fa)Trdp(to[i],x),cnt++;
    for(int i=0;i<=lim;i++)f[x][i][0]=1;
    if(!cnt)return;
    for (int i=first[x];i;i=next[i])if(to[i]!=fa)
    {
        int v=to[i];
        for(int j=0;j<=lim;j++)
        {
            ll t,f1=!j?0:f[v][j-1][0]+f[v][j-1][1]+f[v][j-1][2],f2=f[v][j][0]+f[v][j][1];
            //f1:修,f2:不修 
            t=(ll)f[x][j][2]*f1+(ll)f[x][j][1]*f2;f[x][j][2]=mo(t);
            t=(ll)f[x][j][1]*f1+(ll)f[x][j][0]*f2;f[x][j][1]=mo(t);
            t=(ll)f[x][j][0]*f1;f[x][j][0]=mo(t);
        }
    }
}
 
int main()
{
    scanf("%d%d%d",&n,&m,&Q);
    for (int i=1,a,b;i<=m;i++) scanf("%d%d",&a,&b),add(a,b),add(b,a);
    if(m<n-1){puts("-1\n-1");return 0;}
    Trdp(1,0);ll sum;
    for(int i=0;i<=lim;i++) if (sum=f[1][i][0]+f[1][i][1]+f[1][i][2]) return printf("%d\n%d\n",i,(int)sum%Q),0;
    return 0;
}
View Code

6.NOI2006 网络收费

——题面太长 省略 建议bzoj上的和codevs上的题面一起看——

题解:

又是一道远古NOI= =

首先它是一道语文题,收费规则是:对于某一个结点,若nA < nB则称之为A付费结点;反之则为B付费结点。

然后就可以分开算交的钱了

然后...其实就挺水的了  首先多叉转二叉,然后设dp[x][S][a]表示以x为根的子树中,它所有祖先的付费状态为S(一个二进制数,0为A,1为B),它的儿子有a个A

dp[x][S][A] = min{dp[lch][S|cur][Alch] + f[rch][S|cur][Arch]}。(cur表示x结点是A还是B)

边界情况是叶子节点

特殊考虑的是如果一个点的收费方式改变,它需要支付费用

然后就A了然后这题卡空间

注意到一个性质:一个点祖先多,儿子就少

所以把后两维压一起

说起来轻松,写起来...就400行出去了,我才不会告诉你我没有想到把S乘以一个数

这里贴一个网上的标程,毕竟自己写的我估计我自己过20天都看不懂了

技术分享图片
//Lib
#include<cstdio>
#include<cstring>
#include<cstdlib>
#include<cmath>
#include<ctime>
 
#include<iostream>
#include<algorithm>
#include<vector>
#include<string>
#include<queue>
using namespace std;
//Macro
#define rep(i,a,b) for(int i=a,tt=b;i<=tt;++i)
#define drep(i,a,b) for(int i=a,tt=b;i>=tt;--i)
#define erep(i,e,x) for(int i=x;i;i=e[i].next)
#define irep(i,x) for(__typedef(x.begin()) i=x.begin();i!=x.end();i++)
#define read() (strtol(ipos,&ipos,10))
#define sqr(x) ((x)*(x))
#define pb push_back
#define PS system("pause");
typedef long long ll;
typedef pair<int,int> pii;
const int oo=~0U>>1;
const double inf=1e100;
const double eps=1e-6;
string name="network",in=".in",out=".out";
//Var
int n,m,ans=oo;
int c[1100],flow[1100][1100],cost[1100][1100],fa[1100][1100];
bool type[1100];
struct T
{
    int v[2100],base;
    int& operator ()(int i,int j){return v[i*base+j];}
}f[2100];
void Init()
{
    scanf("%d",&n);m=1<<n;int a;
    rep(i,1,m)scanf("%d",&a),type[i]=a==0;
    rep(i,1,m)scanf("%d",c+i);
    rep(i,1,m)
        rep(j,i+1,m)
            scanf("%d",&a),flow[i][j]=flow[j][i]=a;
}
int Calc(int x,int j,int k)
{
    int ca=0,cb=0;
    rep(i,1,n)
    {
        if(k&1) ca+=cost[i][x];
        else    cb+=cost[i][x];
        k>>=1;
    }
    if(j)return ca+(type[x]?0:c[x]);
    else return cb+(type[x]?c[x]:0);
}
int TDP(int i,int j,int k,int h)
{
    int ret=f[i](j,k);
    if(ret)return ret;
    if(h)
    {
        ret=oo;
        int tmp,now,ls,tk;
        now=j<((1<<h)-j);
        tk=(k<<1)+now;
        ls=1<<h-1;
        for(int l=j-ls<0?0:j-ls;l<=j&&l<=ls;l++)
        {
            tmp=TDP(i<<1,l,tk,h-1)+TDP(i<<1^1,j-l,tk,h-1);
            ret=min(ret,tmp);   
        }
    }
    else ret=Calc(i-m+1,j,k);
    f[i](j,k)=ret;
    return ret;
}
void Work()
{
    rep(i,1,m)
    {
        int k=i+m-1;
        rep(j,1,n){k>>=1;fa[j][i]=k;}
    }
    rep(i,1,n+1)
        rep(j,1<<i-1,(1<<i)-1)f[j].base=1<<i-1;
    rep(i,1,m)rep(j,i+1,m)rep(k,1,n)
        if(fa[k][i]==fa[k][j])
        {
            cost[k][i]+=flow[i][j],
            cost[k][j]+=flow[i][j]; 
            break;
        }
    rep(i,0,m)
        ans=min(ans,TDP(1,i,0,n));  
    cout<<ans<<endl;
}
int main()
{
//  freopen((name+in).c_str(),"r",stdin);
//  freopen((name+out).c_str(),"w",stdout);
    Init();
    Work();
    return 0;
}
View Code

7.NOI2008奥运物流

题解:环套树dp的常规套路

先看树,再看环,然后把环套到树上

首先我们确定一个事情:修改一个点肯定是把它连到1节点上

对于环:我们手解方程(1h)

对于树:dp[x][i][j]表示节点x为根的子树,深度为i,修改j次获得最大值,dp即可(1h 30min)

环套到树上:我们手算环对答案的贡献(2h)

思考环需要套到哪(2h 30min)发现套到节点1贡献最大,具体手推

然后我们枚举1处的环有多长,dp即可(3h 30min)

如果你的手不是很健康 可以去网上搜论文 徐源盛《对一类动态规划问题的研究》里面有详细题解

技术分享图片
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstdlib>
#include<cstring>
#include<cmath>
using namespace std;
double _MinT;
#define Tryans(a) ((_MinT = (a)) > ans ? ans = _MinT : 1)
const int maxn=70;
const double inf=1e23;
int first[maxn],to[maxn],next[maxn],cnt;
double val[maxn];
double pw[maxn];
int r[maxn],size[maxn];
double dp[maxn][maxn][maxn];
double f[maxn][maxn];
int n,m;
double k;
inline void add(int a,int b){to[cnt]=b,next[cnt]=first[a],first[a]=cnt++;}
inline void pre()
{
    for(int i=0;i<=n;first[i++]=-1)
        for(int j=0;j<=n;j++)
            for(int k=0;k<=n;k++)dp[i][j][k]=-inf;
    cnt=0;
    //cout<<666;
}
inline void packdp(int x,int depth)
{
    for(int i=0;i<=size[x];i++)
        for(int j=0;j<=m;j++)f[i][j]=-inf;
     
    f[0][0]=0.;
    for(int i=first[x],s=1;~i;i=next[i],s++)
    {
        for(int j=0;j<=m;j++)
            for(int k=0;k<=j;k++)
                f[s][j]=max(f[s][j],f[s-1][k]+dp[to[i]][j-k][depth]);
    }
//  cout<<666;
}
inline void Trdp(int x)
{
    size[x]=0;
    for(int i=first[x];~i;i=next[i],size[x]++)Trdp(to[i]);
    packdp(x,2);
    for(int i=0;i<=n;i++)
        for(int j=1;j<=m;j++)
            dp[x][j][i]=f[size[x]][j-1]+val[x]*k;
    for(int i=0;i<=n;i++)
    {
        packdp(x,i+1);
        for(int j=0;j<=m;j++)
            dp[x][j][i]=max(dp[x][j][i],f[size[x]][j]+val[x]*pw[i]);
    }
}
int main()
{
    scanf("%d%d%lf",&n,&m,&k);
    pw[0]=1,pw[1]=k;
    for(int i=1;i<=n;i++)scanf("%d",&r[i]);
    for(int i=1;i<=n;i++)scanf("%lf",&val[i]);
    for(int i=2;i<maxn;i++)pw[i]=pw[i-1]*k;
    double ans=-inf;
    for(int i=r[1],len=2,j;i^1;i=r[i],len++)
    {
        pre();
        for(j=2;j<=n;j++)
            if(j^i) add(r[j],j);else add(1,j);
        Trdp(1);
       // cout<<666;
        Tryans(dp[1][m-(r[i]!=1)][0]/(1.-pw[len]));
    }
    printf("%.2lf",ans);
}
View Code

8.bzoj2616 PERIODNI

题解:(OS:对着上课笔记写不就行了

 这可能是我见过最假的树形dp

车的特性决定了它如果放在某一行,就相当于某一行某一列被一把剪子剪掉了,我们对于剩下的棋盘继续做dp,这样就可以转移了

技术分享图片
#include<cstdio>
#include<iostream>
#include<algorithm>
#define LL long long
using namespace std;
const int N = 1001, M = 1000001;
const LL MOD = 1000000007;
int n, K, root, G, H[N], h[N], len[N], son[N][2];
LL A[M], g[N][N], f[N][N];
void exgcd(LL a, LL b, LL &g, LL &x, LL &y) 
{
    if(!b) { g = a; x = 1LL; y = 0LL; return; }
    exgcd(b, a%b, g, y, x); y -= a/b*x;
}
void built(int &k, int l, int r) 
{
    int s = r; 
    if(l > r) return;
    if(!k) k = ++G;
    for(int i = l; i < r; i++) 
        if(H[i] < H[s]) s = i;
    len[k] = r-l+1; h[k] = H[s];
    for(int i = l; i <= r; i++) 
        H[i] -= h[k];
    built(son[k][0], l, s-1);
    built(son[k][1], s+1, r);
}
LL calc(int a, int b, int k) 
{
    if(!k) return 1;
    if(a < k || b < k) return 0;
    LL fz = (A[a] * A[b]) % MOD;
    LL fm = (A[a-k] * A[b-k]) % MOD;
    LL y, x, g;
    fm = (fm * A[k]) % MOD;
    exgcd(fm, MOD, g, x, y);
    x = (x < 0) ? x += MOD : x;
    return (fz * x) % MOD;
}
void Dfs(int k) 
{
    g[k][0] = f[k][0] = 1;
    if(!k) return;
    Dfs(son[k][0]); Dfs(son[k][1]);
    for(int i = 1; i <= K; i++)
        for(int j = 0; j <= i; j++) 
            g[k][i] = (g[k][i]+f[son[k][0]][j]*f[son[k][1]][i-j]) % MOD;
    for(int i = 1; i <= K; i++)
        for(int j = 0; j <= i; j++)
            f[k][i] = (f[k][i]+g[k][j]*calc(len[k]-j, h[k], i-j)) % MOD;
}
int main() 
{
    scanf("%d%d\n", &n, &K); 
    A[0] = 1LL;
    for(int i = 1; i <= n; i++) 
        scanf("%d", &H[i]);
    for(int i = 1; i <= M; i++) 
        A[i] = ((LL)i * A[i-1]) % MOD;
    built(root, 1, n); 
    Dfs(root);
    printf("%lld\n", f[root][K]);
    return 0;
}
View Code

NOI2009 二叉查找树 正确性尚存疑

NOI2012 迷失游乐园 数学公式太多,待我学LaTeX归来再码





以上是关于树形dp的主要内容,如果未能解决你的问题,请参考以下文章

Starship Troopers(HDU 1011 树形DP)

HDU1520 Anniversary party(树形dp入门题)

[填坑][支线任务]树形DP 树形背包

hdu1561 树形dp+背包

BZOJ_1060_时态同步_树形DP

HDU 1520 Anniversary party (树形DP)