ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

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

2019-09-12 20:52:04  阅读:231  来源: 互联网

标签:frac int 题解 ans NOI2012 siz nsiz dist P2081


题目链接

这的确是一道树形\(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

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有