KS 2018 RoundA:第三題

在這裏插入圖片描述

在這裏插入圖片描述

在這裏插入圖片描述

在這裏插入圖片描述

這一題學到了新的技巧,以及需要注意的很多細節(類似這種競賽題目和平時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方法,但是這裏的真正難點也不能忽視。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章