字符串前缀哈希法

基本原理 

即h[i]的公式为

步骤

首先先预处理所有的前缀哈希

把字符串看做p进制的数(不能把字母映射成0),

当p = 131 或者 13331,Q = 2^64在一般情况下,假定不会出现冲突

已知h[r],h[l-1],如何求[l,r]的哈希值? 

把h[l]移到与h[r]对齐的部分 h[l-1] * p^(r - l + 1)

h[r] -  h[l-1]*p^(r - l + 1)

假设你已经计算了 h[r]h[l-1],它们分别代表了以下内容:

  • h[r] 包含了从 1r 的所有字符的哈希值。
  • h[l-1] 包含了从 1l-1 的所有字符的哈希值。

我们想得到子串 [l, r] 的哈希值,可以认为这个哈希值是从 h[r] 中扣除 h[l-1] 所代表的那部分内容后剩下的部分。

为什么需要乘以 Base^{r-l+1}

  • h[l-1] 代表的部分是从 1l-1 的字符的哈希值。要计算从第 l 个字符到第 r 个字符的哈希值,需要将 h[l-1] 对应的部分移到 h[r] 的第 l 个字符开始的地方。这就需要将 h[l-1] 乘以 Base^{r-l+1}
  • 乘以 Base^{r-l+1} 的目的是将原来属于前 l-1 个字符的影响“移位”到从 lr 的位置上,从而能够从 h[r] 中正确扣除这一部分。

字符串前缀哈希的优势

可以快速判断请你判断 [l1,r1] 和 [l2,r2] 这两个区间所包含的字符串子串是否完全相同。

题目

一:

#include<bits/stdc++.h>

using namespace std;
typedef unsigned long long ULL;
const int N = 1e5+10,Base = 131;
ULL h[N],p[N];
// h[i]前i个字符的hash值
// p数组用于存储 Base 的不同次方值
int n,m;
string s;

ULL Calcular(int l, int r){
    return h[r] - h[l - 1] * p[r - l + 1];
}
int main() {
    cin >> n >> m;
    cin >> s;

    p[0] = 1; // Base^0 = 1
    h[0] = 0; // 初始哈希值为0

    for(int i = 0; i < n; i++){
        p[i + 1] = p[i] * Base;// 计算Base的幂次
        h[i + 1] = h[i] * Base + s[i];// 计算前缀哈希值
    }

    while(m--){
        int l1,r1,l2,r2;cin >> l1 >> r1 >> l2 >> r2;
        if(Calcular(l1,r1) == Calcular(l2,r2)) cout << "Yes\n";
        else cout << "No\n";
    }

    return 0;
}

 二:

#include<bits/stdc++.h>

using namespace std;
typedef unsigned long long ULL;
const int N = 5e3+10,Base = 131;
ULL h[N],p[N];
map<ULL,int> mp;
// h[i]前i个字符的hash值
// p数组用于存储 Base 的不同次方值
// mp用于检测之前是否有完全相同的子串
int n,m;
string s;
void InitiaHash()
{
    p[0] = 1; // Base^0 = 1
    h[0] = 0; // 初始哈希值为0

    for(int i = 0; i < n; i++){
        p[i + 1] = p[i] * Base;// 计算Base的幂次
        h[i + 1] = h[i] * Base + s[i];// 计算前缀哈希值
    }
}

ULL GetHash(int l, int r){
    return h[r] - h[l - 1] * p[r - l + 1];
}

//
bool check(int mid){
    mp.clear();
    int l = 0, r = mid;
    while(r <= n - mid){
        // 根据mid分为两个自串
        mp[GetHash(l + 1,l + mid)] = 1;
        if(mp[GetHash(r + 1, r + mid)] == 1) return 1;
        l ++, r ++;
    }
    return 0;
}

int main() {
    cin >> n;
    cin >> s;

    InitiaHash();

   int l = 0, r = n;
   while(l < r){
        // mid表示满足要求的子串的长度
        int mid = (l + r + 1) >> 1;
        // 找到了但是长度可以增大所以l = mid,增大mid值
        if(check(mid)) l = mid;
        else r = mid - 1;
   }
   cout << l << '\n';

    return 0;
}

 r <= n - mid是什么意思?

r <= n - mid的目的是确保我们在字符串中检查的每一个子串都能完整地取出,并且其长度为 mid。这个条件是用来限制 r 指针的位置,以确保 r 指向的子串不会超出字符串的范围。

1. r 指针的含义
  • r 指针表示右半部分 当前正在检查的子串的起始位置。
2. 为什么 r <= n - mid

因为不能让右半部分检查的范围超过n,故r + mid <= n即r <= n - mid

二分查找的思路

二分查找的核心思想是通过缩小搜索范围逐步逼近最终答案。具体到这个问题中:

  1. 二分查找的初始范围

    • l 是二分查找的左边界,初始时为 0(表示最短的子串长度)。
    • r 是二分查找的右边界,初始时为 n(表示最长的子串长度,整个字符串)。
  2. check(mid) 返回 1

如果 check(mid) 返回 1,说明存在长度为 mid 的子串满足条件。但是,这并不意味着这是最长的满足条件的子串,因为可能存在更长的子串也满足条件。

因此,我们需要继续尝试更大的子串长度,也就是更新 l = mid,并在下一次二分查找中尝试更大的 mid 值(即右侧的部分)。

为什么check函数是检查[l + 1,l + mid]以及[r + 1,r + mid]

为什么使用 l + 1 而不是 l

当你调用 GetHash(l + 1, l + mid) 时:

  • l + 1 对应的其实是字符串的第 l + 1 个字符(注意这里从 1 开始算,因为哈希数组 h是基于 1 索引的)。
  • GetHash(l + 1, l + mid) 会计算从第 l + 1 个字符到第 l + mid 个字符的哈希值,即这个子串的哈希值。

如果你用 GetHash(l, l + mid)

  • 由于哈希数组 h[] 是 1 索引的,而字符串 s[] 是 0 索引的,GetHash(l, l + mid) 会计算从 ll + mid - 1 的哈希值。
  • l 本身是 0 索引,如果 l = 0,则 h[l] 对应的是空字符串的哈希值,这样就会导致计算错误,因为 h[0] 并不是第 0 个字符的哈希值,而是一个特殊的初始值。
  • 28
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值