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方法,但是这里的真正难点也不能忽视。

posted @ 2019-11-04 19:29  于老师的父亲王老爷子  阅读(22)  评论(0编辑  收藏  举报