POJ3071 Football

POJ3071 Football

翻译 岛田小雅

题目描述

\(2^n\) 个队伍参加一场单淘汰制足球锦标赛,它们被编号为 \(1,2,…,2^n\)。每一轮比赛,未被淘汰的队伍按照升序被放在一个列表里,接下来,列表里的第 \(1\) 个队伍跟第 \(2\) 个队伍比赛,第 \(3\) 个队伍跟第 \(4\) 个队伍比赛,等等。获胜的队伍晋级下一轮,战败的队伍被淘汰。\(n\) 轮之后,只有一个队伍留下来,那个队伍就是冠军。

有一个矩阵 \(P=[p_{ij}]\)\(p_{ij}\) 表示队伍 \(i\) 在一场比赛中战胜队伍 \(j\) 的概率,决定了哪支队伍更容易赢得冠军。

输入格式

多组测试点。每个测试点都由一个 \(n\) \((1\leqslant{n}\leqslant{7})\) 开头,接下来的 \(2^n\) 行每行有 \(2^n\) 个值,\(i\) 行的第 \(j\) 个值是 \(p_{i,j}\)。矩阵 \(P\) 满足对任意 \(i\neq{j}\)\(p_{ij}=1-p_{ji}\),并且对任意 \(i\)\(p_{ii}=0\)

输入结束的标志是一个数字 \(-1\)

矩阵 \(P\) 中的所有数据都以小数给出,为避免精度问题,请使用 \(\texttt{double}\) 类型作答。

输出格式

对每个测试点输出一行整数,表示哪个队伍最有可能成为冠军。为了避免精度问题,数据保证任意两个队伍之间成为冠军的概率之差不小于 \(0.01\)

样例

输入

2
0.0 0.1 0.2 0.3
0.9 0.0 0.4 0.5
0.8 0.6 0.0 0.6
0.7 0.5 0.4 0.0
-1

输出

2

补充

在样例中,第一轮队伍 \(1\) 对阵队伍 \(2\),队伍 \(3\) 对阵 队伍 \(4\)。两场比赛的胜者在第二轮对阵来决出冠军。队伍 \(2\) 获得冠军的概率如下:

\[\begin{align} P(队伍2获胜) &=P(2击败1)P(3击败4)P(2击败3)+P(2击败1)P(4击败3)P(2击败4)\\ &=p_{21}p_{34}p_{23}+p_{21}p_{43}p_{24}\\ &=0.9\times{0.6}\times{0.4}+0.9\times{0.4}\times{0.5}\\ &=0.396 \end{align} \]

队伍 \(3\) 的获胜概率紧随其后,为 \(0.372\)

来源

Stanford Local 2006

题解

作者 岛田小雅

远离破 OJ。

这是一道概率 DP 题。

准备一个用来存状态的数组 \(f_{i,j}\),表示队伍 \(i\) 在第 \(j\) 轮晋级的概率。边界:\(f_{i,0}=1\)

令队伍 \(i\) 在第 \(j\) 轮遇到的对手是队伍 \(k\),转移方程:\(f_{i,j}=f_{i,j}+f_{i,j-1}\times{f_{k,j-1}}\times{p_{i,k}}\)

这样的话,有一个问题。我们知道队伍 \(i\) 在每一轮遇到的对手都有一个范围。以队伍 \(1\) 为例:第一轮队伍 \(1\) 的对手只能是队伍 \(2\),而第二轮的对手只能是队伍 \(3\) 或队伍 \(4\)(取决于哪支队伍晋级)……

那怎么找到 \(k\) 的范围呢?其实不难。我们把所有的队伍从小到大排列后两两分组,每经过一轮,再把相邻的两个组合并。我们会发现队伍 i 可能的对手只能是跟自己在当前轮“一组”的人。以队伍 \(1\) 为例:第一轮它的对手只能是跟自己一组的队伍 \(2\)。而经过一轮后,队伍 \(1\) 晋级,队伍 \(2\) 淘汰,同时队伍 \(1\) 和队伍 \(2\) 的小组和队伍 \(3\) 和队伍 \(4\) 的小组合并。因为队伍 \(1\) 已经把自己原来小组里的队伍 \(2\) 淘汰掉了,所以它的对手就只能是队伍 \(3\) 或队伍 \(4\)。再经过一轮,队伍 \(1\) 把队伍 \(2\)\(3\)\(4\) 都淘汰了,可能遇到的对手就是队伍 \(5\)\(6\)\(7\)\(8\)(如果参赛的队伍有这么多的话:D)。

由此我们总结出规律:队伍 \(i\) 在第 \(j\) 轮可能遇到的对手只能是队伍 \(k\) \((\frac{i-1}{2^{j-1}}\neq{\frac{k-1}{2^{j-1}}}\bigwedge{\frac{i-1}{2^j}={\frac{k-1}{2^j}}})\)

具体实现见 AC 代码 1。

但是这题没那么简单。如果用三层循环(同 AC 代码 1)的话,破 OJ 会莫名其妙 TLE(据说是卡了我 1e5 的常)

于是除了像 AC 代码 1 那样把所有的 \(\texttt{cin}\)\(\texttt{cout}\) 全部改成 \(\texttt{scanf()}\)\(\texttt{printf()}\) 以外(我也不知道为什么明明流同步了居然还被卡了),还可以考虑优化最内层的循环 \(k\)。因为我们只需要可能作为队伍 \(i\) 对手的 \(k\) 的区间,如果通过一个队伍一个队伍暴力比较条件来找,有很多队伍会被重复算 \(n\)(虽然 n 的最大值只有 7 而已),浪费了时间(一本正经胡说八道)。我们可以先定位队伍 \(i\) 在哪个区间里,然后再根据那个区间来寻找它在该轮的对手区间。

这里我们需要用到一个在 AC 代码 1 中已经用到的一个小细节,那就是把所有的队伍编号 \(-1\)。然后我们开始考虑怎么优化找 \(k\) 的过程。

给手玩过程画一张图:

根据上图,我们很容易就能发现队伍 \(i\) 在回合 \(j\) 中的对手 \(k\) 只会出现在和自己在第 \(j+1\) 轮一组的队伍之中,而至于队伍 \(i\) 在下一轮分组的前一半还是后一半,取决于它的第 \(j\) 位二进制值是真还是假。如果是假,那就在前一半,而为真时,在后一半。

然后我们就能根据定位到的区间左端和区间长度精准锁定 \(k\) 的范围啦。

具体实现见 AC 代码 2。

不过代码 2 直接交还是 TLE。最后是怎么过的呢——

为避免 \(\texttt{G++}\) 玄学问题,请使用 \(\texttt{C++}\) 语言提交。

(超小声)其实第一次用 cin 和 cout 提交就是对的……

AC 代码 1

作者 岛田小雅
#include <algorithm>
#include <cstring>
#include <iostream>
#include <map>
#include <queue>
#include <stdio.h>
using namespace std;

const int N = (1<<7)+2;
int n;
double p[N][N], f[N][N];

int main()
{
    while(scanf("%d",&n))
    {
        if(n == -1) break;
        int maxn = (1<<n);
        for(int i = 0; i < maxn; i++)
        {
            for(int j = 0; j < maxn; j++) scanf("%lf",&p[i][j]);
            f[i][0] = 1;
        }
        for(int j = 1; j <= n; j++)
        {
            for(int i = 0; i < maxn; i++)
            {
                f[i][j] = 0;
                for(int k = 0; k < maxn; k++)
                {
                    // if(i == k) continue;
                    if((i/(1<<j-1)!=k/(1<<j-1)) && (i/(1<<j)==k/(1<<j))) f[i][j] += f[i][j-1]*f[k][j-1]*p[i][k];
                }
            }
        }
        int ans = 0;
        for(int i = 1; i < maxn; i++) if(f[i][n] > f[ans][n]) ans = i;
        printf("%d\n",ans+1);
    }
    return 0;
}

AC 代码 2

作者 岛田小雅
#include <algorithm>
#include <cstring>
#include <iostream>
#include <map>
#include <queue>
#include <stdio.h>
using namespace std;

const int N = (1<<7)+2;
int n;
double p[N][N], f[N][N];

int main()
{
    ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
    while(cin>>n && n!=-1)
    {
        int maxn = (1<<n);
        for(int i = 0; i < maxn; i++)
        {
            for(int j = 0; j < maxn; j++) cin >> p[i][j];
            f[i][0] = 1;
        }
        for(int j = 1; j <= n; j++)
        {
            for(int i = 0; i < maxn; i++)
            {
                f[i][j] = 0;
                int k = i>>j<<j; // 把k推到队伍i当前分组的最前端,手玩出来这么算是对的问我为什么我也不知道
                int pointer = (1<<j-1);
                int len = (1<<(j-1));
                if(!(i&pointer)) k += len;
                for(int _ = 0; _ < len; _++)
                {
                    int _k = k+_;
                    f[i][j] += f[i][j-1]*f[_k][j-1]*p[i][_k];
                }
            }
        }
        int ans = 0;
        for(int i = 1; i < maxn; i++) if(f[i][n] > f[ans][n]) ans = i;
        cout << ans+1 << '\n';
    }
    return 0;
}
posted @ 2022-09-29 01:03  岛田小雅  阅读(32)  评论(0编辑  收藏  举报