compress words (KMP/字符串哈希,双哈希)

本文探讨了一种字符串连接方法,避免单词间的重复字段,并利用KMP算法求最长相同前后缀。同时,文章详细阐述了双哈希策略,包括如何处理字符串哈希值的计算和比较,以及在不同编程环境下的注意事项。此外,还分析了字符串操作的时间复杂度,提出了优化技巧,如使用s.size()代替strlen(),以及双哈希的实现和滚动数组优化。
摘要由CSDN通过智能技术生成

在这里插入图片描述
题目链接
这下好了,霍,一打开这篇,目录惊人,一道题这么多问题,显然,某人智商堪忧

连接若干单词,规避单词尾和下一个单词首的重复字段

kmp求最长相同前后缀

交到coedforce上,next数组报错,应该是同名了,保险起见改成nex

 还是对kmp算法的理解不够深刻,成功匹配时j=le,
	不成功,一种是不相等,终究还是主串比完了也没能全部匹配上
	就看最后这个j表示匹配哪儿 
	如果主串最后一个字符也未能与s[0]比较成功,j就会变为-1,继而加一得0 
	想象匹配过程中 
	abcdefgh
		 fgheg 
#include <iostream>
#include <math.h>
using namespace std;
#include <string>
const int N=1e6+10;
string t,s;
int len;//后续输入字符串的长度 
int nex[N];
void getNext(){
	int i=0;
	int j=-1;
	nex[0]=-1;
	while(i<len){
		if(j==-1||s[i]==s[j]){
			i++;
			j++;
			nex[i]=j;
		}
		else j=nex[j];
	}
}
int kmp(int st){
	int i=st,j=0;
	int le=t.size();
	while(i<le){
		while(j!=-1&&t[i]!=s[j])j=nex[j];
		i++;j++;	
	}
//	while(i<le){
//		if(j==-1||t[i]==s[j]){
//			i++;
//			j++;
//		}
//		else j=next[j];
//	}
	return j;
//还是对kmp算法的理解不够深刻,成功匹配时j=le,
//	不成功,一种是不相等,终究还是主串比完了也没能全部匹配上
//	就看最后这个j表示匹配哪儿 
//	如果主串最后一个字符也未能与s[0]比较成功,j就会变为-1,继而加一得0 
//	想象匹配过程中 
//	abcdefgh
//		 fgheg 
}
int main(){
	int n;
	cin>>n;
	cin>>t;
	for(int i=1;i<n;i++){
		cin>>s;
		len=s.size();
		getNext();
		int st;//成型字符串的开始匹配位置 ,不必回回从0开始
		st=t.size()-len; 
		st=max(0,st);
		int pos=kmp(st); 
		t+=s.substr(pos,len-pos+1);
	}
	cout<<t;
	return 0;
}

字符串双哈希

思路:

把第一个单词当作所求字符串t
之后输入一个单词s后便算出这个单词的字符串前缀哈希,
由 于 重 叠 的 长 度 是 m i n ( s t r l e n ( t ) , s t r l e n ( s ) ) , 由于重叠的长度是min(strlen(t),strlen(s)), min(strlen(t),strlen(s)),
在s中这段长度,分别比较 s的前缀哈希值和
t对应后缀段的哈希值,确定要将s的某段加入t,进一步求出t完整段的哈希值一遍getH取得后缀段的哈希值

WC我没想到这道题耗了我如此之久
问题一堆:

问题一

不断拼接一段字符串 s s s到原有字符串 t t t上,总共长度数据范围是 1 e 6 1e6 1e6,为了求已有长度字符串的哈希值,反复使用 s t r l e n ( t + 1 ) strlen(t+1) strlent+1
n n n的范围是 1 e 5 1e5 1e5,strlen时间复杂度是 O ( n ) O(n) O(n), 外层循环+内部 s t r l e n , 复 杂 度 O ( n 2 ) strlen,复杂度O(n2) strlen,O(n2)循环里面还用strlen真的是超时到绝望
strlen

	scanf("%s",t+1);
	int st=1;
	for(int j=1;j<n;j++){
		scanf("%s",s+1);
		int len=strlen(t+1);
		int le=strlen(s+1);
		for(int i=st;i<=len;i++){
			int id=getID(t[i]);
			h[i]=(h[i-1]*bas+id)%mod;
		}
		……
		……
		……

strlen所作的仅仅是一个计数器的工作,它从内存的某个位置(可以是字符串开头,中间某个位置,甚至是某个不确定的内存区域)开始扫描,直到碰到第一个字符串结束符’\0’为止,然后返回计数器值(长度不包含’\0’)。

我们用cin>>s和scanf(%s)输入字符串时,会在接受完字符串后在末尾加个’\0’,也就是NULL作为字符串结束符。

所以用strlen函数求解字符串长度的时候,复杂度就是O(n)。如果在循环中用

for (int i = 0; i < strlen(s); ++i)//复杂度O(n2) 那就麻烦了,可能会超时,解决办法:

(1)先把strlen(s)先用变量存储起来,再放入循环中;

(2)或者用s[i] != ‘\0’;

问题二

1、
ll mod[2]={1e9+7,999999998};编译器上运行没问题,交到codeforce上,compile error,理由是1e9+7将算作double类型的浮点数, 1.000000000 ∗ 1 0 7 1.000000000*10^7 1.000000000107
ll mod[2]={1000000007,999999998};✔
2、类似的还有,cin>>s+1,报错说是没有重载运算符啥的operator
3、还有之前的next的重名

问题三

双重哈希或者多重哈希,一定要每一种哈希函数之下的哈希值都相等才能判断两字符串相等,是个&&的关系,所以

st=len+1;
int pos=0;
ll H[2]={0,0};
for(int i=1;i<=le&&i<=len;i++){
			int id=getID(s[i]);
			int ok=1;
			for(int k=0;k<2;k++){
			H[k]=(H[k]*bas[k]+id)%mod[k];//s字符串s[1--i]这段哈希值
//	ll temp=((h[k][len]-h[k][len-i]*p[k][i])%mod[k]+mod[k])%mod[k];
			if(H[k]!=getH(len-i+1,len,k))ok=0;
			}
			if(ok)pos=i;//12345
		}

只要在一个哈希函数下的哈希值对应不相等,就不能算作字符串相等

问题四

H[2][N]优化为H[2]嘛,算是个临时变量,专门用来记录输入下一个单词的前缀哈希值,不断输入单词,所以H数组每次都要记得初始化啊,不能在全局变量定义了就完事了(只初始化一次),因为==H[k]=(H[k]*bas[k]+id)%mod[k];==要用到上次的H【k】值,有点像滚动数组的优化
如果不用滚动数组优化,就不用次次初始化啦,
H[k][i]=(H[k][i-1]*bas[k]+id)%mod[k];
耗空间呀

	for(int i=1;i<=le&&i<=len;i++){
			int id=getID(s[i]);
			int ok=1;
			for(int k=0;k<2;k++){
			H[k][i]=(H[k][i-1]*bas[k]+id)%mod[k];//s字符串s[1--i]这段哈希值
//			ll temp=((h[k][len]-h[k][len-i]*p[k][i])%mod[k]+mod[k])%mod[k];
			if(H[k][i]!=getH(len-i+1,len,k))ok=0;
			}
			if(ok)pos=i;//12345
		}

问题五

用字符串hash可以O ( n ) 处理,O ( 1 )比较。因为每个串只会被扫一遍,可以直接暴力,预处理 O ( n ) ,暴力O ( n ) O(n)查找最长相同部分。

由于字符串长达到了 1 0 6 10^6 106
单模hash 已经搞不了,要用更安全的双 hash,取 2 个较大的模数 mod1 和 mod2,以及一个素数 p (p 不用太大,单模的 p 也不用太大,模数一定要大)

ll bas[2]={131,2333333};
ll mod[2]={1000000007,999999998};

1e5可以用 P取131,mod取1<<64,通过unsigned long long来取模的 组合 单哈希,
一旦到了1e6还是老老实实用双哈希甚至三哈希,其实也不是什么难事儿,把p数组,mod数组,H数组,h数组,都变成k维就好,多一个循环for(int j=0;j<k;k++)……就好

建议取以上两种哈希函数的P和mod组合,很强,后面那一组用来单哈希在这题甚至可以AC

问题六

这题大小写字母是当作不相等的,一开始理解反了,白费我写一个
int getID(char ch){//65 97
if(ch>=‘A’&&ch<=‘Z’)ch+=32;
return ch;
}
这样可,但没必要,P进制P取得已经够大了,48——65——97——122,完全可以错开取不同值,直接用s[i]就ok
int getID(char c){
if(c>=‘a’&&c<=‘z’) return c-‘a’;
if(c>=‘0’&&c<=‘9’) return c-‘0’+26;
return c-‘A’+36;
}

问题七

复杂度分析的不好,一开始超时还以为是快速幂和初始化P的幂次的初始化的选择问题
测试证明,这题初始化P幂次的数组p来得比快速幂快
反正 初始化P幂次的数组p 就是O(n)的复杂度,不会造成实质性的超时,用这个

代码

单哈希
2333333
999999998
就连131和unsigned long long的组合也会被卡

#include <iostream>
#include <string.h>
using namespace std;
typedef long long ll;
const int N=1e6+10;
char t[N];
char s[N];
ll bas=2333333;
ll mod=999999998;
ll h[N];
ll H[N];
ll p[N];
void init(){
	p[0]=1;
	for(int i=1;i<N;i++){//N位最高幂次是N-1
		p[i]=(p[i-1]*bas)%mod;
	}
}
ll getH(int l,int r){
	return ((h[r]-h[l-1]*p[r-l+1])%mod+mod)%mod;
}
int main(){
	init();
	int n;
	cin>>n;
	scanf("%s",t+1);
	int len=strlen(t+1);
	int st=1;
	for(int j=1;j<n;j++){
		scanf("%s",s+1);
		int le=strlen(s+1);
		for(int i=st;i<=len;i++){
			h[i]=(h[i-1]*bas+t[i])%mod;
		}
		st=len+1;
		int pos=0;
		for(int i=1;i<=le&&i<=len;i++){
			H[i]=(H[i-1]*bas+s[i])%mod;//s字符串s[1--i]这段哈希值
			if(H[i]==getH(len-i+1,len))pos=i;//12345
		}
		for(int i=pos+1;i<=le;i++){
			t[++len]=s[i];
		}
	}
	printf("%s",t+1);
	return 0;
}

双哈希(So easy,先写个单哈希,再逐一将 m o d − > m o d [ k ] , b a s − > b a s [ k ] , h [ i ] − > h [ k ] [ i ] mod->mod[k],bas->bas[k],h[i]->h[k][i] mod>mod[k],bas>bas[k],h[i]>h[k][i],求哈希值时用个k循环,要注意的就是判断字符串相等时一定要每个哈希函数下的哈希值都满足条件

用二维数组比写两个数组好多了,还可以根据编译器报错的地方补k

#include <iostream>
#include <string.h>
using namespace std;
typedef long long ll;
const int N=1e6+10;
char t[N];
char s[N];
ll bas[2]={131,2333333};
ll mod[2]={1000000007,999999998};
ll h[2][N];
ll H[2][N];
ll p[2][N];
void init(){
	p[0][0]=p[1][0]=1;
	for(int i=1;i<N;i++){//N位最高幂次是N-1
		for(int k=0;k<2;k++)
		p[k][i]=(p[k][i-1]*bas[k])%mod[k];
	}
}
ll getH(int l,int r,int k){
	return ((h[k][r]-h[k][l-1]*p[k][r-l+1])%mod[k]+mod[k])%mod[k];
}
int main(){
	init();
	int n;
	cin>>n;
	scanf("%s",t+1);
	int len=strlen(t+1);
	int st=1;
	for(int j=1;j<n;j++){
		scanf("%s",s+1);
		int le=strlen(s+1);
		for(int i=st;i<=len;i++){
			for(int k=0;k<2;k++)
			h[k][i]=(h[k][i-1]*bas[k]+t[i])%mod[k];
		}
		st=len+1;
		int pos=0;
		for(int i=1;i<=le&&i<=len;i++){
			int ok=1;
			for(int k=0;k<2;k++){
			H[k][i]=(H[k][i-1]*bas[k]+s[i])%mod[k];//s字符串s[1--i]这段哈希值
			if(H[k][i]!=getH(len-i+1,len,k))ok=0;
			}
			if(ok)pos=i;//12345
		}
		for(int i=pos+1;i<=le;i++){
			t[++len]=s[i];
		}
	}
	printf("%s",t+1);
	return 0;
}

最后还可以用滚动数组的思想优化一下空间
H[2][N]–>H[2]

	ll H[2]={0,0};
		for(int i=1;i<=le&&i<=len;i++){
			int ok=1;
			for(int k=0;k<2;k++){
			H[k]=(H[k]*bas[k]+s[i])%mod[k];//s字符串s[1--i]这段哈希值
			if(H[k]!=getH(len-i+1,len,k))ok=0;
			}
			if(ok)pos=i;//12345
		}

反过来求哈希值,搞清楚各位的权值

……cba cba……
<—— ——>
求abc
反过来求反过来字符串的哈希值(高位到低位),和原来一样
h = h ∗ b a s + a n s [ i ] h=h*bas+ans[i] h=hbas+ans[i]
正着顺序求反过来字符串的哈希值(低位到高位),
H + = H ∗ w e i g h t — — w e i g h t ( 11 ∗ b a s 1 ∗ b a s ∗ b a s ) H+=H * weight ——weight(1 1*bas 1*bas*bas) H+=Hweightweight(11bas1basbas)
由低位到高位求数值 各位权值为1 1bas 1bas*bas

s . s i z e ( ) 比 s t r l e n ( s ) 快 好 多 s.size()比strlen(s)快好多 s.size()strlen(s)

在这里插入图片描述

中间是倒着求字符串哈希值,用ans.size()
最顶上是把ans.size()换成strlen(ans)
最底下是开了数组双哈希的情形

#include <iostream>
#include <string.h>
using namespace std;
typedef unsigned long long ull;
const int N=1e6+10;
//ll bas[2]={131,2333333};
//ll mod[2]={1000000007,999999998};

char ans[N];
char s[N];
//string ans,s;
ull bas=2333333;
ull mod=999999998;
int main(){
	int n;
	cin>>n;
	cin>>ans;
	for(int j=1;j<n;j++){
		cin>>s;
		int len=strlen(ans);
		int le=strlen(s);
		ull bass=1;
		int pos=0;
		ull h=0;//一定每次要初始化的呀,只要不是通过数组记录每一段的哈希值
//		而是一个变量记录某一段的哈希值一定要惊醒初始化 
		ull H=0;
		for(int i=1;i<=min(len,le);i++){//枚举长度cdefabc  abcmnjk
			h=(h*bas+ans[len-i])%mod;//求字符串反过来的哈希值 cba
			H=(H+s[i-1]*bass)%mod;
//由于倒过来,由低位到高位求数值 各位权值为1 1*bas 1*bas*bas 
			bass=bass*bas%mod;
			if(H==h)pos=i;//i其实是相等字符的下一个的下标了 
		}
//		ans+=substr(pos);//如果是string ans; ans[len++]是不行的 
		for(int i=pos;i<le;i++){
			ans[len++]=s[i];
		} 
	}
	cout<<ans;
	return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值