CF482C Game with Strings
Solution
观察发现 \(m\leq 20\) ,那么我们可以想到状压DP。
将位置压起来,设 \(f_i\) 表示此时状态为 \(i\) ,距离确定一个字符串所需要的期望,那么可得:
\[f_i=\sum_{!(i\And(1<<j)}\frac {f_{i|(1<<j)}}{tot}+1
\]
其中 \(tot\) 表示状态 \(i\) 中还有几个位置没问(也就是还有几个 \(0\) ),那因为询问剩余位置的概率相同,所以此方程是对的。
但是每次询问不一定就能唯一一个字符串,可能是好几个,所以需要处理出来对于每一个状态能确定几个字符串。
那么设 \(num_i\) 表示符合 \(i\) 这个状态的字符串的数量,可得:
\[f_i=\sum_{!(i\And(1<<j)}\frac {f_{i|(1<<j)}}{tot}\times \frac {num_{i|(1<<j)}}{num_i}+1
\]
详细解释一下 \(\dfrac {num_{i|(1<<j)}}{num_i}\) :首先设 \(k=i|(1<<j)\) ,那么 \(num_k\leq num_i\) ,因为 \(k\) 多了一位确定的。而 \(i\) 有 \(num_k\) 种可能变成 \(k\) ,剩下的 \(num_i-num_k\) 是直接得到唯一确定字符串的,所以继续转移的可能就是 \(\dfrac {num_{i|(1<<j)}}{num_i}\) 。
好,转移方程得到了,怎么求得 \(num\) 数组呢?
这个时候,就要再来一个辅助的东西——设 \(unf_i\) 表示状态为 \(i\) 的情况下,哪些字符串是符合的,或者说是不能直接确定的(这里是拿二进制状态存串是否满足条件),因为在 \(i\) 这个状态下,某几个串表达的东西可能一样。
对于 \(unf\) 数组,直接 \(O(n^2)\) 暴力匹配即可,然后从大到小更新。
那么 \(num_i\) 就是 \(unf_i\) 中 \(1\) 的数量了。
注意:代码中状压时枚举要往后错一位。
Code
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=(1ll<<20)+10,M=55;
int m,n,unf[N],num[N];
char s[M][M];
double f[N];
signed main(){
scanf("%lld",&n);
for(int i=1;i<=n;i++) scanf("%s",s[i]+1);
m=strlen(s[1]+1);
for(int i=1;i<=n;i++)
for(int j=i+1;j<=n;j++){
int tmp=0;
for(int k=1;k<=m;k++)
if(s[i][k]==s[j][k]) tmp|=(1ll<<(k-1));
unf[tmp]|=(1ll<<(i-1)); unf[tmp]|=(1ll<<(j-1));
}
for(int i=(1ll<<m)-1;i>=1;i--)
for(int j=1;j<=m;j++)
if(i&(1ll<<(j-1))) unf[i^(1ll<<(j-1))]|=unf[i];
for(int i=0;i<(1ll<<m);i++)
for(int j=1;j<=n;j++)
if(unf[i]&(1ll<<(j-1))) num[i]++;
for(int i=(1ll<<m)-2;i>=0;i--){
if(!num[i]) continue;
int tot=m;
for(int j=1;j<=m;j++)
if(i&(1ll<<(j-1))) --tot;
for(int j=1;j<=m;j++){
if(i&(1ll<<(j-1))) continue;
f[i]+=
f[i|(1ll<<(j-1))]/(double)tot*((double)num[i|(1ll<<(j-1))]/(double)num[i]);
}
f[i]+=1.0;
}
printf("%.10lf\n",f[0]);
return 0;
}