【字符串】 優雅的暴力——字符串下的哈希判重問題


  在刷題/比賽時經常會遇到判重的問題,那么這次就來講一講字符串上的判重問題。

 

▎哈希是什么

  判重我們通常會想到什么?小編首先想到的是桶排序,這種排序正是用了哈希的方法,其實把哈希理解為一堆桶更合適。

  比如說現在給你一堆數字,讓你判斷一共有幾種數字(也就是重復出現的不算): 1 5 4 1 1 3 5 6 。以哈希的思想來解決就是這樣的:

  

  放若干個桶,每個桶代表一種數,遇到相應的數字就放進去,判斷幾種數字就轉換成了判斷有幾個有東西的桶就可以了。

  那么,接下來思考一個問題:怎么存這些桶?要存這些桶只要用絕對不可能重復出現的量來代表桶的序號,例如……數組下標!我們可以利用數組下標來當做桶,每個桶里面東西的個數就是對應數組元素的值。比如說用一個叫做a的數組來存這些桶,當遇到數字3時,只要將a[ 3 ] ++;就可以了。

  其實這就是哈希,所以說理解成一堆桶更形象。

 

▎字符串下的哈希

  看到這里,你一定會想,字符串哈希有什么好講的?不也是一個道理嗎?當然不行!仔細想想,數組下標怎么存儲成字符串呢?數組下標都是整數的啊!

  此時出路就很敞亮了,我們可以把字符串轉換成整數處理!

  還記得嗎?在最開始學習時還學過ASCII碼,我們可以通過強制轉換替換成整數。

  可是問題又來了,如何用ASCII表示字符串?例如AB,其中A的ASCII碼是65,B的ASCII碼是66。

  1)用加的:AB表示為65+66=131。反例:BA表示為66+65=131,可AB和BA不一樣;

  2)用減的、乘的、除的,似乎都同上,表示出的值都不唯一;

  3)放在一起:AB表示為6566,這樣的確舉不出什么反例了,但是數字的值變大了,同時也區分不回去了,6566究竟是6和566呢?還是6,56和6呢?似乎不知道原來的字符串長什么樣了。

  自然而然,我們便想到了轉進制,這樣不易發生問題。那么取什么樣的進制會不發生或少發生問題呢?我們往往會取27,233,19260817等等,具體會視情況而定。(稍后會有例題講解)。

  有時會超出unsigned long long的范圍,那該怎么辦呢?那么我們就要用取模的方法了,通常會模一個很大的質數,模多少可以看看題后的數據規模是多大。

  有些時候會發生一些情況,比如3%2=1,5%2=1(打個比方,一般模數不會這么小),所以兩個數取模后當成了一個數來處理,這便叫做哈希沖突,在做題時要減少這種沖突的產生。

 

▎例題——【模板】字符串哈希

題目描述

如題,給定N個字符串(第i個字符串長度為Mi,字符串內包含數字、大小寫字母,大小寫敏感),請求出N個字符串中共有多少個不同的字符串。

輸入輸出格式

輸入格式:

 

第一行包含一個整數N,為字符串的個數。

接下來N行每行包含一個字符串,為所提供的字符串。

 

輸出格式:

 

輸出包含一行,包含一個整數,為不同的字符串個數。

 

輸入輸出樣例

輸入樣例#1: 
5
abc
aaaa
abc
abcc
12345
輸出樣例#1: 
4

說明

時空限制:1000ms,128M

數據規模:

對於30%的數據:N<=10,Mi≈6,Mmax<=15;

對於70%的數據:N<=1000,Mi≈100,Mmax<=150

對於100%的數據:N<=10000,Mi≈1000,Mmax<=1500

樣例說明:

樣例中第一個字符串(abc)和第三個字符串(abc)是一樣的,所以所提供字符串的集合為{aaaa,abc,abcc,12345},故共計4個不同的字符串。

   這道題完全是模板題,直接套思路就好了。

 

▎Code speaks louder than words!

  話不多說,直接上代碼(詳見注釋)

  

 1 #include<iostream>
 2 #include<algorithm>
 3 using namespace std;
 4 string s;int n;int hash[10000],mod=19270817,k=30,ans=1;
 5 int Hash(string str)
 6 {
 7         int len=str.length();
 8         int value=0;
 9         for(int i=0;i<len;i++)
10         value=value*k+((int)str[i]-96);//轉進制
11         return value;//這里其實也可以模一下,不過數據規模沒有那么大
12 }
13 int main()
14 {
15         cin>>n;
16         for(int i=1;i<=n;i++)
17         {
18                 cin>>s;
19                 hash[i]=Hash(s);//存儲每個字符串轉換后的哈希值
20         }
21         sort(hash+1,hash+n+1);//排序,目的是為了排除相同哈希值的字符串
22         for(int i=2;i<=n;i++)
23         if(hash[i]!=hash[i-1]) ans++;//如果哈希值不同,那么兩個字符串就不一樣
24         cout<<ans;
25         return 0;
26 }

 

▎map是啥?

  說來對於這種題來說還有一大利器——map。簡單介紹一下:

  1)頭文件:#include<map>

  2)定義:map< 類型 , 類型 > 變量名;

       第一個類型是數組下標的類型,第二個變量是數組值的類型

  3)用處:map定義出來的東西可以理解為數組下標為任意的數組,這恰恰起到了剛才那道題最開始思路的效果

  4)舉個栗子:比如說要定義一個數組下標是字符串的整型數組s,可以這么寫map< string , int > s;

  怎么解剛才那道題?直接普通哈希就可以了,就不寫注釋了。

 1 #include<iostream>
 2 #include<map>
 3 using namespace std;
 4 map<string,int>s;string str[100000];int n,ans;
 5 int main()
 6 {
 7     cin>>n;
 8     for(int i=1;i<=n;i++)
 9     {
10         cin>>str[i];
11         s[str[i]]=1;
12     }
13     for(int i=1;i<=n;i++)
14     {
15         if(s[str[i]]==1)
16         {
17             ans++;
18             s[str[i]]=0;
19         }
20     }
21     cout<<ans;
22     return 0;
23 }

 

▎為什么放着map不用而用前一種方法

  map看起來好用,就像數組一樣,其實map只是單單的映射,簡單來說就是暴力查詢,時間復雜度可想而知,這速度很慢,有時是可以AC題目的,但有時是滿足不了題目的要求的時間的,所以還是老老實實用字符串下的哈希吧。

 


免責聲明!

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



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