先来看一道例题:
【HNOI2013】游走
一个无向连通图,顶点从1编号到N,边从1编号到M。
小Z在该图上进行随机游走,初始时小Z在1号顶点,每一步小Z以相等的概率随机选 择当前顶点的某条边,沿着这条边走到下一个顶点,获得等于这条边的编号的分数。当小Z 到达N号顶点时游走结束,总分为所有获得的分数之和。
现在,请你对这M条边进行编号,使得小Z获得的总分的期望值最小。
【分析】
设
d
[
i
]
d[i]
d[i]为走上
i
i
i号边的期望次数,则对
d
[
i
]
d[i]
d[i]从小到大排序后,有:
a
n
s
=
∑
i
=
1
m
d
[
i
]
∗
(
m
−
i
+
1
)
ans=\sum_{i=1}^md[i]*(m-i+1)
ans=i=1∑md[i]∗(m−i+1)
关键是求
d
[
i
]
d[i]
d[i]。我们发现,通过一条边的次数与到达两顶点的有关。于是,设
f
[
i
]
f[i]
f[i]为通过
i
i
i号点的期望次数,则(
d
[
i
]
d[i]
d[i]为
i
i
i的度数):
f
[
i
]
=
∑
j
∈
s
o
n
[
i
]
f
[
j
]
d
[
j
]
+
1
f[i]=\sum_{j \in son[i] } \frac{f[j]}{d[j]}+1
f[i]=j∈son[i]∑d[j]f[j]+1
然而,由于图有环,转移顺序并不明确。所以说,我们把它当做一般的方程看待,其中
f
[
i
]
f[i]
f[i]、
f
[
j
]
f[j]
f[j]为未知数。
对应每一个
i
∈
[
1
,
n
−
1
]
i \in [1,n-1]
i∈[1,n−1],都有上面的方程。这样,我们就得到了
n
−
1
n-1
n−1个方程组。
方程组?那就拿高斯消元乱搞一下不就好了?!
【代码】
#include<cstdio>
#include<cstring>
#include<cmath>
#include<algorithm>
#include<vector>
using namespace std;
const int mn = 505;
const double eps = 1e-6;
vector<int > g[mn];
double f[mn][mn], d[mn], b[mn];
struct edge{
int a, b;
double val;
bool operator <(const edge x) const {return val < x.val;}
}e[mn * mn];
inline int dcmp(double x)
{
if(fabs(x) < eps) return 0;
return x < 0 ? -1 : 1;
}
inline void gauss(int n)
{
for(int i=1;i<=n;++i) {
int p=i;
for(int k=i+1;k<=n;++k) if(fabs(f[k][i])>fabs(f[p][i])) p=k;
if(i!=p)
{
swap(b[i], b[p]);
for(int j = 1; j <= n; j++)
swap(f[i][j], f[p][j]);
}
for(int k=i+1;k<=n;++k) {
double h=f[k][i]/f[i][i];
b[k]-=h*b[i];
for(int j=i;j<=n;++j) f[k][j]-=h*f[i][j];
}
}
for(int i=n;i>=1;--i) {
for(int j=i+1;j<=n;++j) b[i]-=d[j]*f[i][j];
d[i]=b[i]/f[i][i];
}
}
int main()
{
int n, m, i, j, x, y;
scanf("%d%d", &n, &m);
for(i = 1; i <= m; i++)
scanf("%d%d", &x, &y), g[x].push_back(y), g[y].push_back(x), e[i] = (edge) {x, y, 0};
for(i = 1; i < n; i++)
{
f[i][i] = 1.0;
int siz = g[i].size();
for(j = 0; j < siz; j++)
{
int to = g[i][j];
if(to != n) f[i][to] = -1.0 / (double)(g[to].size());
}
}
b[1] = 1, gauss(n - 1);
for(i = 1; i <= m; i++)
e[i].val = d[e[i].a] / (double)(g[e[i].a].size()) + d[e[i].b] / (double)(g[e[i].b].size());
sort(e + 1, e + 1 + m);
double ans = 0;
for(i = 1; i <= m; i++)
ans += (m - i + 1) * e[i].val;
printf("%.3lf\n", ans);
}
【hdu4035】Maze
给定一棵树,每一个节点有两个值 k i k_i ki和 e i e_i ei,表示在i号节点,有 k i % k_i\% ki%的概率被传送至节点1,有 e i % e_i\% ei%的概率走出这棵树。现在有一个人从1号节点出发,每到一个节点 i i i,他会随机选择一条相邻边走,求他走出这棵树期望需要多少步。
【分析】
不难想到设
f
[
i
]
f[i]
f[i]表示从
i
i
i出发走出迷宫的期望步数,则(
d
[
i
]
d[i]
d[i]表示
i
i
i节点的度数):
f
[
i
]
=
∑
j
∈
s
o
n
[
i
]
(
f
[
j
]
+
1
)
∗
(
1
−
k
[
i
]
−
e
[
i
]
)
d
[
i
]
+
(
f
[
f
a
[
i
]
]
+
1
)
∗
(
1
−
k
[
i
]
−
e
[
i
]
)
d
[
i
]
+
e
[
i
]
∗
0
+
k
[
i
]
∗
f
[
1
]
f[i]=\sum_{j \in son[i]}(f[j]+1)*\frac{(1-k[i]-e[i])}{d[i]} + (f[fa[i]]+1)*\frac{(1-k[i]-e[i])}{d[i]}+e[i]*0+k[i]*f[1]
f[i]=j∈son[i]∑(f[j]+1)∗d[i](1−k[i]−e[i])+(f[fa[i]]+1)∗d[i](1−k[i]−e[i])+e[i]∗0+k[i]∗f[1]
由于他可以随便乱走,所以我们依旧无法确定状态的转移顺序。所以如果不再变形,则只能用高斯消元。
然而,这道题的规模不允许我们这样搞(
n
≤
10000
n \leq 10000
n≤10000)。所以我们得在这个方程上搞些事情。
若按照树形dp的方式做,那么
f
[
f
a
[
i
]
]
f[fa[i]]
f[fa[i]]为未知状态,而
f
[
j
]
,
j
∈
s
o
n
[
i
]
f[j], j \in son[i]
f[j],j∈son[i]为已知状态。所以,我们设:
f
[
i
]
=
a
[
i
]
∗
f
[
f
a
[
i
]
]
+
b
[
i
]
∗
f
[
1
]
+
c
[
i
]
f[i]=a[i]*f[fa[i]]+b[i]*f[1]+c[i]
f[i]=a[i]∗f[fa[i]]+b[i]∗f[1]+c[i]
将i换为
j
,
j
∈
s
o
n
[
i
]
j,j \in son[i]
j,j∈son[i],令
p
[
i
]
=
(
1
−
k
[
i
]
−
e
[
i
]
)
d
[
i
]
p[i]=\frac{(1-k[i]-e[i])}{d[i]}
p[i]=d[i](1−k[i]−e[i]),代入原方程:
f
[
i
]
=
∑
j
∈
s
o
n
[
i
]
(
a
[
j
]
∗
f
[
i
]
+
b
[
j
]
∗
f
[
1
]
+
c
[
j
]
+
1
)
∗
p
[
i
]
+
(
f
[
f
a
[
i
]
]
+
1
)
∗
p
[
i
]
+
f
[
1
]
∗
k
[
i
]
f[i]=\sum_{j \in son[i]}(a[j]*f[i]+b[j]*f[1]+c[j]+1)*p[i]+(f[fa[i]]+1)*p[i]+f[1]*k[i]
f[i]=j∈son[i]∑(a[j]∗f[i]+b[j]∗f[1]+c[j]+1)∗p[i]+(f[fa[i]]+1)∗p[i]+f[1]∗k[i]
令
t
[
i
]
=
1
−
∑
j
∈
s
o
n
[
i
]
a
[
j
]
∗
p
[
i
]
t[i]=1-\sum_{j \in son[i]}a[j]*p[i]
t[i]=1−∑j∈son[i]a[j]∗p[i],整理得:
f
[
i
]
=
p
[
i
]
t
[
i
]
f
[
f
a
[
i
]
]
+
∑
j
∈
s
o
n
[
i
]
b
[
j
]
∗
p
[
i
]
+
k
[
i
]
t
[
i
]
∗
f
[
1
]
+
∑
j
∈
s
o
n
[
i
]
c
[
j
]
∗
p
[
i
]
+
p
[
i
]
t
[
i
]
f[i]=\frac{p[i]}{t[i]}f[fa[i]]+\frac{\sum_{j \in son[i]}b[j]*p[i] + k[i]}{t[i]}*f[1] + \frac{\sum_{j \in son[i]} c[j]*p[i]+p[i]}{t[i]}
f[i]=t[i]p[i]f[fa[i]]+t[i]∑j∈son[i]b[j]∗p[i]+k[i]∗f[1]+t[i]∑j∈son[i]c[j]∗p[i]+p[i]
所以:
{
a
[
i
]
=
p
[
i
]
t
[
i
]
b
[
i
]
=
∑
j
∈
s
o
n
[
i
]
b
[
j
]
∗
p
[
i
]
+
k
[
i
]
t
[
i
]
c
[
i
]
=
∑
j
∈
s
o
n
[
i
]
c
[
j
]
∗
p
[
i
]
+
p
[
i
]
t
[
i
]
\begin{cases} a[i]=\frac{p[i]}{t[i]}\\ b[i]=\frac{\sum_{j \in son[i]}b[j]*p[i] + k[i]}{t[i]}\\ c[i]=\frac{\sum_{j \in son[i]} c[j]*p[i]+p[i]}{t[i]} \end{cases}
⎩⎪⎪⎨⎪⎪⎧a[i]=t[i]p[i]b[i]=t[i]∑j∈son[i]b[j]∗p[i]+k[i]c[i]=t[i]∑j∈son[i]c[j]∗p[i]+p[i]
一次树形dp即可把a、b、c求出来。答案为:
f
[
1
]
=
b
[
1
]
∗
f
[
1
]
+
c
[
1
]
⟺
f
[
1
]
=
c
[
1
]
1
−
b
[
1
]
f[1]=b[1]*f[1]+c[1] \Longleftrightarrow f[1]=\frac{c[1]}{1-b[1]}
f[1]=b[1]∗f[1]+c[1]⟺f[1]=1−b[1]c[1]
如果计算过程中出现了除0的情况,输出“impossible”。(若向某一个方向走的概率达到了1,则不可能走出树)
【代码】
#include<cstdio>
#include<cstring>
#include<cmath>
#include<algorithm>
#include<vector>
using namespace std;
const int mn = 10005;
const double eps = 1e-10;
vector<int >g[mn];
double a[mn], b[mn], c[mn], e[mn], k[mn], p[mn];
int d[mn];
bool flag;
inline void dp(int s, int fa)
{
double tmp = 0;
a[s] = p[s], b[s] = k[s], c[s] = 1 - k[s] - e[s];
int m = g[s].size();
for(int i = 0; i < m; i++)
{
int to = g[s][i];
if(to != fa)
{
dp(to, s);
if(flag)
return;
tmp += a[to] * p[s], b[s] += b[to] * p[s], c[s] += c[to] * p[s];
}
}
tmp = 1.0 - tmp;
if(fabs(tmp) < eps)
{
flag = 1;
return;
}
a[s] /= tmp, b[s] /= tmp, c[s] /= tmp;
}
inline void init(int n)
{
for(int i = 1; i <= n; i++)
g[i].clear();
flag = 0;
}
int main()
{
int T, n, i, x, y;
scanf("%d", &T);
for(int tt = 1; tt <= T; tt++)
{
scanf("%d", &n), init(n);
for(i = 1; i < n; i++)
scanf("%d%d", &x, &y), g[x].push_back(y), g[y].push_back(x);
for(i = 1; i <= n; i++)
scanf("%lf%lf", &k[i], &e[i]), k[i] /= 100.0, e[i] /= 100.0, p[i] = 1 - k[i] - e[i];
for(i = 1; i <= n; i++)
d[i] = g[i].size();
for(i = 1; i <= n; i++)
p[i] /= (double)(d[i]);
dp(1, 0), printf("Case %d: ", tt);
if(flag || fabs(1 - b[1]) < eps)
puts("impossible");
else
printf("%.6lf\n", c[1] / (1 - b[1]));
}
}