基础数据结构 -哈希

Hash 表

Hash 表就是将集合的域很大的映射到小的域上。所以使用它就很产生两个问题:一、如何映射;二、如何处理不同元素的相同映射。

  • 对于问题一,我们可以使用一个 H a s h Hash Hash 函数进行进行映射,对于整数域而言,通常这个 H a s h Hash Hash 函数就是取模。

  • 对于问题二,我们可以利用邻接表的数据结构来表示 H a s h Hash Hash 表,为每一个映射开一个链表,这样相同映射的元素就会在同一个链表中,解决冲突。

典型的用法就是记录一个序列中每个整数出现的次数,整数范围不大,那么直接用数字当下标就行了,但是如果数字太大了,就可以使用 H a s h Hash Hash 表将数字映射到它的 H a s h Hash Hash 值,在这里储存它的次数。

【例题】雪花雪花雪花

N N N 片雪花,每片雪花由六个角组成,每个角都有长度。

i i i 片雪花六个角的长度从某个角开始顺时针依次记为 a i , 1 , a i , 2 , … , a i , 6 a_{i,1},a_{i,2},…,a_{i,6} ai,1,ai,2,,ai,6

因为雪花的形状是封闭的环形,所以从任何一个角开始顺时针或逆时针往后记录长度,得到的六元组都代表形状相同的雪花。

例如 a i , 1 , a i , 2 , … , a i , 6 a_{i,1},a_{i,2},…,a_{i,6} ai,1,ai,2,,ai,6 a i , 2 , a i , 3 , … , a i , 6 , a i , 1 a_{i,2},a_{i,3},…,a_{i,6},a_{i,1} ai,2,ai,3,,ai,6ai,1 就是形状相同的雪花。

a i , 1 , a i , 2 , … , a i , 6 a_{i,1},a_{i,2},…,a_{i,6} ai,1,ai,2,,ai,6 a i , 6 , a i , 5 , … , a i , 1 a_{i,6},a_{i,5},…,a_{i,1} ai,6,ai,5,,ai,1 也是形状相同的雪花。

我们称两片雪花形状相同,当且仅当它们各自从某一角开始顺时针或逆时针记录长度,能得到两个相同的六元组。

求这 N N N 片雪花中是否存在两片形状相同的雪花。

数据范围
1 ≤ N ≤ 100000 , 0 ≤ a i , j < 10000000 1≤N≤100000, 0≤a_{i,j}<10000000 1N100000,0ai,j<10000000

分析:

为了能提高从集合中找出同类型的雪花的效率,因此使用 H a s h Hash Hash 表存储相同映射的雪花。所以设计 H a s h Hash Hash 函数为 H ( a i , 1 , a i , 2 , … , a i , 6 ) = ( ∑ j = 1 6 a i , j + ∏ j = 1 6 a i , j )   m o d   P H(a_{i,1},a_{i,2},…,a_{i,6}) = (\sum_{j=1}^6a_{i,j}+\prod_{j=1}^6a_{i,j})~mod~P H(ai,1,ai,2,,ai,6)=(j=16ai,j+j=16ai,j) mod P P P P 为一个较大的质数。

于是我们对于每一个插入 H a s h Hash Hash 表的雪花,都会查看是否该 H a s h Hash Hash 值已存在。并在这个 H a s h Hash Hash 值下的链表中找出是否有相同类型的雪花,因为雪花具有旋转的特性,所以不提前处理储存的雪花进行比较的话,其比较的事件复杂度为 O ( n 2 ) O(n^2) O(n2) ,如果利用字符串的最小表示法则可化为 O ( n ) O(n) O(n) 时间复杂度。

代码如下:

#include <bits/stdc++.h>
using namespace std;
typedef unsigned long long ULL;
const int N = 300005;

int n;
char s[N];

ULL h[N], p[N];

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

int sa[N];

int get_m_s(int k1, int k2) {
    int l = 0, r = min(n - k1 + 1, n - k2 + 1);
    while(l < r) {
        int mid = (l + r + 1) >> 1;
        if(get(k1, k1 + mid - 1) == get(k2, k2 + mid - 1))
            l = mid;
        else r = mid - 1;
    }
    return l;
}

bool cmp(const int &x1, const int &x2) {
    int d = get_m_s(x1, x2);
    int x1_v = d > (n - x1 + 1) ? -1 : s[x1 + d];
    int x2_v = d > (n - x2 + 1) ? -1 : s[x2 + d];
    
    return  x1_v < x2_v;
}


int main()
{
    scanf("%s", s + 1);
    
    n = strlen(s + 1);
    
    p[0] = 1;
    
    for(int i = 1; i <= n; ++i) {
        h[i] = h[i - 1] * 131 + s[i] - 'a' + 1;
        p[i] = p[i - 1] * 131;
    }
        
    for(int i = 1; i <= n; ++i) 
        sa[i] = i;
    
    
    sort(sa + 1, sa + 1 + n, cmp);
    
    for(int i = 1; i <= n; ++i) {
        cout << sa[i] - 1 << ' ';
    }
    cout << endl;
    
    for(int i = 1; i <= n; ++i) {
        cout << get_m_s(sa[i], sa[i - 1]) << ' ';
    }
    return 0;
}

字符串哈希

在字符串匹配问题中,如果只是暴力匹配,其时间复杂度就是 O ( N M ) O(NM) O(NM) ,使用 K M P KMP KMP 可以变为 N + M N+M N+M ,但是字符串哈希可以变为 O ( N ) O(N) O(N)

将一个字符串看成是一个 P P P 进制数,对于每个字符对应一个大于 0 0 0 的数值。一般来说,这个数值肯定是 P P P 进制内的数。

比如对于小写字母中 a = 1 a=1 a=1 b = 2 b = 2 b=2 c = 3 c = 3 c=3 z = 26 z=26 z=26 。为了避免数值太大,所以需要把这些数映射到一个有限的区间中,比如 [ 0 , 2 64 ] [0,2^{64}] [0,264]

选取进制时也是有讲究的,通常取 P = 131 P=131 P=131 p = 13331 p=13331 p=13331 ,此时冲突概论极低。

对于我们已知的字符串 S S S H a s h Hash Hash 值为 H ( S ) H(S) H(S) ,那么在 S S S 后添加一个字符 c c c 构成的新字符串 S + c S + c S+c H a s h Hash Hash值就是 H ( S + c ) = ( H ( S ) × P + v a l u e ( c ) )   m o d   M H(S +c) = (H(S)\times P+value(c))~mod~M H(S+c)=(H(S)×P+value(c)) mod M

如果已知了 H a s h Hash Hash H ( S ) H(S) H(S) H ( S + T ) H(S+T) H(S+T) 后,那么对于 H a s h Hash Hash H ( T ) = ( H ( S + T ) − H ( S ) × P l e n g t h ( T ) )   m o d   M H(T) = (H(S+T) - H(S)\times P^{length(T)})~mod~M H(T)=(H(S+T)H(S)×Plength(T)) mod M

因此我们可以预处理出字符串 S S S 的所有前缀的 H a s h Hash Hash 值,之后就可以 O ( 1 ) O(1) O(1) 的时间复杂度来获取任意子串的 H a s h Hash Hash 值了。

【例题】兔子与兔子

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

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

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

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

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

数据范围
1 ≤ l e n g t h ( S ) , m ≤ 1000000 1≤length(S),m≤1000000 1length(S),m1000000

分析:

对于两者的 D N A DNA DNA 序列,预处理出它们的 H a s h Hash Hash 值 ,然后对于每次询问就是 O ( 1 ) O(1) O(1) 时间复杂度。

代码如下:

#include <bits/stdc++.h>
using namespace std;
typedef unsigned long long ULL;
const int N = 1000005;
const int P  = 131;
ULL h[N], p[N];

void init(string &str) {
    int n = str.size();
    p[0] = 1;
    for(int i = 1; i <= n; ++i) {
        h[i] = h[i - 1] * P + str[i - 1] - '0' + 1;
        p[i] = p[i - 1] * P;
    }
}

ULL get(int l, int r)  // 计算子串 str[l ~ r] 的哈希值
{
    return h[r] - h[l - 1] * p[r - l + 1];
}

int main()
{
    string str;
    cin >> str;
    
    init(str);
    
    int t;
    cin >> t;
    while(t--) {
        int l1, r1, l2, r2;
        scanf("%d%d%d%d", &l1, &r1, &l2, &r2);
        
        if(get(l1, r1) == get(l2, r2)) {
            puts("Yes");
        } else puts("No");
    } 
    
    return  0;
}

【例题】回文子串的最大长度

如果一个字符串正着读和倒着读是一样的,则称它是回文的。

给定一个长度为 N N N 的字符串 S S S,求他的最长回文子串的长度是多少。

分析:

如果这个字符串具有长度为 n n n 的回文子串,那么对于小于等于 n n n 的子串都存在一个回文串 。
但是对于奇回文串、偶回文串的长度变化不一致,所以要两者分别处理。

故长度具有单调性,所以问题转换为二分答案问题了,现在就是如何快速的解决这个判定问题了,使用字符串哈希显然可以使得原本 n 2 n^2 n2 的时间复杂度将为 O ( n ) O(n) O(n) ,乘上 ( l o g ( n ) ) (log(n)) (log(n)) 的判定,于是问题就可以解决了。

代码如下:

#include <bits/stdc++.h>
using namespace std;
typedef unsigned long long ULL;
const int N = 1000005;
const int P = 131;

ULL lh[N], p[N], rh[N];
char s[N];

void init(int n) {
    for(int i = 1; i <= n; ++i) {
        lh[i] = lh[i - 1] * P + s[i] - 'a' + 1;
        p[i] = p[i - 1] * P;
    }
    
    rh[n + 1] = 0;
    for(int i = n; i >= 1; --i) {
        rh[i] = rh[i + 1] * P + s[i] - 'a' + 1; 
    }
}

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

ULL getR(int l, int r)  // 计算子串 str[l ~ r] 的哈希值
{
    return rh[l] - rh[r + 1] * p[r - l + 1];
}


bool checkO(int x, int n) {
    for(int i = x + 1; i + x <= n; ++i) {
        if(getL(i - x, i) == getR(i, i + x)) {
            return true;
        }
    }
    return false;
}

bool checkE(int x, int n) {
    for(int i = x + 1; i + x - 1 <= n; ++i) {
        if(getL(i - x, i - 1) == getR(i, i + x - 1)) {
            return true;
        }
    }
    return false;
}


int main()
{
    p[0] = 1;
    int tt = 1;
    while(scanf("%s", s + 1) && 
        !(s[1] == 'E' && s[2] == 'N' && s[3] == 'D')) {
        
        int n = strlen(s + 1);
        
        init(n);
        
        int p, q;
        
        int l = 0, r = n / 2 + 1;
        while(l < r) {
            int mid = (l + r + 1) >> 1;
            if(checkO(mid, n)) l = mid;
            else r = mid - 1;
        }
        
        p = l;
        
        l = 0, r = n / 2 + 1;
        while(l < r) {
            int mid = (l + r + 1) >> 1;
            if(checkE(mid, n)) l = mid;
            else r = mid - 1;
        }
        
        q = l;
        
        printf("Case %d: %d\n", tt++, max(2 * p + 1, 2 * q));
    }
    return 0;
}

【例题】后缀数组

后缀数组 ( S A ) (SA) (SA) 是一种重要的数据结构,通常使用倍增或者 D C 3 DC3 DC3 算法实现,这超出了我们的讨论范围。

在本题中,我们希望使用快排、 H a s h Hash Hash 与二分实现一个简单的 O ( n l o g 2 n ) O(nlog^2n) O(nlog2n) 的后缀数组求法。

详细地说,给定一个长度为 n n n 的字符串 S S S(下标 0 0 0 n − 1 n−1 n1),我们可以用整数 k ( 0 ≤ k < n ) k(0≤k<n) k(0k<n) 表示字符串 S S S 的后缀 S ( k ∼ n − 1 ) S(k∼n−1) S(kn1)

把字符串 S S S 的所有后缀按照字典序排列,排名为 i i i 的后缀记为 S A [ i ] SA[i] SA[i]

额外地,我们考虑排名为 i i i 的后缀与排名为 i − 1 i−1 i1 的后缀,把二者的最长公共前缀的长度记为 H e i g h t [ i ] Height[i] Height[i]

我们的任务就是求出 S A SA SA H e i g h t Height Height 这两个数组。

分析:

题目其实已经给出了算法了,只不过要做到 O ( n l o g 2 n ) O(nlog^2n) O(nlog2n) ,就得在快速排序的比较环节优化了。
因为这个后缀数组是在同一个字符串上,所以预处理出该字符串的 H a s h Hash Hash 值后,对于该字符串上的任意两个子串,我们都可以理由 O ( l o g n ) O(logn) O(logn) 时间复杂度来找出它们的最长公共前缀,这样比较就可以 O ( l o g n ) O(logn) O(logn) 比较出两字符串的字典序了。

代码如下:

#include <bits/stdc++.h>
using namespace std;
typedef unsigned long long ULL;
const int N = 300005;

int n;
char s[N];

ULL h[N], p[N];

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

int sa[N];

int get_m_s(int k1, int k2) {
    int l = 0, r = min(n - k1 + 1, n - k2 + 1);
    while(l < r) {
        int mid = (l + r + 1) >> 1;
        if(get(k1, k1 + mid - 1) == get(k2, k2 + mid - 1))
            l = mid;
        else r = mid - 1;
    }
    return l;
}

bool cmp(const int &x1, const int &x2) {
    int d = get_m_s(x1, x2);
    int x1_v = d > (n - x1 + 1) ? -1 : s[x1 + d];
    int x2_v = d > (n - x2 + 1) ? -1 : s[x2 + d];
    
    return  x1_v < x2_v;
}


int main()
{
    scanf("%s", s + 1);
    
    n = strlen(s + 1);
    
    p[0] = 1;
    
    for(int i = 1; i <= n; ++i) {
        h[i] = h[i - 1] * 131 + s[i] - 'a' + 1;
        p[i] = p[i - 1] * 131;
    }
        
    for(int i = 1; i <= n; ++i) 
        sa[i] = i;
    
    
    sort(sa + 1, sa + 1 + n, cmp);
    
    for(int i = 1; i <= n; ++i) {
        cout << sa[i] - 1 << ' ';
    }
    cout << endl;
    
    for(int i = 1; i <= n; ++i) {
        cout << get_m_s(sa[i], sa[i - 1]) << ' ';
    }
    return 0;
}
  • 5
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值