這一題學到了新的技巧,以及需要注意的很多細節(類似這種競賽題目和平時leetcode做題目的區別)
首先是小數據集的做法:
很自然的,對dict裏面的每個詞在字符串裏面找是不是有符合條件的子串,找的時候肯定使用滑動窗口,計算每個字符出現的頻率。
然後是大數據集的做法:
不能夠使用一個詞一個詞地找這種做法,因爲詞的數量太多。但是考慮到:如果所有詞的長度和爲M,那麼不同長度的數量最多爲sqrt(M)(1,2,3。。。這樣的)。所以可以每次看一個長度的。
但是怎麼看一個長度的呢?難道是設置窗口大小爲這麼大,然後每滑動一次,就把所有的這個長度的詞都遍歷一遍嗎?不是,還有更好的方法。
這裏使用的是hash的方法,這一個hash就可以解決:1.首尾字母是否相同2.出現的字母頻率是否都相同。這就是它神奇的地方,不然使用我那個方法的話,太浪費了。
先看代碼:
#include <iostream>
#include <vector>
#include <string>
#include <sstream>
#include <unordered_map>
#include <unordered_set>
using namespace std;
int seed = 13331;
unsigned long long getHash(char a, char b, vector<int>& freq) {
unsigned long long res = seed*a + b;
for (int i = 0; i < 26; ++i)
res = res * seed + freq[i];
return res;
}
string getStr(char s1, char s2, int n, int a, int b, int c, int d);
int main() {
int T;
cin >> T;
int iCase = 0;
while (iCase < T) {
++iCase;
int L;
string word;
unordered_map<unsigned long long, int> m;
unordered_set<int> lens;
cin >> L;
vector<int> freq(26, 0);
while (L-- > 0) {
cin >> word;
lens.insert(word.size());
for (int i = 0; i < 26; ++i)
freq[i] = 0;
//for (char c : word)
//freq[c-'a']++;
for (int i = 1; i < word.size()-1; ++i)
freq[word[i]-'a']++;
m[getHash(word[0], word.back(), freq)]++;
}
//for (auto& item : m)
//cout << item.first << " " << item.second << endl;
char s1, s2;
int n, a, b, c, d;
cin >> s1 >> s2 >> n >> a >> b >> c >> d;
string str = getStr(s1, s2, n, a, b, c, d);
//cout << str << endl;
int res = 0;
for (int len : lens) {
if (len > str.size())
continue;
for (int i = 0; i < 26; ++i)
freq[i] = 0;
int i = 1;
while (i < len-1)
freq[str[i++]-'a']++;
unsigned long long hash = getHash(str[0], str[len-1], freq);//這裏寫成int來
if (m.find(hash) != m.end()) {
res += m[hash];
m.erase(hash);
}
i = len;
while (i < str.size()) {
freq[str[i-1]-'a']++;
freq[str[i-len+1]-'a']--;
hash = getHash(str[i-len+1], str[i], freq);
if (m.find(hash) != m.end()) {
res += m[hash];
m.erase(hash);
}
++i;
}
}
cout << "Case #" << iCase << ": " << res << endl;
}
}
stringstream ss;
string getStr(char s1, char s2, int n, int a, int b, int c, int d) {
ss.str("");
ss << s1 << s2;
int x1 = s1, x2 = s2;
int x;
for (int i = 3; i <= n; ++i) {
x = ((long long)a*x2 + (long long)b*x1 + c) % d;//溢出的話會提示RE
ss << (char)(97+x%26);
x1 = x2;
x2 = x;
}
return ss.str();
}
hash爲什麼能做到這一點先放到後面,這裏先討論另一個問題。
我想先說明爲什麼需要討論不同的單詞長度。既然字符串的hash就能夠做到對兩個string判斷是否符合題目的要求,那爲什麼還需要用到單詞的長度呢?
答案是給滑動窗口提出限制(和這篇博文一樣)。沒有長度的要求的話滑動窗口不知道怎麼滑動,而有了限制才知道什麼時候收縮窗口。而且爲了覆蓋所有的情況,需要對所有不同的長度都實驗一遍。(可以不要滑動窗口啊?但是不用滑動窗口的話,就需要把所有子串的hash值算出來,代價更大)。所以自己給滑動窗口提出限制是一個比較普遍的方法,而且爲了需要覆蓋所有情況,需要明白共有多少情況。
hash的作用:
我的想法是:以13331進制計算出一個數。爲什麼能夠進行首位字符的比較?因爲將首位字母計算進去了。爲什麼可以比較每個字符出現的頻率?因爲計算了每個字符出現的頻率也計算進了最終的值當中。
爲什麼用13331:可能因爲是一個比較大的素數?這裏存疑。
思維擴散開去,類似這種用hash的做法還可以用到什麼地方?
不用首位字母相同的判斷permutation等等,應該還有好多。
(這裏的一個關於效率的小問題:每次滑動,就需要計算一次hash,比較浪費,但這是沒有辦法的事,比起我之前想的每次滑動遍歷所有相同長度的string要好多了)
自己在coding的時候犯的錯誤:
這是一個讓我抓狂的問題,因爲leetcode的問題幾乎不用爲數據類型發愁,溢出也會明確告訴你的,但是這裏只是告訴你錯誤,而不知道是算法錯誤還是哪裏的小錯誤。
我犯的錯誤有:
1.忽視了溢出:
x = ((long long)a*x2 + (long long)b*x1 + c) % d;
這一行我開始是沒有寫long long 類型強轉的。檢查的時候偶然纔想起來可能溢出,這裏就是導致RE的地方。Leetcode上幾乎不要考慮這個問題,但是這裏是需要的。
以後做KS的時候見到這種數據加減乘除的地方都要考慮溢出的問題
2.定義錯了類型
unsigned long long hash = getHash(str[0], str[len-1], freq);
這裏我一開始大意了,定義的類型爲int,導致了WA。這導致我花了很多時間檢查是不是其他地方出了問題。
另外在這一題當中,對我而言設計使用hash是第一次見到,是需要理解的。但是其實這一題大數據集對於搞過acm的人來說難的地方可能在於之前說的不同長度爲sqrt(M)(我猜),這是難想到的點。用hash的做法可能對於acm的人來說是一個普遍方法(?),所以我還是需要掌握這裏的hash方法,但是這裏的真正難點也不能忽視。