哇塞今天居然做出来了两道TC哎~(≧▽≦)/~(好像这是什么值得高兴的事情一样)
题目描述
给出一个字符串s,问把s中的字符打乱后得到回文串的概率是多少(|s|<=50)
分析
哎呀又是自己想出来的可开心啦
首先我们不考虑精度问题,先计算出打乱之后的所有可能字符串的数量:
设第i个字母在s中出现的次数为vis[i],s的长度为len。考虑把这些字母放进len个空位中
1.按照字典序枚举,先放’a’,如果vis[1]>0,说明原字符串中有’a’,那么我们现在要在len个空位中选择vis[1]个放上’a’,方案数为
,剩下的空位数为len-vis[1]
2. 当我们要接着放第x个字母时,要在len-vis[1]个空位中选vis[x]个放上字母,方案数为
3. 以此类推,设枚举到第i个字符时剩下的空位数为left[i],那么所有可能的字符串数
接着,我们要计算回文串的数量
很重要的一点就是:记有x个字符出现了奇数次,如果x>1,那么这个字符串不论怎么打乱都不可能是回文串。为什么呢?
我们按照原字符串长度len的奇偶性把题目分成两种情况:
- len为奇数: 在最中间的地方放上某一个字符p,然后在左右两边放上其他字母。
- len为偶数: 直接在左右两边放字母
由于回文串是左右对称的,所以只要知道左边的排列,右边的排列也就固定了。所以,假设字符m在左边出现了k次,那么:
- m!=p 它在整个字符串中出现的总次数为k*2=vis[m],vis[m]一定是偶数
- m=p 它在整个字符串中出现的总次数为k*2+1=vis[m],vis[m]为奇数
因此,回文串中出现了奇数次的字符最多只有1个
那么,和之前同理,我们可以计算出回文串出现的方案数,然后除一下就可以了
最后,我们来讨论一下精度问题——显然,总方案数tot是会暴long long的,那么怎么么办呢?这里有一个十分神奇的操作叫做double,它居然可以存到308位。还有一种更加神奇的操作叫做long double,大概几千位都不是问题的吧,所以居然就这么水过了emmm
代码
//tc is healthy, just do it
#include <bits/stdc++.h>
const int N=55;
using namespace std;
template<class T> void checkmin(T &a,const T &b) { if (b<a) a=b; }
template<class T> void checkmax(T &a,const T &b) { if (b>a) a=b; }
class PalindromePermutations {
public:
double palindromeProbability( string word ) ;
};
int n,len,lef,vis[N],a[N],flag=0,cnt=0;
double c[N][N],ans=1,tot=1;
void Init(){
for(int i=1;i<=n;i++)c[i][i]=c[i][0]=1,c[i][1]=i*1.0;
for(int i=3;i<=n;i++)
for(int j=2;j<i;j++)
c[i][j]=c[i-1][j]+c[i-1][j-1];
}
double PalindromePermutations::palindromeProbability(string word) {
n=word.length();
Init();
for(int i=0;i<n;i++)vis[word[i]-'a'+1]++;
len=n;
for(int i=1;i<=26;i++){
if(!vis[i])continue;
if(vis[i]&1)flag++,len=n-1;//注意是-1不是-vis[i],一开始就挂在这里了呢
else cnt++,a[cnt]=vis[i]/2;
}
if(flag>1||len%2==1){
ans=0;
return ans;
}
lef=n;
for(int i=1;i<=26;i++){
if(!vis[i])continue;
tot*=c[lef][vis[i]];
lef-=vis[i];
}//计算总方案数
lef=len/2;
for(int i=1;i<=cnt;i++){
ans*=c[lef][a[i]];
lef-=a[i];
}//计算回文串数
return ans/tot;
}