CSP 收集卡牌 状压dp (详细解析)

题目链接: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;
}
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值