0x14.基础数据结构 — hash表与字符串hash

目录

  • 一、Hash表
    • 1.AcWing 137. 雪花雪花雪花
      • 0.hash表+链表
      • 1.字符串的最小表示法
  • 二、字符串 h a s h hash hash
    • 0.AcWing 138. 兔子与兔子
    • 1.luogu P3370 【模板】字符串哈希
    • 3.AcWing 139. 回文子串的最大长度
  • 三、一个永远都不可能被hack的hash函数

声明:
本系列博客是《算法竞赛进阶指南》+《算法竞赛入门经典》+《挑战程序设计竞赛》的学习笔记,主要是因为我三本都买了 按照《算法竞赛进阶指南》的目录顺序学习,包含书中的少部分重要知识点、例题解题报告及我个人的学习心得和对该算法的补充拓展,仅用于学习交流和复习,无任何商业用途。博客中部分内容来源于书本和网络(我尽量减少书中引用),由我个人整理总结(习题和代码可全都是我自己敲哒)部分内容由我个人编写而成,如果想要有更好的学习体验或者希望学习到更全面的知识,请于京东搜索购买正版图书:《算法竞赛进阶指南》——作者李煜东,强烈安利,好书不火系列,谢谢配合。


下方链接为学习笔记目录链接(中转站)


学习笔记目录链接


ACM-ICPC在线模板


一、Hash表

H a s h Hash Hash 表又称散列表,一般由 H a s h Hash Hash 函数与链表结构共同实现。

有一种称为开散列的解决方案是,建立一个邻接表结构,以 H a s h Hash Hash 函数的值域作为表头数组,映射后的值相同的原始信息被分到同一类。

例如,统计一个长度为 N N N的随机数序列 A A A中每一个数字分别出现了多少次,设计 H a s h Hash Hash 函数为 H ( x ) = ( x   m o d   P ) + 1 H(x)=(x\ mod\ P)+1 H(x)=(x mod P)+1 P P P 为一个比较大的质数,显然,这个 H a s h Hash Hash函数将序列 A A A 分为 P P P 类,对于每一个 A [ i ] A[i] A[i] ,定位到 h e a d [ H ( A [ i ] ) ] head[H(A[i])] head[H(A[i])] 指向的表头,如果链表中不包含 A [ i ] A[i] A[i],插入新节点,如果已经存在,出现次数累加 1 1 1 ,由于是随机数 A [ i ] A[i] A[i]会均匀分散在每一个表头,整体的时间复杂度为 Θ ( N ) \Theta(N) Θ(N)

1.AcWing 137. 雪花雪花雪花

0x14.基础数据结构 — hash表与字符串hash_第1张图片

0.hash表+链表

设计Hash函数为 H ( a 1 , a 2 , ⋯   , a 6 ) = ( ∑ i = 1 6 a i + Π i = 1 6 a i )   m o d   p H(a_1,a_2,\cdots,a_6) = (\sum^{6}_{i=1}a_i + \Pi^{6}_{i=1}a_i)\ mod\ p H(a1,a2,,a6)=(i=16ai+Πi=16ai) mod p ,(累加和累乘)其中 p p p 是一个我们自己选择的一个大质数

然后我们依次把每个雪花插入 H a s h Hash Hash表中,在对应的链表中查找是否已经有相同的雪花。

作者的标程

#include 
#include 
#include 
#include 
#define ll long long
using namespace std;
const int N = 100006, P = 99991;
int n, a[6], b[6];
struct S {
	int s[6];
};
vector<S> snow[N];

int H() {
	int s = 0, k = 1;
	for (int i = 0; i < 6; i++) {
		(s += a[i]) %= P;
		k = (ll)k * a[i] % P;
	}
	return (s + k) % P;
}

bool pd() {
	for (int i = 0; i < 6; i++)
		for (int j = 0; j < 6; j++) {
			bool v = 1;
			for (int k = 0; k < 6; k++)
				if (a[(i+k)%6] != b[(j+k%6)]) {
					v = 0;
					break;
				}
			if (v) return 1;
			v = 1;
			for (int k = 0; k < 6; k++)
				if (a[(i+k)%6] != b[(j-k+6)%6]) {
					v = 0;
					break;
				}
			if (v) return 1;
		}
	return 0;
}

bool insert() {
	int h = H();
	for (unsigned int i = 0; i < snow[h].size(); i++) {
		memcpy(b, snow[h][i].s, sizeof(b));
		if (pd()) return 1;
	}
	S s;
	memcpy(s.s, a, sizeof(s.s));
	snow[h].push_back(s);
	return 0;
}

int main() {
	cin >> n;
	for (int i = 1; i <= n; i++) {
		for (int j = 0; j < 6; j++) scanf("%d", &a[j]);
		if (insert()) {
			cout << "Twin snowflakes found." << endl;
			return 0;
		}
	}
	cout << "No two snowflakes are alike." << endl;
	return 0;
}

1.字符串的最小表示法

判断是否有相同雪花的方式就是直接暴力枚举就好
若只有旋转操作,可以用字符串的最小表示:
字符串长度为n,旋转n次,取字典序最小的那一种,即为字符串的最小表示。
现在有翻转操作,所以我们对原序列求最小表示,再对翻转后的序列求一个最小表示

#include
#include
#include
#include
#include
#include
#include
#define ls (p<<1)
#define rs (p<<1|1)
#define mid (l+r>>1)
#define over(i,s,t) for(register int i=s;i<=t;++i)
#define lver(i,t,s) for(register int i=t;i>=s;--i)
//#define int __int128
using namespace std;

typedef long long ll;
typedef pair<int,int> PII;
typedef pair<ll,ll> PLL;

const int N=1e5+7;
const int mod=1e9+7;
const ll INF=1e15+7;
const double EPS=1e-10;

int n;
int snows[N][6],id[N];

void get_min(int *b){//字符串的最小表示
    static int a[12];
    for(int i=0;i<12;i++)
        a[i]=b[i%6];
    int i=0,j=1,k;
    while(i<6&&j<6){
        for(k=0;k<6&&a[i+k]==a[j+k];k++);//每次直接找就完事了
        if(k==6)break;//说明全部相等
        if(a[i+k]>a[j+k]){//谁大往后跳,小的永远不动,也就是答案
            i+=k+1;//i+k是跑到字符相等的位置,要再加1往后移一位
            if(i==j)//如果相等就往后退
                i++;
        }
        else {
            j+=k+1;//一样
            if(i==j)
                j++;
        }
    }
    k=min(i,j);
    for(int i=0;i<6;++i)
        b[i]=a[i+k];
}

bool cmp_array(int a[],int b[]){//比较a和b字典序谁最小
    for(int i=0;i<6;++i){
        if(a[i]<b[i])
            return true;
        else if(a[i]>b[i])
            return false;
    }
    return false;//全部相等
}

bool cmp_val(int a,int b){//如果正反一比都是false说明全相等
    for(int i=0;i<6;++i){
        if(snows[a][i]<snows[b][i])
            return true;
        else if(snows[a][i]>snows[b][i])
            return false;
    }
    return false;
}
int main()
{
    scanf("%d",&n);
    int snow[6],rsnow[6];
    over(i,0,n-1){
        for(int j=0,k=5;j<6;j++,k--){
            scanf("%d",&snow[j]);//原序列
            rsnow[k]=snow[j];//翻转序列
        }
        get_min(snow);
        get_min(rsnow);
        if(cmp_array(snow,rsnow))memcpy(snows[i],snow,sizeof snow);
        else memcpy(snows[i],rsnow,sizeof rsnow);
        id[i]=i;
    }
    sort(id,id+n,cmp_val);
    for(int i=1;i<n;++i){
        if(!cmp_val(id[i],id[i-1])&&!cmp_val(id[i-1],id[i])){//如果相等(这里神)
            puts("Twin snowflakes found.");
            return 0;
        }
    }
    puts("No two snowflakes are alike.");
    return 0;
}

二、字符串 h a s h hash hash

将一个任意长度的字符串映射为一个非负整数,并且其冲突概率几乎为零。

取一个固定值P,将字符串看作P进制数,并为每一个字符分配一个大于0的数值,例如对于小写字母,令 a = 1 , b = 2 , c = 3 , ⋯ , z = 26 a=1,b=2,c=3,⋯,z=26 a=1,b=2,c=3,,z=26,取一个固定值M,求出该 P P P进制数对 M M M的余数,作为该字符串的 H a s h Hash Hash值。

一般来说,我们取 P = 131 P=131 P=131 13331 13331 13331 (质数) , 此时 H a s h Hash Hash值冲突的概率极低;同时,取 M = 2 64 M=2^{64} M=264 ,用 u n s i g n e d   l o n g   l o n g unsigned\ long\ long unsigned long long储存 H a s h Hash Hash值,这样的话算术溢出就相当于取模

除非特殊构造的数据,上述 H a s h Hash Hash算法很难冲突;保险起见,可以多取几组 P P P M M M(例如大质数),多进行几组 H a s h Hash Hash运算,结果都相等时才认为字符串相等

对字符串的各种操作,都可以直接对 P P P进制数进行操作反映到 H a s h Hash Hash值上:

已知字符串S的 H a s h Hash Hash值是 H ( S ) H(S) H(S),那么在S后面添加一个字符c之后新的字符串的Hash值为 H ( S + c ) = (   H   ( s ) ∗ P   +   v a l u e [ c ]   )   m o d   M H(S+c)=(\ H\ (s)∗P\ +\ value[c]\ )\ mod\ M H(S+c)=( H (s)P + value[c] ) mod M

已知字符串S的 H a s h Hash Hash值为 H ( S ) H(S) H(S) ,字符串 S + T S+T S+T H a s h Hash Hash值为 H ( S + T ) H(S + T) H(S+T),那么T的Hash值就是: H ( T ) = ( H ( S + T )   −   H ( S )   ∗   P l e n g t h ( T ) )   m o d   M 。 H(T)=(H(S+T)\ −\ H(S)\ ∗\ P ^{length(T)} )\ mod\ M。 H(T)=(H(S+T)  H(S)  Plength(T)) mod M

通过上面的操作,我们就可以在 Θ ( N ) \Theta(N) Θ(N)的时间内与处理所有字符串的 H a s h Hash Hash值,并在 Θ ( 1 ) \Theta(1) Θ(1)的时间内查询任意字串的 H a s h Hash Hash值。

0.AcWing 138. 兔子与兔子

0x14.基础数据结构 — hash表与字符串hash_第2张图片
这道其实就是上面思路的一个模板题。

#include
#include
#include
#include
#include
#include
#include
#define ls (p<<1)
#define rs (p<<1|1)
#define mid (l+r>>1)
#define over(i,s,t) for(register int i=s;i<=t;++i)
#define lver(i,t,s) for(register int i=t;i>=s;--i)
//#define int __int128
using namespace std;

typedef long long ll;
typedef unsigned long long ull;
typedef pair<int,int> PII;

const int N=1e6+7;
const int mod=1e9+7;
const ll INF=1e15+7;
const double EPS=1e-10;
const int p=131;//13331

char str[N];
ull h[N],power[N];

ull get(int l,int r){//要用ull
    return h[r]-h[l-1]*power[r-l+1];
}

int n,m;
int main()
{
    scanf("%s",str+1);
    int len=strlen(str+1);
    power[0]=1;//p的n次方
    over(i,1,len){
        h[i]=h[i-1]*p+str[i]-'a'+1;
        power[i]=power[i-1]*p;
    }
    scanf("%d",&m);
    while(m--){
        int l1,r1,l2,r2;
        scanf("%d%d%d%d",&l1,&r1,&l2,&r2);
        if(get(l1,r1)==get(l2,r2))
            puts("Yes");
        else puts("No");
    }
    return 0;
}

1.luogu P3370 【模板】字符串哈希

0x14.基础数据结构 — hash表与字符串hash_第3张图片
给出一个 h a s h hash hash模板

#include
#include
#include
#include
#include
#include
#include
#define ls (p<<1)
#define rs (p<<1|1)
#define mid (l+r>>1)
#define over(i,s,t) for(register int i=s;i<=t;++i)
#define lver(i,t,s) for(register int i=t;i>=s;--i)
//#define int __int128
using namespace std;

typedef long long ll;
typedef unsigned long long ull;
typedef pair<int,int> PII;

const int N=1e5+7;
const int mod=1e9+7;
const ll INF=1e15+7;
const double EPS=1e-10;
const int p=131;//13331
int n,m;
ull a[N];
char str[N];
ull get_hash(char s[]){
    ull res=0;
    int len=strlen(s);
    over(i,0,len-1){
        res=res*p+(ull)s[i];
    }
    return res;
}

int main()
{
    scanf("%d",&n);
    over(i,1,n){
        cin>>str;
        a[i]=get_hash(str);
    }
    int ans=1;
    sort(a+1,a+1+n);
    over(i,1,n-1){
        if(a[i]!=a[i+1])
            ans++;
    }
    printf("%d\n",ans);
    return 0;
}

3.AcWing 139. 回文子串的最大长度

0x14.基础数据结构 — hash表与字符串hash_第4张图片
大佬的直接 h a s h hash hash的解法:
(据他说是 O ( n ) O(n) O(n),我看着像 O ( n l o g n ) O(nlogn) O(nlogn)
https://www.acwing.com/solution/AcWing/content/893/
下面是他的代码,我加了一些注释。

#include 
#include 
#include 
#include 
#include 
#include 

#define boost ios_base::sync_with_stdio(false); cin.tie(0); cout.tie(0);
#define fo(v,a,b) for(int v=(a); v<=(b); v++)
#define fr(v,a,b) for(int v=(a); v>=(b); v--)
#define rng(v,a,b) for(int v=(a); v<(b); v++)

using namespace std;
typedef long long ll;
typedef unsigned long long ull;
template<typename T> T& chmax(T& a, T b) { a = a > b ? a : b; return a;}
template<typename T> T& chmin(T& a, T b) { a = a < b ? a : b; return a;}
const int maxn=1e6+6,P=131;
char s[maxn],a[maxn*2];
ull H1[maxn*2],H2[maxn*2],g[maxn*2];
int main()
{
    int T=0;
    while(cin >> (s+1) && strcmp(s+1, "END")) {
        cout << "Case " << ++T << ": ";
        int len = strlen(s+1), tl=0;
        a[++tl] = '#';//填上#号整个序列翻倍,这样奇数长度都变成偶数长度
        fo(i,1,len) {
            a[++tl] = s[i]; a[++tl] = '#';
        }
        a[tl+1] = '\0'; len=tl;

        g[0] = 1;//p的n次方
        fo(i,1,len) {//正序
            H1[i] = H1[i-1]*P+a[i];//求hash数组 
            g[i] = g[i-1]*P;
        }
        fr(i,len,1)//逆序 ,因为是判断是否回文,所以必须再求逆序的,左边用正序,右边用逆序
            H2[i] = H2[i+1]*P+a[i];

        int ans=0,l;
        fo(i,1,len) {//遍历一遍
            l=ans;
            if(i+l>=len || i-l<1) break;//超了就break
            if(H1[i+l]-H1[i-1]*g[l+1] != H2[i-l]-H2[i+1]*g[l+1])
                continue;//不相等说明a[i]=='#',已#号为分界线左右长度为分别长度为l的子串相等,长度为l的子串为回文子串
            while(a[i+l+1] == a[i-l-1] && i+l+1<=len && i-l-1>0) l++;//相等l++,(一定是回文)
            chmax(ans,l);//因为a数组被#扩充了一倍,所以长度不再是2*l,而是l。取最大的l
        }
        cout << ans << '\n';
    }
    return 0;
}

书中给出的做法:
枚举回文串中心的位置 i = [ 1 , N ] i = [1, N] i=[1,N],检查从中心往外左右两侧最长可以扩展到多长:

求出一个最大的数p使得 S [ i − p , i ] = r e v e r s e ( S [ i , i + p ] ) S[i - p, i] = reverse(S[i, i + p]) S[ip,i]=reverse(S[i,i+p]),那么此回文串长度为 2 ∗ p + 1 2 * p + 1 2p+1

求出一个最大的数q使得 S [ i − q , i − 1 ] = = r e v e r s e ( S [ i , i + q − 1 ] ) S[i - q, i - 1] == reverse(S[i, i + q - 1]) S[iq,i1]==reverse(S[i,i+q1]),那么此回文串的长度为 2 ∗ q 2 * q 2q

根据上一道题目,我们已经知道如何通过 Θ ( N ) \Theta(N) Θ(N)的预处理使得可以在 Θ ( 1 ) \Theta(1) Θ(1)的时间内计算原字符串任意字串的 H a s h Hash Hash值;类似的,对原字符串倒着进行一遍处理,就能在 Θ ( 1 ) \Theta(1) Θ(1)的时间内计算原字符串任意字串的逆序的 H a s h Hash Hash值。

对于每一个位置i,可以使用二分的方法在 Θ ( log ⁡ N ) \Theta(\log{N}) Θ(logN)的时间内找到p、q的位置;于是,本解法的总时间复杂度为 Θ ( N log ⁡ N ) \Theta(N \log{N}) Θ(NlogN)

三、一个永远都不可能被hack的hash函数

5年了,这题还是没人写得出来

下面这三个链接是一个有趣的故事
BZOJ3097:http://www.lydsy.com/JudgeOnline/problem.php?id=3097

BZOJ3098:http://www.lydsy.com/JudgeOnline/problem.php?id=3098

BZOJ3099:http://www.lydsy.com/JudgeOnline/problem.php?id=3099


typedef unsigned long long u64;
typedef pair<int, int> PII;
const int MaxN = 100000;
inline int hash_handle(const char *s, const int &n, const int &l, const int &base,const int &mod1, const int &mod2)
{
    int li_n;
    static PII li[MaxN];
    
    u64 hash_pow_l;
    u64 val;
    hash_pow_l = 1;
    
    for (int i = 1; i <= l; i++)
        hash_pow_l = (hash_pow_l * base) % mod1;
    
    li_n = 0;
    val = 0;
    
    for (int i = 0; i < l; i++)
        val = (val * base + s[i] - 'a') % mod1;
    li[li_n++].first = val;
    
    for (int i = l; i < n; i++)
    {
        val = (val * base + s[i] - 'a') % mod1;
        val = (val + mod1 - ((s[i - l] - 'a') * hash_pow_l) % mod1) % mod1;
        li[li_n++].first = val;
    }
    hash_pow_l = 1;
    for (int i = 1; i <= l; i++)
        hash_pow_l = (hash_pow_l * base) % mod2;
    
    li_n = 0;
    val = 0;
    
    for (int i = 0; i < l; i++)
        val = (val * base + s[i] - 'a') % mod2;
    
    li[li_n++].second = val;
    for (int i = l; i < n; i++)
    {
        val = (val * base + s[i] - 'a') % mod2;
        val = (val + mod2 - ((s[i - l] - 'a') * hash_pow_l) % mod2) % mod2;
        li[li_n++].second = val;
    }
    sort(li, li + li_n);
    li_n = unique(li, li + li_n) - li;
    return li_n;
}

谁敢 h a c k hack hack我?

注:如果您通过本文,有(qi)用(guai)的知识增加了,请您点个赞再离开,如果不嫌弃的话,点个关注再走吧,日更博主每天在线答疑 ! 当然,也非常欢迎您能在讨论区指出此文的不足处,作者会及时对文章加以修正 !如果有任何问题,欢迎评论,非常乐意为您解答!( •̀ ω •́ )✧

你可能感兴趣的:(【算法竞赛学习笔记】,#,【哈希】)