• (通俗易懂小白入门)字符串Hash+map判重——暴力且优雅


    字符串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映射中就可以得到不同的字符串的个数

    代码:

     1 #include<iostream>
     2 #include<stdio.h>
     3 #include<string.h>
     4 #include<string>
     5 #include<map>
     6 using namespace std;
     7 
     8 typedef unsigned long long ull;
     9 const int N = 10005;
    10 const int base = 31;
    11 
    12 ull operate(string s){
    13     int len = s.size();
    14     ull ans = 1;
    15     for(int i = 0; i < len ; i++){
    16         ans = ans * base + s[i] - 'a' + 1;
    17     }
    18     return ans;
    19 }
    20 
    21 int main(){
    22     int t;
    23     scanf("%d", &t);
    24     map<ull, int> mp;
    25     while(t--){
    26         int n;
    27         scanf("%d", &n);
    28         mp.clear();
    29         for(int i = 1; i <= n; i++){
    30             string s;
    31             cin>>s;
    32             ull sum = operate(s);
    33             mp[sum]++;
    34         }
    35         printf("%d
    ", mp.size());
    36     }
    37     return 0;
    38 }

    例题二 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]中,有点类似前缀和

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

    这里需要注意的点是,对于一个字符串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开始则更为方便)

     1 #include<set>
     2 #include<map>
     3 #include<stdio.h>
     4 #include<string>
     5 #include<string.h>
     6 #include<iostream>
     7 using namespace std;
     8 
     9 typedef long long ll;
    10 typedef unsigned long long ull;    //自动取模?! 
    11 const int N = 100005;
    12 const int seed = 31;
    13 ull base[N];
    14 ull Hash[N];            //类似于前缀和 hash[i]存放长度为i时整个字符串代表的整数值 
    15 
    16 int main(){
    17     int m, l;
    18     while(scanf("%d%d", &m, &l) != EOF){
    19         string s;
    20         cin>>s;
    21         int len = s.size();
    22         int ans = 0;
    23         map<ull, int> mp;
    24         base[0] = 1;
    25         for(int i = 1; i <= l; i++)            //存放seed^i的权重 
    26             base[i] = base[i-1] * seed;
    27         Hash[0] = s[0] - 'a' + 1;
    28         for(int i = 1; i < len; i++){
    29             Hash[i] = Hash[i-1] * seed + s[i] - 'a' + 1;
    30         } 
    31         for(int i = 0; i < l && i + m*l <= len; i++){            //采用一种去头添尾的神仙方法 
    32 //            cout<<"LLLL"<<endl;
    33             mp.clear();
    34             for(int j = i; j <= i + (m-1)*l; j += l){
    35                 //每次将一个小子串代表的大数放入map中
    36                 if(j != 0){
    37                     ull sum = Hash[j+l-1] - Hash[j-1] * base[l];
    38 //                    cout<<sum<<endl;
    39                     mp[sum]++;
    40                 }else{
    41                     ull sum = Hash[j+l-1];        //如果是下标0开始则不需要减 
    42 //                    cout<<sum<<endl;
    43                     mp[sum]++;
    44                 }
    45             }
    46             //            cout<<mp.size()<<endl;
    47             if(mp.size() == m) ans++;
    48 //            cout<<"size"<<mp.size()<<endl;
    49 //            cout<<"mp[1]"<<mp[1]<<endl;
    50 //            else ans--;
    51             //去头添尾开始 
    52             for(int j = i + l; j + m*l <= len; j+=l){
    53                 //添尾 
    54                 ull sum = Hash[j + m*l -1] - Hash[j + (m-1)*l - 1] * base[l];
    55 //                cout<<"添尾"<<endl;
    56 //                cout<<sum<<endl;
    57                 mp[sum]++;
    58 //                cout<<"size"<<mp.size()<<endl;
    59 //                cout<<"mp[1]"<<mp[1]<<endl;
    60                 //去头
    61                 if(j-l == 0){
    62                     sum = Hash[j-1];
    63                     mp[sum]--;
    64                     if(mp[sum] == 0) mp.erase(sum);
    65 //                    cout<<"去头"<<endl;
    66 //                    cout<<sum<<endl;
    67 //                    cout<<"size"<<mp.size()<<endl;
    68                 }else{
    69                     sum = Hash[j-1] - Hash[j-l-1] * base[l];
    70                     mp[sum]--;
    71                     if(mp[sum] == 0) mp.erase(sum);
    72 //                    cout<<"去头"<<endl;
    73 //                    cout<<sum<<endl;
    74 //                    cout<<"size"<<mp.size()<<endl;
    75                 }
    76                 
    77                 if(mp.size() == m) ans++;
    78             }
    79         }
    80         if(s.size() == 1) ans=0;
    81         printf("%d
    ", ans);
    82     }
    83     return 0;
    84 } 
    如果有任何意见请在评论区积极留言
  • 相关阅读:
    Java读取文件,将字符串转化成日期类型,将日期类型进行加减
    javaweb文件下载 部署到服务器文件下载有问题
    linux下安装mysql5.5
    eclipse下修改项目名导致tomcat内发布名不一致的解决方法
    openclinica学习遇到的问题
    Ubuntu 安装joomla出错(Could not connect to the database. Connector returned number: The MySQL adap)解决办法
    JSP中Include指令和Include动作的区别
    JFrame容器
    JavaScript函数调用
    JavaScript
  • 原文地址:https://www.cnblogs.com/YLTFY1998/p/11393691.html
Copyright © 2020-2023  润新知