『笔记』哈希

定义

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

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

说人话哈希的过程,其实可以看作对一个串的单向加密过程,并且需要保证所加的密不能高概率重复,通过这种方式来替代一些很费时间的操作。

特点及作用

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

对于字符哈希的实现,可以分为无错哈希多重哈希进制哈希等。

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

进制哈希

进制哈希的核心便是给出一个固定进制(通常选用 \(131\) 进制),将一个串的每一个元素看做一个进制位上的数字,所以这个串就可以看做一个该进制的数,那么这个数就是这个串的哈希值;则我们通过比对每个串的的哈希值,即可判断两个串是否相同。

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

具体操作:

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

这样 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

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

持续更新。

posted @ 2021-01-07 16:36  Frather  阅读(213)  评论(1编辑  收藏  举报