每天一点点算法开始了,为什么我就写不出很爽的代码。今天被邀请去水了一场类似ACM的程序设计竞赛,他说是期末考试难度,结果那个难度……题目只做出了一半,剩下的实在是…….出题人的语文水平太差了,没交代清楚题要干什么,很多细节描述的模棱两可。如果你考我程序设计,我没问题;通过模糊的信息表达来考察语文理解能力就没意思了,为啥不多打几个字把问题交代清楚呢?为节省空间,本文压缩了某些代码,实际工程切勿模仿。
概念 哈希是一种典型的利用空间换时间的算法,通过申请大量的空间,来达到在$O(1)$的时间复杂度内快速查询元素的目的。如最简单的,假设输入序列是0,1,2,3,4,5
,申请一个数组作为哈希表。那么此时的哈希计算公式就可以是:H[i] = i % size
。size
是哈希表的长度,或者说是数组的长度。那么0存储在0号位置,5存储在5号位置。元素插入哈希表和查询哈希表内元素都可以在内完成。
假设表长为5,如果要插入的序列是0,2,4,7
,就不能用上面的映射公式了,因为7%5 == 2%5
,也就是2号位置会存储2或7,导致了冲突(collision)。这个时候就需要用探测的方法来确定7该存在哪里,常用的方法有:线性探测法 、平方探测法 。
道理就是这么个道理,有很多方法可以实现哈希,如数组、map等。
哈希插入 现在手动模拟一下用平方探测法(只考虑正数项)来模拟下哈希插入的过程。经验而言,哈希表的长度最好是素数。第一行中,首先输入表长,若表长不为素数,则增加表长转换为最近的素数,而后输入元素的数量$N$。在下一行输入$N$个数据,插入到哈希表中,并输出插入的位置;若无法插入,则输出-
。
样例输入:
样例输出:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 #include <iostream> #include <array> using namespace std;array<int , 10005> arr; int m, n;bool isprime (int num) { if (num == 1 ) return false ; for (int i = 2 ; i * i <= num; i++) if (num % i == 0 ) return false ; return true ; } void insert (int a) { for (int i = 0 ; i < m; i++){ int idx = (a + i * i) % m; if (arr[idx] == 0 ){ arr[idx] = 1 ; cout << idx; return ; } } cout << "-" ; } int main () { int a; cin >> m >> n; while (!isprime (m)) m += 1 ; for (int i = 0 ; i < n; i++){ cin >> a; if (i != 0 ) cout << " " ; insert (a); } return 0 ; }
哈希查找 第二种常见应用是先把一个序列的元素插入到哈希表中;而后查询另一个序列的元素有没有在哈希表中,计算总共的查找次数。和上面不同的是,插入元素时不能单纯的将哈希表的数值设为1了,而应该是具体的数值,来看一个实例。在第一行输入三个数字,第一个数字是哈希表长度,和上述问题一样需要处理为素数;第二个为输入序列的长度$N$;第三为查询序列长度是$M$。第二行给出$N$个元素,用二次探测法插入到哈希表中,如果不能插入,打印插入失败信息;第三行给出$M$个元素,在哈希表中查询这些元素,记录查询次数。
输入样例:1 2 3 4 5 4 10 6 4 15 11 11 4 15 2
输出样例:1 2 15 cannot be inserted.2.5
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 #include <iostream> #include <array> using namespace std;array<int , 10005> arr; int size_, n, m;bool isPrime (int m) { if (m == 1 ) return false ; for (int i = 2 ; i * i <= m; i++) if (m % i == 0 ) return false ; return true ; } void insert (int val) { for (int i = 0 ; i < size_; i++){ int idx = (val + i * i) % size_; if (arr[idx] == 0 ){ arr[idx] = val; return ; } } cout << val << " cannot be inserted." << endl; } int main () { cin >> size_ >> n >> m; while (!isPrime (size_)) size_ ++; cout << size_ << endl; int t; double cnt{0 }; for (int i = 0 ; i < n; i++){ cin >> t; insert (t); } for (int i = 0 ; i < m; i++){ cin >> t; for (int j = 0 ; j < size_; j++){ cnt++; int idx = (t + j * j) % size_; if (arr[idx] == t || arr[idx] == 0 ) break ; } } printf ("%0.1f" , cnt / m); return 0 ; }
哈希应用 哈希在算法领域还是很常见的一种技巧,有很高的应用价值。接下来看几道例题,比单纯的哈希插入、查找要有意思的多。
映射唯一 一种最简单的实例,通过数组实现哈希。先给出一个整数$N$,随后给出$N$行的人员信息,格式为:队伍编号-队员编号 成绩
,以队伍编号为单位,计算得分最多的单位。思路就是:以每个队伍的编号为数组的下标,下标对应的数组元素为队伍的分数。这里能这么做的原因是,在哈希的过程中,队伍编号是唯一的。
输入样例:1 2 3 4 5 6 7 6 3 -10 99 11 -5 87 102 -1 0 102 -3 100 11 -9 89 3 -2 61
输出样例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 #include <iostream> #include <array> using namespace std;array<int , 1005> arr = {0 }; int main () { int n, g, p, s, maxn{-1 }, id{-1 }; cin >> n; for (int i = 0 ; i < n; i++){ scanf ("%d-%d %d" , &g, &p, &s); arr[g] += s; } for (int i = 0 ; i < 1005 ; i++){ if (arr[i] > maxn){ maxn = arr[i]; id = i; } } cout << id << " " << maxn; return 0 ; }
map 实现哈希 map数据结构是什么请移步此处 。假设开学要查抄学生携带的违规物品,输入第一行给出两个正整数$N$和$M$,分别是学生人数和需要被查缴的物品种类数。第二行给出$M$个需要被查缴的物品编号,其中编号为 4 位数字。随后$N$行,每行给出一位学生的姓名缩写(由 1-4 个大写英文字母组成)、个人物品数量$K$、以及$K$个物品的编号,顺次检查每个学生携带的物品,如果有需要被查缴的物品存在,则按以下格式输出该生的信息和其需要被查缴的物品的信息(注意行末不得有多余空格)。
输入样例:1 2 3 4 5 6 4 2 2333 6666 CYLL 3 1234 2345 3456 U 4 9966 6666 8888 6666 GG 2 2333 7777 JJ 3 0012 6666 2333
输出样例:1 2 3 4 U: 6666 6666 GG: 2333 JJ: 6666 2333 3 5
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 #include <iostream> #include <map> using namespace std;int main () { int n, m, k, cnt1{0 }, cnt2{0 }; cin >> n >> m; string s, name; map <string, int > check; for (int i = 0 ; i < m; i++){ cin >> s; check[s] = 1 ; } for (int i = 0 ; i < n; i++){ cin >> name >> k; int flag{0 }; for (int j = 0 ; j < k; j++){ cin >> s; if (check[s] == 1 ){ cnt2++; if (flag == 0 ){ cout << name << ":" ; flag = 1 ; cnt1 ++; } cout << " " << s; } } if (flag == 1 ) cout << endl; } cout << cnt1 << " " << cnt2; return 0 ; }
字符串过滤 给定两个字符串,删除第一个字符串中所包含的第二个字符串的字符,实现两个字符串相减。如果每次输出字符都判断一下这个字符是否在第二个字符串中,复杂度约为$O(n^2)$,必然会超时。如果使用哈希,开辟一块内存空间,就能大规模减少时间复杂度到$O(n)$。
样例输入:1 2 They are students. aeiou
样例输出:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 #include <iostream> #include <vector> using namespace std;int main () { string str1, str2; getline (cin, str1); getline (cin, str2); bool flag[256 ] = {false }; for (int i = 0 ; i < str1.length (); i++) flag[str1[i]] = true ; for (int i = 0 ; i < str2.length (); i++) flag[str2[i]] = false ; for (int i = 0 ; i < str1.length (); i++) { if (flag[str1[i]] == true ) printf ("%c" , str1[i]); } return 0 ; }
减少计算量 $3n+1$猜想的步骤是:如果一个数是偶数,那将它除以2;如果一个数是奇数,那么将它乘以3后加1,经过有限步骤一定可以取值为1。为了节省计算,可以记录递推过程中遇到的每一个数。如计算$n=3$时,需要计算3、5、8、4、2、1
,当下一次遇到5、8、4、2
的时候,就不用计算了,可以直接判断得到结果。称5、8、4、2
是3的覆盖数。我们称一个序列中的某几个数是关键数,如果这几个关键数不被其他数字所覆盖。现在给出一个序列,序列中元素的最大值为100,最小值为2,计算其中的关键数,并从大到小输出。
这就是一个典型的哈希问题,创建一个数组,以序列元素为数组的下标进行查询;数组的取值表示是否为关键数,这里设取值为1表示关键数。如果序列中的值对应数组元素的取值是1,则输出。因为序列中最大值为100,所以能设计的最大取值范围是99*3+1=298。凑个整,直接申请300大小的数组。那么来编写代码。
输入样例:
输出样例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 #include <iostream> #include <array> #include <vector> #include <algorithm> using namespace std;array<int , 300> arr = {0 }; vector<int > v; bool cmp (int a, int b) { return a > b ? true : false ; } int main () { int k, tmp, flag{0 }; cin >> k; v.resize (k); for (int i = 0 ; i < k; i++){ cin >> tmp; v[i] = tmp; while (tmp != 1 ) { if (tmp % 2 != 0 ) tmp = tmp * 3 + 1 ; tmp = tmp / 2 ; if (arr[tmp] == 1 ) break ; arr[tmp] = 1 ; } } sort (v.begin (), v.end (), cmp); for (int i = 0 ; i < v.size (); i++){ if (arr[v[i]] == 0 ){ if (flag == 0 ){ cout << v[i]; flag = 1 ; } else cout << " " << v[i]; } } return 0 ; }
结语 如果后续遇到了其他类型的不错题目,会继续追加到这里。