POJ 2057 The Lost House [树状DP]
题意:一只蜗牛将壳忘在了一棵树的某一个末结点(叶子)上。它想找回自己的壳,但忘记是丢在哪个结点上了,只好从树根开始网上爬,一个结点一个结点地找。在一些结点上居住着毛毛虫,它们会告诉蜗牛该结点以及它的子树上是否有它的壳,这样可以节省些时间。两个结点如果相连,则距离为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]即为最优。
写了这么多,自己都觉得有些絮叨。这两天脑子昏昏沉沉的,这道题找别人题解看了两天,才渐渐想明白,因此就在这里一点点地分析思路,也是为了帮助向我一样做这题怎么想都想不明白的人吧。
此外,可能看上面的分析觉得很复杂,但如果看看代码,会觉得简单且好理解不少。有时候看文字解释,真不如读两行代码来得明白。
1 #include<stdio.h> 2 #include<string.h> 3 #include<vector> 4 #include<algorithm> 5 #include<iostream> 6 #define maxn 1005 7 using namespace std; 8 9 int su[maxn], fail[maxn], le[maxn], n, root; 10 bool w[maxn]; 11 vector<int> a[maxn]; 12 bool cmp(int x,int y) 13 { 14 return (fail[x] + 2) * le[y] < (fail[y] + 2) * le[x]; 15 } 16 void dp(int x) 17 { 18 if (a[x].empty()) 19 { 20 le[x] = 1; 21 return; 22 } 23 for (int i = 0; i < a[x].size(); i++) 24 { 25 int y = a[x][i]; 26 dp(y); 27 le[x] += le[y]; 28 } 29 sort(a[x].begin(), a[x].end(), cmp); 30 for (int i = 0; i < a[x].size(); i++) 31 { 32 int y = a[x][i]; 33 su[x] += (fail[x] + 1) * le[y] + su[y]; 34 fail[x] += fail[y] + 2; 35 } 36 if (w[x]) fail[x] = 0; 37 } 38 int main() 39 { 40 //freopen("data.in", "r", stdin); 41 while (~scanf("%d", &n) && n) 42 { 43 memset(fail, 0, sizeof(fail)); 44 memset(su, 0, sizeof(su)); 45 memset(le, 0, sizeof(le)); 46 memset(w, 0, sizeof(w)); 47 for (int i = 1; i <= n; i++) 48 a[i].clear(); 49 for (int i = 1; i <= n; i++) 50 { 51 int j; 52 char ch; 53 scanf("%d %c", &j, &ch); 54 if (j == -1) root = i; 55 else a[j].push_back(i); 56 w[i] = (ch == 'Y' ? 1 : 0);//w[i]表示该结点是否有毛毛虫 57 } 58 dp(root); 59 printf("%.4lf\n", 1. * su[root] / le[root]); 60 } 61 return 0; 62 }