字符串Hash

今天我们要讲解的是用于处理字符串匹配查重的一个算法,当我们处理一些问题如给出10000个字符串输出其中不同的个数,或者给一个长度100000的字符串,找出其中相同的字符串有多少个(这样描述有点不清楚但是大致的意思就是当字符串长度很长,而且涉及到多个字符串之间反复比较时,由于比较的次数多,字符串长,很容易就超时了,而字符串Hash则是一种将字符串转换成整数,再借助一些STL工具如map可以很快完成查重工作)

这里给出两个例题辅助讲解

例题一

比如有t组输入,每次输入n个字符串(1<=n<=10000),且字符串只有小写字母,每个字符串长度1~10000(当然这只是个例子,也可能更长,题目也会更多变),对于这n个字符串,输出不同的字符串的数量,(如aaa, bbb, aaa则输出2)

例题分析

这是字符串Hash的模板题,我们要做的就是将一个个字符串转换成整数,然后扔到map中判断一下重复即可,而转换的方法则是重点,在此就不得不提一下,我们所知晓的二进制(base-2),一个二进制数1010可以转换成十进制2^3 + 2^1 == 10,而我们对于一个字符串“abab”,也可以把它当做是一个更大进制的数,如31,37,41...(因为我们通常将字符‘a’~‘z’以:单个字符 - ‘a’ 转换成整数,而进制的选择最好比单个整数大,且为质数更好),并且如果我们单单用:单个字符 - ‘a’ 转换成整数则还会遇到一个问题,就是当两个字符串“aab”和“ab”前缀相同时,由于a转换成0,则两个字符串转换成的整数(以base-31为例)0*31^2 + 0*31^1 + 1*31^0 == 0*31^1 + 1*31^0将无法从数值上进行区分,就没有达到我们需要的效果,所以我们采用:单个字符 - ‘a’ + 1的形式进行字符的转换,这样‘a’~‘z’则代表1~26,有效对其进行了区分

对于例题一,我们要做的就是输入的同时,将每一个字符串转换成一个大整数,而此时又要注意一个问题,就是当我们的字符串过长,以31进制为例,我们所塑造出的大整数很容易就超过int,long long,乃至unsigned long long的范围,此时我们很容易想到hash的方法,就是对这个很大的整数进行MOD操作,给定一个MOD数值,这样一个很大的数就可以被限制在一个固定区间内,但是还是会出现问题,MOD如果不够大则很容易出现两个大整数MOD后的值相同的情况,这里我们希望MOD的值是一个很大数如2^64,这样重复的进率就会很小,在这里我们需要提及一个巧妙的技巧,对于数据类型为unsigned long long的整数,它会自动进行取模,所以不用担心它会溢出(也省略了mod操作),所以我们用unsigned long long存放每一个字符串对于转换成base-31后的整数,然后将这些数放入一个map映射中就可以得到不同的字符串的个数

代码:

 #include<iostream>
#include<stdio.h>
#include<string.h>
#include<string>
#include<map>
using namespace std; typedef unsigned long long ull;
const int N = ;
const int base = ; ull operate(string s){
int len = s.size();
ull ans = ;
for(int i = ; i < len ; i++){
ans = ans * base + s[i] - 'a' + ;
}
return ans;
} int main(){
int t;
scanf("%d", &t);
map<ull, int> mp;
while(t--){
int n;
scanf("%d", &n);
mp.clear();
for(int i = ; i <= n; i++){
string s;
cin>>s;
ull sum = operate(s);
mp[sum]++;
}
printf("%d\n", mp.size());
}
return ;
}

例题二 HDU4821 String

本题只为了借助题干中的问题辅助讲解字符串Hash,并不要求完全搞清楚题目该怎么解,理解题意和题解核心即可,同样是有t组输入,每组输入一个字符串(长度1~100000),同时输入两个整数m和l,求在这个字符串中,长度为m*l的子串(子串由m个长度为l的小子串拼接而成)且满足这个子串的小子串两两互不完全相同(如:aab和aaa不同)

题目核心分析

对于一个字符串如abcabcbcaabc,l==3,m==3,则需要找到这个长串中长度为3*3==9的子串,且组成它的3个长度为3的小子串两两不完全相同,同样的我们需要将这个长串转换成一个进制为base的大整数同时执行MOD操作,同样用unsigned long long作为数据存储的类型,我们在输入这个字符串后从下标0开始不断求出长度为i的子串的对应的base进制的值(自动取模)存放在Hash[i]中,有点类似前缀和

 Hash[] = ;
for(int i = ; i < len; i++){
Hash[i] = Hash[i-] * seed + s[i] - 'a' + ;
}

这里需要注意的点是,对于一个字符串abcabc中的,前一个abc和第二个abc我们如何操作才能使得它们所代表的值是一样的,因为字符串相同,但是出现的位置不同,如果用前缀和的形式相减得到ans = Hash[l + i - 1] - Hash[i - 1],则由于随着字符串的增长,越靠后的子串中字符×base的次方就越高,则ans = Hash[l + i - 1] - Hash[i - 1]当l==0和l==3时尽管它们都是对abcabc中的abc子串执行计算差的操作,后面的那个得到的ans一定会更大,所以我们需要一种方法取平衡这种由于base^n次方造成的影响,我们需要引入一个辅助数组base[],base[i]存放base进制时base^i的值,而对于字符串abcabc,我们已经求出了下标为i时的前缀和(base进制且自动取模),ans = Hash[i + l - 1] - Hash[i - 1] * base[l]则无论子串的位置如何都能通过成base[l]将多的次方平衡掉,使得只要小子串是相同的,则差ans就是相同的,这样我们又可以通过map进行去重操作了

由于是初步讲解字符串hash操作,针对例题二的具体思路中还有一个(去头添尾)的操作没有讲解,具体可以看代码,也有一些注释,而普通的做法会超时,但是出于对字符串的Hash的介绍到此已经够了

这里需要注意的是,在解题时你的字符串输入后是从下标0开始还是从下标1开始的,会对ans = Hash[i + l - 1] - Hash[i - 1] * base[l]这个部分有着轻微的数值上的+1-1影响,请不要盲目照搬

代码:

(我的这个字符串从0开始处理,会有一些边界问题多加处理,如果从1开始则更为方便)

 #include<set>
#include<map>
#include<stdio.h>
#include<string>
#include<string.h>
#include<iostream>
using namespace std; typedef long long ll;
typedef unsigned long long ull; //自动取模?!
const int N = ;
const int seed = ;
ull base[N];
ull Hash[N]; //类似于前缀和 hash[i]存放长度为i时整个字符串代表的整数值 int main(){
int m, l;
while(scanf("%d%d", &m, &l) != EOF){
string s;
cin>>s;
int len = s.size();
int ans = ;
map<ull, int> mp;
base[] = ;
for(int i = ; i <= l; i++) //存放seed^i的权重
base[i] = base[i-] * seed;
Hash[] = s[0] - 'a' + 1;
for(int i = ; i < len; i++){
Hash[i] = Hash[i-] * seed + s[i] - 'a' + ;
}
for(int i = ; i < l && i + m*l <= len; i++){ //采用一种去头添尾的神仙方法
// cout<<"LLLL"<<endl;
mp.clear();
for(int j = i; j <= i + (m-)*l; j += l){
//每次将一个小子串代表的大数放入map中
if(j != ){
ull sum = Hash[j+l-] - Hash[j-] * base[l];
// cout<<sum<<endl;
mp[sum]++;
}else{
ull sum = Hash[j+l-]; //如果是下标0开始则不需要减
// cout<<sum<<endl;
mp[sum]++;
}
}
// cout<<mp.size()<<endl;
if(mp.size() == m) ans++;
// cout<<"size"<<mp.size()<<endl;
// cout<<"mp[1]"<<mp[1]<<endl;
// else ans--;
//去头添尾开始
for(int j = i + l; j + m*l <= len; j+=l){
//添尾
ull sum = Hash[j + m*l -] - Hash[j + (m-)*l - ] * base[l];
// cout<<"添尾"<<endl;
// cout<<sum<<endl;
mp[sum]++;
// cout<<"size"<<mp.size()<<endl;
// cout<<"mp[1]"<<mp[1]<<endl;
//去头
if(j-l == ){
sum = Hash[j-];
mp[sum]--;
if(mp[sum] == ) mp.erase(sum);
// cout<<"去头"<<endl;
// cout<<sum<<endl;
// cout<<"size"<<mp.size()<<endl;
}else{
sum = Hash[j-] - Hash[j-l-] * base[l];
mp[sum]--;
if(mp[sum] == ) mp.erase(sum);
// cout<<"去头"<<endl;
// cout<<sum<<endl;
// cout<<"size"<<mp.size()<<endl;
} if(mp.size() == m) ans++;
}
}
if(s.size() == ) ans=;
printf("%d\n", ans);
}
return ;
}

(通俗易懂小白入门)字符串Hash+map判重——暴力且优雅的更多相关文章

  1. hdu 4821 字符串hash+map判重 String (长春市赛区I题)

    http://acm.hdu.edu.cn/showproblem.php?pid=4821 昨晚卡了非常久,開始TLE,然后优化了之后,由于几个地方变量写混.一直狂WA.搞得我昨晚都失眠了,,. 这 ...

  2. POJ3087 Shuffle'm Up —— 打表找规律 / map判重

    题目链接:http://poj.org/problem?id=3087 Shuffle'm Up Time Limit: 1000MS   Memory Limit: 65536K Total Sub ...

  3. hdu 5012 bfs --- 慎用STL 比方MAP判重

    http://acm.hdu.edu.cn/showproblem.php?pid=5012 发现一个问题 假设Sting s = '1'+'2'+'3'; s!="123"!!! ...

  4. BFS以及hash表判重的应用~

    主要还是讲下hash判重的问题吧 这道题目用的是除法求余散列方式 前几天看了下算法导论 由于我们用的是线性再寻址的方式来解决冲突问题 所以hash表的大小(余数的范围)要包含我们要求的范围 对mod的 ...

  5. HDU4821---字符串hash,map判重

    这是2013年长春区域赛的铜牌题...然而第一次做的时候一直觉得会超时的..最后才知道并没有想象中的那么恐怖: 这题有两个注意的地方: (1)h[i] = h[i-1] * seed + s[i] - ...

  6. Hdu 4821 (字符串hash+map)

    题目链接https://vjudge.net/problem/HDU-4821 题意:给定字符串S ,询问用几个子串满足 : 1.长度为n*len  . 2. n个子串都不相同. 题解:倒序hash将 ...

  7. (通俗易懂小白入门)网络流最大流——EK算法

    网络流 网络流是模仿水流解决生活中类似问题的一种方法策略,来看这么一个问题,有一个自来水厂S,它要向目标T提供水量,从S出发有不确定数量和方向的水管,它可能直接到达T或者经过更多的节点的中转,目前确定 ...

  8. 字符串hash+回文树——hdu6599

    拖了很久才补的回文树,感觉网上的博客都是一个做法..回文树统计不同种类的回文串出现次数,然后用字符串hash来判每个回文子串是否符合要求 #include<bits/stdc++.h> u ...

  9. 字符串hash入门

    简单介绍一下字符串hash 相信大家对于hash都不陌生 翻译过来就是搞砸,乱搞的意思嘛 hash算法广泛应用于计算机的各类领域,像什么md5,文件效验,磁力链接 等等都会用到hash算法 在信息学奥 ...

随机推荐

  1. Redis原子性写入HASH结构数据并设置过期时间

    Redis中提供了原子性命令SETEX或SET来写入STRING类型数据并设置Key的过期时间: > SET key value EX NX ok > SETEX key value ok ...

  2. 洛谷P2598 [ZJOI2009]狼和羊的故事 题解

    题目链接: https://www.luogu.org/problemnew/show/P2598 分析: 我们知道此题的目的是将狼和羊分割开,很容易想到狼在S,羊在T中. 首先,我们可以在狼,羊,空 ...

  3. 关于java爬虫以及一些实例

    首先是工具介绍 Jsoup jsoup 是一款Java 的HTML解析器,可直接解析某个URL地址.HTML文本内容.它提供了一套非常省力的API,可通过DOM,CSS以及类似于jQuery的操作方法 ...

  4. 快速掌握mongoDB(五)——通过mongofiles和C#驱动操作GridFS

    1 GridFS简介 当前Bson能存储的最大尺寸是16M,我们想把大于16M的文件存入mongoDB中怎么办呢?mongoDB提供的GridFS就是专门做这个的.使用GridFS存储大文件时,文件被 ...

  5. 给hexo添加宠物

    开始 之前在博客园上看到,公告栏里有人竟然在养鱼,觉得很好玩!一直念念不忘的,于是就想着在hexo中也来养几只,因为我用的事Next的Muse主题,所以有一个非常合适的侧边栏,先来看看效果. 点击此处 ...

  6. Openstack中用keypair生成和访问虚机的方法

    Openstack中用keypair生成和访问虚机的方法 标签:task   iso   perm   cte   生成   复制   vol   rsa   sla Openstack中用镜像文件生 ...

  7. 华三F100 系列防火墙 - 浮动路由联动NQA 实现双线路自动切换

    公司 有两条公网线路,一条移动作为日常主用线路,一条联通作为备用线路. 为了实现主备线路自动切换,配置了浮动路由 但浮动路由只能在 主用接口为down状态时才能浮出接管默认路由.如果故障为非物理链路故 ...

  8. springboot整合elasticsearch(基于es7.2和官方high level client)

    前言 最近写的一个个人项目(传送门:全终端云书签)中需要用到全文检索功能,目前 mysql,es 都可以做全文检索,mysql 胜在配置方便很快就能搞定上线(参考这里),不考虑上手难度,es 在全文检 ...

  9. Pyinstaller 打包工具的使用!!!

    打包成一个文件夹: pyinstaller xxx.py 打包成单个文件: pyinstaller -F xxx.py 打包成不显示终端的单个文件: pyinstaller -F -w xxx.py ...

  10. 从windows10迁移到Linux Deepin

    如题, 这几天从windows系统迁移到deepin的linux系统花了很多时间, 以致最近都没时间来博客园.现在将这几天的成果分享出来, 顺便也做个记录.先不多说, 上一张新系统界面. 其实在装de ...