斗地主一局中至少有一人有至少一个炸弹的概率是多少?
斗地主大家都会的吧,一副扑克三个人玩,四个相同的或两张王都算炸弹。那么斗地主一局中至少有一人有至少一个炸弹的概率是多少?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;
}