[SHOI2014] 概率充电器
题意:
给定一棵n个点的树,每个点有$q_u$的概率亮,每条边有$p_{u,v}$的概率存在。
如果一个连通块里有亮的点那么这个连通块里所有点都变成亮的。
问期望有多少个点是亮的。
$n\leq 500000$。
题解:
一开始按照传统dp那样推了……其实树上期望的题一般都不是传统题。
用期望的线性性拆一下,其实就是求每个点被点亮的概率之和。
考虑设$f_u$表示u被点亮的概率,但这东西有点难求,需要容斥。
于是直接设$f_u$表示u没被点亮的概率,那么有$f_u = (1- q_u )\prod f_v + p_{u,v}(1-f_v )$。
发现如果直接树形dp的话父子时间会互相影响,这数据范围也做不了高斯消元。
于是考虑换根dp,直接dfs一遍算出点1的答案,然后再dfs一遍往下算答案即可。
复杂度$O(n)$。
套路:
- 较复杂的期望dp$\rightarrow$先用线性性拆一下。
- 求期望合法的个数$\rightarrow$求每个合法的概率之和。
- 期望的本质就是算概率。
- 换根dp$\rightarrow$先dfs一遍算出根的答案,再把每个点的原答案加上父亲的答案去掉它的贡献。
代码:
#include<bits/stdc++.h> #define maxn 500005 #define maxm 500005 #define inf 0x7fffffff #define ll long long #define rint register int #define debug(x) cerr<<#x<<": "<<x<<endl #define fgx cerr<<"--------------"<<endl #define dgx cerr<<"=============="<<endl using namespace std; int n,hd[maxn],to[maxn<<1],nxt[maxn<<1],cnt; double P[maxn<<1],Q[maxn],F[maxn],G[maxn]; inline int read(){ int x=0,f=1; char c=getchar(); for(;!isdigit(c);c=getchar()) if(c=='-') f=-1; for(;isdigit(c);c=getchar()) x=x*10+c-'0'; return x*f; } inline void addedge(int u,int v,double p){ to[++cnt]=v,P[cnt]=p,nxt[cnt]=hd[u],hd[u]=cnt; to[++cnt]=u,P[cnt]=p,nxt[cnt]=hd[v],hd[v]=cnt; } inline void dfs1(int u,int fa){ F[u]=1.0-Q[u]; for(int i=hd[u];i;i=nxt[i]){ int v=to[i]; double p=P[i]; if(v==fa) continue; dfs1(v,u); F[u]*=(1.0+p*(F[v]-1.0)); } } inline void dfs2(int u,int fa){ if(u==1) G[u]=F[u]; for(int i=hd[u];i;i=nxt[i]){ int v=to[i]; double p=P[i]; if(v==fa) continue; if((1.0+p*(F[v]-1.0)==0)){cout<<n<<".000000"<<endl,exit(0);} double tp=G[u]/(1.0+p*(F[v]-1.0)); G[v]=F[v]*(1.0+p*(tp-1.0)),dfs2(v,u); } } int main(){ n=read(); for(int i=1;i<n;i++){ int u=read(),v=read(),p=read(); addedge(u,v,p/100.0); } for(int i=1;i<=n;i++) Q[i]=read()/100.0; dfs1(1,0),dfs2(1,0); double ans=0; for(int i=1;i<=n;i++) ans+=1.0-G[i]; printf("%.6lf\n",ans); return 0; }