The Lost House POJ - 2057(树形dp+贪心 (双线最优子结构问题))

思路

题意:有一只蜗牛爬上树睡着之后从树上掉下来,发现后面的"房子"却丢在了树上面, 现在这 只蜗牛要求寻找它的房子,它又得从树根开始爬起,现在要求一条路径使得其找到房子 所要爬行的期望距离最小. 爬行距离如下计算, 题目规定每一个分支和枝末都看做是 一个节点, 这些节点之间的距离都是1, 在分支上可能会有热心的毛毛虫, 这些毛毛虫 会如实的告诉蜗牛他之前是否经过这条路径, 也正是因为毛毛虫, 因此询问毛毛虫的顺 序使得这题的期望是不同的. 输入数据时给定的一个邻接关系,通过上一个节点来构图 同时字符 'Y'表示该点有毛毛虫, 字符'N'表示该点

解法:dp[i][0]表示以该点为根的节点找不到房子时要爬行最少的距离, 这个值有什么用呢?
这个值就是用去计算房子落在其他叶子节点时,其对爬行距离的负面效应有多大.
dp[i][1]表示以该点为根的节点在选择好所有分支点的爬行方案后,枚举完房子落在该
子树所有叶子节点上的总爬行距离的最小值,这是一个值告诉我们这棵子树对爬行距离
的正面效应有多大.那么有动态方程: ch[x]表示x节点一共有多少个孩子节点

 dp[i][0] = sum{ dp[j][0] + 2 } 当i没有毛毛虫且要求j是i的孩子节点,这个是很好
 理解的, 多出来的2就是连接该孩子节点的边来回的两次 
 dp[i][0] = 0 当该点有毛毛虫的时候, 原因是因为毛毛虫会告诉我们这点下面没有房子
 当一个节点时叶子节点的时候,那么 dp[i][0] = dp[i][1] = 0; 
 
 要明确dp[i][1]是表示在遇到分支选择的先后顺序决定后,我们枚举房子在其各个叶子
 上的所要爬行的总距离
 dp[i][1] = sum{ (sum{dp[1..j-1][0]+2}+1}*ch[j] +  dp[j][1]}, 其中j是i的孩子 
 这个方程要怎么去理解呢? 意思翻译过来就是遍历i的孩子中的j号子树所有叶子节点所
 要爬行的最短距离.其值就是: 
 前面[1, j-1]号子树没有找到节点所爬行的最短距离加上走了的多余的边再加上遍历j
 号子树所有叶子节点所爬行的最短距离, 那么这里显然谁前谁后就会决定最后值的大小
 
 现在只考虑序列中任意两棵子树A,B, 如果A放前面的话, 枚举完所有房子所在位置后的总距离就是
 ans1 = (ch[A] + dp[A][1]) + ((dp[A][0]+2)*ch[B] + dp[B][1] + ch[B])
 前一个括号是假设枚举房子落在A的所有叶子上, 后面的扩后是枚举在B的所有叶子上 
 ans2 = (ch[B] + dp[B][1]) + ((dp[B][0]+2)*ch[A] + dp[A][1] + ch[A])
 
 ans1 - ans2 = (dp[A][0]+2)*ch[B] - (dp[B][0]+2)*ch[A]

名义上是期望值,而实际上就是找一条路径。什么路径呢?从根节点走遍所有的叶子节点所花费步数最短的路径。

明确了题意后该怎么做呢?

首先看我们需要什么?

目前有个根节点,我们需要知道从他向一个分支走,失败步数是多少,成功步数是多少?

那么怎么维护我们需要的东西呢?

首先我们先给他们起个名:suc,fai;

其次再给一个节点的叶子节点的个数起个名:son

起名完事之后我们就要更新了。

先谈叶子节点,显然叶子节点的suc[x]=0,fai[x]=0,son[x]=1;

之后就是向上更新了,son和fai也很好搞

对于son的更新son[fa]+=son[x];

对于fai的更新fai[fa]+=fai[x]+2(此时worm[x]=0);否则的话不用管。

对于最不好弄的suc更新:

首先对于我们当前讨论的要走的子节点pn,这时候,蜗牛已经经过了走p1~pn-1的失败的步数,所以这些失败的步数是要记录的,我们给它命名为cnt-fai,此时我们会多走几个cnt-fai呢?我们发现,一共会多走son[pn]个cnt-fai,然而每次走过一个叶子节点后,更新一次suc[x],然后再将其视为失败,返回,找下一个叶子节点,此时我们就会发现,再返回到px的时候,这个蜗牛还需要继续返回一层,即返回到目前的根节点x,多走出一步,多走多少个一步呢?仍然是son[pn]个,用公式来写就是这样:suc[x]+=(cntfai+1)∗son[pn]+suc[pn]suc[x]+=(cntfai+1)∗son[pn]+suc[pn]suc[x]+=(cntfai+1)*son[pn]+suc[pn]

同时更新cntfai,cntfai+=fai[pn]+2cntfai+=fai[pn]+2cntfai+=fai[pn]+2这个2就是指去以及回来的两步

以上,dp的部分差不多搞定了,观察上面的式子发现,唯一不能确定的就是pn是什么鬼?

也就是说,对于一个根节点x我们按照什么顺序来讨论他的子节点会使得x的suc最小呢?

以下引用discuss里某神犇的证明

假设交换相邻的两颗子树的选择顺序,设P1,P2为选他们的概率,A1,A2为房子确实在上面所需的步数,B1,B2为实际上不在上面所需的步数,则
调整后:Delta=P1A1+P2(B1+A2)-P2A2-P1(B2+A1)=P2B1-P1B2
于是Delta<0 <=> B1/P1小于B2/P2
而题设情况即为Delta<0
于是应按照B/L排序 即遍历此子树所需步数/其所含叶子树

转化为公式呢?就是这个东西

(fai[u]+2)∗son[v]<(fai[v]+2)∗sonu∗son[v]<(fai[v]+2)∗sonu*son[v]<(fai[v]+2)*son[u]

其中u和v分别是两个子节点。

后记:这题是道好题,做完后能学到不少东西,值得一做,然而自己在做的时候对于这种双线的把握还不是很好,比较欠缺,什么鬼的排序根本没想到,自己对于这种难题的把握还是差很多啊!


作者:wzq_QwQ
来源:CSDN
原文:https://blog.csdn.net/wzq_QwQ/article/details/46359905?utm_source=copy
版权声明:本文为博主原创文章,转载请附上博文链接!

我的理解
:树形dp的思路,求出走每颗子树的最小步数。因为对于每颗子树,要么在这里找到壳,要么没有找到壳,所以设置dp[i][0]表示从i开始,遍历完所有子树,不能找到壳的最少步数,dp[i][1]表示,在最优策略下找到壳的最少步数(这和我们普通的dp不同,他是由两个最优子结构决定的,我们设的dp转移是一个最优子结构,而dp[i][1]表示的最优策略也是一个最优子结构。我们发现其实在最优策略下找到壳是可以贪心的,因为必然是朝失败次数少的方向转移)。 对于这样的双线最优子结构问题,常见的思路都是通过贪心来优化掉其中一线,否则复杂度会爆。就这题来说,如果不贪心优化的话,对于dp[i][1]就需要状压来求(二进制枚举顺序).

代码

#include <stdio.h>
#include <algorithm>
#include <iostream>
#include <string.h>
#include <math.h>
using namespace std;
const int maxn=1e3+8;
typedef long long ll;
struct Edge
{
    int u,v,w,next;
}edge[maxn];
int num,n;
int head[maxn];
int dp[maxn][2];
int vis[maxn];
int ch[maxn];
void addEdge(int u,int v,int w)
{
    edge[num].u=u;
    edge[num].v=v;
    edge[num].w=w;
    edge[num].next=head[u];
    head[u]=num++;
}
void init()
{
    memset(head,-1,sizeof(head));
    memset(dp,0,sizeof(dp));
    memset(ch,0,sizeof(ch));
    memset(vis,0,sizeof(vis));
    num=0;
}
bool cmp(int a,int b)
{
    return (dp[a][0]+2)*ch[b]<(dp[b][0]+2)*ch[a];
}
int dfs(int x)
{
    if(head[x]==-1)
    {
        dp[x][0]=dp[x][1]=0;
        return ch[x]=1;
    }
    vector<int> g;
    for(int i=head[x];i!=-1;i=edge[i].next)
    {
        int v=edge[i].v;
        ch[x]+=dfs(v);
        g.push_back(v);
    }
    sort(g.begin(),g.end(),cmp);
    for(int i=0;i<g.size();i++)
    {
        dp[x][1]+=dp[x][0]*ch[g[i]]+dp[g[i]][1]+ch[g[i]];
        //前面失败的+g[i]次成功的
        dp[x][0]+=dp[g[i]][0]+2;//找不到必然是子树找不到+来回两次跑
    }
    if(vis[x]) dp[x][0]=0;
    return ch[x];
}
int main(int argc, char const *argv[])
{
    while(scanf("%d",&n)!=EOF&&n)
    {
        char s[5];
        int p;
        scanf("%d %s",&p,s);
        init();
        for(int i=2;i<=n;i++)
        {
            scanf("%d %s",&p,s);
            vis[i]=(s[0]=='Y');
            addEdge(p,i,0);
        }
        dfs(1);
        printf("%.4f\n",1.0*dp[1][1]/ch[1]);
    }
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值