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