题面
有一副互不相同的牌共 n + m + 1 n+m+1 n+m+1 张,编号 1 ∼ n + m + 1 1\sim n+m+1 1∼n+m+1。有两个人,第一个人 n n n 张,第二个人 m m m 张。另外有一张牌放在桌子上。
两个人玩游戏轮流操作(其中第一个人为先手)。有如下 2 2 2 中操作类型:
-
猜测:猜桌上的那张牌是什么。如果猜对了则获胜,猜错了则失败。操作完之后游戏结束。
-
指定:报一张牌的名字,如果对方手上有这张牌,则将该牌丢弃,游戏继续;如果对方手上没有这张牌,对方则会表示他不用由此牌。
现在假设两个人都知道自己的牌分别是什么,但是不知道桌上和对方手里的牌具体是什么。
若双方都采取最优策略进行游戏,问先手和后手获胜概率。
若选手答案与标准答案的误差不超过 1 0 − 9 10^{-9} 10−9 则认为正确。
数据范围: 0 ≤ n , m ≤ 1000 0\le n,m\le 1000 0≤n,m≤1000 。
题解
虽然是采取最优策略进行的,但是往往没有必胜策略。这里所说的最优策略是指,让获胜概率最大化。
不难发现,当角色丢弃一张牌后,又变成了一个总牌数更少的、先后手交换的起始局面,因此可以设 f ( n , m ) f(n,m) f(n,m) 为输入 n , m n,m n,m 时先手的获胜概率。
我们先解决边界情况: n = 0 n=0 n=0 或 m = 0 m=0 m=0 。
若对方的牌数为 0,那么可以直接推出桌上那张牌的编号,下一步便会直接「猜测」,然后 100% 取胜。为了避免这种情况,对方绝对不能给下一步进行的机会。所以,整个游戏的流程就是:先手「猜测」、判断输赢。
赢的概率是 m + 1 m+1 m+1 张牌中随机一张选对的概率, f ( n , m ) = 1 m + 1 f(n,m)=\frac{1}{m+1} f(n,m)=m+11 。
然后,我们考虑都有牌的情况。
如果当前状态等价于一个初始状态的话,没有人会直接猜测的,证明详见:笑长(zhang3)博客 思路第 11 行开始。
不(挺)难发现,「指定」有两种方式:
- 「交流」:询问一张自己没有的牌。如果对方有,对方会将其丢弃,然后你知道在场不可能再有那张牌。或者对方没有,说明就是桌面上那张牌,但是下一步不是你的……
- 「欺骗」:询问一张自己有的牌。你知道这是你的牌,对方不可能有,也不可能是桌上,所以会得到“没有牌”的指定结果。如果对方信以为真,下一步真去「猜测」了,那么你就赢了。这个操作的存在使得在「交流」中,即使得到了“没有牌”的结果,对方也没有100%的底气「猜测」。
另一方面,如果对方不信,你也不可能去「猜测」,对方便会知道你在「欺骗」,进而知道你有这张牌。大家都知道了,那么这张牌就等于被丢弃了——被你这个先手丢弃了。从对方「不信」开始,这一结果就被笃定。
于是,围绕着先手「交流」或「欺骗」,后手「信任」(意味着下一步「猜测」)或「不信」,就有这样的状态转移表:
后
手
先
手
「
交
流
」
「
欺
骗
」
(
丢
弃
)
m
m
+
1
(
1
−
f
(
m
−
1
,
n
)
)
−
「
信
任
」
0
1
「
不
信
」
1
m
+
1
1
−
f
(
m
,
n
−
1
)
\begin{matrix} _{后手}~^{先手}&「交流」&「欺骗」\\ (丢弃) & \frac{m}{m+1}(1-f(m-1,n)) & -\\ 「信任」& 0 & 1\\ 「不信」& \frac{1}{m+1} & 1-f(m,n-1) \end{matrix}
后手 先手(丢弃)「信任」「不信」「交流」m+1m(1−f(m−1,n))0m+11「欺骗」−11−f(m,n−1)
由于指定与猜测结果的随机性很大,所以决策上,先后手没有必胜策略,也会选择以一定概率进行「交流」/「欺骗」,或「信任」/「不信」。我们设先手选择「交流」的概率为 p p p ,后手选择「信任」的概率为 q q q 。
那么可以得出这个大式子:
f
(
n
,
m
)
p
,
q
=
p
(
q
+
(
1
−
q
)
(
1
−
f
(
m
,
n
−
1
)
)
)
+
(
1
−
p
)
(
−
q
m
+
1
+
1
m
+
1
+
m
m
+
1
(
1
−
f
(
m
−
1
,
n
)
)
)
f(n,m)_{p,q}=p\Big(q+(1-q)\big(1-f(m,n-1)\big)\Big)\\ +(1-p)\left( -\frac{q}{m+1}+\frac{1}{m+1}+\frac{m}{m+1}(1-f(m-1,n)) \right)
f(n,m)p,q=p(q+(1−q)(1−f(m,n−1)))+(1−p)(−m+1q+m+11+m+1m(1−f(m−1,n)))
在博弈中,双方对于
p
p
p 和
q
q
q 的选取都是透明的,对于确定的
p
p
p ,后手会调整
q
q
q 使得答案最小,对于确定的
q
q
q ,先手也会调整
p
p
p 使得答案最大,所以,答案最终就会变为
min
q
∈
[
0
,
1
]
(
max
p
∈
[
0
,
1
]
f
(
n
,
m
)
p
,
q
)
\min_{q\in[0,1]}\left( \max_{p\in[0,1]} f(n,m)_{p,q} \right)
q∈[0,1]min(p∈[0,1]maxf(n,m)p,q)
假使我们知道了
q
q
q ,那么不难发现式子就会变为关于
p
p
p 的一次式,
p
p
p 一定会选取
0
,
1
0,1
0,1 其中一个了。当
p
p
p 分别取
0
,
1
0,1
0,1 的时候,就是两个关于
q
q
q 的一次式(刚好分别是上述
f
(
n
,
m
)
p
,
q
f(n,m)_{p,q}
f(n,m)p,q 式子的第二行和第一行),
q
q
q 不论取什么值,最终答案都将是这两个一次式中较大的那个。还是把两个式子写出来吧:
p
=
0
:
f
=
−
q
m
+
1
+
1
m
+
1
+
m
m
+
1
(
1
−
f
(
m
−
1
,
n
)
)
p
=
1
:
f
=
q
+
(
1
−
q
)
(
1
−
f
(
m
,
n
−
1
)
)
p=0:f=-\frac{q}{m+1}+\frac{1}{m+1}+\frac{m}{m+1}(1-f(m-1,n))\\ ~\\ p=1:f=q+(1-q)\big(1-f(m,n-1)\big)~~~~~~~~~~~~~~~~~~~~~~~~~~~\,
p=0:f=−m+1q+m+11+m+1m(1−f(m−1,n)) p=1:f=q+(1−q)(1−f(m,n−1))
那么,就相当于在这两个一次式对应一次函数形成的下凸包中,取最小值点。由于我们发现这两个一次函数的斜率异号,所以破案了——最终的 q q q 就在这两个一次函数的交点处!
得到 q q q 后,就能得到该 f ( n , m ) f(n,m) f(n,m) 。 q q q 的取值于不同的 n , m n,m n,m 大抵是不一样的,因此需要每次算。
时间复杂度 O ( n m ) O(nm) O(nm) 。
CODE
记忆化搜索,但其实完全没必要。
//真的猛士,敢于将打到一半的代码提交
#include<map>
#include<set>
#include<cmath>
#include<queue>
#include<stack>
#include<random>
#include<vector>
#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
using namespace std;
#define MAXN 1005
#define LL long long
#define ENDL putchar('\n')
#define DB double
#define lowbit(x) (-(x) & (x))
#define FI first
#define SE second
//#pragma GCC optimize(2)
int xchar() {
static const int maxn = 1000000;
static char b[maxn];
static int pos = 0,len = 0;
if(pos == len) pos = 0,len = fread(b,1,maxn,stdin);
if(pos == len) return -1;
return b[pos ++];
}
//#define getchar() xchar()
LL read() {
LL f = 1,x = 0;int s = getchar();
while(s < '0' || s > '9') {if(s<0)return -1;if(s=='-')f=-f;s = getchar();}
while(s >= '0' && s <= '9') {x = (x<<1) + (x<<3) + (s^48);s = getchar();}
return f*x;
}
void putpos(LL x) {if(!x)return ;putpos(x/10);putchar((x%10)^48);}
void putnum(LL x) {
if(!x) {putchar('0');return ;}
if(x<0) putchar('-'),x = -x;
return putpos(x);
}
void AIput(LL x,int c) {putnum(x);putchar(c);}
const int MOD = 1000000007;
int n,m,s,o,k;
DB dp[1005][1005];
bool f[1005][1005];
DB DP(int x,int y) {
if(!x || !y) return 1.0/(y+1);
if(f[x][y]) return dp[x][y];
f[x][y] = 1;
DB k1 = DP(y,x-1),b1 = 1.0-k1;
DB k2 = -1.0/(y+1),b2 = (1.0-DP(y-1,x))*y/(y+1) - k2;
DB q = (b1-b2) / (k2-k1);
return dp[x][y] = k1*q+b1;
}
int main() {
n = read();m = read();
DB ans = DP(n,m);
printf("%.13f %.13f\n",ans,1-ans);
return 0;
}