2019 ICPC-GNYR C • Pass the Buck 题解

题目

Description

In the Pass the Buck game, there are several players and each player has one or more neighbors among the other players.

Each game begins with one player chosen at random who receives a buck (the current holder). (For this game, chosen at random means each possible outcome has the same probability.)

Then, at each step, if the current holder has d neighbors, he/she chooses an integer k in the range [0, d] at random. If 0 is chosen, the current holder is the winner and keeps the buck. Otherwise, the holder passes the buck to the neighbor with index k, who becomes the new holder.

The game continues until some holder wins.

Write a program to find the probability that player j wins if player k is the first holder.

The configuration of players and neighbors can be modeled as a graph with the players as vertices and an edge between two vertices if the corresponding players are neighbors. For example, if they are seated in a row then the neighbors are the players to the left and right (if any).

Pass the Buck Image 1

If they are seated around a table, then each player has a neighbor to the right and left.

If they are seated in multiple rows, they may have neighbors left, right, in front and/or behind.

Pass the Buck Image 2

Format

Input

The input consists of multiple lines of input. The first line contains the number, N, (2 <= N <= 20) of players followed by a space followed by the number, P, (1 <= P <= 20) of start/winner pairs.

The next N lines of input give the neighbors of each player. Line m gives the neighbors of player m. The first integer on the line is the number, ***d(m)***, of neighbors of player m. This is followed by d(m) integers giving the indices of the neighbors of m, separated by spaces.

The next P lines of input give the index, s, of the start holder and the index, w, of the player whose probability of winning when s is the first holder is to be found. Each line contains three integers, j, s(j) and w(j) separated by spaces where j is the index of the pair.

Output

Your program will generate P lines of output.
The j t h j^{th} jth line contains the integer j a space and a 5 decimal point value which is the probability that player w(j) wins if player s(j) is the first holder.

Samples

InputOutput
5 4
1 2
2 1 3
2 2 4
2 3 5
1 4
1 1 1
2 2 2
3 3 3
4 4 4
1 0.61818
2 0.47273
3 0.45455
4 0.47273

思路

分析题意,似乎题目所求解的获胜情况并不是有限的。例如:在图一中,当发牌给1号时,若求解1号获胜的概率,则可以考虑以这些情况:1→1(结束),1→2→1→1(结束),1→2→3→2→1→1(结束),1→2→3→4→3→2→1→1(结束),1→2→3→4→5→4→3→2→1→1(结束)。这些只是典型的几种,但诸如1→2→1→2→1→2→1→2→1→2→1→2→1→2→1→2→1→2→1→2→1→1(结束)的类似情况也不乏可能性(即使可能性相当小)。

简单dfs方案

于是有了第一种思路:dfs。对于给定的起点start和终点end以及行至该起点的概率,找到起点所有的邻接点,平均分配概率,再逐一将邻接点坐标赋值给start,以此方式进行递归运算。为减小误差,当某条路径的基准概率低于1e-13(实验得到)时停止搜索。代码如下:

double ProbabilityWin(int start, int end, double presetprob) {
    double ret = 0.0;
    if (presetprob >= 0.0000000000001) {
        int i;
        if (start == end) ret += presetprob / (double)(degree[start] + 1);
        for (i = 1; i <= N; i++) {
            if (graph[start][i]) 
            	ret += ProbabilityWin(i, end, presetprob / (double)(degree[start] + 1));
        }
    }
    return ret;
}

其中graph为图的邻接矩阵,degree记录了各节点的度。调用时设定presetprob = 1.0

显而易见,结局是悲剧的。用题目中给的样例打表计时:

The algorithm took 1046.88ms to solve the problem

然后试试几组更大的数据:

The algorithm took 8640.62ms to solve the problem

更大的数据…抱歉,三十分钟过去了…

基于bfs优化后的算法

接着分析过程,大致如下图所示:

流程图1

上图描述了题述第一种情况中前五轮模拟过程。我们以此方式进行1号的胜率计算。注意到在第III轮至第IV轮的计算中,出现了部分元素(即2)的重复计算,而在第IV轮至第V轮的计算中,只需进行三类运算(1,3,5)的情况下我们却计算了六次(1,3,1,3,3,5)。

因此考虑进行概率合并。合并后流程图如下:

流程图2

数学解释

下面使用数学语言描述这一过程:记节点 V V V 的度为 d ( V ) d(V) d(V)。定义二元有序对

N = ( V , p V ) N=(V,pV) N=(V,pV)

其中 V V V 为节点, p V pV pV 为我们预先定义该节点的的初始概率。

定义任意二元有序对的函数

f ( N ) = { ( V ′ , p V ′ ) ∣ V ′ 为 V 的 所 有 邻 接 节 点 , p V ′ = p V d ( V ) + 1 } f(N) = \lbrace (V', pV') | V'为V的所有邻接节点, pV'=\frac{pV}{d(V) + 1} \rbrace f(N)={(V,pV)VV,pV=d(V)+1pV}

先给出周游前的集合 N S ( 0 ) = { ( S t a r t , p S t a r t = 1 ) } NS(0) = \lbrace (Start, pStart = 1) \rbrace NS(0)={(Start,pStart=1)} 。接着在每一部中依此方式求得 N S ( n ) NS(n) NS(n),即:

N S ( n ) = ⋃ f ( K ) , K ∈ N S ( n − 1 ) NS(n) = \bigcup f(K), K \in NS(n - 1) NS(n)=f(K),KNS(n1)

其中,每次运算结束后,合并 N S ( n ) NS(n) NS(n) 中所有节点相同的有序对,方法如下:

⋃ { ( V , p V i ) } = { ( V , ∑ p V i ) } \bigcup \lbrace (V, pV_i) \rbrace = \lbrace (V, \sum {pV_i}) \rbrace {(V,pVi)}={(V,pVi)}

特殊地,若在某周游步骤中 N S ( n − 1 ) NS(n-1) NS(n1) 包含一节点为终点的有序对 ( E n d , p E n d ) (End, pEnd) (End,pEnd) ,则在总胜率中加上 p E n d d ( E n d ) + 1 \frac{pEnd}{d(End)+1} d(End)+1pEnd 以统计总胜率。在某次递增的胜率小于1e-7(实验得到)时终止计算,输出计算结果。

代码如下:

double ProbabilityWin(int start, int end) {
    double ret = 0.0, curp[21], nextp[21], roundp;
    int i, j;
    for (i = 0; i < 21; i++) curp[i] = nextp[i] = 0.0;
    roundp = 0.0;
    curp[start] = 1.0;
    do {
        ret += roundp;
        roundp = 0.0;
        for (i = 0; i <= N; i++) nextp[i] = 0.0;
        for (i = 1; i <= N; i++)
            if (curp[i] > 0.0)
                for (j = 1; j <= N; j++)
                    if (graph[i][j]) {
                        if (i == j) {
                            if (i == end) roundp += curp[i] / (double)degree[i];
                        }
                        else nextp[j] += curp[i] / (double)degree[i];
                    }
        for (i = 0; i < 21; i++) curp[i] = nextp[i];
    } while (roundp == 0.0 || roundp > 0.0000001);
    return ret;
}

该实现中使用两数组probnextp来表示当前一轮周游以及下一轮周游的 N S NS NS 集合,roundp为当前一轮周游得到的胜率。

以上算法成功将几组数据运算时间压缩到了1ms。

代码

#include "iostream"
#include "cstdio"
using namespace std;

bool graph[21][21] = { false };
int degree[21] = { 0 };
int N, P;

double ProbabilityWin(int start, int end) {
    double ret = 0.0, curp[21], nextp[21], roundp;
    int i, j;
    for (i = 0; i < 21; i++) curp[i] = nextp[i] = 0.0;
    roundp = 0.0;
    curp[start] = 1.0;
    do {
        ret += roundp;
        roundp = 0.0;
        for (i = 0; i <= N; i++) nextp[i] = 0.0;
        for (i = 1; i <= N; i++)
            if (curp[i] > 0.0)
                for (j = 1; j <= N; j++)
                    if (graph[i][j]) {
                        if (i == j) {
                            if (i == end) roundp += curp[i] / (double)degree[i];
                        }
                        else nextp[j] += curp[i] / (double)degree[i];
                    }
        for (i = 0; i < 21; i++) curp[i] = nextp[i];
    } while (roundp == 0.0 || roundp > 0.0000001);
    return ret;
}

int main() {
    int i, j, k, t, index, start, end;
    cin >> N >> P;
    for (i = 1; i <= N; i++) {
        cin >> j;
        for (k = 0; k < j; k++) {
            cin >> t;
            graph[i][t] = graph[t][i] = true;
        }
        graph[i][i] = true;
    }
    for (i = 1; i <= N; i++) {
        for (j = 1; j <= N; j++) {
            if (graph[i][j]) degree[i]++;
        }
    }
    for (i = 1; i <= P; i++) {
        cin >> index >> start >> end;
        printf("%d %.5lf\n", index, ProbabilityWin(start, end));
    }
    return 0;
}
  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

brandonw3612

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值