【[SHOI2014]概率充电器】解题报告

partychicken

2018-12-31 16:43:11

Solution

## 概率+树型dp ### Part I 拿到这道题的一瞬间,反应了一下——**这不是道假期望题吗?** 为什么?我们考虑这道题的答案 $$\sum p_i*w_i$$ 而,$w_i$全TMD是$1$ !!!所以,就对$p_i$**求个和**就行了。 **所以这题是概率dp** ___ ### Part II 考虑这个dp怎么写,一片乌鸦飞过,这TM怎么写啊!!! 聪明的同学不难发现,我们任意选取一个点作为根,然后考虑每个节点,不仅它的子节点可以对它亮的概率产生贡献,它的**父节点同样可以产生贡献**。 这咋dp啊?这dp个p啊? 诶?好像有个做法:我们枚举每个儿子为根,然后对每种情况进行处理,每次根节点的答案是正确的,因为它没有父亲~~(没爹的孩子果然简单易懂呢)~~。复杂度$O(n^2)$,看一眼数据范围,50w,emmm,~~松松松n方过百万~~显然过不去 考虑优化,发现每次换根的贡献只有一部分点,我们把这些点维护一下,复杂度。。。等等,这咋写啊?然后,我的思路就离正解越来越远。。。~~思路逐渐变态~~ ___ ### Part III 上回书说到,每个点的概率来源有三 - 它的子节点对它的贡献(即整棵子树中有节点进入充电状态,且通过边传递给它的概率) - 它自己的贡献(即它最初为充电状态的概率) - 它的父节点对它的贡献(即除子树外的部分有节点进入充电状态,且通过边传递给它的概率) 如果只考虑前两个,貌似dp方程比较显然 $$dp[now]+=(1-dp[now])\times dp[v]\times e[i].val$$ 其中,$dp[x]$指点$x$的概率,$v$是$now$的子节点,$e[i].val$是连接$now$,$v$的边连通的概率 然后,考虑父节点的贡献。我们用上面dp转移出的概率从根节点向下转移,这样**每个节点向下转移的时候已经是被充分计算过的**,没有后效性。 感觉哪里不对。。。 以一对儿父子关系为例,$a$是$b$的父节点。第一遍dp的时候,**$b$对$a$产生了贡献**,第二遍dp时**这个贡献又作为$a$的一部分贡献给了$b$**。 发现统计重复了,我们需要的是**$b$对$a$作贡献前$a$的dp值**,换言之,即**把$b$对$a$做的贡献排除出去**。 考虑第一遍的dp方程 $$dp[a]_{now}=dp[a]_{last}+(1-dp[a]_{last})\times dp[b]\times e[i].val$$ 每个变量的意义上同,特殊地,我们区分了$dp[a]_{now}$与$dp[a]_{last}$,分别指**转移后的dp[a] (即现在已知的dp[a])** 与 **转移前的dp[a] (即所求的,对dp[b] 在第二次dp中产生贡献的部分)** 化简可得 $$dp[a]_{last}=\dfrac{dp[a]_{now}-dp[b]\times e[i].val}{1-dp[b]\times e[i].val}$$ 求得$dp_{last}$后,沿用第一次的转移 $$dp[b]+=(1-dp[b])\times dp[a]_{last}\times e[i].val$$ 进行第二次dp,所得的结果就是最终答案。此题完结。 另外还有一些细节见代码(当然,看完上面的那些要是自己能打出来就不要看代码了) ___ ### Part IV 又到了喜闻乐见的放代码环节 ``` #include<bits/stdc++.h> using namespace std; const double eps=1e-7; struct Edge { int to,nxt; double val; Edge(){} Edge(int to,int nxt,double val):to(to),nxt(nxt),val(val){} }e[1000010]; int head[500010],cnt; void addedge(int u,int v,double val) { e[++cnt]=Edge(v,head[u],val); head[u]=cnt; } int n; double dp[500010]; void dfs1(int now,int fa)//第一遍dp { for(int i=head[now];i;i=e[i].nxt) { int vs=e[i].to; if(fa==vs) continue; dfs1(vs,now); dp[now]+=dp[vs]*(1-dp[now])*e[i].val; } } void dfs2(int now,int fa)//第二遍dp { for(int i=head[now];i;i=e[i].nxt) { int vs=e[i].to; if(fa==vs) continue; if(fabs(1-dp[vs]*e[i].val)<=eps) //坑:如果分母为0直接跳过。 //原因:分母为0说明dp[vs]为1,不需要贡献 { dfs2(vs,now); continue; } dp[vs]+=(1-dp[vs])*(dp[now]-dp[vs]*e[i].val)/(1-dp[vs]*e[i].val)*e[i].val; dfs2(vs,now); } } int main() { cin>>n; for(int i=1;i<n;i++) { int u,v; double val; cin>>u>>v>>val; addedge(u,v,val/100); addedge(v,u,val/100); } for(int i=1;i<=n;i++) { cin>>dp[i];//自己的贡献一开始就算进去 dp[i]/=100; } dfs1(1,0); dfs2(1,0); double ans=0; for(int i=1;i<=n;i++) { ans+=dp[i]; } cout<<fixed<<setprecision(6);//保留6位小数 cout<<ans<<endl; } ``` ___ ### Part V 回过头来看这道题,发现我一开始的思路并没有问题,只是没想清楚。 我们统计父节点贡献的时候,**本质上就是换根,**把原来的父节点变成子节点,那么**原来对父节点的贡献要还原,而父节点要对子节点进行贡献**。而第二遍dp的本质是对重复信息的利用,以达到换根效率的最优效果——$O(1)$换根。 题目不错,值得一做。