• 算法竞赛进阶指南 基本数据结构 0x14哈希表


    Hash表

    Hash表又称散列表,一般由Hash函数(散列函数)与链表结构共同实现。与离散化思想类似。

    解决冲突:有一种称为“开散列”的解决方案是,建立一个邻接表结构,以Hash函数的值域作为表头数组head,映射后的值相同的原始信息被分为同一类,构成一个链表接在对应的表头之后,链表的节点上可以保存原始信息和一些统计数据。

    设计Hash函数为 H ( x ) = ( x m o d    P ) + 1 H(x)=(x\mod P)+1 H(x)=(xmodP)+1,其中P是一个比较大的质数,但不超过N。显然,这个Hash函数把数列A分成P类,我们可以依次考虑数列中的每个数A[i],定位到head[A[i]]这个表头所指向的链表。如果该链表中不包含A[i],我们就在表头后插入一个新节点A[i],并在该节点上记录A[i]出现了1次,否则我们就直接找到已经存在的A[i]节点将其出现次数加1。因为整数序列A是随机的,所以最终所有A[i]会比较均匀地分散在各个表头之后。

    对于非随机的数列,我们可以设计更好的Hash函数来保证其时间复杂度。

    同样地,如果我们需要维护的是比大整数复杂得多的信息的某些性质(如是否存在、出现次数等),也可以用Hash表来解决。字符串就是一种比较一般化的信息。

    0、AcWing 137. 雪花雪花雪花

    题意 :

    • 有 N 片雪花,每片雪花由六个角组成,每个角都有长度。
    • 第 i 片雪花六个角的长度从某个角开始顺时针依次记为 ai,1,ai,2,…,ai,6。
    • 因为雪花的形状是封闭的环形,所以从任何一个角开始顺时针或逆时针往后记录长度,得到的六元组都代表形状相同的雪花。
    • 我们称两片雪花形状相同,当且仅当它们各自从某一角开始顺时针或逆时针记录长度,能得到两个相同的六元组。
    • 求这 N 片雪花中是否存在两片形状相同的雪花。
    • 1≤N≤100000,

    思路 :

    • 定义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)modP,其中/P是我们自己选取的一个较大的质数。显然,对于两片形状相同的雪花,它们六个角的长度之和、长度之积都相等,因此它们的Hash函数值也相等。
    • 建立一个Hash表,把N片雪花依次插入。对于每片雪花 a i , 1 , a i , 2 , . . . , a i , 6 a_{i,1},a_{i,2},...,a_{i,6} ai,1,ai,2,...,ai,6,我们直接扫描表头 H ( a i , 1 , a i , 2 , . . . , a i , 6 ) H(a_{i,1},a_{i,2},...,a_{i,6}) H(ai,1,ai,2,...,ai,6)对应的链表,检查是否存在与a_{i,1},a_{i,2},…,a_{i,6}形状相同的雪花即可。
    • 对于随机数据,期望的时间复杂度为 O ( N 2 P ) O(\frac{N^2}{P}) O(PN2);取P为最接近N质数,期望的时间复杂度为 O ( N ) O(N) O(N)
    • 在下一节中,我们将学习循环同构串的“最小表示法”,进一步提高判断两片雪花形状是否相同的效率。
    • 注意6 * sizeof(int) 不要写成 sizeof a,因为,我们定义的a数组有10个长度
    #include 
    #include 
    using namespace std;
    const int N = 1e5 + 10, P = 99991;
    
    int n;
    int head[N], ne[N], tot, snow[N][6];
    
    int H(int *a) {
        int sum = 0, mul = 1;
        for (int i = 0; i < 6; ++ i) {
            sum = (sum + a[i]) % P;
            mul = (long long)mul * a[i] % P;
        }
        return (sum + mul) % P;
    }
    bool equal(int *a, int *b) {
        for (int i = 0; i < 6; ++ i) {
            for (int j = 0; j < 6; ++ j) {
                bool eq = 1;
                for (int k = 0; k < 6; ++ k) {
                    if (a[(i + k) % 6] != b[(j + k) % 6]) eq = 0;
                }
                if (eq) return 1;
                eq = 1;
                for (int k = 0; k < 6; ++ k) {
                    if (a[(i + k) % 6] != b[(j - k + 6) % 6]) eq = 0;
                }
                if (eq) return 1;
            }
        }
        return 0;
    }
    bool insert(int *a) {
        int val = H(a);
        for (int i = head[val]; i; i = ne[i]) {
            if (equal(snow[i], a)) return 1;
        }
        ++ tot;
        memcpy(snow[tot], a, 6 * sizeof(int));
        ne[tot] = head[val];
        head[val] = tot;
        return 0;
    }
    
    int main() {
        scanf("%d", &n);
        for (int i = 0; i < n; ++ i) {
            int a[10];
            for (int j = 0; j < 6; ++ j) scanf("%d", &a[j]);
            if (insert(a)) {
                puts("Twin snowflakes found.");
                return 0;
            }
        }
        puts("No two snowflakes are alike.");
    }
    

    字符串Hash

    下面介绍的字符串Hash函数把一个任意长度的字符串映射成一个非负整数,并且其冲突概率几乎为

    取一固定值P,把字符串看作P进制数,并分配一个大于0的数值,代表每种字符。一般来说,我们分配的数值都远小于P。例如,对于小写字母构成的字符串,可以令a = 1,b = 2,…,z = 26。取一固定值M,求出该P进制数对M的余数,作为该字符串的Hash值(->(P进制数->取模))。

    一般来说,我们取 P = 131P = 13331,此时Hash值产生冲突的概率极低,只要Hash值相同,我们就可以认为原字符串是相等的。通常我们取M = 2^{64},即直接使用unsigned long long类型存储这个Hash值,在计算时不处理算术溢出问题,产生溢出时相当于自动 2 64 2^{64} 264取模,这样可以避免低效的取模(mod)运算

    除了在极特殊构造的数据上,上述Hash算法很难产生冲突,一般情况下上述Hash算法完全可以出现在题目的标准解答中。我们还可以多取一些恰当的P和M的值(例如大质数),多进行几组Hash运算,当结果都相同时才认为原字符串相等,就更加难以构造出使这个Hash产生错误的数据。

    对字符串的各种操作,都可以直接对P进制数进行算术运算反映到Hash值上。

    如果我们已知字符串S的Hash值为H(S),那么在S后添加一个字符串构成的新字符串S+c的Hash值就是H(S+c)=(H(s) * P + value[c]) mod M。其中,乘P就相当于P进制下的左移运算,value[c]是我们为c选定的代表数值

    如果我们已知字符串S的Hash值为H(S),字符串S+T的Hash值为H(S+T),那么字符串T的Hash值H(T) = (H(S+T) - H(S) * P l e n g t h ( T ) P^{length(T)} Plength(T)) mod M。

    通过上面两种操作,我们可以通过O(N)的时间预处理字符串所有前缀Hash值,并在O(1)的时间内查询它的任意子串的Hash值

    0、AcWing 138. 兔子与兔子

    题意 :

    • 我们首先选取一个好长好长的 DNA 序列(小兔子是外星生物,DNA 序列可能包含 26 个小写英文字母)。
    • 然后我们每次选择两个区间,询问如果用两个区间里的 DNA 序列分别生产出来两只兔子,这两个兔子是否一模一样。
    • 注意两个兔子一模一样只可能是他们的 DNA 序列一模一样。
    • 1≤length(S),m≤1000000
    #include 
    #include  // strlen的头文件
    using namespace std;
    typedef unsigned long long ULL; // ULL
    const int N = 1e6 + 10, P = 131; // P可以是int
    
    char str[N]; // char数组的方式
    ULL h[N], power[N]; // h和power都是ULL
    
    ULL get(int l, int r) {
        return h[r] - h[l - 1] * power[r - l + 1]; // 公式
    }
    
    int main() {
        scanf("%s", str + 1); // 输入char数组,注意下标从1开始
        int n = strlen(str + 1); // 得到char数组长度
        power[0] = 1; // 注意乘法数组初始化!
        for (int i = 1; i <= n; ++ i) {
            h[i] = h[i - 1] * P + str[i] - 'a' + 1; // 最后+1,因为否则a就是0了
            power[i] = power[i - 1] * P;
        }
        int m;
        scanf("%d", &m);
        while (m -- ) {
            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");
        }
    }
    
    

    1、AcWing 139. 回文子串的最大长度

    题意 :

    • 给定一个长度为 N 的字符串 S,求他的最长回文子串的长度是多少。
    • 输入将包含最多 30 个测试用例,每个测试用例占一行,以最多 1000000 个小写字符的形式给出。
    • 输入以一个以字符串 END 开头的行表示输入终止。

    思路 :

    • 我们发现回文串分为两类:奇回文串、偶回文串
    • 于是在本题中,我们可以枚举回文子串的中心位置i=1~N,看从这个中心位置出发向左右两侧最长可以扩展出多长的回文串,也就是说:
      1、求出一个最大的整数p,使得S[i-p ~ i] = reverse(S[i ~ i+p]),那么以i为中心的最长奇回文子串的长度就是2p+1
      2、求出一个最大的整数q,使得S[i-q ~ i-1] = reverse(S[i ~ i+q-1]),那么以i-1和i之间的夹缝为中心的最长偶回文子串的长度就是2
      q
    • 根据上一题,我们已知在O(N)预处理前缀Hash值后,可以O(1)计算任意字串的Hash值;类似地,我们可以倒着做一遍预处理,就可以O(1)计算任意字串倒着读的Hash值
    • 于是我们可以对p和q进行二分答案,用Hash值O(1)比较一个正着读的字串和一个倒着读的字串是否相等,从而在O(logN)的时间内求出最大的p和q
    • 因此,总的时间复杂度就是O(NlogN)
    • 这里有一个小技巧,在每个字符前添加一个非字母的字符,假设为#,比如 abc 被扩展成 #a#b#c,这样的话,得到所有回文字串必然是奇数长度的。那么我们就可以直接遍历每个字符,尝试以所有字符为对称中心(这样就不需要考虑夹缝的情况了)的回文串最大长度可能是多少
    • 可以发现,当回文串最左边的字符是#时(#a#a#),实际回文串的长度就是填充后对称中心左边子串的长度;而当回文串最左边的字符是字母时(a#b#a),实际回文串的长度就是填充后对称中心左边子串的长度+1
    • 有一个名为Manacher的算法可以O(N)求解该问题,感兴趣的读者可以自行查阅相关资料
    #include 
    #include 
    using namespace std;
    typedef unsigned long long ull;
    const int N = 2e6 + 10, P = 131;
    
    char s[N];
    ull h1[N], h2[N], p[N];
    
    ull get(ull h[], int l, int r) {
        return h[r] - h[l - 1] * p[r - l + 1];
    }
    
    int main() {
        int cnt = 0;
        while (scanf("%s", s + 1) && strcmp(s + 1, "END")) {
            int n = strlen(s + 1) * 2;
            for (int i = n; i; i -= 2) {
                s[i] = s[i / 2];
                s[i - 1] = 'z' + 1;
            }
            p[0] = 1ull;
            for (int i = 1, j = n; i <= n; ++ i, -- j) {
                h1[i] = h1[i - 1] * P + s[i] - 'a' + 1;
                h2[i] = h2[i - 1] * P + s[j] - 'a' + 1;
                p[i] = p[i - 1] * P;
            }
            int ans = 0;
            for (int i = 1; i <= n; ++ i) {
                int l = 0, r = min(i - 1, n - i);
                while (l < r) {
                    int mid = (l + r + 1) >> 1;
                    if (get(h1, i - mid, i - 1) == get(h2, n + 1 - (i + mid), n + 1 - (i + 1))) {
                        l = mid;
                    } else {
                        r = mid - 1;
                    }
                }
                if (s[i - l] <= 'z') {
                    ans = max(ans, l + 1);
                } else {
                    ans = max(ans, l);
                }
            }
            printf("Case %d: %d\n", ++ cnt, ans);
        }
    }
    
    

    (Skip)2、后缀数组

  • 相关阅读:
    【基于langchain + streamlit 完整的与文档对话RAG】
    RocketMQ实战之在线停机和扩容
    揭秘华为云GaussDB(for Influx)最佳实践:hint查询
    Tomcat服务(部署、虚拟主机配置、优化)
    美格智能SLM927智能模组,轻松打造功能丰富的智能终端
    信息学奥赛一本通题解目录(没写完)
    gitee如何自动筛选文件上传
    使用 JPA、Hibernate 和 Spring Data JPA 进行审计
    阿里云安全中心需要购买吗?功能及价格告诉你值不值!
    Win11系统设置闪退的解决方案
  • 原文地址:https://blog.csdn.net/m0_51448653/article/details/127097659