状态压缩 + 概率问题 (first one)

链接:http://acm.hdu.edu.cn/showproblem.php?pid=5816

题意: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;
}







评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值