读完题可以很容易的想到暴力模拟,枚举每一张卡片,然后看谁被淘汰,去掉被淘汰的人后从他的下一个人开始重复上面的操作,直到剩下一个人,那么一个人获胜的概率就是他赢的状态数除以总状态数,复杂度为 O ( n ! ) O(n!) O(n!),直接 T T T飞。
考虑优化,我们发现复杂度主要是被人的编号所约束,因为我们在考虑一个人获胜的情况时,还具体考虑了其他人存活的情况和是哪个人现在坐庄。事实上这些考虑都是多余的,因为卡牌上数字的意思是当前坐庄的人向后第几个,并不是人的编号,所以只要我们知道现在是第几轮(或者知道现在还剩下多少人),坐庄的是第几个,要求胜利的人处在第几个位置,那么就可以枚举卡片,找到被淘汰的人的位置,然后根据被淘汰的人的位置推出下一轮坐庄的人的位置和要求胜利的人的位置(若被淘汰的人是要求胜利的人,那么跳过这次枚举的卡片),这样就可以快速地进行转移了。
令 f [ i ] [ j ] [ k ] f[i][j][k] f[i][j][k]表示当前是第 i i i轮(或者也可以定义为还剩有 i i i个人,不过代码略有改动),坐庄的是第 j j j个,要求胜利的人处在第 k k k个位置时获胜的概率。对于每一个情况,枚举卡牌进行转移,令当前卡牌淘汰的人是第 p p p个,那么分下面几种情况:
-
p = k p=k p=k,直接跳过。
-
p < k p<k p<k,那么第 p p p个人被淘汰后,下一轮坐庄的是第 p p p个人(注意不要弄混淆,一个人被淘汰后,后面的人的位置会往前移),要求胜利的人处在第 k − 1 k-1 k−1个位置,那么转移方程为:
f [ i ] [ j ] [ k ] = f [ i ] [ j ] [ k ] + f [ i + 1 ] [ p ] [ k − 1 ] × 1 m f[i][j][k]=f[i][j][k]+f[i+1][p][k-1]×\frac{1}{m} f[i][j][k]=f[i][j][k]+f[i+1][p][k−1]×m1
- p > k p>k p>k,那么要求胜利的人处在原来的位置,若 p p p为当前位置最后的人,则下一轮坐庄的是第 1 1 1个人,否则还是第 p p p个,那么转移方程为:
{ f [ i ] [ j ] [ k ] = f [ i ] [ j ] [ k ] + f [ i + 1 ] [ p ] [ k ] × 1 m ( p ≠ n − i + 1 ) f [ i ] [ j ] [ k ] = f [ i ] [ j ] [ k ] + f [ i + 1 ] [ 1 ] [ k ] × 1 m ( p = n − i + 1 ) \begin{cases} f[i][j][k]=f[i][j][k]+f[i+1][p][k]×\frac{1}{m}(p≠n-i+1)\\f[i][j][k]=f[i][j][k]+f[i+1][1][k]×\frac{1}{m}(p=n-i+1) \end{cases} {f[i][j][k]=f[i][j][k]+f[i+1][p][k]×m1(p=n−i+1)f[i][j][k]=f[i][j][k]+f[i+1][1][k]×m1(p=n−i+1)
注意:下面所有代码里玩家的编号从0开始,讲解中玩家的编号从1开始
复杂度约为 O ( n 3 m ) O(n^3m) O(n3m)
#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
using namespace std;
const int N=51;
int n,m,card[N];
double f[N][N][N];
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=m;i++)scanf("%d",&card[i]);
for(int i=0;i<n;i++)f[n][0][0]=1;
for(int i=n-1;i>=1;i--)
for(int j=0;j<n-i+1;j++)
for(int k=0;k<n-i+1;k++)
for(int p=1;p<=m;p++)
{
int kill=(j+card[p]-1)%(n-i+1);
if(kill==k)continue;
if(kill==n-i)f[i][j][k]+=f[i+1][0][k]/m;
else if(kill<k)f[i][j][k]+=f[i+1][kill][k-1]/m;
else f[i][j][k]+=f[i+1][kill][k]/m;
}
for(int i=0;i<n-1;i++)printf("%.2lf%% ",f[1][0][i]*100);
printf("%.2lf%%\n",f[1][0][n-1]*100);return 0;
}
$\ $
尝试能不能进一步优化,观察代码发现只有可能优化掉对坐庄的人的枚举,因为另外两维都很重要,若坐庄人的位置在每一轮都是同样的位置,那么就能优化掉。不过坐庄人的位置一直在变,怎么优化?事实上我们需要的只是剩余人数和坐庄人与要求获胜的人的相对位置,所以我们可以假设每次坐庄人都在第一个位置,转移的话就是相当于把下一次的坐庄人的位置看成是第一个,那么要求胜利的人的位置也要相对移动。相对移动的位置自己画一下就能发现规律,定义 f [ i ] [ j ] f[i][j] f[i][j]表示当前是第 i i i轮(或者定义为还剩有 i i i个人,代码会略有改动),要求胜利的人处在第 j j j个位置时获胜的概率,令 p p p为被淘汰的人的位置,下面给出转移:
{ f [ i ] [ j ] = f [ i ] [ j ] ( p = j ) f [ i ] [ j ] = f [ i ] [ j ] + f [ i + 1 ] [ j − p ] × 1 m ( p < j ) f [ i ] [ j ] = f [ i ] [ j ] + f [ i + 1 ] [ n − i + 1 − p + j ] × 1 m ( p > j ) \begin{cases} f[i][j]=f[i][j](p=j)\\f[i][j]=f[i][j]+f[i+1][j-p]×\frac{1}{m}(p<j)\\f[i][j]=f[i][j]+f[i+1][n-i+1-p+j]×\frac{1}{m}(p>j)\end{cases} ⎩⎪⎨⎪⎧f[i][j]=f[i][j](p=j)f[i][j]=f[i][j]+f[i+1][j−p]×m1(p<j)f[i][j]=f[i][j]+f[i+1][n−i+1−p+j]×m1(p>j)
复杂度约为 O ( n 2 m ) O(n^2m) O(n2m)
#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
using namespace std;
const int N=51;
double f[N][N];
int n,m,card[N];
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=m;i++)
scanf("%d",&card[i]);f[n][0]=1;
for(int i=n-1;i>=1;i--)
for(int j=0;j<n-i+1;j++)
for(int k=1;k<=m;k++)
{
int kill=(card[k]-1)%(n-i+1);
if(kill==j)continue;
if(kill<j)f[i][j]+=f[i+1][j-kill-1]/m;
else f[i][j]+=f[i+1][n-i-kill+j]/m;
}
for(int i=0;i<n-1;i++)
printf("%.2lf%% ",f[1][i]*100);
printf("%.2lf%%\n",f[1][n-1]*100);
return 0;
}