【算法分享】哈希入门:不同子串统计与哈希冲突构造

【算法分享】哈希入门:不同子串统计与哈希冲突构造

一、字符串哈希(Rolling Hash)简介与基础实现 🚀

在处理字符串比较、查找子串等问题时,字符串哈希 是一项高效而常用的技巧。它能将一个字符串或子串映射为一个整数(哈希值),从而在 O(1) 时间内判断两个子串是否相等。

字符串哈希本质上是将字符串看做一个进制数 用如下的公式计算哈希值:

\(\mathrm{Hash}(s) = \left(\sum_{i=0}^{n-1} (s[i] - 'a') \cdot \text{base}^i \right) \bmod \text{mod}\)

哈希函数的一般形式:

我们给一个字符串比如 "abc",按照以下方法计算哈希值:

res = 0;

for ( c : s)

{

res = (res *base + c)% mod;

}

在 C++ 中我们用的是 ASCII 值,也就是 'a' == 97,'b' == 98,以此类推。

设base = 131,所以 "abc" 的哈希过程是:

res = 0

res = 0 * 131 + 'a' = 0 + 97 = 97

res =97 * 131 + 'b' = 12715 + 98 = 12813

res = ... * 131 + 'c' = ...

二、字符串哈希经典例题🚀

📚题目描述

如题,给定 \(N\) 个字符串(第 \(i\) 个字符串长度为 \(M_i\),字符串内包含数字、大小写字母,大小写敏感),请求出 \(N\) 个字符串中共有多少个不同的字符串。

(链接:https://www.luogu.com.cn/problem/solution/P3370)

#include

using namespace std;

typedef unsigned long long ll; // 定义无符号长整型别名,方便后续使用

// 哈希函数参数,b是基数,m是取模数

const ll b=131;

const ll m=1e9+7;

// 字符串哈希函数,计算字符串s的哈希值

ll hash_kk(string s) {

ll res=0; // 结果变量初始化为0

for (char c : s) {

// 每个字符依次乘以基数b后加上当前字符ASCII码,再对m取模,避免溢出

res=(res*b+c)%m;

}

return res; // 返回字符串的哈希值

}

int main() {

ios_base::sync_with_stdio(false);

cin.tie(0);

cout.tie(0);

ll n;

cin>>n; // 读取字符串个数

vector kk; // 用于存储所有字符串的哈希值

for(ll i=0;i

string s;

cin>>s; // 读取字符串

kk.push_back(hash_kk(s)); // 计算哈希值并存入kk中

}

ll ans=1; // 计数不同字符串的数量,至少有一个

sort(kk.begin(),kk.end()); // 将所有哈希值排序,方便去重

for(ll i=1;i

// 遍历排序后的哈希值数组,统计不重复的哈希值数量

if(kk[i]!=kk[i-1])

ans++;

}

cout<

return 0;

}

⚠️ 注意事项

该模板使用的是单哈希,虽然简单高效,但存在哈希冲突风险。

在极端测试数据下可能出现错误判定(哈希碰撞),通常加双哈希/多哈希解决。

unsigned long long 自然溢出模拟模\(2^{64}\) 的效果。

三、哈希的风险与哈希冲突

❗ 哈希冲突是什么?

单哈希在理论上是把字符串映射到一个大整数范围内,但这个映射并不是完美的“一对一”。不同的字符串可能计算出相同的哈希值,这种现象称为哈希冲突(Hash Collision)。

我们来看一道题 BZOJ Hash Killer I

🔍 题目背景

VFleaKing 采用的是单哈希:

采用 unsigned long long(64位无符号整数)存储哈希值;

不取模,使用自然溢出模拟 模 \(2^{64}\);

通过哈希排序去重判断子串是否相同。

🚩 风险点

这种写法存在隐患:

虽然64位空间很大,但任意输入都可能有不同子串碰撞哈希值;

一旦发生碰撞,程序会误判不同子串为相同,导致结果错误(WA);

攻击者或测试者可以构造特殊数据,使程序在实际测试中失败。

🎯 冲突构造思路

利用以下性质:

\(Hash(s)=\sum\limits_{i=0}^{l-1}​(s[i]−′a′)×base^{l-1-i}\mod 2^{64}\)

若找到两个不同的字符串 s 、t,满足:

\({Hash}(s) \equiv {Hash}(t) \pmod{2^{64}}, \quad s \neq t

\)

\({Hash}(s) -{Hash}(t)=k*{2^{64}}, \quad k \neq 0

\)

我们现在已经理解VFleaKing 的哈希逻辑,接着就是寻找哈希冲突的两个子串,(即不同的子串,哈希值相同)

可以基于数学构造”哈希冲突“

#include

using namespace std;

typedef unsigned long long ll;

const ll base = 2;

//计算无符号64位哈希

ll hashk(string s) {

ll res=0;

for (char c:s) {

res=(res*base)+(c-'a');

}

return res;

}

int main() {

ios_base::sync_with_stdio(false);

cin.tie(0);

cout.tie(0);

int l = 64;

int m = 3; // 拼接次数

int n = l * m;

//构造两个字符串

string s(l,'a');

s[0]='c';//'c'-'a'=2

string t(l,'a');

ll r1=hashk(s);

ll r2=hashk(t);

//构造哈希冲突

string k;

if (r1==r2) {

for (int i = 0; i < m; i++) {

if (i % 2 == 0)

k += s;

else

k += t;

}

cout << n << " " << l << "\n";

cout << k<< "\n";

}

return 0;

}

🔐 四、提高哈希稳定性的做法 — 双哈希

🧮 1. 双哈希(Double Hashing)

双哈希就是对同一字符串使用两个不同的哈希函数 计算哈希值,通常选择不同的base 和(或)不同的模数 mod。

\(hash(s)=\sum\limits_{i=0}^{n-1}​s[i]⋅base^{i} \mod mod\)

const int b1=131,b2=1331;

const int m1=1e9+7,m2=1e9+9;

ll hashk(string s,ll b,ll m) {

ll res=0;

for (char c:s) {

res=(res*b)%m;

}

return res;

}

以洛谷 字符串 Hash(数据加强)为例

说明/提示

设第 \(i\) 个字符串长度为 \(m_i\)。

本题评测由三个子任务构成:

第一个子任务(卡大模数 Hash):

测试点

$n = $

$m_i = $

\(1\sim 6\)

\(10^6\)

\(6\)

第二个子任务(卡底数为偶数的自然溢出 Hash):

测试点

\(n=\)

\(m_i\approx\)

\(m_{max}=\)

\(1\sim 6\)

\(10^4\)

\(1000\)

\(1500\)

第三个子任务:(卡奇数底数哈希)

测试点

\(n=\)

\(m_i\approx\)

\(1\sim 6\)

\(2^{11}+1\)

\(2^{11}\)

为了避免哈希冲突,我们选用双哈希

#include

using namespace std;

typedef unsigned long long ull;

//定义两个基数和模

const ull b1=131;

const ull b2=1331;

const ull m1=1e9+7;

const ull m2=1e9+9;

//计算字符串的哈希值

ull hashf(string s,ull b,ull m) {

ull res=0;

for (char c:s) {

res=(res*b+c)%m;

}

return res;

}

int main()

{

ios::sync_with_stdio(false);

cin.tie(0);

cout.tie(0);

int n;

cin>>n;

vector v;

int sum=1;

while(n--) {

string s;

cin>>s;

//组合双哈希,简单拼接或者异或(冲突概率高)

ull ans1=hashf(s,b1,m1);

ull ans2=hashf(s,b2,m2);

//把两个哈希值拼成一个大数,保证组合后的的唯一

ull ans=ans1*m2+ans2;

v.push_back(ans);

//寻找无重复的hash值

}

sort(v.begin(),v.end());

for (int i=1;i

if (v[i]!=v[i-1]) {

sum++;

}

}

cout<

return 0;

}

📝 比较

方案

优点

缺点

单哈希

实现简单,速度快

容易碰撞,安全性较低

双哈希

碰撞概率极低,实战中最常用

稍复杂,计算开销略增

取模策略

保证数值范围,避免溢出

模运算较慢

一般来说,我们使用单哈希就足够了,双哈希的题在竞赛中出现的并不是很多。

🎎 相关推荐

芝麻贷app下载必下款吗?真实测评助你轻松借款
市场营销案例分析:顾客永远是正确的
🎯 365bet娱乐app

市场营销案例分析:顾客永远是正确的

📅 08-26 👀 2342
256g的手机有哪些
🎯 365bet地址

256g的手机有哪些

📅 11-16 👀 1525