P1654 OSU!
题意:给
n
n
n 个数,代表
n
n
n 次操作,每次操作有
p
[
i
]
p[i]
p[i] 的概率使得当前的值为
1
1
1 ,
1
−
p
[
i
]
1-p[i]
1−p[i] 的概率为
0
0
0,最终这个串里每一段不重复的连续个
1
1
1 的长度
X
X
X 对答案贡献为
X
3
X^3
X3,求这个答案的期望。
笔记:
考虑每一次操作对答案的贡献。
那么就需要知道前一次答案的期望,然后考虑差值并转移。
做法:
在这个题目中如果之前的答案为
X
3
X^3
X3 ,那么当前选择
1
1
1 的答案是
(
X
+
1
)
3
(X+1)^3
(X+1)3 ,做差后发现答案多了
3
∗
X
2
+
3.
∗
X
+
1
3 * X^2 + 3.*X + 1
3∗X2+3.∗X+1 。
显然在这个假设中,
X
X
X 代表的是之前连续的
1
1
1 的期望。
所以每一次选择
1
1
1 都会对最后的答案产生一次相应的贡献。
此时定义
f
[
i
]
f[i]
f[i] 为前
i
i
i 个数,以
i
i
i 为1 结尾的期望。
最后的答案是
f
[
i
]
f[i]
f[i] 的和。
维护
f
[
i
]
f[i]
f[i] 需要维护
X
X
X。
定义
x
1
[
i
]
x1[i]
x1[i] 为前
i
i
i 个数,以
i
i
i 结尾,连续的
1
1
1 的期望个数。
定义
x
2
[
i
]
x2[i]
x2[i] 为前
i
i
i 个数,以
i
i
i 结尾,连续的
1
1
1 的平方的期望个数。
虽然意义不同,但最后
f
f
f,
x
1
x1
x1,
x
2
x2
x2 的转移方法类似。
类似题:WJMZBMR打osu! / Easy
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N = 1e5 + 10;
double x1[N] , x2[N] , f[N] , p[N] , ans;
int n;
signed main()
{
cin >> n;
for(int i = 1; i <= n ; i ++)
cin >> p[i];
for(int i = 1 ; i <= n ; i ++)
{
x1[i] = (x1[i - 1] + 1) * p[i];
x2[i] = (x2[i - 1] + 2 * x1[i - 1] + 1) * p[i];
f[i] = (3 * x2[i - 1] + 3 * x1[i - 1] + 1) * p[i];
ans = ans + f[i];
}
printf("%.1lf\n" , ans);
}
Two Frogs 概率DP
Two Frogs
题意:
河道里有 n 个荷叶排成一排,从第
i
i
i 个
(
i
<
n
)
(i<n)
(i<n) 荷叶出发可以跳到第
(
i
,
i
+
a
i
]
(i,i+a_i]
(i,i+ai] 个荷叶上,有两只青蛙从第
1
1
1 个荷叶出发,每一步都独立地等概率随机地跳向后边的荷叶,求两只青蛙以相同步数到达第
n
n
n 个荷叶的概率。
所有数据都不超过
8000
8000
8000 。
笔记:
首先注意到这是一道概率,而不是期望。
一开始当作期望做转移的时候每次次数 +
1
1
1了。
对于求概率只需要把最初状态的概率设为
1
1
1 正确转移即可。
做法:
看到数据范围可以知道最后可以把 “相同步数” 的方案划分成 1 ~
n
n
n 步,然后分开求,最后累加概率作为答案。
考虑每一次跳跃的答案的贡献,就如同题意所说:从第
i
i
i 个
(
i
<
n
)
(i<n)
(i<n) 荷叶出发可以跳到第
(
i
,
i
+
a
i
]
(i,i+a_i]
(i,i+ai] 个荷叶上。所以
i
i
i 这个状态就可以转移到
(
i
,
i
+
a
i
]
(i,i+a_i]
(i,i+ai] 的若干状态。
所以综上两点,加上数据范围暗示,我们可以定义两位状态数组:
d
p
[
i
]
[
j
]
dp[i][j]
dp[i][j] 表示身处
i
i
i 这个荷叶,花费了我
j
j
j 步的概率,最后枚举
j
j
j 作为答案。
考虑转移:如题意所述,显然第一维可以倒推至
i
=
1
i=1
i=1 为止,此时枚举
j
j
j 作为答案。对于第二维,如果我需要
j
j
j 步到达
1
1
1 号荷叶,说明我从
(
1
,
1
+
a
1
]
(1,1+a_1]
(1,1+a1] 的某个位置的
j
−
1
j-1
j−1 步转移过来,所以第二维顺序枚举步数即可:
d
p
[
i
]
[
k
]
=
1
a
i
(
d
p
[
i
+
1
]
[
k
−
1
]
+
d
p
[
i
+
2
]
[
k
−
1
]
+
.
.
.
.
.
.
+
d
p
[
i
+
a
i
]
[
k
−
1
]
)
dp[i][k]=\dfrac{1}{a_i}(dp[i+1][k-1]+dp[i+2][k-1]+......+dp[i+a_i][k-1])
dp[i][k]=ai1(dp[i+1][k−1]+dp[i+2][k−1]+......+dp[i+ai][k−1])
对于中间的一段权值
s
u
m
=
(
d
p
[
i
+
1
]
[
k
−
1
]
+
d
p
[
i
+
2
]
[
k
−
1
]
+
.
.
.
.
.
.
+
d
p
[
i
+
a
i
]
[
k
−
1
]
)
sum=(dp[i+1][k-1]+dp[i+2][k-1]+......+dp[i+a_i][k-1])
sum=(dp[i+1][k−1]+dp[i+2][k−1]+......+dp[i+ai][k−1]),可以通过前缀和
O
1
O_1
O1得到,在用的时候直接转移即可。
补充:
这道题除了起点
d
p
[
n
]
[
0
]
=
1
dp[n][0]=1
dp[n][0]=1 表示在
n
n
n 号点跳了
0
0
0 次的概率为
1
1
1 以外,前缀和也需要提前处理
k
=
0
k=0
k=0 这一维,因为在转移的时候需要用到
k
=
0
k=0
k=0 的前缀和。
#include<bits/stdc++.h>
using namespace std;
const int N = 8010;
typedef long long LL;
const int mod = 998244353;
int ans , b[N];
int n , a[N];
int dp[N][N] , sum[N][N];
LL qmi(LL a , int k)
{
int res = 1;
while(k)
{
if(k & 1) res = (res * a) % mod;
a = a * a % mod;
k >>= 1;
}
return res;
}
signed main()
{
cin >> n;
for(int i = 1 ; i <= n - 1 ; i ++)
{
scanf("%lld" , &a[i]);
}
for(int i = 1 ; i <= n ; i ++)
{
b[i] = qmi(i , mod - 2);
sum[i][0] = 1;
}
dp[n][0] = 1;
for(int i = n - 1 ; i >= 1 ; i --)
{
for(int k = 1 ; k <= n - 1 ; k ++)
{
dp[i][k] = ((LL) b[a[i]] * (sum[i + 1][k - 1] - sum[i + a[i] + 1][k - 1])) % mod;
sum[i][k] = (sum[i + 1][k] + dp[i][k]) % mod;
}
}
for(int i = 1 ; i <= n - 1 ; i ++)
{
ans = (ans + (LL)dp[1][i] * dp[1][i]) % mod; // 两只青蛙概率是独立的,乘法原理
}
printf("%lld\n" , ans);
}
收集邮票
收集邮票
题意:
有
n
n
n 种不同的邮票,皮皮想收集所有种类的邮票。唯一的收集方法是到同学凡凡那里购买,每次只能买一张,并且买到的邮票究竟是
n
n
n 种邮票中的哪一种是等概率的,概率均为
1
/
n
1/n
1/n。皮皮购买第
k
k
k 次邮票需要支付
k
k
k 元钱。
现在皮皮手中没有邮票,皮皮想知道自己得到所有种类的邮票需要花费的钱数目的期望。
输入格式
一行,一个数字 N
(
N
≤
10000
)
(N≤10000)
(N≤10000)。
题解笔记:
考虑每一次操作对答案的贡献。
发现每一次的贡献不好写出,因为每一次获得某一种类的邮票所花费的钱取决于我当前第几次购买邮票,这件事我们并不知道。
但是如果我们先笼统的定义
g
[
i
]
g[i]
g[i] 表示已经得到
i
i
i 类邮票的花费钱的期望,那么转移:
g
[
i
]
g[i]
g[i] =
n
−
i
n
(
g
[
i
+
1
]
+
▲)
+
i
n
(
g
[
i
]
+
▲)
\frac{n-i}{n}(g[i+1]+▲)+\frac{i}{n}(g[i]+▲)
nn−i(g[i+1]+▲)+ni(g[i]+▲)
这个转移的框架是很容易写出的,只是我们不知道怎么求当前的这一次买票对答案的贡献 ▲。
因为期望具有线性性质,所以期望是可以转移期望的,这也是概率DP的前提。
虽然我们不知道获得
i
i
i 种邮票会发生在我当前第几次购买邮票,但我们完全可以定义一个期望值:
f
[
i
]
f[i]
f[i] 表示获得
i
i
i 种邮票会发生第几次购买邮票的期望次数。
g
[
i
]
g[i]
g[i] 表示获得
i
i
i 种邮票花费钱的期望——即答案。
这样我们可以发现
f
[
i
]
f[i]
f[i] 可以辅助
g
[
i
]
g[i]
g[i] 算出当前的贡献 ▲。
考虑具体转移过程:
f
[
i
]
f[i]
f[i] =
n
−
i
n
(
f
[
i
+
1
]
+
1
)
+
i
n
(
f
[
i
]
+
1
)
\frac{n-i}{n}(f[i+1]+1)+\frac{i}{n}(f[i]+1)
nn−i(f[i+1]+1)+ni(f[i]+1)
因为不论发生哪一种概率,通往哪一种情况,都会使得我买邮票的操作次数 + 1。
g
[
i
]
g[i]
g[i] =
n
−
i
n
(
g
[
i
+
1
]
+
f
[
i
+
1
]
+
1
)
+
i
n
(
g
[
i
]
+
f
[
i
]
+
1
)
\frac{n-i}{n}(g[i+1]+f[i+1]+1)+\frac{i}{n}(g[i]+f[i]+1)
nn−i(g[i+1]+f[i+1]+1)+ni(g[i]+f[i]+1)
我们来理解一下
g
[
i
]
g[i]
g[i] 的贡献 ▲ 是怎么得到的。
假设当前抽到了没有的一张邮票,那么当前的贡献
f
[
i
+
1
]
+
1
f[i+1]+1
f[i+1]+1 表示的就是获得
i
+
1
i+1
i+1 张邮票的期望次数,加上自己的这一次买票的次数,所以当前次数为
f
[
i
+
1
]
+
1
f[i+1]+1
f[i+1]+1 , 花费了
f
[
i
+
1
]
+
1
f[i+1]+1
f[i+1]+1 元钱。
假设当前抽到了已经拥有的一张邮票,那么当前的贡献
f
[
i
]
+
1
f[i]+1
f[i]+1 表示的就是我获得当前
i
i
i 张邮票的期望次数,加上自己浪费的这一次买票的次数,所以当前次数
f
[
i
]
+
1
f[i]+1
f[i]+1 表示为花费了
f
[
i
]
+
1
f[i]+1
f[i]+1 元钱。
#include<bits/stdc++.h>
using namespace std;
const int N = 1e5 + 10;
using namespace std;
double f[N] , g[N];
double n ;
signed main()
{
cin >> n;
for(int i = n - 1 ; i >= 0 ; i --)
{
f[i] = ((n - i) * f[i + 1] + n) / (n - i);
g[i] = ((n - i) / n * (g[i + 1] + f[i + 1] + 1) + (i / n) * (f[i] + 1)) / (1 - (i / n));
}
printf("%.2lf\n" , g[0]);
}
2021 ccpc 哈尔滨 G. Damaged Bicycle__状压 + 期望dp
题意:
你从
1
1
1 号点出发,步行速度
t
m
/
s
t_{m/s}
tm/s,学校设立了
k
k
k 个自行车点,你可以去
A
[
i
]
A[i]
A[i] 号自行车站点骑上某一个自行车,骑行速度
r
m
/
s
r_{m/s}
rm/s,但是自行车有
p
[
i
]
p[i]
p[i] 的概率是坏的,你需要找到最优的策略使得到
n
n
n 点的期望路程最短,到不了输出
−
1
-1
−1。
图是常规读入
n
n
n 点,
m
m
m条边,双向有边权。
数据范围:
1
≤
t
≤
r
≤
1
0
4
,
1
≤
n
,
m
≤
1
0
5
,
1
≤
u
i
,
v
i
≤
n
,
1
≤
w
i
≤
1
0
4
,
0
≤
k
≤
18
1 \le t\le r\le10^4,1 \le n,m\le 10^5,1 \le u_i,v_i\le n,1 \le w_i\le 10^4,0 \le k \le 18
1≤t≤r≤104,1≤n,m≤105,1≤ui,vi≤n,1≤wi≤104,0≤k≤18
1
≤
a
i
≤
n
,
0
≤
p
i
≤
100
1 \le a_i \le n,0 \le p_i \le 100
1≤ai≤n,0≤pi≤100
笔记:
数据范围考虑状压。
可以
d
f
s
dfs
dfs 暴力枚举自行车的使用顺序,然后考虑状态之前的转移。
可以把起点当作一个永远坏的自行车站点。
题解:
下文 “站点” 指的是包括原点的自行车点。
因为站点不多,所以可以预处理出每一个站点到所有点的最短路。
定义
f
[
i
]
[
j
]
f[i][j]
f[i][j] 表示当前在
j
j
j 站点,经过的站点的集合为
i
i
i 的到达终点的期望路程,当然这里的经过是按最短距离走的。
因为最终答案是倒推的,类似于下文 奖励关 这道题,每一次的选择都是相当于往集合里添加元素(或运算),最后的空集一定是某个集合转移过来的,所以最终答案是
f
[
1
]
[
0
]
f[1][0]
f[1][0] ,表示当前只选了
1
1
1 号点的集合的答案,也就是考虑了所有情况的答案。
考虑转移:
对于每个站点都有两大类情况,
p
[
i
]
p[i]
p[i] 的概率对应【第一大类】,如果是坏的车子,接下来有可能及时止损,徒步去终点【第一A类】,或者是继续探索下一个站点【第一B类】。【第二大类】是
1
−
p
[
i
]
1-p[i]
1−p[i] 的概率车子是好的,好的车子的话骑上直接走了,没什么别的情况。
对于每个站点遇到的决策分为两类:
Case1:【第一A类】+ 【第二大类】。
Case2:【第一B类】+ 【第二大类】。
Case1 :
d
i
s
t
[
n
o
w
]
[
n
]
/
t
∗
p
[
n
o
w
]
+
(
1
−
p
[
n
o
w
]
)
∗
d
i
s
t
[
n
o
w
]
[
n
]
/
r
dist[now][n] / t * p[now] + (1 - p[now]) * dist[now][n] / r
dist[now][n]/t∗p[now]+(1−p[now])∗dist[now][n]/r
及时止损 + 车子是好的。
Case2:
(
1.0
−
p
[
n
o
w
]
)
∗
(
d
i
s
t
[
n
o
w
]
[
n
]
/
r
)
+
p
[
n
o
w
]
∗
(
d
i
s
t
[
n
o
w
]
[
A
[
i
]
]
/
t
+
d
f
s
(
(
s
t
a
∣
(
1
<
<
i
)
)
,
i
)
)
(1.0 - p[now]) * (dist[now][n] / r) + p[now] * (dist[now][A[i]] / t + dfs((sta | (1 << i)) , i))
(1.0−p[now])∗(dist[now][n]/r)+p[now]∗(dist[now][A[i]]/t+dfs((sta∣(1<<i)),i))
车子是好的 + 探索下一个站点。
对于每个点的
f
[
i
]
[
j
]
f[i][j]
f[i][j] 值就是
m
i
n
(
c
a
s
e
1
,
c
a
s
e
2
)
min(case1,case2)
min(case1,case2) 。
#include<bits/stdc++.h>
using namespace std;
typedef pair<long double,int> PII;
const int N = 1e6 + 10;
const int M = 1e6 + 10;
int n , m ;
int k , A[N * 2] , ne[N * 2] , e[N * 2] , idx , h[N * 2] , st[22][N] , q;
double dist[22][N] , p[N] , t , r , w[N] , ans , f[M][22];
void add(int a , int b , double c)
{
e[idx] = b , ne[idx] = h[a] , w[idx] = c, h[a] = idx ++;
}
void djst(int u , int id)
{
for(int i = 0 ; i <= n ; i ++)
{
dist[id][i] = 1e18; st[id][i] = 0;
}
dist[id][u] = 0;
priority_queue<PII,vector<PII>,greater<PII>> heap;
heap.push({0 , u});
while(heap.size())
{
auto t = heap.top();
heap.pop();
int ver = t.second;
if(st[id][ver]) continue;
st[id][ver] = 1;
for(int i = h[ver] ; ~ i ; i = ne[i])
{
int j = e[i];
if(dist[id][j] > dist[id][ver] + w[i])
{
dist[id][j] = dist[id][ver] + w[i];
heap.push({dist[id][j] , j});
}
}
}
}
double dfs(int sta , int now)
{
if(f[sta][now] != -1) return f[sta][now];
double tmp = dist[now][n] / t * p[now] + (1 - p[now]) * dist[now][n] / r;
for(int i = 1 ; i <= q ; i ++)
{
int now2 = sta >> i & 1;
if(now2 == 1) continue;
tmp = min(tmp , (1.0 - p[now]) * (dist[now][n] / r) + p[now] * (dist[now][A[i]] / t + dfs((sta | (1 << i)) , i)));
}
f[sta][now] = tmp;
return f[sta][now];
}
signed main()
{
memset(h, -1, sizeof h);
cin >> t >> r;
cin >> n >> m;
while (m -- )
{
int a , b;
double c;
scanf("%d %d %lf" , &a , &b , &c);
add(a , b , c);
add(b , a , c);
}
cin >> q;
A[0] = 1;
p[0] = 1;
for(int i = 1 ; i <= q ; i ++)
{
scanf("%d %lf" , &A[i] , &p[i]);
p[i] = p[i] / 100.0;
}
for(int i = 0 ; i <= q ; i ++)
{
djst(A[i] , i);
}
double inf = 1e18;
if(dist[0][n] > (inf) / 2.0)
{
cout << -1 << endl;
return 0;
}
double sum = inf;
for(int i = 0 ; i <= M - 5; i ++)
{
for(int j = 0 ; j <= 20 ; j ++)
{
f[i][j] = -1;
}
}
dfs(1 , 0);
printf("%lf\n" , f[1][0]);
}
P2473 [SCOI2008] 奖励关__状压 + 期望dp
奖励关
题意: 读入一个
n
n
n,和
k
k
k,代表从
k
k
k 类宝物中等概率的抛出
n
n
n 轮宝物,如果你在第
i
i
i 轮接收了这个宝物,就会获得
p
[
i
]
p[i]
p[i] 的分值,接下来
k
k
k 行,读入
p
[
i
]
p[i]
p[i] 以及每一个物品的若干个前驱,必须先选完若干个它的前驱物品,才能选第
i
i
i 个物品。求按最优策略平均情况能得多少分(
0
0
0 代表读入结束)。
输入
1 2
1 0
2 0
输出
1.500000
1
≤
n
≤
100
,
1
≤
k
≤
15
,
−
1
0
6
≤
p
i
≤
1
0
6
1≤n≤100, 1≤k≤15,−10^6≤pi≤10^6
1≤n≤100,1≤k≤15,−106≤pi≤106
题解笔记: 首先考虑到等概率抛出物品可以知道是一道概率DP,数据范围暗示需要考虑状压。
首先考虑第
i
i
i 轮的物品对答案的贡献。
发现第
i
i
i 轮的贡献可能是直接选取,也可能是为将来最优物品做前驱来选取。
也就是说这个转移需要考虑未来的最优状况,而并不只取决于前
i
i
i 轮的最优决策。
既然前
i
i
i 轮的选取不能决定第
i
i
i 轮的情况,那么前
i
i
i 轮能决定的是什么?
显然是前驱,也就是说只有前
i
i
i 轮选了
i
i
i 的前驱才能考虑选择
i
i
i 这个物品,而物品是否选择取决于未来
i
+
1
i+1
i+1 的最优解。
基于以上两点我们定义:
f
[
i
]
[
j
]
f[i][j]
f[i][j] 为从
1
——
(
i
−
1
)
1——(i-1)
1——(i−1) 轮选择了
j
j
j 这个集合,以
j
j
j 为前驱决策从
i
——
n
i——n
i——n 轮最优得分。
f
[
i
]
[
j
]
+
=
m
a
x
(
f
[
i
+
1
]
[
j
]
,
f
[
i
+
1
]
[
(
j
∣
(
1
<
<
(
c
−
1
)
)
)
]
+
p
[
c
]
)
;
f[i][j] += max(f[i + 1][j] , f[i + 1][(j | (1 << (c - 1)))] + p[c]);
f[i][j]+=max(f[i+1][j],f[i+1][(j∣(1<<(c−1)))]+p[c]);
对于选取当前物品,转移的时候,从
j
∣
(
1
<
<
c
)
j | (1 << c)
j∣(1<<c) ,转移过来(
c
c
c 枚举物品)。
对于不选取当前物品,
j
j
j 不变,从
i
+
1
i+1
i+1 轮更新过来,两种决策取最大值。
最后
j
=
0
j=0
j=0 的时候,选择的物品一定是合法的方案得到的分值,此时作为答案输出。
其实正序考虑的时候也是因为无法保证当前的状态
j
j
j 一定可以由之前抛出的宝物得到,而逆序可以。
每一次决策考虑平均情况,
f
[
i
]
[
j
]
/
=
k
f[i][j]/=k
f[i][j]/=k。
#include<bits/stdc++.h>
using namespace std;
const int N = 105;
int k , n , num , need[N] , p[N];
double f[N][(1 << 21)];
signed main()
{
cin >> n >> k;
for(int i = 1 ; i <= k ; i ++)
{
cin >> p[i];
while(cin >> num)
{
if(num == 0) break;
need[i] = (need[i] | (1 << (num - 1)));
}
}
for(int i = n ; i >= 1 ; i --)
{
for(int j = 0 ; j < (1 << k) ; j ++)
{
for(int c = 1 ; c <= k ; c ++)
{
if((need[c] & j) == need[c])
{
f[i][j] += max(f[i + 1][j] , f[i + 1][(j | (1 << (c - 1)))] + p[c]);
}
else
{
f[i][j] += f[i + 1][j];
}
}
f[i][j] /= k;
}
}
printf("%.6lf\n" , f[1][0]);
}
概率充电器
题意:
n
n
n 个充电元件,每个原件有
q
i
q_i
qi % 的概率自动成为电源,然后每个元件会各自沿着它们的导线
i
i
i 以
p
i
p_i
pi % 的概率传向相邻的点间接充电,求得到充电的元件个数期望。
输入:
第一行
n
n
n 。
接下来
n
−
1
n-1
n−1 行,每行有
点
a
,点
b
,概率
p
i
点a,点b,概率p_i
点a,点b,概率pi %。
最后一行
n
n
n 个数字 , 代表第
i
i
i 个点的自动成为电源的概率
q
i
q_i
qi %。
5
1 2 90
1 3 80
1 4 70
1 5 60
100 10 20 30 40
笔记::
一道概率的题,首先
n
−
1
n-1
n−1 条边描述了一课树,而且不难发现这道题期望即概率。
1.虽然树是有根的,但是在充电的过程是没有根的,树中的某一个点可能被自己的子树的点充电也有可能被祖先节点充电,也有可能自己成为了电源而带电。
2.一开始只考虑简单的概率做差来转移父亲和儿子之间的概率,显然这种简单的换根DP不准确。
题解:
首先说明这道题要用到一个简单的 容斥原理,这道题的 DP 转移基于这个小推论。
假设发生事件
A
A
A 的概率为
P
(
A
)
P(A)
P(A), 发生事件
B
B
B 的概率为
P
(
B
)
P(B)
P(B) ,那么发生
A
,
B
A,B
A,B 中至少一件事的概率为
P
(
A
)
+
P
(
B
)
−
P
(
A
)
∗
P
(
B
)
P(A) + P(B) - P(A) * P(B)
P(A)+P(B)−P(A)∗P(B)。
这道题的第一步要想到 笔记1 所说的三种情况,即一个元件带电的原因是哪三个?
如果
u
u
u 这个元件带电了,那么假设先把带电的原因仅仅划分为两部分,比如说: 自己本身带电 为事件
A
A
A ,儿子节点带电传导上来 为事件
B
B
B ,那么我们可以先通过这个小推论算出这个元件由这两种情况带电的概率。
具体来说:假设当前考虑的节点为
u
u
u ,
u
u
u 的儿子节点为
j
j
j,每个点初始的期望
d
p
[
u
]
=
q
i
dp[u]=q_i
dp[u]=qi 即电源概率。
P
(
A
)
=
d
p
u
P(A)=dp_u
P(A)=dpu,
P
(
B
)
=
w
i
∗
d
p
j
P(B)=w_i*dp_j
P(B)=wi∗dpj,
d
p
u
=
P
(
A
)
+
P
(
B
)
−
P
(
A
)
∗
P
(
B
)
dp_u=P(A) + P(B) - P(A) * P(B)
dpu=P(A)+P(B)−P(A)∗P(B)。
因为这个推论每次划分两个事件就可以,所以对于
u
u
u 的每一个儿子都可以用上式连续的更新
d
p
[
u
]
dp[u]
dp[u] 的值。
现在的
d
p
[
u
]
dp[u]
dp[u] 考虑了两种情况,还差祖先节点传导的情况,这个时候继续小推论。
假设当前考虑的节点为
j
j
j ,
j
j
j 的父亲节点为
u
u
u,正在二次换根更新
d
p
[
j
]
dp[j]
dp[j] 的值,使得
d
p
[
j
]
dp[j]
dp[j] 为考虑了三种情况的概率
a
n
s
j
ans_j
ansj,显然最后的答案为
∑
j
=
1
n
a
n
s
j
\displaystyle\sum_{j=1}^{n}ans_j
j=1∑nansj。
此时类似于之前的情况,把事件划分成
P
(
A
)
=
d
p
[
u
]
P(A)=dp[u]
P(A)=dp[u] 代表之前的情况,
P
(
B
)
=
🔺
P(B)=🔺
P(B)=🔺,这个🔺表示刨去
j
j
j 的子树,由
j
j
j 的父亲节点
u
u
u (包括
u
u
u ) 之外的点传导至
j
j
j 点的期望。
如果有了这个
🔺
🔺
🔺,那么我们就可以求出
d
p
j
=
P
(
A
)
+
P
(
B
)
−
P
(
A
)
∗
P
(
B
)
dp_j=P(A) + P(B) - P(A) * P(B)
dpj=P(A)+P(B)−P(A)∗P(B),也就是所谓的
a
n
s
j
ans_j
ansj 了。
求
🔺
🔺
🔺 就属于换根DP的思维了,首先考虑
d
p
[
u
]
dp[u]
dp[u] 这个合法状态是如何构成的,以及需要哪一部分来更新
d
p
[
j
]
dp[j]
dp[j] 使之合法。
突然发现,我们刚刚求
d
p
[
j
]
dp[j]
dp[j] 三种情况合并的那个
d
p
j
=
P
(
A
)
+
P
(
B
)
−
P
(
A
)
∗
P
(
B
)
dp_j=P(A) + P(B) - P(A) * P(B)
dpj=P(A)+P(B)−P(A)∗P(B) 也可以表示
u
u
u 的状态构成。
那么我们换成
u
u
u 的值,
d
p
[
u
]
=
P
(
A
)
+
P
(
B
)
−
P
(
A
)
∗
P
(
B
)
dp[u]=P(A) + P(B) - P(A) * P(B)
dp[u]=P(A)+P(B)−P(A)∗P(B),
P
(
A
)
P(A)
P(A) 表示子树
j
j
j 带来的贡献,此时解方程可以得到
P
(
B
)
=
(
d
p
[
u
]
−
P
(
A
)
)
/
(
1
−
P
(
A
)
)
P(B)=(dp[u]-P(A))/(1-P(A))
P(B)=(dp[u]−P(A))/(1−P(A)),表示 刨去
u
u
u 的子树
j
j
j 的贡献,
u
u
u 的祖先节点(包括
u
u
u 成为电源)及其之外的点传导至
u
u
u 点的期望。
除法小心分母为
0
0
0 ,分母为
0
0
0 表示
j
j
j 这个点百分百有电,
c
o
n
t
i
n
u
e
continue
continue 就行。
那么我们假设要求的
🔺
🔺
🔺 的值为
u
p
up
up,那么
u
p
=
P
(
B
)
up=P(B)
up=P(B),
d
o
w
n
=
P
(
A
)
=
w
[
i
]
∗
d
p
[
j
]
down=P(A)=w[i]*dp[j]
down=P(A)=w[i]∗dp[j] 。
此时我们对
d
p
[
j
]
dp[j]
dp[j] 和
🔺
🔺
🔺 最后再来一次小容斥,
d
p
[
j
]
=
(
d
p
[
j
]
+
u
p
∗
w
[
i
]
−
u
p
∗
w
[
i
]
∗
d
p
[
j
]
)
;
dp[j] = (dp[j] + up * w[i] - up * w[i] * dp[j]);
dp[j]=(dp[j]+up∗w[i]−up∗w[i]∗dp[j]);
这里注意真实的
u
p
=
u
p
∗
w
[
i
]
up=up*w[i]
up=up∗w[i] ,一开始我写代码漏掉了,就是说外边的贡献乘上边权才是外边的点到这个点真正的贡献,不然这个从
u
u
u 来的贡献
🔺
🔺
🔺 还没经过这条边没法对
j
j
j 点做贡献。
总结:
这道题的状态很多,而且有一些容斥的知识使得转移比较复杂,虽然多,但是每一步都是很简单的,慢慢推导没有很难理解的点,加油!
代码:
#include <bits/stdc++.h>
#define int long long
#define double long double
#define endl '\n'
using namespace std;
const int N = 1e6 + 10;
int ne[N] , f[N] , e[N] , idx , h[N];
double dp[N] , a[N] , w[N] , ans;
int n , m;
void add(int a , int b , double c)
{
e[idx] = b , ne[idx] = h[a] , w[idx] = c , h[a] = idx ++;
}
double dfs1(int u , int father)
{
for(int i = h[u] ; ~ i ; i = ne[i])
{
int j = e[i];
if(j == father) continue;
dfs1(j , u);
double down = w[i] * dp[j];
dp[u] = (dp[u] + down - dp[u] * down);
}
return dp[u];
}
double dfs2(int u , int father)
{
ans = (ans + dp[u]);
for(int i = h[u] ; ~ i ; i = ne[i])
{
int j = e[i];
if(j == father) continue;
double down = w[i] * dp[j];
if((1.0 - down) == 0)
{
dfs2(j , u); continue;
}
else
{
double up = (dp[u] - down) / (1.0 - down);
dp[j] = (dp[j] + up * w[i] - up * w[i] * dp[j]);
dfs2(j , u);
}
}
}
signed main()
{
memset(h , -1 , sizeof h);
cin >> n;
for(int i = 1 ; i < n ; i ++)
{
int a , b ;
double w;
cin >> a >> b >> w;
w = w / 100.0;
add(a , b , w);
add(b , a , w);
}
for(int i = 1 ; i <= n ; i ++)
{
cin >> dp[i];
dp[i] = dp[i] / 100.0;
}
dfs1(1 , -1);
dfs2(1 , -1);
printf("%.6Lf\n" , ans);
}