您的位置:首页 > 其它

POJ 2057 The Lost House [树状DP]

2013-08-23 21:46 513 查看
题意:一只蜗牛将壳忘在了一棵树的某一个末结点(叶子)上。它想找回自己的壳,但忘记是丢在哪个结点上了,只好从树根开始网上爬,一个结点一个结点地找。在一些结点上居住着毛毛虫,它们会告诉蜗牛该结点以及它的子树上是否有它的壳,这样可以节省些时间。两个结点如果相连,则距离为1。问蜗牛找到壳的最小期望距离为多少。



接着给了一个例子(上图):

  假如蜗牛以结点2,4,5的顺序找,则壳分别在结点2,4,5时它需要爬行的距离为1,4,6。则它找到壳的期望为(1 + 4 + 6) / 3 = 11/3。

  考虑另一种情况,假设蜗牛先往结点3的方向爬,而在结点3处有毛毛虫,它可以告诉毛毛虫有关壳的信息,如果壳不在结点3和它的子树上,蜗牛就可以直接调转方向去2了。则以结点4,5,2的遍历顺序来考虑,它需要爬行的距离分别为2, 4, 3,则期望为(2 + 4 + 3) / 3 = 3。该例子中3即为最小期望距离。

思路:

  该题真的很锻炼思维呀,脑细胞死了不少。。

  首先需要用一个leaf[]数组来存储某节点子树上的叶子数,如上图,leaf[1] = 3, leaf[3] = 2。

  此外,对于某结点x,壳只可能有两种状态:在或者不在结点x的子树上。因此,用success[x]表示当壳在结点x的子树上时蜗牛在子树每个叶子上分别找到壳的爬行距离(距离0是从结点x开始算起)之和,如上图,success[3] = 1 + 3;当先访问结点3时,success[1] = 2 + 4 + 3。 这里与上面例子的期望的计算过程对比可以看出,当success表示的值总为最优情况下的距离时,最终期望即为success[1] / leaf[1]。当x为叶子结点时,success[x] = 0。

  当然,我们还要考虑在结点x的子树上找不到壳的情况,用fail[x]表示蜗牛在子树每个叶子上分别没有找到壳时的爬行距离(距离0是从结点x开始算起)之和,该值也应是最优的。最优体现在:如果x处有一只毛毛虫,而且提示x的子树上没有壳时,或者x结点为叶子时,蜗牛便不会再继续前进了,此时fail[x] = 0;否则,蜗牛就只好往前摸索了,此时fail[x] = sum(fail[y] + 2)。其中y是x的儿子结点。还是以上图为例,对于结点3来说,假如此处没有毛毛虫且壳不在3的子树上时,fail[3] = (fail[4] + 2) + (fail[5] + 2)。每个结点都要加2,是因为还要算上折返的距离。比如对于fail[4],蜗牛还要爬过边<3, 4>和<4, 3>,其他结点同理。

  问题的重点是,如何得出每一个点的success值。

  依然是先从简单的开始考虑,先不考虑让success最优。对于结点x,先将它的儿子结点按照遍历顺序从1开始编号,假设目前要遍历的为结点i,则结点1到(i-1)都已经遍历过了,且它们的子树上都没有壳。这里在计算success[x]的同时也在计算fail[x]。每遍历完一个儿子结点y时就有 fail[x] += fail[y] + 2。则当目前要遍历的结点为i时,fail[x]中恰好存储的是访问过结点1到(i-1)且都没有找到壳时的总距离。此时递推公式就比较好理解了:

  success[x] += (fail[x] + 1) * leaf[i] + success[i]。

  需要理解的是,success[i]表示的为结点i子树上找到壳的总距离,该距离是从结点i开始算起的。如果想将该距离转化为以结点x为起点,则需要加上在访问结点i之前走过的总路程,该总路程即为fail[x] + 1。蜗牛找完结点i-1后回到了x,然后从x爬向结点i,这里的加1即为边<x, i>的长度。此外,还需要考虑到结点i的子树上有不止一片叶子,则对于每一片叶子,在访问i之前都爬过了fail[x] + 1的距离,所以需要增加的总距离为(fail[x] + 1) * leaf[i]。

  现在,将最优的前提也考虑进去,则对于结点x,其儿子的访问顺序将会决定是否最优。

  假设x的其中两个儿子为y1, y2。

  如果先y1后y2的顺序遍历到y2,此时有:

  Δsuccess1 = (fail[x] + 1) * leaf[y1] + success[y1] + (fail[x] + fail[y1] + 2 + 1) * leaf[y2] + success[y2];

  如果先y2后y1的顺序遍历到y1,此时有:

  Δsuccess2 = (fail[x] + 1) * leaf[y2] + success[y2] + (fail[x] + fail[y2] + 2 + 1) * leaf[y1] + success[y1];

  Δsuccess1 - Δsuccess2 = (fail[y1] + 2) * leaf[y2] - (fail[y2] + 2) * leaf[y1]。

  这里用到了贪心,对于结点x,如果想让success[x]为最优,则每次遍历时取的儿子结点都应让success[x]的增量为最小。实现的方法是,当访问到结点x时,先用sort函数对x的所有儿子结点排序,排序的规则就是上面那个增量差值的公式,增量小的在前,然后依次遍历,这样得到的success[x]即为最优。

  写了这么多,自己都觉得有些絮叨。这两天脑子昏昏沉沉的,这道题找别人题解看了两天,才渐渐想明白,因此就在这里一点点地分析思路,也是为了帮助向我一样做这题怎么想都想不明白的人吧。

  此外,可能看上面的分析觉得很复杂,但如果看看代码,会觉得简单且好理解不少。有时候看文字解释,真不如读两行代码来得明白。

 

#include<stdio.h>
#include<string.h>
#include<vector>
#include<algorithm>
#include<iostream>
#define maxn 1005
using namespace std;

int su[maxn], fail[maxn], le[maxn], n, root;
bool w[maxn];
vector<int> a[maxn];
bool cmp(int x,int y)
{
return (fail[x] + 2) * le[y] < (fail[y] + 2) * le[x];
}
void dp(int x)
{
if (a[x].empty())
{
le[x] = 1;
return;
}
for (int i = 0; i < a[x].size(); i++)
{
int y = a[x][i];
dp(y);
le[x] += le[y];
}
sort(a[x].begin(), a[x].end(), cmp);
for (int i = 0; i < a[x].size(); i++)
{
int y = a[x][i];
su[x] += (fail[x] + 1) * le[y] + su[y];
fail[x] += fail[y] + 2;
}
if (w[x]) fail[x] = 0;
}
int main()
{
//freopen("data.in", "r", stdin);
while (~scanf("%d", &n) && n)
{
memset(fail, 0, sizeof(fail));
memset(su, 0, sizeof(su));
memset(le, 0, sizeof(le));
memset(w, 0, sizeof(w));
for (int i = 1; i <= n; i++)
a[i].clear();
for (int i = 1; i <= n; i++)
{
int j;
char ch;
scanf("%d %c", &j, &ch);
if (j == -1) root = i;
else a[j].push_back(i);
w[i] = (ch == 'Y' ? 1 : 0);//w[i]表示该结点是否有毛毛虫
}
dp(root);
printf("%.4lf\n", 1. * su[root] / le[root]);
}
return 0;
}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: