《新概念字符串哈希》
大意给你\(2*N\)个字符串集合,每个集合有\(L\)个长度为4的字符串片段,这些片段满足只有最多不超过\(M\)种,现告诉你里面有正好\(N\)对不互相同的集合,使得这一对内满足:相同的字符串片段数正好为\(\cfrac{L}{2}\)。
找出每个集合对应的那另一个集合。
\(N,M\le 2e4\quad L\le 200 \quad 保证字符串为随机生成\)
我们首先可以想到字符串哈希,那如果一个一个去判断的话时间复杂度会飙升至\(\Theta(N^2L)\)这肯定会T飞,所以我们考虑优化掉那个全判断一遍的\(N\)。
我们可以先考虑同种字符串片段在同源的集合中出现且为哈希值最小值的概率为\(\cfrac{1}{3}\),因为 \(\quad不同的片段数量:相同的片段数量=2:1\quad\),再考虑在非同源集合中出现且为哈希值最小值的概率为\(\cfrac{1}{L}\),因为每一种出现次数的期望为\(\cfrac{N*L}{M}\approx L\)。
我们取两种不同的哈希,以他们的最小值先快速筛同源,同源两两筛到一起的概率便为\(\cfrac{1}{9}\),非同源两两筛到一起的概率便为\(\cfrac{1}{L^2}\approx \cfrac{1}{N}\)
只根据一个最小值判断肯定会有大量误判,但是只要我们对最终答案的判断为严谨的比对,得到的就一定为正确的答案,至于时间,可以手算证明每次减少需要判断答案的数量的\(\cfrac{1}{9}\)只需要不到\(100\)次即可将\(2e5\)数量级的全处理完。
致命的常数优化(这玩意虽然叫常数优化但是它能直接把我从0分全T的情况变成90分):
对于其中一种哈希,我们只是在排序时用到它,那我们考虑它重复的不会非常多,只保留前一部分也可以很好的完成排序,在初始化的时候便可以考虑只处理前\(\cfrac{3N}{L}\)小的数;
同时我们考虑判断同源的虫时如果在两两判断的前期还没有判出一对相同的字符串,那我们大概率是可以直接跳过了,但由于确实可能存在后面一样的情况,所以在数量级变小以后就不能再这么快速排除,以免反复排除正确情况导致无法得到答案。
时间复杂度 \(\Theta(NL)\)
#include<bits/stdc++.h>
using namespace std;
int n,m,l,hash_n=1,mod=1e6+3,base=128;
int hsh[2000000],hnxt[40000],hkey[40000];
int h1[40000][200],h2[40000][200];
int wordpos[8000000],wordcnt[40000];
int rd[40000],work[40000],level[40000],unf,ans[40000];
string c;
bool cmp(int a,int b){
for(int i=1;i<=l;i++)
if(h1[a][i]!=h1[b][i])return h1[a][i]>h1[b][i];
return false;
}
int getid(int key){//把哈希值转回到 1~n之间
int h=key%mod;//以便于使用数值
if(h<0)h+=mod;
for(int i=hsh[h];i;i=hnxt[i]){
if(hkey[i]==key)return i;
}//hkey记录每个地址对应的真正哈希值
hkey[hash_n]=key;//防止冲突以后出现错误
hnxt[hash_n]=hsh[h];//hnxt记录冲突以后该跳到哪个位置
hsh[h]=hash_n;
return hash_n++;
}
int main(){
freopen("dna.in","r",stdin);
freopen("dna.out","w",stdout);
cin>>n>>m>>l;
n*=2;
cin>>c;
for(int i=1;i<=n;i++){
for(int j=1;j<=l;j++){
for(int k=1;k<=4;k++){
h1[i][j]*=base;
h1[i][j]+=(int)c[((i-1)*l+j-1)*4+k-1];//字符串转哈希值
}
}
}
for(int i=1;i<=n;i++){
for(int j=1;j<=l;j++){
wordcnt[h1[i][j]=getid(h1[i][j])]++;
}//重新赋值的同时统计各种字符串的出现次数
}
m=hash_n;
for(int i=2;i<m;i++)wordcnt[i]+=wordcnt[i-1];
for(int i=1;i<=n;i++){
for(int j=1;j<=l;j++){
wordpos[--wordcnt[h1[i][j]]]=i;
}//记录每种字符串分别在哪个集合里出现过
}
wordcnt[m]=n*l;
for(int i=1;i<m;i++){
rd[i]=i;
}//初始化随机数组
for(int i=1;i<=n;i++){
work[i]=i;//初始化两两判断的数组,
}//一定是每个数都出现,否则就会少判
unf=n;
for(int i=1;i<m;i++){
for(int j=wordcnt[i];j<wordcnt[i+1];j++){
h2[wordpos[j]][++level[wordpos[j]]]=i;
}//把 h1中的值按降序赋给 h2
}
while(unf>0){
random_shuffle(rd+1,rd+1+m);
for(int i=1;i<=n;i++)level[i]=0;
for(int i=1;i<min(m,max(100,m/l*3));i++)
for(int j=wordcnt[rd[i]];j<wordcnt[rd[i]+1];j++)
h1[wordpos[j]][++level[wordpos[j]]]=rd[i];//这里其实是按赋值的顺序算大小的,就省去重算哈希值的步骤了
sort(work+1,work+unf+1,cmp);//按第一种哈希判同源可能
int res=0;
for(int i=1;i<=unf;i++){
int s=0,x1=work[i],x2=-1;
if(i<unf){
x2=work[i+1];
for(int j=1,k=1;j<=l&&k<=l;){
if(h2[x1][j]==h2[x2][k])s++,k++,j++;
else if(h2[x1][j]<h2[x2][k])j++;
else k++;
if(unf>=max(100,n/10)&&j>5&&s==0)break;
}//按第二种哈希判同源可能
}
if(s>=l/2){
ans[x1]=x2;
ans[x2]=x1;
i++;
}
else work[++res]=x1;
}
unf=res;
}
for(int i=1;i<=n;i++)printf("%d \n",ans[i]);
return 0;
}