真是一道神题=。=做了一上午
题目大意:给你N个等长字符串(N<=50),长度<=20,对方在其中等概率选择一个字符串,你每次等概率地询问一个位置的字符(不重复),问能够知道对方选择的是哪个字符串的期望询问次数。
因为长度小于等于20,所以可以比较显然地想到状压DP。接下来YY一下转移方程、
首先我们需要知道,按照当前猜的位置,能否得到答案。那么用一个数组f[i]表示猜的状态是f[i]时,还没有猜出的字符串的状态、
我们可以先找到哪些操作不能区分两个字符串。那么我们枚举两个字符串的序号i,j,那么对于每一位s[j][place]==s[i][place],我们都无法区分,则将当前猜测的状态或上1<<place。最终的状态也要或上(1<<j)|(1<<i)。
那么显然,f[i]=所有(1<<j)==0的f[i+(1<<j)]的异或和。这是因为如果f[i+(1<<j)]猜不出某个字符串,那么f[i]一定也猜不出。
这样,我们就求出了f数组。
接下来进行dp。先想一想方程怎么写:
首先,我们发现:当每个位置都猜出来了,也就是f[i]=0的情况,dp[i]也为0。然后,对于每一个没有猜过的位置,我们都要猜一次。所以先算出还有多少个位置没猜,记为tot。还有就是,由于对方选择的字符串也是随机的,所以要考虑每猜一次都有可能得到答案的情况。令cnt(i)为状态i中1的个数,接下来就可以转移了。
dp[i]=1+∑(zt&(1<<i)==0)dp[zt|(1<<i)]/tot*cnt(qf[zt|(1<<i)])/cnt(qf[zt])。
这是因为有cnt(qf[(1<<i)])/cnt(qf[zt])的概率我们要继续猜下去,剩下的概率是选择的字符串已经被区分出来了,贡献是0。
顺便说一句,一开始我用枚举每一位求cnt,死活TLE。最后看别人代码的时候,发现有个函数可以直接求int变量二进制位中1的个数。。。
那么总的复杂度就是O(2^L*L)啦,终于能跑过了。
附代码
#include<bits/stdc++.h>
#define N 55
#define L 20
using namespace std;
double dp[1<<L];
int d[1<<L],n;
char s[N][L+1];
long long qf[1<<L];
int done,zt,l;
inline double cnt(long long u){
return (double)__builtin_popcount(int(u%(1<<25)))+__builtin_popcount(int(u/(1<<25)));
}
int cx[300];
int main(){
scanf("%d",&n);
for(int i=0;i<n;i++) scanf("%s",s[i]);
if(n==1){
puts("0");
return 0;
}
qf[0]=(1ll<<n)-1;
l=strlen(s[0]);
for(int i=0;i<n;i++){
for(int j=i+1;j<n;j++){
int sav=0;
for(int k=0;k<l;k++)
if(s[i][k]==s[j][k]) sav|=(1<<k);
qf[sav]|=(1LL<<i)|(1LL<<j);
}
}
for(int i=(1<<l)-1;i>=0;i--)
for(int j=0;j<l;j++)
if((i>>j)&1)
qf[i^(1<<j)]|=qf[i];
for(int zt=(1<<l)-1;zt>=0;zt--){
if(qf[zt]==0) continue;
int tot=0;
for(int i=0;i<l;i++)
if(!(zt&(1<<i))) tot++;
dp[zt]=1;
for(int i=0;i<l;i++)
if(!(zt&(1<<i))) dp[zt]+=dp[zt|(1<<i)]/tot*cnt(qf[zt|(1<<i)])/cnt(qf[zt]);
}
printf("%.15lf\n",dp[0]);
return 0;
}