在刷題/比賽時經常會遇到判重的問題,那麼這次就來講一講字串上的判重問題。
▎雜湊是什麼
判重我們通常會想到什麼?小編首先想到的是桶排序,這種排序正是用了雜湊的方法,其實把雜湊理解為一堆桶更合適。
比如說現在給你一堆數字,讓你判斷一共有幾種數字(也就是重複出現的不算): 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行每行包含一個字串,為所提供的字串。
輸出格式:
輸出包含一行,包含一個整數,為不同的字串個數。
輸入輸出樣例
說明
時空限制: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題目的,但有時是滿足不了題目的要求的時間的,所以還是老老實實用字串下的雜湊吧。