哈希

定义

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

\(Hash\) 也称散列、哈希,对应的英文都是 \(Hash\) 。基本原理就是把任意长度的输入,经过 \(Hash\) 算法变成固定长度的输出。这个映射的规则就是对应的 \(Hash\) 算法,而原始数据映射后的二进制串就是哈希值。活动开发中常用的 \(MD5\) (密码学有木有?!)\(SHA\) 都是历史悠久的 \(Hash\) 算法。(某神仙逼乎)ios

说人话哈希的过程,其实能够看做对一个串的单向加密过程,而且须要保证所加的密不能高几率重复,经过这种方式来替代一些很费时间的操做。算法

特色及做用

哈希能够应用于字符串单向加密,加速查询(例如百度搜索关键字)等等。数组

对于字符哈希的实现,能够分为无错哈希多重哈希进制哈希等。函数

下面对于这三类最经常使用哈希展开论述。加密

进制哈希

进制哈希的核心即是给出一个固定进制(一般选用 \(131\) 进制),将一个串的每个元素看作一个进制位上的数字,因此这个串就能够看作一个该进制的数,那么这个数就是这个串的哈希值;则咱们经过比对每一个串的的哈希值,便可判断两个串是否相同。spa

也就是把字符赋予进制和模数,将每个字符串映射为一个小于模数数字,而后判断是否相同。翻译

具体操做:code

设置进制为 \(131\) ,模数为 \(998244353\) ,如今对一个字符串 \(s\) 进行哈希.htm

这样 hash[len] 里面就是字符串s的哈希值了。

char s[10];
  cin >> (s + 1);
  int len = strlen(s + 1);
  int base = 131, mod = 998244353;
  for (int i = 1; i <= len; ++i)	  
{
        hash[i] = ((hash[i - 1] * base) + s[i]) % mod;
      
}

\(hash\) 还有一个方便的操做就是取子串的 \(hash\) 值。

hash[l, r] = (hash[r] - hash[l - 1] * pw[r - l + 1]) % mod
//伪代码 pw[r-l+1]为进制数的(r-l+1)次方

无错哈希

记录每个已经诞生的哈希值,而后对于每个新的哈希值,咱们均可以来判断是否和已有的哈希值冲突,若是冲突,那么能够将这个新的哈希值不断加上一个大质数,直到再也不冲突 (简单粗暴)

代码:

for (int i = 1; i <= m; i++) //m个串
{
    cin >> str; //下一行的check为bool型
    while (check[hash(str)])
        hash[i] += 19260817;
    hash[i] += hash(str);
}

此种方式相似有桶查找,故存在弊端:

  • 数据过大时,\(check\) 数组就显得比较乏力。
  • 数据具备跳跃性时,会大幅浪费统计次数。

多重哈希

用不一样的两种或多种方式对数据进行哈希,而后分别比对每一种哈希值是否相同。

这显然是增长了空间和时间,但也确实增长告终果的正确性。

代码:

//多重哈希的判断操做
//check 表示当前hash的判断结果,ans表示目前相同hash操做相同的次数

for(伪代码排序,用来使哈希值单调(更好判断相 / 不一样的数量))
for (int i = 1; i <= m; i++)
{
    check = true;
    for (int j = 1; j <= qwq; j++)
    if (hash[j][i] == hash[j][i + 1])
    {
        check = false;
        break;
    }
    if (check)
        ans++; //此为判断相同个数
}

例题

洛谷 P3370 【模板】字符串哈希

题目描述

给定 \(N\) 个字符串(第 \(i\) 个字符串长度为 \(M_i\) ,字符串内包含数字、大小写字母,大小写敏感),请求出 \(N\) 个字符串中共有多少个不一样的字符串。

代码(单哈希):

#include <iostream>
#include <cstring>
#include <algorithm>
#include <cstdio>
using namespace std;
typedef unsigned long long ull;
ull base = 131;
ull a[10010];
char s[10010];
int n, ans = 1;
int prime = 233317;
ull mod = 212370440130137957ll;
ull hashe(char s[])
{
    int len = strlen(s);
    ull ans = 0;
    for (int i = 0; i < len; i++)
        ans = (ans * base + (ull)s[i]) % mod + prime;
    return ans;
}
int main()
{
    scanf("%d", &n);
    for (int i = 1; i <= n; i++)
    {
        scanf("%s", s);
        a[i] = hashe(s);
    }
    sort(a + 1, a + n + 1);
    for (int i = 1; i < n; i++)
    {
        if (a[i] != a[i + 1])
            ans++;
    }
    printf("%d", ans);
}

注意:

\(hash\) 操做会致使哈希冲突,即两个不一样的字符处理后的哈希值相同,这样会形成结果出错。

解决方案:

  • 模数取大质数

    适度增长剩余系,减小哈希冲突概率(可是模数过大会致使爆负数)

  • 双模数哈希

    相似于多重哈希。设置两个不一样的哈希模数,当且仅当两次哈希结果都想同时才断定相同,出错概率微乎其微(只要几率足够小,咱们就能够把它看做不可能事件)。

    神仙题目见 BZOJ3098(hzwer版)(含题解)。

LOJ 103. 子串查找

题目描述

给定一个字符串 \(A\) 和一个字符串 \(B\) ,求 \(B\)\(A\) 中的出现次数。\(A\)\(B\) 中的字符均为英语大写字母或小写字母。

\(A\) 中不一样位置出现的 \(B\) 可重叠。

思路

一道经典的模板题

代码

/*
By Frather_

*/
#include <iostream>
#include <iostream>
#include <cstdio>
#include <cstring>
#include <cmath>
#include <algorithm>
#include <queue>
#include <vector>
#include <set>
#include <map>
#include <stack>
#define ll long long
#define InF 0x7fffffff
#define kMax 10e5
#define kMin -10e5
#define kMOD 998244353
#define P 133
using namespace std;
/*=========================================快读*/
int read()
{
    int x = 0, f = 1;
    char c = getchar();
    while (c < '0' || c > '9')
    {
        if (c == '-')
            f = -1;
        c = getchar();
    }
    while (c >= '0' && c <= '9')
    {
        x = (x << 3) + (x << 1) + (c ^ 48);
        c = getchar();
    }
    return x * f;
}
/*=====================================定义变量*/
char a[1000010], b[1000010];
int t[1000010];
int sum[1000010];
int s;
int ans;
/*===================================自定义函数*/

/*=======================================主函数*/
int main()
{
    cin >> a + 1 >> b + 1;
    int la = strlen(a + 1);
    int lb = strlen(b + 1);
    t[0] = 1;
    for (int i = 1; i <= 1000010; i++)
        t[i] = t[i - 1] * P;
    for (int i = 1; i <= la; i++)
        sum[i] = (sum[i - 1] * P + a[i] - 'A' + 1);
    for (int i = 1; i <= lb; i++)
        s = (s * P + b[i] - 'A' + 1);
    for (int i = 0; i <= la - lb; i++)
        if (s == sum[i + lb] - sum[i] * t[lb])
            ans++;
    printf("%d\n", ans);
    return 0;
}

一些奇技淫巧

  • 使用 unsigned long long

    在数据足够大时,能够触发该数据类型的天然溢出,省去了哈希操做中的取模。

最后

鸣谢 笨蛋花的小窝qwqKnightL

鸣谢《信息学奥赛一本通提升篇》,《算法竞赛进阶指南》。

持续更新。

相关文章
相关标签/搜索