ZOJ 3777 11th省赛 B Problem Arrangement【状态压缩DP】

题目链接

http://acm.zju.edu.cn/onlinejudge/showProblem.do?problemId=5264

思路

题意就是n个题目,第i个题目放到第j个位置的有趣值是p[i][j],问你随机排列使得有趣值总和大于等于m的期望值是多少。
这个期望值很好算就是n!/cnt。

直接dfs会超时,这里用到了DP,而DP的话,如果设dp[i][j]表示放了前i个题目,有趣值总和为j的方案数的话,会有个很大的问题,就是我们根本不知道dp[i][j]时的排列情况,从而无法计算出dp[i+1]时的有趣值。所以状态中必须要多一维来表示当前的排列状况,然而排列状况最多有12个位置,总不可能开个12维的数组吧…(好像可以?试试?)

最简便的方法是用状态压缩,因为每一位只有“有题目”和“没题目”两种状态,所以完全可以用一个n位的二进制数来表示,这里n<=12所以int绰绰有余。这样这个十二维的数组就瞬间压缩成一个维度了,是不是很奇妙。
于是设计状态dp[i][j] 表示排列状态为i时,有趣值和为j时的方案总数

然后排列状态的表示设计好了,怎么对这个状态进行操作呢,这就牵扯到位运算了。
(i >> k)&1,判断第k+1位是否为1
(1 << k ) | i ,把第k+1位置为1

状态的操作也搞定了,接下来就是状态转移
dp[ i | (1 << k)][j + p[tot + 1][k+1]]+=dp[i][j] foreach (1 >> k)&1==0

转移搞定了,想一下计算顺序,i咋一看应该按照1的个数从少到多来遍历,但这个很难实现。其实可以直接从小到大直接遍历i,为什么呢,因为假设a推出了b,那么b的1的个数肯定比a多一个,换句话说,从b中删掉一个1所形成的数在这之前必须全都遍历完,而假设b删了一个1形成了a,那么肯定a<b,所以只要增序遍历i就能满足。
j的顺序倒无所谓,因为肯定用不到当前i。

边界是dp[0][0]=1

卧槽想了这么多总算能把代码写出来了,于是我兴冲冲地写了一段代码:

dp[0][0]=1
for(int i=0 ; i<=((1<<n)-1) ; ++i)
{
    for(int j=0 ; j<=m ; ++j)
    {
        int tot=count_one(i);
        for(int k=0 ; k<=n-1 ; ++k)
        {
            if(((i>>k)&1)==0)
            {
                int new_i=(i|(1<<k));
                int new_j=j+p[tot+1][k+1];
                if(new_j>m)new_j=m;
                dp[new_i][new_j]+=dp[i][j];
            }
        }
    }
}

然后光荣TLE,(一脸卧槽)。
心灰意冷地去看了看大神们的代码,发现长得差不多?!(卧槽*2)
仔细看了下,发现了关键所在:我的count_one(i)的位置好像有点奇怪….(当时有种拍死自己的冲动)

改成这样就AC了,1200ms:

dp[0][0]=1
for(int i=0 ; i<=((1<<n)-1) ; ++i)
{
    int tot=count_one(i);
    for(int j=0 ; j<=m ; ++j)
    {
        for(int k=0 ; k<=n-1 ; ++k)
        {
            if(((i>>k)&1)==0)
            {
                int new_i=(i|(1<<k));
                int new_j=j+p[tot+1][k+1];
                if(new_j>m)new_j=m;
                dp[new_i][new_j]+=dp[i][j];
            }
        }
    }
}

然后又发现几个小优化。
把k和j的嵌套顺序换一下,这样可以跳过几次j的循环,500ms:

dp[0][0]=1;
for(int i=0 ; i<=((1<<n)-1) ; ++i)
{
    int tot=count_one(i);
    for(int k=0 ; k<=n-1 ; ++k)
    {
        if(i&(1<<k))continue;
        for(int j=0 ; j<=m ; ++j)
        {
            int new_i=(i|(1<<k));
            int new_j=j+p[tot+1][k+1];
            if(new_j>m)new_j=m;
            dp[new_i][new_j]+=dp[i][j];
        }
    }

AC代码

#include <bits/stdc++.h>
using namespace std;


int p[13][13];
int dp[5000][500+10];
int factorial[13];
inline int gcd(int a, int b)
{
    if(a%b==0)return b;
    else return gcd(b,a%b);
}
inline int count_one(unsigned int n)
{
    int cnt=0;
    for(int i=0 ; i<=31 ; ++i)
    {
        if((n>>i)&1)cnt++;
    }
    return cnt;
}

void init()
{
    int fac=1;
    for(int i=1 ; i<=13 ; ++i)
    {
        fac*=i;
        factorial[i]=fac;
    }
    factorial[0]=0;
}
int main()
{
    init();
    int T;
    scanf("%d",&T);
    while(T--)
    {
        memset(dp,0,sizeof dp);
        int n,m;
        scanf("%d%d",&n,&m);
        for(int i=1 ; i<=n ; ++i)
        {
            for(int j=1 ; j<=n ; ++j)
            {
                scanf("%d",&p[i][j]);
            }
        }
        dp[0][0]=1;
        for(int i=0 ; i<=((1<<n)-1) ; ++i)
        {
            int tot=count_one(i);
            for(int k=0 ; k<=n-1 ; ++k)
            {
                if(((i>>k)&1))continue;
                for(int j=0 ; j<=m ; ++j)
                {
                    int new_i=(i|(1<<k));
                    int new_j=j+p[tot+1][k+1];
                    if(new_j>m)new_j=m;
                    dp[new_i][new_j]+=dp[i][j];
                }
            }
        }
        int fac=factorial[n],cnt=dp[((1<<n)-1)][m];
        if(cnt==0)
            printf("No solution\n");
        else
        {
            int g=gcd(fac,cnt);
            printf("%d/%d\n",fac/g,cnt/g);
        }
    }
    return 0;
}

你可能感兴趣的:(ACM)