Hash入门

Hash(哈希)

一、概述

1、Hash

Hash其实是一种散列技术,散列技术是指在记录的存储位置 和它的关键字 之间建立一个确定的对应关系f ,使每一个关键字都对应一个存储位置 。即:存储位置=f(关键字)。这样,在查找的过程中,只需要通过 这个对应关系f 找到 给定值key 的映射f(key) 。只要集合中存在关键字和key相等 的记录,则必在存储位置f(key)处 。我们把这种对应关系f 称为散列函数或哈希函数。
注: 关键字可以是很多形式,可以是字符串、数字、多项式、图形等等。

2、Hash冲突

在理想的情况下,每一个 关键字,通过哈希函数计算出来的地址都是不一样的。但是在实际情况中,我们常常会碰到两个关键字key1≠key2 ,但是f(key1) = f(key2) , 这种现象称为冲突 ,并把key1和key2 称为这个散列函数的同义词

二、引例

  • 题意: 给出abcdefg七个整数,并且猜想ad+bc+cf=g ,但是无法进行如此庞大的计算,请验证猜想是否成立
  • 输入: 第一行一个正整数T,表示有T组数据。每组数据输入一行七个整数abcdefg,保证1 ≤ \leq T ≤ \leq 1000 , -1e9 ≤ \leq a,b,c,g ≤ \leq 1e9 , 0 ≤ \leq d,e,f ≤ \leq 1e9 ,保证不会出现指数和底数同为0的情况。
  • 输出: 每组数据输出一行,若猜想成立,输出Yes,否则输出No.
  • 思路: 取模做法。对多项式的值取模一个很大的质数,对g同等操作(二者取模的质数相同)。验证在哈希表中,多项式的存储位置是否等于g的存储位置。质数越大越好,这样哈希冲突的概率就越小。
    由于数据范围很大,所以需要使用快速幂qpow 。这里科普两个小知识:
  • ①同余 。即:a×b%n=(a%n × b%n)%n 。
  • ②快速幂 。eg.求ab
    b为偶数:
    原式=1×ab =(a2)b/2
    b为奇数:
    原式=1×ab =a×(a2)b/2 (此处指数的除法是程序中的整除)
    这种方法可以a一直取平方,b一直除以2,则可将复杂度降为O(log b),会快很多

ps:平时可以了解一下哪些算法较快,例如:输入方式速度快读>cin >scanf (由于读入字符串比数字快,所以可以用读字符串的方式来读入数字,快读详情自行搜索)

sample input
2
1 1 4 5 1 4 258
114514 1919810 1 2 3 4 1

sample output
Yes
No

#include<bits/stdc++.h>
#define ll long long
const ll mod=1e9+1e5+1e4+11;    //质数 
ll qpow(ll a,ll b)
{
	ll res=1;
	while(b)
	{
		if(b&1)  //b为奇数 
		{
			res*=a;
			res%=mod; 
		} 
		b>>=1;  //即b/2
		a*=a;
		a%=mod;
	}
	return res;
 } 
void solve()
{
	ll a,b,c,d,e,f,g;
	scanf("%lld%lld%lld%lld%lld%lld%lld",&a,&b,&c,&d,&e,&f,&g);
	ll ans=0;
	ans+=qpow(a,d);
	ans%=mod;
	ans=(ans+mod)%mod;   //ans有可能是负数 
	ans+=qpow(b,e);
	ans%=mod;
	ans=(ans+mod)%mod;
	ans+=qpow(c,f);
	ans%=mod;
	ans=(ans+mod)%mod;
	g=(g+mod)%mod;
	if(ans==g) puts("Yes");
	else puts("No");
}
int main()
{
	int T=1;
	scanf("%d",&T);
	while(T--)
	{
		solve();
	}
}

三、字符串Hash

常用场景: 询问次数较多,需要用哈希去预处理字符串,使询问时可以O(1)的输出

引例
  • 题意: 给出两个长度为1e5的字符串,并给出q次询问每次询问给出一个l,r,询问两个字符串l~r是否相同
  • 输入: 读入两行字符串,下一行输入q,接下来输入q行,每行输入l,r,并用空格分隔
  • 输出: 相同输出“yes",否则输出”no“
  • 思路: 建立一个如下的哈希表:
    Hash[ r r r ]=s[ 1 ]× p p pr+s[ 2 ]× p p pr-1…s[ r ]× p p p0
    Hash[ l l l - 1 ]=s[ 1 ]× p p p^l -1^+s[ 2 ]× p p pl-2…s[ l - 1 ]× p p p0
    Hash[ l l l ~ r r r ]=s[ l l l p p pr-l+s[ l + 1 ]× p p pr-l-1…s[ r ]× p p p0
void get_hash()
{
    return((hash[ r ]-hash[ l - 1 ] * pow(p, r - l + 1)) % mod + mod) % mod;
}

sample input
abcde
bbcda
2
1 5
2 4

sample output
no
yes

例题 UVA - 11475
  • 题意: 给定字符串,请在末尾尽可能少地补上几个字符,使其成为回文串
  • 输入: 输入多组数据,每组输入一行字符串
  • 输出: 输出补完后的回文串
  • 思路: ①找出原字符串中从结尾往前本身就为回文串的部分 ②补上剩余字符串的倒序
    建一个正向哈希数组和反向哈希数组,判断正向和反向是否相等即可

Sample Input
aaaa
abba
amanaplanacanal
xyz

Sample Output
aaaa
abba
amanaplanacanalpanama
xyzyx

#include<bits/stdc++.h>
using namespace std;
const int maxn=1e6+11;
const long long mod=1e9+1e5+1e4+11;
char s[maxn];
unsigned long long h[maxn],h_[maxn],po[maxn],p=131;   //p是一个质数 可以自己定的 
//无符号长整型能帮助自然溢出   //h数组是正向哈希数组 h_数组是反向哈希数组 
unsigned long long get_ha(int l,int r)
{
	return h[r]-h[l-1]*po[r-l+1];
}
unsigned long long get_ha2(int l,int r)
{
	return h_[r]-h[l-1]*po[r-l+1];
}
void solve()
{
	int len=strlen(s+1);
	po[0]=1,h[0]=0;
	for(int i=1;i<maxn;i++)  po[i]=po[i-1]*p;
	for(int i=1;i<=len;i++)
	{
		h[i]=h[i-1]*p+s[i];
		h_[i]=h_[i-1]*p+s[len-i+1];
	}
	int minlen=0;  //前面可以保留的长度 
	for(int i=1;i<=len;i++)   //从前往后开始枚举 
    {   //枚举正向哈希数组中的i~len是否等于反向哈希数组的1~(len-i+1) 
     	unsigned long long pre=get_ha(i,len);  //前
		unsigned long long suf=get_ha2(1,len-i+1);  //后 
		if(pre==suf) 
		{
			minlen=i-1;
			break;
		}
	}
	printf("%s",s+1);
	for(int i=minlen;i>=1;i--)  printf("%c",s[i]);
	puts("");
}
int main()
{
	int T=1;
	while(~scanf("%s",s+1))
	{
		solve();
	}
	return 0; 
 }

四、树哈希

树形哈希

例题

原题链接:
洛谷P5043

  • 题意: 我们把 N个点,N−1 条边的连通无向图称为树。若将某个点作为根,从根开始遍历,则其它的点都有一个前驱,这个树就成为有根树。对于两个树 T 1 T_1 T1 T 2 T_2 T2 ,如果能够把树 T 1 T_1 T1 的所有点重新标号,使得树 T 1 T_1 T1 和树 T 2 T_2 T2 完全相同,那么这两个树是同构的。也就是说,它们具有相同的形态。现在,给你 M个无根树,请你把它们按同构关系分成若干个等价类。
  • 输入: 第一行,一个整数 M。接下来 M 行,每行包含若干个整数,表示一个树。第一个整数 N表示点数。接下来 N个整数,依次表示编号为1到 N的每个点的父亲结点的编号。根节点父亲结点编号为0。
  • 输出: 输出 M行,每行一个整数,表示与每个树同构的树的最小编号。

sample input
4
4 0 1 1 2
4 2 0 2 3
4 0 1 1 1
4 0 1 2 3

sample output
1
1
3
1

hint
编号为 1, 2, 4的树是同构的。编号为 3 的树只与它自身同构。对于100% 的数据,1≤N,M≤50。

#include<bits/stdc++.h>
using namespace std;
#define ull unsigned long long
const int maxn=60;
vector<int>v[maxn],ans[maxn];
ull f[maxn],Size[maxn];
int prime[maxn<<1];
int vis[20000];
void get_prime()
{  //求出前100的素数是哪些 
	int cnt=0;	
	for(int i=2;cnt<100;i++)
	{
		if(vis[i]) continue;
		prime[++cnt]=i;
		for(int j=i*i;j<=10000;j+=i) vis[j]=1;
	}
} 
void dfs(int x,int fa){
	Size[x]=f[x]=1;  //以他自身为子树的哈希值是1 
	for(int i=0;i<v[x].size();i++)
	{
		int y=v[x][i];  //y为x的子节点 
		if(y==fa) continue;
		dfs(y,x);  //遍历y节点 
		Size[x]+=Size[y];  //累加y节点的子树大小 
		f[x]+=f[y]*prime[Size[y]];
	}
}
int main(){
	get_prime();
	int n;
	scanf("%d",&n);  //有n棵树 
	for(int i=1;i<=n;i++)
	{
		int m;
		scanf("%d",&m);  //第i棵树的大小是m 
		for(int j=1;j<=m;j++)
		{
			int x;
			scanf("%d",&x);  //在x和j之间建边 
			if(x) v[x].push_back(j),v[j].push_back(x);  
		} 
		for(int j=1;j<=m;j++)
		{   //以每个节点为根 求一遍dfs 
			dfs(j,0);            //求出j为根的哈希值 
			ans[i].push_back(f[j]);
		}
		sort(ans[i].begin(),ans[i].end());  //进行排序 
		for(int j=1;j<=i;j++)
		{   //和之前所有的哈希值判断一遍 
			int ok=1;
			if(ans[j].size()!=ans[i].size()) continue;
			for(int k=0;k<ans[j].size();k++)
			{
				if(ans[i][k]!=ans[j][k])
				{  //某一个哈希值不相等 说明不是同构 
					ok=0;
					break;
				}
			}
			if(ok) 
			{   //如果全部相等 说明第i个子树和第j个子树同构 
				printf("%d\n",j);
				break;
			}
		}
		for(int j=1;j<=m;j++)
		{
			v[j].clear();  //初始化 进入下一轮 
		} 
	} 
}

(待更新其他例题…)

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值