题目链接:https://www.acwing.com/problem/content/4012/
题目分析
这道题涉及到概率问题,从数据的规模我们也容易看出,这是一个状态压缩dp算法的题目。做这类题目的话难点就在于如何定义状态以及状态间如何转移。(如果不了解状态压缩dp算法,建议了解后再看)
状态定义
这道题的状态其实比较明显,一个维度是手上卡牌的集合,一个维度是手上硬币的数量。如果知道这两个维度了,那么是可以确定该状态下的抽卡次数v的。而如果还知道到达该状态的概率p的话,那么就可以通过p*v得到到达该状态的期望抽卡次数。那么可以定义:
dp[s][j]:手中牌的集合为s,硬币数量为j的情况的概率
状态转移
对于状态dp[s][j],遍历所有最后一次可能选择的(包含在s中的)卡牌。假设选择的是i号卡牌,那么两种情况:
1、当前有硬币,i号卡牌可以作为一枚硬币,这样上个状态就是dp[s][j-1](手中牌一样,但是硬币少一个)。
2、不论当前是否有硬币,i号卡牌都可以作为一张新牌,这样上一个状态就是dp[ s ^ (1<<i) ] [ j ] ( 手中的牌去掉i号牌 ,硬币不变 )。
很明显,dp[s][j]=所有的上个状态的值*选中这种状态的概率(没读懂的话看代码)。
但是,还需要注意一点,那就是:无法从已经结束的状态转移过来。已经结束的意思就是,如果s中的卡牌数加上使用 j 个硬币可以换的卡牌数 大于等于了 n。
结果的导出
要计算结果,就需要对所有处于结束状态的 dp[s][j]进行处理。
处于结束状态的条件:
n<= s中的卡牌数加上使用 j 个硬币可以换的卡牌数 <n+1
AC代码:
#include<iostream>
#include<cstdio>
using namespace std;
const int MAX_N=16;
double dp[1<<MAX_N][80];
double p[MAX_N];
int main()
{
int n,k;
cin>>n>>k;
for(int i=0;i<n;i++)cin>>p[i];
dp[0][0]=1;
for(int s=1;s!=(1<<n);s++)
{
int cnt=0,t=s;//cnt统计s二进制下1的个数,即该状态手上卡牌的张数
while(t)
{
cnt+=t&1;
t>>=1;
}
for(int j=0;j<=(n-1)*k;j++)
{
for(int i=0;i<n;i++)
{
if((1<<i)&s)//i号卡牌在集合s中
{
if(j>0&&cnt+(j-1)/k<n)dp[s][j]+=p[i]*dp[s][j-1];//cnt+(j-1)/k<n避免从结束状态继续转移
if(cnt-1+j/k<n)dp[s][j]+=p[i]*dp[s^(1<<i)][j];//cnt-1+j/k<n也是如此
}
}
}
}
double ans=0;
for(int s=1;s!=(1<<n);s++)
{
int cnt=0,t=s;
while(t)
{
cnt+=t&1;
t>>=1;
}
int j=(n-cnt)*k;
while(j<=(n-1)*k&&j<(n-cnt+1)*k)//仅处理处于结束状态的
{
ans+=(cnt+j)*dp[s][j];
j++;
}
}
printf("%.10f",ans);
return 0;
}