斗地主一局中至少有一人有至少一个炸弹的概率是多少?

斗地主大家都会的吧,一副扑克三个人玩,四个相同的或两张王都算炸弹。那么斗地主一局中至少有一人有至少一个炸弹的概率是多少?qq游戏出现炸弹的概率是不是故意调大了?

我的回答

斗地主一局中炸弹出现的概率(≈0.544668)确实挺大,下面是我的推导思路(动态规划):

“至少一人有炸弹”与“每个人都没炸弹”互为对立命题,方便起见我们先求“每个人都没炸弹”的概率。

假设当三个玩家(A、B、C)的座次选定,一副牌随机打乱发完时,A得到1-17号、B得到18-34号、C(地主)得到35-54号。现在不妨把这个发牌方式转化为另一种等效的方式:先发4张1,从A、B、C的54个牌位中随机挑选4个位置,再发4张2,从剩下的50个排位中随机挑选4个位置……因此整个发牌过程就划分成了14个阶段(13*4普通牌+1*2Joker)。

现在我们需要每次发的4张普通牌(或者2张Joker)不能落到同一段(1-17、18-34、35-54),因为同一段的牌位属于同一个人,这样就构成炸弹了。

定义$d_{ijk}^{(m)}$表示在第$m$阶段、A获得$i$张牌、B获得$j$张牌、C获得$k$张牌时没有出现炸弹的概率,那么在阶段与阶段之间应该有如下递推公式:

\[d_{ijk}^{(m)} = \sum p_{(i'j'k' \rightarrow ijk)} \cdot d_{i'j'k'}^{(m-1)}\]

这里的$(i’,j’,k’)$是$m-1$阶段可以转移到$m$阶段$(i,j,k)$的合法状态,$p_{(i’j’k’ \rightarrow ijk)}$是其对应的转移概率,再注意考虑好边界条件应该就可以做了。

其实你仔细观察会发现,表示阶段这个上标实际上是多余的,在我们重新确定的发牌过程中$(i,j,k)$本身就足以定义好状态了,所以上面的公式可以重新改写成下面这个最终版:

\[d_{ijk} = \sum p_{(i'j'k' \rightarrow ijk)} \cdot d_{i'j'k'}\]

简单写了段C/C++的求解代码:

#include <iostream>
#include <vector>

const double eps = 1e-9;    // 浮点数允许误差
const int PLAIN_ROUND = 13; // 普通发牌回合数
const int TOTAL_ROUND = 14; // 所有发牌回合数
const int CIVIL_CARDS = 17; // 平民最多手牌数
const int DIZHU_CARDS = 20; // 地主最多手牌数
const std::vector<std::vector<int>> plain_dist({
    {0,1,3},{0,2,2},{0,3,1},{1,0,3},{1,1,2},{1,2,1}
   ,{1,3,0},{2,0,2},{2,1,1},{2,2,0},{3,0,1},{3,1,0}
});
const std::vector<std::vector<int>> joker_dist({
    {0,1,1},{1,0,1},{1,1,0}
});

int main() {
    static double dp[CIVIL_CARDS+1][CIVIL_CARDS+1] = {1.0};
    for (int round=0; round < TOTAL_ROUND; ++round) {
        auto dist = &joker_dist;
        if (round < PLAIN_ROUND) { dist = &plain_dist; }
        for (int i=CIVIL_CARDS; i >= 0; --i) {
        for (int j=CIVIL_CARDS; j >= 0; --j) {
            int k; if (round < PLAIN_ROUND) {
                k = (round + 1) * 4 - i - j;
            } else {
                k = (round+1)*4 - 2 - i - j;
            }
            dp[i][j] = 0; for (auto &tuple : *dist) {
                int di=tuple[0], dj=tuple[1], dk=tuple[2];
                int _i = i - di, _j = j - dj, _k = k - dk;
                if (dp[_i][_j] < eps) { continue; }
                int si = CIVIL_CARDS - _i;
                int sj = CIVIL_CARDS - _j;
                int sk = DIZHU_CARDS - _k;
                double prob(int, int, int, int, int, int);
                double p = prob(si, sj, sk, di, dj, dk);
                if (p < eps) { continue; }
                dp[i][j] += p * dp[_i][_j];
            }
        }
        }
    }
    double ans = 1.0 - dp[CIVIL_CARDS][CIVIL_CARDS];
    std::cout << ans << std::endl;
    return 0;
}

double prob(int sa, int sb, int sc, int a, int b, int c) {
    if (a<0 ||   b<0 || c<0 ||  sa<a ||  sb<b || sc<c
    ||sa>CIVIL_CARDS||sb>CIVIL_CARDS||sc>DIZHU_CARDS)
        return 0.0;
    double ra = 1.0, rb = 1.0, rc = 1.0, bs = 1.0;
    for (int i=0; i < a; ++i) { ra = ra*(sa-i)/(i+1); }
    for (int i=0; i < b; ++i) { rb = rb*(sb-i)/(i+1); }
    for (int i=0; i < c; ++i) { rc = rc*(sc-i)/(i+1); }
    double sabc = sa + sb + sc, abc = a + b + c;
    for (int i=0; i < abc; ++i) bs = bs*(sabc-i)/(i+1);
    return (ra * rb * rc / bs);
}

为了保险起见做了个随机模拟,结果也印证了之前的结论:

#include <iostream>
#include <cstdlib>
#include <ctime>
#include <cstring>
#include <algorithm>
using namespace std;

void random_cards(int card[], int st, int ed) {
    for (int i=ed-1; i > st; --i) {
        int j = st + rand()%(i-st+1);
        if (j != i) swap(card[j], card[i]);
    }
}

int main() {
    srand((unsigned)time(NULL));
    int card[54];
    for (int i = 0; i < 54; ++i)
        card[i] = i / 4;
    int num = 0, sum = 1000000;
    int cnt[3][14];
    for (int t = 0; t < sum; ++t) {
        memset(cnt, 0x00, 3*14*sizeof(int));
        random_cards(card, 0, 54);
        for (int i = 0; i < 54; ++i) {
            int tmp = ++cnt[min(2, i/17)][card[i]];
            if (tmp >= 4 || tmp >= 2 && card[i] == 13) {
                ++num; break;
            }
        }
    }
    cout << double(num) / sum << endl;
    return 0;
}