萬能的進制哈希


萬能的進制哈希


題外話:

  為什么要學字符串算法?

  為了快速比較兩個字符串是否相等,眾所周知垃圾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; } }

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM