【洛谷】P4284 [SHOI2014] 概率充电器(期望+换根dp)
题意
给定一棵 \(n\) 个节点的树。进行充电时,每条边是否可以导电以概率决定,每一个节点自身是否直接进行充电也由概率决定。随后电能可以从直接充电的节点经过通电的边使得其他充节点进行间接充电。
求进入充电状态的节点个数的期望。
\(n \leq 5 \times 10^5\)。
思路
设 \(p_i\) 为第 \(i\) 个节点通电的概率,由于每个节点的价值为 \(1\),那么答案实际上就是:
根据题意可以推出一个节点通电的三种情况;
-
该节点本身直接充电
-
该节点子树内存在一个节点通电,且通过导电的边使该节点通电
-
该节点子树外存在一个节点通电,且通过导电的边使该节点通电
由于需要分别考虑子树内和子树外的影响,不难想到换根dp。
设 \(g_u\) 为只考虑以 \(u\) 为根的子树时,\(u\) 通电的概率。设事件 \(A\) 为根节点自身通电,事件 \(B_v\) 为子节点 \(v\) 通电且边 \((u,v)\) 导通、根据概率的加法公式,可以得到:
由于各个儿子节点在只考虑子树的情况下相互独立,于是这个公式可以拓展到有多个儿子的情况下。
接下来考虑子树外的节点使该节点通电的情况。对于一个点通电的概率 \(P_u\),可以拆分为由 \(v\) 节点使他通电和除 \(v\) 节点外使他通电的情况。分别令事件 \(A\) 为 \(v\) 节点使他通电,事件 \(B\) 为除 \(v\) 节点外使他通电的情况,套用公式:
其中,\(P(A \cup B)=p_u\),\(P(A)=g_u \times w_{(u,v)}\)。整理可以得到:
那么 \(v\) 子树外使他导通的概率就为 \(P(B) \times W_{u,v}\),与 \(g_v\) 套用加法公式,就可以得到 \(p_v\) 的值。
最后,需要注意特判 \(P(A)=g_u \times w_{(u,v)}=1\) 的特殊情况,在这种情况下该节点一定可以由子树外的节点导通,就没必要更新了。
code:
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<cmath>
using namespace std;
const int N=5e5+10;
const double eps=1e-6;
int h[N],idx,n,q[N];double p[N],ans;
struct edge{int v,w,nex;}e[N<<1];
void add(int u,int v,int w){e[++idx]=edge{v,w,h[u]};h[u]=idx;}
void dfs1(int u,int fa)//子树内
{
p[u]=1.0*q[u]/100;
for(int i=h[u];i;i=e[i].nex)
{
int v=e[i].v;if(v==fa) continue;dfs1(v,u);
p[u]=p[u]+p[v]*e[i].w/100.0-p[u]*p[v]*e[i].w/100.0;
}
}
bool check(double aa,double bb)
{
if(aa+eps>bb&&aa-eps<bb) return 1;
return 0;
}
void dfs2(int u,int fa) //子树外
{
ans+=p[u];
for(int i=h[u];i;i=e[i].nex)
{
int v=e[i].v;if(v==fa) continue;
if(check(p[v]*double(e[i].w)/100,1)) {dfs2(v,u);continue;}
double k=(p[u]-p[v]*e[i].w/100.0)/(1-p[v]*e[i].w/100.0)*e[i].w/100.0;
// printf("%lf\n",k);
p[v]=p[v]+k-k*p[v];dfs2(v,u);
}
}
int main()
{
scanf("%d",&n);for(int u,v,w,i=1;i<n;i++) scanf("%d%d%d",&u,&v,&w),add(u,v,w),add(v,u,w);
for(int i=1;i<=n;i++) scanf("%d",&q[i]);dfs1(1,0);dfs2(1,0);printf("%lf\n",ans);
return 0;
}