题目链接
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;
}