万能的进制哈希

万能的进制哈希


题外话:

  为什么要学字符串算法?

  为了快速比较两个字符串是否相等,众所周知垃圾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;
        }
    }
原文地址:https://www.cnblogs.com/knife-rose/p/11230936.html