题解 P2081 【[NOI2012]迷失游乐园】

emptysetvvvv

2019-02-02 18:24:54

Solution

## 背景 Update2022.2.22: 纪念我的首篇题解,虽然我已经是个失败者了,但是 zcysky 成我班助了!有空更张 zcysky 照片(逃 ## 思路 ### PART I 题意:给定带权的树或基环树,从随机一点出发走**每个点至多经过一次**的路径,问期望路径长度。 $son_u$ 表示 $u$ 的子结点数量;$fa_u$ 表示 $u$ 的父结点数量,取值为 $1$ 或 $2$(因为有环)。 $down_u$ 表示从 $u$ 出发,第一步向下走的期望路径长度,「下」指子结点方向即叶子方向; $up_u$ 表示从 $u$ 出发,第一步向上走的期望路径长度,「上」指父结点方向。 > 注意:只规定了第一步向上走,第二步允许去 $u$ 的父亲 $k$ 的其他子结点。 那么从 $u$ 出发的期望路径长度为: $$\large ans_u=\dfrac{down_u\cdot son_u+up_u\cdot fa_u}{son_u+fa_u}$$ ### PART II 先考虑 50pts 的普通树做法。 $down_u$ 显然只不依赖任何的 $up$ 值: $$\large down_u=\dfrac{1}{son_u}\sum_{v\text{ is son of }u}(down_v+w_{u,v})$$ $v$ 是 $u$ 的子结点,$w_{u,v}$ 表示边权。特别的,叶子结点 $down$ 值为 $0$。 对于普通树,$u$ 至多有一个父结点 $k$,而 $up_u$ 是要依赖 $up_k,down_k$ 的: $$\large up_u=w_{u,k}+\dfrac{up_k\cdot fa_k+down_k\cdot son_k-down_u-w_{u,k}}{fa_k+son_k-1}$$ 解释如下: 不论如何,第一步贡献 $w_{u,k}$。 粗略地看,继续向上走贡献 $up_k\cdot fa_k$,转而向下走贡献 $down_k\cdot son_k$,考虑到不能走回来,故应减去 $down_u+w_{u,k}$。 第二步向上有 $fa_k$ 种选择,向下有 $son_k-1$ 种选择(不允许回来),故分母为 $fa_k+son_k-1$。 特别的,若 $k$ 仅与 $u$ 相连即 $fa_k+son_k-1=0$,则 $up_u=w_{u,k}$,没有后一项。 实现时,计算 $up,down$ 值,由于前者依赖后者,而后者不依赖前者,故流程如下: 1. 从根节点开始,递归每个子结点计算 $down$ 值; 2. 递归至 $u$ 时,先递归子结点,得到每个 $down_v$,然后计算 $down_u$,返回; 3. 得到该普通树的 $down$ 值后,从根节点的每个子结点开始,递归计算 $up$ 值; 4. 递归至 $u$ 时,根据父结点 $k$ 的信息计算 $up_u$,然后递归子结点计算,返回; 5. 根据 $up_i,down_i$ 计算 $ans_i$,最终结果为 $\displaystyle\dfrac{1}{n}\sum_{i=1}^nans_i$。 ### PART III 再来考虑基环树的情况。 ![](https://cdn.luogu.com.cn/upload/image_hosting/1vtxcxat.png) > 如图所示是一棵并不优美的基环树,箭头指向父结点($1$ 或 $2$ 个),加粗结点是环上结点,以下简称「黑点」,其余结点简称「白点」。 我们将基环树看做**若干普通树根节点相连成环**的结果。 注意到无论从哪一点出发,第一步向下,活动范围始终在普通树内部,即:无论黑白,$down$ 值不变。 若 $u$ 是白点,只有唯一父亲 $k$,尽管此时 $fa_k$ 不一定为 $1$(白点 $u$ 的父亲 $k$ 是黑点时 $fa_k=2$),但显然考虑推导过程,$up_u$ 仍符合上文的公式。 若 $u$ 是黑点,则 $up_u$ 的计算显得和繁琐。 > $u$ 第一步去了另一个黑点,第二步可以选择继续在环上走,还可以选择钻入该黑点的子树。 为了方便处理,我们先通过深搜得到: - 黑点总数 $t$; - $\text{dfn[u]}$,维护 $u$ 是搜到的第几个黑点; - $\text{path[i]}$,维护第 $i(1\leqslant i\leqslant t)$ 个黑点的实际编号 $u(1\leqslant u\leqslant n)$; - $\text{disl[i],disr[i]}$,类似于链表的意味,维护第 $i$ 个黑点与左右相邻黑点的距离。 对于黑点 $u$,先规定第一步必须逆时针走,则有: $$\large up_u=\sum_{i}P_i\times \left(\dfrac{down_{\text{path[i]}}\cdot son_{\text{path[i]}}}{son_{\text{path[i]}}+1}+w\right)$$ 其中,$i$ 依次取 $\text{dfn[u]}+1,\text{dfn[u]}+2,\text{dfn[u]}+3,\ldots,t,1,2,3,\ldots\text{dfn[u]}-1$。 > 嗯,这个式子有失数学美感,但严谨的讲,的确是这个样子。不要紧,我们来看看这是什么意思。 根据期望的定义,期望等于**执行第 $k$ 步的概率与第 $k$ 步带来的贡献的乘积**的累加和。 先来看概率,$P_i$ 表示走到该点的概率,规定了第一步逆时针,即 $P_{\text{dfn[u]}+1}=1$。 第 $i$ 个黑点的下一个黑点是 $i\bmod t+1$,需要在第 $i$ 个黑点的 $son_{\text{path[i]}}$ 个子结点和下一个黑点之中选择下一步去哪里,那么 $P_{i\bmod t+1}=\dfrac{1}{son_{\text{path[i]}}+1}P_i$。 再来看从顺时针方向的上一个黑点走到第 $i$ 个黑点的贡献,首先有边权 $w$,即 $\text{disl[i]}$。 其次,我们需要考虑如果从此钻入第 $i$ 个黑点的子树产生的贡献,即 $\dfrac{down_{\text{path[i]}}\cdot son_{\text{path[i]}}}{son_{\text{path[i]}}+1}$。 特别的,如果第 $i$ 个黑点逆时针方向的下一个黑点就是 $u$ 的话,只能选择钻入子树而不能继续走重复的,那么分母就应该为 $son_{\text{path[i]}}$ 即钻入该子树的贡献直接就是 $down_{\text{path[i]}}$。 刚刚规定了第一步必须逆时针走,现在再规定第一步必须顺时针求一遍,最终给 $up$ 值除以 $2$ 即可。 > 当然,也可以在一开始就令 $P=\dfrac{1}{2}$,代码中也是这样实现的。 这很好理解,由于路径不重复,所以不可能顺逆时针交替,顺逆各求一遍一定包含了所有情况,只不过每个概率都放大了 $2$ 倍,现在除以 $2$ 即可。 理清逻辑关系,所有 $down$ 值只依赖其子结点的 $down$ 值,黑点的 $up$ 值依赖 $down$ 值,白点的 $up$ 值依赖黑点的 $up$ 值和 $down$ 值,故流程如下: 1. 从每个黑点即每个普通树的根节点开始,递归每个子结点计算 $down$ 值,方法同普通树; 2. 计算每个黑点的 $up$ 值,顺逆时针各求一遍,然后除以 $2$,如上文所示; 3. 从每个黑点的每个子结点开始,递归计算白点的 $up$ 值,方法同普通树; 4. 根据 $up_i,down_i$ 计算 $ans_i$,最终结果为 $\displaystyle\dfrac{1}{n}\sum_{i=1}^nans_i$。 ## 代码 > 带空格有注释,神犇请自行跳过。 实际上, $\varnothing$ 是一个蒻不禁风的被全机房吊着打的萌新,她的代码糟糕透了,特别是充实感紧凑美给人带来的视觉震撼程度都比 CK6100LGEV2 不知道差到哪里去了,她用了整整 100 行,而 CK6100LGEV2 只用了 60 行(逃)。 ```cpp #include <cstdio> const int maxn = 100005; int n, m, fa[maxn], son[maxn]; double up[maxn], down[maxn], ans; struct Edge { int to, next, w; Edge() {} Edge(int to, int next, int w): to(to), next(next), w(w) {} } e[maxn<<1]; int head[maxn], cnt; void add(int u, int v, int w) { e[++cnt] = Edge(v, head[u], w); head[u] = cnt; } #define v e[i].to int pos; bool vis[maxn], flag;//是否在环上 void dfs1(int u, int k) { vis[u] = true; for(int i = head[u]; i; i = e[i].next) if(v != k) { if(vis[v]) { pos = v; return; } dfs1(v, u); if(!flag and pos) { if(pos == u) flag = true; return; } if(flag) break; } vis[u] = false; }//将所有环上结点的 vis 标记为 true int t, disl[22], disr[22], dfn[maxn], path[22]; void dfs2(int u, int k) { dfn[u] = ++t, path[t] = u; fa[u] = 2; for(int i = head[u]; i; i = e[i].next) if(v != k and vis[v]) { if(!dfn[v]) dfs2(v, u); disr[dfn[u]] = disl[dfn[v]] = e[i].w; break; } }//处理环上信息 void dp_down(int u, int k) { for(int i = head[u]; i; i = e[i].next) if(v != k and !vis[v]) { fa[v] = 1; dp_down(v, u); son[u]++; down[u] += down[v] + e[i].w; } if(son[u]) down[u] /= son[u]; } void dp_up(int u, int k, int w) { up[u] = w; if(fa[k] + son[k] - 1) up[u] += (up[k]*fa[k]+down[k]*son[k]-down[u]-w) / (fa[k]+son[k]-1); for(int i = head[u]; i; i = e[i].next) if(v != k) dp_up(v, u, e[i].w); } void work1() { dp_down(1, 0); for(int i = head[1]; i; i = e[i].next) dp_up(v, 1, e[i].w); } double P; #define nxt(x) (x==t?1:x+1) #define pre(x) (x==1?t:x-1) void work2() { dfs1(1, 0); dfs2(pos, 0); for(int i = 1; i <= t; ++i) dp_down(path[i], 0); for(int i = 1, x; i <= t; ++i) { x = path[i]; P = 0.5; for(int j = nxt(i), y; j != i; j = nxt(j)) { y = path[j]; if(nxt(j) == i) up[x] += P * (disl[j]+down[y]); else up[x] += P * (down[y]*son[y]/(son[y]+1)+disl[j]); P /= (son[y]+1); } P = 0.5; for(int j = pre(i), y; j != i; j = pre(j)) { y = path[j]; if(pre(j) == i) up[x] += P * (disr[j]+down[y]); else up[x] += P * (down[y]*son[y]/(son[y]+1)+disr[j]); P /= (son[y]+1); } for(int j = head[x]; j; j = e[j].next) if(!vis[e[j].to]) dp_up(e[j].to, x, e[j].w); } } #undef v int main() { scanf("%d %d", &n, &m); for(int i = 1, u, v, w; i <= m; ++i) scanf("%d %d %d", &u, &v, &w), add(u, v, w), add(v, u, w); if(n != m) work1();//普通树 else work2();//基环树 for(int i = 1; i <= n; ++i) ans += (down[i]*son[i]+up[i]*fa[i]) / (son[i]+fa[i]); printf("%.5lf\n", ans / n); } ```