• 万能的进制哈希


    万能的进制哈希


    题外话:

      为什么要学字符串算法?

      为了快速比较两个字符串是否相等,众所周知垃圾C++在比较两个字符串的时候效率并不高,所以我们需要设计一种算法更高效地比较字符串

      大致用途:

          1.判断两个字符串是否相等;

          2.判断一个字符串是否曾经出现过;

          3.让某些用户口吐芬芳的时候网页可以自动屏蔽掉;



    定义:

      百度百科:Hash,一般翻译做散列、杂凑,或音译为哈希,是把任意长度的输入(又叫做预映射pre-image)通过散列算法变换成固定长度的输出,该输出就是散列值。

      人话翻译:把字符赋予进制和模数,将每一个字符串映射为一个小于模数数字。

      具体操作:

          我们设置进制(base)为131,模数(mod)为1e9+7,现在我们对一个字符串s进行哈希   

      char s[10];
      cin>>(s+1);
      int len=strlen(s+1);
      int base=131,mod=1e9+7;
      for(int i=1;i<=len;++i)
      {
        hash[i] = ( ( hash[i-1] * base ) + s[i] ) % mod ;
      }

       这样hash[len]里面就是字符串s的哈希值了;

       hash还有一个方便的操作就是取子串的hash值

        

        hash[l,r] = ( hash [r] - hash[l-1] * pw[r-l+1] ) %mod
        //伪代码 pw[r-l+1]为base的(r-l+1)次方 

     


    注意:

      哈希冲突:

        什么是哈希冲突:比如orz的哈希值是2333,然而sto的哈希值也是2333,这样就会产生哈希冲突,从而让哈希算法判断失误。

        解决方法:

     1.模数选取大质数

        如果选取合数那么他的剩余系将会有所浪费(不了解剩余系请找一篇数论博客QwQ),如果质数过小将会导致剩余系过小,哈希冲突几率增大(质数过大爆负数,谨慎设置)

     2.双模数哈希

        我们可以通过设置两个不同的哈希方式,对于一个字符串,当且仅当两个哈希值都相同时才判定相当。

        这种方法相当有效,除非出题人对着你的数据卡你,否则正确率近乎100%(详情请见BZOJ Hash Killer 3


    实战应用:

      1.hash判断最长公共前缀

        POJ2758

        题目概述:给定一个字符串,要求维护两种操作在字符串中插入一个字符询问某两个位置开始的 LCP(最长公共前缀)插入操作次数

        插入<=200,字符串总长度<=50000,查询次数<=20000。

        分析:插入<=200,考虑每次插入暴力维护 复杂度200*50000

        每次查询二分LCP的长度,然后hash O(1)判断是否相等

      2.哈希判断回文串

        SP7586

        求一个字符串中包含几个回文串?

        manachar?其实哈希也很好用,而且复杂度只多一个log呢QwQ

        对于该串维护正反两个哈希值,我们称为正向哈希和反向哈希

        每次二分一个回文串长度,用正反哈希O(1)判断是否相等

        对于奇偶回文串可以考虑每个字符中间插入一个新字符,也可以分开处理

      3.线段树维护哈希

        洛谷P2757

        给出一个1到n的排列,问是否存在长度大于等于3的等差子序列

        分析:其实只要找长度等于3的就好了嘛QwQ

        一个01串,从前往后扫描这个序列,将扫描过的数字对应位置变为1

        对于每一个数字,如果目前不能构成等差序列,那么他两侧的01串必然是一个回文串,我们可以对01串维护一个哈希值进行比较

        由于我们需要动态修改和区间查询哈希值,所以我们考虑权值线段树来维护正反哈希。

        线段树维护哈希细节较多,这里我细致地说一下,同时为了代码清晰可读,这里利用unsigned long long 自然溢出,略去取模操作

        

    首先一些变量函数

    ans1[500010]//正向哈希线段树节点
    ans2[500010]//反向哈希线段树节点
    query1函数:正向哈希查询
    query2函数:反向哈希查询 

    线段树push_up向上维护操作

    要将正哈希的左儿子的哈希值乘上进制的右区间长度次方,反哈希的右儿子乘上进制的左区间长度次方

        ans1[p] = ans1[ls(p)] * pw[r-mid] + ans1[rs(p)] ;
        ans2[p] = ans2[rs(p)] * pw[mid-l+1] + ans2[ls(p)] ;//注意ls(p)和rs(p)的区别

    本题目不需要push_down下放操作,后面会提到

    查询时分类讨论:

      完全在左儿子区间:直接返回左儿子值;

      完全在右儿子区间:直接返回右儿子值;

      二者都在:

           正向:左儿子值*右查询长度+右儿子值;

           反向:右儿子值*左查询长度+左儿子值;

    inline int query1(int tl,int tr,int l,int r,int p)
    {
        if(tl<=l&&r<=tr) return ans1[p];
        if(tr<=mid)    return query1(tl,tr,l,mid,ls(p));
        else if(mid<tl) return query1(tl,tr,mid+1,r,rs(p));
        else
        {
            int lx=query1(tl,tr,l,mid,ls(p));
            int rx=query1(tl,tr,mid+1,r,rs(p));
            return lx*pw[min(tr,r)-mid]+rx; 
        }
    }
    inline int query2(int tl,int tr,int l,int r,int p)
    {
        if(tl<=l&&r<=tr) return ans2[p];
        if(tr<=mid)    return query2(tl,tr,l,mid,ls(p));
        else if(mid<tl) return query2(tl,tr,mid+1,r,rs(p));
        else
        {
            int lx=query2(tl,tr,l,mid,ls(p));
            int rx=query2(tl,tr,mid+1,r,rs(p));
            return rx*pw[mid-max(tl,l)+1]+lx;
        }
    }

    多测不清空,爆零两行泪

    下面贴完整代码

    #include<bits/stdc++.h>
    using namespace std;
    #define int unsigned long long 
    #define ls(p) (p<<1)
    #define rs(p) (p<<1|1)
    #define mid ((l+r)>>1)
    inline int read()
    {
        int x=0,f=1;
        char ch;
        for(ch=getchar();(ch<'0'||ch>'9')&&ch!='-';ch=getchar());
        if(ch=='-') f=0,ch=getchar();
        while(ch>='0'&&ch<='9'){x=(x<<1)+(x<<3)+ch-'0';ch=getchar();}
        return f?x:-x;
    }
    int T,base=131;
    int a[100010],n;
    int ans1[500010],ans2[500010];
    int pw[100010];
    bool flag;
    inline void update(int tl,int tr,int l,int r,int p)
    {
        if(tl<=l&&r<=tr)
        {
            ans1[p]=ans2[p]=1;
            return;
        }
        if(tl<=mid)
            update(tl,tr,l,mid,ls(p));
        else
            update(tl,tr,mid+1,r,rs(p));
        ans1[p] = ans1[ls(p)] * pw[r-mid] + ans1[rs(p)] ;
        ans2[p] = ans2[rs(p)] * pw[mid-l+1] + ans2[ls(p)] ;
    }
    inline int query1(int tl,int tr,int l,int r,int p)
    {
        if(tl<=l&&r<=tr) return ans1[p];
        if(tr<=mid)    return query1(tl,tr,l,mid,ls(p));
        else if(mid<tl) return query1(tl,tr,mid+1,r,rs(p));
        else
        {
            int lx=query1(tl,tr,l,mid,ls(p));
            int rx=query1(tl,tr,mid+1,r,rs(p));
            return lx*pw[min(tr,r)-mid]+rx; 
        }
    }
    inline int query2(int tl,int tr,int l,int r,int p)
    {
        if(tl<=l&&r<=tr) return ans2[p];
        if(tr<=mid)    return query2(tl,tr,l,mid,ls(p));
        else if(mid<tl) return query2(tl,tr,mid+1,r,rs(p));
        else
        {
            int lx=query2(tl,tr,l,mid,ls(p));
            int rx=query2(tl,tr,mid+1,r,rs(p));
            return rx*pw[mid-max(tl,l)+1]+lx;
        }
    }
    signed main()
    {
        T=read();
        for(int i=pw[0]=1;i<=100000;++i)
            pw[i] = pw[i-1] * base;
        while(T--)
        {
            n=read();
            flag=0;
            memset(ans1,0,sizeof(ans1));
            memset(ans2,0,sizeof(ans2));
            for(int i=1;i<=n;++i)
            {
                a[i]=read();
                if(!flag)
                {
                    int d=min(a[i]-1,n-a[i]);
                    if(d)
                    {
                        if(query1(a[i]-d,a[i],1,n,1)^query2(a[i],a[i]+d,1,n,1))
                            flag=1;
                    }
                    update(a[i],a[i],1,n,1);
                }
            }
            puts(flag?"Y":"N");
        }
    return 0;
    }

      4.哈希判断循环节

        CF508E

        给定一个数字串,要求维护以下两个操作:

        1.将l到r区间内数字全部改为k

        2.询问l到r区间内是否存在长度为k的循环节

        前置神仙结论:判断一个字符串[ l , r ] 是否有长度为k的循环节,只需判断 [ l+d , r ] 和 [ l , r-d ] 是否相等。

        有了上述结论,这道题就变成了一个哈希值的区间修改和区间查询问题,再码一颗线段树就可以了

        在这里放一下上面没有展示的push_down下放代码

        

    inline void push_down(int l,int r,int p)
    {
        int k=tag[p];
        ans[ls(p)] = val[k][mid-l+1]; // val[k][len] 预处理出来 
        ans[rs(p)] = val[k][r-mid];  //表示字符串内全部都是k的长度为len的 哈希值 
        tag[ls(p)]=tag[rs(p)]=tag[p];
        tag[p]=-1; // 修改可能存在 0 所以tag 要赋成 -1  
    }
       for(int i=0;i<10;++i)
        {
            for(int j=1;j<=100005;++j)
            {
                val[i][j]=val[i][j-1] * base + i;
            }
        }
  • 相关阅读:
    泛型编程 --迭代器
    cpp输入输出加速
    算法训练 加法运算(指针的一个测试)
    蓝桥杯-基础练习-字母图形
    蓝桥杯-基础练习-特殊回文数
    win10下稍微美观下Git
    mysql8.0以上版本注册驱动并建立数据库的连接公共代码
    idea使用的一些问题解决记录
    单链表逆转(递归指针实现)
    increment/decrement/dereference操作符
  • 原文地址:https://www.cnblogs.com/knife-rose/p/11230936.html
Copyright © 2020-2023  润新知