【C++天梯计划】1.8 哈希算法(hash)

🎆🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎆
今天我要开启一个新计划----【C++天梯计划】
目的是通过天梯计划,通过题目和知识点串联的方式,完成C++复习与巩固。

什么是哈希算法?

哈希算法,一般翻译做散列、杂凑,或音译为哈希,是把任意长度的输入(又叫做预映射pre-image)通过散列算法变换成固定长度的输出,该输出就是散列值。这种转换是一种压缩映射,也就是,散列值的空间通常远小于输入的空间,不同的输入可能会散列成相同的输出,所以不可能从散列值来确定唯一的输入值。简单的说就是一种将任意长度的消息压缩到某一固定长度的消息摘要的函数。

哈希基本概念

若结构中存在和关键字K相等的记录,则必定在f(K)的存储位置上。由此不需比较便可直接取得所查记录。称这个对应关系f为散列函数(Hash function)按这个事先建立的表为散列表。

对不同的关键字可能得到同一散列地址,即key1≠key2,而f(key1)=f(key2),这种现象称碰撞。具有相同函数值的关键字对该散列函数来说称做同义词。综上所述,根据散列函数H(key)和处理冲突的方法将一组关键字映射到一个有限的连续的地址集(区间)上,并以关键字在地址集中的“象” 作为记录在表中的存储位置,这种表便称为散列表,这一映象过程称为散列造表或散列,所得的存储位置称散列地址。

若对于关键字集合中的任一个关键字,经散列函数映象到地址集合中任何一个地址的概率是相等的,则称此类散列函数为均匀散列函数(Uniform Hash function),这就是使关键字经过散列函数得到一个“随机的地址”,从而减少冲突。

哈希算法性质

所有散列函数都有如下一个基本特性:如果两个散列值是不相同的(根据同一函数),那么这两个散列值的原始输入也是不相同的。这个特性是散列函数具有确定性的结果。但另一方面,散列函数的输入和输出不是一一对应的,如果两个散列值相同,两个输入值很可能是相同的,但不绝对肯定二者一定相等(可能出现哈希碰撞)。输入一些数据计算出散列值,然后部分改变输入值,一个具有强混淆特性的散列函数会产生一个完全不同的散列值。
典型的散列函数都有无限定义域,比如任意长度的字节字符串,和有限的值域,比如固定长度的比特串。在某些情况下,散列函数可以设计成具有相同大小的定义域和值域间的一一对应。一一对应的散列函数也称为排列。可逆性可以通过使用一系列的对于输入值的可逆“混合”运算而得到。

例题1:子串判重

题目描述

给定一个含有 2626 个小写英文字母的字符串。有 mm 次询问,每次给出 22 个区间,请问这两个区间里的子字符串是否一样?

输入

第一行输入一个字符串 SS 。
第二行一个数字 mm,表示 mm 次询问。
接下来 mm 行,每行四个数字 l1,r1,l2,r2l1,r1,l2,r2 ,分别表示此次询问的两个区间,注意字符串的位置从1开始编号。
数据范围:
1≤length(S),m,l1,r1,l2,r2≤10000001≤length(S),m,l1,r1,l2,r2≤1000000。

输出

对于每次询问,输出一行表示结果。
如果两个子串完全一样,输出 Yes,否则输出 No(注意大小写)。

输入输出样例

输入
aabbaabb
3
1 3 5 7
1 3 6 8
1 2 1 2
输出
Yes
No
Yes

思路

字符串哈希问题,注意本题数据量较大,因此不要循环字符串时求字符串长度,如果在循环中求,循环一次,求解一次,效率很低。

代码
#include <bits/stdc++.h>
using namespace std;

const int N = 1e6 +10,P = 131;
typedef unsigned long long ULL;
int m;
char s[N];
ULL h[N],p[N];

//求子串的哈希
ULL fun(int l,int r){
	return h[r] - h[l-1] * p[r-l+1];
} 

int main(){
	scanf("%s%d",s+1,&m);//下标从1开始取
	p[0] = 1;
	//计算次数过多,不用每次循环都求一次长度
	int len = strlen(s+1); 
	//预处理前缀和,以及p^n 
	for(int i = 1;i <= len;i++){
		p[i] = p[i-1] * P;
		h[i] = h[i-1] * P + s[i]-'a'+1;
	} 
	
	int l1,r1,l2,r2;
	while(m--){
		scanf("%d%d%d%d",&l1,&r1,&l2,&r2);
		if(fun(l1,r1)==fun(l2,r2)) printf("Yes\n");
		else printf("No\n");
	}
	return 0;
}

例题2:三个火枪手

题目描述

三个火枪手阿多斯、波尔多斯、阿拉密斯要给好朋友达达尼昂留下一份加密的信息。
阿多斯写出他们要留下的仅包含大写字母的字符串 AA ,波尔多斯将这个字符串复制了一遍,得到字符串 BB,阿拉密斯在字符串 BB 的任意一个位置(也可以在首尾的位置)插入一个大写字母得到最终的字符串 CC。
现给定字符串 CC,请编程计算出最初的字符串 AA。

输入

第 11 行输入一个整数 NN,代表字符串 CC 的长度;
第 22 行读入一个长度为 NN 的字符串 CC;

输出

输出计算出的字符串 AA;
请注意,可能由于波尔多斯、阿拉密斯的失误,得到最终得到的字符串 CC 有误,导致无法还原出字符串 AA,这种情况下,请输出NOT POSSIBLE;
也有可能计算发现无法唯一的还原出字符串 AA,这种情况下,请输出 NOT UNIQUE。

输入输出样例

输入
7
XYAZXYZ
输出
XYZ
输入
7
ABCDEFG
输出
NOT POSSIBLE
输入
9
ABABABABA
输出
NOT UNIQUE

思路

例如有字符串 XYAZXYZXYAZXYZ ,枚举每个位置,将每个位置删除后,计算出剩余字符串各一半的哈希值,比较是否相等。
要注意特殊情况,比如字符串 AAAAAAAAAAAAAA,虽然删除每个位置都是可以的,但要注意,得到的解是唯一的,因此不能以可以删除位置的数量作为判断答案是否唯一的标准;可以用删除一个字符后,剩余字符串一半字符的哈希值,作为判断答案唯一的标准。

代码
#include<bits/stdc++.h>
using namespace std;

typedef unsigned long long ULL;
const int N = 2.5e6 + 10,P = 131;
char s[N];
ULL p[N],h[N];
set<ULL> st;//记录哈希值 
int n;

void gethash() {
	p[0] = 1;
	for(int i = 1; i <= n; i++) {
		p[i] = p[i-1] * P;
		h[i] = h[i-1] * P + (s[i] - 'A' + 1);
	}
}

ULL get(int l,int r) {
	if(l > r) return 0;
	return h[r] - h[l-1] * p[r - l + 1];
}

int main() {
	scanf("%d%s",&n,s+1);

	gethash();//计算哈希值

	//枚举每个可能被删除的字符
	ULL l,r;
	int c = 0,mid = n / 2 + 1;
	char res = ' ';
	for(int i = 1; i <= n; i++) {
		//拼出前后两半的哈希值
		if (i == 1) {
			l = get(2, mid);
			r = get(mid + 1, n);
		} else if(i == n) {
			l = get(1, mid - 1);
			r = get(mid, n - 1);
		}else if(i < mid) {
			l = get(1,i-1) * p[mid-(i+1)+1] + get(i+1,mid);
			r = get(mid+1,n);
		} else if(i > mid) {
			l = get(1,mid - 1);
			r = get(mid+1,i-1) * p[n-(i+1)+1] + get(i+1,n);
		} else if(i == mid) {
			l = get(1,mid - 1);
			r = get(mid + 1,n);
		}

		if(l == r) {
			st.insert(l);
			if(st.size() > 1) {
				printf("NOT UNIQUE\n");
				return 0;
			}
			//判断答案是哪一半
			if(i <= mid) res = 'R';//右侧一半
			else res = 'L';//左侧一半
		}
	}

	if(st.size() == 0) printf("NOT POSSIBLE");
	else {
		s[mid] = '\0';
		if(res == 'L') printf("%s",s+1);
		else printf("%s",s+mid+1);
	}
	return 0;
}
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值