字符串Hash總結


轉載自:遠航休息棧

Hash是什么意思呢?某度翻譯告訴我們:

  • hash 英[hæʃ] 美[hæʃ]
  • n. 剁碎的食物; #號; 蔬菜肉丁;
  • vt. 把…弄亂; 切碎; 反復推敲; 搞糟;

我覺得Hash是引申出 把...弄亂 的意思。

今天就來談談Hash的一種——字符串hash。

據我的理解,Hash就是一個像函數一樣的東西,你放進去一個值,它給你輸出來一個值。輸出的值就是Hash值。一般Hash值會比原來的值更好儲存(更小)或比較。

那字符串Hash就非常好理解了。就是把字符串轉換成一個整數的函數。而且要盡量做到使字符串對應唯一的Hash值。

字符串Hash的種類還是有很多種的,不過在信息學競賽中只會用到一種名為“BKDR Hash”的字符串Hash算法。

它的主要思路是選取恰當的進制,可以把字符串中的字符看成一個大數字中的每一位數字,不過比較字符串和比較大數字的復雜度並沒有什么區別(高精數的比較也是O(n)

那么我們選擇什么進制比較好?

首先不要把任意字符對應到數字0,比如假如把a對應到數字0,那么將不能只從Hash結果上區分ab和b(雖然可以額外判斷字符串長度,但不把任意字符對應到數字0更加省事且沒有任何副作用),一般而言,把a-z對應到數字1-26比較合適。

關於進制的選擇實際上非常自由,大於所有字符對應的數字的最大值,不要含有模數的質因子(那還模什么),比如一個字符集是a到z的題目,選擇27、233、19260817都是可以的。

模數的選擇(盡量還是要選擇質數):

絕大多數情況下,不要選擇一個109

最穩妥的辦法是選擇兩個109如果能背過或在考場上找出一個1018偷懶的寫法就是直接使用unsigned long long,不手動進行取模,它溢出時會自動對264

用luogu P3370為例。

這是自然溢出hash(100)

#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
typedef unsigned long long ull;
ull base=131;
ull a[10010];
char s[10010];
int n,ans=1;
ull hashs(char s[])
{
    int len=strlen(s);
    ull ans=0;
    for (int i=0;i<len;i++)
        ans=ans*base+(ull)s[i];
    return ans&0x7fffffff;
}
main()
{
    scanf("%d",&n);
    for (int i=1;i<=n;i++)
    {
        scanf("%s",s);
        a[i]=hashs(s);
    }
    sort(a+1,a+n+1);
    for (int i=2;i<=n;i++)
        if (a[i]!=a[i-1])
            ans++;
    printf("%d\n",ans);
}

這是單模數hash(80)

#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
typedef unsigned long long ull;
ull base=131;
ull a[10010];
char s[10010];
int n,ans=1;
ull mod=19260817;
ull hashs(char s[])
{
    int len=strlen(s);
    ull ans=0;
    for (int i=0;i<len;i++)
        ans=(ans*base+(ull)s[i])%mod;
    return ans;
}
main()
{
    scanf("%d",&n);
    for (int i=1;i<=n;i++)
    {
        scanf("%s",s);
        a[i]=hashs(s);
    }
    sort(a+1,a+n+1);
    for (int i=2;i<=n;i++)
        if (a[i]!=a[i-1])
            ans++;
    printf("%d\n",ans);
}

這是雙hash(100)

#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
typedef unsigned long long ull;
ull base=131;
struct data
{
    ull x,y;
}a[10010];
char s[10010];
int n,ans=1;
ull mod1=19260817;
ull mod2=19660813;
ull hash1(char s[])
{
    int len=strlen(s);
    ull ans=0;
    for (int i=0;i<len;i++)
        ans=(ans*base+(ull)s[i])%mod1;
    return ans;
}
ull hash2(char s[])
{
    int len=strlen(s);
    ull ans=0;
    for (int i=0;i<len;i++)
        ans=(ans*base+(ull)s[i])%mod2;
    return ans;
}
bool comp(data a,data b)
{
    return a.x<b.x;
}
main()
{
    scanf("%d",&n);
    for (int i=1;i<=n;i++)
    {
        scanf("%s",s);
        a[i].x=hash1(s);
        a[i].y=hash2(s);
    }
    sort(a+1,a+n+1,comp);
    for (int i=2;i<=n;i++)
        if (a[i].x!=a[i-1].x || a[i-1].y!=a[i].y)
            ans++;
    printf("%d\n",ans);
}

這是只用一個10^18質數的hash(100)

#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
typedef unsigned long long ull;
ull base=131;
ull a[10010];
char s[10010];
int n,ans=1;
ull mod=212370440130137957ll;
ull hashs(char s[])
{
    int len=strlen(s);
    ull ans=0;
    for (int i=0;i<len;i++)
        ans=(ans*base+(ull)s[i])%mod;
    return ans;
}
main()
{
    scanf("%d",&n);
    for (int i=1;i<=n;i++)
    {
        scanf("%s",s);
        a[i]=hashs(s);
    }
    sort(a+1,a+n+1);
    for (int i=2;i<=n;i++)
        if (a[i]!=a[i-1])
            ans++;
    printf("%d\n",ans);
}

Hash還有一方面,就是它可以處理子串信息。對於一個字符串,我們可以預處理它1−l

對於一個字符串l−r

這樣的話hash就可以水過字符串匹配的題目

cogs1570

【題目描述】

法國作家喬治·佩雷克(Georges Perec,1936-1982)曾經寫過一本書,《敏感字母》(La disparition),全篇沒有一個字母‘e’。他是烏力波小組(Oulipo Group)的一員。下面是他書中的一段話:

Tout avait Pair normal, mais tout s’affirmait faux. Tout avait Fair normal, d’abord, puis surgissait l’inhumain, l’affolant. Il aurait voulu savoir où s’articulait l’association qui l’unissait au roman : stir son tapis, assaillant à tout instant son imagination, l’intuition d’un tabou, la vision d’un mal obscur, d’un quoi vacant, d’un non-dit : la vision, l’avision d’un oubli commandant tout, où s’abolissait la raison : tout avait l’air normal mais…

佩雷克很可能在下面的比賽中得到高分(當然,也有可能是低分)。在這個比賽中,人們被要求針對一個主題寫出甚至是意味深長的文章,並且讓一個給定的“單詞”出現次數盡量少。我們的任務是給評委會編寫一個程序來數單詞出現了幾次,用以得出參賽者最終的排名。參賽者經常會寫一長串廢話,例如500000個連續的‘T’。並且他們不用空格。

因此我們想要盡快找到一個單詞出現的頻數,即一個給定的字符串在文章中出現了幾次。更加正式地,給出字母表{'A','B','C',...,'Z'}和兩個僅有字母表中字母組成的有限字符串:單詞W和文章T,找到W在T中出現的次數。這里“出現”意味着W中所有的連續字符都必須對應T中的連續字符。T中出現的兩個W可能會部分重疊。

【輸入格式】

輸入包含多組數據。

輸入文件的第一行有一個整數,代表數據組數。接下來是這些數據,以如下格式給出:

第一行是單詞W,一個由{'A','B','C',...,'Z'}中字母組成的字符串,保證1<=|W|<=10000(|W|代表字符串W的長度)

第二行是文章T,一個由{'A','B','C',...,'Z'}中字母組成的字符串,保證|W|<=|T|<=1000000。

【輸出格式】

對每組數據輸出一行一個整數,即W在T中出現的次數。

【樣例輸入】

3
BAPC
BAPC
AZA
AZAZAZA
VERDI
AVERDXIVYERDIAN

【樣例輸出】

1
3
0

代碼

#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
typedef unsigned long long ull;
ull base=131;
ull po[100010],hs[100010*100];
char s1[100010],s2[100010*100];
int n,ans=1,T;
ull geth(int l,int r)
{
    return (ull)hs[r]-po[r-l+1]*hs[l-1];
}
main()
{
    freopen("oulipo.in","r",stdin);
    freopen("oulipo.out","w",stdout);
    po[0]=1;
    for (int i=1;i<=10010-5;i++)
        po[i]=po[i-1]*base;
    scanf("%d",&T);
    while(T--)
    {
        scanf("%s%s",s1+1,s2+1);
        int l1=strlen(s1+1),l2=strlen(s2+1);
        ull a1=0,ans=0;
        for (int i=1;i<=l1;i++)
            a1=a1*base+(ull)s1[i];
        for (int i=1;i<=l2;i++)
            hs[i]=hs[i-1]*base+s2[i];
        for (int i=1;i+l1-1<=l2;i++)
            if (a1==geth(i,i+l1-1))
                ans++;
        printf("%d\n",ans);
    }
}

hash好像可以暴力水過很多字符串算法。。

1、kmp

問題:給兩個字符串S1,S2,求S2是否是S1的子串,並求S2在S1中出現的次數

把S2 Hash出來,在S1里找所有長度為|S2|

2、AC自動機

問題:給N個單詞串,和一個文章串,求每個單詞串是否是文章串的子串,並求每個單詞在文章中出現的次數。

把每一個單詞hash成整數,再把文章的每一個子串hash成整數,接下來只需要進行整數上的查找即可。

復雜度:O(|A|2+|S|)

用AC自動機可以做到O(|A|+|S|)

3、后綴數組

問題:給兩個字符串S1,S2,求它們的最長公共子串的長度。

將S1的每一個子串都hash成一個整數,將S2的每一個子串都hash成一個整數

兩堆整數,相同的配對,並且找到所表示的字符串長度最大的即可。

復雜度:O(|S1|2+|S2|2)

用后綴數組可以優化到O(|S|log|S|)

4、馬拉車

問題:給一個字符串S,求S的最長回文子串。

先求子串長度位奇數的,再求偶數的。枚舉回文子串的中心位置,然后二分子串的長度,直到找到一個該位置的最長回文子串,不斷維護長度最大值即可。

復雜度:O(|S|log|S|)

用manacher可以做到O(|S|)

5、擴展kmp

問題:給一個字符串S,求S的每個后綴與S的最長公共前綴

枚舉每一個后綴的起始位置,二分長度,求出每個后綴與S的最長公共前綴。

復雜度:O(|S|log|S|)

用extend-kmp可以做到O(|S|)

后記

hash真是一種優雅的暴力。

因為字符串特殊的性質,我們可以二分得處理它,一般都有單調性。

歡迎大家吐槽或評論,如要轉載請注明地址,謝謝~(雖然不一定有人能看到。。。)


免責聲明!

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



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