题意:T组数据,每组数据先是三个数,分别是敌人的血量,a类卡片的个数,b类卡片的个数。接下来m个数,为b类卡片能给敌人造成伤害数。摸到a类卡片可以从所有卡片中再抽两张,如果剩余卡片数不足两张,这个时候就只抽一张。问在你这一轮能把怪物打死的可能性是多少?要求是p/q的形式
解析:一个小点:如果我们抽一张b卡片,怪物就会死掉,那么这个概率怎么算呢。此时应该在总概率上加上1/m,但如果这么算,最后的值肯定是一个小数,我们可以认为在后面的所有可能中,它的概率是1.实际上就要乘上后面所有卡片的全排列,来保证除以(n+m)!时,后面的部分概率是一。
题解的思路是抽到不能再抽时算方案数。这样算方案数可以保证是不重不多的。不能再抽又分两种情况:1是没有牌了,a只能再抽一个了。另一种是b比a多一。(如果抽到a就抽两张b,如果又抽到一张a,a抽到 两张b,此时就是两张a,3张b)
利用状态压缩,枚举全部的方案。如果该种方案是能够杀死怪物的,就加上抽到不能再抽时的方案数。这个方案数是抽取k张b卡片,k-1张a卡片的方案数,乘上k-1张a卡片的全排列,k张b卡片的全排列,以及从n张卡片中选择k-1张的组合数,和剩下未抽取的全排列数。如果是没有牌可抽了(此处默认如果用了所有的牌,就一定能杀死怪物)那么,就是n张a,k张b的方案数,乘以从n张选n张a的组合数1,乘以n张a的全排列,m张b的全排列,剩下没有卡片了,不用乘。这种状态是和上一种区分开来的,这一种中a卡片的数量一定是多于m-1的,n > m - 1 与 k - 1 和k之间的方案数是一定没有重叠的。
自己仿照的代码:
//注意原数组,子集数组,统计二进制一的个数的数组:都要开到2^n
#include <iostream>
#include <cstdio>
#include <algorithm>
using namespace std;
long long f[30],dp[30][30];
int bc[1 << 22],C[30][30],sum[1 << 22],a[1 << 22];
typedef long long lli;
lli gcd(lli a, lli b)
{
if (b == 0) return a;
return gcd(b, a % b);
}
void init()
{
//注意这里要开到i的20次方才对
bc[0] = 0;
for(int i = 1; i<(1<<20); i ++)
{
bc[i] = bc[i - (i &-i) ] + 1;
//bc[i] = bc[i^(i&-i)] + 1;
}
f[0] = 1;
for(int i = 1; i < 22; i ++)
{
f[i] = f[i - 1] * i;
}
dp[0][0] = dp[0][1] = 1;//最初 的状态只取一张b牌一种可能性
for(int i = 1; i < 22; i ++)
{
dp[i][0] = 1;
for(int j = 1; j < i; j ++)
{
dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
}
dp[i][i] = dp[i][i + 1] = dp[i][i - 1];
}
C[0][0] = 1;
for(int i = 1; i < 22 ; i ++)
{
C[i][0] = C[i][i] = 1;
for(int j = 1; j < i; j ++)
{
C[i][j] = C[i - 1][j] + C[i - 1][j - 1];
}
}
}
int main()
{
//freopen("in.txt","r",stdin);
// freopen("out.txt","w",stdout);
init();
int T;
scanf("%d",&T);
while(T--)
{
bool tflag = false;
int summ = 0;
int p,n,m;
scanf("%d%d%d",&p,&n,&m);
for(int i = 0; i < m; i ++)
{
scanf("%d",&a[(1 << i)]);
summ += a[(1 << i)];
}
if(summ >= p)
tflag = true;
if(tflag == false)
cout<<"woyaowa"<<endl;
sum[0] = 0;
for(int i = 1; i < (1 << m); i ++)
{
sum[i] = sum[i - (i & -i)] + a[i & -i];
}
long long up = 0,down = f[m + n];
for(int i = 0; i < (1 << m); i ++)
{
if(bc[i] <= n + 1 && sum[i] >= p)
{
up += dp[bc[i] - 1][bc[i]] * C[n][bc[i] - 1] * f[bc[i]] * f[bc[i] - 1] * f[n + m - 2*bc[i] + 1];
}
if(bc[i] == m && bc[i] < n + 1&& tflag )
up += dp[n][m] * f[n] * f[m];
}
long long tm = gcd(up,down);
printf("%I64d/%I64d\n",up/tm,down/tm);
}
return 0;
}
附官方题解:这题其实有O(2^M)的做法. 方法用f[i][j]表示A类牌和B类牌分别抽到i张和j张,且抽牌结束前保证i>=j的方案数,这个数组可以用O(n^2)的dp预处理得到. 接下来枚举B类牌的每个子集,如果这个子集之和不小于P,用k表示子集的1的个数,将方案总数加上取到这个集合刚好A类卡片比B类卡片少一(过程结束)的方案数:f[k-1][k] * C(n, k - 1) * (k - 1)! * k! * (n + m – 2*k + 1)! . 如果子集包含了所有的B类卡片,则还需要再加上另一类取牌结束的情况,也就是取完所有牌,此时应加上的方案数为f[n][m] * n! * m! . 最后的总方案数除以(n+m)!就是答案.
对标程的注释,本人渣渣,状态压缩都不会。。。。
#include <cstdio>
#include <iostream>
using namespace std;
typedef long long lli;
int bc[1<<20], sum[1<<20], tmp[1<<20];
int C[21][21];
lli f[21][21], fact[21];
lli gcd(lli a, lli b)
{
if (b == 0) return a;
return gcd(b, a % b);
}
void init()
{
int i, j;
//bc[i]表示i的二进制表示中一的个数是多少
bc[0] = 0;
for (i=1; i<(1<<20); i++)
bc[i] = bc[i-(i&-i)] + 1;
//fact[i]表示i的全排列
fact[0] = 1;
for (i=1; i<=20; i++) fact[i] = fact[i-1] * i;
//C表示组合数C[n][m]n表示下面的
C[0][0] = 1;
for (i=1; i<=20; i++)
{
C[i][0] = C[i][i] = 1;
for (j=1; j<i; j++) C[i][j] = C[i-1][j] + C[i-1][j-1];
}
//i张a卡片,j张b卡片正确实现的方案数
//当前的方案数,等于i-1 , j 的方案数 + i,j - 1的方案数
//由于正确的方案数来说 当a为k - 1时,B最多为k,让B更大时的方案数等于为k时的方案数,为了后面的使用(大概吧)
f[0][0] = f[0][1] = 1;
for (i=1; i<=20; i++)
{
f[i][0] = 1;
for (j=1; j<i; j++)
{
f[i][j] = f[i-1][j] + f[i][j-1];
}
f[i][i] = f[i][i+1] = f[i][i-1];
}
}
int main()
{
freopen("in.txt","r",stdin);
lli a, b, d;
int p, n, m, t, i;
init();
scanf("%d", &t);
while (t --)
{
scanf("%d %d %d", &p, &n, &m);
for (i=0; i<m; i++) scanf("%d", &tmp[1<<i]);//tmp存储的时候就是按照1左移存储的
//按照一定规则枚举所有sum的和
sum[0] = 0;
for (i=1; i<(1<<m); i++)//这个遍历不存在一张都不选的这种情况
sum[i] = sum[i-(i&-i)] + tmp[i&-i];//每次都保证减去最低位后的状态是已经求出的,或者状态是空sum[0]。比如从1000开始到1001(1000求出)到1010(1000求出)到1011(减去最低位后为1010也已求出)。那么为什么地位减去后一定是已求出的呢,因为减去小于其本身,sum[i]又是顺序遍历的
//此处还有问题,为什么是不重不漏的呢?每次到临界值(1,10,100,1000)时sum[i]的值都等于tmp[i].当减去最低位后,不肯能是等于它本身的,此时又再加上了另一个数,
//注意此处的1,0是二进制。实际上是sum[1] = tmp[1],sum[10] = tmp[10],sum[11] = sum[10] + temp[1] = temp[10] + temp[1],以此类推sum[1111] = temp[1] + temp[10] + temp[100] + temp[1000]
//其实很简单的啦,每次sum[i]表示的都是对于i的二进制数来说0,代表取到1代表取不到。肯定是全部的状态(不包括都取不到)
for (i=0; i<(1<<m); i++)//k = bc[i].表示从所有b中选取n个b卡片,实际卡片的个数。此时b选择哪几个卡片是确定的,而a选那几个是不确定的,所以需要乘以a的组合数。并对选取的a卡片和b卡片全排列,再将剩下未选择的卡片全排列(因为后面的概率是1了,剩下的全排列除以(n + m)!保证是1)
{
if (sum[i] >= p && bc[i] <= n + 1)
{
a += C[n][bc[i]-1] * f[bc[i]-1][bc[i]] * fact[bc[i]-1] * fact[bc[i]] * fact[n+m-2*bc[i]+1];
//还有一种状态是所有的卡片都取光了,既然都取光了,那么对于A类卡片的组合数一定是1,那么他们的方案数也应该是取n张a卡片,m张b卡片。再分别对他们全排列,而已经取光了,后面就不再取了
//在这种状态中我们可以看到b中的卡片都被取光了,而a中依旧有剩余的卡片,那么剩下的a中剩余的卡片一定会被取光。最终的结果就是a,b都取光了
//在这种情况下我们可以看到b都取关了,而a还有剩余。那就意味着n > m+1,与计算k-1和k,k+1这种情况是不同的
if (bc[i] == m && bc[i] < n + 1) a += f[n][m] * fact[n] * fact[m];
}
//本质上这两种情况都是意味着不能再抽牌了
}
b = fact[n+m];
d = gcd(a, b);
printf("%I64d/%I64d\n", a / d, b / d);
}
return 0;
}