哈希学习笔记+杂题(基础2 字符串哈希)

字符串系列

前言:

骗分神器,我之前竟然没有学。

一、哈希学习笔记+杂题(基础2 字符串哈希)

相关题单:戳我

1.哈希(hash)简介

哈希算法(Hash Algorithm),又称散列算法。有两种用法,第一种就是将一字符串转化成任意进制的数,目的是方便存储。第二种就是将大范围的数映射成小范围的数,目的也是方便存储。(其实还是一种比较抽象+玄学的思想)。oi中哈希主要都是以字符串哈希,离散化,状态压缩这三类为主。是一种字符串骗分利器(可以处理很多kmp,字典树,后缀数组的操作,虽然可能时间复杂度不优+容易被卡)。

使用哈希的特点:

  • 一种用于统计复杂信息的的不完美算法。

  • 构造哈希函数将复杂信息映射到便于统计的信息上,通过对映射后的信息进行处理来实现复杂信息的维护。

  • 两元素映射后相同,是两元素相同的必要条件。

  • 可能会丢失部分信息。

  • 从哈希值不能反向推导出原始数据(所以哈希算法也叫单向哈希算法)。

  • 对输入数据非常敏感,哪怕原始数据只修改了一个 Bit,最后得到的哈希值也大不相同。

  • 散列冲突的概率要很小,对于不同的原始数据,哈希值相同的概率非常小。

  • 哈希算法的执行效率要尽量高效,针对较长的文本,也能快速地计算出哈希值。

2.操作介绍

字符串哈希有很多种映射构造的方法,正是这些方法才使得它可以避免绝大多数情况的冲突。

我么定义一个字符串为\(s\),进制数为\(base\),模数为\(mod\),哈希数组为\(sum\),预处理进制数的平方数组为\(f\)

(一)单哈希

最简单的哈希,就是利用递推求。有类似于前缀和的性质,可以实现很多种操作。

预处理哈希数组:

	for(int i=1;i<=len;i++)
	{
		sum[i]=(sum[i-1]*base+s[i])%mod;//char数组在运算中会自动转化成int类型
	}

预处理进制数的平方

	f[0]=1;
	for(int i=1;i<=n;i++) f[i]=f[i-1]*base%mod;//提前处理好

截取子串:

inline int get(int l,int r)
{
	return (sum[r]-sum[l-1]*f[r-l+1]%mod+mod)%mod;//由于减了之后可能变成负数,那就加一个mod再模,利用前缀和的性质
}

减小哈希冲突的方法:

(1)增大模数,使用大质数做为模数。

(2)进制数可以古怪一点,一般使用的是131,13331,19260817等。

(3)使用其他哈希处理方法。

(二)双哈希

说白了就是你可以使用两个模数进行取模,可以大大的降低冲突的概率。

(三)自然溢出

#define int unsigned long long

将int类型宏定义成unsigned long long类型,unsigned long long类型只能是非负数,你甚至不需要模数,只管乘就可以了,本质上是对unsigned long long类型的值域取模,有时候也容易被卡。

(四)无错哈希

你可以建立一个哈希表(常数爆表),判断每一个对应的哈希出现没有,出现了就选择另外一个还没有使用的数当作其的哈希值,并记录下来。这样就不存在有哈希冲突的情况了,缺点是常数很大。

语文水平让我汗流浃背,你要是没听懂很正常,还是做题理解吧\bx\bx。

3.习题

P3370 【模板】字符串哈希

给定了\(n\)个字符串,问你这里面有几个不同的字符串,由于字符串哈希可以将一个字符串表示为一个数,所以我们可以使用哈希表示每一个字符串,将值放入一个桶中,然后看之前是否已经存在同样的字符串,如果没有,就\(ans++\),最后输出\(ans\),即可。

代码:

#include<iostream>
#include<cmath>
#include<cstring>
#include<algorithm>
#include<queue>
#include<cstdio>
#include<map>
#define int long long
using namespace std;
inline int max(int x,int y){return x>y?x:y;}
inline int min(int x,int y){return x>y?y:x;}
const int M=1e4+5,mod=998244353,base=13331;//单哈希的模数最好使用一个大质数
int n,ans=0;
char s[M];
int sum[M];
map<int,int> mapp;//由于处理出来的哈希值会很大(mod为998244353)所以普通的数组装不下,那就可以使用map(map的使用方法我可能下辈子会写ing),你可以把map当成一个很大的桶。

inline int hashx(int len)
{
	for(int i=1;i<=len;i++)
	{
		sum[i]=(sum[i-1]*base+(int)s[i])%mod;
	}
	return sum[len];
}

signed main()
{
	ios::sync_with_stdio(false);
	cin.tie(0);cout.tie(0);
	cin>>n;
	for(int i=1;i<=n;i++)
	{
		cin>>s+1;
		int x=hashx(strlen(s+1));//处理出每个字符串的哈希值
		if(mapp[x]) continue;
		else//没有出现过
		{
			mapp[x]=1,++ans;//记录出现,累加答案
		}
	}
	cout<<ans<<"\n";
	return 0;
}

P1102 A-B 数对

给定一个数组,要你取出两个数\(a,b\)使得\(a-b=c\)其实可以不使用哈希的,先将原数组排序,然后利用二分查找找出满足条件的数的个数,统计答案。

代码:

#include<iostream>
#include<cmath>
#include<cstring>
#include<algorithm>
#include<queue>
#include<cstdio>
#define int long long
using namespace std;
inline int max(int x,int y){return x>y?x:y;}
inline int min(int x,int y){return x>y?y:x;}
const int M=2e5+5;
int n,c,ans=0;
int a[M];
signed main()
{
	ios::sync_with_stdio(false);
	cin.tie(0);cout.tie(0);
	cin>>n>>c;
	for(int i=1;i<=n;i++) cin>>a[i];
	sort(a+1,a+n+1);//排序
	for(int i=1;i<=n;i++)
	{
		if(a[i]-c<=0) continue;//那肯定找不到了
		else
		{
			int posl=lower_bound(a+1,a+n+1,a[i]-c)-a;
			int posr=upper_bound(a+1,a+n+1,a[i]-c)-a;//找出区间
			//cout<<posl<<" "<<posr<<"\n";
			if(a[posl]!=a[i]-c) continue;
			else ans+=posr-posl;//累加答案
		}
	}
	cout<<ans<<"\n";
	return 0;
}

P2957 [USACO09OCT] Barn Echoes G

给定两个字符串,找出最长的重复部分,两个字符串的重复部份指的是同时是一个字符串的前缀和另一个字符串的后缀的字符串。

字符串哈希由于具有前缀和的性质,所以可以在\(O(1)\)的时间内截取一段子串的哈希值。这道题要进行分类讨论,有两种情况,A的前缀与B的后缀重合与A的后缀与B的前缀重合,去这两种情况重合部分的最大值进行输出。

代码:

#include<iostream>
#include<cmath>
#include<cstring>
#include<algorithm>
#include<queue>
#include<cstdio>
#define int long long
using namespace std;
inline int max(int x,int y){return x>y?x:y;}
inline int min(int x,int y){return x>y?y:x;}
const int M=105,mod=998244353,base=131;
int n,m;
int sa[M],sb[M],f[M];
char a[M],b[M];

inline void pre()
{
	f[0]=1;
	for(int i=1;i<=80;i++) f[i]=f[i-1]*base%mod;
}//预处理进制数各个次项

inline int get1(int l,int r)//截取A 的子串
{
	return (sa[r]%mod-sa[l-1]*f[r-l+1]%mod+mod)%mod;
}

inline int get2(int l,int r)//截取B 的子串
{
	return (sb[r]%mod-sb[l-1]*f[r-l+1]%mod+mod)%mod;
}

signed main()
{
	ios::sync_with_stdio(false);
	cin.tie(0);cout.tie(0);
	cin>>a+1;
	cin>>b+1;
	n=strlen(a+1),m=strlen(b+1);
	pre();//预处理进制数的各个次项,方便在O(1)的时间中求出子串 
	for(int i=1;i<=n;i++) sa[i]=(sa[i-1]*base+a[i])%mod;
	for(int i=1;i<=m;i++) sb[i]=(sb[i-1]*base+b[i])%mod;
	//分类讨论
	//A的前串+B的后串
	int ans=0;
	for(int i=1;i<=min(n,m);i++)
	{
		if(sa[i]==get2(m-i+1,m)) ans=i;//A的前i个与B的后i个进行比较
	}
	//A的后串+B的前串
	for(int i=1;i<=min(n,m);i++)
	{
		if(get1(n-i+1,n)==sb[i]) ans=max(ans,i);//A的后i个与B的前i个进行比较
	}
	cout<<ans<<"\n";
	return 0;
}

P8630 [蓝桥杯 2015 国 B] 密文搜索

给定一个原串,然后给定\(n\)个密码串,问你将这\(n\)个密码串在原串中出现的次数总和,其中密码串是被打乱了,需要考虑密码串字符的所有排列情况。这题只是利用哈希的思想,有些时候,哈希的思想也很巧妙。

如果我们将每一个密码串都排好序,放入一个map中,统计每个字符串出现的次数,然后不断从前向后在原串上截取一段长度为8的子串,然后将这段子串排序,观察map中是否出现了这个子串,出现了就加上已经处理好的出现次数,累加答案。

因为密码串可以随意颠倒排列的顺序,所以对于原串的一段区间,如果出现的字符种类和数量都和一个密码串一样,那么这个时候肯定就可以匹配,为了方便判断,就将每一个密码串与子串都排序。

代码:

#include<iostream>
#include<cmath>
#include<cstring>
#include<algorithm>
#include<queue>
#include<cstdio>
#include<map>
#define int long long
using namespace std;
inline int max(int x,int y){return x>y?x:y;}
inline int min(int x,int y){return x>y?y:x;}
const int M=1025,mod=998244353,base=131;
int n,m,ans;
int sa[M],sum[M][9],f[M];
string s,c;
map<string,int> mapp;//直接拿string去映射
signed main()
{
	ios::sync_with_stdio(false);
	cin.tie(0);cout.tie(0);
	cin>>s;
	cin>>n;
	for(int i=1;i<=n;i++)
	{
		cin>>c;
		sort(c.begin(),c.end());//将密码串排序
		mapp[c]++;//次数加1
	}
	for(int i=0;i<s.size()-7;i++)
	{
		c=s.substr(i,8);//以i为开头,截取一段长度为8的子串
		sort(c.begin(),c.end());
		ans+=mapp[c];//累加答案
	}
	cout<<ans<<"\n"; 
	return 0;
}

P1481 魔族密码

其实就是一个字符串的最长上升序列,需要一点的DP基础。

代码:

#include<iostream>
#include<cmath>
#include<cstring>
#include<algorithm>
#include<queue>
#include<cstdio>
#define int long long
using namespace std;
inline int max(int x,int y){return x>y?x:y;}
inline int min(int x,int y){return x>y?y:x;}
const int M=2005,N=76,base=131,mod=998244353;
int n,ans=0;
int dp[M];
char s[M][N];
signed main()
{
	ios::sync_with_stdio(false);
	cin.tie(0);cout.tie(0);
	cin>>n;
	for(int i=1;i<=n;i++)
	{
		cin>>s[i];
		dp[i]=1;
		for(int j=i-1;j>=1;j--)
		{
			if(strstr(s[i],s[j])==s[i])//可以转移
			{
				dp[i]=max(dp[j]+1,dp[i]);//递推答案
			}
		}
		ans=max(dp[i],ans);
	}
	cout<<ans<<"\n";
	return 0;
}

P3879 [TJOI2010] 阅读理解

本来是字典树的模板题,但是我们可以使用STL莽过去(这就是STL+哈希带给我的自信)。有\(n\)篇文章,\(q\)个询问,每篇文章中都有一些单词。那么我们可以使用map将每篇文章中出现的单词记录下来,然后对于每一个询问,查询每一个map中是否出现过,出现了就输出文章编号。

但是map占用空间较大,你不可能真的将每一个文章开一个map存下每一个单词,那样会MLE,本题你可能需要一点优化,你可以让map映射一个vector数组,对于已经出现的单词你只需要修改对应的vector,这样你就可以只用开一个map,同时避免记录重复的单词。

90ptsMLE代码(未优化):

#include<iostream>
#include<cmath>
#include<cstring>
#include<algorithm>
#include<queue>
#include<cstdio>
#include<map>
using namespace std;
inline int max(int x,int y){return x>y?x:y;}
inline int min(int x,int y){return x>y?y:x;}
const int M=1e3+5;
int n,x,m;
string str;
map<string,int> mapp[M];//真就开n个map记录每一个文章中的单词
signed main()
{
	ios::sync_with_stdio(false);
	cin.tie(0);cout.tie(0);
	cin>>n;
	for(int i=1;i<=n;i++)
	{
		cin>>x;
		for(int j=1;j<=x;j++) cin>>str,mapp[i][str]=1;//记录第i个出现了str这个单词
	}
	cin>>m;
	for(int i=1;i<=m;i++)
	{
		cin>>str;
		for(int j=1;j<=n;j++)
		{
			if(mapp[j][str]) cout<<j<<" ";//如果这篇文章有就输出
		}
		cout<<"\n";
	}
	return 0;
}

优化代码:

#include<iostream>
#include<cmath>
#include<cstring>
#include<algorithm>
#include<queue>
#include<cstdio>
#include<map>
using namespace std;
inline int max(int x,int y){return x>y?x:y;}
inline int min(int x,int y){return x>y?y:x;}
const int M=1e3+5,base=131,mod=998244353;
int n,x,m,sum[21];
string str;
map<string,vector<int> > s;//只开一个,映射一个vector记录答案
signed main()
{
	ios::sync_with_stdio(false);
	cin.tie(0);cout.tie(0);
	cin>>n;
	for(int i=1;i<=n;++i)
	{
		cin>>x;
		for(int j=1;j<=x;++j)
		{
			cin>>str;
			int k=s[str].size()-1;
			if(k==-1||s[str][k]!=i)
			{
				s[str].push_back(i);//记录每一个单词在哪些文章中出现了
			}
		}
	}
	cin>>m;
	for(int i=1;i<=m;++i)
	{
		cin>>str;
		for(int i=0;i<s[str].size();++i)
		{
			cout<<s[str][i]<<" ";//输出
		}
		cout<<"\n";
	}
	return 0;
}

P7469 [NOI Online 2021 提高组] 积木小赛

这题就有点强度了,它有点卡模数和进制数。

由于A的操作是可以丢掉任意多块积木,而B只能丢掉自己的前缀和后缀(相当于A留下的是可以不连续的子序列,B留下的是自己的子串),所以明显B的限制要强一点,那就从B的操作入手。数据范围只有\(n\le3000\)。所以可以\(O(n^2)\)的暴力枚举每一个B的子串,观察每一个子串是否与A匹配。

现在就可以知道有哪些B的子串可以与A匹配了,但是要求的是方案数,有可能B有多个相同的子串,那可以处理出B的哈希数组,这样就可以在\(O(1)\)时间的获知任意一段B的子串的哈希值,将这些哈希值全部放入一个数组中,最后排序后去重看有多少个不相同哈希值那就是答案。

本题恶心在于它卡模数,出题人可以故意构造特殊的数据去卡一些以较常见的哈希函数,这道题我的处理方法是使用自然溢出(也就是unsigned long long),进制数设为19260817,就可以过掉此题。

代码:

#include<iostream>
#include<cmath>
#include<cstring>
#include<algorithm>
#include<queue>
#include<cstdio>
#define int unsigned long long
using namespace std;
inline int max(int x,int y){return x>y?x:y;}
inline int min(int x,int y){return x>y?y:x;}
const int M=3e3+5,base=19260817;//进制数换一个+自然溢出
int n,cnt;
int sum[M],f[M],t[M*M];
char a[M],b[M];

inline void pre()
{
	f[0]=1;
	for(int i=1;i<=n;i++) f[i]=f[i-1]*base;//预处理进制数的平方
}


inline int get(int l,int r)
{
	return sum[r]-sum[l-1]*f[r-l+1];//O(1)截取一段子串的哈希值
}

signed main()
{
	ios::sync_with_stdio(false);
	cin.tie(0);cout.tie(0);
	cin>>n;pre();
	cin>>a+1;
	cin>>b+1;
	for(int i=1;i<=n;i++) sum[i]=(sum[i-1]*base+b[i]);//处理哈希数组
	for(int i=1;i<=n;i++)
	{
		int p=1;
		for(int j=i;j<=n;j++)//O(n^2)枚举B的子串,边枚举边匹配
		{
			while(p<=n&&a[p]!=b[j]) p++;
			if(p>n) break;
			p++;
			t[++cnt]=get(i,j);//取得子串的哈希值后放入数组
		}
	}
	//cout<<cnt<<"\n"; 
	sort(t+1,t+cnt+1);
	int len=unique(t+1,t+cnt+1)-t-1;//排序后去重,unique是去重函数,返回的len是数组中不同的数的个数,也就是本题不同的哈希值的个数
	cout<<len<<"\n"; 
	return 0;
}

P6739 [BalticOI 2014 Day1] Three Friends 题解

也是较为有趣的一道题,对一个字符串进行两步操作,先将这个字符串复制为两份,然后再在其中插入一个字符,最后给你操作完后得到的字符串,问你一开始进行操作的字符串,如果有多种情况或者无解输出特定的语句。

很显然,将一个数乘上2后加上1,得到的一定是一个奇数,那么如果输入的是偶数长度那么必然无解。接下来将原序列分为两部分,分别记录两段的哈希值,有一段必然长度会比另一端长1,那我们枚举长的一段中每一个字符,判断删去这一个字符之后是否哈希值与短的一段相同,相同答案就多1。

最后对于输出,有多个答案就输出多解,只有一个答案那输出就是短的那一段,无解就输出无解。

代码:

#include<iostream>
#include<cmath>
#include<cstring>
#include<algorithm>
#include<queue>
#include<cstdio>
#define int long long
using namespace std;
inline int max(int x,int y){return x>y?x:y;}
inline int min(int x,int y){return x>y?y:x;}
const int M=2e6+5,base=131,mod=998244353;//这题不是很卡模数
int n;
int f[M],sum[M];
char a[M];

inline void pre()
{
	f[0]=1;
	for(int i=1;i<=n;i++) f[i]=f[i-1]*base%mod; //预处理进制数的次方
}

inline int get(int l,int r)
{
	if(l>r) return 0;
	return (sum[r]-sum[l-1]*f[r-l+1]%mod+mod)%mod;//O(1)截取子串
}

signed main()
{
	ios::sync_with_stdio(false);
	cin.tie(0);cout.tie(0);
	cin>>n;pre();
	cin>>a+1;
	if(n%2==0)
	{
		cout<<"NOT POSSIBLE\n";//偶数必然无解
		return 0;
	}
	int mid=n/2+1,l,r,flag=0,j=0,m=0;//分成两段
	for(int i=1;i<=n;i++) sum[i]=(sum[i-1]*base+a[i])%mod;//处理哈希数组
	for(int i=1;i<=n;i++)//枚举咯
	{
		if(i<=mid)
		{
			r=get(mid+1,n);
			l=(get(1,i-1)*f[mid-i]+get(i+1,mid))%mod;
		}
		else
		{
			l=get(1,mid-1);
			r=(get(mid,i-1)*f[n-i]+get(i+1,n))%mod;
		}
		if(l==r)//前后两段相同
		{
			if(flag&&l!=m)
			{
				cout<<"NOT UNIQUE\n";//有多个解
				return 0;
			}
			flag=1,j=i,m=l;
		}
	}
	if(flag)//只有一个解
	{
		if(j<mid) cout<<a+mid+1<<"\n";
		else a[mid]=0,cout<<a+1<<"\n";//输出短的一段
		return 0;
	}
	else cout<<"NOT POSSIBLE\n";//无解
	return 0;
}

P2879 [USACO07JAN] Tallest Cow S

这题不是哈希,但是不知道为什么贴上了哈希的标签(或许有人用了哈希做法)。给你一段奶牛序列,给出最高的奶牛的下标与它的高度,再给你一些数对,代表这两头牛互相可以看的到(其实就是中间的牛都比互相可以看到的两头牛矮)。问你每一头奶牛最高可能有多高。

这里介绍一种简单的贪心+差分做法,由于已经给出了最高的奶牛,那么对于每一组限制,中间的都比他们矮,我们就可以贪心的考虑中间的奶牛只比两边的牛矮了一个单位,相当于是将这一段区间减上1,对于序列的区间操作,我们可以直接利用差分,两边一加一减,最后做一个前缀和,输出时加上最高的牛的高度就是答案了。

代码:

#include<iostream>
#include<cmath>
#include<cstring>
#include<algorithm>
#include<queue>
#include<cstdio>
#include<map>
#define int long long
using namespace std;
inline int max(int x,int y){return x>y?x:y;}
inline int min(int x,int y){return x>y?y:x;}
const int M=1e4+5;
int n,m,h,r;
int c[M];
map<pair<int,int> ,int> v;

signed main()
{
	cin>>n>>m>>h>>r;
	int a,b;
	for(int i=1;i<=r;i++)
	{
		cin>>a>>b;
		if(a>b) swap(a,b);
		if(v[make_pair(a,b)]) continue;//哦,我是说为什么要加上哈希的标签,因为如果输入中给出相同两头牛,其实只用减上一次
		c[a+1]--,c[b]++;//差分
		v[make_pair(a,b)]=1;//记录这个区间已经出现了
	}
	for(int i=1;i<=n;i++)
	{
		c[i]+=c[i-1];//前缀和
		cout<<c[i]+h<<"\n";//加上最高的牛的高度(一开始我们没有初始化)
	}
	return 0;
}

P2852 [USACO06DEC] Milk Patterns G

这题一眼顶真肯定是具有单调性的,也就是说答案具有递增的趋势,对于较小的长度答案必然更大。数据范围 \(N(1 \le N \le 2 \times 10 ^ 4)\),所以不好一个一个长度的枚举,但是因为答案有单调性,我们可以使用二分+哈希的做法(SA就可以去死了)。

二分最长出现了\(k\)次的子串的长度,然后check就直接截取所有长度为判定长度\(x\)的子串,然后看是否有超过\(k\)次的子串。还是相当简单的。

代码:

#include<iostream>
#include<cmath>
#include<cstring>
#include<algorithm>
#include<queue>
#include<cstdio>
#include<map>
#define int long long
using namespace std;
inline int max(int x,int y){return x>y?x:y;}
inline int min(int x,int y){return x>y?y:x;}
const int M=2e4+5,mod=998244353,base=131;
int n,k;
int a[M],f[M],sum[M];
map<int,int> mapp;

inline void pre()
{
	f[0]=1;
	for(int i=1;i<=n;i++) f[i]=f[i-1]*base%mod;//进制数次方
}

inline int get(int l,int r)
{
	return (sum[r]-sum[l-1]*f[r-l+1]%mod+mod)%mod;
}
//O(1)截取
inline int check(int x)//x是当前判断的长度 
{
	mapp.clear();//每次判断都清空哒
	int res=0;
	for(int i=1;i<=n-x+1;i++)//截取出所有长度为x的子串
	{
		int xx=get(i,i+x-1);
		mapp[xx]++;
		res=max(mapp[xx],res);//记录出现次数最多的子串
	}
	if(res>=k) return 1;//出现次数大于等于k,那就说明这个长度x满足要求
	else return 0;
}

signed main()
{
	//ios::sync_with_stdio(false);
	//cin.tie(0);cout.tie(0);
	cin>>n>>k;pre();
	for(int i=1;i<=n;i++)
	{
		cin>>a[i];
		sum[i]=(sum[i-1]*base+a[i])%mod;//处理哈希
	}
	int l=0,r=n/k;//二分长度
	while(l<r)
	{
		//cout<<l<<" "<<r<<"\n";
		int mid=(l+r+1)>>1;
		if(check(mid)) l=mid;
		else r=mid-1;
	}
	cout<<l<<"\n";
	return 0;
}

P6306 「Wdsr-1」小铃的书

状态压缩,也是属于哈希的一种(几乎每一道双向搜索都加上了哈希的标签,因为将一张图的状态压缩成一个数字这种思想就是哈希)。

首先一看题,这不是STL水题嘛,但是出题人空间只开了8.00MB,也就是说直接使用map只能获得50pts的好成绩,优化是没办法优化一点,那么我们可能需要考虑其他的做法,由于只有一个特殊的数,所以我们可以使用100进制来存每一本书,对应的如果我们发现有位置出现了没法整除\(k\)的数,那么累加就可以了,主要还是看代码。

代码:

#include<iostream>
#include<cmath>
#include<cstring>
#include<algorithm>
#include<queue>
#include<cstdio>
#define int long long
using namespace std;
inline int max(int x,int y){return x>y?x:y;}
inline int min(int x,int y){return x>y?y:x;}
const int mod=100;
int n,k,a,maxx,f[18];
int s[99][99];//s数组代表的是,第几个100的次方,数是多少
int ans;
inline void pre()
{
	f[0]=1;
	for(int i=1;i<=9;++i) f[i]=f[i-1]*100;
}

signed main()
{
	pre();
	cin>>n>>k;
	for(int i=1;i<=n;++i)
	{
		cin>>a;
		for(int j=0;a!=0;++j)
		{
			if(a%mod) ++s[j][a%mod];//第j个100的次方,是a%mod
			a/=mod;maxx=max(maxx,j);
		}
	}
	for(int i=0;i<=maxx;++i)
	{
		for(int j=0;j<=99;++j)
		{
			s[i][j]%=k;
			if(s[i][j]==1) ans+=f[i]*j;//如果余上了1,那么就是那个特殊的数,累加
		}
	}
	cout<<ans<<endl;
	return 0;
}

CF471D MUH and Cube Walls

给两堵墙,问 a 墙中与 b 墙顶部形状相同的区间有多少个。(这个翻译是相当的简洁)。

形状相同,其实就是上下变化幅度相同,很明显说的就是差分数组,那么我们可以将两个序列分别差分,由于值域较大\(1 \le b_i \le 10^9 1 \le a_i \le 10^9\),模数又是和其一个量级的,所以出现哈希冲突的可能就会增大,那么有两种操作,一种是使用自然溢出,用unsigned long long去取模,简单快捷方便,还有一种就是使用离散化,将数的值域压缩为\(1e5\)级别的。

我用的是离散化(可以试一下自然溢出,其实还更好写一点),将差分数组离散化之后,枚举所有A的长度与B相同的子串,进行比较哈希值是否相同,累加答案。

代码:

#include<iostream>
#include<cmath>
#include<cstring>
#include<algorithm>
#include<queue>
#include<cstdio>
#define int long long
using namespace std;
inline int max(int x,int y){return x>y?x:y;}
inline int min(int x,int y){return x>y?y:x;}
const int M=4e5+5,mod=998244353,base=19260817;
int n,m;
int cnt;
int a[M],b[M],c[M],f[M];
int sum[3][M];

inline void pre()
{
	f[0]=1;
	for(int i=1;i<=n;i++) f[i]=f[i-1]*base%mod;
}

inline int get(int l,int r)
{
	return (sum[1][r]-sum[1][l-1]*f[r-l+1]%mod+mod)%mod;
}

signed main()
{
	ios::sync_with_stdio(false);
	cin.tie(0);cout.tie(0);
	cin>>n>>m;pre();
	for(int i=1;i<=n;i++) cin>>a[i];
	for(int i=1;i<=m;i++) cin>>b[i];
	for(int i=1;i<=n;i++) a[i]=a[i+1]-a[i],c[++cnt]=a[i];
	for(int i=1;i<=m;i++) b[i]=b[i+1]-b[i],c[++cnt]=b[i];//差分
	m--,n--;//差分之后数组长度减少1
	sort(c+1,c+cnt+1);
	int len=unique(c+1,c+cnt+1)-c-1;//排序后去重
	for(int i=1;i<=n;i++) a[i]=lower_bound(c+1,c+len+1,a[i])-c;
	for(int i=1;i<=m;i++) b[i]=lower_bound(c+1,c+len+1,b[i])-c;//离散化
	for(int i=1;i<=n;i++) sum[1][i]=(sum[1][i-1]*base+a[i])%mod;
	for(int i=1;i<=m;i++) sum[2][i]=(sum[2][i-1]*base+b[i])%mod;
	int ans=0;
	for(int i=1;i+m-1<=n;i++)//枚举A中每一个长度为m的子串进行比较
	{
		if(get(i,i+m-1)==sum[2][m]) ++ans;
	}
	cout<<ans<<"\n";
	return 0;
}
posted @ 2024-01-23 16:35  call_of_silence  阅读(47)  评论(0编辑  收藏  举报