算法基础-字符串哈希

字符串哈希

1. 回忆:哈希函数与哈希冲突

• 哈希函数:将关键字映射成对应的地址的函数,记为 Hash(key) = Addr 。
• 哈希冲突:哈希函数可能会把两个或两个以上的不同关键字映射到同⼀地址,这种情况称为哈希冲 突。

2. 字符串哈希

定义⼀个把字符串映射到整数的函数hash ,这就是字符串哈希。说⽩了,就是将⼀个字符串⽤⼀个 整数表⽰。

3. 字符串哈希中的哈希函数

在字符串哈希中,有⼀种冲突概率较⼩的哈希函数,将字符串映射成 p 进制数字:
hash ( s ) = s [ i ] × p ( i =0 ∑ n −1 ni −1 mod M )
其中p, 通常取质数131 或者13331 。如果把哈希值定义为 unsigned long long 类型,在 C++ 中,溢出就会⾃动取模。
(你没有看错,字符串哈希就是背⼀个公式即可......)
但是,实际求哈希值时,我们⽤的是 前缀哈希 的思想来求,这样会和下⾯的多次询问⼦串哈希⼀致。

4. 前缀哈希数组

单次计算⼀个字符串的哈希值复杂度是 O ( N )。如果需要多次询问⼀个字符串的⼦串的哈希值,每次 重新计算效率⾮常低下。
⼀般利⽤ 前缀和思想先预处理字符串中每个前缀的哈希值 ,这样的话每次就能快速求出⼦串的哈希
了。

cpp 复制代码
typedef unsigned long long ULL;
const int N = 1e6 + 10, P = 13331;
char s[N];
int len;
ULL f[N]; // 前缀哈希数组
ULL p[N]; // 记录 p 的 i 次⽅
// 处理前缀哈希数组以及 p 的 i 次⽅数组
void init_hash()
{
f[0] = 0; p[0] = 1;
for(int i = 1; i <= len; i++)
{
f[i] = f[i - 1] * P + s[i];
p[i] = p[i - 1] * P;
}
}
// 快速求得任意区间的哈希值
ULL get_hash(int l, int r)
{
return f[r] - f[l - 1] * p[r - l + 1];
}

如果题⽬只是简单的求单个字符串的哈希值:

cpp 复制代码
typedef unsigned long long ULL;
const int N = 1e6 + 10;
int len;
char s[N];
ULL gethash()
{
ULL ret = 0;
for(int i = 1; i <= len; i++)
{
ret = ret * p + s[i];
}
return ret;
}

1.1 【模板】字符串哈希

题⽬来源: 洛⾕
题⽬链接: P3370 【模板】字符串哈希
难度系数: ★★

题目描述

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

友情提醒:如果真的想好好练习哈希的话,请自觉。

输入格式

第一行包含一个整数 N,为字符串的个数。

接下来 N 行每行包含一个字符串,为所提供的字符串。

输出格式

输出包含一行,包含一个整数,为不同的字符串个数。

输入输出样例

输入 #1复制

复制代码
5
abc
aaaa
abc
abcc
12345

输出 #1复制

复制代码
4

说明/提示

数据范围

对于 30% 的数据:N≤10,Mi​≈6,Mmax​≤15。

对于 70% 的数据:N≤1000,Mi​≈100,Mmax​≤150。

对于 100% 的数据:N≤10000,Mi​≈1000,Mmax​≤1500。

样例说明

样例中第一个字符串 abc 和第三个字符串 abc 是一样的,所以所提供字符串的集合为 {aaaa,abc,abcc,12345},故共计 4 个不同的字符串。

拓展阅读

以下的一些试题从不同层面体现出了字符串哈希算法的正确性分析。


【解法】

字符串哈希模板题,就是背⼀个哈希函数~


【参考代码】

cpp 复制代码
#include <iostream>
#include <string>   // 字符串操作必备(原代码漏了,会编译报错!)
#include <algorithm>// sort排序函数
using namespace std;

typedef unsigned long long ULL; // 无符号长整型:溢出自动取模,避免哈希冲突
const int N = 1e4 + 10;  // 最多1万个字符串,留10余量
const int P = 131;       // 哈希基数(经验值,131/13331都可以)
int n;                   // 字符串个数
ULL a[N];                // 存储每个字符串的哈希值

// 字符串哈希函数:输入字符串s,返回对应的哈希值
ULL get_hash(string& s)
{
    ULL ret = 0;         // 初始哈希值为0
    // 遍历字符串每个字符(s.size()是字符串长度)
    for(int i = 0; i < s.size(); i++)
    {
        // 核心公式:当前哈希值*P + 当前字符的ASCII码
        ret = ret * P + s[i];
    }
    return ret;
}

int main()
{
    cin >> n;  // 读入字符串个数
    
    // 遍历每个字符串,计算哈希值并存入数组a
    for(int i = 1; i <= n; i++)
    {
        string s;
        cin >> s;                // 读入字符串
        a[i] = get_hash(s);      // 计算哈希值并存储
    }
    
    // 排序:相同哈希值会排在一起,方便去重统计
    sort(a + 1, a + 1 + n);
    
    int ret = 1;  // 至少有1个不同字符串,初始值为1
    // 遍历排序后的哈希数组,统计不同值的数量
    for(int i = 2; i <= n; i++)
    {
        // 如果当前哈希值和前一个不同,说明是新字符串
        if(a[i] != a[i - 1])
            ret++;
    }
    
    cout << ret << endl;  // 输出不同字符串的数量
    return 0;
}

1.2 兔⼦与兔⼦

题⽬来源: 洛⾕
题⽬链接: P10468 兔⼦与兔⼦
难度系数: ★★

题目描述

很久很久以前,森林里住着一群兔子。

有一天,兔子们想要研究自己的 DNA 序列。

我们首先选取一个好长好长的 DNA 序列(小兔子是外星生物,DNA 序列可能包含 26 个小写英文字母)。

然后我们每次选择两个区间,询问如果用两个区间里的 DNA 序列分别生产出来两只兔子,这两个兔子是否一模一样。

注意两个兔子一模一样只可能是他们的 DNA 序列一模一样。

输入格式

第一行输入一个 DNA 字符串 S。

第二行一个数字 m,表示 m 次询问。

接下来 m 行,每行四个数字 l1​,r1​,l2​,r2​,分别表示此次询问的两个区间,注意字符串的位置从 1 开始编号。

输出格式

对于每次询问,输出一行表示结果。

如果两只兔子完全相同输出 Yes,否则输出 No(注意大小写)。

输入输出样例

输入 #1复制

复制代码
aabbaabb
3
1 3 5 7
1 3 6 8
1 2 1 2

输出 #1复制

复制代码
Yes
No
Yes

说明/提示

数据保证,1≤∣S∣,m≤106。其中,∣S∣ 为字符串 S 的长度。


【解法】

构造字符串的前缀哈希数组,在前缀哈希数组中快速拿到区间的哈希值。

【参考代码】


cpp 复制代码
#include <iostream>
#include <string>   // 字符串操作必备
using namespace std;

typedef unsigned long long ULL; // 无符号长整型:溢出自动取模,降低冲突
const int N = 1e6 + 10;  // 字符串最长1e6,留10余量
const int P = 13331;     // 哈希基数(比131更大,冲突概率更低)

int n;                   // 字符串长度(补空格后)
string s;                // 存储DNA字符串(补前导空格,下标从1开始)
ULL f[N];                // 前缀哈希数组:f[i] = 前i个字符的哈希值
ULL p[N];                // 幂次数组:p[i] = P^i

// 初始化前缀哈希和幂次数组
void init_hash()
{
    p[0] = 1;  // 初始化:P^0 = 1(任何数的0次方都是1)
    for(int i = 1; i <= n; i++)
    {
        // 前缀哈希公式:前i个字符的哈希 = 前i-1个的哈希*P + 第i个字符的ASCII码
        f[i] = f[i - 1] * P + s[i];
        // 幂次更新:P^i = P^(i-1) * P
        p[i] = p[i - 1] * P;
    }
}

// 计算区间[l,r]的哈希值
ULL get_hash(int l, int r)
{
    // 核心公式:区间哈希 = 前r个哈希 - 前l-1个哈希 * P^(r-l+1)
    return f[r] - f[l - 1] * p[r - l + 1];
}

int main()
{
    cin >> s;          // 读入DNA字符串(比如"aabbaabb")
    n = s.size();      // 原字符串长度(8)
    s = " " + s;       // 补前导空格,让字符串下标从1开始(变成" aabbaabb")
    
    init_hash();       // 初始化哈希数组
    
    int m;
    cin >> m;          // 读入询问次数
    while(m--)
    {
        int l1, r1, l2, r2;
        cin >> l1 >> r1 >> l2 >> r2; // 读入两个区间
        
        // 计算两个区间的哈希值
        ULL hash1 = get_hash(l1, r1);
        ULL hash2 = get_hash(l2, r2);
        
        // 哈希值相同→子串相同,否则不同
        if(hash1 == hash2)
            cout << "Yes" << endl;
        else
            cout << "No" << endl;
    }
    return 0;
}
相关推荐
lixzest2 小时前
C++中经常用的头文件介绍
数据结构·c++·算法
保持低旋律节奏2 小时前
linux——进程调度(时间片+优先级轮转调度算法O(1))
linux·运维·算法
狂炫冰美式2 小时前
当硅基神明撞上人类的“叹息之墙”:距离证明哥德巴赫猜想,AI还有多远?
前端·算法·架构
一起养小猫2 小时前
《Java数据结构与算法》第四篇(四):二叉树的高级操作查找与删除实现详解
java·开发语言·数据结构·算法
前端小白在前进3 小时前
力扣刷题:千位分割数
javascript·算法·leetcode
free-elcmacom3 小时前
机器学习高阶教程<11>当数据开始“折叠”:流形学习与深度神经网络如何发现世界的隐藏维度
人工智能·python·神经网络·学习·算法·机器学习·dnn
小年糕是糕手3 小时前
【C/C++刷题集】string类(一)
开发语言·数据结构·c++·算法·leetcode
努力学算法的蒟蒻3 小时前
day40(12.21)——leetcode面试经典150
算法·leetcode·面试
ToddyBear3 小时前
从字符游戏到 CPU 指令集:一道算法题背后的深度思维跃迁
数据结构·算法