首先考慮一定會對的DP方法:設dp[S][i]代表當目前猜的位置的二進制表示為S,且所選的答案為第i個字串時,期望還要再猜幾次。計算這個DP值的方法並不難,如果S裡的這些位置已經可以唯一確定第i個字串的話,他的DP值就是0,否則\displaystyle dp[S][i]=1+\frac{1}{k}\sum_{j} dp[S| (2^j)][i],其中j滿足他是還沒被猜過的位置,k是還沒被猜過的位置個數。最後答案就是\displaystyle \frac{1}{n}\sum_{i=0}^{n-1} dp[0][i]。但這樣時間和空間都會爆掉,所以需要改進。
事實上我們可以把這些DP值的第2維壓起來,也就是令dp2[S]=dp[S][0]+...+dp[S][n-1],而因為當S能唯一辨別第i個字串時dp[S][i]=0,也就是我們可以把剛才的轉移式改寫成:若S能唯一辨別第i個字串,則\displaystyle dp[S][i]=\frac{1}{k}\sum_{j} dp[S| (2^j)][i](其實他就是0=0+...+0)。那麼由這條式子就可以推出dp2的轉移式了,他會是\displaystyle dp2[S]=num[S]+\frac{1}{k}\sum_{j} dp[S| (2^j)],其中num[S]代表S辨別不出來的字串的個數。
所以問題轉換為要如何求出所有的num[S],考慮任兩個字串s_i,s_j,設s_i和s_j共同的部份的集合為S0,那麼所有S0的子集合都沒辦法辨識出s_i和s_j。因此如果對於每個i,找出所有j對應的S0(當然j\neq i),那麼所有這些集合的子集合的聯集就是所有沒辦法辨識出s_i的集合。可以用DFS來處理這個問題,設in[S]代表S這個集合已經在剛才那些集合的聯集裡了,那麼所有S的子集合也會在裡面,因此當我們多加入一個集合,準備把他的所有子集合的in值設成1的時候,就直接DFS下去,並且遇到in值已經是1的數就直接return 就好了,這樣複雜度會是O(n2^m),其中m是字串的長度。
但這樣傳上去TLE了,後來看解才知道,其實可以令d[S]代表「用S沒辦法辨識出的字串集合」,那麼當我們找到s_i和s_j的S0時,讓d[S0]|=(2^i)根(2^j),這樣就得到了初步的d值們,但我們還要求,如果S2是S1的子集合,且S1不能辨識i,那麼S2也不行,而這只要按照數字大到小,把d[S]的值拿去 or 上所有d[S']的值就可以了,其中S'是包含S且他們只差一個bit的集合。最後看d[S]有幾位是1就知道他不能辨別多少字串了。
code :
#include<bits/stdc++.h> #define DB double #define LL long long using namespace std; const int maxn=20,maxm=50 ; int n,m,num[1<<maxn] ; char s[maxm][maxn] ; LL bit[1<<maxn] ; void getnum() { for(int i=0;i<(1<<n);i++) num[i]=m ; for(int i=0;i<m;i++) for(int j=i+1;j<m;j++) { int same=0 ; for(int k=0;k<n;k++) if(s[i][k]==s[j][k]) same|=(1<<k) ; bit[same]|=(1LL<<i) ; bit[same]|=(1LL<<j) ; } for(int i=((1<<n)-1);i>=0;i--) { for(int j=0;j<n;j++) if(!(i&(1<<j))) bit[i]|=bit[i^(1<<j)] ; num[i]=__builtin_popcountll(bit[i]) ; } } DB dp[1<<maxn] ; DB DP(int S) { if(dp[S]>=0) return dp[S] ; DB &ans=dp[S] ; ans=0 ; int k=0 ; for(int i=0;i<n;i++) if(!(S&(1<<i))) k++ , ans+=DP(S^(1<<i)) ; ans=ans/k+num[S] ; return ans ; } main() { scanf("%d",&m) ; for(int i=0;i<m;i++) { scanf("%s",s[i]) ; if(!i) n=strlen(s[i]) ; } getnum() ; fill(dp,dp+(1<<n),-1) ; dp[(1<<n)-1]=0 ; printf("%.15f\n",DP(0)/m) ; }
沒有留言:
張貼留言