AcWing 841. 字符串哈希

题目描述


分析:

字符串 hash 小试牛刀

我们在之前模拟散列时,设置的哈希函数为将一个元素(element, e)输入哈希函数中,输出是一个整数,而那时的 e e e 为一个有范围的整数。现在我们考虑更复杂的情形, e e e 为一个字符串,为了区分,记为 E E E,字符串哈希函数就是将一个字符串 E E E 映射为一个整数,使得该整数可以尽可能地代表一个字符串 E E E
假设字符串 E E E 由大写字母 A − Z A-Z AZ 和小写字母 a − z a-z az 构成,在这个基础上,我们可以对字母进行编码:利用 c o n v e r t ( ) convert() convert() 函数,将 A − Z A-Z AZ 1 − 26 1-26 126 a − z a-z az 27 − 52 27-52 2752,举个例子 c o n v e r t ( D ) = 4 convert(D) = 4 convert(D)=4。这样就把大小写字母对应到了五十三进制中。接着,按照将五十三进制转换为十进制的思路,由进制转换的结论可知,在进制转换中,得到的十进制是唯一的,由此便可将字符串映射成整数的需求。这里转换成的整数最大为 5 3 l e n g t h − 1 53^{length}-1 53length1(进制转换的基本性质), l e n g t h length length 为字符串的长度。实现代码如下:

// 哈希函数 h() 本质即为进制转化
int h(char* E)
{
	int ans = 0;
	for (int i = 0; E[i]; i ++)
	{
		if (E[i] >= 'A' && E[i] <= 'Z') ans = ans * 53 + (E[i] - 'A');
		else if (E[i] >= 'a' && E[i] <= 'z') ans = ans * 53 + (E[i] - 'a') + 26;
	}
	return ans;
}

假设字符串为"BCDE",进行哈希映射后得到的整数为 5 ∗ 5 3 0 + 4 ∗ 5 3 1 + 3 ∗ 5 3 2 + 2 ∗ 5 3 3 5*53^0+4*53^1+3*53^2+2*53^3 5530+4531+3532+2533。从代码和样例中可以看出为什么我们在之前将 A − Z A-Z AZ 设为 1 − 26 1-26 126 而不是 0 − 25 0-25 025,这是因为如果 “A” 为 0 0 0,那么 “AA”、“AAA” 都为 0 0 0 了。

字符串 hash 进阶

对于上面的分析,我们可以总结出一个更系统的式子:

S [ i ] = S [ i − 1 ] ∗ p + E [ i ] S[i]=S[i-1]*p+E[i] S[i]=S[i1]p+E[i]

其中 p p p 为我们所采取的进制, E [ i ] E[i] E[i] 表示字符串的第 i i i 位是什么, S [ i ] S[i] S[i] 表示字符串的前 i i i 个字符的子串的 hash 值。这样,当 i i i 取遍 1 − l e n g t h 1-length 1length 后, S [ l e n g t h ] S[length] S[length] 就是整个字符串的哈希值,而其他位置保存了部分字串的 hash 值。注意这里没有了 c o n v e r t ( ) convert() convert() 函数,因为我们打算直接是用字符串字符的 ASCII 码 ,因此 p p p 进制的选择就扑朔迷离了。
在转换过程中,字符串与整数是一一对应的,但由于没有适当的处理,当字符串字符较长时,产生的数会非常大,没办法用一般的数据类型存储。因此,按照之前对哈希的理解,要进行取模,即:

S [ i ] = ( S [ i − 1 ] ∗ p + E [ i ] ) % k S[i]=(S[i-1]*p+E[i]) \%k S[i]=(S[i1]p+E[i])%k

通过这种方式可以把字符串转换成范围上能接受的整数。但这可能又产生另外的问题,也就是 hash值产生冲突。而根据y总的经验值, p p p 133 133 133 13331 13331 13331 k k k 2 64 2^{64} 264,在 99.99 % 99.99 \% 99.99% 的情况下是不会产生冲突的。因此这和普通哈希是有区别的,普通哈希是可以处理冲突的,而字符串哈希是不考虑冲突的(无法解决?)

子串的 hash值

考虑求解子串的 hash值,也就是求解 S [ l . . . r ] S[l...r] S[l...r]。由于上面介绍的取模运算在括号最外层,而我们接下来需要考虑括号内的问题,所以先暂时把取模运算放在一边,简化讨论。即:

S [ i ] = ( S [ i − 1 ] ∗ p + E [ i ] ) S[i]=(S[i-1]*p+E[i]) S[i]=(S[i1]p+E[i])

子串的 hash值, S [ l . . . r ] S[l...r] S[l...r] 实际上等于把字符串 E [ l . . . r ] E[l...r] E[l...r] p p p 进制转换为十进制,也就是如下式子所表示的:

S [ l . . . r ] = E [ l ] ∗ p r − l + E [ l + 1 ] ∗ p r − l − 1 + . . . + E [ r ] ∗ p 0 S[l...r]=E[l]*p^{r-l}+E[l+1]*p^{r-l-1}+...+E[r]*p^0 S[l...r]=E[l]prl+E[l+1]prl1+...+E[r]p0

看到这个式子有没有觉得头大呢?没关系,这是很正常的,因为它有许多的参数需要理清,先看一个实例:

ac841a.jpg

上面的 S [ l . . . r ] S[l...r] S[l...r] 的公式可以通过 S [ j ] S[j] S[j] 推导出:

S [ r ] = S [ r − 1 ] ∗ p + E [ r ] S[r]=S[r-1]*p+E[r] S[r]=S[r1]p+E[r]
= ( S [ r − 2 ] ∗ p + E [ r − 1 ] ) ∗ p + E [ r ] \qquad =(S[r-2]*p+E[r-1])*p+E[r] =(S[r2]p+E[r1])p+E[r]
= S [ r − 2 ] ∗ p 2 + E [ r − 1 ] ∗ p + E [ r ] \qquad =S[r-2]*p^2+E[r-1]*p+E[r] =S[r2]p2+E[r1]p+E[r]
= . . . \qquad = ... =...
= S [ l − 1 ] ∗ p r − l + 1 + E [ l ] ∗ p r − l + . . . + E [ r ] ∗ p 0 \qquad = S[l-1]*p^{r-l+1}+E[l]*p^{r-l}+...+E[r]*p^0 =S[l1]prl+1+E[l]prl+...+E[r]p0
= S [ l − 1 ] ∗ p r − l + 1 + S [ l . . . r ] \qquad = S[l-1]*p^{r-l+1}+S[l...r] =S[l1]prl+1+S[l...r]

移项可以得到:

S [ l . . . r ] = S [ r ] − S [ l − 1 ] ∗ p r − l + 1 S[l...r]=S[r]-S[l-1]*p^{r-l+1} S[l...r]=S[r]S[l1]prl+1

于是就得到了子串 E [ i . . . j ] E[i...j] E[i...j] 的 hash值 S [ i . . . j ] S[i...j] S[i...j],加上原来的取模操作可以得到:

S [ l . . . r ] = ( S [ r ] − S [ l − 1 ] ∗ p r − l + 1 ) % k S[l...r]=(S[r]-S[l-1]*p^{r-l+1})\%k S[l...r]=(S[r]S[l1]prl+1)%k

由于C++有 unsigned long long(ULL) 类型表示范围为 [ 0 , 2 64 − 1 ] [0,2^{64}-1] [0,2641],因此当需要对 2 64 2^{64} 264 取模时,可以用 ULL 类型存储该数,当出现溢出时就相当于进行了取模运算


代码(C++)

#include <iostream>

using namespace std;

typedef unsigned long long ULL;

const int N = 100010, P = 131;
char E[N];
// POW 数组进行提前打表操作,i 中记录了 p^i
ULL H[N], POW[N];

//返回值为 ULL,相当于进行了取模运算
ULL subhash(int l, int r)
{
    // P 的 r-l+1 次幂,存储在对应下标中
    return H[r] - H[l - 1] * POW[r - l + 1];
}

int main()
{
    int n, m;
    // 对于字符串 E,从下标为 1 的位置读入
    scanf("%d%d%s", &n, &m, E + 1);
    
    POW[0] = 1;
    
    for (int i = 1; i <= n; i ++)
    {
        // H[i] 表示字符串前 i 个字符的子串 hash值
        H[i] = H[i - 1] * P + E[i];
        // 打表幂运算
        POW[i] = POW[i - 1] * P;
    }
    
    while (m --)
    {
        int l1, r1, l2, r2;
        scanf("%d%d%d%d", &l1, &r1, &l2, &r2);
        
        if (subhash(l1, r1) == subhash(l2, r2)) puts("Yes");
        else puts("No");
    }
    
    return 0;
}

代码(Python3):

def sub(l, r):
    return (H[r] - H[l - 1] * POW[r - l + 1]) % (2 ** 64)


if __name__ == '__main__':
    n, m = map(int, input().split())
    N, P = 100010, 131
    E = input()
    
    H = [0] * N
    POW = [0] * N
    POW[0] = 1
    
    for i in range(len(E)):
        H[i + 1] = (H[i] * P + ord(E[i])) % (2 ** 64)
        POW[i + 1] = (POW[i] * P) % (2 ** 64)
        
    for _ in range(m):
        l1, r1, l2, r2 = map(int, input().split())
        if sub(l1, r1) == sub(l2, r2):
            print('Yes')
        else:
            print('No')

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
字符串哈希算法是一种将字符串映射为数字的算法,常用于字符串的比较和匹配。在C++中,可以使用字符串哈希算法来加速字符串的比较操作。 引用\[1\]中的代码示例展示了一个使用字符串哈希算法的C++代码。该代码使用了前缀和数组和字符串数组来存储字符串,并通过计算哈希值来比较两个子串是否相等。其中,哈希值的计算使用了前缀和数组和幂运算。 引用\[2\]中的解释指出,使用字符串哈希的目的是为了比较字符串时不直接比较字符串本身,而是比较它们对应映射的数字。这样可以将子串的哈希值的时间复杂度降低到O(1),从而节省时间。 引用\[3\]中的代码示例也展示了一个使用字符串哈希算法的C++代码。该代码使用了前缀和数组和字符串数组来存储字符串,并通过计算哈希值来比较两个子串是否相等。与引用\[1\]中的代码类似,哈希值的计算也使用了前缀和数组和幂运算。 综上所述,字符串哈希算法是一种将字符串映射为数字的算法,常用于字符串的比较和匹配。在C++中,可以使用前缀和数组和幂运算来计算字符串哈希值,并通过比较哈希值来判断两个子串是否相等。 #### 引用[.reference_title] - *1* [C++算法题 # 33 字符串哈希](https://blog.csdn.net/weixin_44536804/article/details/123425533)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insertT0,239^v3^insert_chatgpt"}} ] [.reference_item] - *2* [字符串哈希(c++)](https://blog.csdn.net/qq_41829492/article/details/120980055)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insertT0,239^v3^insert_chatgpt"}} ] [.reference_item] - *3* [AcWing 841. 字符串哈希(C++算法)](https://blog.csdn.net/YSA__/article/details/108453403)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insertT0,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值