哈希基础例题


哈希前置知识请戳这里-> 哈希绪论

昨天我们对哈希的基础知识有了一定的了解,并已经知道了如何求子串、拼接子串的哈希值,今天我们就这两个操作分析一些基础例题,加深理解和掌握。

例题一:子串查找

LOJ #103. 子串查找
显然这是一道kmp算法的模板题

朴素的做法是枚举文本串的每一个位置作为模式串开始比较的位置。设枚举到主串的位置是 i i i N N N为主串(即输入的第一行)的长度, M M M为模式串(即输入的第二行)的长度, s a sa sa为主串, s b sb sb为模式串,则当且仅当 s a sa sa i i i到第 i + M − 1 i+M-1 i+M1与模式串一一匹配,才能使 a n s + 1 ans+1 ans+1。这样在最坏情况下(比如 s a 、 s b sa、sb sasb所有字符均为 a a a),复杂度达到了 O ( N M ) O(NM) O(NM),是不可接受的。现在我们考虑用哈希处理,那么可以将第二步比较的操作复杂度降低到 O ( 1 ) O(1) O(1),因此只要先预处理出 s a sa sa的前缀哈希表,则当我们枚举每一个位置 i i i时,利用前缀表获得区间 [ i , i + M − 1 ] [i,i+M-1] [i,i+M1]子串的哈希值,再与 s b sb sb的哈希值相比较即可,总体复杂度就降低到了 O ( N + M ) O(N+M) O(N+M),已经和kmp算法一样优秀了就是常数有点大
下面来看代码:

#include<stdio.h>
#include<string.h>
#define ll long long
#define N 1000000007
#define M 14371003
#define max 1000005
char s[max], ss[max];
int a[max], p[max];
int main()
{
    scanf("%s%s", s + 1, ss + 1);
    int len = strlen(ss + 1), ans = 0, cnt = 0, l = strlen(s + 1);
    if (len > l) { printf("0"); return 0; }
    a[1] = s[1], p[0] = 1, p[1] = M;
    for (int i = 1; i <= len; i++)
        ans = ((ll)ans * M + ss[i]) % N;//获得sb的哈希值
    for (int i = 2; i <= l; i++)
    {
        a[i] = ((ll)a[i - 1] * M + s[i]) % N;
        p[i] = (ll)p[i - 1] * M % N;
    }
    for (int i = 1; i <= l + 1 - len; i++)
        if (((a[i + len - 1] - (ll)a[i - 1] * p[len]) % N + N) % N == ans)cnt++;
        //查询子串哈希值公式
    printf("%d", cnt);
    return 0;
}

接下来我们看两道道有难度的题目

例题二:字符串的删除操作

LOJ #2823. 「BalticOI 2014 Day 1」三个朋友
这道题我们很容易想到利用逆向思维解决:枚举每一个删除的位置,然后检查删除后的字符串是否由两个完全相同的字符串构成,而判断字符串是否相同就可以利用哈希的思想了。

首先,输入的字符串长度一定要是奇数(删掉一个字符是长度是偶数),否则直接输出无解。稍微修改一下昨天拼接字符串的公式就得到了在区间 [ l , r ] [l,r] [l,r]删除位置 x x x对应的字符后子串的哈希值公式: a n s = g e t ( l , x − 1 ) ∗ b a s e r − x + g e t ( x + 1 , r ) ans=get(l,x-1)*base^{r-x}+get(x+1,r) ans=get(l,x1)baserx+get(x+1,r)相当于是把 [ l , x − 1 ] 、 [ x + 1 , r ] [l,x-1]、[x+1,r] [l,x1][x+1,r]两段字符串拼接起来。然后就是需要注意若删除的位置是在原字符串前一半位置,则比较 h a s h ( [ 1 , x − 1 ] + [ x + 1 , l e n 2 ] ) hash([1,x-1]+[x+1,\frac{len}{2}]) hash([1,x1]+[x+1,2len]) h a s h ( [ l e n 2 + 1 , l e n ] ) hash([\frac{len}{2}+1,len]) hash([2len+1,len])是否相等,其中 l e n len len为原字符串的长度, x x x在后半段同理。(其实就是分类讨论 x x x的位置)
最后只要根据符合条件的 x x x的数量来输出多解一解还是无解(需要考虑 x x x不同但字符串相同的情况)
总体复杂度 O ( N ) O(N) O(N)

下面是代码:

#include<stdio.h>
#include<string.h>
#include<algorithm>
#define N 1000000007
#define M 14371003
#define ll long long
int ans[2000020], p[2000020];
int gethash(int l, int r)
{
	return ((ans[r] - (ll)ans[l - 1] * p[r - l + 1]) % N + N) % N;
}
int del(int l, int r, int x)//删除操作
{
	return ((ll)gethash(l, x - 1) * p[r - x] + gethash(x + 1, r)) % N;
}
int main()
{
	int n, f = 0, res, t1, t2, t3, t4; char s[2000020];//f标记是否出现过符合条件的x
	scanf("%d%s", &n, s);
	ans[0] = s[0], p[0] = 1;
	for (int i = 1; i ^ n; i++)
	{
		ans[i] = ((ll)ans[i - 1] * M + s[i]) % N;
		p[i] = (ll)p[i - 1] * M % N;
	}
	if (n & 1)//只要长度为奇数才可能有解
	{
		t1 = (n >> 1), t2 = n - 1, t3 = t1 - 1, t4 = t1 + 1;
		for (int i = 0; i ^ t4; i++)//枚举前一半
			if (!(del(0, t1, i) ^ gethash(t4, t2)))//哈希值相等
			{
				if (f && (del(0, t2, i) ^ del(0, t2, res)))
				{//符合条件的x已经出现且与之前得到的字符串不同则输出多解
					printf("NOT UNIQUE");
					return 0;
				}
				f = 1, res = i;
			}
		for (int i = t4; i ^ n; i++)//枚举后一半
		{
			if (!(gethash(0, t3) ^ del(t1, t2, i)))
			{
				if (f && (del(0, t2, i) ^ del(0, t2, res)))
				{
					printf("NOT UNIQUE");
					return 0;
				}
				f = 1, res = i;
			}
		}
	}
	if (!f)//未出现符合条件的位置x或者原字符串长度为偶数则输出无解
	{
		printf("NOT POSSIBLE");
		return 0;
	}
	if (res <= t1)printf("%s", s + 1 + t1);//x出现在前一半
	else//x出现在后一半
	{
		s[t1] = '\0';
		printf("%s", s);
	}
	return 0;
}

例题三:字符串合并操作的应用

牛客 白兔的字符串
对于循环同构,我们可以先预处理出字符串 T T T所有循环同构字符串的哈希值,而每一个循环同构字符串都由区间 [ i + 1 , l e n T ] 、 [ 1 , i ] [i+1,len_T]、[1,i] [i+1,lenT][1,i]拼接而成,可以利用子串拼接的公式进行求解。接着对于每一个给出的 S S S枚举所有起点 i i i,检查子串 [ i , i + l e n T − 1 ] [i,i+len_T-1] [i,i+lenT1]的值是否在哈希表中出现过。当然是先对哈希表进行排序,之后二分查找哈希值即可。总复杂度 O ( n ∗ l e n T ∗ l o g l e n T ) O(n*len_T*loglen_T) O(nlenTloglenT)数据比较毒瘤,换了好几个质数
代码:

#include <stdio.h>
#include <string.h>
#include <algorithm>
#define ll long long
#define N 2147483587
#define M 25165843
char s[10000005];
int hash[10000005], base[1000005], rev[1000005];
int get(int l, int r)
{
    return ((hash[r] - (ll)hash[l - 1] * base[r - l + 1]) % N + N) % N;
}
int merge(int l1, int r1, int l2, int r2)//子串合并操作
{
    return ((ll)get(l1, r1) * base[r2 - l2 + 1] + get(l2, r2)) % N;
}
int main()
{
    scanf("%s", s + 1);
    int len = strlen(s + 1), n; base[0] = 1;
    for (int i = 1; i <= len; ++i)
    {
        hash[i] = ((ll)hash[i - 1] * M + s[i]) % N;
        base[i] = ((ll)M * base[i - 1]) % N;
    }
    for (int i = 1; i < len; ++i)
        rev[i] = merge(i + 1, len, 1, i);//合并区间[i+1,len_T]、[1,i]得到同构子串
    rev[len] = hash[len];
    std::sort(rev + 1, rev + len + 1);//排序,方便二分查找
    scanf("%d", &n);
    while (n--)
    {
        int ans = 0;
        scanf("%s", s + 1);
        int lens = strlen(s + 1);
        if (lens < len) { puts("0"); continue; }
        for (int i = 1; i <= lens; ++i)
            hash[i] = ((ll)hash[i - 1] * M + s[i]) % N;
        lens -= len - 1;
        for (int i = 1; i <= lens; ++i)//枚举每一个起始位置
            if (std::binary_search(rev + 1, rev + len + 1, get(i, i + len - 1)))++ans;
        //在哈希表中能找到对应的哈希值,那么答案加一
        printf("%d\n", ans);
    }
    return 0;
}

今天的例题到这里就结束了

明天更新二维哈希、自然溢出以及二分哈希表
不见不散

  • 4
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值