CodeForces 698C LRU
吐槽一句:这数据造得真强….
题意:有一个大小为k的缓存区,每次从n种物品中按照一定的概率选取一种物品尝试放进去.同一个物品每一次选取的概率都是相同的.如果这种物品已经放进去过就不再放进去.如果缓存区满了就把放进去的时间离现在最远的物品拿出来.问10^100次后每个物品在缓冲区中的概率.
如果正着做的话似乎状态会扩展得很多而且都没什么用.
注意到最后一次放进去的一个物品肯定在缓冲区中,而第一次放进去的物品对结果几乎没有影响,这提示我们关注最后的几次放置.假如缓冲区至少有两个位置,且倒数第二个物品和倒数第一个物品种类不同,那么倒数第二个物品也肯定在缓冲区中.
因此最终缓冲区中存放着我们所放进去的倒数几个物品.那么我们在物品的选取序列中从最后倒着往前扫,数出前k种不同的物品,就是缓冲区中最后存放的k种物品.而选取序列中,每个位置都按照给定的概率出现n种物品.
因此我们可以令F[i][S]表示缓冲区已经拥有了i种物品时拥有的物品集合为S的情况发生的概率.那么枚举所有不在S中的物品.如果所有不在S中的物品的概率之和为P,某种不在S中的物品x被选中的概率为pi,那么拿到的下一种不在S中的物品是x的概率为pi/P(在这之前可能拿了不止一次,也可能拿到过属于S的物品).可以预处理一下被选中的物品属于某个集合的概率以及属于某个集合的物品个数.
提醒一下,这道题的最后一个数据点简直了….我在程序中只使用了集合中包含元素个数=k的状态更新答案,因为我假定无穷次放置之后每个物品至少会拿到一次,又因为k<=n,所以最终缓存区一定是满的.但这个推理不成立,因为” 无穷次放置之后每个物品至少会拿到一次”这个前提是错的.物品被拿到的概率区间是[0,1]闭区间,虽然保证了k<=n,但并没有保证选中概率不为0的物品个数大于等于k,所以可能放不满k个位置,这时选中概率不为0的物品出现概率都是1.对于这种情况,我们需要特判一下.(后来想了想,此时可以直接输出答案,不需要跑DP,又sb了一次)
调的时候我想到k>n的时候不能只考虑集合元素个数为k的情况,但看到数据保证k<=n就没处理.想到了概率可以为0,但只是做了防止除0的处理,并没有想到缓存区可以放不满...思维还是不够缜密啊.
Codeforces评论区里貌似还有两种神做法...一会儿搞一搞 http://codeforces.com/blog/entry/46148
#include<cstdio> long double f[1<<20]; double p[1<<20]; int g[1<<20]; double ans[20]; int main(){ f[0]=1; int n,k;scanf("%d%d",&n,&k); int cnt=n; for(int i=0;i<n;++i){ scanf("%lf",&p[1<<i]); if(p[1<<i]==0)cnt--;//如果概率为0的物品太多,可能放不满k个 } if(cnt<k)k=cnt; int lim=1<<n; for(int i=1;i<lim;++i){ p[i]=p[i&(-i)]+p[i^(i&(-i))];//printf("%f\n",p[i]); g[i]=(i&1)+g[i>>1]; } // printf("%f\n",p[lim-1]); for(int i=0;i<lim;++i){//printf("%f\n",f[i]); for(int j=0;j<n;++j){ if(i&(1<<j))continue; if(p[i]!=1)f[i^(1<<j)]+=f[i]*p[1<<j]/(1-p[i]);//防止除零 } if(g[i]==k){ for(int j=0;j<n;++j){ if(i&(1<<j))ans[j]+=f[i]; } } } for(int i=0;i<n;++i)printf("%.7f%c",ans[i],(i==n-1)?'\n':' '); return 0; }
来更新啦
好像其中一个人只是讲了他在打比赛的时候推着推着推回去了的故事…(不要问我是怎么把这看成一个做法的…我英语拙计…)
另一个做法是容斥(别人的高科技代码传送门http://codeforces.com/contest/699/submission/19261510 ).
分别考虑每个物品i.假如这个物品i最后一次出现后,又出现了j个其他物品(j=0,1,2,3,….inf),如果这个物品最后出现了,那么这j个其他物品中最多只能含有k-1种物品.考虑一个含有k-1种物品的集合,假如这个集合中所有物品的概率之和为x,那么上面这些情况发生的概率分别为ai,ai*x,ai*x^2,…..ai*x^inf,概率之和P=ai/(1-x){从P=ai+x*P推出}.如果我们枚举所有含有k-1个物品的集合,把它们的P加起来,将导致大量重复.含有k-2,k-3…个物品的情况都被重复计数.因此我们枚举k-2个物品的集合,每个k-2个物品的集合在每个包含它的k-1个物品的集合中被加上了一次.接下来每个k-3个物品的集合在k-2个物品的集合中被加上,在k-1个物品的集合中被减去…这个容斥的系数可以预处理出来(突然发现自己并不会容斥……)
别人的题解:
#include<cstdio> double a[20]; double p[1<<20]; int g[1<<20]; double C[21][21]; double h[21]; int main(){ int n,k;scanf("%d%d",&n,&k); for(int i=0;i<n;++i){ scanf("%lf",a+i); } int lim=(1<<n)-1; for(int i=0;i<n;++i){ p[1<<i]=a[i]; } for(int i=1;i<lim;++i){ p[i]=p[i&(-i)]+p[i^(i&(-i))]; g[i]=(i&1)+g[i>>1]; } C[0][0]=1; for(int i=1;i<=20;++i)C[i][0]=1; for(int i=1;i<=20;++i){ for(int j=1;j<=i;++j){ C[i][j]=C[i-1][j]+C[i-1][j-1]; } } for(int i=k-1;i>=0;--i){ h[i]=1; for(int j=i+1;j<=k-1;++j){ h[i]-=h[j]*C[(n-1)-i][j-i]; } } // for(int i=0;i<=k-1;++i)printf("%d %f\n",i,h[i]); double ans=0; for(int i=0;i<n;++i){ if(a[i]==0||a[i]==1||k==1){ printf("%.7f%c",a[i],(i==n-1)?'\n':' '); continue; } ans=0; for(int j=0;j<lim;++j){ if((g[j]<k)&&(!(j&(1<<i)))){ // printf("%f\n",H[g[j]]); ans+=h[g[j]]/(1-p[j]); } } printf("%.7f%c",a[i]*ans,(i==n-1)?'\n':' '); } return 0; }