[JLOI2013] 卡牌游戏
P2059 [JLOI2013] 卡牌游戏
题目描述
\(N\) 个人坐成一圈玩游戏。一开始我们把所有玩家按顺时针从 \(1\) 到 \(N\) 编号。首先第一回合是玩家 \(1\) 作为庄家。每个回合庄家都会随机(即按相等的概率)从卡牌堆里选择一张卡片,假设卡片上的数字为 \(X\),则庄家首先把卡片上的数字向所有玩家展示,然后按顺时针从庄家位置数第 \(X\) 个人将被处决(即退出游戏)。然后卡片将会被放回卡牌堆里并重新洗牌。被处决的人按顺时针的下一个人将会作为下一轮的庄家。那么经过 \(N-1\) 轮后最后只会剩下一个人,即为本次游戏的胜者。现在你预先知道了总共有 \(M\) 张卡片,也知道每张卡片上的数字。现在你需要确定每个玩家胜出的概率。
这里有一个简单的例子:
例如一共有 \(4\) 个玩家,有四张卡片分别写着3,4,5,6.
第一回合,庄家是玩家 \(1\) ,假设他选择了一张写着数字 \(5\) 的卡片。那么按顺时针数 1,2,3,4,1,最后玩家 \(1\) 被踢出游戏。
第二回合,庄家就是玩家 \(1\) 的下一个人,即玩家 \(2\).假设玩家 \(2\) 这次选择了一张数字 \(6\),那么 2,3,4,2,3,4,玩家 \(4\) 被踢出游戏。
第三回合,玩家 \(2\) 再一次成为庄家。如果这一次玩家 \(2\) 再次选了 \(6\),则玩家 \(3\) 被踢出游戏,最后的胜者就是玩家 \(2\)。
输入格式
第一行包括两个整数 \(N,M\) 分别表示玩家个数和卡牌总数。
接下来一行包含 \(M\) 个整数,分别给出每张卡片上写的数字。
输出格式
输出一行包含 \(N\) 个百分比形式给出的实数,四舍五入到两位小数。分别给出从玩家 \(1\) 到玩家 \(N\) 的胜出概率,每个概率之间用空格隔开,最后不要有空格。
输入输出样例 #1
输入 #1
5 5
2 3 5 7 11
输出 #1
22.72% 17.12% 15.36% 25.44% 19.36%
输入输出样例 #2
输入 #2
4 4
3 4 5 6
输出 #2
25.00% 25.00% 25.00% 25.00%
说明/提示
对于 \(30\%\) 的数据,有 \(1\le N\le 10\)。
对于 \(50\%\) 的数据,有 \(1\le N\le 30\)。
对于 \(100\%\) 的数据,有 \(1\le N\le 50\), \(1\le M\le 50\), \(1\le\) 每张卡片上的数字 \(\le 50\)。
题解
很明显是一道概率的 DP 题。
F1:30pts
首先我们根据题意,最简单的做法就是状压 DP,我们设 \(dp_{i,s}\) 表示在已经被处决的 \(s\) 状态下,由 \(i\) 操作的概率。转移的时候只需要根据当前的状态找出这一次被处决的人和下一轮操作的人进行转移即可。
#include<bits/stdc++.h>
#define N 55
#define int long long
using namespace std;
int n,m,a[N],vis[N],pre[N];
double ans[N];
unordered_map<int,double>f[N],g[N];//第i轮,第j个人操作的概率
set<int>v,v1;
int findf(int x,int num,int vis){
int nw=x;
for(int i=1;i<num;i++){
++nw;
if(nw>n)nw-=n;
while(vis&(1<<(nw-1))){
++nw;
if(nw>n)nw-=n;
}
}
return nw;
}
signed main(){
cin>>n>>m;
for(int i=1;i<=m;i++)cin>>a[i];
f[1][0]=1;
v1.insert(0);
for(int i=1;i<n;i++){
v=v1,v1.clear();
for(int i=1;i<=n;i++)g[i]=f[i],f[i].clear();
for(int s:v){
for(int x=1;x<=n;x++){//枚举操作的人
if(s&(1<<(x-1))||g[x][s]==0)continue;
for(int j=1;j<=m;j++){
int num=a[j]%(n-i+1);
if(!num)num=n-i+1;
int y=findf(x,num,s);//死的人
int z=findf(y,2,s|(1<<(y-1)));//下一个操作的人
f[z][s|(1<<(y-1))]+=g[x][s]*1.0/m*1.0;
v1.insert(s|(1<<(y-1)));
}
}
}
}
int v=(1<<n)-1;
for(int i=1;i<=n;i++){
printf("%.2lf%% ",f[i][v^(1<<(i-1))]*100.0);
}
return 0;
}
F2: 100pts
我们发现,F1 之所以 TLE,主要是因为状态过多导致,那我们想一种解法不关注当前死的人,只考虑当前有多少人,也就是说,我们把所有死的人都删去,把留下的人重新标号。但是为了能保证所有人的状态可以分开,我们只能采用倒推。
设 \(dp_{i,j}\) 表示在还剩 \(i\) 个人时 1 号坐庄 \(j\) 的胜率(即活着的概率)。初始值很好设,\(f_{1,1}=1\)。
至于转移,对于当前还剩 \(i\) 个人,我们从 1 到 \(i\) 先枚举当前赢的人为 \(j\),然后再枚举使用了那张卡牌,根据这张卡牌,我们可以算出由 1 做庄会使 \(t\) 被处决,那么下一次坐庄的人就变成了 \(t+1\),那么下一次 \(j\) 的 位置就变为了 \(j-t(j>t)\) 或者 \(j+n-t(j<t)\),将下一次的概率转移到当前即可。
#include<bits/stdc++.h>
#define int long long
#define N 55
using namespace std;
int n,m,a[N];
double f[N][N];
//还剩i个人的时候,由1做庄,j还活着的概率
signed main(){
cin>>n>>m;
for(int i=1;i<=m;i++)cin>>a[i];
f[1][1]=1;
for(int i=2;i<=n;i++){//还剩i个人
for(int j=1;j<=i;j++){//表示第j个人活着的概率
for(int k=1;k<=m;k++){
int t=a[k]%i;//庄家往后t个人
if(!t)t=i;
if(j<t)f[i][j]+=f[i-1][j+i-t]/m;//必须除以m
if(j>t)f[i][j]+=f[i-1][j-t]/m;
}
}
}
for(int i=1;i<=n;i++){
printf("%.2lf%% ",f[n][i]*100.0);
}
return 0;
}

浙公网安备 33010602011771号