题解 P2081 【[NOI2012]迷失游乐园】
作者:互联网
这的确是一道树形\(dp\)的好(毒瘤)题,理一理思路应该也不是太难,主要是基环树那\(50\)分不好拿
Solution [NOI2012]迷失游乐园
题目大意:给定一个\(n\)个点\(m\)条边的无向图,每条边有一长度,问等概率选一点出发,等概率走到与当前节点相邻节点,不重复走一节点,所走路径的期望长度
分析:我们首先来看\(50\)分,也就是原图是一颗树的情况.
先假设以\(1\)号点为根,那么设\(f[u]\)为从\(u\)出发,在这个子树内走,所走路径的期望长度.
显然我们\(1\;min\)就可以列出来一个状态转移方程:
\[f[u] = \frac{\sum{(f[v] + w_{u,v})}}{siz[u]}\]
其中\(v\)为\(u\)的儿子,\(w_{u,v}\)为连接\(u,v\)两点的路径长度,\(siz[u]\)为\(u\)点的儿子个数
这个正确性是显而易见的,路径长度乘上概率而已
设\(ans[u]\)为从\(u\)点出发,所走路径的期望长度,也就是以\(u\)点为根,在这颗树内所走路径的期望长度,最终答案为\(ans = \frac{\sum_{1}^{n}{ans_{i}}}{n}\)
\(ans[1] = f[1]\),但是我们如何通过\(ans[1]\)来求其它的\(ans\)?处理技巧有很多,比如记录一个父亲,但是如果一个点连着很多点就可以轻松把你卡掉,那么我们可以用一种类似\(bfs\)的方法来求\(ans\)
比如这颗树:
我们已经知道了以\(1\)为根的答案,也就是\(ans[1]\),假设现在我们要求\(ans[2]\),实际上就是以\(2\)为根的答案.这个树变成了酱紫:
画图真的逼死强迫症
我们会很愉快的发现,\(3,6\)这一颗子树的\(f\)值并没有什么变动诶,\(4,5\)也是酱紫的,那么这意味这什么?我们可以在\(O(1)\)的时间内从已知的\(ans\)值推出与它相邻节点的\(ans\)值,那么只需要一次\(bfs\)就可以在\(O(n)\)的时间内算出所有的\(ans\)值啦!
设我们已知\(ans[u]\),要推出\(ans[v]\)(\(v\)是\(u\)的相邻节点)
那么设少了\(v\)这个点后,\(ans[u]\)变为了\(tmp\),那么:
\(tmp = \frac{ans[u] \times degree[u] - w_{u,v} - f[v]}{degree[u] - 1}\)
其中\(degree[u]\)表示\(u\)点度数,当\(degree[u] = 1\)时特判\(tmp = 0\)(因为\(siz\)是以\(1\)为根是算的,不能直接用)
然后我们可以算\(ans[v]\)啦
\(ans[v] = \frac{f[v] * siz[v] + w_{u,v} + tmp}{siz[v] + 1}\),原来\(v\)一直作为子树存在,所以可以放心的用\(siz[v]\)
然后\(50 pts\) get!
然后就是毒瘤的基环树了……
我们来看看有基环时图长啥样子
好像有点歪,不管了就酱紫了
我们发现所谓基环树就是一个大的环上"生长"出了很多棵树
我们一个拓扑排序就可以把环给逮出来,基本操作
我们定义"上"这个方向是从树往基环走,"下"是从基环往树上走,每棵树的树根都在基环上
那么基环的存在对"下"的答案是没有影响的,我们只需要将"上"这部分的答案累计入答案即可
在基环上我们有两个行走防线,顺时针和逆时针,那么对于一颗树,我们可以在根新建两个虚拟点,分别表示在基环上的两个行走方向,虚拟点的\(f\)值即为在基环上路径的期望长度,当然,在程序实现中我们并不需要真的建点,只需要改改\(ans\)就好了,然后套用第一问的方法,用树根的\(ans\)去向"下"更新子树内\(ans\)即可,最后答案任为\(\frac{\sum_{1}^{n}ans_{i}}{n}\)
以顺时针走举例吧,逆时针差不多,我们设\(tot\)为从\(i\)这个点第一步向"上"走,顺时针走的期望长度:
\[tot = \sum[P_j \cdot (f[j] \cdot \frac{siz[j]}{siz[j] + 1} + w_{j-1,j})]\]从\(j\)这个点向下走的期望长度为\(f[j]\),走进去的概率为\(\frac{siz[j]}{siz[j] + 1}\),注意:如果下一个点为起始点,分母为\(siz[j]\)(因为不能访问重复点,也就必须走进子树内)
对于\(P_j\),每次离开循环时我们将其乘上\(\frac{1}{siz[j] + 1}\),因为不向下走进子树,继续在环上走的概率是\(\frac{1}{siz[j] + 1}\)
虽然我们有两个虚拟点,但是我们可以将顺逆时针的答案累计起来再去更新\(ans\).那么我们现在设\(sum\)为顺逆时针的\(tot\)之和
\[ans[u] = \frac{f[u] * siz[u] + sum}{siz[u] + 2}\]
然后愉快的更新一波\(ans\)即可
上丑陋的代码:注意特判0!!
#include <cstdio>
#include <cctype>
#include <cstring>
#include <vector>
#include <queue>
using namespace std;
typedef double type;
const int maxn = 1e5 + 100;
inline int read(){
int x = 0;char c = getchar();
while(!isdigit(c))c = getchar();
while(isdigit(c))x = x * 10 + c - '0',c = getchar();
return x;
}
struct Edge{int to,dist;};
vector<Edge> G[maxn];int degree[maxn];
inline void addedge(int from,int to,int dist){
G[from].push_back(Edge{to,dist});
degree[to]++;
}
int n,m;
namespace Tree{//为一棵树时
type f[maxn],ans[maxn];
int vis[maxn],siz[maxn];
void dfs(int u,int faz){//树形dp求f
for(auto e : G[u]){
if(e.to == faz)continue;
siz[u]++;
dfs(e.to,u);
f[u] += f[e.to] + e.dist;
}
if(siz[u])f[u] /= siz[u];//特判siz是否为0
}
void bfs(int s){//更新答案
queue<int> Q;
Q.push(s),vis[s] = 1;
while(!Q.empty()){
int u = Q.front();Q.pop();
for(auto e : G[u]){
if(vis[e.to])continue;
int nsiz = siz[u] + (u != s);//nsiz即度数,除了起始点(根节点)都要加1
type newfu = (nsiz == 1) ? 0 : (ans[u] * nsiz - e.dist - f[e.to]) / (nsiz - 1);
ans[e.to] = (f[e.to] * siz[e.to] + newfu + e.dist) / (siz[e.to] + 1);//这两处见上文
Q.push(e.to),vis[e.to] = 1;
}
}
}
inline void work(){
dfs(1,0);
ans[1] = f[1];
bfs(1);
type tmp = 0;
for(int i = 1;i <= n;i++)
tmp += ans[i];
printf("%.5f\n",tmp / n);
}
}
namespace Circle{
type f[maxn],ans[maxn];
int vis[maxn],siz[maxn],inc[maxn],nxt[maxn],pre[maxn],tag[maxn],dis[32][32],ctot;//tag[u]表示节点u对应环上点的编号(离散化),inc表示是否在环上,dis表示距离
void toposort(){//拓扑找环
queue<int> Q;
for(int i = 1;i <= n;i++)inc[i] = 1;
for(int i = 1;i <= n;i++)
if(degree[i] == 1)Q.push(i);
while(!Q.empty()){
int u = Q.front();Q.pop();
inc[u] = 0;
for(auto e : G[u])
if(--degree[e.to] == 1)Q.push(e.to);
}
}
void dfs(int u,int faz){//树形dp
for(auto e : G[u]){
if(e.to == faz || inc[e.to])continue;
siz[u]++;
dfs(e.to,u);
f[u] += f[e.to] + e.dist;
}
if(siz[u])f[u] /= siz[u];
}
void bfs(int s){//同上
queue<int> Q;
Q.push(s),vis[s] = 1;
while(!Q.empty()){
int u = Q.front();Q.pop();
for(auto e : G[u]){
if(vis[e.to] || inc[e.to])continue;//这里注意,bfs是别跑环上去了,就在子树内更新
int nsiz = siz[u] + (u != s);
type newfu = (nsiz == 1) ? 0 : (ans[u] * nsiz - e.dist - f[e.to]) / (nsiz - 1);
ans[e.to] = (f[e.to] * siz[e.to] + newfu + e.dist) / (siz[e.to] + 1);
Q.push(e.to),vis[e.to] = 1;
}
}
}
vector<int> cir;
void dfs_circle(int u,int faz){//找出环上对应关系
if(tag[u])return;
tag[u] = ++ctot;
for(auto e : G[u]){
if(!inc[e.to] || e.to == faz)continue;
pre[e.to] = u,nxt[u] = e.to;
dfs_circle(e.to,u);
dis[tag[u]][tag[e.to]] = dis[tag[e.to]][tag[u]] = e.dist;
break;
}
}
inline void work(){
toposort();//找环
for(int i = 1;i <= n;i++)
if(inc[i])dfs(i,0),cir.push_back(i);//从环上向"下"求出f
dfs_circle(cir[0],0);
for(int now : cir){//枚举环上点
type div = 1.0,tot = 0,w = 0;
for(int pos = nxt[now];pos != now;pos = nxt[pos]){
w = dis[tag[pre[pos]]][tag[pos]];
if(nxt[pos] == now)tot += div * (w + f[pos]);
else tot += div * (w + f[pos] * siz[pos] / (siz[pos] + 1));
div /= (siz[pos] + 1);
}
div = 1.0,w = 0;
for(int pos = pre[now];pos != now;pos = pre[pos]){
w = dis[tag[pos]][tag[nxt[pos]]];
if(pre[pos] == now)tot += div * (w + f[pos]);
else tot += div * (w + f[pos] * siz[pos] / (siz[pos] + 1));
div /= (siz[pos] + 1);
}
ans[now] = (f[now] * siz[now] + tot) / (siz[now] + 2);//加入了2个虚拟点
}//同上文题解
for(int now : cir)siz[now] += 2;//虽然程序没有加入虚拟点,对siz的影响任然要加入,上文siz不加是因为我们要算概率
for(int now : cir)bfs(now);//更新答案
type sum = 0;
for(int i = 1;i <= n;i++)sum += Circle::ans[i];
printf("%.5f\n",sum / n);
}
}
int main(){
n = read();m = read();
for(int u,v,d,i = 1;i <= m;i++)
u = read(),v = read(),d = read(),addedge(u,v,d),addedge(v,u,d);
if(m == n - 1)Tree::work();
else Circle::work();
return 0;
}
%f
和%lf
真的迷,下次我浮点数用\(cin / cout\)来\(IO\)算了,坑了我很多次了,Windows下迷得很
祝大家都可以愉快的A了这道题
标签:frac,int,题解,ans,NOI2012,siz,nsiz,dist,P2081 来源: https://www.cnblogs.com/colazcy/p/11515080.html