专栏文章
卡哈希
算法·理论参与者 28已保存评论 32
文章操作
快速查看文章及其快照的属性,并进行相关操作。
- 当前评论
- 32 条
- 当前快照
- 1 份
- 快照标识符
- @mm77v6bf
- 此快照首次捕获于
- 2026/03/01 11:54 7 天前
- 此快照最后确认于
- 2026/03/01 11:54 7 天前
本文基于 NOI WC 2024 csy 专家进行的讲解。
什么?你还在为卡不掉选手的十模哈希而感到痛苦?
什么?你还在 CF hack 时看着对方的随机五十模数哈希而感到迷茫?
不要担心,现隆重向您推出——LLL 算法卡哈希!
在这之前,我们快速回顾一下几种简单的卡哈希方法。
OI 中常见的字符串哈希函数一般形如下式:
其中 ,且 一般为质数。
多模哈希是选择多组 ,将 构成的有序对作为哈希结果。
单模哈希
相信这个东西的卡法已经为人熟知了,直接随机一堆字符串看看有没有相同即可,利用了生日悖论。
这个做法挂亿会机也能找到双模哈希的碰撞。
自然溢出哈希
,对 的奇偶性分类讨论:
- 为偶数,那么有
所以只需要构造低 位相同的字符串即可。
- 为奇数,我们考虑字符集为 的字符串 ,定义其取反 为 变为 , 变为 后得到的字符串。令 ,那么容易发现 。
多模哈希
众所周知,某些选手在不会写 SA 或者 Z-function 或者 Manacher 等等字符串算法时会选择二分哈希这种做法,对于写正解的选手非常不公平,在介绍如何针对这些选手之前,先对 Lenstra-Lenstra-Lovász Lattice Reduction 算法 进行介绍。
格与基
本文只关注欧几里得空间中由向量加法构成的格,所以我们给出格的定义:
格 是欧几里得空间 的一个离散加法子群,且其中的元素可由 个整系数的向量 基 线性组合生成。
换句话说,基就是 个整数向量 ,格就是所有的 。
Gram-Schmidt 正交化
先定义正交基:
正交基 是一组基 ,满足:
Gram-Schmidt 正交化可以将一组基 变为一组正交基 。
Gram-Schmidt 正交化采用增量法构造,加入第一个向量时,直接令 即可。
加入第 个向量时,我们考虑计算 对 的投影,从 中减去投影,具体可以参考这个图:

然后我们就得到了这个公式:
LLL 算法主体
LLL 算法有一个参数 ,需要满足 ,一般取 。
定义一组基 的 LLL reduced 条件:
的一个等价表述为:
其中 为 的 Gram-Schmidt 正交化基,还是增量法,对于 ,我们什么也不用做就可以满足两个条件。
现在假设我们已经把 变换成了 LLL reduced 的,我们加入了新向量 ,我们先求出这些向量的 Gram-Schmidt 正交化基 ,为了满足 ,我们枚举 从 到 ,执行:
( 表示 四舍五入。)检查一下 是否成立,如果成立,我们直接令 ,否则,我们交换 与 ,并令 ,与更前面的基进行比较。
这个算法的正确性证明和终止性证明和时间复杂度证明都比较复杂,所以就不写了(就是不会)。
LLL reduced 基有如下性质:
设 是 维格 的一组 LLL reduced 基,那么有:
其中 为格 中的最短向量长度,求解该向量被称为 SVP 问题(Shortest Vector Problem)。
证明可以归纳法,所以我们直接把 拉满(比如 )就能产生一组不错的基,这个算法的时间复杂度是 ,其中 是 ,当然实际运用中严重跑不满。
这样 LLL 算法就找到了 SVP 问题的一个近似解。
CVP
格中的困难问题之一——Closest Vector Problem,寻找格 中与向量 最近的向量(虽然这和怎么卡哈希没有关系,还是讲一下吧)。
Babai's nearest plane algorithm:首先将 跑一遍 LLL 得到 LLL reduced 基 ,置 ,枚举 从 到 ,令 ,最后的 就是一个比较好的解。
近似程度有多好呢?我们有这个算法求出的解至多是正确答案的 倍。
对 LLL 卡常
下面是 sagemath 中 LLL 算法的实现,假设我们需要对由 生成的格进行 LLL 算法。
首先计算 Gram-Schmidt 正交化基 和 矩阵,计算 ,令 表示我们准备好添加 进入 LLL reduced 基了。
首先定义
reduce(k, l) 过程:如果 ,令 ,然后对于所有 ,将 减去 。最后令 即可。
然后定义
exchange(k) 过程:交换 ,令 更新 。然后对于所有 ,交换 ,对于所有 ,令 ,并置 。
等会补充证明。
然后令 ,重复执行直到 :
reduce(k, k - 1)- 如果 ,倒序枚举 ,
reduce(k, l),将 加 。 - 否则,
exchange(k)并令 。
卡掉哈希!
考虑将构造哈希碰撞转换为求解一个格的 SVP 问题。
首先看这个题:
PYTHONh0 = 45740974929179720441799381904411404011270459520712533273451053262137196814399
# 2**168 + 355
g = 374144419156711147060143317175368453031918731002211L
def shitty_hash(msg):
h = h0
msg = map(ord, msg)
for i in msg:
h = (h + i) * g
# This line is just to screw you up :))
h = h & 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
return h - 0xe6168647f636
也就是 的 ,你需要构造两个串满足其哈希相同。
不妨设我们构造的两个串为 ,那么有:
容易将问题转化为构造长 的整数序列 和整数 ,满足 ,考虑构造这样的格:
其中 是一个大常数,比如 ,然后跑 LLL 算法,求一个 SVP。
注意到我们的 非常大,所以求出的 SVP 如果在 这个维度不是 ,那么一定非常长,所以这个维度一定是 ,就满足了条件。
由于我不想下 sagemath,于是就有了下面的代码:
PYTHONfrom sympy import *
mod = 2 ** 256
h0 = 45740974929179720441799381904411404011270459520712533273451053262137196814399
g = 374144419156711147060143317175368453031918731002211
K = 2 ** 200
N = 50
y = []
for i in range(N):
y.append([])
for j in range(N + 1):
if i == j:
y[i].append(Rational(1, 1))
elif j == N:
y[i].append(Rational(K * pow(g, N - i, mod), 1))
else:
y[i].append(Rational(0, 1))
def dot(x, y):
n = len(x)
ans = 0
for i in range(n):
ans += x[i] * y[i]
return ans
delta = Rational(99, 100)
n, m = N, N + 1
y_star = [[y[0][i] for i in range(m)]]
mu = [[Rational(0, 1) for i in range(m)] for j in range(n)]
for i in range(1, n):
print('Gram-Schmidt: i =', i)
y_star.append([y[i][j] for j in range(m)])
for j in range(i):
mu[i][j] = dot(y_star[j], y_star[i]) / dot(y_star[j], y_star[j])
for k in range(m):
y_star[i][k] -= y_star[j][k] * mu[i][j]
gamma = [dot(y_star[i], y_star[i]) for i in range(n)]
def round(x):
return (x + Rational(1, 2)).floor()
def reduce(k, l):
if abs(mu[k][l]) > Rational(1, 2):
for i in range(m):
y[k][i] -= round(mu[k][l]) * y[l][i]
for j in range(l):
mu[k][j] -= round(mu[k][l]) * mu[l][j]
mu[k][l] -= round(mu[k][l])
def exchange(k):
y[k - 1], y[k] = y[k], y[k - 1]
nu = mu[k][k - 1]
delta = gamma[k] + nu * nu * gamma[k - 1]
mu[k][k - 1] = nu * gamma[k - 1] / delta
gamma[k] *= gamma[k - 1] / delta
gamma[k - 1] = delta
for j in range(k - 1):
mu[k - 1][j], mu[k][j] = mu[k][j], mu[k - 1][j]
for i in range(k + 1, n):
xi = mu[i][k]
mu[i][k] = mu[i][k - 1] - nu * mu[i][k]
mu[i][k - 1] = mu[k][k - 1] * mu[i][k] + xi
k = 1
maxk = 1
while k < n:
if maxk < k:
maxk = k
print('LLL: new k =', k)
reduce(k, k - 1)
if gamma[k] >= (delta - mu[k][k - 1] ** 2) * gamma[k - 1]:
for l in range(k - 2, -1, -1):
reduce(k, l)
k = k + 1
else:
exchange(k)
if k > 1:
k = k - 1
def shitty_hash(msg):
h = h0
msg = map(ord, msg)
for i in msg:
h = (h + i) * g
# This line is just to screw you up :))
h = h & 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
return h - 0xe6168647f636
s1 = 'a' * n
s2 = ''
for i in range(n):
s2 += chr(ord(s1[i]) + y[0][i])
print(y[0])
print(max(y[0]), min(y[0]), max(y[0]) - min(y[0]))
print(s1)
print(s2)
print(shitty_hash(s1))
print(shitty_hash(s2))
if shitty_hash(s1) == shitty_hash(s2):
print('ok')
else:
print('wa')
在一万两千多次迭代之后,LLL 算法给出的如下的 :
并产生了两个串:
CPPaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
pSrogamvi~g]Xk_U[aUnEEI^g\QprSd_QHdLFXqf`a^]]Ngiaa
并其 hash 值都是 。
Tree Attack
有的时候,选手会对每个字符给定一个随机权值的方法来防止自己的哈希被 LLL 卡掉,这是非常不好的。
Tree Attack 尝试找到 满足:
中的 将被限制于 中,定义一个簇 是 的子集,定义簇的和为:
我们维护系数 上的簇 ,。
我们可以将两个簇 合并为一个满足 的簇 :不妨设 ,我们将 中的 全部取反然后令 即可。
初始令 ( 在下面会进行分析),我们令 ,并创建 个簇 ,其中仅包含 。
在每一轮中,我们对所有簇 按照 排序,然后合并相邻的簇,如果我们得到了一个 的簇 ,将不在 中的所有 全部置为 ,并返回这些 。
的值应该取多少?我们可以合理地假设 在 中均匀分布,那么在 轮之后,最大值应当除以大约 ,所以 轮以后,最大值大约为 ,所以 是一个合理的选择。
Multi Tree Attack
在 Tree Attack 的基础上,将每个簇能得到的最小的 个值存储,显然可以在 时间内合并两个簇。
实战演练
叉掉 CF Submission 251754156,来自 在 CF Round 934 (Div. 1) 对 B 题的提交。
题意简述:
定义字符串 是 -好的当且仅当存在一个非回文的长 的 的子串。
对于字符串 定义 为所有满足 是 -好的正整数 之和。
给定长 字符串 与 次询问 ,求 的值。
题解:
如果串里全部都相同,那么答案为 。如果所有奇数位置的字符和偶数位置的字符都相同,那么 只能是偶数。
注意特判一下整个串是否是回文串即可。
重点来了,我们需要快速判断一个子串 是回文的。
这位选手采用了四模哈希与固定底数 ,模数列表为 。
用上面的模板可以找到如下碰撞:
CPPaaaaaaadaaaacacacacacbaadbcaccbaacecaaacadebabaaaa
aabbbaaabfbaaeabacadaaacaaafaaadbaaaaaeafaaabaaaaa
所以构造这组数据:
CPP1
100 1
aaaaaaadaaaacacacacacbaadbcaccbaacecaaacadebabaaaaaaaaabaaafaeaaaaabdaaafaaacaaadacabaeaabfbaaabbbaa
1 100
即可叉掉此提交!
参考代码:
PYTHONimport fractions as f
K = 2 ** 50
N = 50
cnt = 4
mod_ = [995662561, 995662609, 995662621, 998244353]
base_ = [31, 31, 31, 31]
y = []
for i in range(N):
y.append([])
for j in range(N):
if i == j:
y[i].append(f.Fraction(1, 1))
else:
y[i].append(f.Fraction(0, 1))
for j in range(cnt):
y[i].append(f.Fraction(pow(base_[j], N - i, mod_[j]) * K, 1))
def dot(x, y):
n = len(x)
ans = 0
for i in range(n):
ans += x[i] * y[i]
return ans
delta = f.Fraction(99, 100)
n, m = N, N + cnt
y_star = [[y[0][i] for i in range(m)]]
mu = [[f.Fraction(0, 1) for i in range(m)] for j in range(n)]
for i in range(1, n):
print('Gram-Schmidt: i =', i)
y_star.append([y[i][j] for j in range(m)])
for j in range(i):
mu[i][j] = dot(y_star[j], y_star[i]) / dot(y_star[j], y_star[j])
for k in range(m):
y_star[i][k] -= y_star[j][k] * mu[i][j]
gamma = [dot(y_star[i], y_star[i]) for i in range(n)]
def round(x):
return (x + f.Fraction(1, 2)).__floor__()
def reduce(k, l):
if abs(mu[k][l]) > f.Fraction(1, 2):
for i in range(m):
y[k][i] -= round(mu[k][l]) * y[l][i]
for j in range(l):
mu[k][j] -= round(mu[k][l]) * mu[l][j]
mu[k][l] -= round(mu[k][l])
def exchange(k):
y[k - 1], y[k] = y[k], y[k - 1]
nu = mu[k][k - 1]
delta = gamma[k] + nu * nu * gamma[k - 1]
mu[k][k - 1] = nu * gamma[k - 1] / delta
gamma[k] *= gamma[k - 1] / delta
gamma[k - 1] = delta
for j in range(k - 1):
mu[k - 1][j], mu[k][j] = mu[k][j], mu[k - 1][j]
for i in range(k + 1, n):
xi = mu[i][k]
mu[i][k] = mu[i][k - 1] - nu * mu[i][k]
mu[i][k - 1] = mu[k][k - 1] * mu[i][k] + xi
k = 1
maxk = 1
while k < n:
if maxk < k:
maxk = k
print('LLL: new k =', k)
reduce(k, k - 1)
if gamma[k] >= (delta - mu[k][k - 1] ** 2) * gamma[k - 1]:
for l in range(k - 2, -1, -1):
reduce(k, l)
k = k + 1
else:
exchange(k)
if k > 1:
k = k - 1
print(y[0])
s1 = s2 = ''
for i in range(n):
now = int(y[0][i])
if now <= 0:
s1 += 'a'
s2 += chr(ord('a') - now)
else:
s1 += chr(ord('a') + now)
s2 += 'a'
print(s1)
print(s2)
def calc_hash(s, b, m):
ans = 0
for i in range(len(s)):
ans = (ans + (ord(s[i]) - ord('a')) * pow(b, N - i, m)) % m
return ans
for i in range(cnt):
print(calc_hash(s1, base_[i], mod_[i]), calc_hash(s2, base_[i], mod_[i]))
构造非回文的“回文”串
叉掉 CF Submission 251715199,来自 在 CF Round 934 (Div. 1) 对 B 题的提交。
这要求我们构造一个串 满足 , 表示 逆序得到的字符串,不妨设 长度为 ,那么:
我们设 ,则对于 ,有 ,所以我们只需要 。
那么我们有:
参考代码:
PYTHONimport fractions as f
K = 2 ** 200
N = 50
cnt = 1
mod_ = [2305843009213693951]
base_ = [1145141919]
y = []
for i in range(N):
y.append([])
for j in range(N):
if i == j:
y[i].append(f.Fraction(1, 1))
else:
y[i].append(f.Fraction(0, 1))
for j in range(cnt):
#y[i].append(f.Fraction(pow(base_[j], N - i, mod_[j]) * K, 1))
y[i].append(f.Fraction((pow(base_[j], 2 * N - i - 1, mod_[j]) - pow(base_[j], i, mod_[j])) % mod_[j] * K, 1))
def dot(x, y):
n = len(x)
ans = 0
for i in range(n):
ans += x[i] * y[i]
return ans
delta = f.Fraction(99, 100)
n, m = N, N + cnt
y_star = [[y[0][i] for i in range(m)]]
mu = [[f.Fraction(0, 1) for i in range(m)] for j in range(n)]
for i in range(1, n):
print('Gram-Schmidt: i =', i)
y_star.append([y[i][j] for j in range(m)])
for j in range(i):
mu[i][j] = dot(y_star[j], y_star[i]) / dot(y_star[j], y_star[j])
for k in range(m):
y_star[i][k] -= y_star[j][k] * mu[i][j]
gamma = [dot(y_star[i], y_star[i]) for i in range(n)]
def round(x):
return (x + f.Fraction(1, 2)).__floor__()
def reduce(k, l):
if abs(mu[k][l]) > f.Fraction(1, 2):
for i in range(m):
y[k][i] -= round(mu[k][l]) * y[l][i]
for j in range(l):
mu[k][j] -= round(mu[k][l]) * mu[l][j]
mu[k][l] -= round(mu[k][l])
def exchange(k):
y[k - 1], y[k] = y[k], y[k - 1]
nu = mu[k][k - 1]
delta = gamma[k] + nu * nu * gamma[k - 1]
mu[k][k - 1] = nu * gamma[k - 1] / delta
gamma[k] *= gamma[k - 1] / delta
gamma[k - 1] = delta
for j in range(k - 1):
mu[k - 1][j], mu[k][j] = mu[k][j], mu[k - 1][j]
for i in range(k + 1, n):
xi = mu[i][k]
mu[i][k] = mu[i][k - 1] - nu * mu[i][k]
mu[i][k - 1] = mu[k][k - 1] * mu[i][k] + xi
k = 1
maxk = 1
while k < n:
if maxk < k:
maxk = k
print('LLL: new k =', k)
reduce(k, k - 1)
if gamma[k] >= (delta - mu[k][k - 1] ** 2) * gamma[k - 1]:
for l in range(k - 2, -1, -1):
reduce(k, l)
k = k + 1
else:
exchange(k)
if k > 1:
k = k - 1
print(y[0])
s1 = s2 = ''
for i in range(n):
now = int(y[0][i])
if now <= 0:
s1 += 'a'
s2 += chr(ord('a') - now)
else:
s1 += chr(ord('a') + now)
s2 += 'a'
print(s1)
print(s2)
print(s1 + s2[::-1])
def calc_hash(s, b, m):
ans = 0
for i in range(len(s)):
ans = (ans + (ord(s[i]) - ord('a')) * pow(b, i, m)) % m
return ans
s = s1 + s2[::-1]
#for i in range(cnt):
# print(calc_hash(s1, base_[i], mod_[i]), calc_hash(s2, base_[i], mod_[i]))
for i in range(cnt):
print(calc_hash(s, base_[i], mod_[i]), calc_hash(s[::-1], base_[i], mod_[i]))
相关推荐
评论
共 32 条评论,欢迎与作者交流。
正在加载评论...