树形DP

DP有些求骗的东西,比如树形DP。
怎么说呢,树也是个DAG,按理说,是DAG就能动规。



用题说事

先看一道题

洛谷P1271 聚会的快乐

得自己建树,人名字符串还要映射……板恶心……

说正事。
这道题里,上司—下属关系就是一个天然的有向边,利用它,从老板开始,往下深搜就好啦~

Created with Raphaël 2.1.0 开始 建树 —————————————决策—————————————— 决策一:不取现在搜到的人 决策二:取现在搜到的人,当他的上司不取才能进行 结束

建树当然是好用的链式前向星啦~不过转二叉树也可以
真正写代码时,是看取不取他的下属
一个下属取,一个下属不取……
在加上本身取不取就是这一棵子树最大值。

代码 75分

再来一道题

洛谷P1271 有线电视网

分析下次上

代码 AC

给你三个题

似乎不止三个……
洛谷 P2014 选课
洛谷 P2015 二叉苹果树
(紫书p282)
UVa 12186 Another Crisis
UVa 1220 Party at Hali-Bula
UVa 1218 Perfect Service
题解会有的……


几个知识点(待补充)

来自紫书、蓝书、挑战程序设计竞赛

树的最大独立集

对于一棵 n 个结点的无根树,选出尽量多的结点,使其中任意两个结点均不相邻(即最大独立集)。
洛谷P1271就是一个最大独立集问题,不过带了权(幽默系数)。然而,P1271是有根的(上司)。

那么,对于最大独立集问题,只需随便“提溜”一个结点“当”根,然后就很开心了。
紫书p281
(紫书p281)
对结点i,只有两种决策:选和不选。即方程:

d(i)=max{1+jgrandson(i)d(j),json(i)d(j)}

d(i)表示以 i 为根结点的子树的最大独立集

写代码时,方法一是记搜(如P1271);方法二是“刷表法”:对结点 i ,计算出d(i)后,更新 i 的父亲和祖父的累加值,这样只需存每个结点的父亲就好(只开一个数组)。

法一代码

    int dp[N][2];
    int dfs(int i,int qu){
        if(dp[i][qu])return dp[i][qu];
        int ans=0;
        for(int e=head[i];e;e=nex[e]){
            int son=dfs(to[e],0);
            if(!qu)son=max(son,dfs(to[e],1));
            ans+=son;
            }
        return dp[i][qu]=ans+son*qu;
        }

法二代码

树的重心(质心)

紫书p281

对于一棵 n 个结点的无根树(又是无根树),找到一个点,使把树变成以它为根的有根树时,最大子树的节点数最小。也就是说,删除它后最大的树的节点数最少。

同样,先“提溜”一个结点“当”根,设d(i)表示以 i 为根的子树中结点个数,则有:

d(i)=1+json(i)d(j)

然后随便dfs就行了。
这只是把d(i)求出来了,删除 i 时,在
f(i)=max{maxjson(i)d(j),nd(i)}

中,取一个最小的f(i),此时 i 就是重心。
紫书p281
(紫书p281)
f(i)表示删除 i 后,最大子树的结点数。n-d(i)表示 i 的父结点连的结点总数。因为 d(i)=1+json(i)d(j) ,d(i)就是图中虚线方块外的结点数,自然n-d(i)就是虚线方块内的结点数。

代码

int f[N],d[N];
int dfs(int i,int fa){
    int sum=0;
    for(int e=head[i];e;e=nex[e])
        if(to[e]!=fa)sum+=dfs(to[e],i);
    return d[i]=sum+1;
    }
int zhongxin(int root){
    int a=dfs(root,-1),ans=-1,ff=INF;
    for(int i=1;i<=n;i++){
        int son=n-d[i];
        for(int e=head[i];e;e=nex[e])
            son=max(son,d[to[e]]);
        if(son<ff){
            ff=son;
            ans=i;
            }
        }
    return ans;
    }

树的直径(树的最长路径、最远点对)

紫书p281、挑战程序设计竞赛p295

对 n 个结点的无根树,找到一条最长路径,即找到两个点,使它们距离最远。

普通算法就是随便提溜一个根root,然后dfs找到根最远的点,再从这个点出发,dfs找最远的点。这样,第二次dfs找的路径就是树的直径。
一眼看就是对的。可为什么对,不说了,不重要。代码很简单,不打了,挑战程序设计竞赛p295有。

重要的是DP算法。
紫书p281
(紫书p281)
dp只能求最长路长度,如果只要求长度,那就开心了。
设d(i)表示以 i 为根的子树中根到叶子的最大距离,则有:

d(i)=1+maxjson(i){d(j)}

其中”1”就是 i 到 j 的路径长度,也可换成权值。然后就好理解了。
最后结果就是root的子节点中,d最大的两个d(u),d(v),再加上2。(d(u)+d(i)+2)。
看上面的图:d(u),d(v)已知,u到灰色节点距离为1,v到灰色节点距离也为1,加起来就是d(u)+d(i)+2(也可带权)。

int d[N];
int dfs(int i,int fa){
    int ans=0;
    for(int e=head[u];e;e=nex[e])
        if(to[e]!=fa)
            ans+=dfs(to[e],i);
    return d[i]=ans+1;
    }

OBST(最优排序二叉树)

蓝书p63
有 n 个符号,每个符号有一个检索频率,互不相同。将它们建成一棵排序二叉树,使总检索次数(所有符号的频率与其深度乘积之和,根的深度==1)最小。
UVa 10304 Optimal Binary Search Tree

这算序列型DP还是树状DP呢?不管,反正这个挺难的。
前方高能!!!
和XXX一样,先选根,再递归左右子树。
根的频率知道了,那么,它的儿子就要乘2,孙子就要乘3……太麻烦!直接所有的都加一次得了。子树?子树再当成根,递归。那方程就是:

d(i,j)=sum(i,j)+maxi<=k<=j{d(i,k1)+d(k+1,j)}

d(i,j)表示序列i~j的总检索次数,sum(i,j)表示i~j频率和。 O(n3)

int sum[N],d[N][N];
int build(int l,int r){
    if(l==r)return sum[l]-sum[l-1];
    if(d[l][r])return d[l][r];
    int son=0;
    for(int k=l;k<=r;k++)
        son=max(son,build(l,k-1)+build(k+1,r));
    return son+(sum[r]-sum[l-1]);
    }
int work(){
    for(int i=1;i<=n;i++)
        sum[i]+=sum[i-1];//直接在输入数组上做前缀和
    return build(1,n);
    }

这只是比较朴素的方法……
前方真正高能!!!
能降至 O(n2) 。记K(i,j)为让d(i,j)取最小值的决策k,有

K(i,j)K(i,j+1)K(j+1,i+1)

证明要四边形不等式!!!


如果对于任意的a1≤a2< b1≤b2,有不等式

m(a1,b1)+m(a2,b2)m(a1,b2)+m(a2,b1)
成立,则m(i,j)满足四边形不等式。

由上面所设,i≤j,所以套不等式:

K(i,i)+K(i+1,j+1)K(i,j+1)+K(i+1,j)

i≤j,而i+1就不一定了,于是K(i+1,j)无意义,扔掉。
就是:
K(i,i)+K(i+1,j+1)K(i,j+1)


所以,枚举k就可改成K(i,j-1)~K(i+1,j)。这两个是早就算过的,从而降至 O(n2)

总结一下

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值